├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── images │ ├── command-docs.gif │ ├── errors.gif │ ├── go-to-declaration.gif │ ├── syntax-highlighting.gif │ └── visual-editor.gif └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── RELEASING.md ├── art-source └── Icons.afdesign ├── get-version.sh ├── icon.png ├── jest.config.js ├── language-configuration.json ├── media ├── NewNodeTemplate.yarn └── yarnspinner.css ├── package-lock.json ├── package.json ├── snippets.json ├── src ├── editor.ts ├── extension.ts ├── nodes.ts ├── preview.ts ├── runner.html └── tsconfig.json ├── syntaxes └── yarnspinner.tmLanguage.json ├── test └── input │ ├── .vscode │ └── settings.json │ ├── Commands-ParseTree.json │ ├── Commands-Tokens.json │ ├── Commands.yarn │ ├── Escaping-ParseTree.json │ ├── Escaping-Tokens.json │ ├── Escaping.yarn │ ├── Expressions-ParseTree.json │ ├── Expressions-Tokens.json │ ├── Expressions.yarn │ ├── FormatFunctions-ParseTree.json │ ├── FormatFunctions-Tokens.json │ ├── FormatFunctions.yarn │ ├── Functions-ParseTree.json │ ├── Functions-Tokens.json │ ├── Functions.yarn │ ├── Identifiers-ParseTree.json │ ├── Identifiers-Tokens.json │ ├── Identifiers.yarn │ ├── IfStatements-ParseTree.json │ ├── IfStatements-Tokens.json │ ├── IfStatements.yarn │ ├── InlineExpressions-ParseTree.json │ ├── InlineExpressions-Tokens.json │ ├── InlineExpressions.yarn │ ├── Jumps-ParseTree.json │ ├── Jumps-Tokens.json │ ├── Jumps.yarn │ ├── Lines-ParseTree.json │ ├── Lines-Tokens.json │ ├── Lines.yarn │ ├── MyCoolCommands.ysls.json │ ├── NodeHeaders-ParseTree.json │ ├── NodeHeaders-Tokens.json │ ├── NodeHeaders.yarn │ ├── ShortcutOptions-ParseTree.json │ ├── ShortcutOptions-Tokens.json │ ├── ShortcutOptions.yarn │ ├── Smileys-ParseTree.json │ ├── Smileys-Tokens.json │ ├── Smileys.yarn │ ├── Types-ParseTree.json │ ├── Types-Tokens.json │ ├── Types.yarn │ ├── VariableStorage-ParseTree.json │ ├── VariableStorage-Tokens.json │ └── VariableStorage.yarn └── webview ├── GroupView.ts ├── NodeView.ts ├── ViewState.ts ├── constants.ts ├── images ├── align-bottom.svg ├── align-center.svg ├── align-left.svg ├── align-middle.svg ├── align-right.svg └── align-top.svg ├── svg.ts ├── tsconfig.json ├── util.ts ├── webpack.config.js └── yarnspinner.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve! 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **What is the current behavior?** 10 | 11 | 12 | 13 | **Please provide the steps to reproduce, and if possible a minimal demo of the problem**: 14 | 15 | 16 | 17 | **What is the expected behavior?** 18 | 19 | 20 | 21 | **Please tell us about your environment:** 22 | 23 | 24 | 25 | - Yarn Spinner Version: 26 | - Extension Version: 27 | - Unity Version: 28 | 29 | **Other information** 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/images/command-docs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YarnSpinnerTool/VSCodeExtension/0e256203e024930e8e46292c4f55e65101c4999c/.github/images/command-docs.gif -------------------------------------------------------------------------------- /.github/images/errors.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YarnSpinnerTool/VSCodeExtension/0e256203e024930e8e46292c4f55e65101c4999c/.github/images/errors.gif -------------------------------------------------------------------------------- /.github/images/go-to-declaration.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YarnSpinnerTool/VSCodeExtension/0e256203e024930e8e46292c4f55e65101c4999c/.github/images/go-to-declaration.gif -------------------------------------------------------------------------------- /.github/images/syntax-highlighting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YarnSpinnerTool/VSCodeExtension/0e256203e024930e8e46292c4f55e65101c4999c/.github/images/syntax-highlighting.gif -------------------------------------------------------------------------------- /.github/images/visual-editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YarnSpinnerTool/VSCodeExtension/0e256203e024930e8e46292c4f55e65101c4999c/.github/images/visual-editor.gif -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the develop branch (for now) 6 | push: 7 | branches: 8 | - "*" 9 | pull_request: 10 | branches: 11 | - "*" 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | env: 21 | RELEASE_FLAG: false 22 | LANGUAGESERVER_REF: "000000" 23 | 24 | # The type of runner that the job will run on 25 | runs-on: ubuntu-latest 26 | 27 | # Steps represent a sequence of tasks that will be executed as part of the job 28 | steps: 29 | # Check out the repo 30 | - name: Check out repository 31 | uses: actions/checkout@v3 32 | 33 | - name: Get Language Server ref 34 | run: echo "LANGUAGESERVER_REF=$(jq -r .languageServerVersion package.json)" >> $GITHUB_ENV 35 | 36 | # The build script uses env-cmd to run its builds, but that tool fails if 37 | # it can't find a file called '.env'. Solve this by creating an empty one. 38 | - name: Create empty .env file 39 | run: touch .env 40 | 41 | # Check out Yarn Spinner, as a dependency 42 | - name: Check out Yarn Spinner 43 | uses: actions/checkout@v3 44 | with: 45 | repository: "YarnSpinnerTool/YarnSpinner" 46 | path: YarnSpinner 47 | ref: ${{ env.LANGUAGESERVER_REF }} 48 | 49 | - name: Fetch all history and tags from all branches for gitversion 50 | run: git fetch --prune --unshallow 51 | 52 | - name: Setup Node.js environment 53 | uses: actions/setup-node@v2.4.1 54 | 55 | # Needed for building the language server 56 | - name: Setup .NET 57 | uses: actions/setup-dotnet@v1 58 | with: 59 | dotnet-version: 9.0.x 60 | 61 | - name: Clean install dependencies 62 | run: npm ci 63 | 64 | - name: Run unit tests 65 | run: npm test 66 | 67 | - name: Get version 68 | id: version 69 | run: ./get-version.sh 70 | 71 | - name: Update metadata in package.json 72 | uses: onlyutkarsh/patch-files-action@v1.0.1 73 | with: 74 | files: "${{github.workspace}}/package.json" 75 | patch-syntax: | 76 | = /version => "${{ steps.version.outputs.MajorMinorCommits }}" 77 | 78 | # If is a push, and the commit message does not contain the string 79 | # '[release]', build as pre-release. (This is used in the final step.) 80 | - name: Add prerelease tag 81 | if: ${{ github.event_name == 'push' && !contains(github.event.head_commit.message, '[release]' ) }} 82 | run: | 83 | echo "RELEASE_FLAG=--pre-release" >> $GITHUB_ENV 84 | 85 | - name: Compile and create .vsix 86 | env: 87 | LANGUAGESERVER_CSPROJ_PATH: ./YarnSpinner/YarnSpinner.LanguageServer/YarnLanguageServer.csproj 88 | run: npx vsce package ${{ env.RELEASE_FLAG }} 89 | 90 | - name: Upload .vsix as artifact 91 | uses: actions/upload-artifact@v4 92 | with: 93 | name: yarn-spinner-${{steps.version.outputs.MajorMinorCommits}}.vsix 94 | path: ${{github.workspace}}/yarn-spinner-${{steps.version.outputs.MajorMinorCommits}}.vsix 95 | 96 | # If this is a push to the main branch, publish to the Marketplace using our configuration. 97 | - name: Publish to Marketplace 98 | if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push' 99 | env: 100 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 101 | run: | 102 | npx vsce publish ${{ env.RELEASE_FLAG }} -i ${{github.workspace}}/yarn-spinner-${{steps.version.outputs.MajorMinorCommits}}.vsix 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,node,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=macos,node,visualstudiocode 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### Node ### 34 | # Logs 35 | logs 36 | *.log 37 | npm-debug.log* 38 | yarn-debug.log* 39 | yarn-error.log* 40 | lerna-debug.log* 41 | 42 | # Diagnostic reports (https://nodejs.org/api/report.html) 43 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 44 | 45 | # Runtime data 46 | pids 47 | *.pid 48 | *.seed 49 | *.pid.lock 50 | 51 | # Directory for instrumented libs generated by jscoverage/JSCover 52 | lib-cov 53 | 54 | # Coverage directory used by tools like istanbul 55 | coverage 56 | *.lcov 57 | 58 | # nyc test coverage 59 | .nyc_output 60 | 61 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 62 | .grunt 63 | 64 | # Bower dependency directory (https://bower.io/) 65 | bower_components 66 | 67 | # node-waf configuration 68 | .lock-wscript 69 | 70 | # Compiled binary addons (https://nodejs.org/api/addons.html) 71 | build/Release 72 | 73 | # Dependency directories 74 | node_modules/ 75 | jspm_packages/ 76 | 77 | # TypeScript v1 declaration files 78 | typings/ 79 | 80 | # TypeScript cache 81 | *.tsbuildinfo 82 | 83 | # Optional npm cache directory 84 | .npm 85 | 86 | # Optional eslint cache 87 | .eslintcache 88 | 89 | # Optional REPL history 90 | .node_repl_history 91 | 92 | # Output of 'npm pack' 93 | *.tgz 94 | 95 | # Yarn Integrity file 96 | .yarn-integrity 97 | 98 | # dotenv environment variables file 99 | .env 100 | .env.test 101 | 102 | # parcel-bundler cache (https://parceljs.org/) 103 | .cache 104 | 105 | # next.js build output 106 | .next 107 | 108 | # nuxt.js build output 109 | .nuxt 110 | 111 | # rollup.js default build output 112 | dist/ 113 | 114 | # Uncomment the public line if your project uses Gatsby 115 | # https://nextjs.org/blog/next-9-1#public-directory-support 116 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 117 | # public 118 | 119 | # Storybook build outputs 120 | .out 121 | .storybook-out 122 | 123 | # vuepress build output 124 | .vuepress/dist 125 | 126 | # Serverless directories 127 | .serverless/ 128 | 129 | # FuseBox cache 130 | .fusebox/ 131 | 132 | # DynamoDB Local files 133 | .dynamodb/ 134 | 135 | # Temporary folders 136 | tmp/ 137 | temp/ 138 | 139 | ### VisualStudioCode ### 140 | .vscode/* 141 | !.vscode/settings.json 142 | !.vscode/tasks.json 143 | !.vscode/launch.json 144 | !.vscode/extensions.json 145 | 146 | ### VisualStudioCode Patch ### 147 | # Ignore all local history of files 148 | .history 149 | 150 | # End of https://www.gitignore.io/api/macos,node,visualstudiocode 151 | 152 | # Visual Studio Code builds 153 | *.vsix 154 | 155 | # TypeScript output 156 | out/ 157 | 158 | src/*.tokens 159 | src/*.interp 160 | src/.antlr 161 | 162 | # Generated code 163 | media/yarnspinner.js* 164 | 165 | src/types/*.d.ts -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YarnSpinnerTool/VSCodeExtension/0e256203e024930e8e46292c4f55e65101c4999c/.gitmodules -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/input/*.json 2 | src/runner.html 3 | 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Test", 10 | "request": "launch", 11 | "runtimeArgs": ["test"], 12 | "runtimeExecutable": "npm", 13 | "skipFiles": ["/**"], 14 | "type": "node" 15 | }, 16 | { 17 | "name": "Extension", 18 | "type": "extensionHost", 19 | "preLaunchTask": "build-extension-development", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 23 | "envFile": "${workspaceFolder}/.env", 24 | "skipFiles": ["${workspaceFolder}/node_modules/**/*.js"] 25 | }, 26 | { 27 | "name": "Extension + LS (wait for attach)", 28 | "type": "extensionHost", 29 | "preLaunchTask": "build-debug", 30 | "request": "launch", 31 | "runtimeExecutable": "${execPath}", 32 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 33 | "env": { 34 | "VSCODE_DEBUG_MODE": "true" 35 | }, 36 | "envFile": "${workspaceFolder}/.env", 37 | "skipFiles": [ 38 | "${workspaceFolder}/node_modules/**/*.js", 39 | "**/app/out/vs/*" 40 | ] 41 | }, 42 | { 43 | "name": "Extension + LS", 44 | "type": "extensionHost", 45 | "preLaunchTask": "build-debug", 46 | "request": "launch", 47 | "runtimeExecutable": "${execPath}", 48 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 49 | "envFile": "${workspaceFolder}/.env", 50 | "skipFiles": [ 51 | "${workspaceFolder}/node_modules/**/*.js", 52 | "**/app/out/vs/*" 53 | ] 54 | }, 55 | { 56 | "name": "Extension + LS (release)", 57 | "type": "extensionHost", 58 | "preLaunchTask": "build-production", 59 | "request": "launch", 60 | "runtimeExecutable": "${execPath}", 61 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 62 | "skipFiles": ["${workspaceFolder}/node_modules/**/*.js"] 63 | }, 64 | // { 65 | // "name": "Extension + LS (Release)", 66 | // "type": "extensionHost", 67 | // "preLaunchTask": "build-release", 68 | // "request": "launch", 69 | // "runtimeExecutable": "${execPath}", 70 | // "args": [ 71 | // "--extensionDevelopmentPath=${workspaceFolder}" 72 | // ], 73 | // }, 74 | { 75 | "name": ".NET Core Attach", 76 | "type": "coreclr", 77 | "request": "attach", 78 | "requireExactSource": false 79 | // "processName": "dotnet YarnLanguageServer.dll --waitForDebugger" 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet-test-explorer.testProjectPath": "**/*Tests.@(csproj|vbproj|fsproj)", 3 | "omnisharp.useModernNet": true, 4 | "files.insertFinalNewline": true, 5 | "editor.formatOnSave": true, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build-debug", 6 | "dependsOn": [ 7 | "build-extension-development", 8 | "build-server-development" 9 | ], 10 | "dependsOrder": "parallel" 11 | }, 12 | { 13 | "label": "build-extension-development", 14 | "command": "npm", 15 | "args": ["run", "buildVSCodeDevelopment"], 16 | "type": "shell", 17 | "problemMatcher": "$tsc-watch" 18 | }, 19 | { 20 | "label": "build-server-development", 21 | "command": "npm", 22 | "args": ["run", "buildServerDevelopment"], 23 | "type": "shell", 24 | "problemMatcher": "$msCompile" 25 | }, 26 | { 27 | "label": "build-production", 28 | "dependsOn": [ 29 | "build-extension-production", 30 | "build-server-production" 31 | ], 32 | "dependsOrder": "parallel" 33 | }, 34 | { 35 | "label": "build-extension-production", 36 | "command": "npm", 37 | "args": ["run", "buildVSCodeProduction"], 38 | "type": "shell", 39 | "problemMatcher": "$tsc-watch" 40 | }, 41 | { 42 | "label": "build-server-production", 43 | "command": "npm", 44 | "args": ["run", "buildServerProduction"], 45 | "type": "shell", 46 | "problemMatcher": "$msCompile" 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .gitignore 4 | vsc-extension-quickstart.md 5 | src/** 6 | LanguageServer/** 7 | .github/** -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased] 4 | 5 | ### Added 6 | 7 | ### Changed 8 | 9 | ### Removed 10 | 11 | ## [3.0.344] 2025-05-16 12 | 13 | ### Added 14 | 15 | - Added a command to create a new `.yarnproject` file in the workspace. 16 | - Nodes in a node group now show the complexity score for their conditions. 17 | - The dialogue preview and exported runner now allow changing the current saliency strategy. 18 | - Detours to nodes now count as references, in addition to jumps. 19 | - Detours now appear as double-ended lines. 20 | - Connection lines in the graph view between a node and a node group now connect to the box surrounding the nodes. 21 | - Clicking on the colour bar at the top of a node view in the graph will now cycle between the available color options. 22 | - The extension will now activate when the workspace contains a .yarn or .yarnproject file, rather than waiting for a .yarn file to be opened. 23 | - Comments inside node headers are now correctly syntax-highlighted. 24 | - Keywords like `jump`, `detour`, `enum` and so on are now syntax-highlighted correctly. 25 | 26 | ### Changed 27 | 28 | - Fixed an issue where generating a graph view or spreadsheet view would fail if the current tab was not a text editor with a .yarn file open. 29 | - Updated VS Code engine from 1.63 to 1.74. 30 | - Commands that depend on the language server being online will now only appear in the command palette if the language server has started. 31 | - Bracket pair colourization is now disabled by default in Yarn scripts. 32 | - Updated to Yarn Spinner v3.0.0-beta2. 33 | - Fixed an issue where node groups would behave incorrectly in the graph view. 34 | - Node groups now appear with a box drawn around them. 35 | - Fixed a bug where deleting a `color:` header from a node would not remove the colour bar from the node view. 36 | 37 | ### Removed 38 | 39 | - The 'node' snippet, which creates a new node, has been removed. (It was being offered at locations where it was syntactically invalid to appear.) 40 | 41 | ## [2.4.6] 2024-02-27 42 | 43 | ### Changed 44 | 45 | - Updated to Yarn Spinner v2.4.2. 46 | 47 | ## [2.4.3] 2023-11-22 48 | 49 | ### Changed 50 | 51 | - Fixed an issue where the built-in function declarations were missing their return type annotations. 52 | - Updated the schema for .ysls.json files: 53 | - Commands may no longer specify a return type. 54 | - Functions must now specify a return type. 55 | - Changed the definition of 'types' to be an enum of "string", "number", "bool", or "any". 56 | - Enums in JSON schema are type sensitive, so a warning will be issued for types that have capital letters. To fix these warnings, change your type names in your `.ysls.json` file to be lowercase. (These warnings have no impact on your Yarn script editing experience or runtime behaviour.) 57 | 58 | ## [2.4.0] 2023-11-15 59 | 60 | ### Changed 61 | 62 | - Updated to Yarn Spinner v2.4.0. 63 | 64 | ## [2.2.137] 2023-10-07 65 | 66 | ### Changed 67 | 68 | - Fixed a bug where not all language server features would successfully register (and would present as a random subset of features, like code completion or code lens, would simply not work.) 69 | 70 | ## [2.2.128] 2023-08-29 71 | 72 | ### Changed 73 | 74 | - Fixed a bug where Yarn files in workspaces that don't have a .yarnproject file would fail to work correctly. 75 | - Improved code completion to be more reliable. 76 | - Fixed a bug where lines that contain no character names but do contain a `#line:` tag would have incorrect syntax highlighting. 77 | - Fixed a bug where certain built-in functions like `dice()` would not type-check correctly. 78 | 79 | ## [2.2.119] 80 | 81 | ### Added 82 | 83 | - Arrows between nodes now always leave a node at the bottom or the right edge, and enter at the top or left edge, which makes it easier to read the graph as a left-to-right flow. 84 | - Code completion has been re-written, and should now be much faster and more reliable. 85 | 86 | ## [2.2.106] 87 | 88 | ### Added 89 | 90 | - Added the ability to jump to the graph view from a text view, by clicking "Show in Graph View" above a node's title. 91 | - Added the ability to select multiple nodes in the graph view. 92 | - To select nodes, click and drag inside the graph view. 93 | - To pan the view, hold the Alt key (Option on macOS) and click and drag inside the graph view, or click and drag the mousewheel. 94 | - Added the ability to move multiple nodes at once in the graph view. 95 | - Yarn preview text now includes any comments present in the source code. 96 | - Increased the size of node previews, and made them a fixed size of 250x125. Preview text now wraps, and if it goes off the end of the node, it fades out as it reaches the bottom. 97 | - Added the ability to visually group nodes in the graph view. 98 | 99 | - To group nodes together, add a `group` header to one or more nodes: 100 | 101 |
102 |                                             title: NodeA
103 |                                             group: Cool Nodes
104 |                                             ---
105 |                                             Lines here...
106 |                                             ===
107 |                                             title: NodeB
108 |                                             group: Cool Nodes
109 |                                             ---
110 |                                             Lines here...
111 |                                             ===
112 |                                             
113 | 114 | - You can have as many groups in a document as you like, but each node can only be in a single group at a time. 115 | 116 | ### Changed 117 | 118 | ### Removed 119 | 120 | ## [2.2.77] 2022-10-31 121 | 122 | ### Added 123 | 124 | - Added the ability to export a spreadsheet of all dialogue for voice-over recording. 125 | - Voice-over spreadsheets can be exported in either Microsoft Excel or CSV format. 126 | - By default, the spreadsheet contains the line ID, character name (where detected), and line text. Additional columns can be added in Settings. 127 | - Added the ability to preview Yarn dialogue in the editor. 128 | - To use this feature, press `Control-Shift-P` (`Command-Shift-P` on macOS), and type "Preview Dialogue". 129 | - Added the ability to save a self-contained HTML previewer of your Yarn dialogue. 130 | - To use this feature, press `Control-Shift-P` (`Command-Shift-P` on macOS), and type "Export Dialogue as HTML". 131 | - Added the ability to save a graphical representation of the Yarn dialogue. 132 | - To use this feature, press `Control-Shift-P` (`Command-Shift-P` on macOS), and type "Export Dialogue as Graph". 133 | - Graphs are exported in [GraphViz format](https://www.graphviz.org). You will need additional software to be able to view these graphs. 134 | - Added the ability to highlight a node with a colour. 135 | 136 | - To use this feature, add the `color` header to a node: 137 | 138 |
139 |                                             title: MyNode
140 |                                             color: red
141 |                                             ---
142 |                                             Lines here...
143 |                                             ===
144 |                                             
145 | 146 | - Valid colours are: `red`, `green`, `blue`, `orange`, `yellow`, and `purple`. 147 | 148 | - The first few lines of a node will now be shown as a preview in the graph view. 149 | - The graph view now starts centered on the first node in the file. 150 | - Clicking 'Add Node' multiple times will now position each new node offset a little from the last, making it easier to see when you've added multiple new nodes. 151 | - Added the ability to zoom in and out of the graph view using the scroll wheel (two-finger scroll on trackpads). 152 | - Replaced the graph view's line-drawing algorithm with one that should be more stable. 153 | - Nodes that don't have a `position` header set will appear stacked up in the graph view, which prevents a problem where it's unclear how many nodes you have in your document. 154 | 155 | ### Changed 156 | 157 | - Adjusted the background color of the graph view to provide better contrast. 158 | - Increased the width of the 'Jump to Node' dropdown to 200px. 159 | - Fixed a bug where the graph view would not update when the Yarn file was changed on Windows. 160 | 161 | ## 2.2.15 162 | 163 | ### Added 164 | 165 | - Added a setting that controls whether the language server is enabled or not. This feature was added for users who aren't using Yarn Spinner 2.0, but want features like syntax highlighting to work. 166 | - Fixed an issue that caused the graph view and the language server to not load files correctly on Windows. 167 | 168 | ## 2.2.1 169 | 170 | ### Changed 171 | 172 | - No code changes in this release from v2.2.0; this release exists only to update the readme on the VS Code page. 173 | 174 | ## 2.2.0 175 | 176 | ### Added 177 | 178 | - Added a Language Server implementation for Yarn Spinner, which adds semantic highlighting, syntax and semantic error detection, code actions, go-to-refence, command detection, and other language features. 179 | - This release of Yarn Spinner for Visual Studio Code adds telemetry that reports on errors that the extension encounters. For more information on what we collect, and how to turn it off, please see README.md. 180 | 181 | ## 2.0.0 182 | 183 | ### Added 184 | 185 | - Support for detecting syntax errors in Yarn Spinner 2.0 source. 186 | - A visual editor for creating, managing and deleting nodes. 187 | 188 | ## 1.1.0 189 | 190 | ### Added 191 | 192 | - Support for highlighting Yarn Spinner v1.1's inline expressions and format functions. 193 | 194 | ## 1.0.0 195 | 196 | - Initial release of this extension. 197 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Yarn Spinner Pty. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yarn Spinner for Visual Studio Code 2 | 3 | > [!TIP] 4 | > To get Yarn Spinner, and support the team behind it, buy a copy for Unity from [the Yarn Spinner Itch.io Store](https://yarnspinner.itch.io) or from [the Unity Asset Store](https://assetstore.unity.com/packages/tools/behavior-ai/yarn-spinner-for-unity-267061), or check out the [Yarn Spinner Documentation](https://docs.yarnspinner.dev) to install from Git. 5 | 6 | [Yarn Spinner](https://yarnspinner.dev) helps you build branching narrative and dialogue in games. It's easy for writers to use, and has powerful features for integrating your content straight into your game. 7 | 8 | You can install this extension for Visual Studio Code from the [Marketplace](https://marketplace.visualstudio.com/items?itemName=SecretLab.yarn-spinner). 9 | 10 | This extension adds language support for Yarn Spinner code, and a visual editor for creating Yarn scripts. 11 | 12 | Yarn Spinner is made possible by your generous patronage. Please consider supporting Yarn Spinner's development by [becoming a patron](https://patreon.com/secretlab)! 13 | 14 | 15 | 16 | ## Syntax Highlighting 17 | 18 | Your Yarn scripts are colour-coded to help you read the code. 19 | 20 | ![Syntax highlighting](./.github/images/syntax-highlighting.gif) 21 | 22 | ## Visual Editor 23 | 24 | Click the 'Show Graph' button when editing a Yarn script, and we'll show you a visual editor that lets you see the position of nodes! 25 | 26 | ![Visual editor](.github/images/visual-editor.gif) 27 | 28 | ## Error Reporting 29 | 30 | Problems in your Yarn script are detected and shown with a red underline. Because your Yarn script is being checked by the full Yarn Spinner compiler, you won't have to switch back to your game engine to see if there are problems anywhere near as often! 31 | 32 | ![Error highlighting](.github/images/errors.gif) 33 | 34 | ## Hover Tooltips for Commands and Variables 35 | 36 | Hover your mouse over commands, and you'll see documentation about what it does and how to use it. If you define a command in your Unity game's C# source code, we'll automatically use the documentation comments. 37 | 38 | If you hover over a variable, we'll show you information about it - its documentation, its default value, and where it was defined. 39 | 40 | ![Command documentation](.github/images/command-docs.gif) 41 | 42 | ## Go To Definition for variables, nodes and commands 43 | 44 | Control-click (command-click on macOS) a variable, a node, or a command, and you'll be taken to where it was defined. If you're using Yarn Spinner in a Unity project and you click on a command this way, we'll take you straight to the C# source code for the command! 45 | 46 | ![Go To Definition](.github/images/go-to-declaration.gif) 47 | 48 | ## And more! 49 | 50 | To learn more about Yarn Spinner, or find out how to use and install this extension, head to the [official site](https://yarnspinner.dev) and the [documentation](https://docs.yarnspinner.dev)! 51 | 52 | ### Defining Custom Commands for Non-Unity Games 53 | 54 | Yarn Spinner for Visual Studio Code will automatically find your commands written in C#. 55 | 56 | If you want to import command and function definitions for a language other than C#, or you want to override information that the language server parses from C#, add a JSON file with the extension ".ysls.json" to your project's folder using the [ysls.json schema](https://github.com/YarnSpinnerTool/YarnSpinner/blob/main/YarnSpinner.LanguageServer/src/Server/Documentation/ysls.schema.json). 57 | 58 | For examples, take a look at this [import example](https://github.com/YarnSpinnerTool/YarnSpinner/blob/main/YarnSpinner.LanguageServer/ImportExample.ysls.json) or the [yarn spinner built in Commands and Functions file](https://github.com/YarnSpinnerTool/YarnSpinner/blob/main/YarnSpinner.LanguageServer/src/Server/Documentation/BuiltInFunctionsAndCommands.ysls.json). 59 | 60 | ### Telemetry 61 | 62 | Yarn Spinner for Visual Studio Code collects crash report data to help us detect and fix problems in the extension. To learn more about what we collect, please see our [privacy policy](http://yarnspinner.dev/YS_VSCode_PrivacyPolicy-2022.06.08.pdf). 63 | 64 | You can disable this telemetry, along with all other telemetry that Visual Studio Code sends, by opening Settings, and going to Application -> Telemetry, and setting "Telemetry Level" to "none". To learn more, please see [Visual Studio Code's instructions on disabling telemetry](https://code.visualstudio.com/docs/getstarted/telemetry#_disable-telemetry-reporting). 65 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing the Visual Studio Code Extension 2 | 3 | The GitHub Actions workflow, `main.yml`, will automatically build and package the extension on every commit. If the commit is a push to the `main` branch, the extension will be released to the Marketplace; unless the commit message contains the string `[release]`, the release will be a pre-release version. 4 | 5 | Releases will not happen for a pull request, or for any commit that isn't on the `main` branch. 6 | -------------------------------------------------------------------------------- /art-source/Icons.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YarnSpinnerTool/VSCodeExtension/0e256203e024930e8e46292c4f55e65101c4999c/art-source/Icons.afdesign -------------------------------------------------------------------------------- /get-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get info about the current commit 4 | most_recent_tag=$(git describe --tags --match="v*" --abbrev=0) 5 | commits_since_tag=$(git rev-list $most_recent_tag..HEAD | wc -l | awk '{$1=$1};1') 6 | sha=$(git log -1 --format=%H) 7 | short_sha=$(git log -1 --format=%h) 8 | branch=$(git rev-parse --abbrev-ref HEAD) 9 | 10 | total_commit_count=$(git rev-list HEAD | wc -l | awk '{$1=$1};1') 11 | 12 | # A regex for extracting data from a version number: major, minor, patch, 13 | # [prerelease] 14 | REGEX='v(\d+)\.(\d+)\.(\d+)(-.*)?' 15 | 16 | raw_version=${1:-"$most_recent_tag"} 17 | 18 | # Extract the data from the version number 19 | major=$(echo $raw_version | perl -pe "s|$REGEX|\1|" ) 20 | minor=$(echo $raw_version | perl -pe "s|$REGEX|\2|" ) 21 | patch=$(echo $raw_version | perl -pe "s|$REGEX|\3|" ) 22 | prerelease=$(echo $raw_version | perl -pe "s|$REGEX|\4|" ) 23 | 24 | # Calculate the semver from the version (should be the same as the version, but 25 | # just in case) 26 | SemVer="$major.$minor.$patch$prerelease" 27 | 28 | MajorMinorCommits="$major.$minor.$total_commit_count" 29 | 30 | # If there are any commits since the current tag and we aren't overriding our 31 | # version, add that note 32 | if [ "$commits_since_tag" -gt 0 -a -z "$1" ]; then 33 | SemVer="$SemVer+$commits_since_tag" 34 | fi 35 | 36 | # Create the version strings we'll write into the AssemblyInfo files 37 | OutputAssemblyVersion=$(echo "$major.$minor.$patch.$commits_since_tag" | perl -pe "s|\/|\\\/|" ) 38 | OutputAssemblyInformationalVersion=$(echo "$SemVer.Branch.$branch.Sha.$sha" | perl -pe "s|\/|\\\/|" ) 39 | OutputAssemblyFileVersion=$(echo "$major.$minor.$patch.$commits_since_tag" | perl -pe "s|\/|\\\/|" ) 40 | 41 | # Update the AssemblyInfo.cs files 42 | for infoFile in $(find . -name "AssemblyInfo.cs"); do 43 | perl -pi -e "s/AssemblyVersion\(\".*\"\)/AssemblyVersion(\"$OutputAssemblyVersion\")/" $infoFile 44 | perl -pi -e "s/AssemblyInformationalVersion\(\".*\"\)/AssemblyInformationalVersion(\"$OutputAssemblyInformationalVersion\")/" $infoFile 45 | perl -pi -e "s/AssemblyFileVersion\(\".*\"\)/AssemblyFileVersion(\"$OutputAssemblyFileVersion\")/" $infoFile 46 | done 47 | 48 | # If we're running in GitHub Workflows, output our calculated SemVer 49 | if [[ -n $GITHUB_OUTPUT ]]; then 50 | echo "SemVer=$SemVer" >> "$GITHUB_OUTPUT" 51 | echo "ShortSha=$short_sha" >> "$GITHUB_OUTPUT" 52 | echo "MajorMinorCommits=$MajorMinorCommits" >> "$GITHUB_OUTPUT" 53 | fi 54 | 55 | # Log our SemVer 56 | echo $SemVer 57 | echo $MajorMinorCommits -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YarnSpinnerTool/VSCodeExtension/0e256203e024930e8e46292c4f55e65101c4999c/icon.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testMatch: ["**/out/?(*.)+(spec|test).[jt]s?(x)"], 6 | passWithNoTests: true, 7 | }; 8 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your 4 | // language does not support line comments 5 | "lineComment": "//" 6 | }, 7 | // symbols used as brackets 8 | "brackets": [ 9 | ["{", "}"], 10 | ["[", "]"], 11 | ["(", ")"], 12 | ["<<", ">>"] 13 | ], 14 | // symbols that are auto closed when typing 15 | "autoClosingPairs": [ 16 | ["{", "}"], 17 | ["[", "]"], 18 | ["(", ")"], 19 | ["\"", "\""], 20 | ["<<", ">>"] 21 | ], 22 | // symbols that that can be used to surround a selection 23 | "surroundingPairs": [ 24 | ["{", "}"], 25 | ["[", "]"], 26 | ["(", ")"], 27 | ["\"", "\""], 28 | ["'", "'"], 29 | ["<", ">"] 30 | ], 31 | "folding": { 32 | "markers": { 33 | // title: is _usually_ the first tag, but isn't guaranteed to 34 | // be; this is probably fine though 35 | "start": "^title:", 36 | "end": "^===$" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /media/NewNodeTemplate.yarn: -------------------------------------------------------------------------------- 1 | title: {NODE_NAME} 2 | position: {NODE_POSITION_X},{NODE_POSITION_Y} 3 | --- 4 | 5 | === -------------------------------------------------------------------------------- /media/yarnspinner.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: clip; 3 | 4 | background: 5 | linear-gradient( 6 | 90deg, 7 | var(--vscode-notebook-cellEditorBackground) 21px, 8 | transparent 1% 9 | ) 10 | center, 11 | linear-gradient( 12 | var(--vscode-notebook-cellEditorBackground) 21px, 13 | transparent 1% 14 | ) 15 | center, 16 | var(--vscode-foreground); 17 | background-size: 22px 22px; 18 | } 19 | 20 | div.nodes { 21 | position: absolute; 22 | width: 100vw; 23 | height: 100vh; 24 | left: 0px; 25 | top: 0px; 26 | transform-origin: 0 0; 27 | } 28 | 29 | div.zoom-container { 30 | position: absolute; 31 | width: 100vw; 32 | height: 100vh; 33 | left: 0px; 34 | top: 0px; 35 | transform-origin: 0 0; 36 | touch-action: none; 37 | user-select: none; 38 | z-index: 1; 39 | } 40 | 41 | div.zoom-container.pan { 42 | cursor: move; 43 | } 44 | 45 | div.box-select { 46 | background-color: var(--vscode-editor-selectionHighlightBackground); 47 | border: 1px solid var(--vscode-focusBorder); 48 | z-index: 999; 49 | position: absolute; 50 | } 51 | 52 | #nodes-header { 53 | position: absolute; 54 | z-index: 2; 55 | padding: 4px; 56 | background-color: var(--vscode-breadcrumb-background); 57 | width: 100vw; 58 | left: 0; 59 | top: 0; 60 | 61 | box-shadow: 0 0 6px var(--vscode-scrollbar-shadow); 62 | 63 | display: flex; 64 | justify-content: space-between; 65 | } 66 | 67 | div.node { 68 | border: 1px solid var(--vscode-notebook-cellBorderColor); 69 | background-color: var(--vscode-editor-background); 70 | 71 | border-radius: 2px; 72 | 73 | position: absolute; 74 | top: 0; 75 | left: 0; 76 | 77 | touch-action: none; 78 | user-select: none; 79 | 80 | width: 200px; 81 | height: 125px; 82 | cursor: pointer; 83 | display: flex; 84 | justify-content: space-between; 85 | align-items: flex-start; 86 | 87 | box-shadow: 0 0 10px var(--vscode-widget-shadow); 88 | 89 | box-sizing: border-box; 90 | overflow: hidden; 91 | } 92 | 93 | div.node .content { 94 | padding: 5px; 95 | padding-top: 10px; 96 | } 97 | 98 | div.node .node-buttons { 99 | padding: 5px; 100 | } 101 | 102 | div.node .node-preview-fade { 103 | background: linear-gradient( 104 | transparent, 105 | var(--vscode-editor-background) 75% 106 | ); 107 | width: 100%; 108 | height: 2em; 109 | position: absolute; 110 | left: 0px; 111 | bottom: 0px; 112 | } 113 | 114 | div.node .color-bar { 115 | position: absolute; 116 | left: 0; 117 | top: 0; 118 | height: 10px; 119 | background-color: transparent; 120 | width: 100%; 121 | } 122 | 123 | div.node.color .node-buttons { 124 | padding-top: 10px; 125 | } 126 | 127 | div.node.color-red .color-bar { 128 | background-color: var(--vscode-charts-red); 129 | } 130 | div.node.color-blue .color-bar { 131 | background-color: var(--vscode-charts-blue); 132 | } 133 | div.node.color-yellow .color-bar { 134 | background-color: var(--vscode-charts-yellow); 135 | } 136 | div.node.color-orange .color-bar { 137 | background-color: var(--vscode-charts-orange); 138 | } 139 | div.node.color-green .color-bar { 140 | background-color: var(--vscode-charts-green); 141 | } 142 | div.node.color-purple .color-bar { 143 | background-color: var(--vscode-charts-purple); 144 | } 145 | 146 | div.node .title { 147 | text-overflow: ellipsis; 148 | overflow-wrap: anywhere; 149 | } 150 | div.node .subtitle { 151 | text-overflow: ellipsis; 152 | overflow-wrap: anywhere; 153 | } 154 | 155 | div.node .preview { 156 | font-size: 0.75em; 157 | opacity: 0.5; 158 | } 159 | 160 | div.node#node-template { 161 | display: none; 162 | } 163 | 164 | div.node.selected { 165 | border: 2px solid var(--vscode-focusBorder); 166 | 167 | /* normal border is 1px and padding is 5px; reduce padding to 4px to account 168 | * for the extra 1px of border while selected */ 169 | padding: 4px; 170 | } 171 | 172 | #node-jump { 173 | min-width: 200px !important; 174 | } 175 | 176 | #graph-debug { 177 | position: absolute; 178 | left: 0; 179 | top: 50px; 180 | z-index: 999; 181 | background: white; 182 | color: black; 183 | padding: 2px; 184 | font-size: 0.5em; 185 | } 186 | 187 | #alignment-buttons { 188 | position: absolute; 189 | left: 8px; 190 | bottom: 8px; 191 | } 192 | 193 | .group { 194 | position: absolute; 195 | z-index: -10; 196 | border: 1px solid var(--vscode-charts-green); 197 | color: var(--vscode-charts-green); 198 | background: rgba(var(--vscode-charts-green), 0.5); 199 | padding: 4px; 200 | opacity: 0.9; 201 | border-radius: 5px; 202 | } 203 | 204 | /* It'd be nice to say 'background color of .group is VS Code Green but with 205 | * 0.25 opacity' but the color is defined in hex, which doesn't carry alpha 206 | * information. Instead, we work around by making groups have a 'background' 207 | * node that's set to green and has an element-wide opacity. */ 208 | .group-background { 209 | position: absolute; 210 | left: 0px; 211 | top: 0px; 212 | width: 100%; 213 | height: 100%; 214 | 215 | background-color: var(--vscode-charts-green); 216 | opacity: 0.25; 217 | 218 | z-index: -1; 219 | } 220 | 221 | .group.node-group { 222 | border-color: var(--vscode-charts-purple); 223 | color: var(--vscode-charts-purple); 224 | background: rgba(var(--vscode-charts-purple), 0.5); 225 | } 226 | 227 | .group.node-group .group-background { 228 | background-color: var(--vscode-charts-purple); 229 | } 230 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yarn-spinner", 3 | "displayName": "Yarn Spinner", 4 | "description": "Adds support for the Yarn Spinner dialogue language.", 5 | "version": "3.0.0", 6 | "homepage": "https://yarnspinner.dev", 7 | "repository": "https://github.com/YarnSpinnerTool/VSCodeExtension", 8 | "license": "MIT", 9 | "sponsor": { 10 | "url": "https://patreon.com/secretlab" 11 | }, 12 | "languageServerVersion": "946db0be77b5064b88a280cb71ce457e4f0e159c", 13 | "keywords": [ 14 | "yarnspinner", 15 | "narrative", 16 | "branching", 17 | "dialogue" 18 | ], 19 | "activationEvents": [ 20 | "workspaceContains:**/*.yarn", 21 | "workspaceContains:**/*.yarnproject" 22 | ], 23 | "main": "./out/extension.js", 24 | "qna": false, 25 | "publisher": "SecretLab", 26 | "icon": "icon.png", 27 | "galleryBanner": { 28 | "color": "#C5E29B", 29 | "theme": "light" 30 | }, 31 | "engines": { 32 | "vscode": "^1.74.0" 33 | }, 34 | "categories": [ 35 | "Programming Languages" 36 | ], 37 | "telemetryKey": "6fafead2-0494-4fc6-a960-90ef5ef8e5c6", 38 | "scripts": { 39 | "vscode:prepublish": "npm run buildProduction", 40 | "buildExtension": "tsc -p ./src/tsconfig.json && cp src/runner.html out/", 41 | "buildWebViewDevelopment": "webpack --mode=development -c ./webview/webpack.config.js", 42 | "buildWebViewProduction": "webpack --mode=production -c ./webview/webpack.config.js", 43 | "buildVSCodeDevelopment": "npm run buildExtension && npm run buildWebViewDevelopment", 44 | "buildVSCodeProduction": "npm run buildExtension && npm run buildWebViewProduction", 45 | "buildServerDevelopment": "env-cmd --no-override -x dotnet build --no-restore -c Debug '$LANGUAGESERVER_CSPROJ_PATH'", 46 | "buildServerProduction": "env-cmd --no-override -x dotnet publish -c Debug -o out/server '$LANGUAGESERVER_CSPROJ_PATH'", 47 | "buildDebug": "concurrently --timings --kill-others-on-fail \"npm:buildVSCodeDevelopment\" \"npm:buildServerDevelopment\"", 48 | "buildProduction": "concurrently --timings --kill-others-on-fail \"npm:buildVSCodeProduction\" \"npm:buildServerProduction\"", 49 | "lint": "eslint . --ext .ts,.tsx", 50 | "test": "tsc -p ./src/tsconfig.json; jest", 51 | "exportLocal": "vsce package", 52 | "prepare": "husky" 53 | }, 54 | "devDependencies": { 55 | "@interactjs/types": "^1.10.17", 56 | "@types/jest": "^27.0.1", 57 | "@types/node": "^12.12.0", 58 | "@types/vscode": "^1.63.0", 59 | "@typescript-eslint/eslint-plugin": "^4.16.0", 60 | "@typescript-eslint/parser": "^4.16.0", 61 | "antlr4ts-cli": "^0.5.0-alpha.4", 62 | "concurrently": "^7.5.0", 63 | "env-cmd": "^10.1.0", 64 | "eslint": "^7.21.0", 65 | "husky": "^9.1.7", 66 | "jest": "^27.1.1", 67 | "jest-each": "^27.1.1", 68 | "leader-line-types": "^1.0.5-2", 69 | "lint-staged": "^15.5.1", 70 | "prettier": "3.5.3", 71 | "ts-jest": "^27.0.5", 72 | "ts-loader": "^9.4.1", 73 | "typescript": "^4.2.2", 74 | "vsce": "^2.9.1", 75 | "webpack": "^5.74.0", 76 | "webpack-cli": "^4.10.0" 77 | }, 78 | "contributes": { 79 | "menus": { 80 | "editor/title": [ 81 | { 82 | "command": "yarnspinner.show-graph", 83 | "group": "navigation", 84 | "when": "editorLangId == yarnspinner" 85 | } 86 | ], 87 | "commandPalette": [ 88 | { 89 | "command": "yarnspinner.showPreview" 90 | }, 91 | { 92 | "command": "yarnspinner.exportPreview" 93 | }, 94 | { 95 | "command": "yarnspinner.graph" 96 | } 97 | ] 98 | }, 99 | "customEditors": [ 100 | { 101 | "viewType": "yarnspinner.yarnNodes", 102 | "displayName": "Yarn Spinner", 103 | "selector": [ 104 | { 105 | "filenamePattern": "*.{yarn,yarnproject,yarn.txt}" 106 | } 107 | ], 108 | "priority": "option" 109 | } 110 | ], 111 | "commands": [ 112 | { 113 | "command": "yarnspinner.show-graph", 114 | "category": "Yarn Spinner", 115 | "title": "Show Graph", 116 | "enablement": "yarnspinner.languageServerLaunched" 117 | }, 118 | { 119 | "command": "yarnspinner.export-spreadsheet", 120 | "category": "Yarn Spinner", 121 | "title": "Export Dialogue as Recording Spreadsheet...", 122 | "enablement": "yarnspinner.languageServerLaunched" 123 | }, 124 | { 125 | "command": "yarnspinner.showPreview", 126 | "category": "Yarn Spinner", 127 | "title": "Preview Dialogue", 128 | "enablement": "yarnspinner.languageServerLaunched" 129 | }, 130 | { 131 | "command": "yarnspinner.exportPreview", 132 | "category": "Yarn Spinner", 133 | "title": "Export Dialogue as HTML...", 134 | "enablement": "yarnspinner.languageServerLaunched" 135 | }, 136 | { 137 | "command": "yarnspinner.graph", 138 | "category": "Yarn Spinner", 139 | "title": "Export Dialogue as Graph...", 140 | "enablement": "yarnspinner.languageServerLaunched" 141 | }, 142 | { 143 | "command": "yarnspinner.exportDebugOutput", 144 | "category": "Yarn Spinner", 145 | "title": "Export Debug Output", 146 | "enablement": "config.yarnspinner.enableExperimentalFeatures && yarnspinner.languageServerLaunched" 147 | }, 148 | { 149 | "command": "yarnspinner.createProject", 150 | "category": "Yarn Spinner", 151 | "title": "Create New Yarn Project...", 152 | "enablement": "yarnspinner.languageServerLaunched" 153 | } 154 | ], 155 | "configurationDefaults": { 156 | "files.associations": { 157 | "*.yarnproject": "jsonc" 158 | }, 159 | "[yarnspinner]": { 160 | "editor.bracketPairColorization.enabled": false, 161 | "editor.colorDecorators": false 162 | } 163 | }, 164 | "languages": [ 165 | { 166 | "id": "yarnspinner", 167 | "aliases": [ 168 | "Yarn Spinner", 169 | "yarnspinner", 170 | "Yarn" 171 | ], 172 | "extensions": [ 173 | ".yarn", 174 | ".yarn.txt", 175 | ".yarnprogram" 176 | ], 177 | "configuration": "./language-configuration.json" 178 | } 179 | ], 180 | "grammars": [ 181 | { 182 | "language": "yarnspinner", 183 | "scopeName": "source.yarnspinner", 184 | "path": "./syntaxes/yarnspinner.tmLanguage.json" 185 | } 186 | ], 187 | "snippets": [ 188 | { 189 | "language": "yarnspinner", 190 | "path": "./snippets.json" 191 | } 192 | ], 193 | "configuration": { 194 | "title": "Yarn Spinner", 195 | "properties": { 196 | "yarnspinner.EnableLanguageServer": { 197 | "order": 1, 198 | "type": "boolean", 199 | "default": true, 200 | "markdownDescription": "If this setting is turned off, language features like errors and code lookup will not be available. Syntax highlighting will remain available. ([Reload the window](command:workbench.action.reloadWindow) to apply changes to this setting.)" 201 | }, 202 | "yarnspinner.CSharpLookup": { 203 | "type": "boolean", 204 | "default": true, 205 | "description": "Parse C# files for Commands and Functions." 206 | }, 207 | "yarnspinner.DeepCommandLookup": { 208 | "type": "boolean", 209 | "default": true, 210 | "description": "Deeper search for undefined command names" 211 | }, 212 | "yarnspinner.OnlySuggestDeclaredVariables": { 213 | "type": "boolean", 214 | "default": true, 215 | "description": "When false, variable suggestions will also include undeclared variables." 216 | }, 217 | "yarnspinner.DidYouMeanThreshold": { 218 | "type": "number", 219 | "default": 0.3, 220 | "description": "Controls the cut off for fuzzy string matching in quick fix suggestions." 221 | }, 222 | "yarnspinner.trace.server": { 223 | "type": "string", 224 | "enum": [ 225 | "off", 226 | "messages", 227 | "verbose" 228 | ], 229 | "default": "messages", 230 | "description": "Traces the communication between VSCode and the Yarn Language Server Instance." 231 | }, 232 | "yarnspinner.extract.format": { 233 | "type": "string", 234 | "enum": [ 235 | "xlsx", 236 | "csv" 237 | ], 238 | "enumDescriptions": [ 239 | "Export as a Microsoft Excel spreadsheet", 240 | "Export as a Comma-Separated Values spreadsheet" 241 | ], 242 | "default": "xlsx", 243 | "description": "The file format to use when exporting a dialogue spreadsheet for voice-over recording." 244 | }, 245 | "yarnspinner.extract.columns": { 246 | "type": "array", 247 | "default": [ 248 | "character", 249 | "text", 250 | "id" 251 | ], 252 | "description": "The list of columns to be exported when exporting a dialogue spreadsheet.\n\nThese will be included in the export in the order presented here. If \"character\", \"text\", \"id\", \"line\", \"file\", \"node\" are included these will be populated with their relevant values." 253 | }, 254 | "yarnspinner.extract.defaultCharacter": { 255 | "type": "string", 256 | "default": "Player", 257 | "description": "The name to use to assign a line without an explicit character to a specific character, this is often your player character." 258 | }, 259 | "yarnspinner.extract.includeCharacters": { 260 | "type": "boolean", 261 | "default": true, 262 | "markdownDescription": "When exporting a dialogue spreadsheet, separate the character name from the rest of the line.\n\nIf your dialogue does not use character names, turn this setting off to prevent lines that contain a colon (`:`) from being read incorrectly." 263 | }, 264 | "yarnspinner.graph.format": { 265 | "type": "string", 266 | "enum": [ 267 | "dot", 268 | "mermaid" 269 | ], 270 | "enumDescriptions": [ 271 | "Export as a DOT graph", 272 | "Export as a mermaid graph" 273 | ], 274 | "default": "dot", 275 | "description": "the format of the graph" 276 | }, 277 | "yarnspinner.graph.clustering": { 278 | "type": "boolean", 279 | "default": true, 280 | "description": "Should the vertices of the graph be clustered based on file or not" 281 | }, 282 | "yarnspinner.enableExperimentalFeatures": { 283 | "type": "boolean", 284 | "default": false, 285 | "description": "Enable experimental Yarn Spinner extension features" 286 | } 287 | } 288 | }, 289 | "jsonValidation": [ 290 | { 291 | "fileMatch": "*.ysls.json", 292 | "url": "./out/server/ysls.schema.json" 293 | } 294 | ] 295 | }, 296 | "dependencies": { 297 | "@vscode/codicons": "^0.0.22", 298 | "@vscode/extension-telemetry": "^0.5.2", 299 | "@vscode/webview-ui-toolkit": "^0.8.4", 300 | "antlr4ts": "^0.5.0-alpha.4", 301 | "curved-arrows": "git+https://github.com/desplesda/curved-arrows.git#fa1edc2ad372c7b9ec668af7f0506702129a5ced", 302 | "leader-line": "^1.0.5", 303 | "vscode-jsonrpc": "^8.0.0", 304 | "vscode-languageclient": "^8.0.0" 305 | }, 306 | "extensionDependencies": [ 307 | "ms-dotnettools.vscode-dotnet-runtime" 308 | ], 309 | "lint-staged": { 310 | "*.{ts,tsx,js,css,md,json}": "prettier --write" 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /snippets.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/nodes.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import { 3 | NotificationHandler, 4 | ProtocolNotificationType, 5 | } from "vscode-languageclient"; 6 | 7 | export interface NodeInfo { 8 | uniqueTitle?: string; 9 | sourceTitle?: string; 10 | subtitle?: string; 11 | nodeGroup?: string; 12 | bodyStartLine: number; 13 | headerStartLine: number; 14 | headers: NodeHeader[]; 15 | jumps: NodeJump[]; 16 | previewText: string; 17 | } 18 | 19 | export interface NodeHeader { 20 | key: string; 21 | value: string; 22 | } 23 | 24 | export interface NodeJump { 25 | destinationTitle: string; 26 | type: "Jump" | "Detour"; 27 | } 28 | 29 | export namespace DidChangeNodesNotification { 30 | export const type = new ProtocolNotificationType< 31 | DidChangeNodesParams, 32 | void 33 | >("textDocument/yarnSpinner/didChangeNodes"); 34 | export type HandlerSignature = NotificationHandler; 35 | export type MiddlewareSignature = ( 36 | params: DidChangeNodesParams, 37 | next: HandlerSignature, 38 | ) => void; 39 | } 40 | 41 | export interface DidChangeNodesParams { 42 | uri: string; 43 | nodes: NodeInfo[]; 44 | } 45 | 46 | export interface DidRequestNodeInGraphViewParams { 47 | uri: string; 48 | nodeName: string; 49 | } 50 | 51 | export type MetadataEntry = { 52 | id: string; 53 | node: string; 54 | lineNumber: string; 55 | tags: string[]; 56 | [key: string]: unknown; 57 | }; 58 | 59 | export interface CompilerOutput { 60 | data: string; 61 | stringTable: Record; 62 | metadataTable: Record; 63 | errors: string[]; 64 | } 65 | export interface VOStringExport { 66 | file: Uint8Array; 67 | errors: string[]; 68 | } 69 | -------------------------------------------------------------------------------- /src/preview.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as fs from "fs"; 3 | import { YarnData, getActiveWorkspaceUri } from "./extension"; 4 | 5 | export class YarnPreviewPanel { 6 | public static currentPanel: YarnPreviewPanel | null; 7 | 8 | public static readonly viewType = "yarnPreview"; 9 | 10 | private readonly _panel: vscode.WebviewPanel; 11 | private readonly _extensionUri: vscode.Uri; 12 | 13 | public static createOrShow(extensionUri: vscode.Uri, yarnData: YarnData) { 14 | const column = vscode.window.activeTextEditor 15 | ? vscode.window.activeTextEditor.viewColumn 16 | : undefined; 17 | 18 | // If we already have a panel, show it. 19 | if (YarnPreviewPanel.currentPanel) { 20 | YarnPreviewPanel.currentPanel.update(yarnData); 21 | YarnPreviewPanel.currentPanel._panel.reveal(column); 22 | return; 23 | } 24 | 25 | // Otherwise, create a new panel. 26 | const panel = vscode.window.createWebviewPanel( 27 | YarnPreviewPanel.viewType, 28 | "Dialogue Preview", 29 | column || vscode.ViewColumn.One, 30 | YarnPreviewPanel.getWebviewOptions(extensionUri), 31 | ); 32 | 33 | panel.onDidDispose((e) => { 34 | // When the panel is disposed, clear our reference to it (so we 35 | // don't attempt to update a disposed panel.) 36 | YarnPreviewPanel.currentPanel = null; 37 | }); 38 | 39 | panel.webview.onDidReceiveMessage((message) => { 40 | switch (message.command) { 41 | case "save-story": { 42 | YarnPreviewPanel.saveHTML( 43 | YarnPreviewPanel.generateHTML( 44 | yarnData, 45 | extensionUri, 46 | false, 47 | ), 48 | ); 49 | break; 50 | } 51 | } 52 | }); 53 | 54 | YarnPreviewPanel.currentPanel = new YarnPreviewPanel( 55 | panel, 56 | extensionUri, 57 | yarnData, 58 | ); 59 | } 60 | 61 | public static saveHTML(html: string) { 62 | let workspaceURI = getActiveWorkspaceUri(); 63 | let defaultDestinationURI: vscode.Uri | undefined; 64 | if (workspaceURI) { 65 | defaultDestinationURI = vscode.Uri.joinPath( 66 | workspaceURI, 67 | `story.html`, 68 | ); 69 | } 70 | 71 | vscode.window 72 | .showSaveDialog({ 73 | defaultUri: defaultDestinationURI, 74 | }) 75 | .then((uri: vscode.Uri | undefined) => { 76 | if (uri) { 77 | const path = uri.fsPath; 78 | fs.writeFile(path, html, (error) => { 79 | if (error) { 80 | vscode.window.showErrorMessage( 81 | `Unable to write to file ${path}`, 82 | error.message, 83 | ); 84 | } else { 85 | vscode.window.showInformationMessage( 86 | `Story written to ${path}`, 87 | ); 88 | } 89 | }); 90 | } 91 | }); 92 | } 93 | 94 | private static getWebviewOptions( 95 | extensionUri: vscode.Uri, 96 | ): vscode.WebviewOptions { 97 | return { 98 | // Enable javascript in the webview 99 | enableScripts: true, 100 | // And restrict the webview to only loading content from our extension's `media` directory. 101 | // localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')] 102 | }; 103 | } 104 | 105 | private constructor( 106 | panel: vscode.WebviewPanel, 107 | extensionUri: vscode.Uri, 108 | yarnData: YarnData, 109 | ) { 110 | this._panel = panel; 111 | this._extensionUri = extensionUri; 112 | 113 | // Set the webview's initial html content 114 | this.update(yarnData); 115 | } 116 | 117 | public update(yarnData: YarnData) { 118 | let html = YarnPreviewPanel.generateHTML( 119 | yarnData, 120 | this._extensionUri, 121 | true, 122 | ); 123 | 124 | this._panel.webview.html = html; 125 | } 126 | 127 | public static generateHTML( 128 | yarnData: YarnData, 129 | extensionURI: vscode.Uri, 130 | includeSaveOption: boolean, 131 | ): string { 132 | const scriptPathOnDisk = vscode.Uri.joinPath( 133 | extensionURI, 134 | "out", 135 | "runner.html", 136 | ); 137 | let contents = fs.readFileSync(scriptPathOnDisk.fsPath, "utf-8"); 138 | 139 | let saveButton: string; 140 | if (includeSaveOption) { 141 | saveButton = ` 142 | window.addEventListener("yarnLoaded", () => { 143 | window.addButton("Export", ["mx-2", "btn-outline-secondary"], () => { 144 | const vscode = acquireVsCodeApi(); 145 | vscode.postMessage({ 146 | command: 'save-story' 147 | }); 148 | }); 149 | }); 150 | `; 151 | } else { 152 | saveButton = ""; 153 | } 154 | 155 | let injectedYarnProgramScript = ` 156 | 164 | `; 165 | 166 | let replacementMarker = ''; 167 | 168 | var html = contents.replace( 169 | replacementMarker, 170 | injectedYarnProgramScript, 171 | ); 172 | 173 | return html; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "lib": ["ES2019"], 6 | "outDir": "../out", 7 | "sourceMap": true, 8 | "strict": true, 9 | "rootDir": ".", 10 | "allowJs": true, 11 | "declaration": true, 12 | "declarationDir": "types" 13 | }, 14 | "include": ["."], 15 | "exclude": ["types"] 16 | } 17 | -------------------------------------------------------------------------------- /syntaxes/yarnspinner.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "Yarn Spinner", 4 | "patterns": [ 5 | { 6 | "include": "#nodes" 7 | } 8 | ], 9 | "repository": { 10 | "nodes": { 11 | "patterns": [ 12 | { 13 | "comment": "Header field name", 14 | "match": "^\\w+\\:", 15 | "name": "keyword.other" 16 | }, 17 | { 18 | "match": "(?<=:).*$", 19 | "name": "keyword.control" 20 | }, 21 | { 22 | "include": "#nodebody" 23 | }, 24 | { 25 | "include": "#comments" 26 | } 27 | ] 28 | }, 29 | "nodebody": { 30 | "begin": "---", 31 | "end": "===", 32 | "patterns": [ 33 | { 34 | "include": "#comments" 35 | }, 36 | { 37 | "include": "#lines" 38 | }, 39 | { 40 | "include": "#commands" 41 | }, 42 | { 43 | "include": "#options" 44 | } 45 | ] 46 | }, 47 | "inline_expressions": { 48 | "begin": "{", 49 | "end": "}", 50 | "name": "keyword.other", 51 | "patterns": [ 52 | { 53 | "include": "#strings" 54 | }, 55 | { 56 | "include": "#operators" 57 | }, 58 | { 59 | "include": "#variables" 60 | }, 61 | { 62 | "include": "#numbers" 63 | } 64 | ] 65 | }, 66 | "comments": { 67 | "patterns": [ 68 | { 69 | "name": "comment.line.double-slash.yarnspinner", 70 | "begin": "\/\/", 71 | "end": "$" 72 | } 73 | ] 74 | }, 75 | "commands": { 76 | "patterns": [ 77 | { 78 | "name": "keyword.other", 79 | "begin": "\\<\\<", 80 | "end": "\\>\\>", 81 | "patterns": [ 82 | { 83 | "comment": "First word in a command", 84 | "match": "(?<=\\<\\<)\\w+", 85 | "name": "support.function" 86 | }, 87 | { 88 | "include": "#keywords" 89 | }, 90 | { 91 | "include": "#strings" 92 | }, 93 | { 94 | "include": "#operators" 95 | }, 96 | { 97 | "include": "#variables" 98 | }, 99 | { 100 | "include": "#numbers" 101 | } 102 | ] 103 | } 104 | ] 105 | }, 106 | "variables": { 107 | "patterns": [ 108 | { 109 | "name": "variable.other", 110 | "match": "\\$\\w+" 111 | } 112 | ] 113 | }, 114 | "keywords": { 115 | "patterns": [ 116 | { 117 | "name": "keyword.control.yarnspinner", 118 | "match": "\\b(if|set|endif|else|elseif|enum|endenum|case|once|endonce|jump|detour|return)\\b" 119 | } 120 | ] 121 | }, 122 | "operators": { 123 | "patterns": [ 124 | { 125 | "name": "keyword.operator", 126 | "match": "\\+|-|\\*\\/|\\!|\\<|\\>|=|==|\\<=|\\>=" 127 | }, 128 | { 129 | "name": "keyword.operator", 130 | "match": "\\b(is|to|not|ne|eq|le|ge|gt|lt|and|or)\\b" 131 | } 132 | ] 133 | }, 134 | "strings": { 135 | "name": "string.quoted.double.yarnspinner", 136 | "begin": "\"", 137 | "end": "\"", 138 | "patterns": [ 139 | { 140 | "name": "constant.character.escape.yarnspinner", 141 | "match": "\\\\." 142 | } 143 | ] 144 | }, 145 | "numbers": { 146 | "name": "constant.numeric", 147 | "match": "\\b([+-]?([0-9]*[.])?[0-9]+)\\b" 148 | }, 149 | "lines": { 150 | "patterns": [ 151 | { 152 | "comment": "Hashtags", 153 | "name": "constant.language", 154 | "match": "#[^\\s]+" 155 | }, 156 | { 157 | "comment": "Shortcut option", 158 | "name": "keyword.control", 159 | "match": "^\\s*-\\>" 160 | }, 161 | { 162 | "include": "#commands" 163 | }, 164 | { 165 | "include": "#inline_expressions" 166 | }, 167 | { 168 | "include": "#format_functions" 169 | } 170 | ] 171 | }, 172 | "options": { 173 | "patterns": [ 174 | { 175 | "begin": "\\[\\[", 176 | "end": "\\]\\]", 177 | "name": "keyword.other", 178 | "patterns": [ 179 | { 180 | "match": "\\|", 181 | "name": "keyword.other" 182 | }, 183 | { 184 | "include": "#inline_expressions" 185 | }, 186 | { 187 | "include": "#format_functions" 188 | }, 189 | { 190 | "comment": "Displayed text in an option", 191 | "match": "[^{|\\]]+", 192 | "name": "keyword.control" 193 | } 194 | ] 195 | } 196 | ] 197 | }, 198 | "format_functions": { 199 | "patterns": [ 200 | { 201 | "begin": "\\[(?!\\[)", 202 | "end": "\\]", 203 | "name": "keyword.other.format_function", 204 | "patterns": [ 205 | { 206 | "include": "#inline_expressions" 207 | }, 208 | { 209 | "name": "string.quoted.double.yarnspinner.format_function", 210 | "begin": "\"", 211 | "end": "\"", 212 | "patterns": [ 213 | { 214 | "name": "keyword.other.format_function.placeholder", 215 | "match": "%" 216 | }, 217 | { 218 | "name": "constant.character.escape.yarnspinner.format_function", 219 | "match": "\\\\." 220 | } 221 | ] 222 | }, 223 | { 224 | "match": "[^{=\\]]+", 225 | "name": "keyword.control" 226 | } 227 | ] 228 | } 229 | ] 230 | } 231 | }, 232 | "scopeName": "source.yarnspinner" 233 | } 234 | -------------------------------------------------------------------------------- /test/input/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "json.schemas": [ 3 | { 4 | "fileMatch": ["**/*.ysls.json"], 5 | "url": "/yarnspinnerlanguageserver.schema.json" 6 | } 7 | ], 8 | "yarnspinner.trace.server": "verbose" 9 | } 10 | -------------------------------------------------------------------------------- /test/input/Commands-Tokens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "line": 1, 4 | "column": 0, 5 | "name": "ID", 6 | "text": "title" 7 | }, 8 | { 9 | "line": 1, 10 | "column": 5, 11 | "name": "HEADER_DELIMITER", 12 | "text": ": " 13 | }, 14 | { 15 | "line": 1, 16 | "column": 7, 17 | "name": "REST_OF_LINE", 18 | "text": "Start" 19 | }, 20 | { 21 | "line": 1, 22 | "column": 12, 23 | "name": "NEWLINE", 24 | "text": "\n", 25 | "channel": "WHITESPACE" 26 | }, 27 | { 28 | "line": 2, 29 | "column": 0, 30 | "name": "BODY_START", 31 | "text": "---" 32 | }, 33 | { 34 | "line": 2, 35 | "column": 3, 36 | "name": "NEWLINE", 37 | "text": "\n", 38 | "channel": "WHITESPACE" 39 | }, 40 | { 41 | "line": 3, 42 | "column": 0, 43 | "name": "COMMENT", 44 | "text": "// Testing commands", 45 | "channel": "COMMENTS" 46 | }, 47 | { 48 | "line": 3, 49 | "column": 19, 50 | "name": "NEWLINE", 51 | "text": "\n", 52 | "channel": "WHITESPACE" 53 | }, 54 | { 55 | "line": 4, 56 | "column": 0, 57 | "name": "NEWLINE", 58 | "text": "\n", 59 | "channel": "WHITESPACE" 60 | }, 61 | { 62 | "line": 5, 63 | "column": 0, 64 | "name": "COMMAND_START", 65 | "text": "\u003C\u003C" 66 | }, 67 | { 68 | "line": 5, 69 | "column": 2, 70 | "name": "COMMAND_TEXT", 71 | "text": "f" 72 | }, 73 | { 74 | "line": 5, 75 | "column": 3, 76 | "name": "COMMAND_TEXT", 77 | "text": "lip Harley3 \u002B1" 78 | }, 79 | { 80 | "line": 5, 81 | "column": 17, 82 | "name": "COMMAND_TEXT_END", 83 | "text": "\u003E\u003E" 84 | }, 85 | { 86 | "line": 5, 87 | "column": 19, 88 | "name": "NEWLINE", 89 | "text": "\n", 90 | "channel": "WHITESPACE" 91 | }, 92 | { 93 | "line": 6, 94 | "column": 0, 95 | "name": "NEWLINE", 96 | "text": "\n", 97 | "channel": "WHITESPACE" 98 | }, 99 | { 100 | "line": 7, 101 | "column": 0, 102 | "name": "COMMENT", 103 | "text": "// Commands that begin with keywords", 104 | "channel": "COMMENTS" 105 | }, 106 | { 107 | "line": 7, 108 | "column": 36, 109 | "name": "NEWLINE", 110 | "text": "\n", 111 | "channel": "WHITESPACE" 112 | }, 113 | { 114 | "line": 8, 115 | "column": 0, 116 | "name": "COMMAND_START", 117 | "text": "\u003C\u003C" 118 | }, 119 | { 120 | "line": 8, 121 | "column": 2, 122 | "name": "COMMAND_TEXT", 123 | "text": "t" 124 | }, 125 | { 126 | "line": 8, 127 | "column": 3, 128 | "name": "COMMAND_TEXT", 129 | "text": "oggle" 130 | }, 131 | { 132 | "line": 8, 133 | "column": 8, 134 | "name": "COMMAND_TEXT_END", 135 | "text": "\u003E\u003E" 136 | }, 137 | { 138 | "line": 8, 139 | "column": 10, 140 | "name": "NEWLINE", 141 | "text": "\n", 142 | "channel": "WHITESPACE" 143 | }, 144 | { 145 | "line": 9, 146 | "column": 0, 147 | "name": "NEWLINE", 148 | "text": "\n", 149 | "channel": "WHITESPACE" 150 | }, 151 | { 152 | "line": 10, 153 | "column": 0, 154 | "name": "COMMAND_START", 155 | "text": "\u003C\u003C" 156 | }, 157 | { 158 | "line": 10, 159 | "column": 2, 160 | "name": "COMMAND_TEXT", 161 | "text": "s" 162 | }, 163 | { 164 | "line": 10, 165 | "column": 3, 166 | "name": "COMMAND_TEXT", 167 | "text": "ettings" 168 | }, 169 | { 170 | "line": 10, 171 | "column": 10, 172 | "name": "COMMAND_TEXT_END", 173 | "text": "\u003E\u003E" 174 | }, 175 | { 176 | "line": 10, 177 | "column": 12, 178 | "name": "NEWLINE", 179 | "text": "\n", 180 | "channel": "WHITESPACE" 181 | }, 182 | { 183 | "line": 11, 184 | "column": 0, 185 | "name": "NEWLINE", 186 | "text": "\n", 187 | "channel": "WHITESPACE" 188 | }, 189 | { 190 | "line": 12, 191 | "column": 0, 192 | "name": "COMMAND_START", 193 | "text": "\u003C\u003C" 194 | }, 195 | { 196 | "line": 12, 197 | "column": 2, 198 | "name": "COMMAND_TEXT", 199 | "text": "i" 200 | }, 201 | { 202 | "line": 12, 203 | "column": 3, 204 | "name": "COMMAND_TEXT", 205 | "text": "ffy" 206 | }, 207 | { 208 | "line": 12, 209 | "column": 6, 210 | "name": "COMMAND_TEXT_END", 211 | "text": "\u003E\u003E" 212 | }, 213 | { 214 | "line": 12, 215 | "column": 8, 216 | "name": "NEWLINE", 217 | "text": "\n", 218 | "channel": "WHITESPACE" 219 | }, 220 | { 221 | "line": 13, 222 | "column": 0, 223 | "name": "NEWLINE", 224 | "text": "\n", 225 | "channel": "WHITESPACE" 226 | }, 227 | { 228 | "line": 14, 229 | "column": 0, 230 | "name": "COMMAND_START", 231 | "text": "\u003C\u003C" 232 | }, 233 | { 234 | "line": 14, 235 | "column": 2, 236 | "name": "COMMAND_TEXT", 237 | "text": "n" 238 | }, 239 | { 240 | "line": 14, 241 | "column": 3, 242 | "name": "COMMAND_TEXT", 243 | "text": "ulled" 244 | }, 245 | { 246 | "line": 14, 247 | "column": 8, 248 | "name": "COMMAND_TEXT_END", 249 | "text": "\u003E\u003E" 250 | }, 251 | { 252 | "line": 14, 253 | "column": 10, 254 | "name": "NEWLINE", 255 | "text": "\n", 256 | "channel": "WHITESPACE" 257 | }, 258 | { 259 | "line": 15, 260 | "column": 0, 261 | "name": "NEWLINE", 262 | "text": "\n", 263 | "channel": "WHITESPACE" 264 | }, 265 | { 266 | "line": 16, 267 | "column": 0, 268 | "name": "COMMAND_START", 269 | "text": "\u003C\u003C" 270 | }, 271 | { 272 | "line": 16, 273 | "column": 2, 274 | "name": "COMMAND_TEXT", 275 | "text": "o" 276 | }, 277 | { 278 | "line": 16, 279 | "column": 3, 280 | "name": "COMMAND_TEXT", 281 | "text": "rion" 282 | }, 283 | { 284 | "line": 16, 285 | "column": 7, 286 | "name": "COMMAND_TEXT_END", 287 | "text": "\u003E\u003E" 288 | }, 289 | { 290 | "line": 16, 291 | "column": 9, 292 | "name": "NEWLINE", 293 | "text": "\n", 294 | "channel": "WHITESPACE" 295 | }, 296 | { 297 | "line": 17, 298 | "column": 0, 299 | "name": "NEWLINE", 300 | "text": "\n", 301 | "channel": "WHITESPACE" 302 | }, 303 | { 304 | "line": 18, 305 | "column": 0, 306 | "name": "COMMAND_START", 307 | "text": "\u003C\u003C" 308 | }, 309 | { 310 | "line": 18, 311 | "column": 2, 312 | "name": "COMMAND_TEXT", 313 | "text": "a" 314 | }, 315 | { 316 | "line": 18, 317 | "column": 3, 318 | "name": "COMMAND_TEXT", 319 | "text": "ndorian" 320 | }, 321 | { 322 | "line": 18, 323 | "column": 10, 324 | "name": "COMMAND_TEXT_END", 325 | "text": "\u003E\u003E" 326 | }, 327 | { 328 | "line": 18, 329 | "column": 12, 330 | "name": "NEWLINE", 331 | "text": "\n", 332 | "channel": "WHITESPACE" 333 | }, 334 | { 335 | "line": 19, 336 | "column": 0, 337 | "name": "NEWLINE", 338 | "text": "\n", 339 | "channel": "WHITESPACE" 340 | }, 341 | { 342 | "line": 20, 343 | "column": 0, 344 | "name": "COMMAND_START", 345 | "text": "\u003C\u003C" 346 | }, 347 | { 348 | "line": 20, 349 | "column": 2, 350 | "name": "COMMAND_TEXT", 351 | "text": "n" 352 | }, 353 | { 354 | "line": 20, 355 | "column": 3, 356 | "name": "COMMAND_TEXT", 357 | "text": "ote" 358 | }, 359 | { 360 | "line": 20, 361 | "column": 6, 362 | "name": "COMMAND_TEXT_END", 363 | "text": "\u003E\u003E" 364 | }, 365 | { 366 | "line": 20, 367 | "column": 8, 368 | "name": "NEWLINE", 369 | "text": "\n", 370 | "channel": "WHITESPACE" 371 | }, 372 | { 373 | "line": 21, 374 | "column": 0, 375 | "name": "NEWLINE", 376 | "text": "\n", 377 | "channel": "WHITESPACE" 378 | }, 379 | { 380 | "line": 22, 381 | "column": 0, 382 | "name": "COMMAND_START", 383 | "text": "\u003C\u003C" 384 | }, 385 | { 386 | "line": 22, 387 | "column": 2, 388 | "name": "COMMAND_TEXT", 389 | "text": "i" 390 | }, 391 | { 392 | "line": 22, 393 | "column": 3, 394 | "name": "COMMAND_TEXT", 395 | "text": "sActive" 396 | }, 397 | { 398 | "line": 22, 399 | "column": 10, 400 | "name": "COMMAND_TEXT_END", 401 | "text": "\u003E\u003E" 402 | }, 403 | { 404 | "line": 22, 405 | "column": 12, 406 | "name": "NEWLINE", 407 | "text": "\n", 408 | "channel": "WHITESPACE" 409 | }, 410 | { 411 | "line": 23, 412 | "column": 0, 413 | "name": "NEWLINE", 414 | "text": "\n", 415 | "channel": "WHITESPACE" 416 | }, 417 | { 418 | "line": 24, 419 | "column": 0, 420 | "name": "COMMENT", 421 | "text": "// Commands with a single character", 422 | "channel": "COMMENTS" 423 | }, 424 | { 425 | "line": 24, 426 | "column": 35, 427 | "name": "NEWLINE", 428 | "text": "\n", 429 | "channel": "WHITESPACE" 430 | }, 431 | { 432 | "line": 25, 433 | "column": 0, 434 | "name": "COMMAND_START", 435 | "text": "\u003C\u003C" 436 | }, 437 | { 438 | "line": 25, 439 | "column": 2, 440 | "name": "COMMAND_TEXT", 441 | "text": "p" 442 | }, 443 | { 444 | "line": 25, 445 | "column": 3, 446 | "name": "COMMAND_TEXT_END", 447 | "text": "\u003E\u003E" 448 | }, 449 | { 450 | "line": 25, 451 | "column": 5, 452 | "name": "NEWLINE", 453 | "text": "\n", 454 | "channel": "WHITESPACE" 455 | }, 456 | { 457 | "line": 26, 458 | "column": 0, 459 | "name": "NEWLINE", 460 | "text": "\n", 461 | "channel": "WHITESPACE" 462 | }, 463 | { 464 | "line": 27, 465 | "column": 0, 466 | "name": "COMMENT", 467 | "text": "// Commands with punctuation", 468 | "channel": "COMMENTS" 469 | }, 470 | { 471 | "line": 27, 472 | "column": 28, 473 | "name": "NEWLINE", 474 | "text": "\n", 475 | "channel": "WHITESPACE" 476 | }, 477 | { 478 | "line": 28, 479 | "column": 0, 480 | "name": "COMMENT", 481 | "text": "//\u003C\u003C!@#$%^\u0026*()\u2044\u20AC\u2039\u203A\uFB01\uFB02\u2021\u00B0\u00B7\u201A\u2018-=_\u002B\u003E\u003E", 482 | "channel": "COMMENTS" 483 | }, 484 | { 485 | "line": 28, 486 | "column": 31, 487 | "name": "NEWLINE", 488 | "text": "\n", 489 | "channel": "WHITESPACE" 490 | }, 491 | { 492 | "line": 29, 493 | "column": 0, 494 | "name": "NEWLINE", 495 | "text": "\n", 496 | "channel": "WHITESPACE" 497 | }, 498 | { 499 | "line": 30, 500 | "column": 0, 501 | "name": "COMMENT", 502 | "text": "// Commands with colons", 503 | "channel": "COMMENTS" 504 | }, 505 | { 506 | "line": 30, 507 | "column": 23, 508 | "name": "NEWLINE", 509 | "text": "\n", 510 | "channel": "WHITESPACE" 511 | }, 512 | { 513 | "line": 31, 514 | "column": 0, 515 | "name": "COMMAND_START", 516 | "text": "\u003C\u003C" 517 | }, 518 | { 519 | "line": 31, 520 | "column": 2, 521 | "name": "COMMAND_TEXT", 522 | "text": "h" 523 | }, 524 | { 525 | "line": 31, 526 | "column": 3, 527 | "name": "COMMAND_TEXT", 528 | "text": "ide Collision:GermOnPorch" 529 | }, 530 | { 531 | "line": 31, 532 | "column": 28, 533 | "name": "COMMAND_TEXT_END", 534 | "text": "\u003E\u003E" 535 | }, 536 | { 537 | "line": 31, 538 | "column": 30, 539 | "name": "NEWLINE", 540 | "text": "\n", 541 | "channel": "WHITESPACE" 542 | }, 543 | { 544 | "line": 32, 545 | "column": 0, 546 | "name": "BODY_END", 547 | "text": "===" 548 | } 549 | ] -------------------------------------------------------------------------------- /test/input/Commands.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | // Testing commands 4 | 5 | <> 6 | 7 | // Commands that begin with keywords 8 | <> 9 | 10 | <> 11 | 12 | <> 13 | 14 | <> 15 | 16 | <> 17 | 18 | <> 19 | 20 | <> 21 | 22 | <> 23 | 24 | // Commands with a single character 25 | <

