├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── node.js.yml │ └── playwright.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── bin ├── build-visualizer.sh └── deploy-gh-pages.sh ├── package-lock.json ├── package.json ├── playwright.config.cjs ├── public ├── images │ ├── backward.png │ ├── chevron-down.svg │ ├── chevron-up.svg │ ├── end.png │ ├── forward.png │ └── start.png ├── index.html ├── style │ ├── cm-theme-light-owl.css │ ├── ellipsis-dropdown.css │ ├── example-list.css │ ├── index.css │ └── parseTree.css └── third_party │ ├── FileSaver.js │ ├── GitHub.bundle.min-2.3.0.js │ ├── autosize.min.js │ ├── codemirror-5.65.12 │ ├── codemirror.min.css │ ├── codemirror.min.js │ ├── placeholder.min.js │ ├── search.min.js │ └── searchcursor.min.js │ ├── css │ └── bootstrap-dropdown-3.3.7.css │ └── d3.min.js ├── src ├── README.md ├── TraceElementWalker.js ├── cmUtil.js ├── components │ ├── ellipsis-dropdown.js │ ├── example-editor.js │ ├── example-list.js │ ├── expanded-input.vue │ ├── main-layout.vue │ ├── parse-results.vue │ ├── parse-tree.vue │ ├── step-controls.vue │ ├── thumbs-up-button.js │ ├── trace-element.vue │ └── trace-label.vue ├── domUtil.js ├── editorErrors.js ├── externalRules.js ├── index.js ├── mainLayout.js ├── ohmEditor.js ├── ohmMode.js ├── parseTree.js ├── persistence.js ├── ruleHyperlinks.js ├── searchBar.js ├── splitters.js ├── timeline.js ├── traceUtil.js └── utils.js ├── test ├── codeMirrorStub.js ├── data │ └── har │ │ ├── 13ed959e625c6c3e9771cb5f8a8973085cdb07d2.json │ │ ├── a724602fd567d007a8578ed021a554a141d7ab4c.js │ │ ├── api.github.com.har │ │ ├── e07edf673c7f2b9f28c5275b857ecd72159c8ad8.json │ │ └── unpkg.com.har ├── editor-e2e.test.js ├── example-pane-e2e.test.js ├── playwrightHelpers.js ├── snapshots │ ├── editor-e2e.test.js-snapshots │ │ ├── arithmetic-all-examples-green-chromium-darwin.png │ │ ├── arithmetic-all-examples-green-chromium-linux.png │ │ ├── arithmetic-all-examples-green-firefox-darwin.png │ │ ├── arithmetic-all-examples-green-firefox-linux.png │ │ ├── arithmetic-all-examples-green-webkit-darwin.png │ │ ├── arithmetic-all-examples-green-webkit-linux.png │ │ ├── arithmetic-first-example-red-chromium-darwin.png │ │ ├── arithmetic-first-example-red-chromium-linux.png │ │ ├── arithmetic-first-example-red-firefox-darwin.png │ │ ├── arithmetic-first-example-red-firefox-linux.png │ │ ├── arithmetic-first-example-red-webkit-darwin.png │ │ ├── arithmetic-first-example-red-webkit-linux.png │ │ ├── arithmetic-new-example-done-chromium-darwin.png │ │ ├── arithmetic-new-example-done-chromium-linux.png │ │ ├── arithmetic-new-example-done-firefox-darwin.png │ │ ├── arithmetic-new-example-done-firefox-linux.png │ │ ├── arithmetic-new-example-done-webkit-darwin.png │ │ └── arithmetic-new-example-done-webkit-linux.png │ ├── example-pane-e2e.test.js-snapshots │ │ ├── hSplitter-before-drag-chromium-darwin.png │ │ ├── hSplitter-before-drag-chromium-linux.png │ │ ├── hSplitter-before-drag-firefox-darwin.png │ │ ├── hSplitter-before-drag-firefox-linux.png │ │ ├── hSplitter-before-drag-webkit-darwin.png │ │ └── hSplitter-before-drag-webkit-linux.png │ └── splitters-e2e.test.js-snapshots │ │ ├── vSplitter-before-drag-chromium-darwin.png │ │ ├── vSplitter-before-drag-chromium-linux.png │ │ ├── vSplitter-before-drag-firefox-darwin.png │ │ ├── vSplitter-before-drag-firefox-linux.png │ │ ├── vSplitter-before-drag-webkit-darwin.png │ │ └── vSplitter-before-drag-webkit-linux.png ├── splitters-e2e.test.js ├── test-TraceElementWalker.js ├── test-ellipsis-dropdown.js ├── test-example-list.mjs └── test-ohmMode.js └── webpack.config.cjs /.eslintignore: -------------------------------------------------------------------------------- 1 | **/third_party/** 2 | **/node_modules/** 3 | build/* 4 | test/data/har/** 5 | test/snapshots/** 6 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | ecmaVersion: 2020, 4 | sourceType: 'module', 5 | }, 6 | 7 | // To minimize dependencies on Node- or browser-specific features, leave the 8 | // env empty, and instead define globals as needed. 9 | env: {}, 10 | extends: ['eslint:recommended', 'google', 'plugin:vue/essential', 'prettier'], 11 | 12 | // Project-wide globals. If other globals are necessary, prefer putting them 13 | // in a comment at the top of the file rather than adding them here. 14 | globals: { 15 | console: true, 16 | exports: true, 17 | module: true, 18 | require: true, 19 | }, 20 | plugins: ['camelcase-ohm', 'html', 'node', 'no-extension-in-require'], 21 | settings: {}, 22 | rules: { 23 | // ----- Exceptions to the configs we extend ----- 24 | 25 | 'arrow-parens': ['error', 'as-needed'], 26 | 27 | // We follow the old Google style guide here. 28 | // The default was changed in https://github.com/google/eslint-config-google/pull/23. 29 | 'block-spacing': ['error', 'always'], // google 30 | 31 | 'new-cap': ['error', {capIsNew: false}], // google: 'error' 32 | 'no-constant-condition': ['error', {checkLoops: false}], // eslint:recommended: 'error' 33 | 'no-invalid-this': 'off', // google 34 | 'no-redeclare': 'off', // eslint:recommended 35 | quotes: ['error', 'single', {avoidEscape: true}], // google 36 | 'require-jsdoc': 'off', // google 37 | 38 | // ----- Extra things we enforce in Ohm ----- 39 | 40 | // Turn off the regular camelcase rule, and use a custom rule which 41 | // allows semantic actions to be named like `RuleName_caseName`. 42 | camelcase: 0, 43 | 'camelcase-ohm/camelcase-ohm': 'error', 44 | 45 | eqeqeq: ['error', 'allow-null'], 46 | 'max-len': ['error', {code: 100, ignoreUrls: true}], 47 | 'max-statements-per-line': ['error', {max: 2}], 48 | 'no-console': 2, 49 | 'no-extension-in-require/main': 2, 50 | 'no-warning-comments': ['error', {terms: ['xxx', 'fixme']}], 51 | strict: ['error', 'global'], 52 | 53 | // ------ Prefer newer language features ----- 54 | 55 | // Object shorthand is allowed by Google style; we are more opinionated. 56 | // https://google.github.io/styleguide/jsguide.html#features-objects-method-shorthand 57 | 'object-shorthand': ['error', 'always'], 58 | 59 | 'prefer-arrow-callback': 'error', 60 | 'prefer-const': 'error', 61 | 'prefer-destructuring': ['error', {object: true, array: false}], 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.x, 18.x, 20.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: npm ci 29 | - run: npm run build --if-present 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [main, master] 5 | pull_request: 6 | branches: [main, master] 7 | jobs: 8 | test: 9 | timeout-minutes: 15 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | ref: ${{ github.event.pull_request.head.ref }} 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 18 18 | - name: Install dependencies 19 | run: npm ci 20 | - name: Install Playwright browsers 21 | run: npx playwright install --with-deps 22 | - name: Run Playwright tests 23 | if: ${{ !startsWith(github.head_ref, 'update-snapshots/') }} 24 | run: npm run test:e2e 25 | - name: Run Playwright tests and update snapshots 26 | if: ${{ startsWith(github.head_ref, 'update-snapshots/') }} 27 | run: npm run update-snapshots 28 | - name: Commit new snapshots 29 | uses: EndBug/add-and-commit@v9 30 | if: ${{ success() && github.event_name == 'pull_request' }} 31 | with: 32 | add: 'test/snapshots' 33 | default_author: github_actions 34 | message: 'Update snapshots' 35 | push: origin ${{ github.head_ref }} 36 | - uses: actions/upload-artifact@v3 37 | if: always() 38 | with: 39 | name: playwright-report 40 | path: playwright-report/ 41 | retention-days: 30 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | build/* 5 | /test-results/ 6 | /playwright-report/ 7 | /playwright/.cache/ 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | test/data/har/** 4 | test/snapshots/** 5 | third_party 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alessandro Warth, Marko Röder, et al. 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 | # Ohm Editor 2 | 3 | [![Build Status](https://github.com/ohmjs/ohm-editor/actions/workflows/node.js.yml/badge.svg)](https://github.com/ohmjs/ohm-editor/actions/workflows/node.js.yml) 4 | [![Live demo](https://img.shields.io/badge/Live%20demo-%E2%86%92-9D6EB3.svg)](https://ohmlang.github.io/editor/) 5 | 6 | A standalone editor for the [Ohm](https://github.com/cdglabs/ohm) language. 7 | 8 | ## Usage 9 | 10 | Clone this repository and run `npm install` in the project root. 11 | 12 | To run the editor in the browser: 13 | 14 | npm start 15 | 16 | ## Development Notes 17 | 18 | - To deploy from your local repository to https://ohmlang.github.io/editor/, use `bin/deploy-gh-pages.sh`. When the script shows the following prompt: 19 | 20 | Do you want to deploy to ohmlang.github.io (y/n)? 21 | 22 | ...you can test things locally by switching to your clone of ohmlang.github.io and running the following command in the repository root: 23 | 24 | python -c "import SimpleHTTPServer; m = SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map; m[''] = 'text/plain'; m.update(dict([(k, v + ';charset=UTF-8') for k, v in m.items()])); SimpleHTTPServer.test();" 25 | 26 | This will serve the contents of the ohmlang.github.io site locally. 27 | 28 | ### Playwright tests 29 | 30 | E2E tests are configured to run on every commit to `main` and each pull request. They can also be run locally via `npm run test:e2e`. 31 | 32 | **To update snapshots:** create a pull request on a branch whose name begins with the prefix `update-snapshots/`. This will trigger the Playwright workflow (see `playwright.yml`) to run with the `--update-snapshots` option and then commit the results to the original branch. If all looks good, you can then merge the PR to main. 33 | -------------------------------------------------------------------------------- /bin/build-visualizer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR_NAME=$(dirname "$0") 4 | EXEC_NAME=$(basename "$0") 5 | BUILD_DIR=$(dirname "$DIR_NAME")/build 6 | 7 | set -e # Exit if any step returns an error code. 8 | 9 | if [ -z "$1" ] || [ ! -d "$1" ]; then 10 | echo "usage: $EXEC_NAME " 11 | echo "Builds the Ohm Editor for distribution and copies it to a given directory" 12 | exit 1 13 | fi 14 | 15 | npm run build 16 | 17 | pushd "$BUILD_DIR" 18 | echo "Ohm editor version: $(git rev-parse HEAD)" > build-info.txt 19 | 20 | # Sync $BUILD_DIR with the assets/ subdirectory of the destination dir. 21 | read -p "Copy editor to $1 (y/n)? " -n 1 -r 22 | if [[ $REPLY =~ ^[Yy]$ ]]; then 23 | popd 24 | rsync -av --delete public/ "$1" 25 | rsync -av --delete "${BUILD_DIR}/" "$1/assets" 26 | fi 27 | -------------------------------------------------------------------------------- /bin/deploy-gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # To run this, you need a checkout of https://github.com/ohmjs/ohmjs.org. 4 | 5 | # Accepts an optional argument, which is the path to the ohmjs.org repository root. 6 | # If not specified, it looks for a directory named ohmjs.org in the same directory 7 | # as this repository. 8 | 9 | set -e 10 | 11 | ROOT=$(npm prefix) 12 | OHM_REV=$(git rev-parse --short main) 13 | 14 | PAGES_DIR="$1" 15 | if [ -z "$1" ]; then 16 | PAGES_DIR="$ROOT/../ohmjs.org" # Default if $1 is empty 17 | fi 18 | 19 | # Now check that the $PAGES_DIR exists. 20 | if [ ! -d "$PAGES_DIR" ]; then 21 | echo "No such directory: $PAGES_DIR" && exit 1 22 | fi 23 | 24 | # Do a build and copy everything to $PAGES_DIR/static/editor 25 | "$ROOT/bin/build-visualizer.sh" "$PAGES_DIR/static/editor" 26 | 27 | # Double check that $PAGES_DIR is actually a git repo. 28 | pushd "$PAGES_DIR" 29 | if ! git rev-parse --quiet --verify main > /dev/null; then 30 | echo "Not a git repository: $PAGES_DIR" && exit 1 31 | fi 32 | 33 | read -p "Do you want to commit to ohmjs.org (y/n)? " -n 1 -r 34 | 35 | # Engage! 36 | if [[ $REPLY =~ ^[Yy]$ ]]; then 37 | git pull --ff-only --no-stat 38 | git add static/editor 39 | git commit -m "Update from ohmjs/ohm-editor@${OHM_REV}" 40 | git push origin main 41 | fi 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ohm-js-editor", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "description": "An IDE for the Ohm language (JavaScript edition)", 6 | "author": "Patrick Dubroy ", 7 | "scripts": { 8 | "build": "webpack --mode production", 9 | "lint": "eslint . --ext .js,.vue", 10 | "build-visualizer": "bash bin/build-visualizer.sh", 11 | "ci-test": "npm run lint && npm test", 12 | "format": "prettier --write . && npm run lint -- --fix", 13 | "postinstall": "true", 14 | "start": "webpack serve --mode development", 15 | "test": "uvu test 'test-.*'", 16 | "test:e2e": "npx playwright test --config=playwright.config.cjs", 17 | "update-snapshots": "npm run test:e2e -- --update-snapshots" 18 | }, 19 | "main": "index.js", 20 | "devDependencies": { 21 | "@playwright/test": "^1.31.2", 22 | "@vue/test-utils": "1.2.2", 23 | "checked-emitter": "^1.0.1", 24 | "css-loader": "^0.26.0", 25 | "eslint": "^8.0.0", 26 | "eslint-config-google": "^0.14.0", 27 | "eslint-config-prettier": "^8.3.0", 28 | "eslint-plugin-camelcase-ohm": "^0.2.1", 29 | "eslint-plugin-html": "^6.2.0", 30 | "eslint-plugin-no-extension-in-require": "^0.2.0", 31 | "eslint-plugin-node": "^11.1.0", 32 | "eslint-plugin-tape": "^1.1.0", 33 | "eslint-plugin-vue": "^7.19.1", 34 | "file-loader": "^0.11.2", 35 | "global-jsdom": "^8.7.0", 36 | "jsdom": "^21.1.0", 37 | "ohm-js": "^17.0.0", 38 | "open": "6.0.0", 39 | "prettier": "^2.4.1", 40 | "uvu": "^0.5.6", 41 | "vue": "^2.6.14", 42 | "vue-loader": "^15.9.8", 43 | "vue-template-compiler": "^2.6.14", 44 | "webpack": "^5.76.1", 45 | "webpack-cli": "^5.0.1", 46 | "webpack-dev-server": "^4.11.1" 47 | }, 48 | "prettier": { 49 | "bracketSpacing": false, 50 | "singleQuote": true, 51 | "trailingComma": "es5" 52 | }, 53 | "bin": { 54 | "ohm-editor": "cli.js" 55 | }, 56 | "bugs": "https://github.com/ohmjs/ohm-editor/issues", 57 | "contributors": [ 58 | "Alex Warth (http://tinlizzie.org/~awarth)", 59 | "Marko Röder ", 60 | "Meixian Li ", 61 | "Saketh Kasibatla " 62 | ], 63 | "engines": { 64 | "node": ">=4.0" 65 | }, 66 | "greenkeeper": { 67 | "ignore": [ 68 | "eslint", 69 | "eslint-config-google", 70 | "eslint-plugin-camelcase-ohm", 71 | "eslint-plugin-html", 72 | "eslint-plugin-no-extension-in-require", 73 | "eslint-plugin-tape" 74 | ] 75 | }, 76 | "homepage": "https://ohmjs.org/editor/", 77 | "keywords": [ 78 | "editor", 79 | "ide", 80 | "javascript", 81 | "ohm", 82 | "ohm-js", 83 | "semantics", 84 | "visualizer", 85 | "prototyping" 86 | ], 87 | "license": "MIT", 88 | "precommit": [ 89 | "lint" 90 | ], 91 | "productName": "Ohm Editor", 92 | "repository": "https://github.com/ohmjs/ohm-editor" 93 | } 94 | -------------------------------------------------------------------------------- /playwright.config.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | // @ts-check 4 | const {defineConfig, devices} = require('@playwright/test'); 5 | 6 | /** 7 | * Read environment variables from file. 8 | * https://github.com/motdotla/dotenv 9 | */ 10 | // require('dotenv').config(); 11 | 12 | /** 13 | * @see https://playwright.dev/docs/test-configuration 14 | */ 15 | module.exports = defineConfig({ 16 | testDir: './test', 17 | /* Maximum time one test can run for. */ 18 | timeout: 10 * 1000, 19 | expect: { 20 | /** 21 | * Maximum time expect() should wait for the condition to be met. 22 | * For example in `await expect(locator).toHaveText();` 23 | */ 24 | timeout: 2000, 25 | }, 26 | /* Run tests in files in parallel */ 27 | fullyParallel: true, 28 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 29 | forbidOnly: !!process.env.CI, 30 | /* Retry on CI only */ 31 | retries: process.env.CI ? 2 : 0, 32 | /* Opt out of parallel tests on CI. */ 33 | workers: process.env.CI ? 1 : undefined, 34 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 35 | reporter: 'html', 36 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 37 | use: { 38 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 39 | actionTimeout: 0, 40 | 41 | /* Base URL to use in actions like `await page.goto('/')`. */ 42 | baseURL: 'http://localhost:8080', 43 | 44 | screenshot: 'only-on-failure', 45 | 46 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 47 | trace: 'on-first-retry', 48 | }, 49 | 50 | /* Configure projects for major browsers */ 51 | projects: [ 52 | { 53 | name: 'chromium', 54 | use: {...devices['Desktop Chrome']}, 55 | }, 56 | 57 | { 58 | name: 'firefox', 59 | use: {...devices['Desktop Firefox']}, 60 | }, 61 | 62 | { 63 | name: 'webkit', 64 | use: {...devices['Desktop Safari']}, 65 | }, 66 | 67 | /* Test against mobile viewports. */ 68 | // { 69 | // name: 'Mobile Chrome', 70 | // use: { ...devices['Pixel 5'] }, 71 | // }, 72 | // { 73 | // name: 'Mobile Safari', 74 | // use: { ...devices['iPhone 12'] }, 75 | // }, 76 | 77 | /* Test against branded browsers. */ 78 | // { 79 | // name: 'Microsoft Edge', 80 | // use: { channel: 'msedge' }, 81 | // }, 82 | // { 83 | // name: 'Google Chrome', 84 | // use: { channel: 'chrome' }, 85 | // }, 86 | ], 87 | 88 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 89 | // outputDir: 'test-results/', 90 | snapshotDir: 'test/snapshots/', 91 | 92 | /* Run your local dev server before starting the tests */ 93 | webServer: { 94 | command: 'npm run start', 95 | port: 8080, 96 | timeout: 5 * 1000, 97 | reuseExistingServer: !process.env.CI, 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /public/images/backward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohmjs/ohm-editor/2ff2a07225031ead520ad22c2bc19bac7ce75ff2/public/images/backward.png -------------------------------------------------------------------------------- /public/images/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/images/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/images/end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohmjs/ohm-editor/2ff2a07225031ead520ad22c2bc19bac7ce75ff2/public/images/end.png -------------------------------------------------------------------------------- /public/images/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohmjs/ohm-editor/2ff2a07225031ead520ad22c2bc19bac7ce75ff2/public/images/forward.png -------------------------------------------------------------------------------- /public/images/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohmjs/ohm-editor/2ff2a07225031ead520ad22c2bc19bac7ce75ff2/public/images/start.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ohm Editor 6 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |
32 |
33 |
    34 |
  • 35 | 38 |
  • 39 |
  • 40 | 43 |
  • 44 |
