├── src ├── public │ ├── version.json │ ├── icon.ico │ ├── icon.png │ ├── images │ │ ├── pixel.png │ │ ├── dropbox.ico │ │ ├── inky-icon.png │ │ ├── renpy-128.png │ │ └── twine-favicon-152.png │ ├── droid-sans-mono.ttf │ ├── templates │ │ └── node.html │ ├── plugins │ │ ├── ace-diff │ │ │ ├── ace-diff-dark.min.css │ │ │ └── ace-diff.min.css │ │ ├── jsoneditor │ │ │ ├── size-overrides.css │ │ │ └── jsoneditor.js │ │ ├── inkjs │ │ │ └── ink-renderer.js │ │ └── runner.js │ ├── theme-ink.js │ ├── theme-yarn.js │ ├── mode-yarn.js │ ├── themes │ │ ├── classic.css │ │ └── blueprint.css │ └── libs │ │ └── uFuzzy.iife.min.js ├── scss │ ├── font │ │ ├── context-menu-icons.eot │ │ ├── context-menu-icons.ttf │ │ ├── context-menu-icons.woff │ │ └── context-menu-icons.woff2 │ ├── jquery.contextMenu.css │ └── normalize.css ├── sw-src.js └── js │ ├── index.js │ ├── libs │ ├── knockout.ace.js │ └── spellcheck_ace.js │ └── classes │ ├── richTextFormatter.js │ ├── richTextFormatterBbcode.js │ ├── richTextFormatterHtml.js │ ├── settings.js │ ├── input.js │ └── node.js ├── Yarn.png ├── doc ├── nodes.png ├── games │ ├── ash.png │ ├── ffn.jpg │ ├── kab.jpeg │ ├── nitw.jpg │ └── lostC.png ├── yarnWebApp.png ├── inkPlaytest.gif ├── yarnMobile.jpeg └── inkErrorReporting.gif ├── electron ├── build │ ├── icon.ico │ └── icon.png ├── yarn-index.html ├── yarn-style.css ├── yarn-main.js ├── package.json └── main.js ├── .prettierrc.json ├── .idea ├── .gitignore ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── misc.xml ├── vcs.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── runConfigurations │ ├── build.xml │ ├── deploy.xml │ └── start.xml └── YarnClassic.iml ├── .github └── workflows │ ├── deploy-to-gh-pages.yml │ ├── release.yml │ └── build.yml ├── testFiles ├── simple-xml-example.xml ├── bbcodeTags.yarn ├── htmlTags.yarn ├── yarnExample.json ├── shortcuts.json ├── Sally.yarn.txt ├── commandsandfunctions.json ├── links.json ├── exportAsRenpyExample.json ├── conditions.json └── assignment.json ├── .eslintrc.js ├── LICENSE.md ├── scripts └── copy-version.js ├── .gitignore ├── CONTRIBUTING.md ├── package.json ├── webpack.config.js └── README.md /src/public/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.4.381" 3 | } -------------------------------------------------------------------------------- /Yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/Yarn.png -------------------------------------------------------------------------------- /doc/nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/nodes.png -------------------------------------------------------------------------------- /doc/games/ash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/games/ash.png -------------------------------------------------------------------------------- /doc/games/ffn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/games/ffn.jpg -------------------------------------------------------------------------------- /doc/games/kab.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/games/kab.jpeg -------------------------------------------------------------------------------- /doc/games/nitw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/games/nitw.jpg -------------------------------------------------------------------------------- /doc/yarnWebApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/yarnWebApp.png -------------------------------------------------------------------------------- /doc/games/lostC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/games/lostC.png -------------------------------------------------------------------------------- /doc/inkPlaytest.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/inkPlaytest.gif -------------------------------------------------------------------------------- /doc/yarnMobile.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/yarnMobile.jpeg -------------------------------------------------------------------------------- /src/public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/public/icon.ico -------------------------------------------------------------------------------- /src/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/public/icon.png -------------------------------------------------------------------------------- /electron/build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/electron/build/icon.ico -------------------------------------------------------------------------------- /electron/build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/electron/build/icon.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /doc/inkErrorReporting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/doc/inkErrorReporting.gif -------------------------------------------------------------------------------- /src/public/images/pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/public/images/pixel.png -------------------------------------------------------------------------------- /src/public/droid-sans-mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/public/droid-sans-mono.ttf -------------------------------------------------------------------------------- /src/public/images/dropbox.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/public/images/dropbox.ico -------------------------------------------------------------------------------- /src/public/images/inky-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/public/images/inky-icon.png -------------------------------------------------------------------------------- /src/public/images/renpy-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/public/images/renpy-128.png -------------------------------------------------------------------------------- /src/scss/font/context-menu-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/scss/font/context-menu-icons.eot -------------------------------------------------------------------------------- /src/scss/font/context-menu-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/scss/font/context-menu-icons.ttf -------------------------------------------------------------------------------- /src/scss/font/context-menu-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/scss/font/context-menu-icons.woff -------------------------------------------------------------------------------- /src/scss/font/context-menu-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/scss/font/context-menu-icons.woff2 -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /src/public/images/twine-favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/HEAD/src/public/images/twine-favicon-152.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /electron/yarn-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Yarn Dialogue Tree Editor 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/sw-src.js: -------------------------------------------------------------------------------- 1 | import { precacheAndRoute } from 'workbox-precaching'; 2 | import { NetworkFirst } from 'workbox-strategies'; 3 | import { registerRoute } from 'workbox-routing'; 4 | 5 | console.log("Yarn's service worker is caching files"); 6 | registerRoute(/\.\/YarnClassic\//, new NetworkFirst()); 7 | precacheAndRoute(self.__WB_MANIFEST); 8 | -------------------------------------------------------------------------------- /electron/yarn-style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Additional styles for the embedded Yarn editor 3 | */ 4 | html, 5 | body { 6 | font-family: Helvetica; 7 | margin: 0; 8 | overflow-y: hidden; 9 | background-color: white; 10 | } 11 | 12 | #yarn-frame { 13 | width: 100%; 14 | height: 100%; 15 | border: none; 16 | overflow-y: hidden; 17 | } 18 | -------------------------------------------------------------------------------- /.idea/runConfigurations/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /testFiles/simple-xml-example.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Start 4 | 5 | [[Node2]] 6 | 7 | 0 8 | 9 | 10 | Node2 11 | 12 | [[Node3]] 13 | 14 | 0 15 | 16 | 17 | Node3 18 | 19 | Empty Text 20 | 21 | 0 22 | 23 | 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es6': true 5 | }, 6 | 'extends': 'eslint:recommended', 7 | 'globals': { 8 | 'Atomics': 'readonly', 9 | 'SharedArrayBuffer': 'readonly' 10 | }, 11 | 'parserOptions': { 12 | 'ecmaVersion': 2018, 13 | 'sourceType': 'module' 14 | }, 15 | "plugins": [ 16 | "jquery" 17 | ], 18 | "extends": [ 19 | "plugin:jquery/slim", 20 | "prettier" 21 | ], 22 | 'rules': { 23 | 'indent': [2, 2], 24 | 'linebreak-style': [ 25 | 'error', 26 | 'unix' 27 | ], 28 | 'quotes': [ 29 | 'error', 30 | 'single' 31 | ], 32 | 'semi': [ 33 | 'error', 34 | 'always' 35 | ] 36 | } 37 | }; -------------------------------------------------------------------------------- /testFiles/bbcodeTags.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | tags: 3 | colorID: 0 4 | position: 150,211 5 | --- 6 | [b]Bold[/b] text 7 | [u]Underlined[/u] text 8 | Text in [i]italics[/i] 9 | Text in [color=#ff0000]red color[/color] and [color=#0000ff]blue color[/color] 10 | [color=#00ff00][b]Green and bold[/b][/color] text 11 | [b][i]bold and italics[/i][/b] 12 | [b][u]bold and underlined[/u][/b] 13 | [i][u]italics and underlined[/u][/i] 14 | [i][u][color=#e900ff]italics, underlined and pink[/color][/u][/i] 15 | [img]image[/img] 16 | <> 17 | [[This link brings you to a new node:|link]] 18 | === 19 | title: link 20 | tags: 21 | colorID: 0 22 | position: 482,212 23 | --- 24 | Empty Text 25 | === 26 | -------------------------------------------------------------------------------- /testFiles/htmlTags.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | tags: 3 | colorID: 0 4 | position: 108,232 5 | --- 6 | Bold text 7 | Underlined text 8 | Text in italics 9 | Text in red color and blue color 10 | Green and bold text 11 | bold and italics 12 | bold and underlined 13 | italics and underlined 14 | italics, underlined and pink 15 | image 16 | <> 17 | [[This link brings you to a new node:|link]] 18 | === 19 | title: link 20 | tags: 21 | colorID: 0 22 | position: 526,227 23 | --- 24 | Empty Text 25 | === 26 | -------------------------------------------------------------------------------- /testFiles/yarnExample.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Start", 4 | "tags": "err erg", 5 | "body": "A: Hey, I'm a character in a script!\n<>\nB: And I am too! You are talking to me!\nB: What would you prefer to do next?\n\n[[Leave|Leave]]\n[[Learn more|LearnMore]]", 6 | "position": { 7 | "x": 283, 8 | "y": 207 9 | }, 10 | "colorID": 0 11 | }, 12 | { 13 | "title": "Leave", 14 | "tags": "ad ber", 15 | "body": "A: Oh, goodbye!\nB: You'll be back soon!", 16 | "position": { 17 | "x": 190, 18 | "y": 539 19 | }, 20 | "colorID": 0 21 | }, 22 | { 23 | "title": "LearnMore", 24 | "tags": "rawText", 25 | "body": "A: HAHAHA\nBlah blah more..", 26 | "position": { 27 | "x": 534, 28 | "y": 534 29 | }, 30 | "colorID": 0 31 | } 32 | ] -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /testFiles/shortcuts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "NonNested", 4 | "tags": "Tag", 5 | "body": "This is a test line\n-> Option 1\n\tThis is the first option\n-> Option 2\n\tThis is the second option\nThis is after both options", 6 | "position": { 7 | "x": 449, 8 | "y": 252 9 | }, 10 | "colorID": 0 11 | }, 12 | { 13 | "title": "Nested", 14 | "tags": "Tag", 15 | "body": "text\n-> shortcut1\n\tText1\n\t-> nestedshortcut1\n\t\tNestedText1\n\t-> nestedshortcut2\n\t\tNestedText2\n-> shortcut2\n\tText2\nmore text", 16 | "position": { 17 | "x": 449, 18 | "y": 252 19 | }, 20 | "colorID": 0 21 | }, 22 | { 23 | "title": "Conditional", 24 | "tags": "Tag", 25 | "body": "This is a test line\n-> Option 1\n\tThis is the first option\n-> Option 2 <>\n\tThis is the second option\n-> Option 3\n\tThis is the third option\nThis is after both options", 26 | "position": { 27 | "x": 449, 28 | "y": 252 29 | }, 30 | "colorID": 0 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /src/public/plugins/ace-diff/ace-diff-dark.min.css: -------------------------------------------------------------------------------- 1 | .acediff__wrap{display:flex;flex-direction:row;position:absolute;bottom:0;top:0;left:0;height:100%;width:100%;overflow:auto}.acediff__gutter{flex:0 0 60px;border-left:1px solid #000;border-right:1px solid #000;overflow:hidden}.acediff__gutter,.acediff__gutter svg{background-color:#272727}.acediff__left,.acediff__right{height:100%;flex:1}.acediff__diffLine{background-color:#004d7a;border-top:1px solid #003554;border-bottom:1px solid #003554;position:absolute;z-index:4}.acediff__diffLine.targetOnly{height:0!important;border-top:1px solid #003554;border-bottom:0;position:absolute}.acediff__connector{fill:#004d7a;stroke:#003554}.acediff__copy--left,.acediff__copy--right{position:relative}.acediff__copy--left div,.acediff__copy--right div{color:#fff;text-shadow:1px 1px rgba(0,0,0,.7);position:absolute;margin:2px 3px;cursor:pointer}.acediff__copy--right div:hover{color:#61a2e7}.acediff__copy--left{float:right}.acediff__copy--left div{right:0}.acediff__copy--left div:hover{color:#f7b742} 2 | /*# sourceMappingURL=/ace-diff-dark.min.css.map */ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Infinite Ammo Inc. and Yarn Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /testFiles/Sally.yarn.txt: -------------------------------------------------------------------------------- 1 | title: Sally 2 | tags: 3 | colorID: 0 4 | position: 524,111 5 | --- 6 | <> 7 | Player: Hey, Sally. 8 | Sally: Oh! Hi. 9 | Sally: You snuck up on me. 10 | Sally: Don't do that. 11 | <> 12 | Player: Hey. 13 | Sally: Hi. 14 | <> 15 | 16 | <> 17 | [[Anything exciting happen on your watch?|Sally.Watch]] 18 | <> 19 | 20 | <> 21 | [[Sorry about the console.|Sally.Sorry]] 22 | <> 23 | [[See you later.|Sally.Exit]] 24 | === 25 | title: Sally.Watch 26 | tags: 27 | colorID: 0 28 | position: 516,538 29 | --- 30 | Sally: Not really. 31 | Sally: Same old nebula, doing the same old thing. 32 | Sally: Oh, Ship wanted to see you. Go say hi to it. 33 | <> 34 | <> 35 | Player: Already done! 36 | Sally: Go say hi again. 37 | <> 38 | === 39 | title: Sally.Exit 40 | tags: 41 | colorID: 6 42 | position: 145,384 43 | --- 44 | Sally: Bye. 45 | === 46 | title: Sally.Sorry 47 | tags: 48 | colorID: 0 49 | position: 873,444 50 | --- 51 | Sally: Yeah. Don't do it again. 52 | === 53 | -------------------------------------------------------------------------------- /src/public/plugins/ace-diff/ace-diff.min.css: -------------------------------------------------------------------------------- 1 | /*! Ace-diff | github.com/ace-diff/ace-diff */ 2 | .acediff__wrap{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;position:absolute;bottom:0;top:0;left:0;height:100%;width:100%;overflow:auto}.acediff__gutter{-webkit-box-flex:0;-ms-flex:0 0 60px;flex:0 0 60px;border-left:1px solid #bcbcbc;border-right:1px solid #bcbcbc;overflow:hidden}.acediff__gutter,.acediff__gutter svg{background-color:#efefef}.acediff__left,.acediff__right{height:100%;-webkit-box-flex:1;-ms-flex:1;flex:1}.acediff__diffLine{background-color:#d8f2ff;border-top:1px solid #a2d7f2;border-bottom:1px solid #a2d7f2;position:absolute;z-index:4}.acediff__diffLine.targetOnly{height:0!important;border-top:1px solid #a2d7f2;border-bottom:0;position:absolute}.acediff__connector{fill:#d8f2ff;stroke:#a2d7f2}.acediff__copy--left,.acediff__copy--right{position:relative}.acediff__copy--left div,.acediff__copy--right div{color:#000;text-shadow:1px 1px hsla(0,0%,100%,.7);position:absolute;margin:2px 3px;cursor:pointer}.acediff__copy--right div:hover{color:#004ea0}.acediff__copy--left{float:right}.acediff__copy--left div{right:0}.acediff__copy--left div:hover{color:#c98100} -------------------------------------------------------------------------------- /electron/yarn-main.js: -------------------------------------------------------------------------------- 1 | // This file serves as e middle layer to communicate between the web app and electron's native features 2 | const electron = require('electron'); 3 | const ipcRenderer = electron.ipcRenderer; 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const yarnWindow = electron.remote.getCurrentWindow(); 7 | let yarn = null; 8 | 9 | const editorFrameEl = document.getElementById('yarn-frame'); 10 | window.addEventListener('yarnReady', (e) => { 11 | 12 | //give the yarn webb app the fs module, so we can ctrl+s in electron without pop ups 13 | yarn = e; 14 | yarn.app.fs = fs; 15 | yarn.app.electron = electron; 16 | yarn.app.path = path; 17 | ipcRenderer.send('yarn-ready'); 18 | console.log('connected to electron', yarn, yarn.electron); 19 | }); 20 | editorFrameEl.src = 'app/index.html'; 21 | 22 | // Called on load yarn data. 23 | window.addEventListener('yarnLoadedData', (e) => { 24 | yarnWindow.setTitle(yarn.app.data.editingPath()); 25 | yarn.app.refreshWindowTitle(); 26 | }); 27 | 28 | // Called on save yarn data. 29 | window.addEventListener('yarnSavedData', (e) => { 30 | // console.log("RENAME TITLE") 31 | yarnWindow.setTitle(yarn.app.data.editingPath()); 32 | yarn.app.refreshWindowTitle(); 33 | }); 34 | -------------------------------------------------------------------------------- /testFiles/commandsandfunctions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "BasicCommands", 4 | "tags": "Tag", 5 | "body": "<>text in between commands<> <> <>", 6 | "position": { 7 | "x": 449, 8 | "y": 252 9 | }, 10 | "colorID": 0 11 | }, 12 | { 13 | "title": "StopCommand", 14 | "tags": "Tag", 15 | "body": "First line\n<>\nThis shouldn't show", 16 | "position": { 17 | "x": 449, 18 | "y": 252 19 | }, 20 | "colorID": 0 21 | }, 22 | { 23 | "title": "FunctionConditional", 24 | "tags": "Tag", 25 | "body": "First line\n<>This shouldn't show<>This should show<>After both", 26 | "position": { 27 | "x": 449, 28 | "y": 252 29 | }, 30 | "colorID": 0 31 | }, 32 | { 33 | "title": "VisitedFunction", 34 | "tags": "Tag", 35 | "body": "<>you have visited VisitedFunctionStart!<><>You have not visited SomeOtherNode!<>", 36 | "position": { 37 | "x": 449, 38 | "y": 252 39 | }, 40 | "colorID": 0 41 | }, 42 | { 43 | "title": "VisitedFunctionStart", 44 | "tags": "Tag", 45 | "body": "Hello[[VisitedFunction]]", 46 | "position": { 47 | "x": 449, 48 | "y": 252 49 | }, 50 | "colorID": 0 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /testFiles/links.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "OneNode", 4 | "tags": "Tag", 5 | "body": "This is a test line", 6 | "position": { 7 | "x": -200, 8 | "y": -65 9 | }, 10 | "colorID": 0 11 | }, 12 | { 13 | "title": "ThreeNodes", 14 | "tags": "", 15 | "body": "This is a test line\nThis is another test line[[Option1]]\n[[Option2]]", 16 | "position": { 17 | "x": 128, 18 | "y": 274 19 | }, 20 | "colorID": 0 21 | }, 22 | { 23 | "title": "Option1", 24 | "tags": "", 25 | "body": "This is Option1's test line", 26 | "position": { 27 | "x": 585, 28 | "y": -156 29 | }, 30 | "colorID": 0 31 | }, 32 | { 33 | "title": "Option2", 34 | "tags": "", 35 | "body": "This is Option2's test line", 36 | "position": { 37 | "x": 607, 38 | "y": 688 39 | }, 40 | "colorID": 0 41 | }, 42 | { 43 | "title": "NamedLink", 44 | "tags": "", 45 | "body": "This is a test line\nThis is another test line\n[[First choice|Option1]]\n[[Second choice|Option2]]", 46 | "position": { 47 | "x": 1089, 48 | "y": 116 49 | }, 50 | "colorID": 0 51 | }, 52 | { 53 | "title": "OneLinkPassthrough", 54 | "tags": "", 55 | "body": "First test line\n[[Option1]]", 56 | "position": { 57 | "x": 148, 58 | "y": -139 59 | }, 60 | "colorID": 0 61 | }, 62 | { 63 | "title": "LinkAfterShortcuts", 64 | "tags": "", 65 | "body": "First test line\n-> Shortcut 1\n\tThis is the first shortcut\n-> Shortcut 2\n\tThis is the second shortcut\n[[First link|Option1]][[Second link|Option2]]", 66 | "position": { 67 | "x": 584, 68 | "y": 265 69 | }, 70 | "colorID": 0 71 | } 72 | ] -------------------------------------------------------------------------------- /src/public/plugins/jsoneditor/size-overrides.css: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width: 600px) { 2 | .swal2-popup{ 3 | padding: 2px !important; 4 | } 5 | .swal2-actions{ 6 | margin: 1px !important; 7 | } 8 | } 9 | 10 | .swal2-title{ 11 | width:100%; 12 | } 13 | .swal2-content{ 14 | max-height: 90vh; 15 | overflow: auto; 16 | } 17 | @media only screen and (max-width: 600px) { 18 | /*.form-control > input {*/ 19 | /* max-width: 100px;*/ 20 | /*}*/ 21 | } 22 | 23 | .table { 24 | width: 100%; 25 | } 26 | .table-editable { 27 | width: 100%; 28 | position: relative; 29 | } 30 | 31 | .table-header { 32 | position: sticky; 33 | top: 0; 34 | background-color: #007; 35 | color: aqua; 36 | } 37 | 38 | .table-footer { 39 | position: sticky; 40 | bottom: 0; 41 | background-color: #007; 42 | color: aqua; 43 | display: flex; 44 | justify-content: space-between; 45 | } 46 | .table-editable .glyphicon { 47 | font-size: 20px; 48 | } 49 | 50 | .table-remove { 51 | color: #700; 52 | cursor: pointer; 53 | text-align: center; 54 | } 55 | 56 | .cell { 57 | width: 180px; 58 | max-width: 180px; 59 | text-align: left; 60 | padding-left: 3px; 61 | padding-right: 3px; 62 | } 63 | .table-remove:hover { 64 | color: #f00; 65 | } 66 | 67 | .table-up:hover, 68 | .table-down:hover { 69 | color: #00f; 70 | } 71 | 72 | .table-add { 73 | width: 120px; 74 | color: #070; 75 | cursor: pointer; 76 | text-align: center; 77 | } 78 | 79 | .table-add:hover { 80 | color: #0b0; 81 | } 82 | 83 | .hide { 84 | display: none; 85 | } -------------------------------------------------------------------------------- /src/public/theme-ink.js: -------------------------------------------------------------------------------- 1 | define('ace/theme/ink', [ 2 | 'require', 3 | 'exports', 4 | 'module', 5 | 'ace/lib/dom', 6 | ], function(require, exports, module) { 7 | exports.isDark = false; 8 | exports.cssClass = 'ace-ink'; 9 | exports.cssText = ` 10 | /* Flow related stuff in blue */ 11 | .ace_editor span.ace_flow.ace_declaration, 12 | .ace_editor span.ace_divert, 13 | .ace_editor span.ace_choice.ace_bullets, 14 | .ace_editor span.ace_choice.ace_label, 15 | .ace_editor span.ace_choice.ace_weaveBracket, 16 | .ace_editor span.ace_gather.ace_bullets, 17 | .ace_editor span.ace_gather.ace_label, 18 | .ace_editor span.ace_glue, 19 | .ace_editor span.ace_include, 20 | .ace_editor span.ace_external { 21 | color: blue; 22 | } 23 | 24 | /* Comments */ 25 | .ace-tm .ace_comment { 26 | color: #84756c; 27 | } 28 | 29 | /* Logic */ 30 | .ace_editor span.ace_var-decl, 31 | .ace_editor span.ace_list-decl, 32 | .ace_editor span.ace_logic:not(.ace_innerContent) { 33 | color: green; 34 | } 35 | 36 | /* Tags */ 37 | .ace_editor span.ace_tag { 38 | color: #AAA; 39 | } 40 | 41 | #main.hideTags .ace_editor span.ace_tag { 42 | color: white; 43 | } 44 | 45 | /* Custom added */ 46 | .ace_editor .ace_marker-layer .ace_selection { 47 | background: rgb(181, 213, 255); 48 | } 49 | .ace_editor.ace_multiselect .ace_selection.ace_start { 50 | box-shadow: 0 0 3px 0px white; 51 | border-radius: 2px; 52 | } 53 | .ace_editor .ace_marker-layer .ace_selection { 54 | background: rgb(181, 213, 255); 55 | } 56 | .ace_editor.ace_multiselect .ace_selection.ace_start { 57 | box-shadow: 0 0 3px 0px white; 58 | border-radius: 2px; 59 | } 60 | `; 61 | 62 | var dom = require('../lib/dom'); 63 | dom.importCssString(exports.cssText, exports.cssClass); 64 | }); 65 | -------------------------------------------------------------------------------- /scripts/copy-version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path'); 3 | 4 | const PATH_MAIN_PACKAGE_JSON = path.resolve(__dirname, '../package.json'); 5 | const PATH_ELECTRON_PACKAGE_JSON = path.resolve(__dirname, '../electron/package.json'); 6 | const PATH_PUBLIC_VERSION_FILE = path.resolve(__dirname, '../src/public/version.json'); 7 | const SCRIPT_FILENAME = path.basename(__filename); 8 | 9 | let currentVersion = require(PATH_MAIN_PACKAGE_JSON).version; 10 | let mustUpdate = false; 11 | 12 | const writeVersionToFile = (version, filename) => { 13 | let data = fs.readFileSync(filename, 'utf8'); 14 | data = data.replace(/\"version\":\s*\"(.+?)\"/g, `"version": "${version}"`); 15 | fs.writeFileSync(filename, data, 'utf8'); 16 | } 17 | 18 | const updateVersion = () => { 19 | currentVersion = getNextVersion(currentVersion); 20 | writeVersionToFile (currentVersion, PATH_MAIN_PACKAGE_JSON); 21 | }; 22 | 23 | const copyVersion = () => { 24 | writeVersionToFile (currentVersion, PATH_ELECTRON_PACKAGE_JSON); 25 | writeVersionToFile (currentVersion, PATH_PUBLIC_VERSION_FILE); 26 | }; 27 | 28 | const getNextVersion = (version) => { 29 | const parts = version.split('.'); 30 | ++parts[2]; 31 | return parts.join('.'); 32 | }; 33 | 34 | const printHelp = () => { 35 | console.log(`\nusage: node ${SCRIPT_FILENAME} [--update]`) 36 | }; 37 | 38 | const checkArgs = (args) => { 39 | if (args.length < 2 || args.length > 3) 40 | return false; 41 | 42 | if ( args.length === 3 && args[2] !== '--update' ) 43 | return false; 44 | 45 | mustUpdate = ( args.length === 3 && args[2] === '--update' ); 46 | 47 | return true 48 | }; 49 | 50 | const main = (args) => { 51 | if (!checkArgs(args)) { 52 | printHelp() 53 | return; 54 | } 55 | 56 | if ( mustUpdate ) 57 | updateVersion(); 58 | 59 | copyVersion(); 60 | }; 61 | 62 | main (process.argv); 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Created by https://www.gitignore.io/api/node,osx,windows 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Vscode settings 23 | .vscode/ 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directory 35 | node_modules 36 | 37 | # Dist 38 | dist 39 | electron/dist 40 | electron/app 41 | yarn-editor 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional REPL history 47 | .node_repl_history 48 | 49 | 50 | ### OSX ### 51 | .DS_Store 52 | .AppleDouble 53 | .LSOverride 54 | 55 | # Icon must end with two \r 56 | Icon 57 | 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | 70 | # Directories potentially created on remote AFP share 71 | .AppleDB 72 | .AppleDesktop 73 | Network Trash Folder 74 | Temporary Items 75 | .apdisk 76 | 77 | 78 | ### Windows ### 79 | # Windows image file caches 80 | Thumbs.db 81 | ehthumbs.db 82 | 83 | # Folder config file 84 | Desktop.ini 85 | 86 | # Recycle Bin used on file shares 87 | $RECYCLE.BIN/ 88 | 89 | # Windows Installer files 90 | *.cab 91 | *.msi 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | # Autogenerated for dev 99 | src/manifest.json 100 | src/sw.js 101 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import '../scss/jquery.contextMenu.css'; 2 | import '../scss/normalize.css'; 3 | import '../scss/spectrum.css'; 4 | import '../scss/style.css'; 5 | 6 | import '../public/web-components.js' 7 | 8 | import { Utils } from './classes/utils'; 9 | 10 | import ko from 'knockout'; 11 | window.ko = ko; 12 | 13 | window.$ = window.jQuery = require('jquery'); 14 | import 'jquery-contextmenu'; 15 | import 'jquery-mousewheel'; 16 | import 'jquery-resizable-dom'; 17 | 18 | import ace from 'ace-builds/src-noconflict/ace'; 19 | window.ace = ace; 20 | ace.config.set('basePath', Utils.getPublicPath()); //needed to import yarn mode 21 | window.define = ace.define; 22 | 23 | import 'ace-builds/src-min-noconflict/ext-language_tools'; 24 | import 'ace-builds/src-min-noconflict/ext-searchbox'; 25 | import './libs/knockout.ace.js'; 26 | import 'jquery.transit'; 27 | 28 | import 'spectrum-colorpicker'; 29 | import 'lightweight-emoji-picker/dist/picker.js'; 30 | 31 | // Keep these imports, they are used elsewhere in the app 32 | import Swal from 'sweetalert2'; 33 | window.Swal = Swal; 34 | 35 | import { App } from './classes/app.js'; 36 | import { version } from '../public/version.json'; 37 | 38 | // Register PWA service worker 39 | if ('serviceWorker' in navigator) { 40 | window.addEventListener('load', () => { 41 | navigator.serviceWorker 42 | .register('sw.js') 43 | .then(registration => { 44 | // registration.pushManager.subscribe({userVisibleOnly: true}); 45 | console.log('SW registered: ', registration); 46 | }) 47 | .catch(registrationError => { 48 | console.log('SW registration failed: ', registrationError); 49 | }); 50 | }); 51 | } 52 | 53 | window.app = new App('Yarn', version); 54 | window.app.run(); 55 | 56 | // Register plugins from plugin folder 57 | import { Plugins } from '../public/plugins'; 58 | const appPlugins = new Plugins(window.app); 59 | window.app.plugins = appPlugins; 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Yarn 2 | 3 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 4 | 5 | ## How to send in your contributions 6 | 7 | There are many ways you can send your contributions to Yarn. You can either **report a bug**, or you can make the changes yourself and **submit a pull request**! 8 | 9 | ### Reporting bugs and opening issues 10 | 11 | Please [report bugs](https://github.com/blurymind/YarnClassic/issues) and open issues generously. Don't be afraid that your idea is silly, or you're reporting a duplicate. We're happy to hear from you. Seriously. 12 | 13 | > ***Please Note:*** Yarn is written by volunteers. If you encounter a problem while using it, we'll do our best to help you, but the authors cannot offer any support. 14 | 15 | ### Submitting a pull request 16 | 17 | * [Fork](https://github.com/blurymind/YarnClassic/fork) and clone the repository 18 | * Create a new branch: git checkout -b my-branch-name 19 | * Make your changes 20 | * Push to your fork and [submit a pull request](https://github.com/blurymind/YarnClassic/compare) 21 | * Pat your self on the back and wait for your pull request to be reviewed. 22 | 23 | If you're unfamiliar with how pull requests work, [GitHub's documentation on them](https://help.github.com/articles/using-pull-requests/) is very good. 24 | 25 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 26 | 27 | * Update the documentation as necessary, as well as making code changes. 28 | * Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 29 | * [Write a good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 30 | 31 | ### Code and other contributions 32 | 33 | Contributions to Yarn (via pull request or otherwise) must be licensed under the MIT license. 34 | -------------------------------------------------------------------------------- /electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yarn-editor", 3 | "main": "main.js", 4 | "version": "0.4.381", 5 | "license": "MIT", 6 | "author": " @infinite_ammo, @seiyria, @beeglebug ,Todor Imreorov", 7 | "description": "Dialogue editor created for \"Night in the Woods\" (and other projects) by @NoelFB and @infinite_ammo with contributions from @seiyria and @beeglebug. It is heavily inspired by and based on the amazing Twine software: http://twinery.org/. This version has been ported over to Electron and extended with further functionality by Todor Imreorov", 8 | "homepage": "https://github.com/blurymind/YarnClassic#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/blurymind/YarnClassic.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/blurymind/YarnClassic/issues" 15 | }, 16 | "scripts": { 17 | "build-web-app-dev": "cd .. && npm run build-dev && npm run copy-web-app", 18 | "build-web-app": "cd .. && npm run build && npm run copy-web-app", 19 | "start": "npm run build-web-app-dev && electron --no-sandbox .", 20 | "build-linux": "npm run build-web-app && electron-builder --linux --publish never", 21 | "build-windows": "npm run build-web-app && electron-builder --windows", 22 | "build-mac": "npm run build-web-app && electron-builder --macos" 23 | }, 24 | "dependencies": { 25 | "electron-is": "^3.0.0" 26 | }, 27 | "devDependencies": { 28 | "copyfiles": "^2.1.1", 29 | "electron": "^6.0.0", 30 | "electron-builder": "^21.2.0" 31 | }, 32 | "lint-staged": { 33 | "package.json": [ 34 | "prettier-package-json --write", 35 | "git add" 36 | ], 37 | "*.js": [ 38 | "eslint --fix", 39 | "git add" 40 | ] 41 | }, 42 | "build": { 43 | "productName": "Yarn Classic", 44 | "fileAssociations": { 45 | "ext": "yarn", 46 | "name": "Yarn File", 47 | "role": "editor" 48 | }, 49 | "win": { 50 | "icon": "build/icon.ico" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /testFiles/exportAsRenpyExample.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "lastSavedUnix": "2022-11-02T18:02:33.452Z", 4 | "language": "en-GB", 5 | "documentType": "yarn", 6 | "markupLanguage": "bbcode", 7 | "filetypeVersion": "2", 8 | "pluginStorage": { 9 | "Runner": {} 10 | } 11 | }, 12 | "nodes": [ 13 | { 14 | "title": "livingRoom", 15 | "tags": "", 16 | "body": "// standard character text\nmc.c happy \"there's nothing interesting on tv today\"\n\n// standard narrator text\nHe couldn't find anything interesting on tv\n\n// wrap commands like this\n<>\n\n// To create a menu with two options, you need each option to be on a new line, followed by the question\nPlay some video games instead?\n[[yes|playsVideoGames]]\n[[no|gets up]]", 17 | "position": { 18 | "x": -274, 19 | "y": -1469 20 | }, 21 | "colorID": 0 22 | }, 23 | { 24 | "title": "playsVideoGames", 25 | "tags": "", 26 | "body": "mc \"I will play some video games\"\nSome time passes by", 27 | "position": { 28 | "x": -279, 29 | "y": -1688 30 | }, 31 | "colorID": 0 32 | }, 33 | { 34 | "title": "gets up", 35 | "tags": "", 36 | "body": "mc \"I am borded, let's do something else\"", 37 | "position": { 38 | "x": -42, 39 | "y": -1461 40 | }, 41 | "colorID": 0 42 | }, 43 | { 44 | "title": "MapUI()", 45 | "tags": "screen renpy", 46 | "body": "// if you put a screen tag in the node, it will be interpreted as a screen instead of a label\nimagebutton:\n xpos 618\n ypos 570\n idle \"BG/house1_idle.png\"\n hover \"BG/house1_hover.png\"\n action Call(\"call_livingRoomUI\")", 47 | "position": { 48 | "x": -510, 49 | "y": -1468 50 | }, 51 | "colorID": 0 52 | }, 53 | { 54 | "title": "call_livingRoomUI", 55 | "tags": "renpy", 56 | "body": "// if you put a renpy tag in the node, the text in the body will be interpreted as is, without trying to be converted to rpy\n$ current_location = \"livingRoom\"\n scene bg house_room\n call screen LivingRoomUI\n return", 57 | "position": { 58 | "x": -514, 59 | "y": -1687 60 | }, 61 | "colorID": 0 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /testFiles/conditions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Start", 4 | "tags": "Tag", 5 | "body": "What are you?\n-> A troll\n <>\n-> A nice person\n <>\n[[Objective]]", 6 | "position": { 7 | "x": 449, 8 | "y": 252 9 | }, 10 | "colorID": 0 11 | }, 12 | { 13 | "title": "Objective", 14 | "tags": "Tag", 15 | "body": "<= 3>>\nBye…\n<>\nIs your objective clear?\n[[Yes|Objective.Yes]]\n[[No|Objective.No]]\n<>\n[[Maybe|Objective.Maybe]]\n<>\n<>\n", 16 | "position": { 17 | "x": 449, 18 | "y": 252 19 | }, 20 | "colorID": 0 21 | }, 22 | { 23 | "title": "Objective.No", 24 | "tags": "Tag", 25 | "body": "Blah blah blah blah\n[[Objective]]", 26 | "position": { 27 | "x": 449, 28 | "y": 252 29 | }, 30 | "colorID": 0 31 | }, 32 | { 33 | "title": "Objective.Yes", 34 | "tags": "Tag", 35 | "body": "Good let's start the mission.", 36 | "position": { 37 | "x": 449, 38 | "y": 252 39 | }, 40 | "colorID": 0 41 | }, 42 | { 43 | "title": "Objective.Maybe", 44 | "tags": "Tag", 45 | "body": "Are you trolling me?\n[[Objective]]", 46 | "position": { 47 | "x": 449, 48 | "y": 252 49 | }, 50 | "colorID": 0 51 | }, 52 | 53 | { 54 | "title": "BasicIf", 55 | "tags": "Tag", 56 | "body": "<>\nText before\n<>Inside if<>Text after", 57 | "position": { 58 | "x": 449, 59 | "y": 252 60 | }, 61 | "colorID": 0 62 | }, 63 | { 64 | "title": "BasicIfElse", 65 | "tags": "Tag", 66 | "body": "<>\nText before\n<>Inside if<>Inside else<>Text after", 67 | "position": { 68 | "x": 449, 69 | "y": 252 70 | }, 71 | "colorID": 0 72 | }, 73 | { 74 | "title": "BasicIfElseIf", 75 | "tags": "Tag", 76 | "body": "<>\nText before\n<>Inside if<>Inside elseif<>Text after", 77 | "position": { 78 | "x": 449, 79 | "y": 252 80 | }, 81 | "colorID": 0 82 | }, 83 | { 84 | "title": "BasicIfElseIfElse", 85 | "tags": "Tag", 86 | "body": "<>\nText before\n<>Inside if<>Inside elseif<>Inside else<>Text after", 87 | "position": { 88 | "x": 449, 89 | "y": 252 90 | }, 91 | "colorID": 0 92 | } 93 | ] 94 | -------------------------------------------------------------------------------- /testFiles/assignment.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Numeric", 4 | "tags": "Tag", 5 | "body": "Test Line\n<>\nTest Line After", 6 | "position": { 7 | "x": 449, 8 | "y": 252 9 | }, 10 | "colorID": 0 11 | },{ 12 | "title": "NumericExpression", 13 | "tags": "Tag", 14 | "body": "Test Line\n<>\nTest Line After", 15 | "position": { 16 | "x": 449, 17 | "y": 252 18 | }, 19 | "colorID": 0 20 | }, 21 | { 22 | "title": "String", 23 | "tags": "Tag", 24 | "body": "Test Line\n<>\nTest Line After", 25 | "position": { 26 | "x": 449, 27 | "y": 252 28 | }, 29 | "colorID": 0 30 | }, 31 | { 32 | "title": "StringExpression", 33 | "tags": "Tag", 34 | "body": "Test Line\n<>\nTest Line After", 35 | "position": { 36 | "x": 449, 37 | "y": 252 38 | }, 39 | "colorID": 0 40 | }, 41 | { 42 | "title": "Boolean", 43 | "tags": "Tag", 44 | "body": "Test Line\n<>\nTest Line After", 45 | "position": { 46 | "x": 449, 47 | "y": 252 48 | }, 49 | "colorID": 0 50 | }, 51 | { 52 | "title": "BooleanExpression", 53 | "tags": "Tag", 54 | "body": "Test Line\n<>\nTest Line After", 55 | "position": { 56 | "x": 449, 57 | "y": 252 58 | }, 59 | "colorID": 0 60 | }, 61 | { 62 | "title": "Null", 63 | "tags": "Tag", 64 | "body": "Test Line\n<>\nTest Line After", 65 | "position": { 66 | "x": 449, 67 | "y": 252 68 | }, 69 | "colorID": 0 70 | }, 71 | { 72 | "title": "NullExpression", 73 | "tags": "Tag", 74 | "body": "Test Line\n<>\nTest Line After", 75 | "position": { 76 | "x": 449, 77 | "y": 252 78 | }, 79 | "colorID": 0 80 | }, 81 | { 82 | "title": "Variable", 83 | "tags": "Tag", 84 | "body": "Test Line\n<><>\nTest Line After", 85 | "position": { 86 | "x": 449, 87 | "y": 252 88 | }, 89 | "colorID": 0 90 | }, 91 | { 92 | "title": "VariableExpression", 93 | "tags": "Tag", 94 | "body": "Test Line\n<><>\nTest Line After", 95 | "position": { 96 | "x": 449, 97 | "y": 252 98 | }, 99 | "colorID": 0 100 | } 101 | ] 102 | -------------------------------------------------------------------------------- /src/public/theme-yarn.js: -------------------------------------------------------------------------------- 1 | define("ace/theme/yarn",["require","exports","module","ace/lib/dom"], function(require, exports, module) { 2 | 3 | exports.isDark = false; 4 | exports.cssClass = "ace-yarn"; 5 | exports.cssText = "\ 6 | .ace-yarn .ace_gutter {\ 7 | background: #e8e8e8;\ 8 | color: #AAA;\ 9 | }\ 10 | .ace-yarn {\ 11 | background: #fff;\ 12 | color: #000;\ 13 | }\ 14 | .ace-yarn .ace_comment {\ 15 | color: #00c171;\ 16 | font-style: italic;\ 17 | }\ 18 | .ace-yarn .ace_variable.ace_language {\ 19 | color: #0086B3;\ 20 | }\ 21 | .ace-yarn .ace_paren {\ 22 | font-weight: bold;\ 23 | }\ 24 | .ace-yarn .ace_string.ace_llink {\ 25 | color: #000;\ 26 | }\ 27 | .ace-yarn .ace_string.ace_rlink {\ 28 | color: #3ecfe9;\ 29 | }\ 30 | .ace-yarn .ace_string.ace_comm {\ 31 | color: #e93ecf;\ 32 | }\ 33 | .ace-yarn .ace_paren.ace_lcomm, .ace-yarn .ace_paren.ace_rcomm {\ 34 | color: #e00ec0;\ 35 | }\ 36 | .ace-yarn .ace_paren.ace_llink, .ace-yarn .ace_paren.ace_rlink {\ 37 | color: #0ec0e0;\ 38 | }\ 39 | .ace-yarn .ace_variable.ace_instance {\ 40 | color: teal;\ 41 | }\ 42 | .ace-yarn .ace_constant.ace_language {\ 43 | font-weight: bold;\ 44 | }\ 45 | .ace-yarn .ace_cursor {\ 46 | color: black;\ 47 | }\ 48 | .ace-yarn .ace_marker-layer .ace_active-line {\ 49 | background: rgb(255, 255, 204);\ 50 | }\ 51 | .ace-yarn .ace_marker-layer .ace_selection {\ 52 | background: rgb(181, 213, 255);\ 53 | }\ 54 | .ace-yarn.ace_multiselect .ace_selection.ace_start {\ 55 | box-shadow: 0 0 3px 0px white;\ 56 | border-radius: 2px;\ 57 | }\ 58 | .ace-yarn.ace_nobold .ace_line > span {\ 59 | font-weight: normal !important;\ 60 | }\ 61 | .ace-yarn .ace_marker-layer .ace_step {\ 62 | background: rgb(252, 255, 0);\ 63 | }\ 64 | .ace-yarn .ace_marker-layer .ace_stack {\ 65 | background: rgb(164, 229, 101);\ 66 | }\ 67 | .ace-yarn .ace_marker-layer .ace_bracket {\ 68 | margin: -1px 0 0 -1px;\ 69 | border: 1px solid rgb(192, 192, 192);\ 70 | }\ 71 | .ace-yarn .ace_gutter-active-line {\ 72 | background-color : rgba(0, 0, 0, 0.07);\ 73 | }\ 74 | .ace-yarn .ace_marker-layer .ace_selected-word {\ 75 | background: rgb(250, 250, 255);\ 76 | border: 1px solid rgb(200, 200, 250);\ 77 | }\ 78 | .ace-yarn .ace_print-margin {\ 79 | width: 1px;\ 80 | background: #e8e8e8;\ 81 | }\ 82 | .ace-yarn .ace_indent-guide {\ 83 | background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\ 84 | }"; 85 | 86 | var dom = require("../lib/dom"); 87 | dom.importCssString(exports.cssText, exports.cssClass); 88 | }); -------------------------------------------------------------------------------- /src/public/mode-yarn.js: -------------------------------------------------------------------------------- 1 | define('ace/mode/yarn', [ 2 | 'require', 3 | 'exports', 4 | 'module', 5 | 'ace/lib/oop', 6 | 'ace/mode/text', 7 | 'ace/mode/text_highlight_rules', 8 | 'ace/mode/behaviour', 9 | ], function(require, exports, module) { 10 | 'use strict'; 11 | 12 | var oop = require('../lib/oop'); 13 | var TextMode = require('./text').Mode; 14 | var TextHighlightRules = require('./text_highlight_rules').TextHighlightRules; 15 | var CstyleBehaviour = require('./behaviour/cstyle').CstyleBehaviour; 16 | 17 | var YarnHighlightRules = function() { 18 | this.$rules = { 19 | start: [ 20 | { 21 | token: 'comment', 22 | regex: '^\\/\\/.*$', 23 | }, 24 | { 25 | token: 'paren.lcomm', 26 | regex: '<<', 27 | next: 'comm', 28 | }, 29 | { 30 | token: 'paren.llink', 31 | regex: '\\[\\[', 32 | next: 'link', 33 | }, 34 | ], 35 | link: [ 36 | { 37 | token: 'string.rlink', 38 | regex: '\\|\\w*[a-zA-Z0-9 ]+', 39 | }, 40 | { 41 | token: 'string.llink', 42 | regex: '[a-zA-Z0-9 ]+', 43 | }, 44 | { 45 | token: 'paren.rlink', 46 | regex: '\\]\\]', 47 | next: 'start', 48 | }, 49 | ], 50 | comm: [ 51 | { 52 | token: 'string.comm', 53 | regex: "[A-Za-z0-9 _.,!:''/$ ]+", 54 | }, 55 | { 56 | token: 'paren.rcomm', 57 | regex: '>>', 58 | next: 'start', 59 | }, 60 | ], 61 | }; 62 | }; 63 | 64 | var Mode = function() { 65 | this.HighlightRules = YarnHighlightRules; 66 | this.$behaviour = new CstyleBehaviour(); 67 | }; 68 | 69 | oop.inherits(YarnHighlightRules, TextHighlightRules); 70 | oop.inherits(Mode, TextMode); 71 | 72 | (function() { 73 | this.type = 'text'; 74 | this.getNextLineIndent = function(state, line, tab) { 75 | return this.$getIndent(line); //automatic indentation of new line. Return '' to disable it 76 | }; 77 | this.$id = 'ace/mode/yarn'; 78 | }.call(Mode.prototype)); 79 | 80 | exports.Mode = Mode; 81 | 82 | /// set context menu 83 | $.contextMenu(app.utils.getEditorContextMenu(/\|/g)); 84 | 85 | /// Enable autocompletion via word guessing 86 | app.editor.setOptions({ 87 | enableBasicAutocompletion: app.settings.autocompleteSuggestionsEnabled(), 88 | enableLiveAutocompletion: app.settings.autocompleteSuggestionsEnabled(), 89 | behavioursEnabled: app.settings.autoCloseBrackets(), 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | include: 13 | - os: windows-latest 14 | build-task: build-windows 15 | - os: ubuntu-latest 16 | build-task: build-linux 17 | - os: macos-latest 18 | build-task: build-mac 19 | 20 | name: Publish for ${{ matrix.os }} 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - name: Get tag and version 25 | run: | 26 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 27 | echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV 28 | 29 | - name: Set environment vars for Windows 30 | if: matrix.os == 'windows-latest' 31 | shell: pwsh 32 | run: | 33 | $env:RELEASE_TAG=$($env:GITHUB_REF.Substring(10)) 34 | $env:RELEASE_VERSION=$($env:GITHUB_REF.Substring(11)) 35 | 36 | echo "RELEASE_TAG=$($env:RELEASE_TAG)" >> $env:GITHUB_ENV 37 | echo "RELEASE_VERSION=$($env:RELEASE_VERSION)" >> $env:GITHUB_ENV 38 | echo "ARTIFACT_FILE=./electron/dist/Yarn Editor Setup $($env:RELEASE_VERSION).exe" >> $env:GITHUB_ENV 39 | echo "ARTIFACT_NAME=Yarn.Editor.Setup.$($env:RELEASE_VERSION).exe" >> $env:GITHUB_ENV 40 | 41 | - name: Set environment vars for Linux 42 | if: matrix.os == 'ubuntu-latest' 43 | run: | 44 | echo "ARTIFACT_FILE=./electron/dist/yarn-editor_${{ env.RELEASE_VERSION }}_amd64.snap" >> $GITHUB_ENV 45 | echo "ARTIFACT_NAME=Yarn.Editor.Setup.${{ env.RELEASE_VERSION }}.snap" >> $GITHUB_ENV 46 | 47 | - name: Set environment vars for Mac 48 | if: matrix.os == 'macos-latest' 49 | run: | 50 | echo "ARTIFACT_FILE=./electron/dist/Yarn Editor-${{ env.RELEASE_VERSION }}.dmg" >> $GITHUB_ENV 51 | echo "ARTIFACT_NAME=Yarn.Editor.Setup.${{ env.RELEASE_VERSION }}.dmg" >> $GITHUB_ENV 52 | 53 | - name: Checkout repository 54 | uses: actions/checkout@v2 55 | 56 | - name: Use Node.js 16.x 57 | uses: actions/setup-node@v1 58 | with: 59 | node-version: 16.x 60 | 61 | - run: npm ci 62 | 63 | - name: npm install and build 64 | run: cd electron && yarn install && yarn run ${{ matrix.build-task }} 65 | env: 66 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 67 | 68 | - name: Upload binaries to release 69 | uses: svenstaro/upload-release-action@v1-release 70 | with: 71 | repo_token: ${{ secrets.GITHUB_TOKEN }} 72 | file: ${{ env.ARTIFACT_FILE }} 73 | asset_name: ${{ env.ARTIFACT_NAME }} 74 | tag: ${{ env.RELEASE_TAG }} 75 | -------------------------------------------------------------------------------- /src/js/libs/knockout.ace.js: -------------------------------------------------------------------------------- 1 | // custom fork of this library due to the original using brance and being unmaintained atm 2 | (function() { 3 | var instances_by_id = {}, // needed for referencing instances during updates. 4 | init_id = 0; // generated id increment storage 5 | 6 | ko.bindingHandlers.ace = { 7 | init: function( 8 | element, 9 | valueAccessor, 10 | allBindingsAccessor, 11 | viewModel, 12 | bindingContext 13 | ) { 14 | var options = allBindingsAccessor().aceOptions || {}; 15 | var value = ko.utils.unwrapObservable(valueAccessor()); 16 | 17 | // Ace attaches to the element by DOM id, so we need to make one for the element if it doesn't have one already. 18 | if (!element.id) { 19 | element.id = 'knockout-ace-' + init_id; 20 | init_id = init_id + 1; 21 | } 22 | 23 | var editor = ace.edit(element.id); 24 | 25 | if (options.theme) editor.setTheme('ace/theme/' + options.theme); 26 | if (options.mode) editor.getSession().setMode('ace/mode/' + options.mode); 27 | 28 | editor.setValue(value); 29 | editor.gotoLine(0); 30 | editor.setShowPrintMargin(false); 31 | editor.getSession().setUseWrapMode(true); 32 | 33 | editor.getSession().on('change', function(delta) { 34 | if (ko.isWriteableObservable(valueAccessor())) { 35 | valueAccessor()(editor.getValue()); 36 | } 37 | }); 38 | 39 | instances_by_id[element.id] = editor; 40 | 41 | // destroy the editor instance when the element is removed 42 | ko.utils.domNodeDisposal.addDisposeCallback(element, function() { 43 | try { 44 | editor.destroy(); 45 | } catch (e) {} 46 | delete instances_by_id[element.id]; 47 | }); 48 | }, 49 | update: function( 50 | element, 51 | valueAccessor, 52 | allBindingsAccessor, 53 | viewModel, 54 | bindingContext 55 | ) { 56 | var value = ko.utils.unwrapObservable(valueAccessor()); 57 | var id = element.id; 58 | 59 | //handle programmatic updates to the observable 60 | // also makes sure it doesn't update it if it's the same. 61 | // otherwise, it will reload the instance, causing the cursor to jump. 62 | if (id !== undefined && id !== '' && instances_by_id.hasOwnProperty(id)) { 63 | var editor = instances_by_id[id]; 64 | var content = editor.getValue(); 65 | if (content !== value) { 66 | editor.setValue(value); 67 | editor.gotoLine(0); 68 | } 69 | } 70 | }, 71 | }; 72 | 73 | ko.aceEditors = { 74 | resizeAll: function() { 75 | for (var id in instances_by_id) { 76 | if (!instances_by_id.hasOwnProperty(id)) continue; 77 | var editor = instances_by_id[id]; 78 | editor.resize(); 79 | } 80 | }, 81 | get: function(id) { 82 | return instances_by_id[id]; 83 | }, 84 | }; 85 | })(); 86 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | update-version: 9 | name: Update version 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Use Node.js 16.x 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 16.x 20 | 21 | - run: npm run update-version 22 | 23 | - name: Commit updated files 24 | run: | 25 | git config --local user.email "action@github.com" 26 | git config --local user.name "GitHub Action" 27 | git commit -m "Update version" -a 28 | 29 | - name: Push updated files 30 | uses: ad-m/github-push-action@master 31 | with: 32 | github_token: ${{ secrets.GH_TOKEN }} 33 | 34 | build: 35 | needs: update-version 36 | 37 | strategy: 38 | matrix: 39 | include: 40 | - os: windows-latest 41 | build-task: build-windows 42 | - os: ubuntu-latest 43 | build-task: build-linux 44 | - os: macos-latest 45 | build-task: build-mac 46 | 47 | name: Build for ${{ matrix.os }} 48 | runs-on: ${{ matrix.os }} 49 | 50 | steps: 51 | - name: Checkout repository 52 | uses: actions/checkout@v2 53 | 54 | - name: Use Node.js 16.x 55 | uses: actions/setup-node@v1 56 | with: 57 | node-version: 16.x 58 | 59 | - run: npm ci 60 | 61 | - name: Read package.json 62 | uses: tyankatsu0105/read-package-version-actions@v1 63 | id: package-version 64 | 65 | - name: Set environment vars for Windows 66 | if: matrix.os == 'windows-latest' 67 | shell: pwsh 68 | run: | 69 | echo "ARTIFACT_FILE=./electron/dist/Yarn Classic Setup ${{ steps.package-version.outputs.version }}.exe" >> $env:GITHUB_ENV 70 | echo "ARTIFACT_NAME=Yarn.Classic.Setup.${{ steps.package-version.outputs.version }}.exe" >> $env:GITHUB_ENV 71 | 72 | - name: Set environment vars for Linux 73 | if: matrix.os == 'ubuntu-latest' 74 | run: | 75 | echo "ARTIFACT_FILE=./electron/dist/yarn-classic_${{ steps.package-version.outputs.version }}_amd64.snap" >> $GITHUB_ENV 76 | echo "ARTIFACT_NAME=Yarn.Classic.Setup.${{ steps.package-version.outputs.version }}.snap" >> $GITHUB_ENV 77 | 78 | - name: Set environment vars for Mac 79 | if: matrix.os == 'macos-latest' 80 | run: | 81 | echo "ARTIFACT_FILE=./electron/dist/Yarn Classic-${{ steps.package-version.outputs.version }}.dmg" >> $GITHUB_ENV 82 | echo "ARTIFACT_NAME=Yarn.Classic.Setup.${{ steps.package-version.outputs.version }}.dmg" >> $GITHUB_ENV 83 | 84 | - name: npm install and build 85 | run: cd electron && yarn install && yarn run ${{ matrix.build-task }} 86 | env: 87 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 88 | 89 | - name: Archive artifacts 90 | uses: actions/upload-artifact@v1 91 | with: 92 | name: ${{ env.ARTIFACT_NAME }} 93 | path: ${{ env.ARTIFACT_FILE }} 94 | env: 95 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 96 | -------------------------------------------------------------------------------- /src/js/classes/richTextFormatter.js: -------------------------------------------------------------------------------- 1 | import { BbcodeRichTextFormatter } from './richTextFormatterBbcode'; 2 | import { HtmlRichTextFormatter } from './richTextFormatterHtml'; 3 | 4 | export const RichTextFormatter = function(app) { 5 | const type = app.settings.markupLanguage(); 6 | 7 | const addExtraPreviewerEmbeds = result => { 8 | const twRegex = /(https?:\/\/twitter.com\/[^\s\<]+\/[^\s\<]+\/[^\s\<]+)/gi; 9 | const instaRegex = /((https:\/\/)?(www.)?instagram.com\/p\/[^\s\<]+)/gi; 10 | const ytRegex = /(?:http(?:s?):\/\/|)(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?‌​[\w\?‌​=]*)?(?:\?t=[0-9]+)?/gi; 11 | const otherUrlPattern = `^(?!${twRegex.source}|${ytRegex.source}|${instaRegex.source})https?:.*$`; 12 | const combinedRegex = new RegExp(otherUrlPattern, 'gm'); 13 | // result = result.replace(combinedRegex, function(id) { 14 | // return ` 15 | //
16 | // 17 | //
18 | // `; 19 | // }); 20 | result = result.replace(combinedRegex, function(id) { 21 | return ` 22 | ${id} 23 | `; 24 | }); 25 | // add tweet embeds :3 26 | // const tweets = []; 27 | // result = result.replace(twRegex, function(id) { 28 | // const extractedtweetId = id.match( 29 | // /https:\/\/twitter.com\/.*\/status\/([0-9]+)/i 30 | // ); 31 | // if (extractedtweetId.length > 1) { 32 | // tweets.push(extractedtweetId[1]); 33 | // return ``; 34 | // } 35 | // }); 36 | // setTimeout(() => { 37 | // const tweetItems = document.querySelectorAll('.tweet'); 38 | // tweets.forEach((tweetPost, index) => { 39 | // twttr.widgets.createTweet(tweetPost, tweetItems[index], { 40 | // align: 'center', 41 | // follow: false, 42 | // }); 43 | // }); 44 | // }, 500); 45 | // create Youtube previews :) 46 | result = result.replace(ytRegex, function(id) { 47 | const extractedId = id.match( 48 | /(?:https\:.*|)(?:www.|)youtu(?:.*\/v\/|.*v\=|\.be\/)([A-Za-z0-9_\-]{11}(?:\?t=[0-9]+)?)/i 49 | ); 50 | if (extractedId.length > 1) { 51 | return ` 52 | 56 | `; 57 | } 58 | }); 59 | // create Instagram previews :) 60 | result = result.replace(instaRegex, function(id) { 61 | const extractedId = id.match( 62 | /((?:https?:\/\/)?(?:www.)?instagram.com\/p\/([^\s\<]+)\/)/i 63 | ); 64 | if (extractedId.length > 2) { 65 | app.log('EXTRACTED', extractedId); 66 | return ` 67 | 68 | `; 69 | } 70 | }); 71 | return result; 72 | }; 73 | return type === 'html' 74 | ? new HtmlRichTextFormatter(app, addExtraPreviewerEmbeds) 75 | : new BbcodeRichTextFormatter(app, addExtraPreviewerEmbeds); 76 | }; 77 | -------------------------------------------------------------------------------- /src/public/plugins/jsoneditor/jsoneditor.js: -------------------------------------------------------------------------------- 1 | export var JSONEditor = function({ id }) { 2 | this.tableAdd = ({ key, value }) => { 3 | var $clone = this.table 4 | .find('tr.hide') 5 | .clone(true) 6 | .removeClass('hide table-line'); 7 | if (key && value) { 8 | console.log('mutate', { key, value, html: $clone.html() }); 9 | $clone.html(` 10 | ${key} 11 | ${value} 12 | 13 | x delete 14 | 15 | `); 16 | $('.table-remove').on('click', function() { 17 | $(this) 18 | .parents('tr') 19 | .detach(); 20 | }); 21 | } 22 | this.table.find('table').append($clone); 23 | }; 24 | this.getValue = () => { 25 | var $rows = this.table.find('tr:not(:hidden)'); 26 | var headers = []; 27 | var data = []; 28 | 29 | // Get the headers (add special header logic here) 30 | $($rows.shift()) 31 | .find('th:not(:empty):not([data-attr-ignore])') 32 | .each(function() { 33 | headers.push( 34 | $(this) 35 | .text() 36 | .toLowerCase() 37 | ); 38 | }); 39 | 40 | // Turn all existing rows into a loopable array 41 | $rows.each(function() { 42 | var $td = $(this).find('td'); 43 | var h = {}; 44 | // Use the headers from earlier to name our hash keys 45 | headers.forEach(function(header, i) { 46 | h[header] = $td.eq(i).text(); // will adapt for inputs if text is empty 47 | }); 48 | 49 | data.push(h); 50 | }); 51 | // Output the result 52 | // var $EXPORT = $('#save'); 53 | // $EXPORT.text(JSON.stringify(data)); 54 | return data; 55 | }; 56 | 57 | this.init = () => { 58 | document.getElementById(id).innerHTML = ` 59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 76 | 77 |
KeyValueRemove
keyval 74 | x delete 75 |
78 | 79 | 84 |
85 |
86 | `; 87 | var $TABLE = $('#table'); 88 | var $BTN = $('#save-btn'); 89 | this.table = $TABLE; 90 | this.rows = $TABLE.find('tr:not(:hidden)'); 91 | var $EXPORT = $('#save-btn'); 92 | $EXPORT.addClass('hide'); 93 | 94 | $('.table-add').on('click', this.tableAdd); 95 | 96 | $('.table-remove').on('click', function() { 97 | console.log({ removeThis: this }); 98 | $(this) 99 | .parents('tr') 100 | .detach(); 101 | }); 102 | 103 | // A few jQuery helpers for exporting only 104 | jQuery.fn.pop = [].pop; 105 | jQuery.fn.shift = [].shift; 106 | $BTN.on('click', this.getValue); 107 | }; 108 | this.init(); 109 | 110 | this.setValue = (newValue = []) => { 111 | newValue.forEach(this.tableAdd); 112 | }; 113 | }; 114 | -------------------------------------------------------------------------------- /electron/main.js: -------------------------------------------------------------------------------- 1 | const electron = require("electron"); 2 | const ipcMain = electron.ipcMain; 3 | const { dialog } = electron; 4 | const isDev = require("electron-is").dev(); 5 | 6 | // Module to control application life. 7 | const app = electron.app; 8 | 9 | // Module to create native browser window. 10 | const BrowserWindow = electron.BrowserWindow; 11 | 12 | // Keep a global reference of the window object, if you don't, the window will 13 | // be closed automatically when the JavaScript object is garbage collected. 14 | let mainWindow; 15 | const yarnVersion = app.getVersion(); 16 | function createWindow() { 17 | // Create the browser window. 18 | mainWindow = new BrowserWindow({ 19 | width: 1200, 20 | height: 800, 21 | minWidth: 800, 22 | minHeight: 600, 23 | maximize: false, 24 | show: false, 25 | autoHideMenuBar: true, 26 | webPreferences: { 27 | nodeIntegration: true 28 | }, 29 | icon: __dirname + '/icon.ico' 30 | }); 31 | mainWindow.setMenu(null); 32 | // and load the index.html of the app. 33 | mainWindow.loadURL(`file://${__dirname}/yarn-index.html`); 34 | 35 | // if (isDev) { 36 | // mainWindow.loadURL(`http://localhost:8080`); 37 | // } 38 | if (isDev) { 39 | mainWindow.webContents.openDevTools(); 40 | } 41 | 42 | mainWindow.on("close", function(event) { 43 | mainWindow.webContents.send("appIsClosing", event); 44 | 45 | event.preventDefault(); 46 | 47 | mainWindow.destroy(); 48 | mainWindow = null; 49 | }); 50 | 51 | mainWindow.webContents.on("dom-ready", () => { 52 | // in case you want to send data to yarn window on init 53 | // if(yarnData){ 54 | // mainWindow.webContents.send('loadYarnDataObject', yarnData) 55 | // }; 56 | // console.log(mainWindow.webContents); 57 | // mainWindow.yarn.app.fs = fs; 58 | mainWindow.webContents.send("initiate", yarnVersion); 59 | mainWindow.show(); 60 | mainWindow.maximize(); 61 | }); 62 | 63 | ipcMain.on("openFile", (event, operation) => { 64 | dialog.showOpenDialog( 65 | { 66 | properties: ["openFile"] 67 | }, 68 | function(files) { 69 | if (files) 70 | mainWindow.webContents.send("selected-file", files[0], operation); 71 | } 72 | ); 73 | }); 74 | 75 | ipcMain.on("saveFileYarn", (event, type, content) => { 76 | dialog.showSaveDialog( 77 | mainWindow, 78 | { filters: [{ name: "story", extensions: [type] }] }, 79 | function(filepath) { 80 | mainWindow.webContents.send("saved-file", filepath, type, content); 81 | } 82 | ); 83 | }); 84 | 85 | ipcMain.on("sendYarnDataToObject", (event, content, startTestNode) => { 86 | // in case you wannt to export yarn object to another embedded app 87 | otherApp.webContents.send("yarnSavedStory", content); 88 | mainWindow.close(); 89 | }); 90 | } 91 | 92 | // This method will be called when Electron has finished 93 | // initialization and is ready to create browser windows. 94 | // Some APIs can only be used after this event occurs. 95 | app.on("ready", createWindow); 96 | 97 | // Quit when all windows are closed. 98 | app.on("window-all-closed", function() { 99 | // On OS X it is common for applications and their menu bar 100 | // to stay active until the user quits explicitly with Cmd + Q 101 | if (process.platform !== "darwin") { 102 | app.quit(); 103 | } 104 | }); 105 | 106 | app.on("activate", function() { 107 | // On OS X it's common to re-create a window in the app when the 108 | // dock icon is clicked and there are no other windows open. 109 | if (mainWindow === null) { 110 | createWindow(); 111 | } 112 | }); 113 | 114 | // In this file you can include the rest of your app's specific main process 115 | // code. You can also put them in separate files and require them here. 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yarn-editor-web", 3 | "description": "Dialogue editor created for \"Night in the Woods\" (and other projects) by @NoelFB and @infinite_ammo with contributions from @seiyria and @beeglebug. It is heavily inspired by and based on the amazing Twine software: http://twinery.org/. This version has been ported over to Electron and extended with further functionality by Todor Imreorov", 4 | "license": "MIT", 5 | "author": "@infinite_ammo, @seiyria, @beeglebug ,Todor Imreorov", 6 | "homepage": "https://github.com/blurymind/YarnClassic#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/blurymind/YarnClassic.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/blurymind/YarnClassic/issues" 13 | }, 14 | "version": "0.4.381", 15 | "scripts": { 16 | "build": "export NODE_OPTIONS=--openssl-legacy-provider && npm run copy-version && webpack -p --progress --mode production --config webpack.config.js", 17 | "build-dev": "npm run copy-version && cross-env NODE_ENV=dev webpack -p --progress --config webpack.config.js", 18 | "build-tiny": "npm run copy-version && NODE_ENV=tiny webpack -p --progress --mode production --config webpack.config.js", 19 | "copy-version": "node ./scripts/copy-version.js", 20 | "copy-web-app": "cd dist && copyfiles *.html ../electron/app && copyfiles **/* ../electron/app && copyfiles **/*/** ../electron/app && copyfiles **/*/**/* ../electron/app", 21 | "predeploy": "npm run build", 22 | "deploy": "gh-pages -d dist", 23 | "dev": "export NODE_OPTIONS=--openssl-legacy-provider && npm run copy-version && cross-env NODE_ENV=dev webpack-dev-server --open --config webpack.config.js", 24 | "format": "npm run lint -- --fix", 25 | "lint": "eslint src/js", 26 | "start": "npm run dev", 27 | "start-pwa": "export NODE_OPTIONS=--openssl-legacy-provider && npm run build && cd dist && copyfiles manifest.json ../src && copyfiles sw.js ../src && npm run dev", 28 | "update-version": "node ./scripts/copy-version.js --update" 29 | }, 30 | "dependencies": { 31 | "@fortawesome/fontawesome-free": "^5.15.1", 32 | "ace-builds": "^1.4.5", 33 | "bbcode": "^0.1.5", 34 | "idb": "^8.0.0", 35 | "jquery": "^3.4.1", 36 | "jquery-contextmenu": "^2.8.0", 37 | "jquery-mousewheel": "^3.1.13", 38 | "jquery-resizable-dom": "^0.35.0", 39 | "jquery.transit": "^0.9.12", 40 | "knockout": "3.3.0", 41 | "lightweight-emoji-picker": "0.0.2", 42 | "nspell": "^2.1.2", 43 | "spectrum-colorpicker": "^1.8.0", 44 | "sweetalert2": "^9.10.13", 45 | "synonyms": "^1.0.1" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.4.5", 49 | "@babel/preset-env": "^7.4.5", 50 | "babel-eslint": "^10.0.2", 51 | "babel-loader": "^8.0.6", 52 | "babel-plugin-add-module-exports": "^1.0.2", 53 | "clean-webpack-plugin": "^3.0.0", 54 | "copy-webpack-plugin": "^5.1.1", 55 | "copyfiles": "^2.3.0", 56 | "cross-env": "^5.2.0", 57 | "css-loader": "^3.0.0", 58 | "css-url-relative-plugin": "^1.0.0", 59 | "electron-builder": "^21.2.0", 60 | "eslint": "^6.0.1", 61 | "eslint-config-prettier": "^6.10.1", 62 | "eslint-plugin-babel": "^5.3.0", 63 | "eslint-plugin-import": "^2.18.0", 64 | "eslint-plugin-jquery": "^1.5.1", 65 | "eslint-plugin-jsx-a11y": "^6.2.1", 66 | "eslint-plugin-prettier": "^3.1.0", 67 | "eslint-plugin-react": "^7.14.2", 68 | "exports-loader": "^0.7.0", 69 | "file-loader": "^4.0.0", 70 | "gh-pages": "^2.0.1", 71 | "glob": "^7.1.4", 72 | "html-webpack-plugin": "^4.0.0-beta.5", 73 | "husky": "^2.7.0", 74 | "image-webpack-loader": "^5.0.0", 75 | "lint-staged": "^8.2.1", 76 | "mini-css-extract-plugin": "^0.7.0", 77 | "optimize-css-assets-webpack-plugin": "^5.0.3", 78 | "preload-webpack-plugin": "^3.0.0-beta.3", 79 | "prettier": "^1.18.2", 80 | "prettier-package-json": "^2.1.0", 81 | "terser-webpack-plugin": "^1.3.0", 82 | "ttf-loader": "^1.0.2", 83 | "url-loader": "^2.0.1", 84 | "webpack": "^4.35.0", 85 | "webpack-cli": "^3.3.5", 86 | "webpack-dev-server": "^3.7.2", 87 | "webpack-pwa-manifest": "^4.0.0", 88 | "workbox-webpack-plugin": "^5.1.3" 89 | }, 90 | "husky": { 91 | "hooks": { 92 | "pre-commit": "lint-staged" 93 | } 94 | }, 95 | "lint-staged": { 96 | "package.json": [ 97 | "prettier-package-json --write", 98 | "git add" 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/public/themes/classic.css: -------------------------------------------------------------------------------- 1 | /*-- General ------------------------------------------------------------------ */ 2 | :root { 3 | --yarnBg: #baf5f2; 4 | --yarnFg: #00dcff; 5 | --text-color: #134347; 6 | } 7 | 8 | #app-bg { 9 | background: -webkit-linear-gradient(45deg, #ddc9b9 0%, #daf0f2 100%); 10 | } 11 | 12 | a { 13 | color: #2f919a; 14 | text-decoration: none; 15 | transition: color 0.25s; 16 | } 17 | 18 | a:hover { 19 | color: #000; 20 | } 21 | .styled-checkbox { 22 | background-color: white; 23 | } 24 | .styled-checkbox>label>svg, 25 | .bbcode-button>svg { 26 | fill: #8a8a8a; 27 | } 28 | 29 | .styled-checkbox:active>label>svg, 30 | .styled-checkbox:hover>label>svg, 31 | .bbcode-button:hover >svg { 32 | fill: #09292b; 33 | } 34 | 35 | /*-- Menu ------------------------------------------------------------------ */ 36 | 37 | .app-menu { 38 | display: flex; 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | width: 100%; 43 | height: 30px; 44 | background-color: transparent; 45 | } 46 | 47 | .app-menu .menu { 48 | display: block; 49 | float: left; 50 | margin-left: 10px; 51 | border-radius: 2px; 52 | box-shadow: 0; 53 | font-size: 0.9em; 54 | color: #666; 55 | cursor: pointer; 56 | background-color: white; 57 | box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4); 58 | margin: 5px; 59 | } 60 | 61 | .title { 62 | box-sizing: border-box; 63 | padding: 5px; 64 | width: fit-content; 65 | min-width: 100px; 66 | line-height: 20px; 67 | font-weight: bold; 68 | float: left; 69 | } 70 | 71 | .menu .dropdown { 72 | color: #2e8074; 73 | background-color: white; 74 | box-shadow: 0 5px 5px rgba(0, 0, 0, 0.4); 75 | } 76 | 77 | .menu .dropdown .item:hover .menu-icon { 78 | color: black; 79 | } 80 | 81 | .menu-icon { 82 | color: #289aa5; 83 | } 84 | 85 | .settings-icon { 86 | color: #289aa5; 87 | } 88 | 89 | /*-- Canvas Buttons ------------------------------------------------------- */ 90 | .app-add-node { 91 | color: grey; 92 | } 93 | 94 | .app-add-node:hover { 95 | color: var(--yarnFg); 96 | } 97 | 98 | .app-button span { 99 | color: grey; 100 | } 101 | 102 | .app-button span:hover { 103 | color: var(--yarnFg); 104 | } 105 | 106 | .app-add-node, .app-button { 107 | opacity: 0.7; 108 | } 109 | 110 | /*-- Search ----------------------------------------------------------------- */ 111 | 112 | .app-search { 113 | position: absolute; 114 | top: 3px; 115 | right: 10px; 116 | } 117 | 118 | .app-search input { 119 | font-family: 'Lucida Console', Monaco, monospace; 120 | margin-left: 1px; 121 | margin-right: 1px; 122 | } 123 | 124 | .app-search .search-field { 125 | border-radius: 15px; 126 | border-width: thin; 127 | border-color: cyan; 128 | margin-left: 0px; 129 | margin-right: 0px; 130 | } 131 | .search-field:focus { 132 | background-color: var(--yarnBg); 133 | } 134 | 135 | .dropdown .item:hover { 136 | background-color: var(--yarnBg); 137 | color: black; 138 | } 139 | 140 | .app-search input[type='checkbox'] { 141 | cursor: pointer; 142 | } 143 | 144 | .app-search .dropdown .item { 145 | display: block; 146 | box-sizing: border-box; 147 | padding: 7px; 148 | } 149 | 150 | #openHelperMenu > span { 151 | color: rgb(121 121 121); 152 | } 153 | /*-- Node ------------------------------------------------------------------ */ 154 | 155 | .node.selected { 156 | box-shadow: 0 0 0 2px #49eff1; 157 | } 158 | 159 | .node:hover { 160 | box-shadow: 0 0 0 2px #49eff1; 161 | } 162 | 163 | .title-style-1 { 164 | background-color: #EBEBEB; 165 | color: black; 166 | } 167 | .title-style-2 { 168 | background-color: #6EA5E0; 169 | } 170 | .title-style-3 { 171 | background-color: #9EDE74; 172 | } 173 | .title-style-4 { 174 | background-color: #FFE374; 175 | } 176 | 177 | .title-style-5 { 178 | background-color: #F7A666; 179 | } 180 | .title-style-6 { 181 | background-color: #C47862; 182 | } 183 | .title-style-7 { 184 | background-color: #97E1E9; 185 | } 186 | .title-style-8 { 187 | background-color: #576574; 188 | color: white; 189 | } 190 | .title-style-9 { 191 | background-color: black; 192 | color: white; 193 | } 194 | 195 | .node.selected { 196 | box-shadow: 0px 0px 0px 2px cyan; 197 | border: none; 198 | } 199 | 200 | .app-info { 201 | color: #b9786d; 202 | } 203 | /*-- ----------------------------Narrow overrides---------------------------- */ 204 | 205 | @media only screen and (max-width: 600px) { 206 | .app-menu { 207 | box-shadow: 0 0; 208 | } 209 | 210 | .app-search { 211 | top: 34px; 212 | left: 0px; 213 | position: absolute; 214 | width: 50%; 215 | padding: 5px 10px 5px 5px; 216 | z-index: 10000; 217 | } 218 | 219 | .app-search input { 220 | padding: unset; 221 | width: 170px; 222 | } 223 | } 224 | 225 | .grid-canvas { 226 | color: grey; 227 | } 228 | 229 | .button, 230 | button { 231 | background: #99ebe4; 232 | } 233 | 234 | .content-data-image { 235 | content: "url(" attr(data-src) ") "; 236 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | const PreloadWebpackPlugin = require('preload-webpack-plugin'); 8 | const CssUrlRelativePlugin = require('css-url-relative-plugin'); 9 | const WebpackPwaManifest = require('webpack-pwa-manifest'); 10 | const WorkboxPlugin = require('workbox-webpack-plugin'); 11 | 12 | const IS_DEV = process.env.NODE_ENV === 'dev'; 13 | 14 | const config = { 15 | mode: IS_DEV ? 'development' : 'production', 16 | devtool: IS_DEV ? 'eval' : 'source-map', 17 | entry: path.resolve(__dirname, 'src', 'js', 'index.js'), 18 | output: { 19 | filename: 'js/[name].[hash].js', 20 | path: path.resolve(__dirname, 'dist'), 21 | }, 22 | node: { 23 | fs: 'empty', 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.js$/, 29 | exclude: /node_modules/, 30 | loader: 'babel-loader', 31 | }, 32 | { 33 | test: /\.(css)$/, 34 | use: [ 35 | { 36 | loader: MiniCssExtractPlugin.loader, 37 | options: { 38 | hmr: IS_DEV, 39 | }, 40 | }, 41 | 'css-loader', 42 | // 'sass-loader', 43 | ], 44 | }, 45 | { 46 | test: /\.(gif|png|jpe?g|svg)$/i, 47 | use: [ 48 | { 49 | loader: 'url-loader', 50 | options: { 51 | limit: 1024, 52 | name: '[name].[ext]', 53 | fallback: 'file-loader', 54 | outputPath: 'public/images', 55 | }, 56 | }, 57 | { 58 | loader: 'image-webpack-loader', 59 | options: { 60 | mozjpeg: { 61 | progressive: true, 62 | quality: 65, 63 | }, 64 | pngquant: { 65 | quality: '65-90', 66 | speed: 4, 67 | }, 68 | gifsicle: { 69 | interlaced: false, 70 | }, 71 | webp: { 72 | quality: 75, 73 | }, 74 | }, 75 | }, 76 | ], 77 | }, 78 | { 79 | test: /\.(ttf|eot|woff|woff2|ico)$/, 80 | use: { 81 | loader: 'file-loader', 82 | options: { 83 | name: 'fonts/[name].[ext]', 84 | }, 85 | }, 86 | }, 87 | ], 88 | }, 89 | plugins: [ 90 | new CleanWebpackPlugin(), 91 | new webpack.ProvidePlugin({ 92 | $: 'jquery', 93 | jQuery: 'jquery', 94 | 'windows.jQuery': 'jquery', 95 | Util: 'exports-loader?Util!bootstrap/js/dist/util', 96 | ko: 'exports-loader?!knockout', 97 | }), 98 | new CopyWebpackPlugin([ 99 | { 100 | from: path.resolve(__dirname, 'src', 'public'), 101 | to: 'public', 102 | }, 103 | ]), 104 | new MiniCssExtractPlugin({ 105 | filename: IS_DEV ? 'css/[name].css' : 'css/[name].[contenthash].css', 106 | chunkFilename: 'css/[id].css', 107 | }), 108 | new webpack.HashedModuleIdsPlugin(), 109 | new PreloadWebpackPlugin({ 110 | include: 'initial', 111 | }), 112 | new CssUrlRelativePlugin(), 113 | new WebpackPwaManifest({ 114 | filename: 'manifest.json', 115 | // start_url: 'YarnClassic/.', 116 | inject: true, 117 | fingerprints: false, 118 | name: 'Yarn Story Editor', 119 | short_name: 'Yarn', 120 | description: 'Yarn Story Editor', 121 | background_color: '#3367D6', 122 | theme_color: '#3367D6', 123 | display: 'fullscreen', 124 | crossorigin: 'use-credentials', //can be null, use-credentials or anonymous 125 | icons: [ 126 | { 127 | src: path.resolve('src/public/icon.png'), 128 | sizes: [96, 128, 192, 512], // multiple sizes, 192 needed by pwa 129 | }, 130 | { 131 | src: path.resolve('src/public/icon.png'), 132 | sizes: [96, 128, 192, 512], // multiple sizes, 192 and 144 needed by pwa 133 | purpose: 'maskable' 134 | }, 135 | { 136 | src: path.resolve('src/public/icon.ico'), 137 | sizes: [32], // you can also use the specifications pattern 138 | }, 139 | ], 140 | share_target: { 141 | // action: 'share-target', 142 | // enctype: 'multipart/form-data', 143 | // method: 'POST', //github.io does not allow post 144 | // params: { 145 | // files: [{ 146 | // name: 'image', 147 | // accept: ['image/*'] 148 | // }] 149 | // } 150 | action: '/YarnClassic/', 151 | method: 'GET', 152 | enctype: 'application/x-www-form-urlencoded', 153 | params: { 154 | title: 'title', 155 | text: 'text', 156 | url: 'url', 157 | }, 158 | }, 159 | }), 160 | new HtmlWebPackPlugin({ 161 | template: path.resolve(__dirname, './src/index.html'), 162 | favicon: path.resolve('src/public/icon.ico'), 163 | minify: { 164 | collapseWhitespace: true, 165 | removeComments: false, // This is mandatory, due to knockout's virtual bindings 166 | useShortDoctype: true, 167 | }, 168 | }), 169 | // new WorkboxPlugin.GenerateSW({ 170 | // swDest: path.resolve(__dirname, 'dist', 'sw.js'), 171 | // exclude: [/\.map$/, /_redirects/], 172 | // runtimeCaching: [{ 173 | // urlPattern: /https:\/\/yarnspinnertool\.github\.io\/YarnClassic\//, 174 | // handler: 'NetworkFirst' //CacheFirst 175 | // }], 176 | // }), 177 | new WorkboxPlugin.InjectManifest({ 178 | swDest: path.resolve(__dirname, 'dist', 'sw.js'), 179 | exclude: [/\.map$/, /_redirects/], 180 | swSrc: path.resolve(__dirname, 'src', 'sw-src.js'), 181 | }), 182 | ], 183 | devServer: { 184 | contentBase: path.join(__dirname, 'src'), 185 | watchContentBase: true, 186 | host: '0.0.0.0', //this will allow you to run it on a smartphone with 8080 port. Use ipconfig or ifconfig to see broadcast address 187 | }, 188 | optimization: { 189 | runtimeChunk: 'single', 190 | splitChunks: { 191 | cacheGroups: { 192 | vendor: { 193 | test: /node_modules/, 194 | chunks: 'initial', 195 | name: 'vendor', 196 | priority: 10, 197 | enforce: true, 198 | }, 199 | }, 200 | }, 201 | minimizer: [], 202 | }, 203 | }; 204 | 205 | if (!IS_DEV) { 206 | const TerserPlugin = require('terser-webpack-plugin'); 207 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 208 | config.optimization.minimizer.push( 209 | new TerserPlugin(), 210 | new OptimizeCSSAssetsPlugin({}) 211 | ); 212 | } 213 | 214 | module.exports = config; 215 | -------------------------------------------------------------------------------- /src/js/libs/spellcheck_ace.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jquery/no-ajax */ 2 | // You also need to load in nspell.js and jquery.js 3 | 4 | // This is a custom made fork that uses nspell instead of typo.js due to major performance issues in the later. 5 | // Please keep this file for now... 6 | var nspell = require('nspell'); 7 | // You should configure these classes. 8 | var editor = 'editor'; // This should be the id of your editor element. 9 | 10 | var utils = require('../classes/utils'); 11 | 12 | var dicPath = utils.Utils.getPublicPath('dictionaries/en/index.dic'); 13 | var affPath = utils.Utils.getPublicPath('dictionaries/en/index.aff'); 14 | // var dicPath = 15 | // "https://raw.githubusercontent.com/elastic/hunspell/master/dicts/en_US/en_US.dic"; 16 | // var affPath = 17 | // "https://raw.githubusercontent.com/elastic/hunspell/master/dicts/en_US/en_US.aff"; 18 | 19 | // Make red underline for gutter and words. 20 | $( 21 | "" 22 | ).appendTo('head'); 23 | $( 24 | "" 25 | ).appendTo('head'); 26 | 27 | // Load the dictionary. 28 | // We have to load the dictionary files sequentially to ensure 29 | var dictionary = null; 30 | 31 | function load_dictionary(dicLanguage) { 32 | console.info(`Loading ${dicLanguage} hunspell dictionary locally`); 33 | dicPath = utils.Utils.getPublicPath(`dictionaries/${dicLanguage}/index.dic`); 34 | affPath = utils.Utils.getPublicPath(`dictionaries/${dicLanguage}/index.aff`); 35 | 36 | $.get(dicPath, function(data) { 37 | dicData = data; 38 | }) 39 | .fail(function() { 40 | const cachedAffData = sessionStorage.getItem('affData'); 41 | const cachedDicData = sessionStorage.getItem('dicData'); 42 | if (cachedAffData && cachedDicData) { 43 | console.info( 44 | `${dicLanguage} found in sessionStorage. Loading dictionary from cache...` 45 | ); 46 | dictionary = new nspell(cachedAffData, cachedDicData); 47 | contents_modified = true; 48 | return; 49 | } 50 | console.error( 51 | `${dicLanguage} not found locally. Loading dictionary from server instead...` 52 | ); 53 | dicPath = `https://raw.githubusercontent.com/wooorm/dictionaries/main/dictionaries/${dicLanguage}/index.dic`; 54 | affPath = `https://raw.githubusercontent.com/wooorm/dictionaries/main/dictionaries/${dicLanguage}/index.aff`; 55 | 56 | $.get(dicPath, function(data) { 57 | dicData = data; 58 | }).done(function() { 59 | $.get(affPath, function(data) { 60 | affData = data; 61 | }).done(function() { 62 | sessionStorage.setItem('affData', affData); 63 | sessionStorage.setItem('dicData', dicData); 64 | dictionary = new nspell(affData, dicData); 65 | contents_modified = true; 66 | }); 67 | }); 68 | }) 69 | .done(function() { 70 | $.get(affPath, function(data) { 71 | affData = data; 72 | }).done(function() { 73 | console.log('Dictionary loaded locally'); 74 | dictionary = new nspell(affData, dicData); 75 | contents_modified = true; 76 | }); 77 | }); 78 | } 79 | exports.load_dictionary = load_dictionary; 80 | 81 | // Check the spelling of a line, and return [start, end]-pairs for misspelled words. 82 | function misspelled(line) { 83 | var multiLangualNonWords = /\s+|\.|\,|\?|\\|\/|\!|\[|\]|"|'|;|:|`|\+|\-|\&|\$|@|~|#|>|<|_|\)|\(|£|\^|%|\*|„|“|\||[0-9]+/g; 84 | var words = line.split(multiLangualNonWords); 85 | // console.log(words); 86 | var i = 0; 87 | var bads = []; 88 | for (word in words) { 89 | var checkWord = words[word]; 90 | if (!dictionary.correct(checkWord)) { 91 | bads[bads.length] = [i, i + words[word].length]; 92 | } 93 | i += words[word].length + 1; 94 | } 95 | return bads; 96 | } 97 | exports.misspelled = misspelled; 98 | 99 | var contents_modified = true; 100 | 101 | var currently_spellchecking = false; 102 | 103 | var markers_present = []; 104 | 105 | // Spell check the Ace editor contents. 106 | function spell_check() { 107 | // Wait for the dictionary to be loaded. 108 | if (dictionary == null) { 109 | return; 110 | } 111 | 112 | if (currently_spellchecking) { 113 | return; 114 | } 115 | 116 | if (!contents_modified) { 117 | return; 118 | } 119 | currently_spellchecking = true; 120 | var session = ace.edit(editor).getSession(); 121 | 122 | // Clear all markers and gutter 123 | clear_spellcheck_markers(); 124 | // Populate with markers and gutter 125 | try { 126 | var Range = ace.require('ace/range').Range; 127 | var lines = session.getDocument().getAllLines(); 128 | for (var i in lines) { 129 | // Check spelling of this line. 130 | var misspellings = misspelled(lines[i]); 131 | 132 | // Add markers and gutter markings. 133 | // if (misspellings.length > 0) { 134 | // session.addGutterDecoration(i, "misspelled"); 135 | // } 136 | for (var j in misspellings) { 137 | var range = new Range(i, misspellings[j][0], i, misspellings[j][1]); 138 | markers_present[markers_present.length] = session.addMarker( 139 | range, 140 | 'misspelled', 141 | 'typo', 142 | true 143 | ); 144 | } 145 | } 146 | } finally { 147 | currently_spellchecking = false; 148 | contents_modified = false; 149 | } 150 | } 151 | exports.spell_check = spell_check; 152 | 153 | var spellcheckEnabled = false; 154 | function enable_spellcheck() { 155 | spellcheckEnabled = true; 156 | ace 157 | .edit(editor) 158 | .getSession() 159 | .on('change', function(e) { 160 | if (spellcheckEnabled) { 161 | contents_modified = true; 162 | spell_check(); 163 | } 164 | }); 165 | // needed to trigger update once without input 166 | contents_modified = true; 167 | spell_check(); 168 | } 169 | exports.enable_spellcheck = enable_spellcheck; 170 | 171 | function disable_spellcheck() { 172 | spellcheckEnabled = false; 173 | // Clear the markers 174 | clear_spellcheck_markers(); 175 | } 176 | exports.disable_spellcheck = disable_spellcheck; 177 | 178 | function clear_spellcheck_markers() { 179 | var session = ace.edit(editor).getSession(); 180 | for (var i in markers_present) { 181 | session.removeMarker(markers_present[i]); 182 | } 183 | markers_present = []; 184 | // Clear the gutter 185 | var lines = session.getDocument().getAllLines(); 186 | for (var i in lines) { 187 | session.removeGutterDecoration(i, 'misspelled'); 188 | } 189 | } 190 | exports.clear_spellcheck_markers = clear_spellcheck_markers; 191 | 192 | function suggest_word_for_misspelled(misspelledWord) { 193 | var array_of_suggestions = dictionary.suggest(misspelledWord); 194 | if (array_of_suggestions.length === 0) { 195 | return false; 196 | } 197 | return array_of_suggestions; 198 | } 199 | exports.suggest_word_for_misspelled = suggest_word_for_misspelled; 200 | -------------------------------------------------------------------------------- /src/public/themes/blueprint.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: #2f919a; 3 | } 4 | /*-- Scrollbar ------------------------------------------------------------------ */ 5 | 6 | ::-webkit-scrollbar-thumb:hover { 7 | background: orange; 8 | border: 1px solid orange; 9 | } 10 | 11 | 12 | /*-- Menu ------------------------------------------------------------------ */ 13 | .app-menu { 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 30px; 19 | background-color: white; 20 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); 21 | } 22 | 23 | .app-menu .menu { 24 | display: block; 25 | float: left; 26 | margin-left: 10px; 27 | border-radius: 2px; 28 | box-shadow: 0; 29 | font-size: 0.9em; 30 | color: #666; 31 | cursor: pointer; 32 | } 33 | 34 | .title { 35 | box-sizing: border-box; 36 | padding: 5px; 37 | width: fit-content; 38 | min-width: 100px; 39 | line-height: 20px; 40 | font-weight: bold; 41 | float: left; 42 | } 43 | 44 | .menu .dropdown { 45 | color: gray; 46 | background-color: white; 47 | border-bottom-left-radius: 12px; 48 | border-bottom-right-radius: 12px; 49 | box-shadow: 0 5px 5px rgba(0, 0, 0, 0.4); 50 | } 51 | 52 | .menu .dropdown .item:hover { 53 | background: orange; 54 | color: black; 55 | } 56 | 57 | .menu .dropdown .item:active { 58 | background: orange; 59 | color: black; 60 | } 61 | 62 | .menu:hover .title { 63 | background: orange; 64 | color: black; 65 | } 66 | 67 | .menu:active .title { 68 | background: orange; 69 | color: #555; 70 | } 71 | 72 | .menu-icon { 73 | color: orange; 74 | } 75 | 76 | .menu .dropdown .item:hover .menu-icon { 77 | color: black; 78 | } 79 | 80 | .add-link { 81 | box-shadow: 0 0 1px 1px #e8e8e8; 82 | } 83 | 84 | .add-link:hover { 85 | background: orange; 86 | } 87 | 88 | #linkHelperMenuFilter { 89 | border-right: 2px solid orange; 90 | border-left: 2px solid orange; 91 | } 92 | 93 | /*-- Search ------------------------------------------------------------------ */ 94 | .app-search { 95 | position: absolute; 96 | top: 0; 97 | right: 10px; 98 | } 99 | 100 | .app-search input { 101 | font-family: 'Lucida Console', Monaco, monospace; 102 | } 103 | 104 | .app-search input[type='checkbox'] { 105 | cursor: pointer; 106 | } 107 | 108 | .app-search .search-field { 109 | border-top: 0; 110 | border-left: 0; 111 | border-right: 0; 112 | border-bottom: 1px solid darkgray; 113 | } 114 | 115 | .app-search .dropdown .item { 116 | display: block; 117 | box-sizing: border-box; 118 | padding: 7px; 119 | } 120 | 121 | /*-- ----------------------------------------------------------------------- */ 122 | 123 | #app-bg { 124 | /* .nodes-holder { */ 125 | width:5000px; 126 | height:5000px; 127 | background-color: CornflowerBlue; 128 | background-size: 50px 50px, 50px 50px, 10px 10px, 10px 10px; 129 | } 130 | 131 | #marquee { 132 | display: none; 133 | border: 1px double white; 134 | background-color: white; 135 | background-color: rgba(255, 255, 255, .1); 136 | } 137 | 138 | .node-editor .form { 139 | top: 2.5%; 140 | } 141 | 142 | .bbcode-button:hover { 143 | background: orange; 144 | } 145 | 146 | .node { 147 | border-top-left-radius: 15px; 148 | border-top-right-radius: 15px; 149 | border-bottom-left-radius: 12px; 150 | border-bottom-right-radius: 12px; 151 | box-shadow: 0 10px 10px rgba(0, 0, 0, 0.4); 152 | } 153 | 154 | .node.selected { 155 | border: inherit; 156 | background-color: orange; 157 | color: black; 158 | } 159 | .node .body { 160 | padding: 0 5px 0 5px; 161 | } 162 | .node.selected .body { 163 | background-color: orange; 164 | color: black; 165 | } 166 | .node .title { 167 | width: 100%; 168 | height: 40px; 169 | line-height: 40px; 170 | border-top-left-radius: 12px; 171 | border-top-right-radius: 12px; 172 | } 173 | .node .tags { 174 | padding: 0; 175 | margin: 0; 176 | width: 100%; 177 | border: 0; 178 | border-bottom-left-radius: 12px; 179 | border-bottom-right-radius: 12px; 180 | background: inherit; 181 | } 182 | .node .tags:empty { 183 | display:none; 184 | } 185 | .node .tags span { 186 | padding: 1px 5px 1px 5px; 187 | margin: 0 0 0 2px; 188 | border-radius: 15px; 189 | background-color: #E0D6C5; 190 | color: #8D8374; 191 | } 192 | 193 | /* SPECIAL CHECKBOX START */ 194 | .styled-checkbox input:checked + label { 195 | background-color: #FBCEB1; 196 | border: 2px solid orange; 197 | } 198 | 199 | .checked-button { 200 | background-color: #EFC7C7; 201 | } 202 | .styled-checkbox input:checked + label.transcribe-button { 203 | background-color: #EFC7C7; 204 | border: 2px solid #DC8484; 205 | } 206 | 207 | .settings-icon { 208 | color: orange; 209 | } 210 | 211 | .tag-style-1 { 212 | border: 1px solid red; 213 | background-color: red !important; 214 | color: yellow !important; 215 | } 216 | .tag-style-2 { 217 | border: 1px solid #6EA5E0 !important; 218 | background-color: #6EA5E0 !important; 219 | color: black !important; 220 | } 221 | .tag-style-3 { 222 | background-color: #9EDE74 !important; 223 | } 224 | .tag-style-4 { 225 | background-color: #FFE374 !important; 226 | } 227 | .tag-style-5 { 228 | background-color: #F7A666 !important; 229 | } 230 | .tag-style-6 { 231 | background-color: #C47862 !important; 232 | } 233 | .tag-style-7 { 234 | background-color: #97E1E9 !important; 235 | } 236 | .tag-style-8 { 237 | background-color: #576574 !important; 238 | color: white; 239 | } 240 | .tag-style-9 { 241 | background-color: black !important; 242 | color: white; 243 | } 244 | .title-style-1 { 245 | background-color: white; 246 | color: black; 247 | } 248 | .title-style-2 { 249 | background-color: #FF6B6B; 250 | color: white; 251 | } 252 | .title-style-3 { 253 | background-color: #1CD1A1; 254 | color: white; 255 | } 256 | .title-style-4 { 257 | background-color: #2E86DE; 258 | color: white; 259 | } 260 | .title-style-5 { 261 | background-color: #FECA57; 262 | color: white; 263 | } 264 | .title-style-6 { 265 | background-color: #5F27CD; 266 | color: white; 267 | } 268 | .title-style-7 { 269 | background-color: #D15519; 270 | color: white; 271 | } 272 | .title-style-8 { 273 | background-color: #576574; 274 | color: white; 275 | } 276 | .title-style-9 { 277 | background-color: black; 278 | color: white; 279 | } 280 | 281 | .app-info { 282 | color: #153d58; 283 | } 284 | /*-- Canvas Buttons ------------------------------------------------------- */ 285 | .app-add-node { 286 | color: grey; 287 | } 288 | 289 | .app-add-node:hover { 290 | color: orange; 291 | } 292 | 293 | .app-button span { 294 | color: grey; 295 | } 296 | 297 | .app-button span:hover { 298 | color: orange; 299 | } 300 | 301 | /* --------------------------- Narrow overrides -----------------------------*/ 302 | @media only screen and (max-width: 600px) { 303 | .app-menu { 304 | box-shadow: 0 0; 305 | } 306 | 307 | .app-search { 308 | position: absolute; 309 | top: 26px; 310 | left: 0px; 311 | width: 100%; 312 | background-color: white; 313 | padding: 5px 10px 5px 5px; 314 | z-index: 10000; 315 | } 316 | 317 | .app-search > input { 318 | padding: unset; 319 | width: 170px; 320 | } 321 | } 322 | 323 | .grid-canvas { 324 | color: lightgrey; 325 | } 326 | 327 | .arrows { 328 | color: white; 329 | } 330 | 331 | .button, 332 | button { 333 | background: orange; 334 | } -------------------------------------------------------------------------------- /src/public/libs/uFuzzy.iife.min.js: -------------------------------------------------------------------------------- 1 | /*! https://github.com/leeoniya/uFuzzy (v1.0.14) */ 2 | var uFuzzy=function(){"use strict";const e=new Intl.Collator("en",{numeric:!0,sensitivity:"base"}).compare,t=1/0,l=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),n="eexxaacctt",r=/\p{P}/gu,i=(e,t,l)=>e.replace("A-Z",t).replace("a-z",l),s={unicode:!1,alpha:null,interSplit:"[^A-Za-z\\d']+",intraSplit:"[a-z][A-Z]",interBound:"[^A-Za-z\\d]",intraBound:"[A-Za-z]\\d|\\d[A-Za-z]|[a-z][A-Z]",interLft:0,interRgt:0,interChars:".",interIns:t,intraChars:"[a-z\\d']",intraIns:null,intraContr:"'[a-z]{1,2}\\b",intraMode:0,intraSlice:[1,t],intraSub:null,intraTrn:null,intraDel:null,intraFilt:()=>!0,sort:(t,l)=>{let{idx:n,chars:r,terms:i,interLft2:s,interLft1:a,start:g,intraIns:u,interIns:f}=t;return n.map(((e,t)=>t)).sort(((t,h)=>r[h]-r[t]||u[t]-u[h]||i[h]+s[h]+.5*a[h]-(i[t]+s[t]+.5*a[t])||f[t]-f[h]||g[t]-g[h]||e(l[n[t]],l[n[h]])))}},a=(e,l)=>0==l?"":1==l?e+"??":l==t?e+"*?":e+`{0,${l}}?`,g="(?:\\b|_)";function u(e){e=Object.assign({},s,e);let{unicode:t,interLft:u,interRgt:f,intraMode:c,intraSlice:o,intraIns:p,intraSub:d,intraTrn:m,intraDel:x,intraContr:b,intraSplit:R,interSplit:L,intraBound:A,interBound:S,intraChars:z}=e;p??=c,d??=c,m??=c,x??=c;let E=e.letters??e.alpha;if(null!=E){let e=E.toLocaleUpperCase(),t=E.toLocaleLowerCase();L=i(L,e,t),R=i(R,e,t),S=i(S,e,t),A=i(A,e,t),z=i(z,e,t),b=i(b,e,t)}let I=t?"u":"";const C='".+?"',y=RegExp(C,"gi"+I),k=RegExp(`(?:\\s+|^)-(?:${z}+|${C})`,"gi"+I);let{intraRules:j}=e;null==j&&(j=e=>{let t=s.intraSlice,l=0,n=0,r=0,i=0;if(/[^\d]/.test(e)){let s=e.length;s>4?(t=o,l=p,n=d,r=m,i=x):3>s||(r=Math.min(m,1),4==s&&(l=Math.min(p,1)))}return{intraSlice:t,intraIns:l,intraSub:n,intraTrn:r,intraDel:i}});let Z=!!R,$=RegExp(R,"g"+I),w=RegExp(L,"g"+I),M=RegExp("^"+L+"|"+L+"$","g"+I),B=RegExp(b,"gi"+I);const D=e=>{let t=[];e=(e=e.replace(y,(e=>(t.push(e),n)))).replace(M,"").toLocaleLowerCase(),Z&&(e=e.replace($,(e=>e[0]+" "+e[1])));let l=0;return e.split(w).filter((e=>""!=e)).map((e=>e===n?t[l++]:e))},T=/[^\d]+|\d+/g,F=(t,n=0,r=!1)=>{let i=D(t);if(0==i.length)return[];let s,h=Array(i.length).fill("");if(i=i.map(((e,t)=>e.replace(B,(e=>(h[t]=e,""))))),1==c)s=i.map(((e,t)=>{if('"'===e[0])return l(e.slice(1,-1));let n="";for(let l of e.matchAll(T)){let e=l[0],{intraSlice:r,intraIns:i,intraSub:s,intraTrn:g,intraDel:u}=j(e);if(i+s+g+u==0)n+=e+h[t];else{let[l,f]=r,c=e.slice(0,l),o=e.slice(f),p=e.slice(l,f);1==i&&1==c.length&&c!=p[0]&&(c+="(?!"+c+")");let d=p.length,m=[e];if(s)for(let e=0;d>e;e++)m.push(c+p.slice(0,e)+z+p.slice(e+1)+o);if(g)for(let e=0;d-1>e;e++)p[e]!=p[e+1]&&m.push(c+p.slice(0,e)+p[e+1]+p[e]+p.slice(e+2)+o);if(u)for(let e=0;d>e;e++)m.push(c+p.slice(0,e+1)+"?"+p.slice(e+1)+o);if(i){let e=a(z,1);for(let t=0;d>t;t++)m.push(c+p.slice(0,t)+e+p.slice(t)+o)}n+="(?:"+m.join("|")+")"+h[t]}}return n}));else{let e=a(z,p);2==n&&p>0&&(e=")("+e+")("),s=i.map(((t,n)=>'"'===t[0]?l(t.slice(1,-1)):t.split("").map(((e,t,l)=>(1==p&&0==t&&l.length>1&&e!=l[t+1]&&(e+="(?!"+e+")"),e))).join(e)+h[n]))}let o=2==u?g:"",d=2==f?g:"",m=d+a(e.interChars,e.interIns)+o;return n>0?r?s=o+"("+s.join(")"+d+"|"+o+"(")+")"+d:(s="("+s.join(")("+m+")(")+")",s="(.??"+o+")"+s+"("+d+".*)"):(s=s.join(m),s=o+s+d),[RegExp(s,"i"+I),i,h]},O=(e,t,l)=>{let[n]=F(t);if(null==n)return null;let r=[];if(null!=l)for(let t=0;l.length>t;t++){let i=l[t];n.test(e[i])&&r.push(i)}else for(let t=0;e.length>t;t++)n.test(e[t])&&r.push(t);return r};let v=!!A,U=RegExp(S,I),N=RegExp(A,I);const P=(t,l,n)=>{let[r,i,s]=F(n,1),[a]=F(n,2),g=i.length,h=t.length,c=Array(h).fill(0),o={idx:Array(h),start:c.slice(),chars:c.slice(),terms:c.slice(),interIns:c.slice(),intraIns:c.slice(),interLft2:c.slice(),interRgt2:c.slice(),interLft1:c.slice(),interRgt1:c.slice(),ranges:Array(h)},p=1==u||1==f,d=0;for(let n=0;t.length>n;n++){let h=l[t[n]],c=h.match(r),m=c.index+c[1].length,x=m,b=!1,R=0,L=0,A=0,S=0,z=0,E=0,C=0,y=0,k=[];for(let t=0,l=2;g>t;t++,l+=2){let n=c[l].toLocaleLowerCase(),r=i[t],a='"'==r[0]?r.slice(1,-1):r+s[t],o=a.length,d=n.length,j=n==a;if(!j&&c[l+1].length>=o){let e=c[l+1].toLocaleLowerCase().indexOf(a);e>-1&&(k.push(x,d,e,o),x+=_(c,l,e,o),n=a,d=o,j=!0,0==t&&(m=x))}if(p||j){let e=x-1,r=x+d,i=!1,s=!1;if(-1==e||U.test(h[e]))j&&R++,i=!0;else{if(2==u){b=!0;break}if(v&&N.test(h[e]+h[e+1]))j&&L++,i=!0;else if(1==u){let e=c[l+1],r=x+d;if(e.length>=o){let s,g=0,u=!1,f=RegExp(a,"ig"+I);for(;s=f.exec(e);){g=s.index;let e=r+g,t=e-1;if(-1==t||U.test(h[t])){R++,u=!0;break}if(N.test(h[t]+h[e])){L++,u=!0;break}}u&&(i=!0,k.push(x,d,g,o),x+=_(c,l,g,o),n=a,d=o,j=!0,0==t&&(m=x))}if(!i){b=!0;break}}}if(r==h.length||U.test(h[r]))j&&A++,s=!0;else{if(2==f){b=!0;break}if(v&&N.test(h[r-1]+h[r]))j&&S++,s=!0;else if(1==f){b=!0;break}}j&&(z+=o,i&&s&&E++)}if(d>o&&(y+=d-o),t>0&&(C+=c[l-1].length),!e.intraFilt(a,n,x)){b=!0;break}g-1>t&&(x+=d+c[l+1].length)}if(!b){o.idx[d]=t[n],o.interLft2[d]=R,o.interLft1[d]=L,o.interRgt2[d]=A,o.interRgt1[d]=S,o.chars[d]=z,o.terms[d]=E,o.interIns[d]=C,o.intraIns[d]=y,o.start[d]=m;let e=h.match(a),l=e.index+e[1].length,r=k.length,i=r>0?0:1/0,s=r-4;for(let t=2;e.length>t;)if(i>s||k[i]!=l)l+=e[t].length,t++;else{let n=k[i+1],r=k[i+2],s=k[i+3],a=t,g="";for(let t=0;n>t;a++)g+=e[a],t+=e[a].length;e.splice(t,a-t,g),l+=_(e,t,r,s),i+=4}l=e.index+e[1].length;let g=o.ranges[d]=[],u=l,f=l;for(let t=2;e.length>t;t++){let n=e[t].length;l+=n,t%2==0?f=l:n>0&&(g.push(u,f),u=f=l)}f>u&&g.push(u,f),d++}}if(t.length>d)for(let e in o)o[e]=o[e].slice(0,d);return o},_=(e,t,l,n)=>{let r=e[t]+e[t+1].slice(0,l);return e[t-1]+=r,e[t]=e[t+1].slice(l,l+n),e[t+1]=e[t+1].slice(l+n),r.length};return{search:(...t)=>((t,n,i,s=1e3,a)=>{i=i?!0===i?5:i:0;let g=null,u=null,f=[];n=n.replace(k,(e=>{let t=e.trim().slice(1);return t='"'===t[0]?l(t.slice(1,-1)):t.replace(r,""),""!=t&&f.push(t),""}));let c,o=D(n);if(f.length>0){if(c=RegExp(f.join("|"),"i"+I),0==o.length){let e=[];for(let l=0;t.length>l;l++)c.test(t[l])||e.push(l);return[e,null,null]}}else if(0==o.length)return[null,null,null];if(i>0){let e=D(n);if(e.length>1){let l=e.slice().sort(((e,t)=>t.length-e.length));for(let e=0;l.length>e;e++){if(0==a?.length)return[[],null,null];a=O(t,l[e],a)}if(e.length>i)return[a,null,null];g=h(e).map((e=>e.join(" "))),u=[];let n=new Set;for(let e=0;g.length>e;e++)if(a.length>n.size){let l=a.filter((e=>!n.has(e))),r=O(t,g[e],l);for(let e=0;r.length>e;e++)n.add(r[e]);u.push(r)}else u.push([])}}null==g&&(g=[n],u=[a?.length>0?a:O(t,n)]);let p=null,d=null;if(f.length>0&&(u=u.map((e=>e.filter((e=>!c.test(t[e])))))),s>=u.reduce(((e,t)=>e+t.length),0)){p={},d=[];for(let l=0;u.length>l;l++){let n=u[l];if(null==n||0==n.length)continue;let r=g[l],i=P(n,t,r),s=e.sort(i,t,r);if(l>0)for(let e=0;s.length>e;e++)s[e]+=d.length;for(let e in i)p[e]=(p[e]??[]).concat(i[e]);d=d.concat(s)}}return[[].concat(...u),p,d]})(...t),split:D,filter:O,info:P,sort:e.sort}}const f=(()=>{let e={A:"ÁÀÃÂÄĄ",a:"áàãâäą",E:"ÉÈÊËĖ",e:"éèêëę",I:"ÍÌÎÏĮ",i:"íìîïį",O:"ÓÒÔÕÖ",o:"óòôõö",U:"ÚÙÛÜŪŲ",u:"úùûüūų",C:"ÇČĆ",c:"çčć",L:"Ł",l:"ł",N:"ÑŃ",n:"ñń",S:"ŠŚ",s:"šś",Z:"ŻŹ",z:"żź"},t=new Map,l="";for(let n in e)e[n].split("").forEach((e=>{l+=e,t.set(e,n)}));let n=RegExp(`[${l}]`,"g"),r=e=>t.get(e);return e=>{if("string"==typeof e)return e.replace(n,r);let t=Array(e.length);for(let l=0;e.length>l;l++)t[l]=e[l].replace(n,r);return t}})();function h(e){let t,l,n=(e=e.slice()).length,r=[e.slice()],i=Array(n).fill(0),s=1;for(;n>s;)s>i[s]?(t=s%2&&i[s],l=e[s],e[s]=e[t],e[t]=l,++i[s],s=1,r.push(e.slice())):(i[s]=0,++s);return r}const c=(e,t)=>t?`${e}`:e,o=(e,t)=>e+t;return u.latinize=f,u.permute=e=>h([...Array(e.length).keys()]).sort(((e,t)=>{for(let l=0;e.length>l;l++)if(e[l]!=t[l])return e[l]-t[l];return 0})).map((t=>t.map((t=>e[t])))),u.highlight=function(e,t,l=c,n="",r=o){n=r(n,l(e.substring(0,t[0]),!1))??n;for(let i=0;t.length>i;i+=2)n=r(n,l(e.substring(t[i],t[i+1]),!0))??n,t.length-3>i&&(n=r(n,l(e.substring(t[i+1],t[i+2]),!1))??n);return r(n,l(e.substring(t[t.length-1]),!1))??n},u}(); 3 | -------------------------------------------------------------------------------- /src/js/classes/richTextFormatterBbcode.js: -------------------------------------------------------------------------------- 1 | const bbcode = require('bbcode'); 2 | 3 | export const BbcodeRichTextFormatter = function(app, addExtraPreviewerEmbeds) { 4 | const self = this; 5 | this.justInsertedAutoComplete = false; 6 | 7 | this.completableTags = Object.freeze([ 8 | { Start: '<<', Completion: '>>', Offset: -2 }, 9 | { 10 | Start: '[colo', 11 | Completion: 'r=#][/color]', 12 | Offset: -9, 13 | BehaviorCompletion: 'r=#][/color', 14 | Func: () => { 15 | app.insertColorCode(); 16 | }, 17 | }, 18 | { 19 | Start: '[b', 20 | Completion: '][/b]', 21 | BehaviorCompletion: '][/b', 22 | Offset: -4, 23 | }, 24 | { 25 | Start: '[i', 26 | Completion: '][/i]', 27 | BehaviorCompletion: '][/i', 28 | Offset: -4, 29 | }, 30 | { 31 | Start: '[img', 32 | Completion: '][/img]', 33 | BehaviorCompletion: '][/img', 34 | Offset: -6, 35 | }, 36 | { 37 | Start: '[u', 38 | Completion: '][/u]', 39 | BehaviorCompletion: '][/u', 40 | Offset: -4, 41 | }, 42 | { 43 | Start: '[url', 44 | Completion: '][/url]', 45 | BehaviorCompletion: '][/url', 46 | Offset: -6, 47 | }, 48 | ]); 49 | 50 | this.getTagOpen = function(tag) { 51 | switch (tag) { 52 | case 'cmd': 53 | return app.settings.documentType() === 'ink' 54 | ? app.editor.getSelectedText().length === 0 55 | ? '~ ' 56 | : '{ ' 57 | : '<<'; 58 | case 'opt': 59 | return app.settings.documentType() === 'ink' 60 | ? app.editor.getSelectedText().length === 0 61 | ? '-> ' 62 | : '* [' 63 | : '[['; 64 | case 'color': 65 | return '[color=#]'; 66 | default: 67 | return `[${tag}]`; 68 | } 69 | }; 70 | 71 | this.getTagClose = function(tag) { 72 | switch (tag) { 73 | case 'cmd': 74 | return app.settings.documentType() === 'ink' 75 | ? app.editor.getSelectedText().length === 0 76 | ? '' 77 | : ' }' 78 | : '>>'; 79 | case 'opt': 80 | return app.settings.documentType() === 'ink' 81 | ? app.editor.getSelectedText().length === 0 82 | ? '' 83 | : ']' 84 | : '|]]'; 85 | default: 86 | return `[/${tag}]`; 87 | } 88 | }; 89 | 90 | this.identifyTag = function(text) { 91 | let tag = 92 | text.lastIndexOf('[') !== -1 93 | ? text.substring(text.lastIndexOf('['), text.length) 94 | : ''; 95 | 96 | return tag; 97 | }; 98 | 99 | this.insertTag = function(tag) { 100 | const tagOpen = self.getTagOpen(tag); 101 | const tagClose = self.getTagClose(tag); 102 | 103 | const selectedRange = JSON.parse( 104 | JSON.stringify(app.editor.selection.getRange()) 105 | ); 106 | 107 | app.editor.session.insert(selectedRange.start, tagOpen); 108 | app.editor.session.insert( 109 | { 110 | column: selectedRange.end.column + tagOpen.length, 111 | row: selectedRange.end.row, 112 | }, 113 | tagClose 114 | ); 115 | 116 | if (tag === 'color') { 117 | if (app.editor.getSelectedText().length === 0) { 118 | app.moveEditCursor(-9); 119 | } else { 120 | app.editor.selection.setRange({ 121 | start: { 122 | row: app.editor.selection.getRange().start.row, 123 | column: app.editor.selection.getRange().start.column - 1, 124 | }, 125 | end: { 126 | row: app.editor.selection.getRange().start.row, 127 | column: app.editor.selection.getRange().start.column - 1, 128 | }, 129 | }); 130 | } 131 | app.insertColorCode(); 132 | } 133 | if (tag === 'img') { 134 | if (app.editor.getSelectedText().length === 0) { 135 | app.moveEditCursor(-6); 136 | app.data.triggerPasteClipboard(); 137 | setTimeout(() => app.moveEditCursor(6), 300); 138 | } 139 | } else if (app.editor.getSelectedText().length === 0) { 140 | if (!app.isEditorInPreviewMode) app.moveEditCursor(-tagClose.length); 141 | } else { 142 | app.editor.selection.setRange({ 143 | start: app.editor.selection.getRange().start, 144 | end: { 145 | row: app.editor.selection.getRange().end.row, 146 | column: app.editor.selection.getRange().end.column - tagClose.length, 147 | }, 148 | }); 149 | } 150 | app.editor.focus(); 151 | }; 152 | 153 | this._convertTag = function(inPattern, outPattern, text) { 154 | const globalRegex = new RegExp(inPattern, 'gi'); 155 | const localRegex = new RegExp(inPattern, 'i'); 156 | 157 | return text.replace(globalRegex, m => { 158 | const match = m.match(localRegex); 159 | const template = eval('`' + outPattern + '`'); 160 | return match.length ? template : null; 161 | }); 162 | }; 163 | 164 | this.convert = function(text) { 165 | let result = text; 166 | 167 | result = self._convertTag('(.*?)<\\/b>', '[b]${match[1]}[/b]', result); 168 | result = self._convertTag('(.*?)<\\/u>', '[u]${match[1]}[/u]', result); 169 | result = self._convertTag('(.*?)<\\/i>', '[i]${match[1]}[/i]', result); 170 | result = self._convertTag( 171 | '(.*?)<\\/img>', 172 | '[img]${match[1]}[/img]', 173 | result 174 | ); 175 | result = self._convertTag( 176 | '(.*?)<\\/color>', 177 | '[color=#${match[1]}]${match[2]}[/color]', 178 | result 179 | ); 180 | result = self._convertTag( 181 | '(.*?)<\\/url>', 182 | '[url]${match[1]}[/url]', 183 | result 184 | ); 185 | 186 | return result; 187 | }; 188 | 189 | this.richTextToHtml = function(text, showRowNumbers = false) { 190 | let rowCounter = 1; 191 | let result = showRowNumbers 192 | ? '
' + 193 | '' + 194 | rowCounter + 195 | '. ' + 196 | text + 197 | '
' // TODO: style this 198 | : text; 199 | 200 | /// Commands in preview mode 201 | result = result.replace(/<(run:"); // TODO: style this 202 | result = result.replace(/>>/gi, ')'); 203 | 204 | /// bbcode color tags in preview mode 205 | result = result.replace(/\[color=#[A-Za-z0-9]+\]/gi, function(colorCode) { 206 | const extractedCol = colorCode.match(/\[color=#([A-Za-z0-9]+)\]/i); 207 | if (extractedCol && extractedCol.length > 1) { 208 | return ( 209 | '[color=#' + 210 | extractedCol[1] + 211 | ']' 214 | ); 215 | } 216 | }); 217 | 218 | /// bbcode local images with path relative to the opened yarn file 219 | result = result.replace(/\[img\][^\[]+\[\/img\]/gi, function(imgTag) { 220 | const extractedImgPath = imgTag.match(/\[img\](.*)\[\/img\]/i); 221 | if (extractedImgPath.length > 1) { 222 | const fullPathToFile = app.data.editingFileFolder(extractedImgPath[1]); 223 | if (app.data.doesFileExist(fullPathToFile)) { 224 | return showRowNumbers 225 | ? ' ' 226 | : ' '; 229 | } else { 230 | // if not a local file, try to load it as a link 231 | return showRowNumbers 232 | ? ' ' 233 | : ' '; 236 | } 237 | } 238 | }); 239 | 240 | if (showRowNumbers) result = addExtraPreviewerEmbeds(result); 241 | /// do this last, as we need the newline characters in previous regex tests 242 | result = result.replace(/[\n\r]/g, function(row) { 243 | let rowAppend = '
'; 244 | rowCounter += 1; 245 | if (showRowNumbers) { 246 | rowAppend += 247 | '
' + rowCounter + '. '; 248 | } 249 | return rowAppend; 250 | }); 251 | 252 | /// other bbcode tag parsing in preview mode 253 | result = bbcode.parse(result); 254 | 255 | return result; 256 | }; 257 | }; 258 | -------------------------------------------------------------------------------- /src/scss/jquery.contextMenu.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /*! 3 | * jQuery contextMenu - Plugin for simple contextMenu handling 4 | * 5 | * Version: v2.7.0 6 | * 7 | * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF) 8 | * Web: http://swisnl.github.io/jQuery-contextMenu/ 9 | * 10 | * Copyright (c) 2011-2018 SWIS BV and contributors 11 | * 12 | * Licensed under 13 | * MIT License http://www.opensource.org/licenses/mit-license 14 | * 15 | * Date: 2018-10-02T14:29:27.829Z 16 | */ 17 | @-webkit-keyframes cm-spin { 18 | 0% { 19 | -webkit-transform: translateY(-50%) rotate(0deg); 20 | transform: translateY(-50%) rotate(0deg); 21 | } 22 | 100% { 23 | -webkit-transform: translateY(-50%) rotate(359deg); 24 | transform: translateY(-50%) rotate(359deg); 25 | } 26 | } 27 | @-o-keyframes cm-spin { 28 | 0% { 29 | -webkit-transform: translateY(-50%) rotate(0deg); 30 | -o-transform: translateY(-50%) rotate(0deg); 31 | transform: translateY(-50%) rotate(0deg); 32 | } 33 | 100% { 34 | -webkit-transform: translateY(-50%) rotate(359deg); 35 | -o-transform: translateY(-50%) rotate(359deg); 36 | transform: translateY(-50%) rotate(359deg); 37 | } 38 | } 39 | @keyframes cm-spin { 40 | 0% { 41 | -webkit-transform: translateY(-50%) rotate(0deg); 42 | -o-transform: translateY(-50%) rotate(0deg); 43 | transform: translateY(-50%) rotate(0deg); 44 | } 45 | 100% { 46 | -webkit-transform: translateY(-50%) rotate(359deg); 47 | -o-transform: translateY(-50%) rotate(359deg); 48 | transform: translateY(-50%) rotate(359deg); 49 | } 50 | } 51 | 52 | @font-face { 53 | font-family: "context-menu-icons"; 54 | font-style: normal; 55 | font-weight: normal; 56 | 57 | src: url("font/context-menu-icons.eot?2gb3e"); 58 | src: url("font/context-menu-icons.eot?2gb3e#iefix") format("embedded-opentype"), url("font/context-menu-icons.woff2?2gb3e") format("woff2"), url("font/context-menu-icons.woff?2gb3e") format("woff"), url("font/context-menu-icons.ttf?2gb3e") format("truetype"); 59 | } 60 | 61 | .context-menu-icon-add:before { 62 | content: "\EA01"; 63 | } 64 | 65 | .context-menu-icon-copy:before { 66 | content: "\EA02"; 67 | } 68 | 69 | .context-menu-icon-cut:before { 70 | content: "\EA03"; 71 | } 72 | 73 | .context-menu-icon-delete:before { 74 | content: "\EA04"; 75 | } 76 | 77 | .context-menu-icon-edit:before { 78 | content: "\EA05"; 79 | } 80 | 81 | .context-menu-icon-loading:before { 82 | content: "\EA06"; 83 | } 84 | 85 | .context-menu-icon-paste:before { 86 | content: "\EA07"; 87 | } 88 | 89 | .context-menu-icon-quit:before { 90 | content: "\EA08"; 91 | } 92 | 93 | .context-menu-icon::before { 94 | position: absolute; 95 | top: 50%; 96 | left: 0; 97 | width: 2em; 98 | font-family: "context-menu-icons"; 99 | font-size: 1em; 100 | font-style: normal; 101 | font-weight: normal; 102 | line-height: 1; 103 | color: #2980b9; 104 | text-align: center; 105 | -webkit-transform: translateY(-50%); 106 | -ms-transform: translateY(-50%); 107 | -o-transform: translateY(-50%); 108 | transform: translateY(-50%); 109 | 110 | -webkit-font-smoothing: antialiased; 111 | -moz-osx-font-smoothing: grayscale; 112 | } 113 | 114 | .context-menu-icon.context-menu-hover:before { 115 | color: #fff; 116 | } 117 | 118 | .context-menu-icon.context-menu-disabled::before { 119 | color: #bbb; 120 | } 121 | 122 | .context-menu-icon.context-menu-icon-loading:before { 123 | -webkit-animation: cm-spin 2s infinite; 124 | -o-animation: cm-spin 2s infinite; 125 | animation: cm-spin 2s infinite; 126 | } 127 | 128 | .context-menu-icon.context-menu-icon--fa { 129 | display: list-item; 130 | font-family: inherit; 131 | line-height: inherit; 132 | } 133 | .context-menu-icon.context-menu-icon--fa::before { 134 | position: absolute; 135 | top: 50%; 136 | left: 0; 137 | width: 2em; 138 | font-family: FontAwesome; 139 | font-size: 1em; 140 | font-style: normal; 141 | font-weight: normal; 142 | line-height: 1; 143 | color: #2980b9; 144 | text-align: center; 145 | -webkit-transform: translateY(-50%); 146 | -ms-transform: translateY(-50%); 147 | -o-transform: translateY(-50%); 148 | transform: translateY(-50%); 149 | 150 | -webkit-font-smoothing: antialiased; 151 | -moz-osx-font-smoothing: grayscale; 152 | } 153 | .context-menu-icon.context-menu-icon--fa.context-menu-hover:before { 154 | color: #fff; 155 | } 156 | .context-menu-icon.context-menu-icon--fa.context-menu-disabled::before { 157 | color: #bbb; 158 | } 159 | 160 | .context-menu-icon.context-menu-icon--fa5 { 161 | display: list-item; 162 | font-family: inherit; 163 | line-height: inherit; 164 | } 165 | .context-menu-icon.context-menu-icon--fa5 i, .context-menu-icon.context-menu-icon--fa5 svg { 166 | position: absolute; 167 | top: .3em; 168 | left: .5em; 169 | color: #2980b9; 170 | } 171 | .context-menu-icon.context-menu-icon--fa5.context-menu-hover > i, .context-menu-icon.context-menu-icon--fa5.context-menu-hover > svg { 172 | color: #fff; 173 | } 174 | .context-menu-icon.context-menu-icon--fa5.context-menu-disabled i, .context-menu-icon.context-menu-icon--fa5.context-menu-disabled svg { 175 | color: #bbb; 176 | } 177 | 178 | .context-menu-list { 179 | position: absolute; 180 | display: inline-block; 181 | min-width: 13em; 182 | max-width: 26em; 183 | padding: .25em 0; 184 | margin: .3em; 185 | font-family: inherit; 186 | font-size: inherit; 187 | list-style-type: none; 188 | background: #fff; 189 | border: 1px solid #bebebe; 190 | border-radius: .2em; 191 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, .5); 192 | box-shadow: 0 2px 5px rgba(0, 0, 0, .5); 193 | } 194 | 195 | .context-menu-item { 196 | position: relative; 197 | -webkit-box-sizing: content-box; 198 | -moz-box-sizing: content-box; 199 | box-sizing: content-box; 200 | padding: .2em 2em; 201 | color: #2f2f2f; 202 | -webkit-user-select: none; 203 | -moz-user-select: none; 204 | -ms-user-select: none; 205 | user-select: none; 206 | background-color: #fff; 207 | } 208 | 209 | .context-menu-separator { 210 | padding: 0; 211 | margin: .35em 0; 212 | border-bottom: 1px solid #e6e6e6; 213 | } 214 | 215 | .context-menu-item > label > input, 216 | .context-menu-item > label > textarea { 217 | -webkit-user-select: text; 218 | -moz-user-select: text; 219 | -ms-user-select: text; 220 | user-select: text; 221 | } 222 | 223 | .context-menu-item.context-menu-hover { 224 | color: #fff; 225 | cursor: pointer; 226 | background-color: #2980b9; 227 | } 228 | 229 | .context-menu-item.context-menu-disabled { 230 | color: #bbb; 231 | cursor: default; 232 | background-color: #fff; 233 | } 234 | 235 | .context-menu-input.context-menu-hover { 236 | color: #2f2f2f; 237 | cursor: default; 238 | } 239 | 240 | .context-menu-submenu:after { 241 | position: absolute; 242 | top: 50%; 243 | right: .5em; 244 | z-index: 1; 245 | width: 0; 246 | height: 0; 247 | content: ''; 248 | border-color: transparent transparent transparent #2f2f2f; 249 | border-style: solid; 250 | border-width: .25em 0 .25em .25em; 251 | -webkit-transform: translateY(-50%); 252 | -ms-transform: translateY(-50%); 253 | -o-transform: translateY(-50%); 254 | transform: translateY(-50%); 255 | } 256 | 257 | /** 258 | * Inputs 259 | */ 260 | .context-menu-item.context-menu-input { 261 | padding: .3em .6em; 262 | } 263 | 264 | /* vertically align inside labels */ 265 | .context-menu-input > label > * { 266 | vertical-align: top; 267 | } 268 | 269 | /* position checkboxes and radios as icons */ 270 | .context-menu-input > label > input[type="checkbox"], 271 | .context-menu-input > label > input[type="radio"] { 272 | position: relative; 273 | top: .12em; 274 | margin-right: .4em; 275 | } 276 | 277 | .context-menu-input > label { 278 | margin: 0; 279 | } 280 | 281 | .context-menu-input > label, 282 | .context-menu-input > label > input[type="text"], 283 | .context-menu-input > label > textarea, 284 | .context-menu-input > label > select { 285 | display: block; 286 | width: 100%; 287 | -webkit-box-sizing: border-box; 288 | -moz-box-sizing: border-box; 289 | box-sizing: border-box; 290 | } 291 | 292 | .context-menu-input > label > textarea { 293 | height: 7em; 294 | } 295 | 296 | .context-menu-item > .context-menu-list { 297 | top: .3em; 298 | /* re-positioned by js */ 299 | right: -.3em; 300 | display: none; 301 | } 302 | 303 | .context-menu-item.context-menu-visible > .context-menu-list { 304 | display: block; 305 | } 306 | 307 | .context-menu-accesskey { 308 | text-decoration: underline; 309 | } 310 | -------------------------------------------------------------------------------- /src/js/classes/richTextFormatterHtml.js: -------------------------------------------------------------------------------- 1 | export const HtmlRichTextFormatter = function(app, addExtraPreviewerEmbeds) { 2 | const self = this; 3 | this.justInsertedAutoComplete = false; 4 | 5 | this.completableTags = Object.freeze([ 6 | { Start: '<<', Completion: '>>', Offset: -2 }, 7 | { 8 | Start: '', 10 | Offset: -9, 11 | Func: () => { 12 | app.insertColorCode(); 13 | }, 14 | }, 15 | { Start: '', Offset: -4 }, 16 | { Start: '', Offset: -6 }, 17 | { Start: '', Offset: -4 }, 18 | { Start: '', Offset: -4 }, 19 | { Start: '', Offset: -6 }, 20 | ]); 21 | 22 | this.getTagOpen = function(tag) { 23 | switch (tag) { 24 | case 'cmd': 25 | return app.settings.documentType() === 'ink' 26 | ? app.editor.getSelectedText().length === 0 27 | ? '~ ' 28 | : '{ ' 29 | : '<<'; 30 | case 'opt': 31 | return app.settings.documentType() === 'ink' 32 | ? app.editor.getSelectedText().length === 0 33 | ? '-> ' 34 | : '* [' 35 | : '[['; 36 | case 'color': 37 | return ''; 38 | default: 39 | return `<${tag}>`; 40 | } 41 | }; 42 | 43 | this.getTagClose = function(tag) { 44 | switch (tag) { 45 | case 'cmd': 46 | return app.settings.documentType() === 'ink' 47 | ? app.editor.getSelectedText().length === 0 48 | ? '' 49 | : ' }' 50 | : '>>'; 51 | case 'opt': 52 | return app.settings.documentType() === 'ink' 53 | ? app.editor.getSelectedText().length === 0 54 | ? '' 55 | : ']' 56 | : '|]]'; 57 | default: 58 | return ``; 59 | } 60 | }; 61 | 62 | this.identifyTag = function(text) { 63 | let tag = 64 | text.lastIndexOf('<') !== -1 65 | ? text.substring(text.lastIndexOf('<'), text.length) 66 | : ''; 67 | return tag; 68 | }; 69 | 70 | this.insertTag = function(tag) { 71 | const tagOpen = self.getTagOpen(tag); 72 | const tagClose = self.getTagClose(tag); 73 | 74 | const selectedRange = JSON.parse( 75 | JSON.stringify(app.editor.selection.getRange()) 76 | ); 77 | 78 | app.editor.session.insert(selectedRange.start, tagOpen); 79 | app.editor.session.insert( 80 | { 81 | column: selectedRange.end.column + tagOpen.length, 82 | row: selectedRange.end.row, 83 | }, 84 | tagClose 85 | ); 86 | 87 | if (tag === 'color') { 88 | if (app.editor.getSelectedText().length === 0) { 89 | app.moveEditCursor(-9); 90 | } else { 91 | app.editor.selection.setRange({ 92 | start: { 93 | row: app.editor.selection.getRange().start.row, 94 | column: app.editor.selection.getRange().start.column - 1, 95 | }, 96 | end: { 97 | row: app.editor.selection.getRange().start.row, 98 | column: app.editor.selection.getRange().start.column - 1, 99 | }, 100 | }); 101 | } 102 | app.insertColorCode(); 103 | } 104 | if (tag === 'img') { 105 | navigator.clipboard.readText().then(text => { 106 | if (app.editor.getSelectedText().length === 0) { 107 | app.moveEditCursor(-7); 108 | app.insertTextAtCursor(` src="${text}"`); 109 | } 110 | }); 111 | } else if (app.editor.getSelectedText().length === 0) { 112 | if (!app.isEditorInPreviewMode) app.moveEditCursor(-tagClose.length); 113 | } else { 114 | app.editor.selection.setRange({ 115 | start: app.editor.selection.getRange().start, 116 | end: { 117 | row: app.editor.selection.getRange().end.row, 118 | column: app.editor.selection.getRange().end.column - tagClose.length, 119 | }, 120 | }); 121 | } 122 | app.editor.focus(); 123 | }; 124 | 125 | this._convertTag = function(inPattern, outPattern, text) { 126 | const globalRegex = new RegExp(inPattern, 'gi'); 127 | const localRegex = new RegExp(inPattern, 'i'); 128 | 129 | return text.replace(globalRegex, m => { 130 | const match = m.match(localRegex); 131 | const template = eval('`' + outPattern + '`'); 132 | return match.length ? template : null; 133 | }); 134 | }; 135 | 136 | this.convert = function(text) { 137 | let result = text; 138 | 139 | result = self._convertTag( 140 | '\\[b\\](.*?)\\[\\/b\\]', 141 | '${match[1]}', 142 | result 143 | ); 144 | result = self._convertTag( 145 | '\\[u\\](.*?)\\[\\/u\\]', 146 | '${match[1]}', 147 | result 148 | ); 149 | result = self._convertTag( 150 | '\\[i\\](.*?)\\[\\/i\\]', 151 | '${match[1]}', 152 | result 153 | ); 154 | result = self._convertTag( 155 | '\\[img\\](.*?)\\[\\/img\\]', 156 | '${match[1]}', 157 | result 158 | ); 159 | result = self._convertTag( 160 | '\\[color=#(.*?)\\](.*?)\\[\\/color\\]', 161 | '${match[2]}', 162 | result 163 | ); 164 | result = self._convertTag( 165 | '\\[url\\](.*?)\\[\\/url\\]', 166 | '${match[1]}', 167 | result 168 | ); 169 | 170 | return result; 171 | }; 172 | 173 | this.richTextToHtml = function(text, showRowNumbers = false) { 174 | let rowCounter = 1; 175 | let result = showRowNumbers 176 | ? '
' + 177 | '' + 178 | rowCounter + 179 | '. ' + 180 | text + 181 | '
' // TODO: style this 182 | : text; 183 | 184 | /// <> 185 | result = result.replace(/<(run:"); // TODO: style this 186 | result = result.replace(/>>/gi, ')
'); 187 | 188 | /// and <color=#...></color> 189 | [ 190 | /<color=#(.*?)>(.*?)<\/color>/, 191 | /(.*?)<\/color>/, 192 | ].forEach(pattern => { 193 | const globalRegex = new RegExp(pattern, 'gi'); 194 | const localRegex = new RegExp(pattern, 'i'); 195 | 196 | result = result.replace(globalRegex, function(colorCode) { 197 | const matches = colorCode.match(localRegex); 198 | if (matches && matches.length > 2) { 199 | return `☗${matches[2]}`; 200 | } 201 | }); 202 | }); 203 | 204 | // local images with path relative to the opened yarn file 205 | result = result.replace(/<img>[^\[]+<\/img>/gi, function( 206 | imgTag 207 | ) { 208 | const extractedImgPath = imgTag.match(/<img>(.*?)<\/img>/i); 209 | if (extractedImgPath.length > 1) { 210 | const fullPathToFile = app.data.editingFileFolder(extractedImgPath[1]); 211 | if (app.data.doesFileExist(fullPathToFile)) { 212 | return showRowNumbers 213 | ? ' ' 214 | : ' '; 217 | } else { 218 | // if not a local file, try to load it as a link 219 | return showRowNumbers 220 | ? ' ' 221 | : ' '; 224 | } 225 | } 226 | }); 227 | 228 | // 229 | result = result.replace(/<b>.*<\/b>/gi, m => { 230 | const content = m.match(/<b>(.*)<\/b>/i); 231 | if (content.length) { 232 | return `${content[1]}`; 233 | } 234 | }); 235 | 236 | // 237 | result = result.replace(/<u>.*<\/u>/gi, m => { 238 | const content = m.match(/<u>(.*)<\/u>/i); 239 | if (content.length) { 240 | return `${content[1]}`; 241 | } 242 | }); 243 | 244 | // 245 | result = result.replace(/<i>.*<\/i>/gi, m => { 246 | const content = m.match(/<i>(.*)<\/i>/i); 247 | if (content.length) { 248 | return `${content[1]}`; 249 | } 250 | }); 251 | 252 | if (showRowNumbers) result = addExtraPreviewerEmbeds(result); 253 | // newLines. Do this last, as we need the newline characters in previous regex tests 254 | result = result.replace(/[\n\r]/g, function(row) { 255 | let rowAppend = '
'; 256 | rowCounter += 1; 257 | if (showRowNumbers) { 258 | rowAppend += 259 | '
' + rowCounter + '. '; 260 | } 261 | return rowAppend; 262 | }); 263 | 264 | /// finaly return the html result 265 | return result; 266 | }; 267 | }; 268 | -------------------------------------------------------------------------------- /src/js/classes/settings.js: -------------------------------------------------------------------------------- 1 | // Get the mechanism to use for storage. 2 | const getStorage = function() { 3 | // if `window.vsCodeApi` exists, we're in the context of the VSCode extension 4 | // which handles all of the settings internally, so we don't need to do anything here 5 | if (window.vsCodeApi) { 6 | return { 7 | getItem: () => {}, 8 | setItem: () => {}, 9 | }; 10 | } else { 11 | return window.localStorage; 12 | } 13 | }; 14 | 15 | export const Settings = function(app) { 16 | const self = this; 17 | const storage = getStorage(); 18 | this.storage = storage; 19 | 20 | ko.extenders.persist = function(target, option) { 21 | target.subscribe(function(newValue) { 22 | storage.setItem(option, newValue); 23 | app.data.db.save(option, newValue); 24 | }); 25 | return target; 26 | }; 27 | 28 | // apply 29 | // 30 | // Applies the current settings 31 | this.apply = function() { 32 | app.setTheme(self.theme()); 33 | app.setLanguage(self.language()); 34 | app.setDocumentType(self.documentType()); 35 | app.toggleInvertColors(); 36 | app.setMarkupLanguage(self.markupLanguage()); 37 | console.log('redraw throttle:', self.redrawThrottle()); 38 | app.workspace.setThrottle(self.redrawThrottle()); 39 | app.setGistCredentials({ 40 | token: self.gistToken(), 41 | file: 42 | self.gistFile() !== null 43 | ? self 44 | .gistFile() 45 | .split('/') 46 | .pop() 47 | : null, 48 | }); 49 | app.setGistPluginsFile( 50 | self.gistPluginsFile() !== null 51 | ? self 52 | .gistPluginsFile() 53 | .split('/') 54 | .pop() 55 | : null 56 | ); 57 | }; 58 | 59 | this.pasteFromClipboard = function(koItem){ 60 | if(koItem && koItem.length > 0) return; 61 | if(!navigator.clipboard) return; 62 | navigator.clipboard 63 | .readText() 64 | .then( 65 | (clipText) => { 66 | app.log("clipboard", clipText) 67 | if(clipText && clipText.length > 5) koItem(clipText) 68 | }, 69 | ); 70 | } 71 | this.guessGistToken = function() { 72 | self.pasteFromClipboard(self.gistToken); 73 | } 74 | this.guessGistFile = function() { 75 | self.pasteFromClipboard(self.gistFile); 76 | } 77 | 78 | this.validateGridSize = function() { 79 | if (self.gridSize() < 20) { 80 | self.gridSize(20); 81 | } 82 | 83 | if (self.gridSize() > 200) { 84 | self.gridSize(200); 85 | } 86 | self.gridSize(parseInt(self.gridSize())); 87 | app.initGrid(); 88 | }; 89 | 90 | // Theme 91 | this.theme = ko 92 | .observable(storage.getItem('theme') || 'dracula') 93 | .extend({ persist: 'theme' }); 94 | 95 | // Document type 96 | this.documentType = ko 97 | .observable(storage.getItem('documentType') || 'yarn') 98 | .extend({ persist: 'documentType' }); 99 | 100 | // Language 101 | this.language = ko 102 | .observable(storage.getItem('language') || 'en-GB') 103 | .extend({ persist: 'language' }); 104 | 105 | // Redraw throttle 106 | this.redrawThrottle = ko 107 | .observable(parseInt(storage.getItem('redrawThrottle') || '130')) 108 | .extend({ persist: 'redrawThrottle' }); 109 | 110 | this.gistToken = ko 111 | .observable(storage.getItem('gistToken')) 112 | .extend({ persist: 'gistToken' }); 113 | 114 | this.gistFile = ko 115 | .observable(storage.getItem('gistFile')) 116 | .extend({ persist: 'gistFile' }); 117 | 118 | this.lastEditedGist = ko 119 | .observable(storage.getItem('lastEditedGist')) 120 | .extend({ persist: 'lastEditedGist' }); 121 | 122 | this.lastStorageHost = ko 123 | .observable(storage.getItem('lastStorageHost')) 124 | .extend({ persist: 'lastStorageHost' }); 125 | 126 | this.gistPluginsFile = ko 127 | .observable(storage.getItem('gistPluginsFile')) 128 | .extend({ persist: 'gistPluginsFile' }); 129 | 130 | // Spellcheck enabled 131 | this.spellcheckEnabled = ko 132 | .observable( 133 | storage.getItem('spellcheckEnabled') !== null 134 | ? storage.getItem('spellcheckEnabled') === 'true' 135 | : true 136 | ) 137 | .extend({ persist: 'spellcheckEnabled' }); 138 | 139 | // Auto Close Tags 140 | this.autoCloseTags = ko 141 | .observable( 142 | storage.getItem('autoCloseTags') !== null 143 | ? storage.getItem('autoCloseTags') === 'true' 144 | : true 145 | ) 146 | .extend({ persist: 'autoCloseTags' }); 147 | 148 | // Autocomplete Suggestions 149 | this.autocompleteSuggestionsEnabled = ko 150 | .observable( 151 | storage.getItem('autocompleteSuggestionsEnabled') !== null 152 | ? storage.getItem('autocompleteSuggestionsEnabled') === 'true' 153 | : true 154 | ) 155 | .extend({ persist: 'autocompleteSuggestionsEnabled' }); 156 | 157 | // Auto Close Brackets 158 | this.autoCloseBrackets = ko 159 | .observable( 160 | storage.getItem('autoCloseBrackets') !== null 161 | ? storage.getItem('autoCloseBrackets') === 'true' 162 | : true 163 | ) 164 | .extend({ persist: 'autoCloseBrackets' }); 165 | 166 | // Night mode 167 | this.invertColorsEnabled = ko 168 | .observable( 169 | storage.getItem('invertColorsEnabled') !== null 170 | ? storage.getItem('invertColorsEnabled') === 'true' 171 | : false 172 | ) 173 | .extend({ persist: 'invertColorsEnabled' }); 174 | 175 | // Snap to grid 176 | this.snapGridEnabled = ko 177 | .observable( 178 | storage.getItem('snapGridEnabled') !== null 179 | ? storage.getItem('snapGridEnabled') === 'true' 180 | : false 181 | ) 182 | .extend({ persist: 'snapGridEnabled' }); 183 | 184 | this.restoreSessionEnabled = ko 185 | .observable( 186 | storage.getItem('restoreSessionEnabled') !== null 187 | ? storage.getItem('restoreSessionEnabled') === 'true' 188 | : false 189 | ) 190 | .extend({ persist: 'restoreSessionEnabled' }); 191 | 192 | this.developmentModeEnabled = ko 193 | .observable( 194 | storage.getItem('developmentModeEnabled') !== null 195 | ? storage.getItem('developmentModeEnabled') === 'true' 196 | : false 197 | ) 198 | .extend({ persist: 'developmentModeEnabled' }); 199 | 200 | // Grid size 201 | this.gridSize = ko 202 | .observable(parseInt(storage.getItem('gridSize') || '40')) 203 | .extend({ persist: 'gridSize' }); 204 | 205 | // Autocreate nodes 206 | this.createNodesEnabled = ko 207 | .observable( 208 | storage.getItem('createNodesEnabled') !== null 209 | ? storage.getItem('createNodesEnabled') === 'true' 210 | : true 211 | ) 212 | .extend({ persist: 'createNodesEnabled' }); 213 | 214 | // Editor stats 215 | this.editorStatsEnabled = ko 216 | .observable( 217 | storage.getItem('editorStatsEnabled') !== null 218 | ? storage.getItem('editorStatsEnabled') === 'true' 219 | : false 220 | ) 221 | .extend({ persist: 'editorStatsEnabled' }); 222 | 223 | // Markup language 224 | this.markupLanguage = ko 225 | .observable(storage.getItem('markupLanguage') || 'bbcode') 226 | .extend({ persist: 'markupLanguage' }); 227 | 228 | // Filetype version 229 | this.filetypeVersion = ko 230 | .observable(storage.getItem('filetypeVersion') || '1') 231 | .extend({ persist: 'filetypeVersion' }); 232 | 233 | // Line Style 234 | this.lineStyle = ko 235 | .observable(storage.getItem('lineStyle') || 'straight') 236 | .extend({ persist: 'lineStyle' }); 237 | 238 | this.fileTabsVisible = ko 239 | .observable( 240 | storage.getItem('fileTabsVisible') !== null 241 | ? storage.getItem('fileTabsVisible') === 'true' 242 | : true 243 | ) 244 | .extend({ persist: 'fileTabsVisible' }); 245 | 246 | this.selectedFileTab = ko 247 | .observable(storage.getItem('selectedFileTab') || 0) 248 | .extend({ persist: 'selectedFileTab' }); 249 | 250 | // Always open nodes in Visual Studio Code Editor 251 | // We don't actually show this in the settings menu; it can only be set by the VSCode extension's settings 252 | this.alwaysOpenNodesInVisualStudioCodeEditor = ko 253 | .observable( 254 | storage.getItem('alwaysOpenNodesInVisualStudioCodeEditor') !== null 255 | ? storage.getItem('alwaysOpenNodesInVisualStudioCodeEditor') === 'true' 256 | : false 257 | ) 258 | .extend({ persist: 'alwaysOpenNodesInVisualStudioCodeEditor' }); 259 | 260 | this.editorSplitDirection = ko 261 | .observable(storage.getItem('editorSplitDirection') || 'left') 262 | .extend({ persist: 'editorSplitDirection' }); 263 | 264 | this.editorSplit = ko 265 | .observable( 266 | storage.getItem('editorSplit') !== null 267 | ? storage.getItem('editorSplit') === 'true' 268 | : false 269 | ) 270 | .extend({ persist: 'editorSplit' }); 271 | 272 | this.editorSplitSize = ko 273 | .observable(storage.getItem('editorSplitSize') || '50%') 274 | .extend({ persist: 'editorSplitSize' }); 275 | }; 276 | -------------------------------------------------------------------------------- /src/public/plugins/inkjs/ink-renderer.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events';// todo remove this emitter 2 | const { Story } = inkjs; 3 | 4 | export var inkRender = function() { 5 | let emiter = new EventEmitter(); //todo add signals 6 | this.emiter = emiter; 7 | this.story = null; 8 | this.log = []; 9 | this.onRecompile = () => {}; 10 | 11 | this.curStory = { 12 | messages: [], 13 | choices: [], 14 | tags: [], 15 | paragraphEl: '', 16 | }; 17 | this.resetStory = () => { 18 | this.prevSavePoints = []; 19 | this.choiceHistory = []; 20 | this.textAreaEl.innerHTML = ''; 21 | this.curStory = { 22 | messages: [], 23 | choices: [], 24 | tags: [], 25 | paragraphEl: '', 26 | }; 27 | this.story.ResetState(); 28 | }; 29 | this.terminate = () => { 30 | if (!this.textAreaEl) return; 31 | try { 32 | emiter.removeAllListeners(); 33 | this.finished = true; 34 | } catch (e) { 35 | console.warn(e); 36 | } 37 | }; 38 | 39 | this.setCurStory = ({ messages, choices, tags, paragraphEl }) => { 40 | this.curStory = { messages, choices, tags, paragraphEl }; 41 | }; 42 | const getMessage = _story => { 43 | let message = []; 44 | while (_story.canContinue) { 45 | message.push(_story.Continue().replace(/\n/g, '')); 46 | } 47 | return message; 48 | }; 49 | 50 | const continueStory = (choiceLabel = '', choicePath = '') => { 51 | const paragraph = getMessage(this.story); 52 | const gotoFindQuery = choiceLabel.includes('"') 53 | ? choiceLabel 54 | : `[${choiceLabel}]`; 55 | 56 | const paragraphEl = document.createElement('p'); 57 | if (choiceLabel) { 58 | const paragraphElementTitle = document.createElement('p'); 59 | paragraphElementTitle.innerHTML = ``; 60 | paragraphElementTitle.onclick = () => 61 | app.navigateToNodeDuringPlayTest(choicePath, gotoFindQuery); 62 | 63 | paragraphEl.appendChild(paragraphElementTitle); 64 | paragraph.forEach(p => { 65 | const message = document.createElement('p'); 66 | message.innerHTML = `${p}
`; 67 | paragraphEl.appendChild(message); 68 | }); 69 | } else { 70 | paragraphEl.innerHTML = paragraph.join('
'); 71 | } 72 | 73 | this.setCurStory({ 74 | ...this.curStory, 75 | messages: this.log 76 | ? [ 77 | ...this.curStory.messages, 78 | choiceLabel ? `--${choiceLabel}--` : '', 79 | paragraph, 80 | ].filter(Boolean) 81 | : [paragraph], 82 | tags: this.story.currentTags, 83 | choices: this.story.currentChoices, 84 | paragraphEl, 85 | }); 86 | 87 | updateText(); 88 | }; 89 | 90 | this.prevSavePoints = []; 91 | const getChoice = (index, label) => { 92 | this.prevSavePoints.push(this.story.state.toJson()); 93 | const choicePath = this.story.state.currentChoices[index].sourcePath.split( 94 | '.' 95 | )[0]; 96 | this.story.ChooseChoiceIndex(index); 97 | continueStory(label, choicePath); 98 | }; 99 | this.rewindStory = () => { 100 | document.getElementById('choiceButtons').remove(); 101 | this.textAreaEl.removeChild(this.textAreaEl.lastElementChild); 102 | this.story.state.LoadJson(this.prevSavePoints.pop()); 103 | continueStory(); 104 | }; 105 | 106 | this.createAndAddParagraph = child => { 107 | console.log('made', child); 108 | if (child.innerHTML) { 109 | const paragraph = document.createElement('p'); 110 | paragraph.appendChild(child); 111 | paragraph.className = 112 | 'story-playtest-bubble story-playtest-answer answer-post fade-in is-paused'; 113 | this.textAreaEl.appendChild(paragraph); 114 | $(paragraph).removeClass('is-paused'); 115 | } 116 | }; 117 | // html update stuff 118 | const updateText = () => { 119 | this.createAndAddParagraph(this.curStory.paragraphEl); 120 | this.textAreaEl.querySelectorAll('div').forEach(n => n.remove()); 121 | const btnWrapper = document.createElement('div'); 122 | btnWrapper.id = 'choiceButtons'; 123 | btnWrapper.className = 'flex-wrap'; 124 | // Debug tools 125 | const reloadBtn = document.createElement('button'); 126 | reloadBtn.innerText = '🔄'; 127 | reloadBtn.title = 'Recompile story'; 128 | reloadBtn.onclick = this.onRecompile; 129 | reloadBtn.className = 'storyPreviewChoiceButton'; 130 | btnWrapper.appendChild(reloadBtn); 131 | const restartBtn = document.createElement('button'); 132 | restartBtn.innerText = '🎬'; //'🔄'; 133 | restartBtn.title = 'Restart story'; 134 | restartBtn.onclick = () => { 135 | this.resetStory(); 136 | continueStory(); 137 | }; 138 | restartBtn.className = 'storyPreviewChoiceButton'; 139 | btnWrapper.appendChild(restartBtn); 140 | const rewindBtn = document.createElement('button'); 141 | rewindBtn.innerText = '⏪'; 142 | rewindBtn.title = 'Go to previous'; 143 | rewindBtn.disabled = this.prevSavePoints.length === 0; 144 | rewindBtn.onclick = () => { 145 | this.rewindStory(); 146 | continueStory(); 147 | }; 148 | btnWrapper.appendChild(rewindBtn); 149 | rewindBtn.className = 'storyPreviewChoiceButton'; 150 | // choices 151 | this.curStory.choices.forEach((choice, index) => { 152 | const btn = document.createElement('button'); 153 | btn.innerText = choice.text; 154 | btn.onclick = e => { 155 | e.stopPropagation(); 156 | getChoice(index, choice.text); 157 | }; 158 | btn.className = 'storyPreviewChoiceButton'; 159 | btnWrapper.appendChild(btn); 160 | }); 161 | this.textAreaEl.appendChild(btnWrapper); 162 | 163 | this.textAreaEl.scrollTo({ 164 | top: this.textAreaEl.scrollHeight + 100, 165 | left: 0, 166 | behavior: 'smooth', 167 | }); 168 | }; 169 | 170 | this.initInk = ( 171 | compiler, 172 | onRecompile, 173 | prevSession, 174 | inkTextData, 175 | startChapter, 176 | htmlIdToAttachTo, 177 | resourcesPath, 178 | debugLabelId, 179 | playtestStyle, 180 | playtestVariables //TODO 181 | ) => { 182 | this.onRecompile = onRecompile; 183 | console.log('INIT INK'); 184 | this.finished = false; 185 | document.getElementById(htmlIdToAttachTo).style.visibility = 'hidden'; 186 | this.textAreaEl = document.getElementById(debugLabelId); 187 | this.textAreaEl.innerHTML = 188 | '

Parsing ink

.

.

.

'; 189 | 190 | this.inkTextData = inkTextData; 191 | this.compiler = compiler; 192 | 193 | this.compiler 194 | .init(response => { 195 | this.textAreaEl.innerHTML = ''; 196 | if (response.errors.length > 0) { 197 | this.textAreaEl.innerHTML = `

Parsing failed:

>

${response.errors.join( 198 | '

' 199 | )}


${response.warnings.join('

')}

`; 200 | this.textAreaEl.onclick = () => { 201 | console.log('====>', response); 202 | app.data.goToErrorInkNode(this.inkTextData, response.errors[0]); 203 | this.textAreaEl.onclick = null; 204 | }; 205 | return; 206 | } 207 | if (response.warnings.length > 0) { 208 | const warningsEl = document.createElement('p'); 209 | warningsEl.className = 'title-warning'; 210 | response.warnings.forEach(warning => { 211 | const warningEl = document.createElement('p'); 212 | warningEl.innerText = warning; 213 | warningEl.onclick = () => { 214 | app.data.goToErrorInkNode(this.inkTextData, warning); 215 | }; 216 | warningsEl.appendChild(warningEl); 217 | }); 218 | this.createAndAddParagraph(warningsEl); 219 | } 220 | this.story = new Story(response.story); 221 | console.log('STORY', this.story); 222 | console.warn('Warnings', response.warnings); 223 | continueStory(); 224 | 225 | //Try to restart story from specific chapter when there is no start chapter specified in the global scope 226 | if ( 227 | this.story.currentText === '' && 228 | this.story.currentChoices.length === 0 229 | ) { 230 | if (startChapter !== app.data.InkGlobalScopeNodeName) { 231 | this.compiler.submit(`-> ${startChapter}\n` + inkTextData); 232 | } else { 233 | const firstFoundNode = inkTextData 234 | .split('\n') 235 | .find(line => line.includes('===')); 236 | this.compiler.submit( 237 | `-> ${firstFoundNode.split('===')[1]}\n` + inkTextData 238 | ); 239 | } 240 | } 241 | }) 242 | .then(() => { 243 | if ( 244 | !prevSession.recompile && 245 | prevSession.story && 246 | prevSession.prevSavePoints.length !== 0 247 | ) { 248 | this.story = prevSession.story; 249 | prevSession.childNodes.forEach(child => 250 | this.textAreaEl.appendChild(child) 251 | ); 252 | this.prevSavePoints = prevSession.prevSavePoints; 253 | continueStory(); 254 | return; 255 | } 256 | if (inkTextData) this.compiler.submit(inkTextData); 257 | }); 258 | }; 259 | }; 260 | -------------------------------------------------------------------------------- /src/scss/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.3 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /** 8 | * Correct `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | main, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | 26 | /** 27 | * Correct `inline-block` display not defined in IE 8/9. 28 | */ 29 | 30 | audio, 31 | canvas, 32 | video { 33 | display: inline-block; 34 | } 35 | 36 | /** 37 | * Prevent modern browsers from displaying `audio` without controls. 38 | * Remove excess height in iOS 5 devices. 39 | */ 40 | 41 | audio:not([controls]) { 42 | display: none; 43 | height: 0; 44 | } 45 | 46 | /** 47 | * Address `[hidden]` styling not present in IE 8/9. 48 | * Hide the `template` element in IE, Safari, and Firefox < 22. 49 | */ 50 | 51 | [hidden], 52 | template { 53 | display: none; 54 | } 55 | 56 | /* ========================================================================== 57 | Base 58 | ========================================================================== */ 59 | 60 | /** 61 | * 1. Set default font family to sans-serif. 62 | * 2. Prevent iOS text size adjust after orientation change, without disabling 63 | * user zoom. 64 | */ 65 | 66 | html { 67 | font-family: sans-serif; /* 1 */ 68 | -ms-text-size-adjust: 100%; /* 2 */ 69 | -webkit-text-size-adjust: 100%; /* 2 */ 70 | } 71 | 72 | /** 73 | * Remove default margin. 74 | */ 75 | 76 | body { 77 | margin: 0; 78 | } 79 | 80 | /* ========================================================================== 81 | Links 82 | ========================================================================== */ 83 | 84 | /** 85 | * Remove the gray background color from active links in IE 10. 86 | */ 87 | 88 | a { 89 | background: transparent; 90 | } 91 | 92 | /** 93 | * Address `outline` inconsistency between Chrome and other browsers. 94 | */ 95 | 96 | a:focus { 97 | outline: thin dotted; 98 | } 99 | 100 | /** 101 | * Improve readability when focused and also mouse hovered in all browsers. 102 | */ 103 | 104 | a:active, 105 | a:hover { 106 | outline: 0; 107 | } 108 | 109 | /* ========================================================================== 110 | Typography 111 | ========================================================================== */ 112 | 113 | /** 114 | * Address variable `h1` font-size and margin within `section` and `article` 115 | * contexts in Firefox 4+, Safari 5, and Chrome. 116 | */ 117 | 118 | h1 { 119 | font-size: 2em; 120 | margin: 0.67em 0; 121 | } 122 | 123 | /** 124 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 125 | */ 126 | 127 | abbr[title] { 128 | border-bottom: 1px dotted; 129 | } 130 | 131 | /** 132 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 133 | */ 134 | 135 | b, 136 | strong { 137 | font-weight: bold; 138 | } 139 | 140 | /** 141 | * Address styling not present in Safari 5 and Chrome. 142 | */ 143 | 144 | dfn { 145 | font-style: italic; 146 | } 147 | 148 | /** 149 | * Address differences between Firefox and other browsers. 150 | */ 151 | 152 | hr { 153 | -moz-box-sizing: content-box; 154 | box-sizing: content-box; 155 | height: 0; 156 | } 157 | 158 | /** 159 | * Address styling not present in IE 8/9. 160 | */ 161 | 162 | mark { 163 | background: #ff0; 164 | color: #000; 165 | } 166 | 167 | /** 168 | * Correct font family set oddly in Safari 5 and Chrome. 169 | */ 170 | 171 | code, 172 | kbd, 173 | pre, 174 | samp { 175 | font-family: monospace, serif; 176 | font-size: 1em; 177 | } 178 | 179 | /** 180 | * Improve readability of pre-formatted text in all browsers. 181 | */ 182 | 183 | pre { 184 | white-space: pre-wrap; 185 | } 186 | 187 | /** 188 | * Set consistent quote types. 189 | */ 190 | 191 | q { 192 | quotes: "\201C" "\201D" "\2018" "\2019"; 193 | } 194 | 195 | /** 196 | * Address inconsistent and variable font size in all browsers. 197 | */ 198 | 199 | small { 200 | font-size: 80%; 201 | } 202 | 203 | /** 204 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 205 | */ 206 | 207 | sub, 208 | sup { 209 | font-size: 75%; 210 | line-height: 0; 211 | position: relative; 212 | vertical-align: baseline; 213 | } 214 | 215 | sup { 216 | top: -0.5em; 217 | } 218 | 219 | sub { 220 | bottom: -0.25em; 221 | } 222 | 223 | /* ========================================================================== 224 | Embedded content 225 | ========================================================================== */ 226 | 227 | /** 228 | * Remove border when inside `a` element in IE 8/9. 229 | */ 230 | 231 | img { 232 | border: 0; 233 | } 234 | 235 | /** 236 | * Correct overflow displayed oddly in IE 9. 237 | */ 238 | 239 | svg:not(:root) { 240 | overflow: hidden; 241 | } 242 | 243 | /* ========================================================================== 244 | Figures 245 | ========================================================================== */ 246 | 247 | /** 248 | * Address margin not present in IE 8/9 and Safari 5. 249 | */ 250 | 251 | figure { 252 | margin: 0; 253 | } 254 | 255 | /* ========================================================================== 256 | Forms 257 | ========================================================================== */ 258 | 259 | /** 260 | * Define consistent border, margin, and padding. 261 | */ 262 | 263 | fieldset { 264 | border: 1px solid #c0c0c0; 265 | margin: 0 2px; 266 | padding: 0.35em 0.625em 0.75em; 267 | } 268 | 269 | /** 270 | * 1. Correct `color` not being inherited in IE 8/9. 271 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 272 | */ 273 | 274 | legend { 275 | border: 0; /* 1 */ 276 | padding: 0; /* 2 */ 277 | } 278 | 279 | /** 280 | * 1. Correct font family not being inherited in all browsers. 281 | * 2. Correct font size not being inherited in all browsers. 282 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 283 | */ 284 | 285 | button, 286 | input, 287 | select, 288 | textarea { 289 | font-family: inherit; /* 1 */ 290 | font-size: 100%; /* 2 */ 291 | margin: 0; /* 3 */ 292 | } 293 | 294 | /** 295 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 296 | * the UA stylesheet. 297 | */ 298 | 299 | button, 300 | input { 301 | line-height: normal; 302 | } 303 | 304 | /** 305 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 306 | * All other form control elements do not inherit `text-transform` values. 307 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 308 | * Correct `select` style inheritance in Firefox 4+ and Opera. 309 | */ 310 | 311 | button, 312 | select { 313 | text-transform: none; 314 | } 315 | 316 | /** 317 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 318 | * and `video` controls. 319 | * 2. Correct inability to style clickable `input` types in iOS. 320 | * 3. Improve usability and consistency of cursor style between image-type 321 | * `input` and others. 322 | */ 323 | 324 | button, 325 | html input[type="button"], /* 1 */ 326 | input[type="reset"], 327 | input[type="submit"] { 328 | -webkit-appearance: button; /* 2 */ 329 | cursor: pointer; /* 3 */ 330 | } 331 | 332 | /** 333 | * Re-set default cursor for disabled elements. 334 | */ 335 | 336 | button[disabled], 337 | html input[disabled] { 338 | cursor: default; 339 | } 340 | 341 | /** 342 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 343 | * 2. Remove excess padding in IE 8/9/10. 344 | */ 345 | 346 | input[type="checkbox"], 347 | input[type="radio"] { 348 | box-sizing: border-box; /* 1 */ 349 | padding: 0; /* 2 */ 350 | } 351 | 352 | /** 353 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 354 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 355 | * (include `-moz` to future-proof). 356 | */ 357 | 358 | input[type="search"] { 359 | -webkit-appearance: textfield; /* 1 */ 360 | -moz-box-sizing: content-box; 361 | -webkit-box-sizing: content-box; /* 2 */ 362 | box-sizing: content-box; 363 | } 364 | 365 | /** 366 | * Remove inner padding and search cancel button in Safari 5 and Chrome 367 | * on OS X. 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Remove inner padding and border in Firefox 4+. 377 | */ 378 | 379 | button::-moz-focus-inner, 380 | input::-moz-focus-inner { 381 | border: 0; 382 | padding: 0; 383 | } 384 | 385 | /** 386 | * 1. Remove default vertical scrollbar in IE 8/9. 387 | * 2. Improve readability and alignment in all browsers. 388 | */ 389 | 390 | textarea { 391 | overflow: auto; /* 1 */ 392 | vertical-align: top; /* 2 */ 393 | } 394 | 395 | /* ========================================================================== 396 | Tables 397 | ========================================================================== */ 398 | 399 | /** 400 | * Remove most spacing between table cells. 401 | */ 402 | 403 | table { 404 | border-collapse: collapse; 405 | border-spacing: 0; 406 | } 407 | -------------------------------------------------------------------------------- /src/public/plugins/runner.js: -------------------------------------------------------------------------------- 1 | import { yarnRender } from './bondage/renderer'; 2 | import { inkRender } from './inkjs/ink-renderer'; 3 | const { JSONEditor } = require('./jsoneditor/jsoneditor'); 4 | 5 | export var Runner = function({ 6 | app, 7 | createButton, 8 | addSettingsItem, 9 | getPluginStore, 10 | onYarnEditorOpen, 11 | onYarnInPreviewMode, 12 | onYarnSavedNode, 13 | onYarnSetDocumentType, 14 | onKeyUp, 15 | onKeyDown, 16 | onLoad, 17 | setPluginStore, 18 | }) { 19 | const self = this; 20 | this.name = 'Runner'; 21 | 22 | // Variables editor dialog 23 | this.onOpenDialog = async () => { 24 | let editor = null; 25 | const { value: formValues } = await Swal.fire({ 26 | title: 'Playtest starting variables', 27 | html: '
', 28 | focusConfirm: false, 29 | customClass: 'swal-wide', 30 | onOpen: () => { 31 | // create the editor 32 | require('./jsoneditor/size-overrides.css'); 33 | editor = new JSONEditor({ id: 'jsoneditor' }); 34 | const localVariables = getPluginStore(self.name); 35 | app.log({ editor }); 36 | // set json 37 | editor.setValue( 38 | typeof localVariables.variables !== 'object' 39 | ? [{ key: 'er', value: 'erd' }] 40 | : localVariables.variables 41 | ); 42 | setPluginStore(self.name, 'runnerVariablesOpen', true); 43 | }, 44 | preConfirm: () => { 45 | setPluginStore(self.name, 'runnerVariablesOpen', false); 46 | return editor.getValue(); 47 | }, 48 | }); 49 | 50 | if (formValues) { 51 | setPluginStore(self.name, 'variables', formValues); 52 | } 53 | }; 54 | 55 | onLoad(() => { 56 | // add actions 57 | addSettingsItem({ 58 | title: 'Playtesting Style', 59 | valueKey: 'playtestStyle', 60 | defaultValue: 'chat', 61 | optionsKey: 'availablePlaytestStyles', 62 | options: [ 63 | { id: 'npc', name: 'Npc bubble' }, 64 | { id: 'chat', name: 'Chat messages' }, 65 | ], 66 | setterKey: 'setPlaytestStyle', 67 | settingsColumn: 'A', 68 | }); 69 | if(app.settings.developmentModeEnabled()) { 70 | // create a button in the file menu 71 | createButton(self.name, { 72 | name: 'Playtest variables', 73 | className: 'item', 74 | attachTo: 'fileMenuDropdown', 75 | onClick: 'onOpenDialog()', 76 | iconName: 'cog', 77 | }); 78 | }; 79 | 80 | const localVariables = getPluginStore(self.name); 81 | if (localVariables.runnerVariablesOpen) self.onOpenDialog(); 82 | }); 83 | 84 | const updateRunnerMode = () => { 85 | // YARN PLAY MODE 86 | if (app.settings.documentType() === 'yarn') { 87 | this.previewStory = new yarnRender(); 88 | 89 | this.gotoLastPlayNode = function() { 90 | if ( 91 | app.editing() && 92 | app.editing().title() !== self.previewStory.node.title 93 | ) { 94 | app.openNodeByTitle(self.previewStory.node.title); 95 | } 96 | app.editor.focus(); 97 | }; 98 | 99 | this.advanceStoryPlayMode = function(speed = 5) { 100 | if (!self.previewStory.finished) { 101 | self.previewStory.changeTextScrollSpeed(speed); 102 | if (self.previewStory.vnSelectedChoice != -1 && speed === 5) { 103 | self.previewStory.vnSelectChoice(); 104 | } 105 | } else { 106 | self.togglePlayMode(false); 107 | self.gotoLastPlayNode(); 108 | } 109 | }; 110 | 111 | this.togglePlayMode = function(playModeOverwrite = false) { 112 | const editor = $('.editor')[0]; 113 | const storyPreviewPlayButton = document.getElementById( 114 | 'storyPlayButton' 115 | ); 116 | const editorPlayPreviewer = document.getElementById('editor-play'); 117 | $('#editor-play').addClass('inYarnMode'); 118 | $('#commandDebugLabel').addClass('inYarnMode'); 119 | app.isEditorInPlayMode(playModeOverwrite); 120 | if (playModeOverwrite) { 121 | //preview play mode 122 | editor.style.display = 'none'; 123 | $('.bbcode-toolbar').addClass('hidden'); 124 | editorPlayPreviewer.style.display = 'flex'; 125 | $(storyPreviewPlayButton).addClass('disabled'); 126 | self.previewStory.emiter.on('finished', function() { 127 | self.togglePlayMode(false); 128 | self.gotoLastPlayNode(); 129 | }); 130 | self.previewStory.emiter.on('startedNode', function(e) { 131 | if (app.isEditorSplit) { 132 | app.workspace.warpToNode( 133 | app.getFirstFoundNode(e.title.toLowerCase().trim()) 134 | ); 135 | } 136 | }); 137 | const localVariables = getPluginStore(self.name); 138 | app.log('variables', localVariables); 139 | app.data.getSaveData('json').then(saveData => { 140 | self.previewStory.initYarn( 141 | JSON.parse(saveData), 142 | app 143 | .editing() 144 | .title() 145 | .trim(), 146 | 'NVrichTextLabel', 147 | false, 148 | 'commandDebugLabel', 149 | app.settings.playtestStyle(), 150 | localVariables.variables || [] 151 | ); 152 | }); 153 | } else { 154 | //edit mode 155 | app.editor.session.setScrollTop(editorPlayPreviewer.scrollTop); 156 | editorPlayPreviewer.style.display = 'none'; 157 | editor.style.display = 'flex'; 158 | $(storyPreviewPlayButton).removeClass('disabled'); 159 | $('.bbcode-toolbar').removeClass('hidden'); 160 | $('.toggle-toolbar').removeClass('hidden'); 161 | $('.editor-counter').removeClass('hidden'); 162 | self.previewStory.terminate(); 163 | } 164 | }; 165 | 166 | onYarnInPreviewMode(() => self.togglePlayMode(false)); 167 | onYarnSavedNode(() => self.togglePlayMode(false)); 168 | 169 | onYarnEditorOpen(() => { 170 | createButton(self.name, { 171 | iconName: 'play', 172 | title: 'Preview', 173 | attachTo: 'bbcodeToolbar', 174 | onClick: 'togglePlayMode(true)', 175 | className: 'bbcode-button bbcode-button-right', 176 | id: 'storyPlayButton', 177 | }); 178 | 179 | const element = document.createElement('div'); 180 | element.innerHTML = ` 181 |
182 |

183 |
184 |
185 | `; 186 | document.getElementById('editorContainer').appendChild(element); 187 | 188 | onKeyDown(e => { 189 | if (!app.editing() || self.previewStory.finished) return; 190 | switch (e.keyCode) { 191 | case app.input.keys.Z: 192 | self.previewStory.changeTextScrollSpeed(10); 193 | if (self.previewStory.vnSelectedChoice != -1) 194 | self.previewStory.vnSelectChoice(); 195 | break; 196 | 197 | case app.input.keys.Up: 198 | if (self.previewStory.vnSelectedChoice != -1) 199 | self.previewStory.vnUpdateChoice(-1); 200 | break; 201 | 202 | case app.input.keys.Down: 203 | if (self.previewStory.vnSelectedChoice != -1) 204 | self.previewStory.vnUpdateChoice(1); 205 | break; 206 | } 207 | }); 208 | onKeyUp(e => { 209 | if (e.keyCode === app.input.keys.Z) { 210 | self.previewStory.changeTextScrollSpeed(200); 211 | if (self.previewStory.vnSelectedChoice != -1) 212 | self.previewStory.vnSelectChoice(); 213 | } 214 | }); 215 | }); 216 | } else { 217 | // INKJS PLAY MODE // 218 | this.previewStory = new inkRender(); 219 | this.prevSession = { 220 | story: null, 221 | prevSavePoints: [], 222 | childNodes: [], 223 | recompile: false, 224 | }; 225 | const compiler = new app.data.InkCompiler(); /// 226 | this.togglePlayMode = function(playModeOverwrite = false) { 227 | const editor = $('.editor')[0]; 228 | const storyPreviewPlayButton = document.getElementById( 229 | 'storyPlayButton' 230 | ); 231 | const editorPlayPreviewer = document.getElementById('editor-play'); 232 | app.isEditorInPlayMode(playModeOverwrite); 233 | $('#editor-play').addClass('inInkMode'); 234 | $('#commandDebugLabel').addClass('inInkMode'); 235 | 236 | if (playModeOverwrite) { 237 | //preview play mode 238 | editorPlayPreviewer.style.display = 'flex'; 239 | $('#editor').addClass('editor-take-half'); 240 | 241 | self.previewStory.emiter.on('finished', function() { 242 | self.togglePlayMode(false); 243 | self.gotoLastPlayNode(); 244 | }); 245 | self.previewStory.emiter.on('startedNode', function(e) { 246 | if (app.isEditorSplit) { 247 | app.workspace.warpToNode( 248 | app.getFirstFoundNode(e.title.toLowerCase().trim()) 249 | ); 250 | } 251 | }); 252 | const localVariables = getPluginStore(self.name); 253 | app.log('VARIABLES::::', localVariables); 254 | 255 | app.data.getSaveData('ink', null, true).then(saveData => { 256 | const onRecompile = () => { 257 | self.prevSession = { ...self.prevSession, recompile: true }; 258 | self.togglePlayMode(true); 259 | }; 260 | self.previewStory.initInk( 261 | compiler, 262 | onRecompile, 263 | self.prevSession, 264 | saveData, 265 | app 266 | .editing() 267 | .title() 268 | .trim(), 269 | 'NVrichTextLabel', 270 | false, 271 | 'commandDebugLabel', 272 | app.settings.playtestStyle(), 273 | localVariables.variables || [] 274 | ); 275 | }); 276 | } else { 277 | //edit mode 278 | app.editor.session.setScrollTop(editorPlayPreviewer.scrollTop); 279 | editorPlayPreviewer.style.display = 'none'; 280 | editor.style.display = 'flex'; 281 | $('#editor').removeClass('editor-take-half'); 282 | $(storyPreviewPlayButton).removeClass('disabled'); 283 | 284 | self.prevSession = { 285 | prevSavePoints: self.previewStory.prevSavePoints, 286 | story: self.previewStory.story, 287 | childNodes: self.previewStory.textAreaEl 288 | ? [...self.previewStory.textAreaEl.childNodes] 289 | : [], 290 | recompile: false, 291 | }; 292 | self.previewStory.terminate(); 293 | } 294 | app.editor.resize(); 295 | }; 296 | onYarnInPreviewMode(() => self.togglePlayMode(false)); 297 | onYarnSavedNode(() => self.togglePlayMode(false)); 298 | this.advanceStoryPlayMode = function(speed = 5) {}; 299 | 300 | onYarnEditorOpen(() => { 301 | createButton(self.name, { 302 | iconName: 'play', 303 | title: 'Preview', 304 | attachTo: 'bbcodeToolbar', 305 | onClick: 'togglePlayMode(!app.isEditorInPlayMode())', 306 | className: 'bbcode-button bbcode-button-right', 307 | id: 'storyPlayButton', 308 | }); 309 | 310 | const element = document.createElement('div'); 311 | element.innerHTML = ` 312 |
313 |

314 |
315 |
316 | `; 317 | document.getElementById('editorContainer').appendChild(element); 318 | }); 319 | } 320 | }; 321 | updateRunnerMode(); 322 | //yarnSetDocumentType 323 | onYarnSetDocumentType(updateRunnerMode); 324 | //TODO remove this ugly hack 325 | app.togglePlayMode = this.togglePlayMode; 326 | }; 327 | -------------------------------------------------------------------------------- /src/js/classes/input.js: -------------------------------------------------------------------------------- 1 | import { FILETYPE } from './utils'; 2 | 3 | export const Input = function(app) { 4 | const self = this; 5 | 6 | const MouseButton = Object.freeze({ 7 | Left: 0, 8 | Middle: 1, 9 | Right: 2, 10 | }); 11 | 12 | const Key = Object.freeze({ 13 | Enter: 13, 14 | Escape: 27, 15 | Space: 32, 16 | Left: 37, 17 | Up: 38, 18 | Right: 39, 19 | Down: 40, 20 | Delete: 46, 21 | A: 65, 22 | C: 67, 23 | D: 68, 24 | O: 79, 25 | S: 83, 26 | V: 86, 27 | W: 87, 28 | X: 88, 29 | Y: 89, 30 | Z: 90, 31 | }); 32 | this.keys = Key; 33 | 34 | this.mouse = { x: 0, y: 0 }; 35 | this.isDragging = false; 36 | this.isScreenTouched = false; 37 | this.isMiddleButtonDown = false; 38 | this.isLeftButtonDown = false; 39 | this.isShiftDown = false; 40 | this.isCtrlDown = false; 41 | this.isHoverOverWorkspace = false; 42 | 43 | // trackMouseEvents 44 | // 45 | // Keeps track of mouse/touch events 46 | this.trackMouseEvents = function() { 47 | $(document).on('pointerdown', e => { 48 | self.isDragging = 49 | e.target.className === 'nodes' || 50 | (e.target.className === 'body' && 51 | (self.isMiddleButtonDown || 52 | app.workspace.selectedNodes.length === 0)); 53 | self.mouse.x = e.pageX; 54 | self.mouse.y = e.pageY; 55 | 56 | self.isMiddleButtonDown = e.button === MouseButton.Middle; 57 | this.isLeftButtonDown = e.button === MouseButton.Left; 58 | 59 | if (app.inWorkspace()) { 60 | if (self.isDragging) { 61 | switch (e.button) { 62 | case MouseButton.Left: 63 | if (e.target.className === 'nodes') 64 | app.workspace.onMarqueeStart({ x: e.pageX, y: e.pageY }); 65 | break; 66 | 67 | case MouseButton.Middle: 68 | app.workspace.onDragStart({ x: e.pageX, y: e.pageY }); 69 | break; 70 | } 71 | } 72 | } else if (app.inEditor() && e.button === MouseButton.Right) { 73 | app.guessPopUpHelper(); 74 | e.preventDefault(); 75 | } 76 | }); 77 | 78 | window.addEventListener('touchstart', () => { 79 | self.isScreenTouched = true; 80 | }); 81 | 82 | $(document).on('pointermove', e => { 83 | self.mouse.x = e.pageX; 84 | self.mouse.y = e.pageY; 85 | }); 86 | 87 | $(document).on('mousemove touchmove', e => { 88 | if (self.isDragging) { 89 | app.focusedNodeIdx = -1; 90 | 91 | const pageX = 92 | self.isScreenTouched && e.changedTouches 93 | ? e.changedTouches[0].pageX 94 | : e.pageX; 95 | 96 | const pageY = 97 | self.isScreenTouched && e.changedTouches 98 | ? e.changedTouches[0].pageY 99 | : e.pageY; 100 | 101 | if (app.inWorkspace()) { 102 | if (e.altKey || self.isMiddleButtonDown || self.isScreenTouched) 103 | app.workspace.onDragUpdate({ x: pageX, y: pageY }); 104 | else app.workspace.onMarqueeUpdate({ x: pageX, y: pageY }); 105 | } 106 | } 107 | }); 108 | 109 | $(document).on('pointerup touchend', e => { 110 | self.isScreenTouched = false; 111 | self.isDragging = false; 112 | 113 | if (e.button === MouseButton.Left) self.isLeftButtonDown = false; 114 | 115 | if (e.button === MouseButton.Middle) self.isMiddleButtonDown = false; 116 | 117 | if (app.inWorkspace()) { 118 | app.workspace.onDragEnd(); 119 | app.workspace.onMarqueeEnd(); 120 | } 121 | }); 122 | 123 | $('.nodes').mousewheel(event => { 124 | // https://github.com/InfiniteAmmoInc/Yarn/issues/40 125 | if (event.altKey) return; 126 | 127 | if (app.inWorkspace() || self.isHoverOverWorkspace) { 128 | app.workspace.onZoom(event.pageX, event.pageY, event.deltaY); 129 | event.preventDefault(); 130 | } 131 | }); 132 | 133 | $('.nodes').hover( 134 | () => { 135 | self.isHoverOverWorkspace = true; 136 | }, 137 | () => { 138 | self.isHoverOverWorkspace = false; 139 | } 140 | ); 141 | 142 | $('.nodes').on('pointerdown', () => { 143 | if (app.isEditorSplit) { 144 | app.focusEditor(false); 145 | app.makeNewNodesFromLinks(); 146 | app.propagateUpdateFromNode(app.editing()); 147 | app.mustUpdateTags = true; 148 | app.updateTagsRepository(); 149 | app.workspace.updateArrows(); 150 | } 151 | }); 152 | 153 | $(document).contextmenu(e => { 154 | if (!app.inWorkspace()) return; 155 | 156 | const canSpawn = 157 | $(e.target).hasClass('nodes') || $(e.target).parents('.nodes').length; 158 | 159 | if (e.button === MouseButton.Right && canSpawn) { 160 | const { x, y } = app.workspace.toWorkspaceCoordinates(e.pageX, e.pageY); 161 | app.newNodeAt(x, y); 162 | } 163 | 164 | return !canSpawn; 165 | }); 166 | }; 167 | 168 | // trackKeyboardEvents 169 | // 170 | // Keeps track of keyboard events 171 | this.trackKeyboardEvents = function() { 172 | $(document).on('keyup keydown', e => { 173 | self.isShiftDown = e.shiftKey; 174 | self.isCtrlDown = e.ctrlKey; 175 | }); 176 | 177 | // Workspace/Editor keyboard shortcuts 178 | $(document).on('keyup', function(e) { 179 | if (e.keyCode === Key.Space) { 180 | if ((app.inWorkspace() && e.altKey) || (app.inEditor() && !e.altKey)) 181 | return; 182 | 183 | app.workspace.scale = 1; 184 | 185 | const selectedNodes = app.workspace.getSelectedNodes(); 186 | const isNodeSelected = selectedNodes.length > 0; 187 | const nodes = isNodeSelected > 0 ? selectedNodes : app.nodes(); 188 | 189 | // Cycle focused node 190 | ++app.focusedNodeIdx; 191 | if (app.focusedNodeIdx < 0 || app.focusedNodeIdx >= nodes.length) 192 | app.focusedNodeIdx = 0; 193 | 194 | if (app.inWorkspace()) { 195 | // Spacebar cycles between all or selected nodes 196 | if (isNodeSelected) { 197 | app.workspace.warpToSelectedNodeByIdx(app.focusedNodeIdx); 198 | } else { 199 | app.workspace.warpToNodeByIdx(app.focusedNodeIdx); 200 | } 201 | } else if (app.inEditor()) { 202 | // alt+Spacebar cycles between nodes and edits the focused node 203 | app.editNode(app.nodes()[app.focusedNodeIdx]); 204 | } 205 | } 206 | }); 207 | 208 | // Workspace keyboard shortcuts (keydown) 209 | $(document).on('keydown', e => { 210 | if (!app.inWorkspace()) return; 211 | 212 | if (e.metaKey || e.ctrlKey) { 213 | switch (e.keyCode) { 214 | case Key.S: 215 | e.preventDefault(); 216 | app.data.trySaveCurrent(); 217 | break; // ctrl+s 218 | case Key.A: 219 | if (e.shiftKey) { 220 | e.preventDefault(); 221 | app.data.tryAppend(); 222 | break; 223 | } // ctrl+shift+a 224 | } 225 | } 226 | if ((e.metaKey || e.ctrlKey) && e.altKey) { 227 | switch (e.keyCode) { 228 | case Key.S: 229 | app.data.trySaveCurrent(); 230 | break; // ctrl+alt+s 231 | } 232 | } else if (e.metaKey || e.ctrlKey) { 233 | switch (e.keyCode) { 234 | case Key.C: // ctrl+c 235 | app.nodeClipboard = app.cloneNodeArray( 236 | app.workspace.getSelectedNodes() 237 | ); 238 | break; 239 | case Key.D: 240 | app.workspace.deselectAll(); 241 | break; // ctrl+d 242 | case Key.O: 243 | app.data.tryOpenFile(); 244 | break; // ctrl+o 245 | case Key.S: 246 | app.data.trySaveCurrent(); 247 | break; // ctrl+s 248 | case Key.X: // ctrl+x 249 | const selected = app.workspace.getSelectedNodes(); 250 | app.nodeClipboard = app.cloneNodeArray(selected); 251 | app.deleteNodes(selected); 252 | break; 253 | case Key.Y: 254 | app.historyDirection('redo'); 255 | break; // ctrl+y 256 | case Key.Z: 257 | app.historyDirection('undo'); 258 | break; // ctrl+z 259 | } 260 | } else { 261 | // Delete 262 | if (e.keyCode === Key.Delete || e.key === 'Delete') { 263 | app.confirmDeleteNodes(app.workspace.getSelectedNodes()); 264 | } 265 | // Arrows 266 | else if (!app.$searchField.is(':focus') && !e.ctrlKey && !e.metaKey) { 267 | if (e.keyCode === Key.A || e.keyCode === Key.Left) 268 | // a or left arrow 269 | app.workspace.onPanLeft(); 270 | else if (e.keyCode === Key.D || e.keyCode === Key.Right) 271 | // d or right arrow 272 | app.workspace.onPanRight(); 273 | else if (e.keyCode === Key.W || e.keyCode === Key.Up) 274 | // w or up arrow 275 | app.workspace.onPanUp(); 276 | else if (e.keyCode === Key.S || e.keyCode === Key.Down) 277 | // s or down arrow 278 | app.workspace.onPanDown(); 279 | } 280 | } 281 | }); 282 | 283 | // Workspace keyboard shortcuts (keyup) 284 | $(document).on('keyup', e => { 285 | if (!app.inWorkspace()) return; 286 | 287 | if (e.metaKey || e.ctrlKey) { 288 | switch (e.keyCode) { 289 | case Key.A: 290 | app.workspace.selectAll(); 291 | break; // ctrl+a 292 | case Key.V: 293 | app.pasteNodes(); 294 | break; // ctrl+v 295 | } 296 | } else { 297 | if (e.keyCode === Key.Enter || e.key === 'Enter') { 298 | const activeNode = app.nodes()[app.focusedNodeIdx]; 299 | if (activeNode) app.editNode(activeNode); 300 | else app.editNode(app.nodes()[0]); 301 | } 302 | } 303 | }); 304 | 305 | // Editor keyboard shortcuts (keydown) 306 | $(document).on('keydown', function(e) { 307 | if (!app.inEditor()) return; 308 | 309 | if (e.metaKey || e.ctrlKey) { 310 | switch (e.keyCode) { 311 | case Key.C: 312 | self.clipboard = app.editor.getSelectedText(); 313 | break; 314 | case Key.X: 315 | document.execCommand('copy'); 316 | app.clipboard = app.editor.getSelectedText(); 317 | app.insertTextAtCursor(''); 318 | break; 319 | case Key.S: 320 | app.data.trySaveCurrent(); 321 | break; 322 | } 323 | } else { 324 | switch (e.keyCode) { 325 | case Key.Escape: 326 | app.saveNode(); 327 | app.closeEditor(); 328 | break; 329 | } 330 | } 331 | }); 332 | 333 | // Editor keyboard shortcuts (keup) 334 | $(document).on('keyup', function(e) { 335 | if (!app.inEditor()) return; 336 | 337 | if ((e.metaKey || e.ctrlKey) && e.altKey) { 338 | switch (e.keyCode) { 339 | case Key.Enter: 340 | app.saveNode(); 341 | app.closeEditor(); 342 | break; //ctrl+alt+enter closes/saves an open node 343 | } 344 | } 345 | }); 346 | 347 | // Settings dialog shortcuts 348 | $(document).on('keydown', function(e) { 349 | if (!app.ui.settingsDialogVisible()) return; 350 | 351 | switch (e.keyCode) { 352 | case Key.Escape: 353 | app.ui.closeSettingsDialog(); 354 | break; 355 | } 356 | }); 357 | 358 | $(document).on('keyup keydown pointerdown pointerup', function(e) { 359 | if (!app.inEditor()) return; 360 | 361 | app.updateEditorStats(); 362 | }); 363 | }; 364 | 365 | // initKnockoutBindings 366 | // 367 | // Enables "preventBubble" and "mousedown" bindings on the .html 368 | this.initKnockoutBindings = function() { 369 | ko.bindingHandlers.preventBubble = { 370 | init: function(element, valueAccessor) { 371 | var eventName = ko.utils.unwrapObservable(valueAccessor()); 372 | ko.utils.registerEventHandler(element, eventName, function(event) { 373 | event.cancelBubble = true; 374 | if (event.stopPropagation) event.stopPropagation(); 375 | }); 376 | }, 377 | }; 378 | 379 | ko.bindingHandlers.mousedown = { 380 | init: function( 381 | element, 382 | valueAccessor, 383 | allBindings, 384 | viewModel, 385 | bindingContext 386 | ) { 387 | var value = ko.unwrap(valueAccessor()); 388 | $(element).mousedown(function() { 389 | value(); 390 | }); 391 | }, 392 | }; 393 | }; 394 | 395 | // init 396 | // 397 | // Initializes the input system 398 | const init = function() { 399 | self.initKnockoutBindings(); 400 | self.trackMouseEvents(); 401 | self.trackKeyboardEvents(); 402 | }; 403 | 404 | init(); 405 | }; 406 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚨 This project is going through planned refactoring and re-evaluation of goals. 2 | 3 | This project's playtesting and syntax highlighting is not fully compatible with Yarn Spinner 2. Please use the [Yarn Spinner Extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=SecretLab.yarn-spinner) if you need those features and have vscode instead. 4 | 5 | To learn more about Yarn Spinner 2, please see the [Yarn Spinner documentation](https://docs.yarnspinner.dev). Yarn Spinner 2 is free and open source, and contains fantastic improvements over Yarn Spinner 1! 6 | To learn more, please join the friendly [Yarn Spinner Discord community](https://discord.gg/yarnspinner)! 7 | 8 | This project's future goals are changing! 9 | - While the vscode extension is going to be used by professional developers, this version will remain to target the casual user. It has a much smaller footprint and can run on anything from the browser or as an installed PWA. We will take steps to continue that trend and slim it down 10 | - Ability to save your work in the cloud will remain and be improved, so you can edit your work from a smartphone or a tablet on the go, without having to move files manually. 11 | - Electron version is going to be deprecated in favor of the PWA version. You can still install the PWA version on your computer or mobile device by clicking on the "Add to home screen" button on the bottom. The PWA version will be on par feature wise with electron and have a smaller footprint on the user's computer. This will also make the project easier to maintain. The pwa api of chromium now supports all that it needs 12 | - Extensibility will be improved by introducing a new extension api. The code needs to be simplified and organised a bit. 13 | --- 14 | 15 | ![Build](https://github.com/blurymind/YarnClassic/workflows/Build/badge.svg) 16 | 17 | # Yarn Classic 🐱 🧺 18 | 19 | Dialogue editor created for "Night in the Woods" (and other projects) by @blurymind, @NoelFB and @infinite_ammo with contributions from @seiyria and @beeglebug. It is heavily inspired by and based on the amazing Twine software: http://twinery.org/ 20 | It supports editing, syntax highlighting and testing for [Yarn](https://github.com/YarnSpinnerTool/YarnSpinner) and [InkleStudio Ink files](https://github.com/inkle/ink) syntax files. 21 | It can also export from Yarn to [twine](https://twinery.org/) and use [github gist](https://gist.github.com/)for cloud backup! 22 | 23 | # 🧶 Live Web APP (Use it in the browser) 24 | https://blurymind.github.io/YarnClassic/ 25 |
26 | Yarn web app 28 | 29 | # 📲 Install the App on your compuer or mobile device 30 | 1. Visit https://blurymind.github.io/YarnClassic/ 31 |
with your web browser (chrome, chromium, opera, brave, or edge - any chromium based browser ideally) 32 | 2. Open the web browser's menu and select "Add to home screen" 33 | 3. When you run Yarn from the home screen, it will work in full screen mode, even when you are offline! 34 |
35 | Yarn web app 37 | 38 | # 📁 Supported formats: 39 | - yarn 1 format open/save (json and yarn) Example (playtesting supported) 40 | - ink format open/save Example (playtesting supported) 41 | - renpy format save Example 42 | - twine (twee/twee2) open/save Example 43 | - bbcode and html/xml tag styling support 44 | 45 | # Ability to save to the cloud (gist) 46 | - Examples are hosted on gist https://gist.github.com/blurymind/1252aaa8f74a394b3ac5695107f16e51 to make a share link you do: 47 | 48 | https://blurymind.github.io/YarnClassic/?gist=gistIdHere&fileName=yourFilename.ext 49 | 50 | with gistId=1252aaa8f74a394b3ac5695107f16e51 and fileName=yarnExample.json in that gist, you do: 51 | 52 | https://blurymind.github.io/YarnClassic/?gist=1252aaa8f74a394b3ac5695107f16e51&fileName=yarnExample.json 53 | 54 | Just replace this with your private or public gist and the file in it you want to share - to create a link 55 | 56 | # 💻 Install it on your desktop 57 | Please follow the steps for the PWA version of the app, as the electron one will be deprecated later this year 58 | 59 | # 🚧 Roadmap 60 | You can see planned features, vote for features or see how you can contribute at the roadmap here: 61 | https://trello.com/b/ZXhhOzDl/yarn-roadmap 62 | 63 | # 🎮 Game engines that bundle Yarn Classic 64 | There are a few game engines that have Yarn Classic bundled with their IDE. That means that you can use it straight in those engines, without need to save files and open files and so on. It's directly integratedin their workflow! 65 | 66 | - Gdevelop : A full-featured, open-source game development software, allowing to create HTML5 and native games without any knowledge in a specific programming language. All the game logic is built up using an intuitive and powerful event-based system. 67 | https://github.com/4ian/GDevelop 68 | 69 | 70 | # 🧠 Yarn runtimes 71 | The runtime is a library that allows your game engine to parse the files that yarn creates. There are a couple of popular ones that you can use. If you have created a runtime, you are working on one or want to get one included with your game engine, these can be of some use to you 72 | 73 | - YarnSpinner : A C# library for interactive dialogue in games! Ideal if you are using Unity3d or another C# game engine! 74 | https://github.com/YarnSpinnerTool/YarnSpinner 75 | 76 | - Bondagejs : A Javascript-based parser for the Yarn dialogue tree markup language. Ideal if your game engine uses html5 technologies to run (Gdevelop and ctjs use it). 77 | https://github.com/hylyh/bondage.js 78 | 79 | - GDYarn : A Yarn runtime for Godot engine - completely written in Gdscript 80 | https://github.com/kyperbelt/GDYarn 81 | 82 | - Chatterbox : Yarn runtime implementation for Game maker 2+ 83 | https://github.com/JujuAdams/Chatterbox 84 | 85 | # Other implementations of Yarn Editor 86 | - Crochet : Fork of YarnClassic 87 | https://github.com/FaultyFunctions/Crochet/ 88 | 89 | - YarnSpinner Loom vscode extension : implementation of YarnClassic as a vscode extension 90 | https://marketplace.visualstudio.com/items?itemName=TranquilMarmot.yarn-spinner-loom 91 | 92 | # 🐬 Features 🦄 93 | 94 | ### Portability 95 | - Yarn Classic has PWA version you can install and run when offline, which has a much smaller footprint than other editors using electron. 96 | - The pwa can run on mobile devices with smaller screens - you can use it on your phone and it's much easier to install. 97 | - There is of course also an electron version of the editor, which is slower on updates but more stable 98 | 99 | ### BBcode and HTML-ish markup styling in editor, Spellchecking, Autocompletion, and more! 100 | - Optional syntax autocompletion (autoclose tags) 101 | - preview of bbcode/html tag effects and goto in trimmed nodes 102 | - optional word guessing and autocompletion 103 | - optional preview bbcode in editor mode 104 | - a color picker (using spectrum.js) to set font color in bbcode 105 | - emoji picker to insert emojis 106 | - nodelink suggestions as you type in the right places 107 | - Night mode - Toggling it will invert all the light colors which the editor currently uses 108 | - A context menu command to visit other nodes via their links in the editor and even create new ones 109 | - Button to go back to the previous edited node. If there is no previous - save and close the current one 110 | ![yarn-0 3 5-newfeatures](https://user-images.githubusercontent.com/6495061/50045609-b646e900-008d-11e9-9f17-2ac6b01908f6.gif) 111 | 112 | ### Language, writing and debugging tools built right into it (Yarn and Ink) 113 | - Ability to playtest [Yarn](https://github.com/YarnSpinnerTool/YarnSpinner) and [Ink](https://github.com/inkle/ink) stories you are working on - straight inside the editor 114 | ![Screenshot](doc/inkPlaytest.gif) 115 | - Spellchecking of words (supported for different languages too) 116 | - misspelled word suggestions in the new context menu - if you have selected a misspelled word 117 | - Similar word suggestion for highlighted words (supported for english only) 118 | - Transcribe text - ability to "talk" to yarn without using hands :o (multi-language supported) 119 | - Yarn can also talk to you - tell you what is written (multi-language supported) 120 | - Support for compilation and debugging of ink files via the wasm port inklecate. It catches the error and opens the knot containing it when exporting or testing! 121 | ![Screenshot](doc/inkErrorReporting.gif) 122 | 123 | ### Load and Save your yarns from your computer, github gists or anything you can send it to on your tablet/phone 124 | - A variety of export formats supported - yarn, json yarn, twee, twee2, xml 125 | - When used from a mobile device, yarn can send its data to any other app, including to google drive 126 | - Ability to store and load all your yarns using a github gist - private or public, doesn't matter ;) 127 | 128 | ### Customization! 129 | - Support for different themes (you can make your own too) 130 | 131 | # ⚙️ Compile and run web app on localhost: 132 | Make sure you have nodejs installed. Then from the root folder 133 | ``` 134 | npm install 135 | 136 | npm start 137 | ``` 138 | You can access it on your smartphone too if it is on the same wifi network 139 | 140 | # To build web app: 141 | ``` 142 | npm run build 143 | 144 | ``` 145 | you will find it in the /dist folder 146 | 147 | # To compile and run electron app: 148 | First of all you need to have compiled the web app (see previous steps) 149 | ``` 150 | cd electron 151 | 152 | npm install 153 | 154 | npm start 155 | ``` 156 | 157 | # To build an electron yarn executable yourself: 158 | ``` 159 | cd electron 160 | 161 | npm run build-windows 162 | 163 | or 164 | 165 | npm run build-linux 166 | ``` 167 | 168 | # 😮 Examples 169 | 170 | Games built using Yarn. 171 | 172 | A short hike: https://store.steampowered.com/app/1055540/A_Short_Hike/ 173 | 174 | ![Screenshot](https://github.com/blurymind/YarnClassic/raw/master/doc/games/ash.png) 175 | 176 | Night in the woods: https://store.steampowered.com/app/481510/Night_in_the_Woods/ 177 | 178 | ![Screenshot](https://github.com/blurymind/YarnClassic/raw/master/doc/games/nitw.jpg) 179 | 180 | Lost Constellation: http://finji.itch.io/lost-constellation 181 | 182 | ![Screenshot](https://github.com/blurymind/YarnClassic/raw/master/doc/games/lostC.png) 183 | 184 | Knights and Bikes: https://store.steampowered.com/app/592480/Knights_And_Bikes/ 185 | 186 | ![Screenshot](https://github.com/blurymind/YarnClassic/raw/master/doc/games/kab.jpeg) 187 | 188 | Far From Noise by George Batchelor (@georgebatch): https://store.steampowered.com/app/706130/Far_from_Noise/ 189 | 190 | ![Screenshot](https://github.com/blurymind/YarnClassic/raw/master/doc/games/ffn.jpg) 191 | 192 | YarnTest: http://hayley.zone/bondage.js/ 193 | 194 | Test drive your Yarn files here ^ 195 | 196 | # How to Connect Nodes 197 | 198 | Node connections work similar to Twine. 199 | [[ask question|question]] leads to "question" node. 200 | ![Screenshot](https://github.com/blurymind/YarnClassic/raw/master/doc/nodes.png) 201 | 202 | 203 | # How to Import Twine Files 204 | 205 | One way to import Twine files into Yarn is to export a "Twee" file from Twine. (txt format) Open this txt file in Yarn as you would any other file. 206 | 207 | Note: This method of importing will not preserve node locations, just each node's title, body and tags. 208 | 209 | # How to Run Your Dialogue in Unity 210 | 211 | You can find basic Yarn parsing and playback example code here: 212 | 213 | https://github.com/InfiniteAmmoInc/yarn-test 214 | 215 | You can find a more advanced Yarn interpreter here: 216 | 217 | https://github.com/YarnSpinnerTool/YarnSpinner 218 | 219 | # Yarn Icon 220 | 221 | Yarn logo/icon created by @Mr_Alistair. 222 | 223 | ![Icon](https://github.com/blurymind/YarnClassic/raw/master/src/public/icon.png) 224 | -------------------------------------------------------------------------------- /src/js/classes/node.js: -------------------------------------------------------------------------------- 1 | import { Utils } from './utils'; 2 | 3 | let globalNodeIndex = 0; 4 | const ClipNodeTextLength = 225; 5 | 6 | export let Node = function(options = {}) { 7 | const self = this; 8 | 9 | this.titleStyles = [ 10 | 'title-style-1', 11 | 'title-style-2', 12 | 'title-style-3', 13 | 'title-style-4', 14 | 'title-style-5', 15 | 'title-style-6', 16 | 'title-style-7', 17 | 'title-style-8', 18 | 'title-style-9', 19 | ]; 20 | 21 | // primary values 22 | this.index = ko.observable(globalNodeIndex++); 23 | this.title = ko.observable(options.title || app.getUniqueTitle()); 24 | this.tags = ko.observable(options.tags || ''); 25 | this.body = ko.observable(options.body || ''); 26 | this.active = ko.observable(options.active || true); 27 | this.width = 200; 28 | this.height = 200; 29 | this.tempOpacity = null; 30 | this.style = null; 31 | this.colorID = ko.observable(options.colorID || 0); 32 | this.checked = false; 33 | this.selected = false; 34 | this.createX = options.x || null; 35 | this.createY = options.y || null; 36 | this.undoManager = null; 37 | 38 | // const elementIsVisibleInViewport = function ({ top, left, bottom, right }, partiallyVisible = false) { 39 | // const { innerHeight, innerWidth } = window; 40 | // return partiallyVisible 41 | // ? ((top > 0 && top < innerHeight) || 42 | // (bottom > 0 && bottom < innerHeight)) && 43 | // ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth)) 44 | // : top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth; 45 | // }; 46 | 47 | // clippedTags 48 | // 49 | // Returns an array of tags objects with id, style and count 50 | this.clippedTags = ko.computed(function() { 51 | app.updateTagsRepository(); 52 | return Utils.uniqueSplit(self.tags(), ' ') 53 | .map(tag => app.tags().find(e => e.text === tag)) 54 | .filter(item => item); 55 | }, this); 56 | 57 | this.clippedBody = ko.computed(function() { 58 | if (app.ui.isScreenNarrow() && app.editing()) { 59 | return; 60 | } 61 | 62 | app.mustRefreshNodes(); // Trick to be able to refresh nodes 63 | 64 | let result = app.getHighlightedText(this.body()); 65 | result = app.richTextFormatter.richTextToHtml(result); 66 | result = result.substr(0, ClipNodeTextLength); 67 | return result; 68 | }, this); 69 | 70 | // internal cache 71 | this.linkedTo = ko.observableArray(); 72 | this.linkedFrom = ko.observableArray(); 73 | 74 | // reference to element containing us 75 | this.element = null; 76 | 77 | this.canDoubleClick = true; 78 | 79 | this.create = function() { 80 | self.style = window.getComputedStyle($(self.element).get(0)); 81 | 82 | if (self.createX && self.createY) { 83 | self.x(self.createX); 84 | self.y(self.createY); 85 | } else { 86 | let parent = $(self.element).parent(); 87 | self.x( 88 | (-parent.offset().left + $(window).width() / 2 - 100) / 89 | app.workspace.scale 90 | ); 91 | self.y( 92 | (-parent.offset().top + $(window).height() / 2 - 100) / 93 | app.workspace.scale 94 | ); 95 | } 96 | 97 | app.workspace.bringToFront(self.element); 98 | app.workspace.startUpdatingArrows(); 99 | 100 | $(self.element) 101 | .css({ opacity: 0, scale: 0.8, y: '-=80px', rotate: '45deg' }) 102 | .transition( 103 | { 104 | opacity: 1, 105 | scale: 1, 106 | y: '+=80px', 107 | rotate: '0deg', 108 | }, 109 | 250, 110 | 'easeInQuad', 111 | function() { 112 | app.workspace.stopUpdatingArrows(); 113 | app.workspace.updateArrows(); 114 | } 115 | ); 116 | self.drag(); 117 | 118 | // OPEN NODE 119 | $(self.element).on('dblclick', function() { 120 | if (self.canDoubleClick) app.editNode(self); 121 | }); 122 | Utils.addDoubleTapDetector(self.element, function() { 123 | if (self.canDoubleClick) app.editNode(self); 124 | }); 125 | 126 | $(self.element).on('click', function(e) { 127 | if (e.ctrlKey) { 128 | if (self.selected) app.workspace.deselectNodes(self); 129 | else app.workspace.selectNodes(self); 130 | } 131 | }); 132 | }; 133 | 134 | this.setSelected = function(select) { 135 | self.selected = select; 136 | 137 | if (self.selected) $(self.element).addClass('selected'); 138 | else $(self.element).removeClass('selected'); 139 | }; 140 | 141 | this.toggleSelected = function() { 142 | self.setSelected(!self.selected); 143 | }; 144 | 145 | this.x = function(inX) { 146 | if (inX != undefined) $(self.element).css({ x: Math.floor(inX) }); 147 | 148 | // if we don't have a style here, it means this node is in the 149 | // process of being created and self.element doesn't exist yet 150 | if (!self.style) { 151 | return; 152 | } 153 | 154 | // m41 here corresponds to the fourth row and first column of the matrix transform 155 | // this is the X value of the transform 156 | return Math.floor(new WebKitCSSMatrix(self.style.webkitTransform).m41); 157 | }; 158 | 159 | this.y = function(inY) { 160 | if (inY != undefined) $(self.element).css({ y: Math.floor(inY) }); 161 | 162 | // if we don't have a style here, it means this node is in the 163 | // process of being created and self.element doesn't exist yet 164 | if (!self.style) { 165 | return; 166 | } 167 | 168 | // m42 here corresponds to the fourth row and second column of the matrix transform 169 | // this is the X value of the transform 170 | return Math.floor(new WebKitCSSMatrix(self.style.webkitTransform).m42); 171 | }; 172 | 173 | this.resetDoubleClick = function() { 174 | self.canDoubleClick = true; 175 | }; 176 | 177 | this.cycleColorDown = function() { 178 | self.doCycleColorDown(); 179 | 180 | setTimeout(self.resetDoubleClick, 500); 181 | self.canDoubleClick = false; 182 | 183 | if (app.input.isShiftDown) app.matchConnectedColorID(self); 184 | 185 | if (self.selected) app.setSelectedColors(self); 186 | 187 | app.setYarnDocumentIsDirty(); 188 | }; 189 | 190 | this.cycleColorUp = function() { 191 | self.doCycleColorUp(); 192 | 193 | setTimeout(self.resetDoubleClick, 500); 194 | self.canDoubleClick = false; 195 | 196 | if (app.input.isShiftDown) app.matchConnectedColorID(self); 197 | 198 | if (self.selected) app.setSelectedColors(self); 199 | 200 | app.setYarnDocumentIsDirty(); 201 | }; 202 | 203 | this.doCycleColorDown = function() { 204 | self.colorID(self.colorID() - 1); 205 | if (self.colorID() < 0) self.colorID(8); 206 | }; 207 | 208 | this.doCycleColorUp = function() { 209 | self.colorID(self.colorID() + 1); 210 | if (self.colorID() > 8) self.colorID(0); 211 | }; 212 | 213 | this.remove = async function() { 214 | return new Promise((resolve, reject) => { 215 | $(self.element).transition( 216 | { opacity: 0, scale: 0.8, y: '-=80px', rotate: '-45deg' }, 217 | 250, 218 | 'easeInQuad', 219 | resolve 220 | ); 221 | }); 222 | }; 223 | 224 | this.drag = function() { 225 | const offset = { x: 0, y: 0 }; // Where inside the node did the mouse click 226 | let dragging = false; 227 | let groupDragging = false; 228 | 229 | $(document.body).on('mousemove touchmove', function(e) { 230 | if (dragging) { 231 | const pageX = 232 | app.input.isScreenTouched && e.changedTouches 233 | ? e.changedTouches[0].pageX 234 | : e.pageX; 235 | const pageY = 236 | app.input.isScreenTouched && e.changedTouches 237 | ? e.changedTouches[0].pageY 238 | : e.pageY; 239 | 240 | let { x, y } = app.workspace.toWorkspaceCoordinates(pageX, pageY); 241 | 242 | x -= offset.x; 243 | y -= offset.y; 244 | 245 | let movedX = x - self.x(); 246 | let movedY = y - self.y(); 247 | 248 | const nodes = app.workspace.getSelectedNodes(); 249 | // Prevent yarn from moving a node when you scroll its contents on a touch screen 250 | if ( 251 | e.originalEvent.type === 'mousemove' || 252 | (nodes.includes(self) && e.originalEvent.type === 'touchmove') 253 | ) { 254 | self.x(x); 255 | self.y(y); 256 | } 257 | 258 | if (groupDragging) { 259 | if (self.selected) { 260 | nodes.splice(nodes.indexOf(self), 1); 261 | } else { 262 | nodes = app.getNodesConnectedTo(self); 263 | } 264 | 265 | if (nodes.length > 0) { 266 | for (let i in nodes) { 267 | if (nodes[i].active()) { 268 | nodes[i].x(nodes[i].x() + movedX); 269 | nodes[i].y(nodes[i].y() + movedY); 270 | } 271 | } 272 | } 273 | } 274 | 275 | app.workspace.updateArrows(); 276 | } 277 | }); 278 | 279 | $(self.element).on('pointerdown', function(e) { 280 | if (!dragging && self.active() && e.button === 0) { 281 | dragging = true; 282 | 283 | if (app.input.isShiftDown || self.selected) { 284 | groupDragging = true; 285 | } 286 | 287 | const { x, y } = app.workspace.toWorkspaceCoordinates(e.pageX, e.pageY); 288 | 289 | offset.x = app.settings.snapGridEnabled() 290 | ? app.workspace.stepify(x - self.x(), app.settings.gridSize()) 291 | : x - self.x(); 292 | offset.y = app.settings.snapGridEnabled() 293 | ? app.workspace.stepify(y - self.y(), app.settings.gridSize()) 294 | : y - self.y(); 295 | } 296 | }); 297 | 298 | $(self.element).on('touchend', function(e) { 299 | app.workspace.selectNodes(self); 300 | }); 301 | // Make sure dragging stops when cursor is above another element 302 | $(document).on('pointerup touchend', function() { 303 | if (dragging || groupDragging) { 304 | dragging = false; 305 | groupDragging = false; 306 | 307 | // this will tell the VSCode extension that we've moved the node 308 | app.setYarnDocumentIsDirty(); 309 | } 310 | }); 311 | }; 312 | 313 | this.moveTo = function(newX, newY) { 314 | app.workspace.startUpdatingArrows(); 315 | 316 | $(self.element).clearQueue(); 317 | $(self.element).transition( 318 | { 319 | x: newX, 320 | y: newY, 321 | }, 322 | app.stopUpdatingArrows, 323 | 500 324 | ); 325 | }; 326 | 327 | this.isConnectedTo = function(otherNode, checkBack) { 328 | if (checkBack && otherNode.isConnectedTo(self, false)) return true; 329 | 330 | let linkedNodes = self.linkedTo(); 331 | for (let i in linkedNodes) { 332 | if (linkedNodes[i] == otherNode) return true; 333 | if (linkedNodes[i].isConnectedTo(otherNode, false)) return true; 334 | if (otherNode.isConnectedTo(linkedNodes[i], false)) return true; 335 | } 336 | 337 | return false; 338 | }; 339 | 340 | this.getLinksInNode = function(node) { 341 | const isYarnDocument = app.settings.documentType() === 'yarn'; 342 | let links = (node || self) 343 | .body() 344 | .match(isYarnDocument ? /\[\[(.*?)\]\]/g : /\-\>(.*)/g); 345 | 346 | if (links != undefined) { 347 | let exists = {}; 348 | 349 | for (let i = links.length - 1; i >= 0; i--) { 350 | if (isYarnDocument) { 351 | links[i] = links[i].substr(2, links[i].length - 4).trim(); //.toLowerCase(); 352 | 353 | if (links[i].indexOf('|') >= 0) { 354 | links[i] = links[i].split('|')[1]; 355 | } 356 | 357 | if (exists[links[i]] != undefined) { 358 | links.splice(i, 1); 359 | } 360 | exists[links[i]] = true; 361 | } else { 362 | links[i] = links[i].substr(2, links[i].length).trim(); 363 | } 364 | } 365 | return links; 366 | } else { 367 | return undefined; 368 | } 369 | }; 370 | 371 | this.updateLinks = function() { 372 | self.resetDoubleClick(); 373 | self.updateLinksFromParents(); 374 | self.updateLinksToChildren(); 375 | }; 376 | 377 | this.updateLinksFromParents = function() { 378 | // If title didn't change there's nothing we need to update on parents 379 | if (!self.oldTitle || self.oldTitle === self.title()) { 380 | return; 381 | } 382 | 383 | self.linkedFrom.removeAll(); 384 | 385 | app.nodes().forEach(parent => { 386 | const parentLinks = self.getLinksInNode(parent); 387 | if (parentLinks) { 388 | if (parentLinks.includes(self.oldTitle)) { 389 | const re1 = RegExp('\\|\\s*' + self.oldTitle + '\\s*\\]\\]', 'g'); 390 | const re2 = RegExp('\\[\\[\\s*' + self.oldTitle + '\\s*\\]\\]', 'g'); 391 | let newBody = parent.body().replace(re1, '|' + self.title() + ']]'); 392 | newBody = newBody.replace(re2, '[[' + self.title() + ']]'); 393 | parent.body(newBody); 394 | self.linkedFrom.push(parent); 395 | } else if (parentLinks.includes(self.title())) { 396 | self.linkedFrom.push(parent); 397 | } 398 | } 399 | }); 400 | 401 | self.oldTitle = undefined; 402 | }; 403 | 404 | this.updateLinksToChildren = function() { 405 | self.linkedTo.removeAll(); 406 | 407 | let links = self.getLinksInNode(); 408 | 409 | if (!links) { 410 | return; 411 | } 412 | 413 | for (let index in app.nodes()) { 414 | let other = app.nodes()[index]; 415 | for (let i = 0; i < links.length; i++) { 416 | if (other != self && other.title().trim() === links[i].trim()) { 417 | self.linkedTo.push(other); 418 | } 419 | } 420 | } 421 | }; 422 | }; 423 | 424 | ko.bindingHandlers.nodeBind = { 425 | init: function( 426 | element, 427 | valueAccessor, 428 | allBindings, 429 | viewModel, 430 | bindingContext 431 | ) { 432 | bindingContext.$rawData.element = element; 433 | bindingContext.$rawData.create(); 434 | }, 435 | 436 | update: function( 437 | element, 438 | valueAccessor, 439 | allBindings, 440 | viewModel, 441 | bindingContext 442 | ) { 443 | $(element).on('pointerdown', function() { 444 | app.workspace.bringToFront(element); 445 | }); 446 | }, 447 | }; 448 | --------------------------------------------------------------------------------