├── 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 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/deploy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/start.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/YarnClassic.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-to-gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy PWA
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | build-and-deploy:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout 🛎️
11 | uses: actions/checkout@v2.3.1
12 |
13 | - name: Install and Build 🔧
14 | run: |
15 | npm install
16 | npm run-script build
17 |
18 | - name: Deploy 🚀
19 | uses: JamesIves/github-pages-deploy-action@4.1.5
20 | with:
21 | branch: gh-pages
22 | folder: dist
--------------------------------------------------------------------------------
/src/public/templates/node.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
67 |
68 |
69 |
70 |
71 | | key |
72 | val |
73 |
74 | x delete
75 | |
76 |
77 |
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 `${tag}>`;
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 = `${choiceLabel} ( ${choicePath} )
`;
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 | '';
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 |
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 |
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 | 
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 |
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 |
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 | 
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 | 
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 | 
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 | 
175 |
176 | Night in the woods: https://store.steampowered.com/app/481510/Night_in_the_Woods/
177 |
178 | 
179 |
180 | Lost Constellation: http://finji.itch.io/lost-constellation
181 |
182 | 
183 |
184 | Knights and Bikes: https://store.steampowered.com/app/592480/Knights_And_Bikes/
185 |
186 | 
187 |
188 | Far From Noise by George Batchelor (@georgebatch): https://store.steampowered.com/app/706130/Far_from_Noise/
189 |
190 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------