45 |
46 |
47 |
48 |
49 | 50 |
51 | 63 | 79 |
80 | 81 |
82 |
83 |
84 | 85 | 95 | 96 | 97 | 98 | 141 | 142 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /public/style/cm-theme-light-owl.css: -------------------------------------------------------------------------------- 1 | /* 2 | A CodeMirror theme based on Sarah Drasner's Night Owl Light: 3 | https://github.com/sdras/night-owl-vscode-theme/blob/main/themes/Night%20Owl-Light-color-theme.json 4 | */ 5 | 6 | .cm-s-light-owl span.cm-comment { 7 | color: #989fb1; 8 | } 9 | .cm-s-light-owl span.cm-string { 10 | color: #c96765; 11 | } 12 | .cm-s-light-owl span.cm-grammarDef { 13 | color: #4876d6; 14 | } 15 | .cm-s-light-owl span.cm-ruleDef { 16 | color: #4876d6; 17 | } 18 | .cm-s-light-owl span.cm-operator { 19 | color: #994cc3; 20 | } 21 | 22 | .cm-s-light-owl span.cm-meta { 23 | color: #989fb1; 24 | } 25 | .cm-s-light-owl span.cm-caseName { 26 | color: #989fb1; 27 | font-style: italic; 28 | } 29 | -------------------------------------------------------------------------------- /public/style/ellipsis-dropdown.css: -------------------------------------------------------------------------------- 1 | button.ellipsis-btn { 2 | background-color: transparent; 3 | border: none; 4 | font-size: 18px; 5 | font-weight: 900; 6 | margin-top: 1px; 7 | padding: 0 4px; 8 | } 9 | 10 | button.ellipsis-btn:hover { 11 | background-color: var(--button-hover-background-color); 12 | border: none; 13 | } 14 | 15 | button.ellipsis-btn:active { 16 | background-color: var(--active-hover-background-color); 17 | } 18 | 19 | .dropdown-menu { 20 | margin-top: 4px; 21 | } 22 | -------------------------------------------------------------------------------- /public/style/example-list.css: -------------------------------------------------------------------------------- 1 | #exampleContainer { 2 | display: flex; 3 | flex-direction: row; 4 | flex: auto; 5 | grid-area: examples; 6 | } 7 | 8 | #exampleContainer h2 { 9 | color: #333; 10 | flex: 0; 11 | font-size: 10px; 12 | font-weight: normal; 13 | margin: 8px 6px 8px 0; 14 | text-transform: uppercase; 15 | } 16 | 17 | #exampleContainer .section-head { 18 | padding-right: 6px; 19 | } 20 | 21 | #exampleList { 22 | list-style: none; 23 | margin: 0; 24 | padding: 0; 25 | } 26 | 27 | .flex-spacer { 28 | flex: 1; 29 | } 30 | 31 | .example-count.pass.fail { 32 | background-color: var(--color-warning-1); 33 | color: var(--button-text-color); 34 | } 35 | 36 | .example-count.pass.fail:hover { 37 | background-color: var(--color-warning-2); 38 | } 39 | 40 | .example-count.pass.fail:active { 41 | background-color: var(--color-warning-3); 42 | } 43 | 44 | .example-count.fail { 45 | background-color: var(--color-failure-1); 46 | color: white; 47 | } 48 | 49 | .example-count.fail:hover { 50 | background-color: var(--color-failure-2); 51 | } 52 | 53 | .example-count.fail:active { 54 | background-color: var(--color-failure-3); 55 | } 56 | 57 | .chevron-btn { 58 | background-color: transparent; 59 | background-image: url('../images/chevron-down.svg'); 60 | background-repeat: no-repeat; 61 | background-position: center; 62 | background-size: 12px; 63 | border: none; 64 | border-radius: 10px; 65 | color: var(--button-text-color); 66 | cursor: default; 67 | height: 20px; 68 | width: 20px; 69 | } 70 | 71 | .chevron-btn:hover { 72 | background-color: var(--button-hover-background-color); 73 | } 74 | 75 | .chevron-btn.closed { 76 | background-image: url('../images/chevron-up.svg'); 77 | } 78 | 79 | #addExampleLink { 80 | color: #999; 81 | font-size: 12px; 82 | margin: 6px 12px; 83 | text-decoration: none; 84 | } 85 | 86 | #addExampleLink:hover { 87 | color: #666; 88 | text-decoration: underline; 89 | } 90 | 91 | #exampleList .example { 92 | align-items: center; 93 | border-bottom: 1px solid #eee; 94 | color: #999; 95 | margin: 0; 96 | white-space: nowrap; 97 | } 98 | 99 | #exampleList .example code { 100 | border-left: 4px solid #bbb; 101 | cursor: default; 102 | flex: 1; 103 | overflow: hidden; 104 | padding: 6px 8px 6px 8px; 105 | text-overflow: ellipsis; 106 | 107 | /* Weirdly, it won't shrink smaller than its content unless we give it an explicit width. */ 108 | width: 1px; 109 | } 110 | 111 | #exampleList .example.fail code { 112 | border-left: 4px solid var(--color-failure-1); 113 | } 114 | 115 | #exampleList .example.pass code { 116 | border-left: 4px solid var(--color-success-1); 117 | } 118 | 119 | #exampleList .example.pendingUpdate code { 120 | color: #bbb; 121 | } 122 | 123 | #exampleList .example.selected { 124 | color: #333; 125 | background-color: #f7f7f7; 126 | } 127 | 128 | #exampleList code { 129 | font-family: Menlo, Monaco, monospace; 130 | font-size: 12px; 131 | } 132 | 133 | #exampleList .example .thumbsUpButton, 134 | #exampleList .example .delete { 135 | cursor: default; 136 | padding: 0 4px 0 4px; 137 | } 138 | 139 | #exampleList .example:not(.selected) .thumbsUpButton:not(:hover) { 140 | filter: grayscale(1); 141 | opacity: 0.6; 142 | } 143 | 144 | #exampleList .example .delete { 145 | color: #999; 146 | font-size: 12px; 147 | margin-right: 4px; 148 | visibility: hidden; 149 | } 150 | 151 | #exampleList .example:hover .delete { 152 | visibility: visible; 153 | } 154 | 155 | #exampleList .example .delete:hover { 156 | color: #666; 157 | } 158 | 159 | @media screen and (min-color-index: 0) and(-webkit-min-device-pixel-ratio:0) { 160 | @media { 161 | /* Safari 6.1+ ONLY */ 162 | #exampleList .example .thumbsUpButton { 163 | font-size: 12px; 164 | } 165 | } 166 | } 167 | 168 | /* Firefox 1+ only */ 169 | #exampleList .example .thumbsUpButton, 170 | x:-moz-any-link { 171 | font-size: 16px; 172 | } 173 | 174 | #exampleList .example:hover { 175 | background-color: #f7f7f7; 176 | } 177 | 178 | #exampleList code:empty::before { 179 | content: '\a0'; /* Insert   to keep the correct height. */ 180 | } 181 | 182 | .startRule { 183 | cursor: default; 184 | color: hsl(0, 0%, 75%); 185 | font-family: inherit; 186 | font-size: 12px; 187 | margin-right: 4px; 188 | } 189 | 190 | .selected .startRule { 191 | color: hsl(0, 0%, 65%); 192 | } 193 | 194 | #startRuleDropdown { 195 | min-width: 85px; 196 | } 197 | 198 | #userExampleContainer > .contents { 199 | border-top: 1px solid #ddd; 200 | flex: 1; 201 | overflow: auto; 202 | position: relative; /* For positioning #exampleEditor */ 203 | } 204 | 205 | #editorOverlay { 206 | background-color: rgba(0, 0, 0, 0.03); 207 | display: flex; 208 | padding: 20px; 209 | 210 | position: absolute; 211 | bottom: 0; 212 | left: 0; 213 | right: 0; 214 | top: 0; 215 | } 216 | 217 | #exampleEditor { 218 | background-color: white; 219 | border-radius: 3px; 220 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); 221 | display: flex; 222 | flex: 1; 223 | flex-direction: column; 224 | } 225 | 226 | #exampleEditor .header { 227 | background-color: #fafafa; 228 | border-bottom: 1px solid #ddd; 229 | border-top-left-radius: 3px; 230 | border-top-right-radius: 3px; 231 | color: #666; 232 | padding: 8px 12px; 233 | } 234 | 235 | #exampleEditor .header .title { 236 | flex: 1; 237 | font-weight: 200; 238 | } 239 | 240 | #exampleEditor .exampleText { 241 | flex: 1; 242 | } 243 | 244 | #exampleEditor > .editorWrapper { 245 | border-bottom: 1px solid #eee; 246 | border-bottom-left-radius: 3px; 247 | border-bottom-right-radius: 3px; 248 | flex: 1; 249 | margin: 0; 250 | position: relative; 251 | } 252 | 253 | #exampleEditor .toolbar { 254 | padding: 0 12px 0 12px; 255 | } 256 | 257 | #exampleEditor .toolbar .gap { 258 | flex: 1; 259 | } 260 | 261 | #exampleEditor .toolbar .errorIcon { 262 | cursor: help; 263 | font-family: sans-serif; /* Ensures the emoji is shown in color. */ 264 | font-size: 13px; 265 | margin: 0 4px; 266 | position: relative; 267 | } 268 | 269 | #exampleEditor .toolbar label { 270 | margin-right: 4px; 271 | text-align: right; 272 | } 273 | 274 | #exampleEditor .toolbar > .contents { 275 | align-items: center; 276 | background-color: white; 277 | border-bottom: 1px solid #eee; 278 | font-size: 12px; 279 | padding: 4px 0; 280 | } 281 | 282 | #exampleEditor .toolbar .thumbsUpButton { 283 | cursor: default; 284 | font-size: 16px; 285 | margin: 0 4px 0 8px; 286 | } 287 | 288 | #exampleEditor .CodeMirror-lines { 289 | padding-bottom: 16px; 290 | padding-top: 16px; 291 | } 292 | 293 | #exampleEditor .CodeMirror-lines pre { 294 | padding-left: 12px; 295 | } 296 | 297 | #exampleEditor .CodeMirror-placeholder { 298 | color: #999; 299 | } 300 | 301 | #exampleEditor.hideInputErrors .error { 302 | display: none; 303 | } 304 | 305 | .fade-enter-active, 306 | .fade-leave-active { 307 | transition: opacity 0.25s; 308 | } 309 | .fade-enter, 310 | .fade-leave-to { 311 | opacity: 0; 312 | } 313 | -------------------------------------------------------------------------------- /public/style/parseTree.css: -------------------------------------------------------------------------------- 1 | #parseTree { 2 | display: flex; 3 | height: 100%; 4 | width: 100%; 5 | } 6 | 7 | #zoomOutButton { 8 | background-color: white; 9 | outline: none; 10 | border: 0; 11 | padding: 3px; 12 | margin: 5px 10px 0 10px; 13 | cursor: pointer; 14 | font-size: 18px; 15 | color: black; 16 | text-align: center; 17 | align-self: flex-start; 18 | } 19 | #zoomOutButton:hover { 20 | font-weight: bold; 21 | } 22 | 23 | #expandedInputWrapper { 24 | border-bottom: 1px solid #ddd; 25 | padding: 8px 0; 26 | position: relative; /* For positioning #expandedInput. */ 27 | } 28 | 29 | /* 30 | The sizer serves as a stand-in for the text that will be rendered in the canvas 31 | element; the style must be consistent with what is actually rendered. 32 | */ 33 | #expandedInputWrapper > #sizer { 34 | font-family: Menlo, Monaco, sans-serif; 35 | font-size: 100%; 36 | } 37 | #expandedInput { 38 | position: absolute; 39 | left: 0; 40 | top: 8px; 41 | } 42 | 43 | #parseResults { 44 | flex: auto; 45 | overflow: auto; 46 | padding: 2px; 47 | } 48 | .pexpr { 49 | color: #333; 50 | display: inline-block; 51 | flex-grow: 1; 52 | font-family: Menlo, Monaco, sans-serif; 53 | font-size: 9px; 54 | overflow: hidden; 55 | white-space: nowrap; 56 | } 57 | .pexpr.zoomBorder { 58 | border: 2px solid blue; 59 | } 60 | .pexpr.seq.failed { 61 | flex-grow: 0; 62 | } 63 | .pexpr.alt > .children > .pexpr { 64 | margin-left: 0; 65 | } 66 | .pexpr.failed > .self .label { 67 | background-color: transparent; 68 | box-sizing: border-box; 69 | color: #d44950; 70 | text-align: left; 71 | } 72 | 73 | .pexpr:not(.failed) > .self .label:hover { 74 | background-color: #ddd; 75 | } 76 | .pexpr:not(.collapsed) > .self .label:hover { 77 | border-bottom-color: #ddd; 78 | } 79 | .pexpr.failed > .self .label:hover { 80 | border-bottom: 1px solid #ccc; 81 | } 82 | 83 | .pexpr.leaf > .self .label, 84 | .pexpr.leaf > .self .label:hover { 85 | border-bottom-color: transparent; 86 | cursor: default; 87 | } 88 | 89 | /* A successful expression in a failed branch */ 90 | #parseResults 91 | > .pexpr 92 | > .children 93 | .pexpr.failed 94 | .pexpr:not(.failed):not(.unevaluated) 95 | > .self 96 | .label { 97 | background-color: #ffd7de; 98 | } 99 | 100 | /* 101 | Use disclosures to distinguish vertically-stacked siblings from parent/children relationships. 102 | */ 103 | .pexpr.disclosure { 104 | padding-left: 10px; 105 | position: relative; /* For positioning the ::before element */ 106 | } 107 | .pexpr.disclosure::before { 108 | color: #bbb; 109 | content: '\25BC'; /* Black Down-pointing triangle */ 110 | font-size: 10px; 111 | left: 2px; 112 | position: absolute; 113 | top: 1px; 114 | width: 8px; 115 | } 116 | .pexpr.disclosure.collapsed::before { 117 | content: '\25B6'; /* Black Right-pointing triangle */ 118 | left: 3px; 119 | } 120 | 121 | /* 122 | By default, give .self a transparent border to enforce spacing between nodes. 123 | It can also be used to put a border around a label without affecting the layout. 124 | */ 125 | .pexpr:not(.unlabeled) > .self { 126 | border: 1px solid transparent; 127 | } 128 | .pexpr.currentParseStep > .self { 129 | border: 1px solid rgb(53, 151, 255) !important; 130 | } 131 | 132 | .pexpr.undecided > .self .label { 133 | background-color: transparent; 134 | border-bottom: 1px solid #dfdfdf; 135 | box-sizing: border-box; 136 | color: #aaa; 137 | font-style: italic; 138 | } 139 | .pexpr > .self .label { 140 | background-color: #eaeaea; 141 | border-bottom: 1px solid #eaeaea; 142 | cursor: pointer; 143 | display: block; 144 | margin: 0; 145 | padding: 2px 2px 1px 2px; 146 | position: relative; 147 | text-align: center; 148 | } 149 | .pexpr > .self .label .caseName::before { 150 | content: '\2014'; 151 | padding: 0 0.5em; 152 | } 153 | .pexpr > .self .label .caseName { 154 | font-style: italic; 155 | opacity: 0.7; 156 | } 157 | 158 | .pexpr.unevaluated > .self .label { 159 | background-color: transparent; 160 | color: #aaa; 161 | } 162 | .pexpr[hidden] { 163 | visibility: hidden; 164 | } 165 | .pexpr.unlabeled > .self .label { 166 | display: none; 167 | } 168 | .pexpr.collapsed > .self .label { 169 | border-bottom: 1px dashed #999; 170 | } 171 | .pexpr.failed.disclosure.collapsed > .self .label { 172 | border-bottom-color: transparent; /* Prevent dashed border */ 173 | } 174 | .pexpr > .children { 175 | display: flex; 176 | flex-direction: row; 177 | } 178 | .pexpr > .children[hidden] { 179 | display: none; 180 | } 181 | .pexpr .vbox { 182 | display: flex; 183 | flex-direction: column; 184 | } 185 | 186 | #visualizerBody { 187 | display: flex; 188 | flex: auto; 189 | flex-direction: column; 190 | overflow: hidden; 191 | } 192 | -------------------------------------------------------------------------------- /public/third_party/FileSaver.js: -------------------------------------------------------------------------------- 1 | /* FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 1.3.2 4 | * 2016-06-16 18:25:19 5 | * 6 | * By Eli Grey, http://eligrey.com 7 | * License: MIT 8 | * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md 9 | */ 10 | 11 | /*global self */ 12 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ 13 | 14 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 15 | 16 | var saveAs = saveAs || (function(view) { 17 | "use strict"; 18 | // IE <10 is explicitly unsupported 19 | if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { 20 | return; 21 | } 22 | var 23 | doc = view.document 24 | // only get URL when necessary in case Blob.js hasn't overridden it yet 25 | , get_URL = function() { 26 | return view.URL || view.webkitURL || view; 27 | } 28 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 29 | , can_use_save_link = "download" in save_link 30 | , click = function(node) { 31 | var event = new MouseEvent("click"); 32 | node.dispatchEvent(event); 33 | } 34 | , is_safari = /constructor/i.test(view.HTMLElement) 35 | , is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent) 36 | , throw_outside = function(ex) { 37 | (view.setImmediate || view.setTimeout)(function() { 38 | throw ex; 39 | }, 0); 40 | } 41 | , force_saveable_type = "application/octet-stream" 42 | // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to 43 | , arbitrary_revoke_timeout = 1000 * 40 // in ms 44 | , revoke = function(file) { 45 | var revoker = function() { 46 | if (typeof file === "string") { // file is an object URL 47 | get_URL().revokeObjectURL(file); 48 | } else { // file is a File 49 | file.remove(); 50 | } 51 | }; 52 | setTimeout(revoker, arbitrary_revoke_timeout); 53 | } 54 | , dispatch = function(filesaver, event_types, event) { 55 | event_types = [].concat(event_types); 56 | var i = event_types.length; 57 | while (i--) { 58 | var listener = filesaver["on" + event_types[i]]; 59 | if (typeof listener === "function") { 60 | try { 61 | listener.call(filesaver, event || filesaver); 62 | } catch (ex) { 63 | throw_outside(ex); 64 | } 65 | } 66 | } 67 | } 68 | , auto_bom = function(blob) { 69 | // prepend BOM for UTF-8 XML and text/* types (including HTML) 70 | // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF 71 | if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { 72 | return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type}); 73 | } 74 | return blob; 75 | } 76 | , FileSaver = function(blob, name, no_auto_bom) { 77 | if (!no_auto_bom) { 78 | blob = auto_bom(blob); 79 | } 80 | // First try a.download, then web filesystem, then object URLs 81 | var 82 | filesaver = this 83 | , type = blob.type 84 | , force = type === force_saveable_type 85 | , object_url 86 | , dispatch_all = function() { 87 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 88 | } 89 | // on any filesys errors revert to saving with object URLs 90 | , fs_error = function() { 91 | if ((is_chrome_ios || (force && is_safari)) && view.FileReader) { 92 | // Safari doesn't allow downloading of blob urls 93 | var reader = new FileReader(); 94 | reader.onloadend = function() { 95 | var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;'); 96 | var popup = view.open(url, '_blank'); 97 | if(!popup) view.location.href = url; 98 | url=undefined; // release reference before dispatching 99 | filesaver.readyState = filesaver.DONE; 100 | dispatch_all(); 101 | }; 102 | reader.readAsDataURL(blob); 103 | filesaver.readyState = filesaver.INIT; 104 | return; 105 | } 106 | // don't create more object URLs than needed 107 | if (!object_url) { 108 | object_url = get_URL().createObjectURL(blob); 109 | } 110 | if (force) { 111 | view.location.href = object_url; 112 | } else { 113 | var opened = view.open(object_url, "_blank"); 114 | if (!opened) { 115 | // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html 116 | view.location.href = object_url; 117 | } 118 | } 119 | filesaver.readyState = filesaver.DONE; 120 | dispatch_all(); 121 | revoke(object_url); 122 | } 123 | ; 124 | filesaver.readyState = filesaver.INIT; 125 | 126 | if (can_use_save_link) { 127 | object_url = get_URL().createObjectURL(blob); 128 | setTimeout(function() { 129 | save_link.href = object_url; 130 | save_link.download = name; 131 | click(save_link); 132 | dispatch_all(); 133 | revoke(object_url); 134 | filesaver.readyState = filesaver.DONE; 135 | }); 136 | return; 137 | } 138 | 139 | fs_error(); 140 | } 141 | , FS_proto = FileSaver.prototype 142 | , saveAs = function(blob, name, no_auto_bom) { 143 | return new FileSaver(blob, name || blob.name || "download", no_auto_bom); 144 | } 145 | ; 146 | // IE 10+ (native saveAs) 147 | if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { 148 | return function(blob, name, no_auto_bom) { 149 | name = name || blob.name || "download"; 150 | 151 | if (!no_auto_bom) { 152 | blob = auto_bom(blob); 153 | } 154 | return navigator.msSaveOrOpenBlob(blob, name); 155 | }; 156 | } 157 | 158 | FS_proto.abort = function(){}; 159 | FS_proto.readyState = FS_proto.INIT = 0; 160 | FS_proto.WRITING = 1; 161 | FS_proto.DONE = 2; 162 | 163 | FS_proto.error = 164 | FS_proto.onwritestart = 165 | FS_proto.onprogress = 166 | FS_proto.onwrite = 167 | FS_proto.onabort = 168 | FS_proto.onerror = 169 | FS_proto.onwriteend = 170 | null; 171 | 172 | return saveAs; 173 | }( 174 | typeof self !== "undefined" && self 175 | || typeof window !== "undefined" && window 176 | || this.content 177 | )); 178 | // `self` is undefined in Firefox for Android content script context 179 | // while `this` is nsIContentFrameMessageManager 180 | // with an attribute `content` that corresponds to the window 181 | 182 | if (typeof module !== "undefined" && module.exports) { 183 | module.exports.saveAs = saveAs; 184 | } else if ((typeof define !== "undefined" && define !== null) && (define.amd !== null)) { 185 | define([], function() { 186 | return saveAs; 187 | }); 188 | } 189 | -------------------------------------------------------------------------------- /public/third_party/autosize.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Autosize 3.0.15 3 | license: MIT 4 | http://www.jacklmoore.com/autosize 5 | */ 6 | !function(e,t){if("function"==typeof define&&define.amd)define(["exports","module"],t);else if("undefined"!=typeof exports&&"undefined"!=typeof module)t(exports,module);else{var n={exports:{}};t(n.exports,n),e.autosize=n.exports}}(this,function(e,t){"use strict";function n(e){function t(){var t=window.getComputedStyle(e,null);p=t.overflowY,"vertical"===t.resize?e.style.resize="none":"both"===t.resize&&(e.style.resize="horizontal"),c="content-box"===t.boxSizing?-(parseFloat(t.paddingTop)+parseFloat(t.paddingBottom)):parseFloat(t.borderTopWidth)+parseFloat(t.borderBottomWidth),isNaN(c)&&(c=0),i()}function n(t){var n=e.style.width;e.style.width="0px",e.offsetWidth,e.style.width=n,p=t,f&&(e.style.overflowY=t),o()}function o(){var t=window.pageYOffset,n=document.body.scrollTop,o=e.style.height;e.style.height="auto";var i=e.scrollHeight+c;return 0===e.scrollHeight?void(e.style.height=o):(e.style.height=i+"px",v=e.clientWidth,document.documentElement.scrollTop=t,void(document.body.scrollTop=n))}function i(){var t=e.style.height;o();var i=window.getComputedStyle(e,null);if(i.height!==e.style.height?"visible"!==p&&n("visible"):"hidden"!==p&&n("hidden"),t!==e.style.height){var r=d("autosize:resized");e.dispatchEvent(r)}}var s=void 0===arguments[1]?{}:arguments[1],a=s.setOverflowX,l=void 0===a?!0:a,u=s.setOverflowY,f=void 0===u?!0:u;if(e&&e.nodeName&&"TEXTAREA"===e.nodeName&&!r.has(e)){var c=null,p=null,v=e.clientWidth,h=function(){e.clientWidth!==v&&i()},y=function(t){window.removeEventListener("resize",h,!1),e.removeEventListener("input",i,!1),e.removeEventListener("keyup",i,!1),e.removeEventListener("autosize:destroy",y,!1),e.removeEventListener("autosize:update",i,!1),r["delete"](e),Object.keys(t).forEach(function(n){e.style[n]=t[n]})}.bind(e,{height:e.style.height,resize:e.style.resize,overflowY:e.style.overflowY,overflowX:e.style.overflowX,wordWrap:e.style.wordWrap});e.addEventListener("autosize:destroy",y,!1),"onpropertychange"in e&&"oninput"in e&&e.addEventListener("keyup",i,!1),window.addEventListener("resize",h,!1),e.addEventListener("input",i,!1),e.addEventListener("autosize:update",i,!1),r.add(e),l&&(e.style.overflowX="hidden",e.style.wordWrap="break-word"),t()}}function o(e){if(e&&e.nodeName&&"TEXTAREA"===e.nodeName){var t=d("autosize:destroy");e.dispatchEvent(t)}}function i(e){if(e&&e.nodeName&&"TEXTAREA"===e.nodeName){var t=d("autosize:update");e.dispatchEvent(t)}}var r="function"==typeof Set?new Set:function(){var e=[];return{has:function(t){return Boolean(e.indexOf(t)>-1)},add:function(t){e.push(t)},"delete":function(t){e.splice(e.indexOf(t),1)}}}(),d=function(e){return new Event(e)};try{new Event("test")}catch(s){d=function(e){var t=document.createEvent("Event");return t.initEvent(e,!0,!1),t}}var a=null;"undefined"==typeof window||"function"!=typeof window.getComputedStyle?(a=function(e){return e},a.destroy=function(e){return e},a.update=function(e){return e}):(a=function(e,t){return e&&Array.prototype.forEach.call(e.length?e:[e],function(e){return n(e,t)}),e},a.destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],o),e},a.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],i),e}),t.exports=a}); -------------------------------------------------------------------------------- /public/third_party/codemirror-5.65.12/codemirror.min.css: -------------------------------------------------------------------------------- 1 | .CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor .CodeMirror-line::selection,.cm-fat-cursor .CodeMirror-line>span::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:0 0}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:0 0}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:0;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none;outline:0}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0} -------------------------------------------------------------------------------- /public/third_party/codemirror-5.65.12/placeholder.min.js: -------------------------------------------------------------------------------- 1 | !function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(r){function n(e){e.state.placeholder&&(e.state.placeholder.parentNode.removeChild(e.state.placeholder),e.state.placeholder=null)}function i(e){n(e);var o=e.state.placeholder=document.createElement("pre"),t=(o.style.cssText="height: 0; overflow: visible",o.style.direction=e.getOption("direction"),o.className="CodeMirror-placeholder CodeMirror-line-like",e.getOption("placeholder"));"string"==typeof t&&(t=document.createTextNode(t)),o.appendChild(t),e.display.lineSpace.insertBefore(o,e.display.lineSpace.firstChild)}function l(e){c(e)&&i(e)}function a(e){var o=e.getWrapperElement(),t=c(e);o.className=o.className.replace(" CodeMirror-empty","")+(t?" CodeMirror-empty":""),(t?i:n)(e)}function c(e){return 1===e.lineCount()&&""===e.getLine(0)}r.defineOption("placeholder","",function(e,o,t){var t=t&&t!=r.Init;o&&!t?(e.on("blur",l),e.on("change",a),e.on("swapDoc",a),r.on(e.getInputField(),"compositionupdate",e.state.placeholderCompose=function(){var t;t=e,setTimeout(function(){var e,o=!1;((o=1==t.lineCount()?"TEXTAREA"==(e=t.getInputField()).nodeName?!t.getLine(0).length:!/[^\u200b]/.test(e.querySelector(".CodeMirror-line").textContent):o)?i:n)(t)},20)}),a(e)):!o&&t&&(e.off("blur",l),e.off("change",a),e.off("swapDoc",a),r.off(e.getInputField(),"compositionupdate",e.state.placeholderCompose),n(e),(t=e.getWrapperElement()).className=t.className.replace(" CodeMirror-empty","")),o&&!e.hasFocus()&&l(e)})}); -------------------------------------------------------------------------------- /public/third_party/codemirror-5.65.12/search.min.js: -------------------------------------------------------------------------------- 1 | !function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror"),require("./searchcursor"),require("../dialog/dialog")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","./searchcursor","../dialog/dialog"],e):e(CodeMirror)}(function(f){"use strict";function r(){this.posFrom=this.posTo=this.lastQuery=this.query=null,this.overlay=null}function p(e){return e.state.search||(e.state.search=new r)}function n(e){return"string"==typeof e&&e==e.toLowerCase()}function d(e,r,o){return e.getSearchCursor(r,o,{caseFold:n(r),multiline:!0})}function m(e,r,o,t,n){e.openDialog?e.openDialog(r,n,{value:t,selectValueOnOpen:!0,bottom:e.options.search.bottom}):n(prompt(o,t))}function h(e){return e.replace(/\\([nrt\\])/g,function(e,r){return"n"==r?"\n":"r"==r?"\r":"t"==r?"\t":"\\"==r?"\\":e})}function a(e){var r=e.match(/^\/(.*)\/([a-z]*)$/);if(r)try{e=new RegExp(r[1],-1==r[2].indexOf("i")?"":"i")}catch(e){}else e=h(e);return e=("string"==typeof e?""==e:e.test(""))?/x^/:e}function y(e,r,o){var t;r.queryText=o,r.query=a(o),e.removeOverlay(r.overlay,n(r.query)),r.overlay=(t=r.query,o=n(r.query),"string"==typeof t?t=new RegExp(t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),o?"gi":"g"):t.global||(t=new RegExp(t.source,t.ignoreCase?"gi":"g")),{token:function(e){t.lastIndex=e.pos;var r=t.exec(e.string);if(r&&r.index==e.pos)return e.pos+=r[0].length||1,"searching";r?e.pos=r.index:e.skipToEnd()}}),e.addOverlay(r.overlay),e.showMatchesOnScrollbar&&(r.annotate&&(r.annotate.clear(),r.annotate=null),r.annotate=e.showMatchesOnScrollbar(r.query,n(r.query)))}function o(n,r,e,o){var t=p(n);if(t.query)return g(n,r);var a,i,s,c,l,u=n.getSelection()||t.lastQuery;u instanceof RegExp&&"x^"==u.source&&(u=null),e&&n.openDialog?(a=null,i=function(e,r){f.e_stop(r),e&&(e!=t.queryText&&(y(n,t,e),t.posFrom=t.posTo=n.getCursor()),a&&(a.style.opacity=1),g(n,r.shiftKey,function(e,r){var o;r.line<3&&document.querySelector&&(o=n.display.wrapper.querySelector(".CodeMirror-dialog"))&&o.getBoundingClientRect().bottom-4>n.cursorCoords(r,"window").top&&((a=o).style.opacity=.4)}))},e=C(s=n),c=u,l=function(e,r){var o=f.keyName(e),t=n.getOption("extraKeys"),t=t&&t[o]||f.keyMap[n.getOption("keyMap")][o];"findNext"==t||"findPrev"==t||"findPersistentNext"==t||"findPersistentPrev"==t?(f.e_stop(e),y(n,p(n),r),n.execCommand(t)):"find"!=t&&"findPersistent"!=t||(f.e_stop(e),i(r,e))},s.openDialog(e,i,{value:c,selectValueOnOpen:!0,closeOnEnter:!1,onClose:function(){v(s)},onKeyDown:l,bottom:s.options.search.bottom}),o&&u&&(y(n,t,u),g(n,r))):m(n,C(n),"Search for:",u,function(e){e&&!t.query&&n.operation(function(){y(n,t,e),t.posFrom=t.posTo=n.getCursor(),g(n,r)})})}function g(o,t,n){o.operation(function(){var e=p(o),r=d(o,e.query,t?e.posFrom:e.posTo);(r.find(t)||(r=d(o,e.query,t?f.Pos(o.lastLine()):f.Pos(o.firstLine(),0))).find(t))&&(o.setSelection(r.from(),r.to()),o.scrollIntoView({from:r.from(),to:r.to()},20),e.posFrom=r.from(),e.posTo=r.to(),n&&n(r.from(),r.to()))})}function v(r){r.operation(function(){var e=p(r);e.lastQuery=e.query,e.query&&(e.query=e.queryText=null,r.removeOverlay(e.overlay),e.annotate&&(e.annotate.clear(),e.annotate=null))})}function x(e,r){var o,t=e?document.createElement(e):document.createDocumentFragment();for(o in r)t[o]=r[o];for(var n=2;nt.length-n)break;(!i||h>i.index+i[0].length)&&(i=o),r=o.index+1}return i}function O(t,e,n){e=m(e,"g");for(var i=n.line,r=n.ch,o=t.firstLine();o<=i;i--,r=-1){var h=t.getLine(i),h=L(h,e,r<0?0:h.length-r);if(h)return{from:x(i,h.index),to:x(i,h.index+h[0].length),match:h}}}function h(t,e,n){if(!d(e))return O(t,e,n);e=m(e,"gm");for(var i=1,r=t.getLine(n.line).length-n.ch,o=n.line,h=t.firstLine();h<=o;){for(var l=0;l>1,l=i(t.slice(0,h)).length;if(l==n)return h;n(this.doc.getLine(e.line)||"").length&&(e.ch=0,e.line++)),0!=r.cmpPos(e,this.doc.clipPos(e))))return this.atOccurrence=!1;var e=this.matches(t,e);return this.afterEmptyMatch=e&&0==r.cmpPos(e.from,e.to),e?(this.pos=e,this.atOccurrence=!0,this.pos.match||!0):(e=x(t?this.doc.firstLine():this.doc.lastLine()+1,0),this.pos={from:e,to:e},this.atOccurrence=!1)},from:function(){if(this.atOccurrence)return this.pos.from},to:function(){if(this.atOccurrence)return this.pos.to},replace:function(t,e){this.atOccurrence&&(t=r.splitLines(t),this.doc.replaceRange(t,this.pos.from,this.pos.to,e),this.pos.to=x(this.pos.from.line+t.length-1,t[t.length-1].length+(1==t.length?this.pos.from.ch:0)))}},r.defineExtension("getSearchCursor",function(t,e,n){return new i(this.doc,t,e,n)}),r.defineDocExtension("getSearchCursor",function(t,e,n){return new i(this,t,e,n)}),r.defineExtension("selectMatches",function(t,e){for(var n=[],i=this.getSearchCursor(t,this.getCursor("from"),e);i.findNext()&&!(0 li > a { 48 | display: block; 49 | padding: 3px 20px; 50 | clear: both; 51 | font-weight: normal; 52 | line-height: 1.42857143; 53 | color: #333; 54 | text-decoration: none; 55 | white-space: nowrap; 56 | } 57 | .dropdown-menu > li > a:hover, 58 | .dropdown-menu > li > a:focus { 59 | color: #262626; 60 | text-decoration: none; 61 | background-color: #f5f5f5; 62 | } 63 | .dropdown-menu > .active > a, 64 | .dropdown-menu > .active > a:hover, 65 | .dropdown-menu > .active > a:focus { 66 | color: #fff; 67 | text-decoration: none; 68 | background-color: #337ab7; 69 | outline: 0; 70 | } 71 | .dropdown-menu > .disabled > a, 72 | .dropdown-menu > .disabled > a:hover, 73 | .dropdown-menu > .disabled > a:focus { 74 | color: #777; 75 | } 76 | .dropdown-menu > .disabled > a:hover, 77 | .dropdown-menu > .disabled > a:focus { 78 | text-decoration: none; 79 | cursor: not-allowed; 80 | background-color: transparent; 81 | background-image: none; 82 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 83 | } 84 | .open > .dropdown-menu { 85 | display: block; 86 | } 87 | .open > a { 88 | outline: 0; 89 | } 90 | .dropdown-menu-right { 91 | right: 0; 92 | left: auto; 93 | } 94 | .dropdown-menu-left { 95 | right: auto; 96 | left: 0; 97 | } 98 | .dropdown-header { 99 | display: block; 100 | padding: 3px 20px; 101 | font-size: 12px; 102 | line-height: 1.42857143; 103 | color: #777; 104 | white-space: nowrap; 105 | } 106 | .dropdown-backdrop { 107 | position: fixed; 108 | top: 0; 109 | right: 0; 110 | bottom: 0; 111 | left: 0; 112 | z-index: 990; 113 | } 114 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Ohm Visualizer 2 | 3 | The visualizer is a work in progress. For now, the visualizer only runs on 4 | a grammar and input that is hardcoded in index.html. Eventually, it should 5 | have a command line which works identically to the regular Ohm command line. 6 | 7 | To run the debugger, just open `visualizer/index.html` in your browser. For 8 | development, use `bin/ohm-visualizer` which opens the visualizer and enables 9 | live reloading whenever `dist/ohm.js` or any of the files in the `visualizer` 10 | directory are changed. 11 | -------------------------------------------------------------------------------- /src/TraceElementWalker.js: -------------------------------------------------------------------------------- 1 | /* global NodeFilter */ 2 | 3 | // Similar to a DOM TreeWalker (https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker), 4 | // but specialized for walking our parse trees. It visits only labeled PExpr nodes, and it 5 | // it visits interior nodes (i.e., `.pexpr.labeled:not(.leaf)` nodes) on the way in AND on 6 | // the way out -- even if they have no actual children. Whereas, the regular TreeWalker just 7 | // does a standard pre-order traversal. 8 | function TraceElementWalker(root, optConfig) { 9 | const config = optConfig || {}; 10 | 11 | this._root = root; 12 | this._walker = root.ownerDocument.createTreeWalker( 13 | root, 14 | NodeFilter.SHOW_ELEMENT, 15 | { 16 | acceptNode(node) { 17 | return node.classList.contains('pexpr') && 18 | node.classList.contains('labeled') 19 | ? NodeFilter.FILTER_ACCEPT 20 | : NodeFilter.FILTER_SKIP; 21 | }, 22 | } 23 | ); 24 | this.isAtEnd = !!config.startAtEnd; 25 | this.currentNode = null; 26 | this.exitingCurrentNode = false; 27 | 28 | // "End" means the next position past the last node. After intializing, a call to 29 | // previousNode() will move the walker back to the last node. 30 | if (config.startAtEnd) { 31 | // Find last sibling of first node. 32 | this._walker.nextNode(); 33 | while (this._walker.nextSibling() != null); 34 | } 35 | } 36 | 37 | TraceElementWalker.prototype._isInInitialState = function () { 38 | return this._walker.currentNode === this._root; 39 | }; 40 | 41 | // Advance to the next node using pre-order traversal. But, unlike the regular TreeWalker, visit 42 | // interior nodes twice -- once going in, and once coming out. 43 | TraceElementWalker.prototype.nextNode = function () { 44 | // Case 1: Entering an interior node or the first node. 45 | if ( 46 | !this.exitingCurrentNode && 47 | (this._isOnInteriorNode() || this._isInInitialState()) 48 | ) { 49 | const oldCurrentNode = this.currentNode; 50 | if ((this.currentNode = this._walker.firstChild()) != null) { 51 | this.exitingCurrentNode = false; 52 | } else { 53 | // The interior node has no actual children. Stay on the same node, but now we are exiting. 54 | this.currentNode = oldCurrentNode; 55 | this.exitingCurrentNode = true; 56 | } 57 | return this.currentNode; 58 | } 59 | 60 | // Case 2: Leaving an interior or leaf node. 61 | if ((this.currentNode = this._walker.nextSibling()) != null) { 62 | this.exitingCurrentNode = false; 63 | } else { 64 | this.currentNode = this._walker.parentNode(); 65 | this.exitingCurrentNode = this.currentNode != null; 66 | } 67 | 68 | if (!this.currentNode) { 69 | this.isAtEnd = true; 70 | } 71 | 72 | return this.currentNode; 73 | }; 74 | 75 | TraceElementWalker.prototype._isOnInteriorNode = function () { 76 | const node = this.currentNode; 77 | return node && !node.classList.contains('leaf'); 78 | }; 79 | 80 | TraceElementWalker.prototype.previousNode = function () { 81 | // Case 1: Entering an interior node (or the first node) backwards 82 | if (this.exitingCurrentNode) { 83 | const oldCurrentNode = this.currentNode; 84 | if ((this.currentNode = this._walker.lastChild()) != null) { 85 | this.exitingCurrentNode = this._isOnInteriorNode(); 86 | } else { 87 | // The interior node has no actual children. Stay on the same node, but now we are entering. 88 | this.currentNode = oldCurrentNode; 89 | this.exitingCurrentNode = false; 90 | } 91 | return this.currentNode; 92 | } 93 | 94 | // Case 2: Going back to an interior or leaf node. 95 | if (this.isAtEnd) { 96 | this.isAtEnd = false; 97 | this.currentNode = this._walker.currentNode; 98 | this.exitingCurrentNode = this._isOnInteriorNode(); 99 | } else if ((this.currentNode = this._walker.previousSibling()) != null) { 100 | this.exitingCurrentNode = this._isOnInteriorNode(); 101 | } else { 102 | this.currentNode = this._walker.parentNode(); 103 | this.exitingCurrentNode = false; 104 | } 105 | 106 | // If we reached the beginning, reset to the initial state. 107 | if (!this.currentNode) { 108 | this._walker.currentNode = this._root; 109 | } 110 | 111 | return this.currentNode; 112 | }; 113 | 114 | // Make `node` the walker's current node, as if we are just stepping into it. 115 | TraceElementWalker.prototype.stepInto = function (node) { 116 | this.currentNode = this._walker.currentNode = node; 117 | this.exitingCurrentNode = false; 118 | this.isAtEnd = false; 119 | }; 120 | 121 | // Make `node` the walker's current node, as if we are just stepping out of it. 122 | TraceElementWalker.prototype.stepOut = function (node) { 123 | this.stepInto(node); 124 | this.exitingCurrentNode = this._isOnInteriorNode(); 125 | }; 126 | 127 | export default TraceElementWalker; 128 | -------------------------------------------------------------------------------- /src/cmUtil.js: -------------------------------------------------------------------------------- 1 | // Private helpers 2 | // --------------- 3 | 4 | function countLeadingWhitespace(str) { 5 | return str.match(/^\s*/)[0].length; 6 | } 7 | 8 | function countTrailingWhitespace(str) { 9 | return str.match(/\s*$/)[0].length; 10 | } 11 | 12 | function indexToHeight(cm, index) { 13 | const pos = cm.posFromIndex(index); 14 | return cm.heightAtLine(pos.line, 'local'); 15 | } 16 | 17 | function isBlockSelectable(cm, startPos, endPos) { 18 | const lastLine = cm.getLine(endPos.line); 19 | return ( 20 | countLeadingWhitespace(cm.getLine(startPos.line)) === startPos.ch && 21 | lastLine.length - countTrailingWhitespace(lastLine) === endPos.ch 22 | ); 23 | } 24 | 25 | // Mark a block of text with `className` by marking entire lines. 26 | function markBlock(cm, startLine, endLine, className) { 27 | for (let i = startLine; i <= endLine; ++i) { 28 | cm.addLineClass(i, 'wrap', className); 29 | } 30 | return { 31 | clear() { 32 | for (let i = startLine; i <= endLine; ++i) { 33 | cm.removeLineClass(i, 'wrap', className); 34 | } 35 | }, 36 | }; 37 | } 38 | 39 | export function containsInterval(cm, interval) { 40 | const startPos = cm.posFromIndex(interval.startIdx); 41 | const endPos = cm.posFromIndex(interval.endIdx); 42 | return cm.getRange(startPos, endPos) === interval.contents; 43 | } 44 | 45 | export function markInterval(cm, interval, className, canHighlightBlocks) { 46 | const startPos = cm.posFromIndex(interval.startIdx); 47 | const endPos = cm.posFromIndex(interval.endIdx); 48 | 49 | // See if the selection can be expanded to a block selection. 50 | if (canHighlightBlocks && isBlockSelectable(cm, startPos, endPos)) { 51 | return markBlock(cm, startPos.line, endPos.line, className); 52 | } 53 | return cm.markText(startPos, endPos, {className}); 54 | } 55 | 56 | export function clearMark(mark) { 57 | if (mark) { 58 | mark.clear(); 59 | } 60 | } 61 | 62 | export function scrollToInterval(cm, interval) { 63 | const startHeight = indexToHeight(cm, interval.startIdx); 64 | const endHeight = indexToHeight(cm, interval.endIdx); 65 | const scrollInfo = cm.getScrollInfo(); 66 | const margin = scrollInfo.clientHeight - (endHeight - startHeight); 67 | if ( 68 | startHeight < scrollInfo.top || 69 | endHeight > scrollInfo.top + scrollInfo.clientHeight 70 | ) { 71 | cm.scrollIntoView( 72 | {left: 0, top: startHeight, right: 0, bottom: endHeight}, 73 | margin > 0 ? margin / 2 : undefined 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/ellipsis-dropdown.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.common.js'; 2 | 3 | const template = ` 4 | 28 | `; 29 | 30 | export default Vue.component('ellipsis-dropdown', { 31 | name: 'ellipsis-dropdown', 32 | template, 33 | props: { 34 | // Specifies the menu items. Format is {