├── .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 | [](https://github.com/ohmjs/ohm-editor/actions/workflows/node.js.yml)
4 | [](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 |
--------------------------------------------------------------------------------
/public/images/chevron-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
49 |
50 |
51 |
52 |
×
53 |
Log into GitHub
54 |
55 |
62 |
63 |
64 |
×
65 |
Save Grammar As
66 |
67 |
78 |
79 |
80 |
81 |
84 |
85 |
95 |
96 |
97 |
98 |
99 |
100 | Arithmetic {
101 | Exp
102 | = AddExp
103 |
104 | AddExp
105 | = AddExp "+" MulExp -- plus
106 | | AddExp "-" MulExp -- minus
107 | | MulExp
108 |
109 | MulExp
110 | = MulExp "*" ExpExp -- times
111 | | MulExp "/" ExpExp -- divide
112 | | ExpExp
113 |
114 | ExpExp
115 | = PriExp "^" ExpExp -- power
116 | | PriExp
117 |
118 | PriExp
119 | = "(" Exp ")" -- paren
120 | | "+" PriExp -- pos
121 | | "-" PriExp -- neg
122 | | ident
123 | | number
124 |
125 | ident (an identifier)
126 | = letter alnum*
127 |
128 | number (a number)
129 | = digit* "." digit+ -- fract
130 | | digit+ -- whole
131 | }
133 |
134 | 2 * (42 - 1) / 9
135 | 1+2*3
136 | oh no
137 | ( 123 )
138 | (2+4)*7
139 |
140 |
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 |
5 |
13 |
27 |
28 | `;
29 |
30 | export default Vue.component('ellipsis-dropdown', {
31 | name: 'ellipsis-dropdown',
32 | template,
33 | props: {
34 | // Specifies the menu items. Format is {
23 | `;
24 | return div;
25 | }
26 |
27 | test('nextNode', () => {
28 | const div = createTestDiv();
29 | const walker = new TraceElementWalker(div);
30 |
31 | // Ensure that it does an in-order traversal of the tree, ignoring any nodes
32 | // that don't have the 'pexpr' and 'labeled' classes (but not their children).
33 | assert.equal(walker.nextNode(), div.querySelector('#node0'));
34 | assert.is(walker.exitingCurrentNode, false);
35 |
36 | assert.equal(walker.nextNode(), div.querySelector('#node1'));
37 | assert.is(walker.exitingCurrentNode, false);
38 |
39 | // Since node2 is not marked as a leaf, it should get visited twice: once on the way
40 | // in, and once on the way out.
41 | assert.equal(walker.nextNode(), div.querySelector('#node2'));
42 | assert.is(walker.exitingCurrentNode, false);
43 |
44 | assert.equal(walker.nextNode(), div.querySelector('#node2'));
45 | assert.is(walker.exitingCurrentNode, true);
46 |
47 | assert.equal(walker.nextNode(), div.querySelector('#node0'));
48 | assert.is(walker.exitingCurrentNode, true);
49 |
50 | assert.equal(walker.nextNode(), div.querySelector('#node4'));
51 | assert.is(walker.exitingCurrentNode, false);
52 |
53 | assert.equal(walker.nextNode(), null);
54 | assert.is(walker.exitingCurrentNode, false);
55 |
56 | div.remove();
57 | });
58 |
59 | test('previousNode', () => {
60 | const div = createTestDiv();
61 | const walker = new TraceElementWalker(div);
62 |
63 | while (walker.nextNode().id !== 'node4'); // Go to last node
64 | assert.equal(walker.currentNode, div.querySelector('#node4'));
65 | assert.equal(walker.exitingCurrentNode, false);
66 |
67 | assert.equal(walker.previousNode(), div.querySelector('#node0'));
68 | assert.equal(walker.exitingCurrentNode, true);
69 |
70 | assert.equal(walker.previousNode(), div.querySelector('#node2'));
71 | assert.equal(walker.exitingCurrentNode, true);
72 | assert.equal(walker.previousNode(), div.querySelector('#node2'));
73 | assert.equal(walker.exitingCurrentNode, false);
74 |
75 | assert.equal(walker.previousNode(), div.querySelector('#node1'));
76 | assert.equal(walker.exitingCurrentNode, false);
77 |
78 | assert.equal(walker.previousNode(), div.querySelector('#node0'));
79 | assert.equal(walker.exitingCurrentNode, false);
80 |
81 | assert.equal(walker.previousNode(), null);
82 | assert.equal(walker.exitingCurrentNode, false);
83 |
84 | div.remove();
85 | });
86 |
87 | test('mixing nextNode and previousNode', () => {
88 | const div = createTestDiv();
89 | const walker = new TraceElementWalker(div);
90 |
91 | walker.nextNode();
92 | assert.equal(walker.previousNode(), null);
93 |
94 | walker.nextNode();
95 | walker.nextNode();
96 | assert.equal(walker.previousNode().id, 'node0');
97 |
98 | walker.nextNode();
99 |
100 | // node2 gets visited twice (entering/leaving) because it doesn't have the 'leaf' class.
101 | assert.equal(walker.nextNode().id, 'node2');
102 | assert.equal(walker.exitingCurrentNode, false);
103 | assert.equal(walker.nextNode().id, 'node2');
104 | assert.equal(walker.exitingCurrentNode, true);
105 |
106 | assert.equal(walker.previousNode().id, 'node2');
107 | assert.equal(walker.exitingCurrentNode, false);
108 | assert.equal(walker.nextNode().id, 'node2');
109 | assert.equal(walker.exitingCurrentNode, true);
110 |
111 | assert.equal(walker.nextNode().id, 'node0');
112 | assert.equal(walker.exitingCurrentNode, true);
113 |
114 | assert.equal(walker.nextNode().id, 'node4');
115 | assert.equal(walker.nextNode(), null);
116 |
117 | div.remove();
118 | });
119 |
120 | test('startAtEnd option', () => {
121 | const div = createTestDiv();
122 | const walker = new TraceElementWalker(div, {startAtEnd: true});
123 |
124 | assert.equal(walker.previousNode().id, 'node4');
125 | assert.equal(walker.exitingCurrentNode, false);
126 |
127 | assert.equal(walker.nextNode(), null);
128 | assert.equal(walker.previousNode().id, 'node4');
129 | assert.equal(walker.exitingCurrentNode, false);
130 |
131 | div.querySelector('#node4').remove();
132 |
133 | const walker2 = new TraceElementWalker(div, {startAtEnd: true});
134 | assert.equal(walker2.previousNode().id, 'node0');
135 | assert.equal(walker2.exitingCurrentNode, true);
136 | assert.equal(walker2.nextNode(), null);
137 | assert.equal(walker2.previousNode().id, 'node0');
138 | assert.equal(walker2.exitingCurrentNode, true);
139 |
140 | div.remove();
141 | });
142 |
143 | test('going backwards from end', () => {
144 | const div = createTestDiv();
145 | const walker = new TraceElementWalker(div);
146 |
147 | while (walker.nextNode());
148 | assert.equal(walker.previousNode(), div.querySelector('#node4'));
149 |
150 | walker.nextNode();
151 | walker.nextNode();
152 | assert.is(walker.previousNode(), div.querySelector('#node4'));
153 | assert.equal(walker.previousNode(), div.querySelector('#node0'));
154 |
155 | div.remove();
156 | });
157 |
158 | test('step into', () => {
159 | const div = createTestDiv();
160 | const walker = new TraceElementWalker(div);
161 |
162 | const $ = sel => div.querySelector(sel);
163 |
164 | walker.stepInto($('#node0'));
165 | assert.equal(walker.previousNode(), null);
166 |
167 | walker.stepInto($('#node0'));
168 | assert.equal(walker.currentNode.id, 'node0');
169 | assert.equal(walker.exitingCurrentNode, false);
170 |
171 | assert.equal(walker.nextNode().id, 'node1');
172 | assert.equal(walker.exitingCurrentNode, false);
173 |
174 | assert.equal(walker.nextNode().id, 'node2');
175 | assert.equal(walker.exitingCurrentNode, false);
176 | assert.equal(walker.nextNode().id, 'node2');
177 | assert.equal(walker.exitingCurrentNode, true);
178 |
179 | walker.stepInto($('#node1'));
180 | assert.equal(walker.exitingCurrentNode, false);
181 | assert.equal(walker.nextNode().id, 'node2');
182 |
183 | div.remove();
184 | });
185 |
186 | test('step into from end', () => {
187 | const div = createTestDiv();
188 | const walker = new TraceElementWalker(div, {startAtEnd: true});
189 |
190 | const $ = sel => div.querySelector(sel);
191 |
192 | walker.stepInto($('#node0'));
193 | assert.equal(walker.isAtEnd, false);
194 |
195 | div.remove();
196 | });
197 |
198 | test('step out', () => {
199 | const div = createTestDiv();
200 | const walker = new TraceElementWalker(div);
201 |
202 | const $ = sel => div.querySelector(sel);
203 |
204 | walker.stepOut($('#node0'));
205 | assert.equal(walker.currentNode, $('#node0'));
206 | assert.equal(walker.previousNode(), $('#node2'));
207 |
208 | walker.stepOut($('#node2'));
209 | assert.equal(walker.currentNode, $('#node2'));
210 | assert.equal(walker.exitingCurrentNode, true);
211 |
212 | // node2 gets visited twice, so after stepping out, we are still on node2, but
213 | // not exiting this time.
214 | assert.equal(walker.previousNode(), $('#node2'));
215 | assert.equal(walker.exitingCurrentNode, false);
216 |
217 | div.remove();
218 | });
219 |
220 | test.run();
221 |
--------------------------------------------------------------------------------
/test/test-ellipsis-dropdown.js:
--------------------------------------------------------------------------------
1 | import 'global-jsdom/register';
2 |
3 | import testUtils from '@vue/test-utils';
4 | import {test} from 'uvu';
5 | import * as assert from 'uvu/assert';
6 | import Vue from 'vue/dist/vue.common.js';
7 |
8 | import EllipsisDropdown from '../src/components/ellipsis-dropdown.js';
9 |
10 | // Helpers
11 | // -------
12 |
13 | function findEl(vm, query) {
14 | return vm.$el.querySelector(query);
15 | }
16 |
17 | // Tests
18 | // -----
19 |
20 | test('showing and hiding the dropdown', async () => {
21 | const counts = {Foo: 0, Bar: 0};
22 | const {vm} = testUtils.mount(EllipsisDropdown, {
23 | propsData: {
24 | items: {
25 | Foo() {
26 | counts.Foo++;
27 | },
28 | Bar: null,
29 | },
30 | },
31 | });
32 |
33 | assert.equal(vm.hidden, true);
34 |
35 | const button = findEl(vm, 'button');
36 | button.click();
37 |
38 | assert.equal(vm.hidden, false); // hidden is false after clicking button
39 | await Vue.nextTick();
40 |
41 | const links = Array.from(vm.$el.querySelectorAll('li > a'));
42 |
43 | const labels = links.map(el => el.textContent);
44 | assert.equal(labels, ['Foo', 'Bar'], 'labels are correct');
45 |
46 | links[0].click();
47 |
48 | await Vue.nextTick();
49 |
50 | assert.equal(counts.Foo, 1); // Foo callback ran
51 | assert.equal(counts.Bar, 0);
52 | assert.equal(vm.hidden, true); // click caused menu to hide
53 |
54 | button.click();
55 | links[0].click();
56 | assert.equal(counts.Foo, 2); // Foo callback ran again
57 | assert.equal(vm.hidden, true); // menu is hidden again
58 |
59 | const disabledItem = findEl(vm, '.disabled');
60 | assert.is(disabledItem.textContent, links[1].textContent); // Bar item is disabled
61 |
62 | button.click();
63 | links[1].click();
64 |
65 | assert.equal(vm.hidden, true); // hidden is false after clicking button
66 | });
67 |
68 | test.run();
69 |
--------------------------------------------------------------------------------
/test/test-ohmMode.js:
--------------------------------------------------------------------------------
1 | import * as ohm from 'ohm-js';
2 | import {test} from 'uvu';
3 | import * as assert from 'uvu/assert';
4 |
5 | import {createModeFactory} from '../src/ohmMode.js';
6 |
7 | class FakeStream {
8 | constructor(str, nextLine) {
9 | this.string = str;
10 | this.pos = 0;
11 | this.nextLine = nextLine;
12 | }
13 |
14 | next() {
15 | if (this.pos < this.string.length) {
16 | return this.string.charAt(this.pos++);
17 | }
18 | }
19 |
20 | skipToEnd() {
21 | this.pos = this.string.length;
22 | }
23 |
24 | match(str) {
25 | if (this.string.slice(this.pos).startsWith(str)) {
26 | this.pos += str.length;
27 | return str;
28 | }
29 | }
30 |
31 | eol() {
32 | return this.pos >= this.string.length;
33 | }
34 |
35 | lookAhead(n) {
36 | assert.is(n, 1);
37 | return this.nextLine;
38 | }
39 | }
40 |
41 | function tokenizeLine(tokenFn, stream, state) {
42 | const tokenTypes = [];
43 | while (!stream.eol()) {
44 | tokenTypes.push(tokenFn(stream, state));
45 | }
46 | return tokenTypes;
47 | }
48 |
49 | function simpleTokenize(str) {
50 | const {token, startState} = createModeFactory(ohm)();
51 | const stream = new FakeStream(str);
52 | return tokenizeLine(token, stream, startState());
53 | }
54 |
55 | test('basic token types', () => {
56 | // As defined in ohm-grammar.ohm:
57 | // token = caseName | comment | ident | operator | punctuation | terminal | any
58 | const tokens = simpleTokenize('G{r(desc)/**/=r2"a".."z"+--caseName}//');
59 | assert.equal(tokens, [
60 | 'grammarDef',
61 | 'punctuation', // {
62 | 'ruleDef',
63 | 'meta', // (desc)
64 | 'comment',
65 | 'operator', // =
66 | 'variable', // r2
67 | 'punctuation', // <
68 | 'variable', // r3
69 | 'punctuation', // >
70 | 'string', // "a"
71 | 'operator', // ..
72 | 'string', // "z"
73 | 'operator', // "+"
74 | 'caseName',
75 | 'punctuation', // }
76 | 'comment',
77 | ]);
78 | });
79 |
80 | test('single-line rule defintions', () => {
81 | let tokens = simpleTokenize('r=a');
82 | assert.equal(tokens, [
83 | 'ruleDef',
84 | 'punctuation',
85 | 'variable',
86 | 'punctuation',
87 | 'operator',
88 | 'variable',
89 | ]);
90 |
91 | tokens = simpleTokenize('myRule(a desc)=a');
92 | assert.equal(tokens, [
93 | 'ruleDef',
94 | 'punctuation',
95 | 'variable',
96 | 'punctuation',
97 | 'variable',
98 | 'punctuation',
99 | 'meta',
100 | 'operator',
101 | 'variable',
102 | ]);
103 | });
104 |
105 | test('multi-line comments', () => {
106 | const {token, startState} = createModeFactory(ohm)();
107 | const state = startState();
108 |
109 | let tokens = tokenizeLine(token, new FakeStream('""/*'), state);
110 | assert.equal(tokens, ['string', 'comment']);
111 | assert.is(state.insideComment, true);
112 |
113 | tokens = tokenizeLine(token, new FakeStream('blah'), state);
114 | assert.equal(tokens, ['comment']);
115 |
116 | tokens = tokenizeLine(token, new FakeStream('*/""'), state);
117 | assert.equal(tokens, ['comment', 'string']);
118 | });
119 |
120 | test('open strings', () => {
121 | const tokens = simpleTokenize('x="hell');
122 | assert.equal(tokens, ['ruleDef', 'operator', 'string']);
123 | });
124 |
125 | test('rule descriptions', () => {
126 | const tokens = simpleTokenize('x=(y)+');
127 | assert.equal(tokens, [
128 | 'ruleDef',
129 | 'operator',
130 | 'punctuation',
131 | 'variable',
132 | 'punctuation',
133 | 'operator',
134 | ]);
135 | });
136 |
137 | test('multi-line rule definitions', () => {
138 | const {token, startState} = createModeFactory(ohm)();
139 | const state = startState();
140 |
141 | let tokens = tokenizeLine(token, new FakeStream('myRule', '="x"'), state);
142 | assert.equal(tokens, ['ruleDef']);
143 |
144 | tokens = tokenizeLine(
145 | token,
146 | new FakeStream('myRule(my desc)', '="x"'),
147 | state
148 | );
149 | assert.equal(tokens, ['ruleDef', 'meta']);
150 | });
151 |
152 | test('grammar name', () => {
153 | const tokens = simpleTokenize('MyGrammr {');
154 | assert.equal(tokens, ['grammarDef', null, 'punctuation']);
155 | });
156 |
157 | test('multi-line rule definition, but no next line', () => {
158 | const tokens = simpleTokenize('G');
159 | assert.equal(tokens, ['variable']);
160 | });
161 |
162 | test('whitespace crossing a line break', () => {
163 | const {token, startState} = createModeFactory(ohm)();
164 | const state = startState();
165 |
166 | // Make sure that the trailing whitespace on the first line and the leading
167 | // whitespace on the second line don't get merged into a single token.
168 | const tokens = tokenizeLine(
169 | token,
170 | new FakeStream('x (an x) ', ' = "x"'),
171 | state
172 | );
173 | assert.equal(tokens, ['ruleDef', null, 'meta', null]);
174 | });
175 |
176 | test.run();
177 |
--------------------------------------------------------------------------------
/webpack.config.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require('path');
4 | const {VueLoaderPlugin} = require('vue-loader');
5 |
6 | module.exports = {
7 | module: {
8 | rules: [
9 | {test: /\.vue$/, loader: 'vue-loader'},
10 | {
11 | test: /\.css$/,
12 | use: ['vue-style-loader', 'css-loader'],
13 | },
14 | ],
15 | },
16 | entry: {
17 | visualizer: './src/index.js',
18 | persistence: './src/persistence.js',
19 | },
20 | resolve: {
21 | // Use the standalone version of Vue that includes the template compiler.
22 | alias: {vue$: 'vue/dist/vue.esm.js'}, // eslint-disable-line quote-props
23 | fallback: {fs: false},
24 | },
25 | output: {
26 | path: path.join(__dirname, 'build'),
27 | clean: true,
28 | filename: '[name]-bundle.js',
29 | publicPath: '/assets/',
30 | },
31 | plugins: [new VueLoaderPlugin()],
32 | devServer: {
33 | static: {
34 | directory: path.join(__dirname, 'public'),
35 | },
36 | port: 8080,
37 | },
38 | };
39 |
--------------------------------------------------------------------------------