├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ ├── deploy-to-gh-pages.yml │ └── release.yml ├── .gitignore ├── .idea ├── .gitignore ├── YarnClassic.iml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── runConfigurations │ ├── build.xml │ ├── deploy.xml │ └── start.xml └── vcs.xml ├── .prettierrc.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── Yarn.png ├── doc ├── games │ ├── ash.png │ ├── ffn.jpg │ ├── kab.jpeg │ ├── lostC.png │ └── nitw.jpg ├── inkErrorReporting.gif ├── inkPlaytest.gif ├── nodes.png ├── yarnMobile.jpeg └── yarnWebApp.png ├── electron ├── build │ ├── icon.ico │ └── icon.png ├── extraDictionaries │ └── app │ │ └── public │ │ └── dictionaries │ │ ├── de │ │ ├── index.aff │ │ └── index.dic │ │ ├── es │ │ ├── index.aff │ │ └── index.dic │ │ ├── fr │ │ ├── index.aff │ │ └── index.dic │ │ ├── it │ │ ├── index.aff │ │ └── index.dic │ │ └── ru │ │ ├── index.aff │ │ └── index.dic ├── main.js ├── package-lock.json ├── package.json ├── yarn-index.html ├── yarn-main.js ├── yarn-style.css └── yarn.lock ├── package-lock.json ├── package.json ├── scripts └── copy-version.js ├── src ├── index.html ├── js │ ├── classes │ │ ├── app.js │ │ ├── data.js │ │ ├── input.js │ │ ├── node.js │ │ ├── richTextFormatter.js │ │ ├── richTextFormatterBbcode.js │ │ ├── richTextFormatterHtml.js │ │ ├── settings.js │ │ ├── storage.js │ │ ├── ui.js │ │ ├── utils.js │ │ └── workspace.js │ ├── index.js │ └── libs │ │ ├── knockout.ace.js │ │ └── spellcheck_ace.js ├── public │ ├── droid-sans-mono.ttf │ ├── icon.ico │ ├── icon.png │ ├── icons.svg │ ├── images │ │ ├── dropbox.ico │ │ ├── inky-icon.png │ │ ├── pixel.png │ │ ├── renpy-128.png │ │ └── twine-favicon-152.png │ ├── libs │ │ ├── bondage.min.js │ │ ├── ink-full.js │ │ ├── ink-full.js.map │ │ └── uFuzzy.iife.min.js │ ├── mode-ink.js │ ├── mode-yarn.js │ ├── plugins │ │ ├── ace-diff │ │ │ ├── ace-diff-dark.min.css │ │ │ ├── ace-diff.min.css │ │ │ └── ace-diff.min.js │ │ ├── bondage │ │ │ └── renderer.js │ │ ├── index.js │ │ ├── inkjs │ │ │ └── ink-renderer.js │ │ ├── jsoneditor │ │ │ ├── jsoneditor.js │ │ │ └── size-overrides.css │ │ ├── plugin-editor.js │ │ ├── resources-editor.js │ │ └── runner.js │ ├── templates │ │ └── node.html │ ├── theme-ink.js │ ├── theme-yarn.js │ ├── themes │ │ ├── blueprint.css │ │ ├── classic.css │ │ └── dracula.css │ ├── version.json │ └── web-components.js ├── scss │ ├── font │ │ ├── context-menu-icons.eot │ │ ├── context-menu-icons.ttf │ │ ├── context-menu-icons.woff │ │ └── context-menu-icons.woff2 │ ├── jquery.contextMenu.css │ ├── normalize.css │ ├── spectrum.css │ └── style.css └── sw-src.js ├── testFiles ├── Sally.yarn.txt ├── assignment.json ├── bbcodeTags.yarn ├── commandsandfunctions.json ├── conditions.json ├── exportAsRenpyExample.json ├── htmlTags.yarn ├── inkJs │ └── inkSample.ink ├── links.json ├── shortcuts.json ├── simple-xml-example.xml ├── stressTest.yarn └── yarnExample.json ├── webpack.config.js └── yarn.lock /.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 | }; -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/YarnClassic.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/js/classes/richTextFormatterHtml.js: -------------------------------------------------------------------------------- 1 | export const HtmlRichTextFormatter = function(app, addExtraPreviewerEmbeds) { 2 | const self = this; 3 | this.justInsertedAutoComplete = false; 4 | 5 | this.completableTags = Object.freeze([ 6 | { Start: '<<', Completion: '>>', Offset: -2 }, 7 | { 8 | Start: '', 10 | Offset: -9, 11 | Func: () => { 12 | app.insertColorCode(); 13 | }, 14 | }, 15 | { Start: '', Offset: -4 }, 16 | { Start: '', Offset: -6 }, 17 | { Start: '', Offset: -4 }, 18 | { Start: '', Offset: -4 }, 19 | { Start: '', Offset: -6 }, 20 | ]); 21 | 22 | this.getTagOpen = function(tag) { 23 | switch (tag) { 24 | case 'cmd': 25 | return app.settings.documentType() === 'ink' 26 | ? app.editor.getSelectedText().length === 0 27 | ? '~ ' 28 | : '{ ' 29 | : '<<'; 30 | case 'opt': 31 | return app.settings.documentType() === 'ink' 32 | ? app.editor.getSelectedText().length === 0 33 | ? '-> ' 34 | : '* [' 35 | : '[['; 36 | case 'color': 37 | return ''; 38 | default: 39 | return `<${tag}>`; 40 | } 41 | }; 42 | 43 | this.getTagClose = function(tag) { 44 | switch (tag) { 45 | case 'cmd': 46 | return app.settings.documentType() === 'ink' 47 | ? app.editor.getSelectedText().length === 0 48 | ? '' 49 | : ' }' 50 | : '>>'; 51 | case 'opt': 52 | return app.settings.documentType() === 'ink' 53 | ? app.editor.getSelectedText().length === 0 54 | ? '' 55 | : ']' 56 | : '|]]'; 57 | default: 58 | return ``; 59 | } 60 | }; 61 | 62 | this.identifyTag = function(text) { 63 | let tag = 64 | text.lastIndexOf('<') !== -1 65 | ? text.substring(text.lastIndexOf('<'), text.length) 66 | : ''; 67 | return tag; 68 | }; 69 | 70 | this.insertTag = function(tag) { 71 | const tagOpen = self.getTagOpen(tag); 72 | const tagClose = self.getTagClose(tag); 73 | 74 | const selectedRange = JSON.parse( 75 | JSON.stringify(app.editor.selection.getRange()) 76 | ); 77 | 78 | app.editor.session.insert(selectedRange.start, tagOpen); 79 | app.editor.session.insert( 80 | { 81 | column: selectedRange.end.column + tagOpen.length, 82 | row: selectedRange.end.row, 83 | }, 84 | tagClose 85 | ); 86 | 87 | if (tag === 'color') { 88 | if (app.editor.getSelectedText().length === 0) { 89 | app.moveEditCursor(-9); 90 | } else { 91 | app.editor.selection.setRange({ 92 | start: { 93 | row: app.editor.selection.getRange().start.row, 94 | column: app.editor.selection.getRange().start.column - 1, 95 | }, 96 | end: { 97 | row: app.editor.selection.getRange().start.row, 98 | column: app.editor.selection.getRange().start.column - 1, 99 | }, 100 | }); 101 | } 102 | app.insertColorCode(); 103 | } 104 | if (tag === 'img') { 105 | navigator.clipboard.readText().then(text => { 106 | if (app.editor.getSelectedText().length === 0) { 107 | app.moveEditCursor(-7); 108 | app.insertTextAtCursor(` src="${text}"`); 109 | } 110 | }); 111 | } else if (app.editor.getSelectedText().length === 0) { 112 | if (!app.isEditorInPreviewMode) app.moveEditCursor(-tagClose.length); 113 | } else { 114 | app.editor.selection.setRange({ 115 | start: app.editor.selection.getRange().start, 116 | end: { 117 | row: app.editor.selection.getRange().end.row, 118 | column: app.editor.selection.getRange().end.column - tagClose.length, 119 | }, 120 | }); 121 | } 122 | app.editor.focus(); 123 | }; 124 | 125 | this._convertTag = function(inPattern, outPattern, text) { 126 | const globalRegex = new RegExp(inPattern, 'gi'); 127 | const localRegex = new RegExp(inPattern, 'i'); 128 | 129 | return text.replace(globalRegex, m => { 130 | const match = m.match(localRegex); 131 | const template = eval('`' + outPattern + '`'); 132 | return match.length ? template : null; 133 | }); 134 | }; 135 | 136 | this.convert = function(text) { 137 | let result = text; 138 | 139 | result = self._convertTag( 140 | '\\[b\\](.*?)\\[\\/b\\]', 141 | '${match[1]}', 142 | result 143 | ); 144 | result = self._convertTag( 145 | '\\[u\\](.*?)\\[\\/u\\]', 146 | '${match[1]}', 147 | result 148 | ); 149 | result = self._convertTag( 150 | '\\[i\\](.*?)\\[\\/i\\]', 151 | '${match[1]}', 152 | result 153 | ); 154 | result = self._convertTag( 155 | '\\[img\\](.*?)\\[\\/img\\]', 156 | '${match[1]}', 157 | result 158 | ); 159 | result = self._convertTag( 160 | '\\[color=#(.*?)\\](.*?)\\[\\/color\\]', 161 | '${match[2]}', 162 | result 163 | ); 164 | result = self._convertTag( 165 | '\\[url\\](.*?)\\[\\/url\\]', 166 | '${match[1]}', 167 | result 168 | ); 169 | 170 | return result; 171 | }; 172 | 173 | this.richTextToHtml = function(text, showRowNumbers = false) { 174 | let rowCounter = 1; 175 | let result = showRowNumbers 176 | ? '
' + 177 | '' + 178 | rowCounter + 179 | '. ' + 180 | text + 181 | '
' // TODO: style this 182 | : text; 183 | 184 | /// <> 185 | result = result.replace(/<(run:"); // TODO: style this 186 | result = result.replace(/>>/gi, ')
'); 187 | 188 | /// and <color=#...></color> 189 | [ 190 | /<color=#(.*?)>(.*?)<\/color>/, 191 | /(.*?)<\/color>/, 192 | ].forEach(pattern => { 193 | const globalRegex = new RegExp(pattern, 'gi'); 194 | const localRegex = new RegExp(pattern, 'i'); 195 | 196 | result = result.replace(globalRegex, function(colorCode) { 197 | const matches = colorCode.match(localRegex); 198 | if (matches && matches.length > 2) { 199 | return `☗${matches[2]}`; 200 | } 201 | }); 202 | }); 203 | 204 | // local images with path relative to the opened yarn file 205 | result = result.replace(/<img>[^\[]+<\/img>/gi, function( 206 | imgTag 207 | ) { 208 | const extractedImgPath = imgTag.match(/<img>(.*?)<\/img>/i); 209 | if (extractedImgPath.length > 1) { 210 | const fullPathToFile = app.data.editingFileFolder(extractedImgPath[1]); 211 | if (app.data.doesFileExist(fullPathToFile)) { 212 | return showRowNumbers 213 | ? ' ' 214 | : ' '; 217 | } else { 218 | // if not a local file, try to load it as a link 219 | return showRowNumbers 220 | ? ' ' 221 | : ' '; 224 | } 225 | } 226 | }); 227 | 228 | // 229 | result = result.replace(/<b>.*<\/b>/gi, m => { 230 | const content = m.match(/<b>(.*)<\/b>/i); 231 | if (content.length) { 232 | return `${content[1]}`; 233 | } 234 | }); 235 | 236 | // 237 | result = result.replace(/<u>.*<\/u>/gi, m => { 238 | const content = m.match(/<u>(.*)<\/u>/i); 239 | if (content.length) { 240 | return `${content[1]}`; 241 | } 242 | }); 243 | 244 | // 245 | result = result.replace(/<i>.*<\/i>/gi, m => { 246 | const content = m.match(/<i>(.*)<\/i>/i); 247 | if (content.length) { 248 | return `${content[1]}`; 249 | } 250 | }); 251 | 252 | if (showRowNumbers) result = addExtraPreviewerEmbeds(result); 253 | // newLines. Do this last, as we need the newline characters in previous regex tests 254 | result = result.replace(/[\n\r]/g, function(row) { 255 | let rowAppend = '
'; 256 | rowCounter += 1; 257 | if (showRowNumbers) { 258 | rowAppend += 259 | '
' + rowCounter + '. '; 260 | } 261 | return rowAppend; 262 | }); 263 | 264 | /// finaly return the html result 265 | return result; 266 | }; 267 | }; 268 | -------------------------------------------------------------------------------- /src/js/classes/settings.js: -------------------------------------------------------------------------------- 1 | // Get the mechanism to use for storage. 2 | const getStorage = function() { 3 | // if `window.vsCodeApi` exists, we're in the context of the VSCode extension 4 | // which handles all of the settings internally, so we don't need to do anything here 5 | if (window.vsCodeApi) { 6 | return { 7 | getItem: () => {}, 8 | setItem: () => {}, 9 | }; 10 | } else { 11 | return window.localStorage; 12 | } 13 | }; 14 | 15 | export const Settings = function(app) { 16 | const self = this; 17 | const storage = getStorage(); 18 | this.storage = storage; 19 | 20 | ko.extenders.persist = function(target, option) { 21 | target.subscribe(function(newValue) { 22 | storage.setItem(option, newValue); 23 | app.data.db.save(option, newValue); 24 | }); 25 | return target; 26 | }; 27 | 28 | // apply 29 | // 30 | // Applies the current settings 31 | this.apply = function() { 32 | app.setTheme(self.theme()); 33 | app.setLanguage(self.language()); 34 | app.setDocumentType(self.documentType()); 35 | app.toggleInvertColors(); 36 | app.setMarkupLanguage(self.markupLanguage()); 37 | console.log('redraw throttle:', self.redrawThrottle()); 38 | app.workspace.setThrottle(self.redrawThrottle()); 39 | app.setGistCredentials({ 40 | token: self.gistToken(), 41 | file: 42 | self.gistFile() !== null 43 | ? self 44 | .gistFile() 45 | .split('/') 46 | .pop() 47 | : null, 48 | }); 49 | app.setGistPluginsFile( 50 | self.gistPluginsFile() !== null 51 | ? self 52 | .gistPluginsFile() 53 | .split('/') 54 | .pop() 55 | : null 56 | ); 57 | }; 58 | 59 | this.pasteFromClipboard = function(koItem){ 60 | if(koItem && koItem.length > 0) return; 61 | if(!navigator.clipboard) return; 62 | navigator.clipboard 63 | .readText() 64 | .then( 65 | (clipText) => { 66 | app.log("clipboard", clipText) 67 | if(clipText && clipText.length > 5) koItem(clipText) 68 | }, 69 | ); 70 | } 71 | this.guessGistToken = function() { 72 | self.pasteFromClipboard(self.gistToken); 73 | } 74 | this.guessGistFile = function() { 75 | self.pasteFromClipboard(self.gistFile); 76 | } 77 | 78 | this.validateGridSize = function() { 79 | if (self.gridSize() < 20) { 80 | self.gridSize(20); 81 | } 82 | 83 | if (self.gridSize() > 200) { 84 | self.gridSize(200); 85 | } 86 | self.gridSize(parseInt(self.gridSize())); 87 | app.initGrid(); 88 | }; 89 | 90 | // Theme 91 | this.theme = ko 92 | .observable(storage.getItem('theme') || 'dracula') 93 | .extend({ persist: 'theme' }); 94 | 95 | // Document type 96 | this.documentType = ko 97 | .observable(storage.getItem('documentType') || 'yarn') 98 | .extend({ persist: 'documentType' }); 99 | 100 | // Language 101 | this.language = ko 102 | .observable(storage.getItem('language') || 'en-GB') 103 | .extend({ persist: 'language' }); 104 | 105 | // Redraw throttle 106 | this.redrawThrottle = ko 107 | .observable(parseInt(storage.getItem('redrawThrottle') || '130')) 108 | .extend({ persist: 'redrawThrottle' }); 109 | 110 | this.gistToken = ko 111 | .observable(storage.getItem('gistToken')) 112 | .extend({ persist: 'gistToken' }); 113 | 114 | this.gistFile = ko 115 | .observable(storage.getItem('gistFile')) 116 | .extend({ persist: 'gistFile' }); 117 | 118 | this.lastEditedGist = ko 119 | .observable(storage.getItem('lastEditedGist')) 120 | .extend({ persist: 'lastEditedGist' }); 121 | 122 | this.lastStorageHost = ko 123 | .observable(storage.getItem('lastStorageHost')) 124 | .extend({ persist: 'lastStorageHost' }); 125 | 126 | this.gistPluginsFile = ko 127 | .observable(storage.getItem('gistPluginsFile')) 128 | .extend({ persist: 'gistPluginsFile' }); 129 | 130 | // Spellcheck enabled 131 | this.spellcheckEnabled = ko 132 | .observable( 133 | storage.getItem('spellcheckEnabled') !== null 134 | ? storage.getItem('spellcheckEnabled') === 'true' 135 | : true 136 | ) 137 | .extend({ persist: 'spellcheckEnabled' }); 138 | 139 | // Auto Close Tags 140 | this.autoCloseTags = ko 141 | .observable( 142 | storage.getItem('autoCloseTags') !== null 143 | ? storage.getItem('autoCloseTags') === 'true' 144 | : true 145 | ) 146 | .extend({ persist: 'autoCloseTags' }); 147 | 148 | // Autocomplete Suggestions 149 | this.autocompleteSuggestionsEnabled = ko 150 | .observable( 151 | storage.getItem('autocompleteSuggestionsEnabled') !== null 152 | ? storage.getItem('autocompleteSuggestionsEnabled') === 'true' 153 | : true 154 | ) 155 | .extend({ persist: 'autocompleteSuggestionsEnabled' }); 156 | 157 | // Auto Close Brackets 158 | this.autoCloseBrackets = ko 159 | .observable( 160 | storage.getItem('autoCloseBrackets') !== null 161 | ? storage.getItem('autoCloseBrackets') === 'true' 162 | : true 163 | ) 164 | .extend({ persist: 'autoCloseBrackets' }); 165 | 166 | // Night mode 167 | this.invertColorsEnabled = ko 168 | .observable( 169 | storage.getItem('invertColorsEnabled') !== null 170 | ? storage.getItem('invertColorsEnabled') === 'true' 171 | : false 172 | ) 173 | .extend({ persist: 'invertColorsEnabled' }); 174 | 175 | // Snap to grid 176 | this.snapGridEnabled = ko 177 | .observable( 178 | storage.getItem('snapGridEnabled') !== null 179 | ? storage.getItem('snapGridEnabled') === 'true' 180 | : false 181 | ) 182 | .extend({ persist: 'snapGridEnabled' }); 183 | 184 | this.restoreSessionEnabled = ko 185 | .observable( 186 | storage.getItem('restoreSessionEnabled') !== null 187 | ? storage.getItem('restoreSessionEnabled') === 'true' 188 | : false 189 | ) 190 | .extend({ persist: 'restoreSessionEnabled' }); 191 | 192 | this.developmentModeEnabled = ko 193 | .observable( 194 | storage.getItem('developmentModeEnabled') !== null 195 | ? storage.getItem('developmentModeEnabled') === 'true' 196 | : false 197 | ) 198 | .extend({ persist: 'developmentModeEnabled' }); 199 | 200 | // Grid size 201 | this.gridSize = ko 202 | .observable(parseInt(storage.getItem('gridSize') || '40')) 203 | .extend({ persist: 'gridSize' }); 204 | 205 | // Autocreate nodes 206 | this.createNodesEnabled = ko 207 | .observable( 208 | storage.getItem('createNodesEnabled') !== null 209 | ? storage.getItem('createNodesEnabled') === 'true' 210 | : true 211 | ) 212 | .extend({ persist: 'createNodesEnabled' }); 213 | 214 | // Editor stats 215 | this.editorStatsEnabled = ko 216 | .observable( 217 | storage.getItem('editorStatsEnabled') !== null 218 | ? storage.getItem('editorStatsEnabled') === 'true' 219 | : false 220 | ) 221 | .extend({ persist: 'editorStatsEnabled' }); 222 | 223 | // Markup language 224 | this.markupLanguage = ko 225 | .observable(storage.getItem('markupLanguage') || 'bbcode') 226 | .extend({ persist: 'markupLanguage' }); 227 | 228 | // Filetype version 229 | this.filetypeVersion = ko 230 | .observable(storage.getItem('filetypeVersion') || '1') 231 | .extend({ persist: 'filetypeVersion' }); 232 | 233 | // Line Style 234 | this.lineStyle = ko 235 | .observable(storage.getItem('lineStyle') || 'straight') 236 | .extend({ persist: 'lineStyle' }); 237 | 238 | this.fileTabsVisible = ko 239 | .observable( 240 | storage.getItem('fileTabsVisible') !== null 241 | ? storage.getItem('fileTabsVisible') === 'true' 242 | : true 243 | ) 244 | .extend({ persist: 'fileTabsVisible' }); 245 | 246 | this.selectedFileTab = ko 247 | .observable(storage.getItem('selectedFileTab') || 0) 248 | .extend({ persist: 'selectedFileTab' }); 249 | 250 | // Always open nodes in Visual Studio Code Editor 251 | // We don't actually show this in the settings menu; it can only be set by the VSCode extension's settings 252 | this.alwaysOpenNodesInVisualStudioCodeEditor = ko 253 | .observable( 254 | storage.getItem('alwaysOpenNodesInVisualStudioCodeEditor') !== null 255 | ? storage.getItem('alwaysOpenNodesInVisualStudioCodeEditor') === 'true' 256 | : false 257 | ) 258 | .extend({ persist: 'alwaysOpenNodesInVisualStudioCodeEditor' }); 259 | 260 | this.editorSplitDirection = ko 261 | .observable(storage.getItem('editorSplitDirection') || 'left') 262 | .extend({ persist: 'editorSplitDirection' }); 263 | 264 | this.editorSplit = ko 265 | .observable( 266 | storage.getItem('editorSplit') !== null 267 | ? storage.getItem('editorSplit') === 'true' 268 | : false 269 | ) 270 | .extend({ persist: 'editorSplit' }); 271 | 272 | this.editorSplitSize = ko 273 | .observable(storage.getItem('editorSplitSize') || '50%') 274 | .extend({ persist: 'editorSplitSize' }); 275 | }; 276 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/droid-sans-mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/public/droid-sans-mono.ttf -------------------------------------------------------------------------------- /src/public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/public/icon.ico -------------------------------------------------------------------------------- /src/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/public/icon.png -------------------------------------------------------------------------------- /src/public/images/dropbox.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/public/images/dropbox.ico -------------------------------------------------------------------------------- /src/public/images/inky-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/public/images/inky-icon.png -------------------------------------------------------------------------------- /src/public/images/pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/public/images/pixel.png -------------------------------------------------------------------------------- /src/public/images/renpy-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/public/images/renpy-128.png -------------------------------------------------------------------------------- /src/public/images/twine-favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/public/images/twine-favicon-152.png -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 */ -------------------------------------------------------------------------------- /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} -------------------------------------------------------------------------------- /src/public/plugins/inkjs/ink-renderer.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events';// todo remove this emitter 2 | const { Story } = inkjs; 3 | 4 | export var inkRender = function() { 5 | let emiter = new EventEmitter(); //todo add signals 6 | this.emiter = emiter; 7 | this.story = null; 8 | this.log = []; 9 | this.onRecompile = () => {}; 10 | 11 | this.curStory = { 12 | messages: [], 13 | choices: [], 14 | tags: [], 15 | paragraphEl: '', 16 | }; 17 | this.resetStory = () => { 18 | this.prevSavePoints = []; 19 | this.choiceHistory = []; 20 | this.textAreaEl.innerHTML = ''; 21 | this.curStory = { 22 | messages: [], 23 | choices: [], 24 | tags: [], 25 | paragraphEl: '', 26 | }; 27 | this.story.ResetState(); 28 | }; 29 | this.terminate = () => { 30 | if (!this.textAreaEl) return; 31 | try { 32 | emiter.removeAllListeners(); 33 | this.finished = true; 34 | } catch (e) { 35 | console.warn(e); 36 | } 37 | }; 38 | 39 | this.setCurStory = ({ messages, choices, tags, paragraphEl }) => { 40 | this.curStory = { messages, choices, tags, paragraphEl }; 41 | }; 42 | const getMessage = _story => { 43 | let message = []; 44 | while (_story.canContinue) { 45 | message.push(_story.Continue().replace(/\n/g, '')); 46 | } 47 | return message; 48 | }; 49 | 50 | const continueStory = (choiceLabel = '', choicePath = '') => { 51 | const paragraph = getMessage(this.story); 52 | const gotoFindQuery = choiceLabel.includes('"') 53 | ? choiceLabel 54 | : `[${choiceLabel}]`; 55 | 56 | const paragraphEl = document.createElement('p'); 57 | if (choiceLabel) { 58 | const paragraphElementTitle = document.createElement('p'); 59 | paragraphElementTitle.innerHTML = ``; 60 | paragraphElementTitle.onclick = () => 61 | app.navigateToNodeDuringPlayTest(choicePath, gotoFindQuery); 62 | 63 | paragraphEl.appendChild(paragraphElementTitle); 64 | paragraph.forEach(p => { 65 | const message = document.createElement('p'); 66 | message.innerHTML = `${p}
`; 67 | paragraphEl.appendChild(message); 68 | }); 69 | } else { 70 | paragraphEl.innerHTML = paragraph.join('
'); 71 | } 72 | 73 | this.setCurStory({ 74 | ...this.curStory, 75 | messages: this.log 76 | ? [ 77 | ...this.curStory.messages, 78 | choiceLabel ? `--${choiceLabel}--` : '', 79 | paragraph, 80 | ].filter(Boolean) 81 | : [paragraph], 82 | tags: this.story.currentTags, 83 | choices: this.story.currentChoices, 84 | paragraphEl, 85 | }); 86 | 87 | updateText(); 88 | }; 89 | 90 | this.prevSavePoints = []; 91 | const getChoice = (index, label) => { 92 | this.prevSavePoints.push(this.story.state.toJson()); 93 | const choicePath = this.story.state.currentChoices[index].sourcePath.split( 94 | '.' 95 | )[0]; 96 | this.story.ChooseChoiceIndex(index); 97 | continueStory(label, choicePath); 98 | }; 99 | this.rewindStory = () => { 100 | document.getElementById('choiceButtons').remove(); 101 | this.textAreaEl.removeChild(this.textAreaEl.lastElementChild); 102 | this.story.state.LoadJson(this.prevSavePoints.pop()); 103 | continueStory(); 104 | }; 105 | 106 | this.createAndAddParagraph = child => { 107 | console.log('made', child); 108 | if (child.innerHTML) { 109 | const paragraph = document.createElement('p'); 110 | paragraph.appendChild(child); 111 | paragraph.className = 112 | 'story-playtest-bubble story-playtest-answer answer-post fade-in is-paused'; 113 | this.textAreaEl.appendChild(paragraph); 114 | $(paragraph).removeClass('is-paused'); 115 | } 116 | }; 117 | // html update stuff 118 | const updateText = () => { 119 | this.createAndAddParagraph(this.curStory.paragraphEl); 120 | this.textAreaEl.querySelectorAll('div').forEach(n => n.remove()); 121 | const btnWrapper = document.createElement('div'); 122 | btnWrapper.id = 'choiceButtons'; 123 | btnWrapper.className = 'flex-wrap'; 124 | // Debug tools 125 | const reloadBtn = document.createElement('button'); 126 | reloadBtn.innerText = '🔄'; 127 | reloadBtn.title = 'Recompile story'; 128 | reloadBtn.onclick = this.onRecompile; 129 | reloadBtn.className = 'storyPreviewChoiceButton'; 130 | btnWrapper.appendChild(reloadBtn); 131 | const restartBtn = document.createElement('button'); 132 | restartBtn.innerText = '🎬'; //'🔄'; 133 | restartBtn.title = 'Restart story'; 134 | restartBtn.onclick = () => { 135 | this.resetStory(); 136 | continueStory(); 137 | }; 138 | restartBtn.className = 'storyPreviewChoiceButton'; 139 | btnWrapper.appendChild(restartBtn); 140 | const rewindBtn = document.createElement('button'); 141 | rewindBtn.innerText = '⏪'; 142 | rewindBtn.title = 'Go to previous'; 143 | rewindBtn.disabled = this.prevSavePoints.length === 0; 144 | rewindBtn.onclick = () => { 145 | this.rewindStory(); 146 | continueStory(); 147 | }; 148 | btnWrapper.appendChild(rewindBtn); 149 | rewindBtn.className = 'storyPreviewChoiceButton'; 150 | // choices 151 | this.curStory.choices.forEach((choice, index) => { 152 | const btn = document.createElement('button'); 153 | btn.innerText = choice.text; 154 | btn.onclick = e => { 155 | e.stopPropagation(); 156 | getChoice(index, choice.text); 157 | }; 158 | btn.className = 'storyPreviewChoiceButton'; 159 | btnWrapper.appendChild(btn); 160 | }); 161 | this.textAreaEl.appendChild(btnWrapper); 162 | 163 | this.textAreaEl.scrollTo({ 164 | top: this.textAreaEl.scrollHeight + 100, 165 | left: 0, 166 | behavior: 'smooth', 167 | }); 168 | }; 169 | 170 | this.initInk = ( 171 | compiler, 172 | onRecompile, 173 | prevSession, 174 | inkTextData, 175 | startChapter, 176 | htmlIdToAttachTo, 177 | resourcesPath, 178 | debugLabelId, 179 | playtestStyle, 180 | playtestVariables //TODO 181 | ) => { 182 | this.onRecompile = onRecompile; 183 | console.log('INIT INK'); 184 | this.finished = false; 185 | document.getElementById(htmlIdToAttachTo).style.visibility = 'hidden'; 186 | this.textAreaEl = document.getElementById(debugLabelId); 187 | this.textAreaEl.innerHTML = 188 | '

Parsing ink

.

.

.

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

Parsing failed:

>

${response.errors.join( 198 | '

' 199 | )}


${response.warnings.join('

')}

`; 200 | this.textAreaEl.onclick = () => { 201 | console.log('====>', response); 202 | app.data.goToErrorInkNode(this.inkTextData, response.errors[0]); 203 | this.textAreaEl.onclick = null; 204 | }; 205 | return; 206 | } 207 | if (response.warnings.length > 0) { 208 | const warningsEl = document.createElement('p'); 209 | warningsEl.className = 'title-warning'; 210 | response.warnings.forEach(warning => { 211 | const warningEl = document.createElement('p'); 212 | warningEl.innerText = warning; 213 | warningEl.onclick = () => { 214 | app.data.goToErrorInkNode(this.inkTextData, warning); 215 | }; 216 | warningsEl.appendChild(warningEl); 217 | }); 218 | this.createAndAddParagraph(warningsEl); 219 | } 220 | this.story = new Story(response.story); 221 | console.log('STORY', this.story); 222 | console.warn('Warnings', response.warnings); 223 | continueStory(); 224 | 225 | //Try to restart story from specific chapter when there is no start chapter specified in the global scope 226 | if ( 227 | this.story.currentText === '' && 228 | this.story.currentChoices.length === 0 229 | ) { 230 | if (startChapter !== app.data.InkGlobalScopeNodeName) { 231 | this.compiler.submit(`-> ${startChapter}\n` + inkTextData); 232 | } else { 233 | const firstFoundNode = inkTextData 234 | .split('\n') 235 | .find(line => line.includes('===')); 236 | this.compiler.submit( 237 | `-> ${firstFoundNode.split('===')[1]}\n` + inkTextData 238 | ); 239 | } 240 | } 241 | }) 242 | .then(() => { 243 | if ( 244 | !prevSession.recompile && 245 | prevSession.story && 246 | prevSession.prevSavePoints.length !== 0 247 | ) { 248 | this.story = prevSession.story; 249 | prevSession.childNodes.forEach(child => 250 | this.textAreaEl.appendChild(child) 251 | ); 252 | this.prevSavePoints = prevSession.prevSavePoints; 253 | continueStory(); 254 | return; 255 | } 256 | if (inkTextData) this.compiler.submit(inkTextData); 257 | }); 258 | }; 259 | }; 260 | -------------------------------------------------------------------------------- /src/public/plugins/jsoneditor/jsoneditor.js: -------------------------------------------------------------------------------- 1 | export var JSONEditor = function({ id }) { 2 | this.tableAdd = ({ key, value }) => { 3 | var $clone = this.table 4 | .find('tr.hide') 5 | .clone(true) 6 | .removeClass('hide table-line'); 7 | if (key && value) { 8 | console.log('mutate', { key, value, html: $clone.html() }); 9 | $clone.html(` 10 | ${key} 11 | ${value} 12 | 13 | x delete 14 | 15 | `); 16 | $('.table-remove').on('click', function() { 17 | $(this) 18 | .parents('tr') 19 | .detach(); 20 | }); 21 | } 22 | this.table.find('table').append($clone); 23 | }; 24 | this.getValue = () => { 25 | var $rows = this.table.find('tr:not(:hidden)'); 26 | var headers = []; 27 | var data = []; 28 | 29 | // Get the headers (add special header logic here) 30 | $($rows.shift()) 31 | .find('th:not(:empty):not([data-attr-ignore])') 32 | .each(function() { 33 | headers.push( 34 | $(this) 35 | .text() 36 | .toLowerCase() 37 | ); 38 | }); 39 | 40 | // Turn all existing rows into a loopable array 41 | $rows.each(function() { 42 | var $td = $(this).find('td'); 43 | var h = {}; 44 | // Use the headers from earlier to name our hash keys 45 | headers.forEach(function(header, i) { 46 | h[header] = $td.eq(i).text(); // will adapt for inputs if text is empty 47 | }); 48 | 49 | data.push(h); 50 | }); 51 | // Output the result 52 | // var $EXPORT = $('#save'); 53 | // $EXPORT.text(JSON.stringify(data)); 54 | return data; 55 | }; 56 | 57 | this.init = () => { 58 | document.getElementById(id).innerHTML = ` 59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 76 | 77 |
KeyValueRemove
keyval 74 | x delete 75 |
78 | 79 | 84 |
85 |
86 | `; 87 | var $TABLE = $('#table'); 88 | var $BTN = $('#save-btn'); 89 | this.table = $TABLE; 90 | this.rows = $TABLE.find('tr:not(:hidden)'); 91 | var $EXPORT = $('#save-btn'); 92 | $EXPORT.addClass('hide'); 93 | 94 | $('.table-add').on('click', this.tableAdd); 95 | 96 | $('.table-remove').on('click', function() { 97 | console.log({ removeThis: this }); 98 | $(this) 99 | .parents('tr') 100 | .detach(); 101 | }); 102 | 103 | // A few jQuery helpers for exporting only 104 | jQuery.fn.pop = [].pop; 105 | jQuery.fn.shift = [].shift; 106 | $BTN.on('click', this.getValue); 107 | }; 108 | this.init(); 109 | 110 | this.setValue = (newValue = []) => { 111 | newValue.forEach(this.tableAdd); 112 | }; 113 | }; 114 | -------------------------------------------------------------------------------- /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/plugins/runner.js: -------------------------------------------------------------------------------- 1 | import { yarnRender } from './bondage/renderer'; 2 | import { inkRender } from './inkjs/ink-renderer'; 3 | const { JSONEditor } = require('./jsoneditor/jsoneditor'); 4 | 5 | export var Runner = function({ 6 | app, 7 | createButton, 8 | addSettingsItem, 9 | getPluginStore, 10 | onYarnEditorOpen, 11 | onYarnInPreviewMode, 12 | onYarnSavedNode, 13 | onYarnSetDocumentType, 14 | onKeyUp, 15 | onKeyDown, 16 | onLoad, 17 | setPluginStore, 18 | }) { 19 | const self = this; 20 | this.name = 'Runner'; 21 | 22 | // Variables editor dialog 23 | this.onOpenDialog = async () => { 24 | let editor = null; 25 | const { value: formValues } = await Swal.fire({ 26 | title: 'Playtest starting variables', 27 | html: '
', 28 | focusConfirm: false, 29 | customClass: 'swal-wide', 30 | onOpen: () => { 31 | // create the editor 32 | require('./jsoneditor/size-overrides.css'); 33 | editor = new JSONEditor({ id: 'jsoneditor' }); 34 | const localVariables = getPluginStore(self.name); 35 | app.log({ editor }); 36 | // set json 37 | editor.setValue( 38 | typeof localVariables.variables !== 'object' 39 | ? [{ key: 'er', value: 'erd' }] 40 | : localVariables.variables 41 | ); 42 | setPluginStore(self.name, 'runnerVariablesOpen', true); 43 | }, 44 | preConfirm: () => { 45 | setPluginStore(self.name, 'runnerVariablesOpen', false); 46 | return editor.getValue(); 47 | }, 48 | }); 49 | 50 | if (formValues) { 51 | setPluginStore(self.name, 'variables', formValues); 52 | } 53 | }; 54 | 55 | onLoad(() => { 56 | // add actions 57 | addSettingsItem({ 58 | title: 'Playtesting Style', 59 | valueKey: 'playtestStyle', 60 | defaultValue: 'chat', 61 | optionsKey: 'availablePlaytestStyles', 62 | options: [ 63 | { id: 'npc', name: 'Npc bubble' }, 64 | { id: 'chat', name: 'Chat messages' }, 65 | ], 66 | setterKey: 'setPlaytestStyle', 67 | settingsColumn: 'A', 68 | }); 69 | if(app.settings.developmentModeEnabled()) { 70 | // create a button in the file menu 71 | createButton(self.name, { 72 | name: 'Playtest variables', 73 | className: 'item', 74 | attachTo: 'fileMenuDropdown', 75 | onClick: 'onOpenDialog()', 76 | iconName: 'cog', 77 | }); 78 | }; 79 | 80 | const localVariables = getPluginStore(self.name); 81 | if (localVariables.runnerVariablesOpen) self.onOpenDialog(); 82 | }); 83 | 84 | const updateRunnerMode = () => { 85 | // YARN PLAY MODE 86 | if (app.settings.documentType() === 'yarn') { 87 | this.previewStory = new yarnRender(); 88 | 89 | this.gotoLastPlayNode = function() { 90 | if ( 91 | app.editing() && 92 | app.editing().title() !== self.previewStory.node.title 93 | ) { 94 | app.openNodeByTitle(self.previewStory.node.title); 95 | } 96 | app.editor.focus(); 97 | }; 98 | 99 | this.advanceStoryPlayMode = function(speed = 5) { 100 | if (!self.previewStory.finished) { 101 | self.previewStory.changeTextScrollSpeed(speed); 102 | if (self.previewStory.vnSelectedChoice != -1 && speed === 5) { 103 | self.previewStory.vnSelectChoice(); 104 | } 105 | } else { 106 | self.togglePlayMode(false); 107 | self.gotoLastPlayNode(); 108 | } 109 | }; 110 | 111 | this.togglePlayMode = function(playModeOverwrite = false) { 112 | const editor = $('.editor')[0]; 113 | const storyPreviewPlayButton = document.getElementById( 114 | 'storyPlayButton' 115 | ); 116 | const editorPlayPreviewer = document.getElementById('editor-play'); 117 | $('#editor-play').addClass('inYarnMode'); 118 | $('#commandDebugLabel').addClass('inYarnMode'); 119 | app.isEditorInPlayMode(playModeOverwrite); 120 | if (playModeOverwrite) { 121 | //preview play mode 122 | editor.style.display = 'none'; 123 | $('.bbcode-toolbar').addClass('hidden'); 124 | editorPlayPreviewer.style.display = 'flex'; 125 | $(storyPreviewPlayButton).addClass('disabled'); 126 | self.previewStory.emiter.on('finished', function() { 127 | self.togglePlayMode(false); 128 | self.gotoLastPlayNode(); 129 | }); 130 | self.previewStory.emiter.on('startedNode', function(e) { 131 | if (app.isEditorSplit) { 132 | app.workspace.warpToNode( 133 | app.getFirstFoundNode(e.title.toLowerCase().trim()) 134 | ); 135 | } 136 | }); 137 | const localVariables = getPluginStore(self.name); 138 | app.log('variables', localVariables); 139 | app.data.getSaveData('json').then(saveData => { 140 | self.previewStory.initYarn( 141 | JSON.parse(saveData), 142 | app 143 | .editing() 144 | .title() 145 | .trim(), 146 | 'NVrichTextLabel', 147 | false, 148 | 'commandDebugLabel', 149 | app.settings.playtestStyle(), 150 | localVariables.variables || [] 151 | ); 152 | }); 153 | } else { 154 | //edit mode 155 | app.editor.session.setScrollTop(editorPlayPreviewer.scrollTop); 156 | editorPlayPreviewer.style.display = 'none'; 157 | editor.style.display = 'flex'; 158 | $(storyPreviewPlayButton).removeClass('disabled'); 159 | $('.bbcode-toolbar').removeClass('hidden'); 160 | $('.toggle-toolbar').removeClass('hidden'); 161 | $('.editor-counter').removeClass('hidden'); 162 | self.previewStory.terminate(); 163 | } 164 | }; 165 | 166 | onYarnInPreviewMode(() => self.togglePlayMode(false)); 167 | onYarnSavedNode(() => self.togglePlayMode(false)); 168 | 169 | onYarnEditorOpen(() => { 170 | createButton(self.name, { 171 | iconName: 'play', 172 | title: 'Preview', 173 | attachTo: 'bbcodeToolbar', 174 | onClick: 'togglePlayMode(true)', 175 | className: 'bbcode-button bbcode-button-right', 176 | id: 'storyPlayButton', 177 | }); 178 | 179 | const element = document.createElement('div'); 180 | element.innerHTML = ` 181 |
182 |

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

314 |
315 |
316 | `; 317 | document.getElementById('editorContainer').appendChild(element); 318 | }); 319 | } 320 | }; 321 | updateRunnerMode(); 322 | //yarnSetDocumentType 323 | onYarnSetDocumentType(updateRunnerMode); 324 | //TODO remove this ugly hack 325 | app.togglePlayMode = this.togglePlayMode; 326 | }; 327 | -------------------------------------------------------------------------------- /src/public/templates/node.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | } -------------------------------------------------------------------------------- /src/public/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.4.381" 3 | } -------------------------------------------------------------------------------- /src/scss/font/context-menu-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/scss/font/context-menu-icons.eot -------------------------------------------------------------------------------- /src/scss/font/context-menu-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/scss/font/context-menu-icons.ttf -------------------------------------------------------------------------------- /src/scss/font/context-menu-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/scss/font/context-menu-icons.woff -------------------------------------------------------------------------------- /src/scss/font/context-menu-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurymind/YarnClassic/ce8052b977da6dcb02936c6d52d9edcb2003bc3c/src/scss/font/context-menu-icons.woff2 -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | ] -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ] -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------