> 26 | 27 | // Commands with punctuation 28 | //<> 29 | 30 | // Commands with colons 31 | <> 32 | === -------------------------------------------------------------------------------- /test/input/Escaping.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | Here's a line with a hashtag #hashtag 4 | Here's a line with an escaped hashtag \#hashtag 5 | 6 | Here's a line with an expression {0} 7 | Here's a line with an escaped expression \{0\} 8 | 9 | // Commented out because this isn't actually allowed, but we're just 10 | // maintaining the pattern here: 11 | // Here's a line with a command < 12 | Here's a line with an escaped command \<\\> 13 | 14 | Here's a line with a comment // wow 15 | Here's a line with an escaped comment \/\/ wow 16 | 17 | Here's a line with an escaped backslash \\ 18 | 19 | Here's some TMP-style styling with a color code: wow 20 | 21 | Here's a url: http:\/\/github.com\/YarnSpinnerTool 22 | 23 | // Escaped markup is handled by the LineParser class, not the main grammar 24 | // itself 25 | Here's some markup: [a]hello[/a] 26 | Here's some escaped markup: \[a\]hello\[/a\] 27 | 28 | -> Here's an option with a hashtag #hashtag 29 | -> Here's an option with an escaped hashtag \#hashtag 30 | 31 | -> Here's an option with an expression {0} 32 | -> Here's an option with an escaped expression \{0\} 33 | 34 | // Commented out because this isn't actually allowed, but we're just 35 | // maintaining the pattern here: 36 | -> Here's an option with a condition <> 37 | -> Here's an option with an escaped condition \<\\> 38 | 39 | -> Here's an option with a comment // wow 40 | -> Here's an option with an escaped comment \/\/ wow 41 | 42 | -> Here's an option with an escaped backslash \\ 43 | 44 | -> Here's some TMP-style styling with a color code: wow 45 | 46 | 47 | -> Here's a url: http:\/\/github.com\/YarnSpinnerTool 48 | 49 | // Escaped markup is handled by the LineParser class, not the main grammar 50 | // itself 51 | -> Here's some markup: [a]hello[/a] 52 | -> Here's some escaped markup: \[a\]hello\[/a\] 53 | 54 | === -------------------------------------------------------------------------------- /test/input/Expressions.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | 4 | <> 5 | <> 6 | <> 7 | 8 | // Expression testing 9 | 10 | <> 11 | <> 12 | 13 | // Test unary operators 14 | 15 | <> 16 | <> 17 | <> 18 | 19 | // Test more complex expressions 20 | <> 21 | <> 22 | 23 | // Test % operator 24 | <> 25 | <> 26 | <> 27 | 28 | // Test floating point math 29 | <> 30 | <> 31 | 32 | 33 | === -------------------------------------------------------------------------------- /test/input/FormatFunctions.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | 4 | <> 5 | <> 6 | <> 7 | <> 8 | 9 | // Case selection 10 | <> 11 | Select: [select value={$gender} male="male" female="female" other="other"/] 12 | 13 | <> 14 | Select: [select value={$gender} male="male" female="female" other="other"/] 15 | 16 | <> 17 | Select: [select value={$gender} male="male" female="female" other="other"/] 18 | 19 | // Cardinal pluralisation 20 | <> 21 | Plural: [plural value={$num} one="one" two="two" few="few" many="many" other="other"/] 22 | 23 | <> 24 | Plural: [plural value={$num} one="one" two="two" few="few" many="many" other="other"/] 25 | 26 | <> 27 | Plural: [plural value={$num} one="one" two="two" few="few" many="many" other="other"/] 28 | 29 | <> 30 | Plural: [plural value={$num} one="one" two="two" few="few" many="many" other="other"/] 31 | 32 | // Ordinal pluralisation 33 | <> 34 | Ordinal: [ordinal value={$ord} one="one" two="two" few="few" many="many" other="other"/] 35 | 36 | <> 37 | Ordinal: [ordinal value={$ord} one="one" two="two" few="few" many="many" other="other"/] 38 | 39 | <> 40 | Ordinal: [ordinal value={$ord} one="one" two="two" few="few" many="many" other="other"/] 41 | 42 | <> 43 | Ordinal: [ordinal value={$ord} one="one" two="two" few="few" many="many" other="other"/] 44 | 45 | <> 46 | <> 47 | <> 48 | 49 | // Value insertion 50 | [select value={$gender} male="male: %" female="female: %" other="other: %"/] 51 | 52 | <> 53 | Mae: Wow, I came [ordinal value={$race_position} one="%st" two="%nd" few="%rd" other="%th"/]! 54 | 55 | // Options 56 | 57 | -> [select value={$gender} male="male" female="female" other="other"/] 58 | -> [plural value={$num} one="one" few="few" many="many" other="other"/] 59 | -> [ordinal value={$ord} one="one" two="two" few="few" many="many" other="other"/] 60 | 61 | === 62 | title: Destination 63 | --- 64 | // no-op 65 | === -------------------------------------------------------------------------------- /test/input/Functions-Tokens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "line": 1, 4 | "column": 0, 5 | "name": "ID", 6 | "text": "title" 7 | }, 8 | { 9 | "line": 1, 10 | "column": 5, 11 | "name": "HEADER_DELIMITER", 12 | "text": ": " 13 | }, 14 | { 15 | "line": 1, 16 | "column": 7, 17 | "name": "REST_OF_LINE", 18 | "text": "Start" 19 | }, 20 | { 21 | "line": 1, 22 | "column": 12, 23 | "name": "NEWLINE", 24 | "text": "\n", 25 | "channel": "WHITESPACE" 26 | }, 27 | { 28 | "line": 2, 29 | "column": 0, 30 | "name": "BODY_START", 31 | "text": "---" 32 | }, 33 | { 34 | "line": 2, 35 | "column": 3, 36 | "name": "NEWLINE", 37 | "text": "\n", 38 | "channel": "WHITESPACE" 39 | }, 40 | { 41 | "line": 3, 42 | "column": 0, 43 | "name": "COMMENT", 44 | "text": "// Function tests", 45 | "channel": "COMMENTS" 46 | }, 47 | { 48 | "line": 3, 49 | "column": 17, 50 | "name": "NEWLINE", 51 | "text": "\n", 52 | "channel": "WHITESPACE" 53 | }, 54 | { 55 | "line": 4, 56 | "column": 0, 57 | "name": "NEWLINE", 58 | "text": "\n", 59 | "channel": "WHITESPACE" 60 | }, 61 | { 62 | "line": 5, 63 | "column": 0, 64 | "name": "COMMENT", 65 | "text": "// \u0022add_three_operands\u0022 is a function that adds three operands together", 66 | "channel": "COMMENTS" 67 | }, 68 | { 69 | "line": 5, 70 | "column": 71, 71 | "name": "NEWLINE", 72 | "text": "\n", 73 | "channel": "WHITESPACE" 74 | }, 75 | { 76 | "line": 6, 77 | "column": 0, 78 | "name": "NEWLINE", 79 | "text": "\n", 80 | "channel": "WHITESPACE" 81 | }, 82 | { 83 | "line": 7, 84 | "column": 0, 85 | "name": "COMMAND_START", 86 | "text": "\u003C\u003C" 87 | }, 88 | { 89 | "line": 7, 90 | "column": 2, 91 | "name": "COMMAND_CALL", 92 | "text": "call " 93 | }, 94 | { 95 | "line": 7, 96 | "column": 7, 97 | "name": "FUNC_ID", 98 | "text": "assert" 99 | }, 100 | { 101 | "line": 7, 102 | "column": 13, 103 | "name": "LPAREN", 104 | "text": "(" 105 | }, 106 | { 107 | "line": 7, 108 | "column": 14, 109 | "name": "FUNC_ID", 110 | "text": "add_three_operands" 111 | }, 112 | { 113 | "line": 7, 114 | "column": 32, 115 | "name": "LPAREN", 116 | "text": "(" 117 | }, 118 | { 119 | "line": 7, 120 | "column": 33, 121 | "name": "NUMBER", 122 | "text": "1" 123 | }, 124 | { 125 | "line": 7, 126 | "column": 34, 127 | "name": "COMMA", 128 | "text": "," 129 | }, 130 | { 131 | "line": 7, 132 | "column": 36, 133 | "name": "NUMBER", 134 | "text": "2" 135 | }, 136 | { 137 | "line": 7, 138 | "column": 37, 139 | "name": "COMMA", 140 | "text": "," 141 | }, 142 | { 143 | "line": 7, 144 | "column": 39, 145 | "name": "NUMBER", 146 | "text": "4" 147 | }, 148 | { 149 | "line": 7, 150 | "column": 40, 151 | "name": "OPERATOR_MATHS_MULTIPLICATION", 152 | "text": "*" 153 | }, 154 | { 155 | "line": 7, 156 | "column": 41, 157 | "name": "NUMBER", 158 | "text": "1" 159 | }, 160 | { 161 | "line": 7, 162 | "column": 42, 163 | "name": "RPAREN", 164 | "text": ")" 165 | }, 166 | { 167 | "line": 7, 168 | "column": 44, 169 | "name": "OPERATOR_LOGICAL_EQUALS", 170 | "text": "==" 171 | }, 172 | { 173 | "line": 7, 174 | "column": 47, 175 | "name": "NUMBER", 176 | "text": "7" 177 | }, 178 | { 179 | "line": 7, 180 | "column": 48, 181 | "name": "RPAREN", 182 | "text": ")" 183 | }, 184 | { 185 | "line": 7, 186 | "column": 49, 187 | "name": "COMMAND_END", 188 | "text": "\u003E\u003E" 189 | }, 190 | { 191 | "line": 7, 192 | "column": 51, 193 | "name": "NEWLINE", 194 | "text": "\n", 195 | "channel": "WHITESPACE" 196 | }, 197 | { 198 | "line": 8, 199 | "column": 0, 200 | "name": "NEWLINE", 201 | "text": "\n", 202 | "channel": "WHITESPACE" 203 | }, 204 | { 205 | "line": 9, 206 | "column": 0, 207 | "name": "COMMENT", 208 | "text": "// function calls as parameters", 209 | "channel": "COMMENTS" 210 | }, 211 | { 212 | "line": 9, 213 | "column": 31, 214 | "name": "NEWLINE", 215 | "text": "\n", 216 | "channel": "WHITESPACE" 217 | }, 218 | { 219 | "line": 10, 220 | "column": 0, 221 | "name": "NEWLINE", 222 | "text": "\n", 223 | "channel": "WHITESPACE" 224 | }, 225 | { 226 | "line": 11, 227 | "column": 0, 228 | "name": "COMMAND_START", 229 | "text": "\u003C\u003C" 230 | }, 231 | { 232 | "line": 11, 233 | "column": 2, 234 | "name": "COMMAND_CALL", 235 | "text": "call " 236 | }, 237 | { 238 | "line": 11, 239 | "column": 7, 240 | "name": "FUNC_ID", 241 | "text": "assert" 242 | }, 243 | { 244 | "line": 11, 245 | "column": 13, 246 | "name": "LPAREN", 247 | "text": "(" 248 | }, 249 | { 250 | "line": 11, 251 | "column": 14, 252 | "name": "FUNC_ID", 253 | "text": "add_three_operands" 254 | }, 255 | { 256 | "line": 11, 257 | "column": 32, 258 | "name": "LPAREN", 259 | "text": "(" 260 | }, 261 | { 262 | "line": 11, 263 | "column": 33, 264 | "name": "NUMBER", 265 | "text": "1" 266 | }, 267 | { 268 | "line": 11, 269 | "column": 34, 270 | "name": "COMMA", 271 | "text": "," 272 | }, 273 | { 274 | "line": 11, 275 | "column": 36, 276 | "name": "NUMBER", 277 | "text": "2" 278 | }, 279 | { 280 | "line": 11, 281 | "column": 37, 282 | "name": "COMMA", 283 | "text": "," 284 | }, 285 | { 286 | "line": 11, 287 | "column": 39, 288 | "name": "FUNC_ID", 289 | "text": "add_three_operands" 290 | }, 291 | { 292 | "line": 11, 293 | "column": 57, 294 | "name": "LPAREN", 295 | "text": "(" 296 | }, 297 | { 298 | "line": 11, 299 | "column": 58, 300 | "name": "NUMBER", 301 | "text": "1" 302 | }, 303 | { 304 | "line": 11, 305 | "column": 59, 306 | "name": "COMMA", 307 | "text": "," 308 | }, 309 | { 310 | "line": 11, 311 | "column": 60, 312 | "name": "NUMBER", 313 | "text": "2" 314 | }, 315 | { 316 | "line": 11, 317 | "column": 61, 318 | "name": "COMMA", 319 | "text": "," 320 | }, 321 | { 322 | "line": 11, 323 | "column": 62, 324 | "name": "NUMBER", 325 | "text": "3" 326 | }, 327 | { 328 | "line": 11, 329 | "column": 63, 330 | "name": "RPAREN", 331 | "text": ")" 332 | }, 333 | { 334 | "line": 11, 335 | "column": 64, 336 | "name": "RPAREN", 337 | "text": ")" 338 | }, 339 | { 340 | "line": 11, 341 | "column": 66, 342 | "name": "OPERATOR_LOGICAL_EQUALS", 343 | "text": "==" 344 | }, 345 | { 346 | "line": 11, 347 | "column": 69, 348 | "name": "NUMBER", 349 | "text": "9" 350 | }, 351 | { 352 | "line": 11, 353 | "column": 70, 354 | "name": "RPAREN", 355 | "text": ")" 356 | }, 357 | { 358 | "line": 11, 359 | "column": 71, 360 | "name": "COMMAND_END", 361 | "text": "\u003E\u003E" 362 | }, 363 | { 364 | "line": 11, 365 | "column": 73, 366 | "name": "NEWLINE", 367 | "text": "\n", 368 | "channel": "WHITESPACE" 369 | }, 370 | { 371 | "line": 12, 372 | "column": 0, 373 | "name": "BODY_END", 374 | "text": "===" 375 | } 376 | ] -------------------------------------------------------------------------------- /test/input/Functions.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | // Function tests 4 | 5 | // "add_three_operands" is a function that adds three operands together 6 | 7 | <> 8 | 9 | // function calls as parameters 10 | 11 | <> 12 | === -------------------------------------------------------------------------------- /test/input/Identifiers-ParseTree.json: -------------------------------------------------------------------------------- 1 | { 2 | "line": 1, 3 | "column": 0, 4 | "name": "dialogue", 5 | "children": [ 6 | { 7 | "line": 1, 8 | "column": 0, 9 | "name": "node", 10 | "children": [ 11 | { 12 | "line": 1, 13 | "column": 0, 14 | "name": "header", 15 | "children": [ 16 | { 17 | "line": 1, 18 | "column": 0, 19 | "name": "ID", 20 | "text": "title" 21 | }, 22 | { 23 | "line": 1, 24 | "column": 5, 25 | "name": "HEADER_DELIMITER", 26 | "text": ": " 27 | }, 28 | { 29 | "line": 1, 30 | "column": 7, 31 | "name": "REST_OF_LINE", 32 | "text": "Start" 33 | } 34 | ] 35 | }, 36 | { 37 | "line": 2, 38 | "column": 0, 39 | "name": "BODY_START", 40 | "text": "---" 41 | }, 42 | { 43 | "line": 3, 44 | "column": 0, 45 | "name": "body", 46 | "children": [ 47 | { 48 | "line": 3, 49 | "column": 0, 50 | "name": "statement", 51 | "children": [ 52 | { 53 | "line": 3, 54 | "column": 0, 55 | "name": "declare_statement", 56 | "children": [ 57 | { 58 | "line": 3, 59 | "column": 0, 60 | "name": "COMMAND_START", 61 | "text": "\u003C\u003C" 62 | }, 63 | { 64 | "line": 3, 65 | "column": 2, 66 | "name": "COMMAND_DECLARE", 67 | "text": "declare " 68 | }, 69 | { 70 | "line": 3, 71 | "column": 10, 72 | "name": "variable", 73 | "children": [ 74 | { 75 | "line": 3, 76 | "column": 10, 77 | "name": "VAR_ID", 78 | "text": "$demo" 79 | } 80 | ] 81 | }, 82 | { 83 | "line": 3, 84 | "column": 16, 85 | "name": "OPERATOR_ASSIGNMENT", 86 | "text": "=" 87 | }, 88 | { 89 | "line": 3, 90 | "column": 18, 91 | "name": "value", 92 | "children": [ 93 | { 94 | "line": 3, 95 | "column": 18, 96 | "name": "NUMBER", 97 | "text": "1" 98 | } 99 | ] 100 | }, 101 | { 102 | "line": 3, 103 | "column": 20, 104 | "name": "EXPRESSION_AS", 105 | "text": "as" 106 | }, 107 | { 108 | "line": 3, 109 | "column": 23, 110 | "name": "FUNC_ID", 111 | "text": "number" 112 | }, 113 | { 114 | "line": 3, 115 | "column": 29, 116 | "name": "COMMAND_END", 117 | "text": "\u003E\u003E" 118 | } 119 | ] 120 | } 121 | ] 122 | }, 123 | { 124 | "line": 4, 125 | "column": 0, 126 | "name": "statement", 127 | "children": [ 128 | { 129 | "line": 4, 130 | "column": 0, 131 | "name": "declare_statement", 132 | "children": [ 133 | { 134 | "line": 4, 135 | "column": 0, 136 | "name": "COMMAND_START", 137 | "text": "\u003C\u003C" 138 | }, 139 | { 140 | "line": 4, 141 | "column": 2, 142 | "name": "COMMAND_DECLARE", 143 | "text": "declare " 144 | }, 145 | { 146 | "line": 4, 147 | "column": 10, 148 | "name": "variable", 149 | "children": [ 150 | { 151 | "line": 4, 152 | "column": 10, 153 | "name": "VAR_ID", 154 | "text": "$\u5B9E\u9A8C" 155 | } 156 | ] 157 | }, 158 | { 159 | "line": 4, 160 | "column": 14, 161 | "name": "OPERATOR_ASSIGNMENT", 162 | "text": "=" 163 | }, 164 | { 165 | "line": 4, 166 | "column": 16, 167 | "name": "value", 168 | "children": [ 169 | { 170 | "line": 4, 171 | "column": 16, 172 | "name": "NUMBER", 173 | "text": "1" 174 | } 175 | ] 176 | }, 177 | { 178 | "line": 4, 179 | "column": 18, 180 | "name": "EXPRESSION_AS", 181 | "text": "as" 182 | }, 183 | { 184 | "line": 4, 185 | "column": 21, 186 | "name": "FUNC_ID", 187 | "text": "number" 188 | }, 189 | { 190 | "line": 4, 191 | "column": 27, 192 | "name": "COMMAND_END", 193 | "text": "\u003E\u003E" 194 | } 195 | ] 196 | } 197 | ] 198 | }, 199 | { 200 | "line": 5, 201 | "column": 0, 202 | "name": "statement", 203 | "children": [ 204 | { 205 | "line": 5, 206 | "column": 0, 207 | "name": "declare_statement", 208 | "children": [ 209 | { 210 | "line": 5, 211 | "column": 0, 212 | "name": "COMMAND_START", 213 | "text": "\u003C\u003C" 214 | }, 215 | { 216 | "line": 5, 217 | "column": 2, 218 | "name": "COMMAND_DECLARE", 219 | "text": "declare " 220 | }, 221 | { 222 | "line": 5, 223 | "column": 10, 224 | "name": "variable", 225 | "children": [ 226 | { 227 | "line": 5, 228 | "column": 10, 229 | "name": "VAR_ID", 230 | "text": "$\u044D\u043A\u0441\u043F\u0435\u0440\u0438\u043C\u0435\u043D\u0442" 231 | } 232 | ] 233 | }, 234 | { 235 | "line": 5, 236 | "column": 23, 237 | "name": "OPERATOR_ASSIGNMENT", 238 | "text": "=" 239 | }, 240 | { 241 | "line": 5, 242 | "column": 25, 243 | "name": "value", 244 | "children": [ 245 | { 246 | "line": 5, 247 | "column": 25, 248 | "name": "NUMBER", 249 | "text": "1" 250 | } 251 | ] 252 | }, 253 | { 254 | "line": 5, 255 | "column": 27, 256 | "name": "EXPRESSION_AS", 257 | "text": "as" 258 | }, 259 | { 260 | "line": 5, 261 | "column": 30, 262 | "name": "FUNC_ID", 263 | "text": "number" 264 | }, 265 | { 266 | "line": 5, 267 | "column": 36, 268 | "name": "COMMAND_END", 269 | "text": "\u003E\u003E" 270 | } 271 | ] 272 | } 273 | ] 274 | }, 275 | { 276 | "line": 6, 277 | "column": 0, 278 | "name": "statement", 279 | "children": [ 280 | { 281 | "line": 6, 282 | "column": 0, 283 | "name": "declare_statement", 284 | "children": [ 285 | { 286 | "line": 6, 287 | "column": 0, 288 | "name": "COMMAND_START", 289 | "text": "\u003C\u003C" 290 | }, 291 | { 292 | "line": 6, 293 | "column": 2, 294 | "name": "COMMAND_DECLARE", 295 | "text": "declare " 296 | }, 297 | { 298 | "line": 6, 299 | "column": 10, 300 | "name": "variable", 301 | "children": [ 302 | { 303 | "line": 6, 304 | "column": 10, 305 | "name": "VAR_ID", 306 | "text": "$m\u1EE5c" 307 | } 308 | ] 309 | }, 310 | { 311 | "line": 6, 312 | "column": 15, 313 | "name": "OPERATOR_ASSIGNMENT", 314 | "text": "=" 315 | }, 316 | { 317 | "line": 6, 318 | "column": 17, 319 | "name": "value", 320 | "children": [ 321 | { 322 | "line": 6, 323 | "column": 17, 324 | "name": "NUMBER", 325 | "text": "1" 326 | } 327 | ] 328 | }, 329 | { 330 | "line": 6, 331 | "column": 19, 332 | "name": "EXPRESSION_AS", 333 | "text": "as" 334 | }, 335 | { 336 | "line": 6, 337 | "column": 22, 338 | "name": "FUNC_ID", 339 | "text": "number" 340 | }, 341 | { 342 | "line": 6, 343 | "column": 28, 344 | "name": "COMMAND_END", 345 | "text": "\u003E\u003E" 346 | } 347 | ] 348 | } 349 | ] 350 | }, 351 | { 352 | "line": 7, 353 | "column": 0, 354 | "name": "statement", 355 | "children": [ 356 | { 357 | "line": 7, 358 | "column": 0, 359 | "name": "declare_statement", 360 | "children": [ 361 | { 362 | "line": 7, 363 | "column": 0, 364 | "name": "COMMAND_START", 365 | "text": "\u003C\u003C" 366 | }, 367 | { 368 | "line": 7, 369 | "column": 2, 370 | "name": "COMMAND_DECLARE", 371 | "text": "declare " 372 | }, 373 | { 374 | "line": 7, 375 | "column": 10, 376 | "name": "variable", 377 | "children": [ 378 | { 379 | "line": 7, 380 | "column": 10, 381 | "name": "VAR_ID", 382 | "text": "$\uD83E\uDDF6" 383 | } 384 | ] 385 | }, 386 | { 387 | "line": 7, 388 | "column": 13, 389 | "name": "OPERATOR_ASSIGNMENT", 390 | "text": "=" 391 | }, 392 | { 393 | "line": 7, 394 | "column": 15, 395 | "name": "value", 396 | "children": [ 397 | { 398 | "line": 7, 399 | "column": 15, 400 | "name": "NUMBER", 401 | "text": "1" 402 | } 403 | ] 404 | }, 405 | { 406 | "line": 7, 407 | "column": 17, 408 | "name": "EXPRESSION_AS", 409 | "text": "as" 410 | }, 411 | { 412 | "line": 7, 413 | "column": 20, 414 | "name": "FUNC_ID", 415 | "text": "number" 416 | }, 417 | { 418 | "line": 7, 419 | "column": 26, 420 | "name": "COMMAND_END", 421 | "text": "\u003E\u003E" 422 | } 423 | ] 424 | } 425 | ] 426 | } 427 | ] 428 | }, 429 | { 430 | "line": 8, 431 | "column": 0, 432 | "name": "BODY_END", 433 | "text": "===" 434 | } 435 | ] 436 | } 437 | ] 438 | } -------------------------------------------------------------------------------- /test/input/Identifiers-Tokens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "line": 1, 4 | "column": 0, 5 | "name": "ID", 6 | "text": "title" 7 | }, 8 | { 9 | "line": 1, 10 | "column": 5, 11 | "name": "HEADER_DELIMITER", 12 | "text": ": " 13 | }, 14 | { 15 | "line": 1, 16 | "column": 7, 17 | "name": "REST_OF_LINE", 18 | "text": "Start" 19 | }, 20 | { 21 | "line": 1, 22 | "column": 12, 23 | "name": "NEWLINE", 24 | "text": "\n", 25 | "channel": "WHITESPACE" 26 | }, 27 | { 28 | "line": 2, 29 | "column": 0, 30 | "name": "BODY_START", 31 | "text": "---" 32 | }, 33 | { 34 | "line": 2, 35 | "column": 3, 36 | "name": "NEWLINE", 37 | "text": "\n", 38 | "channel": "WHITESPACE" 39 | }, 40 | { 41 | "line": 3, 42 | "column": 0, 43 | "name": "COMMAND_START", 44 | "text": "\u003C\u003C" 45 | }, 46 | { 47 | "line": 3, 48 | "column": 2, 49 | "name": "COMMAND_DECLARE", 50 | "text": "declare " 51 | }, 52 | { 53 | "line": 3, 54 | "column": 10, 55 | "name": "VAR_ID", 56 | "text": "$demo" 57 | }, 58 | { 59 | "line": 3, 60 | "column": 16, 61 | "name": "OPERATOR_ASSIGNMENT", 62 | "text": "=" 63 | }, 64 | { 65 | "line": 3, 66 | "column": 18, 67 | "name": "NUMBER", 68 | "text": "1" 69 | }, 70 | { 71 | "line": 3, 72 | "column": 20, 73 | "name": "EXPRESSION_AS", 74 | "text": "as" 75 | }, 76 | { 77 | "line": 3, 78 | "column": 23, 79 | "name": "FUNC_ID", 80 | "text": "number" 81 | }, 82 | { 83 | "line": 3, 84 | "column": 29, 85 | "name": "COMMAND_END", 86 | "text": "\u003E\u003E" 87 | }, 88 | { 89 | "line": 3, 90 | "column": 31, 91 | "name": "NEWLINE", 92 | "text": "\n", 93 | "channel": "WHITESPACE" 94 | }, 95 | { 96 | "line": 4, 97 | "column": 0, 98 | "name": "COMMAND_START", 99 | "text": "\u003C\u003C" 100 | }, 101 | { 102 | "line": 4, 103 | "column": 2, 104 | "name": "COMMAND_DECLARE", 105 | "text": "declare " 106 | }, 107 | { 108 | "line": 4, 109 | "column": 10, 110 | "name": "VAR_ID", 111 | "text": "$\u5B9E\u9A8C" 112 | }, 113 | { 114 | "line": 4, 115 | "column": 14, 116 | "name": "OPERATOR_ASSIGNMENT", 117 | "text": "=" 118 | }, 119 | { 120 | "line": 4, 121 | "column": 16, 122 | "name": "NUMBER", 123 | "text": "1" 124 | }, 125 | { 126 | "line": 4, 127 | "column": 18, 128 | "name": "EXPRESSION_AS", 129 | "text": "as" 130 | }, 131 | { 132 | "line": 4, 133 | "column": 21, 134 | "name": "FUNC_ID", 135 | "text": "number" 136 | }, 137 | { 138 | "line": 4, 139 | "column": 27, 140 | "name": "COMMAND_END", 141 | "text": "\u003E\u003E" 142 | }, 143 | { 144 | "line": 4, 145 | "column": 29, 146 | "name": "NEWLINE", 147 | "text": "\n", 148 | "channel": "WHITESPACE" 149 | }, 150 | { 151 | "line": 5, 152 | "column": 0, 153 | "name": "COMMAND_START", 154 | "text": "\u003C\u003C" 155 | }, 156 | { 157 | "line": 5, 158 | "column": 2, 159 | "name": "COMMAND_DECLARE", 160 | "text": "declare " 161 | }, 162 | { 163 | "line": 5, 164 | "column": 10, 165 | "name": "VAR_ID", 166 | "text": "$\u044D\u043A\u0441\u043F\u0435\u0440\u0438\u043C\u0435\u043D\u0442" 167 | }, 168 | { 169 | "line": 5, 170 | "column": 23, 171 | "name": "OPERATOR_ASSIGNMENT", 172 | "text": "=" 173 | }, 174 | { 175 | "line": 5, 176 | "column": 25, 177 | "name": "NUMBER", 178 | "text": "1" 179 | }, 180 | { 181 | "line": 5, 182 | "column": 27, 183 | "name": "EXPRESSION_AS", 184 | "text": "as" 185 | }, 186 | { 187 | "line": 5, 188 | "column": 30, 189 | "name": "FUNC_ID", 190 | "text": "number" 191 | }, 192 | { 193 | "line": 5, 194 | "column": 36, 195 | "name": "COMMAND_END", 196 | "text": "\u003E\u003E" 197 | }, 198 | { 199 | "line": 5, 200 | "column": 38, 201 | "name": "NEWLINE", 202 | "text": "\n", 203 | "channel": "WHITESPACE" 204 | }, 205 | { 206 | "line": 6, 207 | "column": 0, 208 | "name": "COMMAND_START", 209 | "text": "\u003C\u003C" 210 | }, 211 | { 212 | "line": 6, 213 | "column": 2, 214 | "name": "COMMAND_DECLARE", 215 | "text": "declare " 216 | }, 217 | { 218 | "line": 6, 219 | "column": 10, 220 | "name": "VAR_ID", 221 | "text": "$m\u1EE5c" 222 | }, 223 | { 224 | "line": 6, 225 | "column": 15, 226 | "name": "OPERATOR_ASSIGNMENT", 227 | "text": "=" 228 | }, 229 | { 230 | "line": 6, 231 | "column": 17, 232 | "name": "NUMBER", 233 | "text": "1" 234 | }, 235 | { 236 | "line": 6, 237 | "column": 19, 238 | "name": "EXPRESSION_AS", 239 | "text": "as" 240 | }, 241 | { 242 | "line": 6, 243 | "column": 22, 244 | "name": "FUNC_ID", 245 | "text": "number" 246 | }, 247 | { 248 | "line": 6, 249 | "column": 28, 250 | "name": "COMMAND_END", 251 | "text": "\u003E\u003E" 252 | }, 253 | { 254 | "line": 6, 255 | "column": 30, 256 | "name": "NEWLINE", 257 | "text": "\n", 258 | "channel": "WHITESPACE" 259 | }, 260 | { 261 | "line": 7, 262 | "column": 0, 263 | "name": "COMMAND_START", 264 | "text": "\u003C\u003C" 265 | }, 266 | { 267 | "line": 7, 268 | "column": 2, 269 | "name": "COMMAND_DECLARE", 270 | "text": "declare " 271 | }, 272 | { 273 | "line": 7, 274 | "column": 10, 275 | "name": "VAR_ID", 276 | "text": "$\uD83E\uDDF6" 277 | }, 278 | { 279 | "line": 7, 280 | "column": 13, 281 | "name": "OPERATOR_ASSIGNMENT", 282 | "text": "=" 283 | }, 284 | { 285 | "line": 7, 286 | "column": 15, 287 | "name": "NUMBER", 288 | "text": "1" 289 | }, 290 | { 291 | "line": 7, 292 | "column": 17, 293 | "name": "EXPRESSION_AS", 294 | "text": "as" 295 | }, 296 | { 297 | "line": 7, 298 | "column": 20, 299 | "name": "FUNC_ID", 300 | "text": "number" 301 | }, 302 | { 303 | "line": 7, 304 | "column": 26, 305 | "name": "COMMAND_END", 306 | "text": "\u003E\u003E" 307 | }, 308 | { 309 | "line": 7, 310 | "column": 28, 311 | "name": "NEWLINE", 312 | "text": "\n", 313 | "channel": "WHITESPACE" 314 | }, 315 | { 316 | "line": 8, 317 | "column": 0, 318 | "name": "BODY_END", 319 | "text": "===" 320 | } 321 | ] -------------------------------------------------------------------------------- /test/input/Identifiers.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | <> 4 | <> 5 | <> 6 | <> 7 | <> 8 | === -------------------------------------------------------------------------------- /test/input/IfStatements-Tokens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "line": 1, 4 | "column": 0, 5 | "name": "ID", 6 | "text": "title" 7 | }, 8 | { 9 | "line": 1, 10 | "column": 5, 11 | "name": "HEADER_DELIMITER", 12 | "text": ": " 13 | }, 14 | { 15 | "line": 1, 16 | "column": 7, 17 | "name": "REST_OF_LINE", 18 | "text": "Start" 19 | }, 20 | { 21 | "line": 1, 22 | "column": 12, 23 | "name": "NEWLINE", 24 | "text": "\n", 25 | "channel": "WHITESPACE" 26 | }, 27 | { 28 | "line": 2, 29 | "column": 0, 30 | "name": "BODY_START", 31 | "text": "---" 32 | }, 33 | { 34 | "line": 2, 35 | "column": 3, 36 | "name": "NEWLINE", 37 | "text": "\n", 38 | "channel": "WHITESPACE" 39 | }, 40 | { 41 | "line": 3, 42 | "column": 0, 43 | "name": "COMMAND_START", 44 | "text": "\u003C\u003C" 45 | }, 46 | { 47 | "line": 3, 48 | "column": 2, 49 | "name": "COMMAND_IF", 50 | "text": "if " 51 | }, 52 | { 53 | "line": 3, 54 | "column": 5, 55 | "name": "KEYWORD_TRUE", 56 | "text": "true" 57 | }, 58 | { 59 | "line": 3, 60 | "column": 9, 61 | "name": "COMMAND_END", 62 | "text": "\u003E\u003E" 63 | }, 64 | { 65 | "line": 3, 66 | "column": 11, 67 | "name": "NEWLINE", 68 | "text": "\n ", 69 | "channel": "WHITESPACE" 70 | }, 71 | { 72 | "line": 4, 73 | "column": 4, 74 | "name": "INDENT", 75 | "text": "\u003Cindent to 4\u003E" 76 | }, 77 | { 78 | "line": 4, 79 | "column": 4, 80 | "name": "TEXT", 81 | "text": "P" 82 | }, 83 | { 84 | "line": 4, 85 | "column": 5, 86 | "name": "TEXT", 87 | "text": "layer: Hey, Sally. " 88 | }, 89 | { 90 | "line": 4, 91 | "column": 24, 92 | "name": "HASHTAG", 93 | "text": "#" 94 | }, 95 | { 96 | "line": 4, 97 | "column": 25, 98 | "name": "HASHTAG_TEXT", 99 | "text": "line:794945" 100 | }, 101 | { 102 | "line": 4, 103 | "column": 36, 104 | "name": "NEWLINE", 105 | "text": "\n " 106 | }, 107 | { 108 | "line": 5, 109 | "column": 4, 110 | "name": "TEXT", 111 | "text": "S" 112 | }, 113 | { 114 | "line": 5, 115 | "column": 5, 116 | "name": "TEXT", 117 | "text": "ally: Oh! Hi. " 118 | }, 119 | { 120 | "line": 5, 121 | "column": 19, 122 | "name": "HASHTAG", 123 | "text": "#" 124 | }, 125 | { 126 | "line": 5, 127 | "column": 20, 128 | "name": "HASHTAG_TEXT", 129 | "text": "line:2dc39b" 130 | }, 131 | { 132 | "line": 5, 133 | "column": 31, 134 | "name": "NEWLINE", 135 | "text": "\n " 136 | }, 137 | { 138 | "line": 6, 139 | "column": 4, 140 | "name": "TEXT", 141 | "text": "S" 142 | }, 143 | { 144 | "line": 6, 145 | "column": 5, 146 | "name": "TEXT", 147 | "text": "ally: You snuck up on me. " 148 | }, 149 | { 150 | "line": 6, 151 | "column": 31, 152 | "name": "HASHTAG", 153 | "text": "#" 154 | }, 155 | { 156 | "line": 6, 157 | "column": 32, 158 | "name": "HASHTAG_TEXT", 159 | "text": "line:34de2f" 160 | }, 161 | { 162 | "line": 6, 163 | "column": 43, 164 | "name": "NEWLINE", 165 | "text": "\n " 166 | }, 167 | { 168 | "line": 7, 169 | "column": 4, 170 | "name": "TEXT", 171 | "text": "S" 172 | }, 173 | { 174 | "line": 7, 175 | "column": 5, 176 | "name": "TEXT", 177 | "text": "ally: Don\u0027t do that. " 178 | }, 179 | { 180 | "line": 7, 181 | "column": 26, 182 | "name": "HASHTAG", 183 | "text": "#" 184 | }, 185 | { 186 | "line": 7, 187 | "column": 27, 188 | "name": "HASHTAG_TEXT", 189 | "text": "line:dcc2bc" 190 | }, 191 | { 192 | "line": 7, 193 | "column": 38, 194 | "name": "NEWLINE", 195 | "text": "\n" 196 | }, 197 | { 198 | "line": 8, 199 | "column": 0, 200 | "name": "DEDENT", 201 | "text": "\u003Cdedent from 4\u003E" 202 | }, 203 | { 204 | "line": 8, 205 | "column": 0, 206 | "name": "COMMAND_START", 207 | "text": "\u003C\u003C" 208 | }, 209 | { 210 | "line": 8, 211 | "column": 2, 212 | "name": "COMMAND_ELSE", 213 | "text": "else" 214 | }, 215 | { 216 | "line": 8, 217 | "column": 6, 218 | "name": "COMMAND_END", 219 | "text": "\u003E\u003E" 220 | }, 221 | { 222 | "line": 8, 223 | "column": 8, 224 | "name": "NEWLINE", 225 | "text": "\n ", 226 | "channel": "WHITESPACE" 227 | }, 228 | { 229 | "line": 9, 230 | "column": 4, 231 | "name": "INDENT", 232 | "text": "\u003Cindent to 4\u003E" 233 | }, 234 | { 235 | "line": 9, 236 | "column": 4, 237 | "name": "TEXT", 238 | "text": "P" 239 | }, 240 | { 241 | "line": 9, 242 | "column": 5, 243 | "name": "TEXT", 244 | "text": "layer: Hey. " 245 | }, 246 | { 247 | "line": 9, 248 | "column": 17, 249 | "name": "HASHTAG", 250 | "text": "#" 251 | }, 252 | { 253 | "line": 9, 254 | "column": 18, 255 | "name": "HASHTAG_TEXT", 256 | "text": "line:a8e70c" 257 | }, 258 | { 259 | "line": 9, 260 | "column": 29, 261 | "name": "NEWLINE", 262 | "text": "\n " 263 | }, 264 | { 265 | "line": 10, 266 | "column": 4, 267 | "name": "TEXT", 268 | "text": "S" 269 | }, 270 | { 271 | "line": 10, 272 | "column": 5, 273 | "name": "TEXT", 274 | "text": "ally: Hi. " 275 | }, 276 | { 277 | "line": 10, 278 | "column": 15, 279 | "name": "HASHTAG", 280 | "text": "#" 281 | }, 282 | { 283 | "line": 10, 284 | "column": 16, 285 | "name": "HASHTAG_TEXT", 286 | "text": "line:305cde" 287 | }, 288 | { 289 | "line": 10, 290 | "column": 27, 291 | "name": "NEWLINE", 292 | "text": "\n" 293 | }, 294 | { 295 | "line": 11, 296 | "column": 0, 297 | "name": "DEDENT", 298 | "text": "\u003Cdedent from 4\u003E" 299 | }, 300 | { 301 | "line": 11, 302 | "column": 0, 303 | "name": "COMMAND_START", 304 | "text": "\u003C\u003C" 305 | }, 306 | { 307 | "line": 11, 308 | "column": 2, 309 | "name": "COMMAND_ENDIF", 310 | "text": "endif" 311 | }, 312 | { 313 | "line": 11, 314 | "column": 7, 315 | "name": "COMMAND_END", 316 | "text": "\u003E\u003E" 317 | }, 318 | { 319 | "line": 11, 320 | "column": 9, 321 | "name": "NEWLINE", 322 | "text": "\n", 323 | "channel": "WHITESPACE" 324 | }, 325 | { 326 | "line": 12, 327 | "column": 0, 328 | "name": "BODY_END", 329 | "text": "===" 330 | } 331 | ] -------------------------------------------------------------------------------- /test/input/IfStatements.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | <> 4 | Player: Hey, Sally. #line:794945 5 | Sally: Oh! Hi. #line:2dc39b 6 | Sally: You snuck up on me. #line:34de2f 7 | Sally: Don't do that. #line:dcc2bc 8 | <> 9 | Player: Hey. #line:a8e70c 10 | Sally: Hi. #line:305cde 11 | <> 12 | === -------------------------------------------------------------------------------- /test/input/InlineExpressions.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | 4 | <> 5 | 6 | <> 7 | 8 | // Test (numbers, expressions, strings, bools, variables) in (commands, shortcut options, regular options, lines) 9 | 10 | // Lines 11 | Number: {1} 12 | Expression: {1+1} 13 | String: {"string"} 14 | Bool: {true} 15 | Variable: {$var} 16 | 17 | // Lines with the expresion at the start (issue #243) 18 | {$var} is great! 19 | 20 | // Options 21 | -> Option Number: {1} 22 | -> Option Expression: {1+1} 23 | -> Option String: {"string"} 24 | -> Option Bool: {true} 25 | -> Option Variable: {$var} 26 | 27 | // Commands 28 | <> 29 | <> 30 | <> 31 | <> 32 | <> 33 | 34 | === 35 | title: Destination 36 | --- 37 | // no-op; required because the test plan will select 38 | // an option at the end of 'Start' and they all end up here 39 | === -------------------------------------------------------------------------------- /test/input/Jumps-ParseTree.json: -------------------------------------------------------------------------------- 1 | { 2 | "line": 1, 3 | "column": 0, 4 | "name": "dialogue", 5 | "children": [ 6 | { 7 | "line": 1, 8 | "column": 0, 9 | "name": "node", 10 | "children": [ 11 | { 12 | "line": 1, 13 | "column": 0, 14 | "name": "header", 15 | "children": [ 16 | { 17 | "line": 1, 18 | "column": 0, 19 | "name": "ID", 20 | "text": "title" 21 | }, 22 | { 23 | "line": 1, 24 | "column": 5, 25 | "name": "HEADER_DELIMITER", 26 | "text": ": " 27 | }, 28 | { 29 | "line": 1, 30 | "column": 7, 31 | "name": "REST_OF_LINE", 32 | "text": "Start" 33 | } 34 | ] 35 | }, 36 | { 37 | "line": 2, 38 | "column": 0, 39 | "name": "BODY_START", 40 | "text": "---" 41 | }, 42 | { 43 | "line": 3, 44 | "column": 0, 45 | "name": "body", 46 | "children": [ 47 | { 48 | "line": 3, 49 | "column": 0, 50 | "name": "statement", 51 | "children": [ 52 | { 53 | "line": 3, 54 | "column": 0, 55 | "name": "line_statement", 56 | "children": [ 57 | { 58 | "line": 3, 59 | "column": 0, 60 | "name": "line_formatted_text", 61 | "children": [ 62 | { 63 | "line": 3, 64 | "column": 0, 65 | "name": "TEXT", 66 | "text": "S" 67 | }, 68 | { 69 | "line": 3, 70 | "column": 1, 71 | "name": "TEXT", 72 | "text": "tart line" 73 | } 74 | ] 75 | }, 76 | { 77 | "line": 3, 78 | "column": 10, 79 | "name": "NEWLINE", 80 | "text": "\n" 81 | } 82 | ] 83 | } 84 | ] 85 | }, 86 | { 87 | "line": 4, 88 | "column": 0, 89 | "name": "statement", 90 | "children": [ 91 | { 92 | "line": 4, 93 | "column": 0, 94 | "name": "jump_statement", 95 | "children": [ 96 | { 97 | "line": 4, 98 | "column": 0, 99 | "name": "COMMAND_START", 100 | "text": "\u003C\u003C" 101 | }, 102 | { 103 | "line": 4, 104 | "column": 2, 105 | "name": "COMMAND_JUMP", 106 | "text": "jump " 107 | }, 108 | { 109 | "line": 4, 110 | "column": 7, 111 | "name": "ID", 112 | "text": "Destination" 113 | }, 114 | { 115 | "line": 4, 116 | "column": 18, 117 | "name": "COMMAND_END", 118 | "text": "\u003E\u003E" 119 | } 120 | ] 121 | } 122 | ] 123 | }, 124 | { 125 | "line": 5, 126 | "column": 0, 127 | "name": "statement", 128 | "children": [ 129 | { 130 | "line": 5, 131 | "column": 0, 132 | "name": "line_statement", 133 | "children": [ 134 | { 135 | "line": 5, 136 | "column": 0, 137 | "name": "line_formatted_text", 138 | "children": [ 139 | { 140 | "line": 5, 141 | "column": 0, 142 | "name": "TEXT", 143 | "text": "T" 144 | }, 145 | { 146 | "line": 5, 147 | "column": 1, 148 | "name": "TEXT", 149 | "text": "his line will never be shown." 150 | } 151 | ] 152 | }, 153 | { 154 | "line": 5, 155 | "column": 30, 156 | "name": "NEWLINE", 157 | "text": "\n" 158 | } 159 | ] 160 | } 161 | ] 162 | } 163 | ] 164 | }, 165 | { 166 | "line": 6, 167 | "column": 0, 168 | "name": "BODY_END", 169 | "text": "===" 170 | } 171 | ] 172 | }, 173 | { 174 | "line": 7, 175 | "column": 0, 176 | "name": "node", 177 | "children": [ 178 | { 179 | "line": 7, 180 | "column": 0, 181 | "name": "header", 182 | "children": [ 183 | { 184 | "line": 7, 185 | "column": 0, 186 | "name": "ID", 187 | "text": "title" 188 | }, 189 | { 190 | "line": 7, 191 | "column": 5, 192 | "name": "HEADER_DELIMITER", 193 | "text": ": " 194 | }, 195 | { 196 | "line": 7, 197 | "column": 7, 198 | "name": "REST_OF_LINE", 199 | "text": "Destination" 200 | } 201 | ] 202 | }, 203 | { 204 | "line": 8, 205 | "column": 0, 206 | "name": "BODY_START", 207 | "text": "---" 208 | }, 209 | { 210 | "line": 9, 211 | "column": 0, 212 | "name": "body", 213 | "children": [ 214 | { 215 | "line": 9, 216 | "column": 0, 217 | "name": "statement", 218 | "children": [ 219 | { 220 | "line": 9, 221 | "column": 0, 222 | "name": "line_statement", 223 | "children": [ 224 | { 225 | "line": 9, 226 | "column": 0, 227 | "name": "line_formatted_text", 228 | "children": [ 229 | { 230 | "line": 9, 231 | "column": 0, 232 | "name": "TEXT", 233 | "text": "D" 234 | }, 235 | { 236 | "line": 9, 237 | "column": 1, 238 | "name": "TEXT", 239 | "text": "estination line" 240 | } 241 | ] 242 | }, 243 | { 244 | "line": 9, 245 | "column": 16, 246 | "name": "NEWLINE", 247 | "text": "\n" 248 | } 249 | ] 250 | } 251 | ] 252 | } 253 | ] 254 | }, 255 | { 256 | "line": 10, 257 | "column": 0, 258 | "name": "BODY_END", 259 | "text": "===" 260 | } 261 | ] 262 | } 263 | ] 264 | } -------------------------------------------------------------------------------- /test/input/Jumps-Tokens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "line": 1, 4 | "column": 0, 5 | "name": "ID", 6 | "text": "title" 7 | }, 8 | { 9 | "line": 1, 10 | "column": 5, 11 | "name": "HEADER_DELIMITER", 12 | "text": ": " 13 | }, 14 | { 15 | "line": 1, 16 | "column": 7, 17 | "name": "REST_OF_LINE", 18 | "text": "Start" 19 | }, 20 | { 21 | "line": 1, 22 | "column": 12, 23 | "name": "NEWLINE", 24 | "text": "\n", 25 | "channel": "WHITESPACE" 26 | }, 27 | { 28 | "line": 2, 29 | "column": 0, 30 | "name": "BODY_START", 31 | "text": "---" 32 | }, 33 | { 34 | "line": 2, 35 | "column": 3, 36 | "name": "NEWLINE", 37 | "text": "\n", 38 | "channel": "WHITESPACE" 39 | }, 40 | { 41 | "line": 3, 42 | "column": 0, 43 | "name": "TEXT", 44 | "text": "S" 45 | }, 46 | { 47 | "line": 3, 48 | "column": 1, 49 | "name": "TEXT", 50 | "text": "tart line" 51 | }, 52 | { 53 | "line": 3, 54 | "column": 10, 55 | "name": "NEWLINE", 56 | "text": "\n" 57 | }, 58 | { 59 | "line": 4, 60 | "column": 0, 61 | "name": "COMMAND_START", 62 | "text": "\u003C\u003C" 63 | }, 64 | { 65 | "line": 4, 66 | "column": 2, 67 | "name": "COMMAND_JUMP", 68 | "text": "jump " 69 | }, 70 | { 71 | "line": 4, 72 | "column": 7, 73 | "name": "ID", 74 | "text": "Destination" 75 | }, 76 | { 77 | "line": 4, 78 | "column": 18, 79 | "name": "COMMAND_END", 80 | "text": "\u003E\u003E" 81 | }, 82 | { 83 | "line": 4, 84 | "column": 20, 85 | "name": "NEWLINE", 86 | "text": "\n", 87 | "channel": "WHITESPACE" 88 | }, 89 | { 90 | "line": 5, 91 | "column": 0, 92 | "name": "TEXT", 93 | "text": "T" 94 | }, 95 | { 96 | "line": 5, 97 | "column": 1, 98 | "name": "TEXT", 99 | "text": "his line will never be shown." 100 | }, 101 | { 102 | "line": 5, 103 | "column": 30, 104 | "name": "NEWLINE", 105 | "text": "\n" 106 | }, 107 | { 108 | "line": 6, 109 | "column": 0, 110 | "name": "BODY_END", 111 | "text": "===" 112 | }, 113 | { 114 | "line": 6, 115 | "column": 3, 116 | "name": "NEWLINE", 117 | "text": "\n", 118 | "channel": "WHITESPACE" 119 | }, 120 | { 121 | "line": 7, 122 | "column": 0, 123 | "name": "ID", 124 | "text": "title" 125 | }, 126 | { 127 | "line": 7, 128 | "column": 5, 129 | "name": "HEADER_DELIMITER", 130 | "text": ": " 131 | }, 132 | { 133 | "line": 7, 134 | "column": 7, 135 | "name": "REST_OF_LINE", 136 | "text": "Destination" 137 | }, 138 | { 139 | "line": 7, 140 | "column": 18, 141 | "name": "NEWLINE", 142 | "text": "\n", 143 | "channel": "WHITESPACE" 144 | }, 145 | { 146 | "line": 8, 147 | "column": 0, 148 | "name": "BODY_START", 149 | "text": "---" 150 | }, 151 | { 152 | "line": 8, 153 | "column": 3, 154 | "name": "NEWLINE", 155 | "text": "\n", 156 | "channel": "WHITESPACE" 157 | }, 158 | { 159 | "line": 9, 160 | "column": 0, 161 | "name": "TEXT", 162 | "text": "D" 163 | }, 164 | { 165 | "line": 9, 166 | "column": 1, 167 | "name": "TEXT", 168 | "text": "estination line" 169 | }, 170 | { 171 | "line": 9, 172 | "column": 16, 173 | "name": "NEWLINE", 174 | "text": "\n" 175 | }, 176 | { 177 | "line": 10, 178 | "column": 0, 179 | "name": "BODY_END", 180 | "text": "===" 181 | } 182 | ] -------------------------------------------------------------------------------- /test/input/Jumps.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | Start line 4 | <> 5 | This line will never be shown. 6 | === 7 | title: Destination 8 | position: 200,200 9 | --- 10 | Destination line 11 | === -------------------------------------------------------------------------------- /test/input/Lines-ParseTree.json: -------------------------------------------------------------------------------- 1 | { 2 | "line": 1, 3 | "column": 0, 4 | "name": "dialogue", 5 | "children": [ 6 | { 7 | "line": 1, 8 | "column": 0, 9 | "name": "node", 10 | "children": [ 11 | { 12 | "line": 1, 13 | "column": 0, 14 | "name": "header", 15 | "children": [ 16 | { 17 | "line": 1, 18 | "column": 0, 19 | "name": "ID", 20 | "text": "title" 21 | }, 22 | { 23 | "line": 1, 24 | "column": 5, 25 | "name": "HEADER_DELIMITER", 26 | "text": ": " 27 | }, 28 | { 29 | "line": 1, 30 | "column": 7, 31 | "name": "REST_OF_LINE", 32 | "text": "Start" 33 | } 34 | ] 35 | }, 36 | { 37 | "line": 2, 38 | "column": 0, 39 | "name": "BODY_START", 40 | "text": "---" 41 | }, 42 | { 43 | "line": 5, 44 | "column": 0, 45 | "name": "body", 46 | "children": [ 47 | { 48 | "line": 5, 49 | "column": 0, 50 | "name": "statement", 51 | "children": [ 52 | { 53 | "line": 5, 54 | "column": 0, 55 | "name": "line_statement", 56 | "children": [ 57 | { 58 | "line": 5, 59 | "column": 0, 60 | "name": "line_formatted_text", 61 | "children": [ 62 | { 63 | "line": 5, 64 | "column": 0, 65 | "name": "TEXT", 66 | "text": "T" 67 | }, 68 | { 69 | "line": 5, 70 | "column": 1, 71 | "name": "TEXT", 72 | "text": "his is a line of dialogue." 73 | } 74 | ] 75 | }, 76 | { 77 | "line": 5, 78 | "column": 27, 79 | "name": "NEWLINE", 80 | "text": "\n" 81 | } 82 | ] 83 | } 84 | ] 85 | }, 86 | { 87 | "line": 7, 88 | "column": 0, 89 | "name": "statement", 90 | "children": [ 91 | { 92 | "line": 7, 93 | "column": 0, 94 | "name": "line_statement", 95 | "children": [ 96 | { 97 | "line": 7, 98 | "column": 0, 99 | "name": "line_formatted_text", 100 | "children": [ 101 | { 102 | "line": 7, 103 | "column": 0, 104 | "name": "TEXT", 105 | "text": "M" 106 | }, 107 | { 108 | "line": 7, 109 | "column": 1, 110 | "name": "TEXT", 111 | "text": "ae: " 112 | }, 113 | { 114 | "line": 7, 115 | "column": 5, 116 | "name": "TEXT", 117 | "text": "\u003C" 118 | }, 119 | { 120 | "line": 7, 121 | "column": 6, 122 | "name": "TEXT", 123 | "text": "shake=.01\u003EHA HA HA HA HA NO I\u0027M FINE." 124 | }, 125 | { 126 | "line": 7, 127 | "column": 43, 128 | "name": "TEXT", 129 | "text": "\u003C" 130 | }, 131 | { 132 | "line": 7, 133 | "column": 44, 134 | "name": "TEXT", 135 | "text": "/" 136 | }, 137 | { 138 | "line": 7, 139 | "column": 45, 140 | "name": "TEXT", 141 | "text": "shake\u003E" 142 | } 143 | ] 144 | }, 145 | { 146 | "line": 7, 147 | "column": 51, 148 | "name": "NEWLINE", 149 | "text": "\n" 150 | } 151 | ] 152 | } 153 | ] 154 | }, 155 | { 156 | "line": 10, 157 | "column": 0, 158 | "name": "statement", 159 | "children": [ 160 | { 161 | "line": 10, 162 | "column": 0, 163 | "name": "line_statement", 164 | "children": [ 165 | { 166 | "line": 10, 167 | "column": 0, 168 | "name": "line_formatted_text", 169 | "children": [ 170 | { 171 | "line": 10, 172 | "column": 0, 173 | "name": "TEXT", 174 | "text": "M" 175 | }, 176 | { 177 | "line": 10, 178 | "column": 1, 179 | "name": "TEXT", 180 | "text": "ae: Testing comments after lines " 181 | } 182 | ] 183 | }, 184 | { 185 | "line": 10, 186 | "column": 44, 187 | "name": "NEWLINE", 188 | "text": "\n" 189 | } 190 | ] 191 | } 192 | ] 193 | }, 194 | { 195 | "line": 11, 196 | "column": 0, 197 | "name": "statement", 198 | "children": [ 199 | { 200 | "line": 11, 201 | "column": 0, 202 | "name": "line_statement", 203 | "children": [ 204 | { 205 | "line": 11, 206 | "column": 0, 207 | "name": "line_formatted_text", 208 | "children": [ 209 | { 210 | "line": 11, 211 | "column": 0, 212 | "name": "TEXT", 213 | "text": "M" 214 | }, 215 | { 216 | "line": 11, 217 | "column": 1, 218 | "name": "TEXT", 219 | "text": "ae: Testing comments after hashtags " 220 | } 221 | ] 222 | }, 223 | { 224 | "line": 11, 225 | "column": 37, 226 | "name": "hashtag", 227 | "children": [ 228 | { 229 | "line": 11, 230 | "column": 37, 231 | "name": "HASHTAG", 232 | "text": "#" 233 | }, 234 | { 235 | "line": 11, 236 | "column": 38, 237 | "name": "HASHTAG_TEXT", 238 | "text": "hashtag" 239 | } 240 | ] 241 | }, 242 | { 243 | "line": 11, 244 | "column": 56, 245 | "name": "NEWLINE", 246 | "text": "\n" 247 | } 248 | ] 249 | } 250 | ] 251 | }, 252 | { 253 | "line": 12, 254 | "column": 0, 255 | "name": "statement", 256 | "children": [ 257 | { 258 | "line": 12, 259 | "column": 0, 260 | "name": "line_statement", 261 | "children": [ 262 | { 263 | "line": 12, 264 | "column": 0, 265 | "name": "line_formatted_text", 266 | "children": [ 267 | { 268 | "line": 12, 269 | "column": 0, 270 | "name": "TEXT", 271 | "text": "M" 272 | }, 273 | { 274 | "line": 12, 275 | "column": 1, 276 | "name": "TEXT", 277 | "text": "ae: Testing comments after line conditions " 278 | } 279 | ] 280 | }, 281 | { 282 | "line": 12, 283 | "column": 44, 284 | "name": "line_condition", 285 | "children": [ 286 | { 287 | "line": 12, 288 | "column": 44, 289 | "name": "COMMAND_START", 290 | "text": "\u003C\u003C" 291 | }, 292 | { 293 | "line": 12, 294 | "column": 46, 295 | "name": "COMMAND_IF", 296 | "text": "if " 297 | }, 298 | { 299 | "line": 12, 300 | "column": 49, 301 | "name": "expression", 302 | "children": [ 303 | { 304 | "line": 12, 305 | "column": 49, 306 | "name": "value", 307 | "children": [ 308 | { 309 | "line": 12, 310 | "column": 49, 311 | "name": "KEYWORD_TRUE", 312 | "text": "true" 313 | } 314 | ] 315 | } 316 | ] 317 | }, 318 | { 319 | "line": 12, 320 | "column": 53, 321 | "name": "COMMAND_END", 322 | "text": "\u003E\u003E" 323 | } 324 | ] 325 | }, 326 | { 327 | "line": 12, 328 | "column": 66, 329 | "name": "NEWLINE", 330 | "text": "\n" 331 | } 332 | ] 333 | } 334 | ] 335 | } 336 | ] 337 | }, 338 | { 339 | "line": 13, 340 | "column": 0, 341 | "name": "BODY_END", 342 | "text": "===" 343 | } 344 | ] 345 | } 346 | ] 347 | } -------------------------------------------------------------------------------- /test/input/Lines-Tokens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "line": 1, 4 | "column": 0, 5 | "name": "ID", 6 | "text": "title" 7 | }, 8 | { 9 | "line": 1, 10 | "column": 5, 11 | "name": "HEADER_DELIMITER", 12 | "text": ": " 13 | }, 14 | { 15 | "line": 1, 16 | "column": 7, 17 | "name": "REST_OF_LINE", 18 | "text": "Start" 19 | }, 20 | { 21 | "line": 1, 22 | "column": 12, 23 | "name": "NEWLINE", 24 | "text": "\n", 25 | "channel": "WHITESPACE" 26 | }, 27 | { 28 | "line": 2, 29 | "column": 0, 30 | "name": "BODY_START", 31 | "text": "---" 32 | }, 33 | { 34 | "line": 2, 35 | "column": 3, 36 | "name": "NEWLINE", 37 | "text": "\n", 38 | "channel": "WHITESPACE" 39 | }, 40 | { 41 | "line": 3, 42 | "column": 0, 43 | "name": "COMMENT", 44 | "text": "// Tests a single line", 45 | "channel": "COMMENTS" 46 | }, 47 | { 48 | "line": 3, 49 | "column": 22, 50 | "name": "NEWLINE", 51 | "text": "\n", 52 | "channel": "WHITESPACE" 53 | }, 54 | { 55 | "line": 4, 56 | "column": 0, 57 | "name": "NEWLINE", 58 | "text": "\n", 59 | "channel": "WHITESPACE" 60 | }, 61 | { 62 | "line": 5, 63 | "column": 0, 64 | "name": "TEXT", 65 | "text": "T" 66 | }, 67 | { 68 | "line": 5, 69 | "column": 1, 70 | "name": "TEXT", 71 | "text": "his is a line of dialogue." 72 | }, 73 | { 74 | "line": 5, 75 | "column": 27, 76 | "name": "NEWLINE", 77 | "text": "\n" 78 | }, 79 | { 80 | "line": 6, 81 | "column": 0, 82 | "name": "NEWLINE", 83 | "text": "\n", 84 | "channel": "WHITESPACE" 85 | }, 86 | { 87 | "line": 7, 88 | "column": 0, 89 | "name": "TEXT", 90 | "text": "M" 91 | }, 92 | { 93 | "line": 7, 94 | "column": 1, 95 | "name": "TEXT", 96 | "text": "ae: " 97 | }, 98 | { 99 | "line": 7, 100 | "column": 5, 101 | "name": "TEXT", 102 | "text": "\u003C" 103 | }, 104 | { 105 | "line": 7, 106 | "column": 6, 107 | "name": "TEXT", 108 | "text": "shake=.01\u003EHA HA HA HA HA NO I\u0027M FINE." 109 | }, 110 | { 111 | "line": 7, 112 | "column": 43, 113 | "name": "TEXT", 114 | "text": "\u003C" 115 | }, 116 | { 117 | "line": 7, 118 | "column": 44, 119 | "name": "TEXT", 120 | "text": "/" 121 | }, 122 | { 123 | "line": 7, 124 | "column": 45, 125 | "name": "TEXT", 126 | "text": "shake\u003E" 127 | }, 128 | { 129 | "line": 7, 130 | "column": 51, 131 | "name": "NEWLINE", 132 | "text": "\n" 133 | }, 134 | { 135 | "line": 8, 136 | "column": 0, 137 | "name": "NEWLINE", 138 | "text": "\n", 139 | "channel": "WHITESPACE" 140 | }, 141 | { 142 | "line": 9, 143 | "column": 0, 144 | "name": "COMMENT", 145 | "text": "// Comments after lines", 146 | "channel": "COMMENTS" 147 | }, 148 | { 149 | "line": 9, 150 | "column": 23, 151 | "name": "NEWLINE", 152 | "text": "\n", 153 | "channel": "WHITESPACE" 154 | }, 155 | { 156 | "line": 10, 157 | "column": 0, 158 | "name": "TEXT", 159 | "text": "M" 160 | }, 161 | { 162 | "line": 10, 163 | "column": 1, 164 | "name": "TEXT", 165 | "text": "ae: Testing comments after lines " 166 | }, 167 | { 168 | "line": 10, 169 | "column": 34, 170 | "name": "TEXT_COMMENT", 171 | "text": "// comment", 172 | "channel": "COMMENTS" 173 | }, 174 | { 175 | "line": 10, 176 | "column": 44, 177 | "name": "NEWLINE", 178 | "text": "\n" 179 | }, 180 | { 181 | "line": 11, 182 | "column": 0, 183 | "name": "TEXT", 184 | "text": "M" 185 | }, 186 | { 187 | "line": 11, 188 | "column": 1, 189 | "name": "TEXT", 190 | "text": "ae: Testing comments after hashtags " 191 | }, 192 | { 193 | "line": 11, 194 | "column": 37, 195 | "name": "HASHTAG", 196 | "text": "#" 197 | }, 198 | { 199 | "line": 11, 200 | "column": 38, 201 | "name": "HASHTAG_TEXT", 202 | "text": "hashtag" 203 | }, 204 | { 205 | "line": 11, 206 | "column": 46, 207 | "name": "TEXT_COMMANDHASHTAG_COMMENT", 208 | "text": "// comment", 209 | "channel": "COMMENTS" 210 | }, 211 | { 212 | "line": 11, 213 | "column": 56, 214 | "name": "NEWLINE", 215 | "text": "\n" 216 | }, 217 | { 218 | "line": 12, 219 | "column": 0, 220 | "name": "TEXT", 221 | "text": "M" 222 | }, 223 | { 224 | "line": 12, 225 | "column": 1, 226 | "name": "TEXT", 227 | "text": "ae: Testing comments after line conditions " 228 | }, 229 | { 230 | "line": 12, 231 | "column": 44, 232 | "name": "COMMAND_START", 233 | "text": "\u003C\u003C" 234 | }, 235 | { 236 | "line": 12, 237 | "column": 46, 238 | "name": "COMMAND_IF", 239 | "text": "if " 240 | }, 241 | { 242 | "line": 12, 243 | "column": 49, 244 | "name": "KEYWORD_TRUE", 245 | "text": "true" 246 | }, 247 | { 248 | "line": 12, 249 | "column": 53, 250 | "name": "COMMAND_END", 251 | "text": "\u003E\u003E" 252 | }, 253 | { 254 | "line": 12, 255 | "column": 56, 256 | "name": "TEXT_COMMANDHASHTAG_COMMENT", 257 | "text": "// comment", 258 | "channel": "COMMENTS" 259 | }, 260 | { 261 | "line": 12, 262 | "column": 66, 263 | "name": "NEWLINE", 264 | "text": "\n" 265 | }, 266 | { 267 | "line": 13, 268 | "column": 0, 269 | "name": "BODY_END", 270 | "text": "===" 271 | } 272 | ] -------------------------------------------------------------------------------- /test/input/Lines.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | // Tests a single line 4 | 5 | This is a line of dialogue. 6 | 7 | Mae: HA HA HA HA HA NO I'M FINE. 8 | 9 | // Comments after lines 10 | Mae: Testing comments after lines // comment 11 | Mae: Testing comments after hashtags #hashtag // comment 12 | Mae: Testing comments after line conditions <> // comment 13 | === -------------------------------------------------------------------------------- /test/input/MyCoolCommands.ysls.json: -------------------------------------------------------------------------------- 1 | { 2 | "Commands": [ 3 | { 4 | "Language": "csharp", 5 | "YarnName": "do_cool_thing", 6 | "DefinitionName": "DoCoolThing", 7 | "Documentation": "Does a cool thing.", 8 | "Parameters": [ 9 | { 10 | "Name": "thing", 11 | "Type": "String", 12 | "Documentation": "The thing to do.", 13 | "DefaultValue": "nifty" 14 | } 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /test/input/NodeHeaders-ParseTree.json: -------------------------------------------------------------------------------- 1 | { 2 | "line": 1, 3 | "column": 0, 4 | "name": "dialogue", 5 | "children": [ 6 | { 7 | "line": 1, 8 | "column": 0, 9 | "name": "node", 10 | "children": [ 11 | { 12 | "line": 1, 13 | "column": 0, 14 | "name": "header", 15 | "children": [ 16 | { 17 | "line": 1, 18 | "column": 0, 19 | "name": "ID", 20 | "text": "title" 21 | }, 22 | { 23 | "line": 1, 24 | "column": 5, 25 | "name": "HEADER_DELIMITER", 26 | "text": ": " 27 | }, 28 | { 29 | "line": 1, 30 | "column": 7, 31 | "name": "REST_OF_LINE", 32 | "text": "Start" 33 | } 34 | ] 35 | }, 36 | { 37 | "line": 2, 38 | "column": 0, 39 | "name": "BODY_START", 40 | "text": "---" 41 | }, 42 | { 43 | "line": 8, 44 | "column": 0, 45 | "name": "body", 46 | "children": [] 47 | }, 48 | { 49 | "line": 8, 50 | "column": 0, 51 | "name": "BODY_END", 52 | "text": "===" 53 | } 54 | ] 55 | }, 56 | { 57 | "line": 9, 58 | "column": 0, 59 | "name": "node", 60 | "children": [ 61 | { 62 | "line": 9, 63 | "column": 0, 64 | "name": "header", 65 | "children": [ 66 | { 67 | "line": 9, 68 | "column": 0, 69 | "name": "ID", 70 | "text": "title" 71 | }, 72 | { 73 | "line": 9, 74 | "column": 5, 75 | "name": "HEADER_DELIMITER", 76 | "text": ": " 77 | }, 78 | { 79 | "line": 9, 80 | "column": 7, 81 | "name": "REST_OF_LINE", 82 | "text": "TestNode1" 83 | } 84 | ] 85 | }, 86 | { 87 | "line": 10, 88 | "column": 0, 89 | "name": "header", 90 | "children": [ 91 | { 92 | "line": 10, 93 | "column": 0, 94 | "name": "ID", 95 | "text": "tags" 96 | }, 97 | { 98 | "line": 10, 99 | "column": 4, 100 | "name": "HEADER_DELIMITER", 101 | "text": ": " 102 | }, 103 | { 104 | "line": 10, 105 | "column": 6, 106 | "name": "REST_OF_LINE", 107 | "text": "one two" 108 | } 109 | ] 110 | }, 111 | { 112 | "line": 11, 113 | "column": 0, 114 | "name": "BODY_START", 115 | "text": "---" 116 | }, 117 | { 118 | "line": 12, 119 | "column": 0, 120 | "name": "body", 121 | "children": [] 122 | }, 123 | { 124 | "line": 12, 125 | "column": 0, 126 | "name": "BODY_END", 127 | "text": "===" 128 | } 129 | ] 130 | }, 131 | { 132 | "line": 13, 133 | "column": 0, 134 | "name": "node", 135 | "children": [ 136 | { 137 | "line": 13, 138 | "column": 0, 139 | "name": "header", 140 | "children": [ 141 | { 142 | "line": 13, 143 | "column": 0, 144 | "name": "ID", 145 | "text": "title" 146 | }, 147 | { 148 | "line": 13, 149 | "column": 5, 150 | "name": "HEADER_DELIMITER", 151 | "text": ": " 152 | }, 153 | { 154 | "line": 13, 155 | "column": 7, 156 | "name": "REST_OF_LINE", 157 | "text": "TestNode2" 158 | } 159 | ] 160 | }, 161 | { 162 | "line": 14, 163 | "column": 0, 164 | "name": "header", 165 | "children": [ 166 | { 167 | "line": 14, 168 | "column": 0, 169 | "name": "ID", 170 | "text": "tags" 171 | }, 172 | { 173 | "line": 14, 174 | "column": 4, 175 | "name": "HEADER_DELIMITER", 176 | "text": ":" 177 | } 178 | ] 179 | }, 180 | { 181 | "line": 15, 182 | "column": 0, 183 | "name": "BODY_START", 184 | "text": "---" 185 | }, 186 | { 187 | "line": 16, 188 | "column": 0, 189 | "name": "body", 190 | "children": [] 191 | }, 192 | { 193 | "line": 16, 194 | "column": 0, 195 | "name": "BODY_END", 196 | "text": "===" 197 | } 198 | ] 199 | }, 200 | { 201 | "line": 17, 202 | "column": 0, 203 | "name": "node", 204 | "children": [ 205 | { 206 | "line": 17, 207 | "column": 0, 208 | "name": "header", 209 | "children": [ 210 | { 211 | "line": 17, 212 | "column": 0, 213 | "name": "ID", 214 | "text": "title" 215 | }, 216 | { 217 | "line": 17, 218 | "column": 5, 219 | "name": "HEADER_DELIMITER", 220 | "text": ": " 221 | }, 222 | { 223 | "line": 17, 224 | "column": 7, 225 | "name": "REST_OF_LINE", 226 | "text": "TestNode3" 227 | } 228 | ] 229 | }, 230 | { 231 | "line": 18, 232 | "column": 0, 233 | "name": "header", 234 | "children": [ 235 | { 236 | "line": 18, 237 | "column": 0, 238 | "name": "ID", 239 | "text": "tags" 240 | }, 241 | { 242 | "line": 18, 243 | "column": 4, 244 | "name": "HEADER_DELIMITER", 245 | "text": ":" 246 | } 247 | ] 248 | }, 249 | { 250 | "line": 19, 251 | "column": 0, 252 | "name": "header", 253 | "children": [ 254 | { 255 | "line": 19, 256 | "column": 0, 257 | "name": "ID", 258 | "text": "test" 259 | }, 260 | { 261 | "line": 19, 262 | "column": 4, 263 | "name": "HEADER_DELIMITER", 264 | "text": ": " 265 | }, 266 | { 267 | "line": 19, 268 | "column": 6, 269 | "name": "REST_OF_LINE", 270 | "text": "example" 271 | } 272 | ] 273 | }, 274 | { 275 | "line": 20, 276 | "column": 0, 277 | "name": "BODY_START", 278 | "text": "---" 279 | }, 280 | { 281 | "line": 21, 282 | "column": 0, 283 | "name": "body", 284 | "children": [] 285 | }, 286 | { 287 | "line": 21, 288 | "column": 0, 289 | "name": "BODY_END", 290 | "text": "===" 291 | } 292 | ] 293 | } 294 | ] 295 | } -------------------------------------------------------------------------------- /test/input/NodeHeaders-Tokens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "line": 1, 4 | "column": 0, 5 | "name": "ID", 6 | "text": "title" 7 | }, 8 | { 9 | "line": 1, 10 | "column": 5, 11 | "name": "HEADER_DELIMITER", 12 | "text": ": " 13 | }, 14 | { 15 | "line": 1, 16 | "column": 7, 17 | "name": "REST_OF_LINE", 18 | "text": "Start" 19 | }, 20 | { 21 | "line": 1, 22 | "column": 12, 23 | "name": "NEWLINE", 24 | "text": "\n", 25 | "channel": "WHITESPACE" 26 | }, 27 | { 28 | "line": 2, 29 | "column": 0, 30 | "name": "BODY_START", 31 | "text": "---" 32 | }, 33 | { 34 | "line": 2, 35 | "column": 3, 36 | "name": "NEWLINE", 37 | "text": "\n", 38 | "channel": "WHITESPACE" 39 | }, 40 | { 41 | "line": 3, 42 | "column": 0, 43 | "name": "COMMENT", 44 | "text": "// this test is a no-op - the test passes if the file compiles.", 45 | "channel": "COMMENTS" 46 | }, 47 | { 48 | "line": 3, 49 | "column": 63, 50 | "name": "NEWLINE", 51 | "text": "\n", 52 | "channel": "WHITESPACE" 53 | }, 54 | { 55 | "line": 4, 56 | "column": 0, 57 | "name": "COMMENT", 58 | "text": "// Start demonstrates a node with a single header, its title (which is mandatory).", 59 | "channel": "COMMENTS" 60 | }, 61 | { 62 | "line": 4, 63 | "column": 82, 64 | "name": "NEWLINE", 65 | "text": "\n", 66 | "channel": "WHITESPACE" 67 | }, 68 | { 69 | "line": 5, 70 | "column": 0, 71 | "name": "COMMENT", 72 | "text": "// TestNode1 demonstrates a node with two headers: its title, and a populated tag header.", 73 | "channel": "COMMENTS" 74 | }, 75 | { 76 | "line": 5, 77 | "column": 89, 78 | "name": "NEWLINE", 79 | "text": "\n", 80 | "channel": "WHITESPACE" 81 | }, 82 | { 83 | "line": 6, 84 | "column": 0, 85 | "name": "COMMENT", 86 | "text": "// TestNode2 demonstrates a node with two headers: its title, and a tag header with no text.", 87 | "channel": "COMMENTS" 88 | }, 89 | { 90 | "line": 6, 91 | "column": 92, 92 | "name": "NEWLINE", 93 | "text": "\n", 94 | "channel": "WHITESPACE" 95 | }, 96 | { 97 | "line": 7, 98 | "column": 0, 99 | "name": "COMMENT", 100 | "text": "// TestNode3 demonstrates a node with three headers: its title, an empty tag, and a populated tag.", 101 | "channel": "COMMENTS" 102 | }, 103 | { 104 | "line": 7, 105 | "column": 98, 106 | "name": "NEWLINE", 107 | "text": "\n", 108 | "channel": "WHITESPACE" 109 | }, 110 | { 111 | "line": 8, 112 | "column": 0, 113 | "name": "BODY_END", 114 | "text": "===" 115 | }, 116 | { 117 | "line": 8, 118 | "column": 3, 119 | "name": "NEWLINE", 120 | "text": "\n", 121 | "channel": "WHITESPACE" 122 | }, 123 | { 124 | "line": 9, 125 | "column": 0, 126 | "name": "ID", 127 | "text": "title" 128 | }, 129 | { 130 | "line": 9, 131 | "column": 5, 132 | "name": "HEADER_DELIMITER", 133 | "text": ": " 134 | }, 135 | { 136 | "line": 9, 137 | "column": 7, 138 | "name": "REST_OF_LINE", 139 | "text": "TestNode1" 140 | }, 141 | { 142 | "line": 9, 143 | "column": 16, 144 | "name": "NEWLINE", 145 | "text": "\n", 146 | "channel": "WHITESPACE" 147 | }, 148 | { 149 | "line": 10, 150 | "column": 0, 151 | "name": "ID", 152 | "text": "tags" 153 | }, 154 | { 155 | "line": 10, 156 | "column": 4, 157 | "name": "HEADER_DELIMITER", 158 | "text": ": " 159 | }, 160 | { 161 | "line": 10, 162 | "column": 6, 163 | "name": "REST_OF_LINE", 164 | "text": "one two" 165 | }, 166 | { 167 | "line": 10, 168 | "column": 13, 169 | "name": "NEWLINE", 170 | "text": "\n", 171 | "channel": "WHITESPACE" 172 | }, 173 | { 174 | "line": 11, 175 | "column": 0, 176 | "name": "BODY_START", 177 | "text": "---" 178 | }, 179 | { 180 | "line": 11, 181 | "column": 3, 182 | "name": "NEWLINE", 183 | "text": "\n", 184 | "channel": "WHITESPACE" 185 | }, 186 | { 187 | "line": 12, 188 | "column": 0, 189 | "name": "BODY_END", 190 | "text": "===" 191 | }, 192 | { 193 | "line": 12, 194 | "column": 3, 195 | "name": "NEWLINE", 196 | "text": "\n", 197 | "channel": "WHITESPACE" 198 | }, 199 | { 200 | "line": 13, 201 | "column": 0, 202 | "name": "ID", 203 | "text": "title" 204 | }, 205 | { 206 | "line": 13, 207 | "column": 5, 208 | "name": "HEADER_DELIMITER", 209 | "text": ": " 210 | }, 211 | { 212 | "line": 13, 213 | "column": 7, 214 | "name": "REST_OF_LINE", 215 | "text": "TestNode2" 216 | }, 217 | { 218 | "line": 13, 219 | "column": 16, 220 | "name": "NEWLINE", 221 | "text": "\n", 222 | "channel": "WHITESPACE" 223 | }, 224 | { 225 | "line": 14, 226 | "column": 0, 227 | "name": "ID", 228 | "text": "tags" 229 | }, 230 | { 231 | "line": 14, 232 | "column": 4, 233 | "name": "HEADER_DELIMITER", 234 | "text": ":" 235 | }, 236 | { 237 | "line": 14, 238 | "column": 5, 239 | "name": "NEWLINE", 240 | "text": "\n", 241 | "channel": "WHITESPACE" 242 | }, 243 | { 244 | "line": 15, 245 | "column": 0, 246 | "name": "BODY_START", 247 | "text": "---" 248 | }, 249 | { 250 | "line": 15, 251 | "column": 3, 252 | "name": "NEWLINE", 253 | "text": "\n", 254 | "channel": "WHITESPACE" 255 | }, 256 | { 257 | "line": 16, 258 | "column": 0, 259 | "name": "BODY_END", 260 | "text": "===" 261 | }, 262 | { 263 | "line": 16, 264 | "column": 3, 265 | "name": "NEWLINE", 266 | "text": "\n", 267 | "channel": "WHITESPACE" 268 | }, 269 | { 270 | "line": 17, 271 | "column": 0, 272 | "name": "ID", 273 | "text": "title" 274 | }, 275 | { 276 | "line": 17, 277 | "column": 5, 278 | "name": "HEADER_DELIMITER", 279 | "text": ": " 280 | }, 281 | { 282 | "line": 17, 283 | "column": 7, 284 | "name": "REST_OF_LINE", 285 | "text": "TestNode3" 286 | }, 287 | { 288 | "line": 17, 289 | "column": 16, 290 | "name": "NEWLINE", 291 | "text": "\n", 292 | "channel": "WHITESPACE" 293 | }, 294 | { 295 | "line": 18, 296 | "column": 0, 297 | "name": "ID", 298 | "text": "tags" 299 | }, 300 | { 301 | "line": 18, 302 | "column": 4, 303 | "name": "HEADER_DELIMITER", 304 | "text": ":" 305 | }, 306 | { 307 | "line": 18, 308 | "column": 5, 309 | "name": "NEWLINE", 310 | "text": "\n", 311 | "channel": "WHITESPACE" 312 | }, 313 | { 314 | "line": 19, 315 | "column": 0, 316 | "name": "ID", 317 | "text": "test" 318 | }, 319 | { 320 | "line": 19, 321 | "column": 4, 322 | "name": "HEADER_DELIMITER", 323 | "text": ": " 324 | }, 325 | { 326 | "line": 19, 327 | "column": 6, 328 | "name": "REST_OF_LINE", 329 | "text": "example" 330 | }, 331 | { 332 | "line": 19, 333 | "column": 13, 334 | "name": "NEWLINE", 335 | "text": "\n", 336 | "channel": "WHITESPACE" 337 | }, 338 | { 339 | "line": 20, 340 | "column": 0, 341 | "name": "BODY_START", 342 | "text": "---" 343 | }, 344 | { 345 | "line": 20, 346 | "column": 3, 347 | "name": "NEWLINE", 348 | "text": "\n", 349 | "channel": "WHITESPACE" 350 | }, 351 | { 352 | "line": 21, 353 | "column": 0, 354 | "name": "BODY_END", 355 | "text": "===" 356 | }, 357 | { 358 | "line": 21, 359 | "column": 3, 360 | "name": "NEWLINE", 361 | "text": "\n", 362 | "channel": "WHITESPACE" 363 | } 364 | ] -------------------------------------------------------------------------------- /test/input/NodeHeaders.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | // this test is a no-op - the test passes if the file compiles. 4 | // Start demonstrates a node with a single header, its title (which is mandatory). 5 | // TestNode1 demonstrates a node with two headers: its title, and a populated tag header. 6 | // TestNode2 demonstrates a node with two headers: its title, and a tag header with no text. 7 | // TestNode3 demonstrates a node with three headers: its title, an empty tag, and a populated tag. 8 | === 9 | title: TestNode1 10 | tags: one two 11 | --- 12 | === 13 | title: TestNode2 14 | tags: 15 | --- 16 | === 17 | title: TestNode3 18 | tags: 19 | test: example 20 | --- 21 | === 22 | -------------------------------------------------------------------------------- /test/input/ShortcutOptions.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | // Testing options 4 | 5 | // Expect 3 options (the second one in the following group of three will be 6 | // marked as disabled) and select the first one 7 | 8 | <> 9 | 10 | -> Option 1 11 | <> 12 | This line should appear. 13 | -> Sub Option 1 14 | // should be selected 15 | This line should also appear. 16 | <> 17 | -> Sub Option 2 18 | -> Sub Option 3 <> 19 | -> Option 2 <> 20 | This line should not appear. 21 | -> Option 3 22 | This line should not appear. 23 | 24 | // Test two shortcut options with no text 25 | 26 | Bea: line text 27 | -> option1 28 | -> option2 29 | Bea: line text2 30 | 31 | 32 | Bea: indented line text 33 | -> indented option1 34 | -> indented option2 35 | Bea: indented line text2 36 | 37 | 38 | Bea: line text 39 | ->indented option1 following unindented line 40 | ->indented option2 following unindented line 41 | option2.1 42 | option2.2 43 | option2.3 44 | option2.4 45 | 46 | // Single-character shortcut options 47 | -> A 48 | B 49 | 50 | (Break up the options) 51 | 52 | // Shortcut options and line tags should work regardless of whitespace 53 | 54 | // No whitespace between condition and line tag 55 | -> Option A <>#line:0e8a7ce 56 | -> Option B #line:0405c66 57 | 58 | (Break up the options) 59 | 60 | // Whitespace between condition and line tag 61 | -> Option A <> #line:0e8a7cd 62 | -> Option B #line:0405c67 63 | 64 | (Break up the options) 65 | 66 | // Single-line shortcut options inside an indented context 67 | <> 68 | -> Option1 69 | <> 70 | -> Option2 71 | <> 72 | 73 | === -------------------------------------------------------------------------------- /test/input/Smileys.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | // Testing smileys 4 | 5 | 6 | Mae: ¯\\_(ツ)_/¯ 7 | Mae: 8 | Mae: :| 9 | Mae: :) 10 | Mae: :( 11 | Mae: :o 12 | Mae: :\\ 13 | Mae: :< 14 | Mae: D: 15 | Mae: -_- 16 | Mae: =_= 17 | Mae: U_U 18 | Mae: O_O 19 | Mae: o_o 20 | Mae: \\o/ 21 | Mae: /o\\ 22 | Mae: o/ 23 | Mae: _o_ 24 | Mae: o> 25 | Mae: / \\ 26 | Mae: \\ / 27 | Mae: ~~ 28 | 29 | // Testing in an option group 30 | 31 | 32 | 33 | -> Mae: ¯\\_(ツ)_/¯ 34 | -> Mae: 35 | -> Mae: :| 36 | -> Mae: :) 37 | -> Mae: :( 38 | -> Mae: :o 39 | -> Mae: :\\ 40 | -> Mae: :< 41 | -> Mae: D: 42 | -> Mae: -_- 43 | -> Mae: =_= 44 | -> Mae: U_U 45 | -> Mae: O_O 46 | -> Mae: o_o 47 | -> Mae: \\o/ 48 | -> Mae: /o\\ 49 | -> Mae: o/ 50 | -> Mae: _o_ 51 | -> Mae: o> 52 | -> Mae: / \\ 53 | -> Mae: \\ / 54 | -> Mae: ~~ 55 | 56 | Separating the option groups here. 57 | 58 | // Testing in an option group, with conditionals 59 | 60 | 61 | -> Mae: ¯\\_(ツ)_/¯ <> 62 | -> Mae: <> 63 | -> Mae: :| <> 64 | -> Mae: :) <> 65 | -> Mae: :( <> 66 | -> Mae: :o <> 67 | -> Mae: :\\ <> 68 | -> Mae: :< <> 69 | -> Mae: D: <> 70 | -> Mae: -_- <> 71 | -> Mae: =_= <> 72 | -> Mae: U_U <> 73 | -> Mae: O_O <> 74 | -> Mae: o_o <> 75 | -> Mae: \\o/ <> 76 | -> Mae: /o\\ <> 77 | -> Mae: o/ <> 78 | -> Mae: _o_ <> 79 | -> Mae: o> <> 80 | -> Mae: / \\ <> 81 | -> Mae: \\ / <> 82 | -> Mae: ~~ <> 83 | === -------------------------------------------------------------------------------- /test/input/Types.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | // Testing types 4 | 5 | // string equality 6 | <> 7 | <> 8 | 9 | // string case sensitivity 10 | <> 11 | <> 12 | 13 | // gt test 14 | < 2)>> 15 | <> 16 | 17 | // lt test 18 | <> 19 | <> 20 | 21 | // lte test 22 | <> 23 | <> 24 | 25 | // gte test 26 | <= 2)>> 27 | <> 28 | === -------------------------------------------------------------------------------- /test/input/VariableStorage.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | // Variable testing 4 | 5 | <> 6 | <> 7 | <> 8 | <> 9 | <> 10 | <> 11 | 12 | // Testing variable storage 13 | <> 14 | 15 | // Test numeric storage 16 | <> 17 | 18 | <> 19 | 20 | // Test numeric storage (negative) 21 | <> 22 | 23 | // Test string storage 24 | <> 25 | <> 26 | 27 | // Test immediate value arithmetic 28 | <> 29 | 30 | <> 31 | 32 | // Test immediate addition 33 | <> 34 | 35 | <> 36 | 37 | // Test immediate subtraction 38 | <> 39 | 40 | <> 41 | <> 42 | 43 | // Test immediate multiplication 44 | <> 45 | 46 | <> 47 | 48 | // Test immediate division 49 | <> 50 | 51 | <> 52 | <> 53 | 54 | // Test immediate assignment addition 55 | <> 56 | 57 | <> 58 | 59 | // Test immediate assignment multiplication 60 | <> 61 | 62 | <> 63 | 64 | // Test immediate assignment division 65 | <> 66 | 67 | <> 68 | 69 | // Test immediate assignment subtraction 70 | <> 71 | 72 | // Testing variable arithmetic 73 | <> 74 | <> 75 | <> 76 | 77 | // Test variable addition 78 | <> 79 | 80 | <> 81 | 82 | // Test variable subtraction 83 | <> 84 | 85 | <> 86 | <> 87 | 88 | // Test variable multiplication 89 | <> 90 | 91 | <> 92 | 93 | // Test variable division 94 | <> 95 | 96 | // Testing string addition 97 | <> 98 | <> 99 | <> 100 | 101 | // "Test immediate string addition 102 | <> 103 | 104 | <> 105 | 106 | // "Test string addition 107 | <> 108 | 109 | === -------------------------------------------------------------------------------- /webview/GroupView.ts: -------------------------------------------------------------------------------- 1 | import { NodeView } from "./NodeView"; 2 | import { Position, Size } from "./util"; 3 | 4 | const taggedGroupViewPadding: number = 20; 5 | const nodeGroupViewPadding: number = 25; 6 | const groupViewHeaderSize: number = 20; 7 | 8 | export enum GroupType { 9 | /** A group defined by the "group" header in a node. */ 10 | TaggedGroup, 11 | /** A Yarn node group defined by the presence of when: headers. */ 12 | NodeGroup, 13 | } 14 | 15 | export class GroupView { 16 | public element: HTMLElement; 17 | 18 | private _nodeViews: NodeView[] = []; 19 | 20 | private _nameNode: Text; 21 | type: GroupType; 22 | 23 | constructor(type: GroupType) { 24 | this.type = type; 25 | this.element = GroupView.createElement(type); 26 | this._nameNode = document.createTextNode("Group"); 27 | this.element.prepend(this._nameNode); 28 | } 29 | 30 | public get nodeViews() { 31 | return this._nodeViews; 32 | } 33 | 34 | public set nodeViews(value: NodeView[]) { 35 | this._nodeViews = value; 36 | 37 | this.refreshSize(); 38 | } 39 | 40 | private _position: Position = { x: 0, y: 0 }; 41 | private _size: Size = { width: 0, height: 0 }; 42 | 43 | private static createElement(type: GroupType): HTMLElement { 44 | const element = document.createElement("div"); 45 | element.classList.add("group"); 46 | 47 | if (type == GroupType.NodeGroup) { 48 | element.classList.add("node-group"); 49 | } 50 | 51 | const background = document.createElement("div"); 52 | background.classList.add("group-background"); 53 | element.appendChild(background); 54 | 55 | return element; 56 | } 57 | 58 | public refreshSize() { 59 | if (this._nodeViews.length == 0) { 60 | this.element.style.display = "none"; 61 | return; 62 | } else { 63 | this.element.style.display = "block"; 64 | } 65 | 66 | const groupViewPadding = 67 | this.type == GroupType.TaggedGroup 68 | ? taggedGroupViewPadding 69 | : nodeGroupViewPadding; 70 | 71 | let left = this._nodeViews.reduce( 72 | (value, node) => (node.left < value ? node.left : value), 73 | Number.POSITIVE_INFINITY, 74 | ); 75 | let top = this._nodeViews.reduce( 76 | (value, node) => (node.top < value ? node.top : value), 77 | Number.POSITIVE_INFINITY, 78 | ); 79 | let right = this._nodeViews.reduce( 80 | (value, node) => (node.right > value ? node.right : value), 81 | Number.NEGATIVE_INFINITY, 82 | ); 83 | let bottom = this._nodeViews.reduce( 84 | (value, node) => (node.bottom > value ? node.bottom : value), 85 | Number.NEGATIVE_INFINITY, 86 | ); 87 | 88 | left -= groupViewPadding; 89 | right += groupViewPadding; 90 | top -= groupViewPadding + groupViewHeaderSize; 91 | bottom += groupViewPadding; 92 | 93 | const width = right - left; 94 | const height = bottom - top; 95 | 96 | if (width <= 0 || height <= 0) { 97 | // Invalid rect! Stop here. 98 | this.element.style.display = "none"; 99 | return; 100 | } 101 | 102 | this.position = { x: left, y: top }; 103 | this.size = { width: width, height: height }; 104 | } 105 | 106 | public set position(position: Position) { 107 | this._position = position; 108 | this.element.style.transform = `translate(${position.x}px, ${position.y}px)`; 109 | } 110 | 111 | public get position(): Position { 112 | return this._position; 113 | } 114 | 115 | public get size(): Size { 116 | return this._size; 117 | } 118 | 119 | public set size(size: Size) { 120 | this._size = size; 121 | this.element.style.width = `${size.width.toString()}px`; 122 | this.element.style.height = `${size.height.toString()}px`; 123 | } 124 | 125 | public set name(name: string) { 126 | this._nameNode.textContent = name; 127 | } 128 | 129 | public get name() { 130 | return this._nameNode.textContent ?? ""; 131 | } 132 | } 133 | 134 | export class NodeGroupView extends GroupView {} 135 | -------------------------------------------------------------------------------- /webview/NodeView.ts: -------------------------------------------------------------------------------- 1 | import { GroupView } from "./GroupView"; 2 | import { NodeInfo } from "./nodes"; 3 | import { getPositionFromNodeInfo, Position, Size } from "./util"; 4 | 5 | type OutgoingConnection = { 6 | connectionType: "Jump" | "Detour"; 7 | } & ( 8 | | { 9 | destinationType: "Node"; 10 | nodeView: NodeView; 11 | } 12 | | { destinationType: "NodeGroup"; groupView: GroupView } 13 | ); 14 | 15 | export class NodeView { 16 | nodeName: string = ""; 17 | element: HTMLElement; 18 | 19 | /** The names of the tagged groups that this node view is in. */ 20 | taggedGroups: string[] = []; 21 | 22 | /** The names of the node groups that the node view is in. */ 23 | nodeGroup: string | null = null; 24 | 25 | private _position: Position = { x: 0, y: 0 }; 26 | 27 | private _color: string | null = null; 28 | 29 | outgoingConnections: OutgoingConnection[] = []; 30 | 31 | public onNodeEditClicked: (node: NodeView) => void = () => {}; 32 | public onNodeDeleteClicked: (node: NodeView) => void = () => {}; 33 | public onNodeDragStart: (node: NodeView) => void = () => {}; 34 | public onNodeDragMove: ( 35 | node: NodeView, 36 | fromPosition: Position, 37 | currentPosition: Position, 38 | ) => void = () => {}; 39 | public onNodeDragEnd: (node: NodeView) => void = () => {}; 40 | public onColorBarClicked: (nodeView: NodeView) => void = () => {}; 41 | 42 | constructor(node: NodeInfo) { 43 | this.element = this.createElement(); 44 | 45 | this.nodeInfo = node; 46 | 47 | this.makeDraggable(); 48 | } 49 | 50 | private makeDraggable() { 51 | /** The current position of the drag, in client space. */ 52 | let currentPosition: Position = { x: 0, y: 0 }; 53 | 54 | const onNodeDragStart = (e: MouseEvent) => { 55 | e.preventDefault(); 56 | e.stopPropagation(); 57 | currentPosition = { x: e.clientX, y: e.clientY }; 58 | 59 | window.addEventListener("mousemove", onNodeDragMove); 60 | window.addEventListener("mouseup", onNodeDragEnd); 61 | 62 | this.onNodeDragStart(this); 63 | }; 64 | 65 | const onNodeDragMove = (e: MouseEvent) => { 66 | e.preventDefault(); 67 | e.stopPropagation(); 68 | 69 | const dragPosition = { x: e.clientX, y: e.clientY }; 70 | 71 | this.onNodeDragMove(this, currentPosition, dragPosition); 72 | 73 | currentPosition = dragPosition; 74 | }; 75 | 76 | const onNodeDragEnd = (e: MouseEvent) => { 77 | window.removeEventListener("mousemove", onNodeDragMove); 78 | window.removeEventListener("mouseup", onNodeDragEnd); 79 | 80 | this.onNodeDragEnd(this); 81 | }; 82 | 83 | const onNodeDoubleClick = (e: MouseEvent) => { 84 | // Double-clicking a node is the same as clicking the 'Edit' button 85 | this.onNodeEditClicked(this); 86 | }; 87 | 88 | this.element.addEventListener("mousedown", onNodeDragStart); 89 | this.element.addEventListener("dblclick", onNodeDoubleClick); 90 | } 91 | 92 | private createElement(): HTMLElement { 93 | const template = document.querySelector( 94 | "#node-template", 95 | ) as HTMLElement; 96 | if (!template) { 97 | throw new Error("Failed to find node view template"); 98 | } 99 | const newElement = template.cloneNode(true) as HTMLElement; 100 | 101 | const deleteButton = newElement.querySelector( 102 | ".button-delete", 103 | ) as HTMLElement; 104 | const editButton = newElement.querySelector( 105 | ".button-edit", 106 | ) as HTMLElement; 107 | 108 | deleteButton.addEventListener("click", () => 109 | this.onNodeDeleteClicked(this), 110 | ); 111 | editButton.addEventListener("click", () => 112 | this.onNodeEditClicked(this), 113 | ); 114 | 115 | const colorBar = newElement.querySelector(".color-bar") as HTMLElement; 116 | 117 | colorBar.addEventListener("mousedown", (e) => { 118 | e.stopPropagation(); 119 | this.onColorBarClicked(this); 120 | }); 121 | 122 | return newElement; 123 | } 124 | 125 | public set nodeInfo(node: NodeInfo) { 126 | this.nodeName = node.uniqueTitle ?? "(Error: no title)"; 127 | this.position = getPositionFromNodeInfo(node) ?? { x: 0, y: 0 }; 128 | 129 | const isInNodeGroup = node.sourceTitle != node.uniqueTitle; 130 | 131 | this.element.id = "node-" + (node.uniqueTitle ?? "$error.unknown"); 132 | this.element.dataset.nodeName = node.uniqueTitle ?? "$error.unknown"; 133 | 134 | this.title = isInNodeGroup ? undefined : node.sourceTitle; 135 | this.subtitle = node.subtitle; 136 | 137 | this.preview = node.previewText; 138 | 139 | // 'groups' is defined as an array, but here we only fetch a single 140 | // 'group' header, so it currently only ever has zero or one elements. 141 | // Once we have a defined way to say a node can be in multiple groups, 142 | // update this code to populate 'groups' with the right number of 143 | // elements. 144 | var groupHeaders = node.headers.filter( 145 | (header) => header.key == "group", 146 | )[0]; 147 | this.taggedGroups = groupHeaders ? [groupHeaders.value] : []; 148 | this.nodeGroup = isInNodeGroup ? (node.sourceTitle ?? null) : null; 149 | 150 | var colorHeader = node.headers.filter( 151 | (header) => header.key == "color", 152 | )[0]; 153 | if (colorHeader) { 154 | this.color = colorHeader.value; 155 | } else { 156 | this.color = null; 157 | } 158 | } 159 | 160 | public set title(newTitle: string | undefined) { 161 | const titleElement = this.element.querySelector( 162 | ".title", 163 | ) as HTMLElement; 164 | if (!newTitle) { 165 | titleElement.style.display = "none"; 166 | } else { 167 | titleElement.style.display = "block"; 168 | titleElement.innerText = newTitle; 169 | } 170 | } 171 | 172 | public set subtitle(newTitle: string | undefined) { 173 | const subtitleElement = this.element.querySelector( 174 | ".subtitle", 175 | ) as HTMLElement; 176 | if (!newTitle) { 177 | subtitleElement.style.display = "none"; 178 | } else { 179 | subtitleElement.style.display = "block"; 180 | subtitleElement.innerText = newTitle; 181 | } 182 | } 183 | 184 | public set preview(newPreview: string) { 185 | const title = this.element.querySelector(".preview") as HTMLElement; 186 | title.innerText = newPreview; 187 | } 188 | 189 | public set position(position: Position) { 190 | this.element.style.transform = `translate(${position.x}px, ${position.y}px)`; 191 | 192 | this._position = position; 193 | } 194 | 195 | public get position(): Position { 196 | return this._position; 197 | } 198 | 199 | public get color(): string | null { 200 | return this._color; 201 | } 202 | 203 | public set color(colorName: string | null) { 204 | this._color = colorName; 205 | 206 | // Remove all classes that begin with 'color-' 207 | const existingColorClasses = Array.from(this.element.classList).filter( 208 | (v) => v.startsWith("color-"), 209 | ); 210 | this.element.classList.remove(...existingColorClasses); 211 | this.element.classList.remove("color"); 212 | 213 | if (colorName) { 214 | this.element.classList.add("color-" + colorName); 215 | this.element.classList.add("color"); 216 | } 217 | } 218 | 219 | translate(dragDeltaViewSpace: Position) { 220 | var newPosition = this.position; 221 | newPosition.x += dragDeltaViewSpace.x; 222 | newPosition.y += dragDeltaViewSpace.y; 223 | this.position = newPosition; 224 | } 225 | 226 | public get top(): number { 227 | return this.position.y; 228 | } 229 | public set top(newValue: number) { 230 | this.position = { x: this.position.x, y: newValue }; 231 | } 232 | 233 | public get left(): number { 234 | return this.position.x; 235 | } 236 | 237 | public set left(newValue: number) { 238 | this.position = { x: newValue, y: this.position.y }; 239 | } 240 | 241 | public get bottom(): number { 242 | return this.position.y + this.element.offsetHeight; 243 | } 244 | public set bottom(newValue: number) { 245 | this.position = { 246 | x: this.position.x, 247 | y: newValue - this.element.offsetHeight, 248 | }; 249 | } 250 | 251 | public get right(): number { 252 | return this.position.x + this.element.offsetWidth; 253 | } 254 | public set right(newValue) { 255 | this.position = { 256 | x: newValue - this.element.offsetWidth, 257 | y: this.position.y, 258 | }; 259 | } 260 | 261 | public get size(): Size { 262 | const { width, height } = this; 263 | return { width, height }; 264 | } 265 | 266 | public get width(): number { 267 | return this.element.offsetWidth; 268 | } 269 | public get height(): number { 270 | return this.element.offsetHeight; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /webview/constants.ts: -------------------------------------------------------------------------------- 1 | export const factor = 0.1; 2 | export const zoomSpeed = 120; 3 | 4 | export const zoomMinScale = 0.1; 5 | export const zoomMaxScale = 2; 6 | 7 | /** How far from the last node each new node will be created */ 8 | export const newNodeOffset = 10; 9 | 10 | export const NodeSize = { 11 | width: 200, 12 | height: 125, 13 | }; 14 | -------------------------------------------------------------------------------- /webview/images/align-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webview/images/align-center.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webview/images/align-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webview/images/align-middle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webview/images/align-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webview/images/align-top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webview/svg.ts: -------------------------------------------------------------------------------- 1 | import * as CurvedArrows from "curved-arrows"; 2 | import { NodeView } from "./NodeView"; 3 | import { NodeSize } from "./constants"; 4 | import { Position, Size } from "./util"; 5 | 6 | enum ArrowConstraints { 7 | None, 8 | LeftToRight, 9 | RightToLeft, 10 | } 11 | 12 | const VSCODE_COLOR_LINE_CHART = "var(--vscode-charts-lines)"; 13 | /** 14 | * Creates an SVG element that contains lines connecting the indicated nodes. 15 | * @param nodes The nodes to draw lines between. 16 | * @param arrowHeadSize The size of the arrowheads at the end of lines. 17 | * @param lineThickness The thickness of the lines. 18 | * @param color The color (as a hex value, CSS variable, or other CSS-compatible 19 | * color) of the lines. 20 | * @returns An SVGElement containing lines between the provided nodes. 21 | */ 22 | export function getLinesSVGForNodes( 23 | nodes: Iterable, 24 | arrowHeadSize = 9, 25 | lineThickness = 2, 26 | color = VSCODE_COLOR_LINE_CHART, 27 | constraints = ArrowConstraints.LeftToRight, 28 | ): SVGElement { 29 | type ArrowDescriptor = [ 30 | sx: number, 31 | sy: number, 32 | c1x: number, 33 | c1y: number, 34 | c2x: number, 35 | c2y: number, 36 | ex: number, 37 | ey: number, 38 | ae: number, 39 | as: number, 40 | type: "Jump" | "Detour", 41 | ]; 42 | 43 | let arrowDescriptors: ArrowDescriptor[] = []; 44 | 45 | for (const fromNode of nodes) { 46 | for (const toNode of fromNode.outgoingConnections) { 47 | try { 48 | let fromPosition = fromNode.position; 49 | 50 | let toPosition: Position; 51 | let fromSize: Size; 52 | let toSize: Size; 53 | 54 | fromSize = fromNode.size; 55 | 56 | switch (toNode.destinationType) { 57 | case "Node": 58 | toPosition = toNode.nodeView.position; 59 | toSize = toNode.nodeView.size; 60 | break; 61 | case "NodeGroup": 62 | toPosition = toNode.groupView.position; 63 | toSize = toNode.groupView.size; 64 | break; 65 | } 66 | 67 | var allowedDirections: CurvedArrows.ArrowOptions = {}; 68 | 69 | switch (constraints) { 70 | case ArrowConstraints.LeftToRight: 71 | allowedDirections.allowedStartSides = [ 72 | "right", 73 | "bottom", 74 | ]; 75 | allowedDirections.allowedEndSides = ["left", "top"]; 76 | break; 77 | case ArrowConstraints.RightToLeft: 78 | allowedDirections.allowedStartSides = [ 79 | "left", 80 | "bottom", 81 | ]; 82 | allowedDirections.allowedEndSides = ["right", "top"]; 83 | break; 84 | case ArrowConstraints.RightToLeft: 85 | allowedDirections.allowedStartSides = []; 86 | allowedDirections.allowedEndSides = []; 87 | break; 88 | } 89 | 90 | const arrow = [ 91 | ...CurvedArrows.getBoxToBoxArrow( 92 | fromPosition.x, 93 | fromPosition.y, 94 | fromSize.width, 95 | fromSize.height, 96 | 97 | toPosition.x, 98 | toPosition.y, 99 | toSize.width, 100 | toSize.height, 101 | 102 | { 103 | padStart: 104 | toNode.connectionType === "Detour" 105 | ? arrowHeadSize 106 | : 0, 107 | padEnd: arrowHeadSize, 108 | ...allowedDirections, 109 | }, 110 | ), 111 | toNode.connectionType, 112 | ] as ArrowDescriptor; 113 | arrowDescriptors.push(arrow); 114 | } catch (e) { 115 | console.error(e); 116 | } 117 | } 118 | } 119 | 120 | const SVG = "http://www.w3.org/2000/svg"; 121 | 122 | let svg = document.createElementNS(SVG, "svg"); 123 | svg.style.position = "absolute"; 124 | svg.style.left = "0"; 125 | svg.style.top = "0"; 126 | svg.setAttribute("width", "100%"); 127 | svg.setAttribute("height", "100%"); 128 | svg.style.overflow = "visible"; 129 | svg.style.zIndex = "-1"; 130 | svg.id = "lines"; 131 | 132 | for (const arrow of arrowDescriptors) { 133 | let [sx, sy, c1x, c1y, c2x, c2y, ex, ey, ae, as, type] = arrow; 134 | 135 | let line = document.createElementNS(SVG, "path"); 136 | 137 | line.setAttribute( 138 | "d", 139 | `M ${sx} ${sy} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${ex} ${ey}`, 140 | ); 141 | line.setAttribute("stroke", color); 142 | line.setAttribute("stroke-width", lineThickness.toString()); 143 | line.setAttribute("fill", "none"); 144 | 145 | svg.appendChild(line); 146 | 147 | let arrowHeadEnd = document.createElementNS(SVG, "polygon"); 148 | 149 | arrowHeadEnd.setAttribute( 150 | "points", 151 | `0,${-arrowHeadSize} ${arrowHeadSize * 2},0, 0,${arrowHeadSize}`, 152 | ); 153 | arrowHeadEnd.setAttribute( 154 | "transform", 155 | `translate(${ex}, ${ey}) rotate(${ae})`, 156 | ); 157 | arrowHeadEnd.setAttribute("fill", color); 158 | arrowHeadEnd.id = "arrow-end"; 159 | 160 | svg.appendChild(arrowHeadEnd); 161 | 162 | if (type === "Detour") { 163 | // Show an arrow head at the start for detours 164 | let arrowHeadStart = document.createElementNS(SVG, "polygon"); 165 | arrowHeadStart.id = "arrow-start"; 166 | 167 | arrowHeadStart.setAttribute( 168 | "points", 169 | `0,${-arrowHeadSize} ${arrowHeadSize * 2},0, 0,${arrowHeadSize}`, 170 | ); 171 | arrowHeadStart.setAttribute( 172 | "transform", 173 | `translate(${sx}, ${sy}) rotate(${as})`, 174 | ); 175 | arrowHeadStart.setAttribute("fill", color); 176 | 177 | svg.appendChild(arrowHeadStart); 178 | } 179 | } 180 | 181 | return svg; 182 | } 183 | -------------------------------------------------------------------------------- /webview/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "AMD", 4 | "target": "es6", 5 | "lib": ["DOM"], 6 | "sourceMap": true, 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "rootDirs": [".", "../src/types"], 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "allowJs": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webview/util.ts: -------------------------------------------------------------------------------- 1 | import { NodeInfo } from "./nodes"; 2 | 3 | /** Decomposes a DOMMatrix into its translation, rotation, scale and skew (where possible). 4 | * @param mat: The matrix to decompose. 5 | */ 6 | export function decomposeTransformMatrix(mat: DOMMatrix) { 7 | var a = mat.a; 8 | var b = mat.b; 9 | var c = mat.c; 10 | var d = mat.d; 11 | var e = mat.e; 12 | var f = mat.f; 13 | 14 | var delta = a * d - b * c; 15 | 16 | let result = { 17 | translation: { x: e, y: f }, 18 | rotation: 0, 19 | scale: { x: 0, y: 0 }, 20 | skew: { x: 0, y: 0 }, 21 | }; 22 | 23 | // Apply QR-like decomposition of the 2D matrix. 24 | if (a != 0 || b != 0) { 25 | var r = Math.sqrt(a * a + b * b); 26 | result.rotation = b > 0 ? Math.acos(a / r) : -Math.acos(a / r); 27 | result.scale = { x: r, y: delta / r }; 28 | result.skew = { x: Math.atan((a * c + b * d) / (r * r)), y: 0 }; 29 | } else if (c != 0 || d != 0) { 30 | var s = Math.sqrt(c * c + d * d); 31 | result.rotation = 32 | Math.PI / 2 - (d > 0 ? Math.acos(-c / s) : -Math.acos(c / s)); 33 | result.scale = { x: delta / s, y: s }; 34 | result.skew = { x: 0, y: Math.atan((a * c + b * d) / (s * s)) }; 35 | } else { 36 | // a = b = c = d = 0 37 | } 38 | 39 | return result; 40 | } 41 | 42 | export interface Position { 43 | x: number; 44 | y: number; 45 | } 46 | 47 | export interface Size { 48 | width: number; 49 | height: number; 50 | } 51 | 52 | export function getWindowSize(): Size { 53 | let viewport = window.visualViewport; 54 | if (viewport == null) { 55 | throw new Error("Failed to get window visual viewport"); 56 | } 57 | return { 58 | width: viewport.width, 59 | height: viewport.height, 60 | }; 61 | } 62 | 63 | /** 64 | * Returns the coordinates of the center of the window. 65 | * @returns The center of the window. 66 | */ 67 | export function getWindowCenter(): Position { 68 | const size = getWindowSize(); 69 | 70 | return { 71 | x: Math.round(size.width / 2), 72 | y: Math.round(size.height / 2), 73 | }; 74 | } 75 | 76 | export function getPositionFromNodeInfo(node: NodeInfo): Position | null { 77 | // Try and find a 'position' header in this node, and parse it; if 78 | // we can't find one, or can't parse it, default to (0,0). 79 | const positionString = node.headers.find((h) => h.key == "position")?.value; 80 | 81 | if (positionString) { 82 | try { 83 | const elements = positionString.split(",").map((i) => parseInt(i)); 84 | return { x: elements[0], y: elements[1] }; 85 | } catch (e) { 86 | return null; 87 | } 88 | } else { 89 | return null; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /webview/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | module.exports = { 4 | entry: "./webview/yarnspinner.ts", 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: "ts-loader", 10 | exclude: /node_modules/, 11 | }, 12 | { 13 | test: /\.svg$/i, 14 | type: "asset/source", 15 | }, 16 | ], 17 | }, 18 | devtool: "eval-source-map", 19 | resolve: { 20 | extensions: [".tsx", ".ts", ".js"], 21 | }, 22 | output: { 23 | filename: "./yarnspinner.js", 24 | path: path.resolve(__dirname, "..", "media"), 25 | }, 26 | optimization: { 27 | minimize: false, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /webview/yarnspinner.ts: -------------------------------------------------------------------------------- 1 | import { NodesUpdatedEvent } from "../src/types/editor"; 2 | 3 | import { Alignment, ViewState } from "./ViewState"; 4 | import { NodeView } from "./NodeView"; 5 | import { getLinesSVGForNodes } from "./svg"; 6 | import { getPositionFromNodeInfo } from "./util"; 7 | 8 | import { MessageTypes, WebViewEvent } from "../src/types/editor"; 9 | import { newNodeOffset } from "./constants"; 10 | 11 | interface VSCode { 12 | postMessage(message: any): void; 13 | } 14 | 15 | export {}; 16 | 17 | declare global { 18 | function acquireVsCodeApi(): VSCode; 19 | } 20 | 21 | const vscode = acquireVsCodeApi(); 22 | 23 | let nodesContainer: HTMLElement; 24 | let zoomContainer: HTMLElement; 25 | 26 | zoomContainer = document.querySelector(".zoom-container") as HTMLElement; 27 | nodesContainer = document.querySelector(".nodes") as HTMLElement; 28 | 29 | let viewState = new ViewState(zoomContainer, nodesContainer); 30 | 31 | viewState.onNodeDelete = (name) => { 32 | var ID = name; 33 | vscode.postMessage({ 34 | type: "delete", 35 | id: ID, 36 | }); 37 | }; 38 | 39 | viewState.onNodeEdit = (name) => { 40 | var ID = name; 41 | vscode.postMessage({ 42 | type: "open", 43 | id: ID, 44 | }); 45 | }; 46 | 47 | viewState.onNodesMoved = (positions) => { 48 | vscode.postMessage({ 49 | type: "move", 50 | positions: positions, 51 | }); 52 | }; 53 | 54 | viewState.updateNodeHeader = (nodeName, headerName, headerValue) => { 55 | vscode.postMessage({ 56 | type: "update-header", 57 | nodeName, 58 | key: headerName, 59 | value: headerValue, 60 | }); 61 | }; 62 | 63 | var buttonsContainer = document.querySelector("#nodes-header"); 64 | 65 | if (!buttonsContainer) { 66 | throw new Error("Failed to find buttons container"); 67 | } 68 | 69 | const alignmentButtonContainer = document.createElement("div"); 70 | alignmentButtonContainer.id = "alignment-buttons"; 71 | alignmentButtonContainer.style.zIndex = "9999"; 72 | document.body.appendChild(alignmentButtonContainer); 73 | 74 | const alignment: Record void; i: string; t: string }> = { 75 | "align-left": { 76 | cb: () => viewState.alignSelectedNodes(Alignment.Left), 77 | i: require("./images/align-left.svg") as string, 78 | t: "Align Left", 79 | }, 80 | // "align-center": { 81 | // cb: () => viewState.alignSelectedNodes(Alignment.Center), 82 | // i: require('./images/align-center.svg') as string, 83 | // t: "Align Center" 84 | // }, 85 | "align-right": { 86 | cb: () => viewState.alignSelectedNodes(Alignment.Right), 87 | i: require("./images/align-right.svg") as string, 88 | t: "Align Right", 89 | }, 90 | "align-top": { 91 | cb: () => viewState.alignSelectedNodes(Alignment.Top), 92 | i: require("./images/align-top.svg") as string, 93 | t: "Align Top", 94 | }, 95 | // "align-middle": { 96 | // cb: () => viewState.alignSelectedNodes(Alignment.Middle), 97 | // i: require('./images/align-middle.svg') as string, 98 | // t: "Align Middle" 99 | // }, 100 | "align-bottom": { 101 | cb: () => viewState.alignSelectedNodes(Alignment.Bottom), 102 | i: require("./images/align-bottom.svg") as string, 103 | t: "Align Bottom", 104 | }, 105 | }; 106 | 107 | const parser = new DOMParser(); 108 | 109 | let alignmentButtons: HTMLElement[] = []; 110 | 111 | for (const alignmentEntryName in alignment) { 112 | const alignmentEntry = alignment[alignmentEntryName]; 113 | 114 | const alignmentButton = document.createElement("vscode-button"); 115 | alignmentButton.id = `button-${alignmentEntryName}`; 116 | alignmentButton.setAttribute("appearance", "icon"); 117 | alignmentButton.addEventListener("click", alignmentEntry.cb); 118 | alignmentButton.title = alignmentEntry.t; 119 | alignmentButton.ariaLabel = alignmentEntry.t; 120 | 121 | const alignmentImage = parser.parseFromString( 122 | alignmentEntry.i, 123 | "image/svg+xml", 124 | ).firstElementChild as SVGElement; 125 | alignmentImage.style.width = "16px"; 126 | alignmentImage.style.height = "16px"; 127 | 128 | alignmentButton.appendChild(alignmentImage); 129 | alignmentButtonContainer.appendChild(alignmentButton); 130 | 131 | alignmentButtons.push(alignmentButton); 132 | } 133 | 134 | viewState.onSelectionChanged = (nodes) => { 135 | if (nodes.length <= 1) { 136 | // We can only align nodes if we have more than 1 selected. 137 | alignmentButtons.forEach((b) => { 138 | b.classList.add("disabled"); 139 | b.setAttribute("disabled", ""); 140 | }); 141 | } else { 142 | alignmentButtons.forEach((b) => { 143 | b.classList.remove("disabled"); 144 | b.removeAttribute("disabled"); 145 | }); 146 | } 147 | }; 148 | 149 | // Script run within the webview itself. 150 | (function () { 151 | // Get a reference to the VS Code webview api. 152 | // We use this API to post messages back to our extension. 153 | 154 | const addNodeButton = buttonsContainer.querySelector("#add-node"); 155 | 156 | if (!addNodeButton) { 157 | throw new Error("Failed to find Add Node button"); 158 | } 159 | 160 | addNodeButton.addEventListener("click", () => { 161 | let nodePosition = viewState.getPositionForNewNode(); 162 | 163 | vscode.postMessage({ 164 | type: "add", 165 | position: nodePosition, 166 | }); 167 | }); 168 | 169 | window.addEventListener("message", (e: any) => { 170 | const event = e.data as WebViewEvent; 171 | 172 | if (event.type == "update") { 173 | nodesUpdated(event); 174 | } else if (event.type == "show-node") { 175 | showNode(event.node); 176 | } 177 | }); 178 | 179 | /** 180 | * @param {NodesUpdatedEvent} data 181 | */ 182 | function updateDropdownList(data: NodesUpdatedEvent) { 183 | const dropdown = document.querySelector("#node-jump"); 184 | 185 | if (dropdown == null) { 186 | throw new Error("Failed to find node dropdown"); 187 | } 188 | 189 | const icon = dropdown.querySelector("#icon"); 190 | 191 | if (!icon) { 192 | throw new Error("Failed to find icon"); 193 | } 194 | 195 | let placeholderOption = document.createElement("vscode-option"); 196 | placeholderOption.innerText = "Jump to Node"; 197 | 198 | let nodeOptions = data.nodes 199 | .map((node) => { 200 | if (!node.uniqueTitle || !node.sourceTitle) { 201 | return undefined; 202 | } 203 | let option = document.createElement("vscode-option"); 204 | option.nodeValue = node.uniqueTitle; 205 | option.innerText = node.sourceTitle; 206 | return option; 207 | }) 208 | .filter((o) => o !== undefined) as HTMLElement[]; 209 | 210 | dropdown.replaceChildren(icon, placeholderOption, ...nodeOptions); 211 | } 212 | 213 | const dropdown = document.querySelector("#node-jump") as HTMLSelectElement; 214 | 215 | if (!dropdown) { 216 | throw new Error("Failed to find node list dropdown"); 217 | } 218 | 219 | dropdown.addEventListener("change", (evt) => { 220 | if (dropdown.selectedIndex > 0) { 221 | // We selected a node. 222 | console.log(`Jumping to ${dropdown.value}`); 223 | 224 | showNode(dropdown.value); 225 | } 226 | dropdown.selectedIndex = 0; 227 | }); 228 | 229 | function showNode(nodeName: string) { 230 | const node = viewState.getNodeView(nodeName); 231 | if (node) { 232 | viewState.focusOnNode(node); 233 | } 234 | } 235 | 236 | /** 237 | * Called whenever the extension notifies us that the nodes in the 238 | * document have changed. 239 | * @param data {NodesUpdatedEvent} Information about the document's 240 | * nodes. 241 | */ 242 | function nodesUpdated(data: NodesUpdatedEvent) { 243 | let nodesWithDefaultPosition = 0; 244 | 245 | for (let nodeInfo of data.nodes) { 246 | let position = getPositionFromNodeInfo(nodeInfo); 247 | 248 | if (!position) { 249 | const position = { 250 | x: newNodeOffset * nodesWithDefaultPosition, 251 | y: newNodeOffset * nodesWithDefaultPosition, 252 | }; 253 | nodeInfo.headers.push({ 254 | key: "position", 255 | value: `${position.x},${position.y}`, 256 | }); 257 | nodesWithDefaultPosition += 1; 258 | } 259 | } 260 | 261 | viewState.nodes = data.nodes; 262 | 263 | updateDropdownList(data); 264 | } 265 | })(); 266 | --------------------------------------------------------------------------------