├── .editorconfig ├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .prettierignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── example.png ├── packages └── prong-editor │ ├── .eslintrc.cjs │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── components │ │ ├── Editor.tsx │ │ └── ErrorBoundary.tsx │ ├── index.ts │ ├── lib │ │ ├── ComputeMenuContents.test.ts │ │ ├── ComputeMenuContentsIntegration.test.ts │ │ ├── JSONSchemaTypes.ts │ │ ├── Linter.ts │ │ ├── ModifyCode.test.ts │ │ ├── Query.test.ts │ │ ├── Search.test.ts │ │ ├── Utils.test.ts │ │ ├── __snapshots__ │ │ │ ├── ComputeMenuContents.test.ts.snap │ │ │ ├── ModifyCode.test.ts.snap │ │ │ ├── Search.test.ts.snap │ │ │ └── Utils.test.ts.snap │ │ ├── cmState.ts │ │ ├── compute-menu-contents.ts │ │ ├── dock.tsx │ │ ├── local-utils.tsx │ │ ├── menu-content │ │ │ ├── schema-based.ts │ │ │ └── type-based.ts │ │ ├── modify-json.ts │ │ ├── popover-menu.tsx │ │ ├── popover-menu │ │ │ ├── KeyboardControls.ts │ │ │ ├── PopoverMenu.tsx │ │ │ ├── PopoverMenuElement.tsx │ │ │ └── PopoverState.ts │ │ ├── projections.ts │ │ ├── query.ts │ │ ├── search.ts │ │ ├── syntax-highlighting.ts │ │ ├── test-utils.ts │ │ ├── utils.ts │ │ ├── vendored │ │ │ ├── parser.ts │ │ │ ├── prettifier.ts │ │ │ ├── resolve-schema.ts │ │ │ ├── utils.ts │ │ │ └── validator.ts │ │ ├── widgets.ts │ │ └── widgets │ │ │ ├── highlighter.ts │ │ │ └── inline-projection-widget.tsx │ ├── projections │ │ ├── Boolean.tsx │ │ ├── CleanUp.tsx │ │ ├── ClickTarget.tsx │ │ ├── ColorChip.tsx │ │ ├── ColorNamePicker.tsx │ │ ├── ColorPicker.tsx │ │ ├── Debugger.tsx │ │ ├── HeuristicJSONFixes.tsx │ │ ├── NumberSlider.tsx │ │ ├── SortObject.tsx │ │ └── standard-bundle.ts │ ├── setupTests.ts │ ├── stylesheets │ │ └── style.css │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── yarn.lock ├── sites └── docs │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ ├── example.png │ ├── favicon.ico │ ├── index.html │ ├── logo.png │ ├── logo192.png │ ├── logo512.png │ └── manifest.json │ ├── scripts │ └── do-build.sh │ ├── src │ ├── App.css │ ├── App.tsx │ ├── constants │ │ ├── tracery-schema.json │ │ ├── vega-lite-v5-schema.json │ │ └── vega-schema.json │ ├── examples │ │ ├── DataTable.tsx │ │ ├── InSituVis.tsx │ │ ├── ProduceExample.tsx │ │ ├── QuietModeCodeMirror.tsx │ │ ├── QuietModeCompare.tsx │ │ ├── QuietModeUs.tsx │ │ ├── RandomWord.tsx │ │ ├── SimpleExample.tsx │ │ ├── StyledMarkdown.tsx │ │ ├── TraceryExample.tsx │ │ ├── VegaExpressionEditor.tsx │ │ ├── VegaLiteDebug.tsx │ │ ├── VegaLiteStyler.tsx │ │ ├── VegaLiteUseCase.tsx │ │ ├── VegaUseCase.tsx │ │ ├── example-data.ts │ │ ├── example-utils.tsx │ │ ├── histograms.tsx │ │ └── tracery.ts │ ├── main.tsx │ ├── stylesheets │ │ ├── quiet-styles.css │ │ ├── tracery-example.css │ │ ├── vega-example.css │ │ └── vega-lite-example.css │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── yarn.lock └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | tab_width 1 -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test and lint 2 | on: [push] 3 | jobs: 4 | test-and-lint-package: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 🛎️ 8 | uses: actions/checkout@v2.3.1 9 | 10 | - name: Install 🔧 11 | run: yarn 12 | working-directory: ./packages/prong-editor 13 | 14 | - name: Test 🔬 15 | run: yarn test 16 | working-directory: ./packages/prong-editor 17 | 18 | - name: Lint 🧵 19 | run: yarn lint 20 | working-directory: ./packages/prong-editor 21 | lint-docs: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 🛎️ 25 | uses: actions/checkout@v2.3.1 26 | 27 | - name: Install 🔧 28 | run: yarn 29 | working-directory: ./sites/docs 30 | 31 | - name: Lint 🧵 32 | run: yarn lint 33 | working-directory: ./sites/docs 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | public/data/* 26 | public/data/ 27 | *.es.js 28 | 29 | sites/docs/public/README.md 30 | 31 | # Logs 32 | logs 33 | *.log 34 | npm-debug.log* 35 | yarn-debug.log* 36 | yarn-error.log* 37 | pnpm-debug.log* 38 | lerna-debug.log* 39 | 40 | node_modules 41 | dist 42 | dist-ssr 43 | *.local 44 | 45 | # Editor directories and files 46 | .vscode/* 47 | !.vscode/extensions.json 48 | .idea 49 | .DS_Store 50 | *.suo 51 | *.ntvs* 52 | *.njsproj 53 | *.sln 54 | *.sw? 55 | 56 | 57 | sites/docs/public/QuietModeCodeMirror.tsx 58 | sites/docs/public/QuietModeCompare.tsx 59 | sites/docs/public/QuietModeUs.tsx 60 | sites/docs/public/data/* 61 | 62 | packages/prong-editor/README.md -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/prong/f9731444079f6477d620a13a11b1a40b652dbcfe/.prettierignore -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "bandspace", 4 | "codemirror", 5 | "Consts", 6 | "dayofyear", 7 | "dedup", 8 | "dont", 9 | "Dragonfruit", 10 | "Dups", 11 | "estree", 12 | "fruitie", 13 | "fyshuffle", 14 | "indata", 15 | "inrange", 16 | "isequal", 17 | "keymap", 18 | "lezer", 19 | "linebreak", 20 | "markclip", 21 | "nodenext", 22 | "pickr", 23 | "postactions", 24 | "preactions", 25 | "Projectional", 26 | "retarget", 27 | "Retargeted", 28 | "ruleset", 29 | "rulesets", 30 | "seedrandom", 31 | "simonwep", 32 | "subgrammars", 33 | "Symlog", 34 | "tabindex", 35 | "targ" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Andrew McNutt 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 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/prong/f9731444079f6477d620a13a11b1a40b652dbcfe/example.png -------------------------------------------------------------------------------- /packages/prong-editor/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | root: true, 5 | env: { browser: true, es2020: true }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "plugin:react-hooks/recommended", 11 | ], 12 | parser: "@typescript-eslint/parser", 13 | parserOptions: { 14 | ecmaVersion: "latest", 15 | sourceType: "module", 16 | project: true, 17 | tsconfigRootDir: __dirname, 18 | }, 19 | plugins: ["react-refresh"], 20 | rules: { 21 | "react-refresh/only-export-components": [ 22 | "warn", 23 | { allowConstantExport: true }, 24 | ], 25 | "@typescript-eslint/no-redundant-type-constituents": 0, 26 | "@typescript-eslint/no-non-null-assertion": "off", 27 | "@typescript-eslint/no-unsafe-call": "off", 28 | "@typescript-eslint/ban-ts-comment": 0, 29 | "@typescript-eslint/no-explicit-any": 0, 30 | "@typescript-eslint/no-unsafe-argument": 0, 31 | "@typescript-eslint/no-unsafe-assignment": 0, 32 | "@typescript-eslint/no-unsafe-member-access": 0, 33 | "@typescript-eslint/no-unsafe-return": 0, 34 | "@typescript-eslint/no-non-null-asserted-optional-chain": 0, 35 | "react-refresh/only-export-components": 0, 36 | "no-unused-vars": "off", 37 | "@typescript-eslint/no-empty-function": 0, 38 | "@typescript-eslint/no-unused-vars": [ 39 | "warn", // or "error" 40 | { 41 | argsIgnorePattern: "^_", 42 | varsIgnorePattern: "^_", 43 | caughtErrorsIgnorePattern: "^_", 44 | }, 45 | ], 46 | "@typescript-eslint/restrict-template-expressions": 0, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /packages/prong-editor/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /packages/prong-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prong-editor", 3 | "version": "0.0.10", 4 | "scripts": { 5 | "dev": " vite build --watch --config vite.config.ts", 6 | "build": "tsc && vite build && npm run cp-docs", 7 | "cp-docs": "cp ../../README.md ./", 8 | "preview": "vite preview", 9 | "test": "jest", 10 | "lint": "eslint src/" 11 | }, 12 | "dependencies": { 13 | "@codemirror/commands": "^6.1.2", 14 | "@codemirror/lang-json": "^6.0.1", 15 | "@codemirror/lint": "^6.1.0", 16 | "@codemirror/view": "^6.7.0", 17 | "@json-schema-tools/traverse": "^1.10.1", 18 | "@lezer/common": "^1.0.2", 19 | "@lezer/json": "^1.0.0", 20 | "codemirror": "^6.0.1", 21 | "d3-color": "^3.1.0", 22 | "jsonc-parser": "^3.0.0", 23 | "lodash.isequal": "^4.5.0", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2", 26 | "react-markdown": "^7.1.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.16.12", 30 | "@jest/globals": "^29.6.1", 31 | "@testing-library/jest-dom": "^5.11.4", 32 | "@testing-library/react": "^11.1.0", 33 | "@testing-library/user-event": "^12.1.10", 34 | "@types/d3": "^7.4.0", 35 | "@types/jest": "^29.5.2", 36 | "@types/json-schema": "^7.0.11", 37 | "@types/lodash.isequal": "^4.5.5", 38 | "@types/node": "^20.4.0", 39 | "@types/react": "^17.0.38", 40 | "@types/react-dom": "^17.0.11", 41 | "@typescript-eslint/eslint-plugin": "^6.0.0", 42 | "@typescript-eslint/parser": "^6.0.0", 43 | "@vitejs/plugin-react": "^1.1.4", 44 | "acorn-jsx": "^5.3.2", 45 | "babel-loader": "^8.2.3", 46 | "eslint": "^8.44.0", 47 | "eslint-plugin-react-hooks": "^4.6.0", 48 | "eslint-plugin-react-refresh": "^0.4.3", 49 | "jest": "^29.6.1", 50 | "ts-jest": "^29.1.1", 51 | "typescript": "^4.5.5", 52 | "vite": "^2.7.13", 53 | "vite-jest": "^0.1.4", 54 | "vite-plugin-dts": "^0.9.9" 55 | }, 56 | "license": "MIT", 57 | "peerDependencies": { 58 | "react": "^16.8.0 || 17.x", 59 | "react-dom": "^16.8.0 || 17.x" 60 | }, 61 | "keywords": [ 62 | "JSON", 63 | "DSL", 64 | "JSONSchema", 65 | "Structure-Editor", 66 | "React", 67 | "codemirror" 68 | ], 69 | "homepage": "https://github.com/mcnuttandrew/prong", 70 | "files": [ 71 | "dist" 72 | ], 73 | "types": "./dist/index.d.ts", 74 | "exports": { 75 | ".": { 76 | "types": "./dist/index.d.ts", 77 | "import": "./dist/prong.es.js", 78 | "require": "./dist/prong.umd.js" 79 | }, 80 | "./style.css": { 81 | "import": "./dist/style.css", 82 | "require": "./dist/style.css" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/prong-editor/src/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | import { json } from "@codemirror/lang-json"; 4 | import { Compartment } from "@codemirror/state"; 5 | import { basicSetup } from "codemirror"; 6 | import { EditorView, ViewUpdate, keymap } from "@codemirror/view"; 7 | import { indentWithTab } from "@codemirror/commands"; 8 | import { EditorState } from "@codemirror/state"; 9 | import { syntaxHighlighting } from "@codemirror/language"; 10 | 11 | import { widgetsPlugin } from "../lib/widgets"; 12 | import SyntaxHighlighting from "../lib/syntax-highlighting"; 13 | import { Projection } from "../lib/projections"; 14 | import { 15 | cmStatePlugin, 16 | setSchema, 17 | setProjections, 18 | setUpdateHook, 19 | cmStateView, 20 | } from "../lib/cmState"; 21 | import PopoverPlugin from "../lib/popover-menu"; 22 | import ProjectionPlugin from "../lib/projections"; 23 | import Panel from "../lib/dock"; 24 | import { popOverState } from "../lib/popover-menu/PopoverState"; 25 | import { syntaxNodeToKeyPath } from "../lib/utils"; 26 | 27 | const languageConf = new Compartment(); 28 | export type SchemaMap = Record; 29 | 30 | export default function Editor(props: { 31 | onChange: (code: string) => void; 32 | code: string; 33 | schema: any; // TODO fix 34 | projections?: Projection[]; 35 | height?: string; 36 | onTargetNodeChanged?: (newNode: any, oldNode: any) => void; 37 | }) { 38 | const { schema, code, onChange, projections, height, onTargetNodeChanged } = 39 | props; 40 | 41 | const [view, setView] = useState(null); 42 | const cmParent = useRef(null); 43 | 44 | // primary effect, initialize the editor etc 45 | useEffect(() => { 46 | const localExtension = EditorView.updateListener.of((v: ViewUpdate) => { 47 | if (v.docChanged) { 48 | const newCode = v.state.doc.toString(); 49 | onChange(newCode); 50 | } 51 | if (onTargetNodeChanged) { 52 | const codeHere = v.state.doc.toString(); 53 | const oldNode = v.startState.field(popOverState).targetNode; 54 | const newNode = v.state.field(popOverState).targetNode; 55 | const nodeIsActuallyNew = !( 56 | oldNode?.from === newNode?.from && oldNode?.to === newNode?.to 57 | ); 58 | if (nodeIsActuallyNew) { 59 | onTargetNodeChanged( 60 | newNode ? syntaxNodeToKeyPath(newNode.node, codeHere) : newNode, 61 | oldNode ? syntaxNodeToKeyPath(oldNode.node, codeHere) : false 62 | ); 63 | } 64 | } 65 | }); 66 | const editorState = EditorState.create({ 67 | extensions: [ 68 | PopoverPlugin(), 69 | ProjectionPlugin(), 70 | basicSetup, 71 | languageConf.of(json()), 72 | keymap.of([indentWithTab]), 73 | Panel(), 74 | cmStatePlugin, 75 | cmStateView, 76 | widgetsPlugin, 77 | localExtension, 78 | syntaxHighlighting(SyntaxHighlighting), 79 | ], 80 | doc: code, 81 | })!; 82 | const view = new EditorView({ 83 | state: editorState, 84 | parent: cmParent.current!, 85 | }); 86 | setView(view); 87 | return () => { 88 | const monocle = document.querySelector("#cm-monocle"); 89 | while (monocle && monocle.firstChild) { 90 | monocle.removeChild(monocle.firstChild); 91 | } 92 | view.destroy(); 93 | }; 94 | // eslint-disable-next-line 95 | }, []); 96 | 97 | useEffect(() => { 98 | // hack :( 99 | setTimeout(() => { 100 | view?.dispatch({ effects: [setSchema.of(schema)] }); 101 | }, 300); 102 | }, [schema, view]); 103 | useEffect(() => { 104 | // hack :( 105 | setTimeout(() => { 106 | view?.dispatch({ effects: [setProjections.of(projections || [])] }); 107 | }, 100); 108 | }, [projections, view]); 109 | 110 | useEffect(() => { 111 | if (view && view.state.doc.toString() !== code) { 112 | // hack :( 113 | setTimeout(() => { 114 | view.dispatch( 115 | view.state.update({ 116 | changes: { from: 0, to: view.state.doc.length, insert: code }, 117 | selection: view.state.selection, 118 | }) 119 | ); 120 | }, 300); 121 | } 122 | }, [code, view]); 123 | useEffect(() => { 124 | setTimeout(() => { 125 | view?.dispatch({ 126 | effects: [setUpdateHook.of([(code: string) => onChange(code)])], 127 | }); 128 | }); 129 | }, [view, onChange]); 130 | return ( 131 |
132 |
133 |
134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /packages/prong-editor/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | 3 | type Props = Record; 4 | interface State { 5 | hasError: boolean; 6 | } 7 | 8 | export default class ErrorBoundary extends Component { 9 | constructor(props: any) { 10 | super(props); 11 | this.state = { hasError: false }; 12 | } 13 | 14 | static getDerivedStateFromError() { 15 | // Update state so the next render will show the fallback UI. 16 | return { hasError: true }; 17 | } 18 | 19 | componentDidCatch(error: any, _errorInfo: any) { 20 | console.error(error); 21 | // You can also log the error to an error reporting service 22 | // logErrorToMyService(error, errorInfo); 23 | } 24 | 25 | render() { 26 | if (this.state.hasError) { 27 | // You can render any custom fallback UI 28 | return
; 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/prong-editor/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Editor } from "./components/Editor"; 2 | export { default as StandardBundle } from "./projections/standard-bundle"; 3 | import { simpleParse, setIn, maybeTrim } from "./lib/utils"; 4 | import prettifier from "./lib/vendored/prettifier"; 5 | export const utils = { simpleParse, setIn, maybeTrim, prettifier }; 6 | 7 | export { type Projection, type ProjectionProps } from "./lib/projections"; 8 | 9 | import "./stylesheets/style.css"; 10 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/ComputeMenuContents.test.ts: -------------------------------------------------------------------------------- 1 | import { generateMenuContent } from "./compute-menu-contents"; 2 | import { findNodeByText } from "./test-utils"; 3 | import { createNodeMap } from "./utils"; 4 | import { vegaCode } from "../../../../sites/docs/src/examples/example-data"; 5 | import { materializeAnyOfOption } from "./menu-content/schema-based"; 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const VegaSchema = require("../../../../sites/docs/src/constants/vega-schema.json"); 8 | 9 | const schema = { 10 | $id: "https://example.com/arrays.schema.json", 11 | $schema: "https://json-schema.org/draft/2020-12/schema", 12 | description: "A representation of a person, company, organization, or place", 13 | type: "object", 14 | properties: { 15 | fruits: { 16 | type: "array", 17 | items: { 18 | type: "string", 19 | }, 20 | }, 21 | vegetables: { 22 | type: "array", 23 | items: { $ref: "#/$defs/veggie" }, 24 | }, 25 | }, 26 | $defs: { 27 | veggie: { 28 | type: "object", 29 | required: ["veggieName", "veggieLike"], 30 | properties: { 31 | veggieName: { 32 | type: "string", 33 | description: "The name of the vegetable.", 34 | }, 35 | veggieLike: { 36 | type: "boolean", 37 | description: "Do I like this vegetable?", 38 | }, 39 | }, 40 | }, 41 | }, 42 | }; 43 | 44 | const exampleData = `{ 45 | "fruits": [ "apple", "orange", "pear" ], 46 | "vegetables": [ 47 | { 48 | "veggieName": "potato", 49 | "veggieLike": true 50 | }, 51 | { 52 | "veggieName": "broccoli", 53 | "veggieLike": false 54 | } 55 | ] 56 | }`; 57 | 58 | test("generateMenuContent - fruit", async () => { 59 | const veg = findNodeByText(exampleData, `"vegetables"`)!; 60 | const fAndVNodeMap = await createNodeMap(schema, exampleData); 61 | expect(fAndVNodeMap).toMatchSnapshot(); 62 | const menuContent = generateMenuContent(veg, fAndVNodeMap, exampleData); 63 | expect(menuContent).toMatchSnapshot(); 64 | }); 65 | 66 | test("generateMenuContent - vega", async () => { 67 | const target = findNodeByText(vegaCode, `"transform"`)!.parent?.lastChild 68 | ?.firstChild?.nextSibling!; 69 | const nodeMap = await createNodeMap(VegaSchema, vegaCode); 70 | // expect(nodeMap).toMatchSnapshot(); 71 | const menuContent = generateMenuContent(target, nodeMap, vegaCode); 72 | expect(menuContent).toMatchSnapshot(); 73 | }); 74 | 75 | test("materializeAnyOfOption", () => { 76 | const literalSchema = { type: "string", $$labeledType: "role" }; 77 | expect(materializeAnyOfOption(literalSchema as any)).toBe('""'); 78 | 79 | const simpleOneOf = { 80 | $$labeledType: "style", 81 | $$refName: "#/definitions/style", 82 | oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }], 83 | }; 84 | expect(materializeAnyOfOption(simpleOneOf as any)).toBe('""'); 85 | 86 | const simpleAnyOf = { 87 | $$labeledType: "style", 88 | $$refName: "#/definitions/style", 89 | anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }], 90 | }; 91 | expect(materializeAnyOfOption(simpleAnyOf as any)).toBe('""'); 92 | 93 | const clip = { 94 | $$labeledType: "clip", 95 | $$refName: "#/definitions/markclip", 96 | oneOf: [ 97 | { 98 | $$refName: "#/definitions/booleanOrSignal", 99 | oneOf: [ 100 | { type: "boolean" }, 101 | { 102 | $$refName: "#/definitions/signalRef", 103 | type: "object", 104 | properties: { 105 | signal: { type: "string", $$labeledType: "signal" }, 106 | }, 107 | required: ["signal"], 108 | $$labeledType: "signalRef", 109 | }, 110 | ], 111 | $$labeledType: "booleanOrSignal", 112 | }, 113 | { 114 | type: "object", 115 | properties: { 116 | path: { 117 | $$labeledType: "path", 118 | $$refName: "#/definitions/stringOrSignal", 119 | oneOf: [ 120 | { type: "string" }, 121 | { 122 | $$refName: "#/definitions/signalRef", 123 | type: "object", 124 | properties: { 125 | signal: { type: "string", $$labeledType: "signal" }, 126 | }, 127 | required: ["signal"], 128 | $$labeledType: "signalRef", 129 | }, 130 | ], 131 | }, 132 | }, 133 | required: ["path"], 134 | additionalProperties: false, 135 | }, 136 | { 137 | type: "object", 138 | properties: { 139 | sphere: { 140 | $$labeledType: "sphere", 141 | $$refName: "#/definitions/stringOrSignal", 142 | oneOf: [ 143 | { type: "string" }, 144 | { 145 | $$refName: "#/definitions/signalRef", 146 | type: "object", 147 | properties: { 148 | signal: { type: "string", $$labeledType: "signal" }, 149 | }, 150 | required: ["signal"], 151 | $$labeledType: "signalRef", 152 | }, 153 | ], 154 | }, 155 | }, 156 | required: ["sphere"], 157 | additionalProperties: false, 158 | }, 159 | ], 160 | }; 161 | expect(materializeAnyOfOption(clip as any)).toBe("true"); 162 | }); 163 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/ComputeMenuContentsIntegration.test.ts: -------------------------------------------------------------------------------- 1 | import { generateMenuContent } from "./compute-menu-contents"; 2 | import { createNodeMap } from "./utils"; 3 | import { parser } from "@lezer/json"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | const VegaSchema = require("../../../../sites/docs/src/constants/vega-schema.json"); 7 | 8 | const vegaCode = `{ 9 | "marks": [ 10 | { 11 | "type": "text", 12 | "from": {"data": "drive"}, 13 | "encode": { 14 | "enter": { 15 | "x": {"scale": "x", "field": "miles"}, 16 | "y": {"scale": "y", "field": "gas"}, 17 | "dx": {"scale": "dx", "field": "side"}, 18 | "dy": {"scale": "dy", "field": "side"}, 19 | "fill": {"value": "#000"}, 20 | "text": {"field": "year"}, 21 | "align": {"scale": "align", "field": "side"}, 22 | "baseline": {"scale": "base", "field": "side"} 23 | } 24 | } 25 | } 26 | ], 27 | "$schema": "https://vega.github.io/schema/vega/v3.0.json", 28 | "width": 800, 29 | "height": 500, 30 | "padding": 5, 31 | 32 | "data": [{ "name": "drive", "url": "data/driving.json"}], 33 | "scales": [ 34 | { 35 | "name": "x", 36 | "type": "linear", 37 | "domain": {"data": "drive", "field": "miles"}, 38 | "range": "width", 39 | "nice": true, 40 | "zero": false, 41 | "round": true 42 | }, 43 | { 44 | "name": "y", 45 | "type": "linear", 46 | "domain": {"data": "drive", "field": "gas"}, 47 | "range": "height", 48 | "nice": true, 49 | "zero": false, 50 | "round": true 51 | }, 52 | { 53 | "name": "align", 54 | "type": "ordinal", 55 | "domain": ["left", "right", "top", "bottom"], 56 | "range": ["right", "left", "center", "center"] 57 | }, 58 | { 59 | "name": "base", 60 | "type": "ordinal", 61 | "domain": ["left", "right", "top", "bottom"], 62 | "range": ["middle", "middle", "bottom", "top"] 63 | }, 64 | { 65 | "name": "dx", 66 | "type": "ordinal", 67 | "domain": ["left", "right", "top", "bottom"], 68 | "range": [-7, 6, 0, 0] 69 | }, 70 | { 71 | "name": "dy", 72 | "type": "ordinal", 73 | "domain": ["left", "right", "top", "bottom"], 74 | "range": [1, 1, -5, 6] 75 | } 76 | ] 77 | }`; 78 | 79 | test("generateMenuContent - smoke test", async () => { 80 | const nodeMap = await createNodeMap(VegaSchema, vegaCode); 81 | const skipTypes = new Set(["PropertyName"]); 82 | parser.parse(vegaCode).iterate({ 83 | enter: (node) => { 84 | if (skipTypes.has(node.type.name)) { 85 | return; 86 | } 87 | expect(() => 88 | generateMenuContent(node.node, nodeMap, vegaCode) 89 | ).not.toThrow(); 90 | }, 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/JSONSchemaTypes.ts: -------------------------------------------------------------------------------- 1 | export type JSONSchemaRef = JSONSchema | boolean; 2 | 3 | export interface JSONSchemaMap { 4 | [name: string]: JSONSchemaRef; 5 | } 6 | 7 | export interface JSONSchema { 8 | id?: string; 9 | $id?: string; 10 | $schema?: string; 11 | type?: string | string[]; 12 | title?: string; 13 | default?: any; 14 | definitions?: { [name: string]: JSONSchema }; 15 | description?: string; 16 | properties?: JSONSchemaMap; 17 | patternProperties?: JSONSchemaMap; 18 | additionalProperties?: boolean | JSONSchemaRef; 19 | minProperties?: number; 20 | maxProperties?: number; 21 | dependencies?: JSONSchemaMap | { [prop: string]: string[] }; 22 | items?: JSONSchemaRef | JSONSchemaRef[]; 23 | minItems?: number; 24 | maxItems?: number; 25 | uniqueItems?: boolean; 26 | additionalItems?: boolean | JSONSchemaRef; 27 | pattern?: string; 28 | minLength?: number; 29 | maxLength?: number; 30 | minimum?: number; 31 | maximum?: number; 32 | exclusiveMinimum?: boolean | number; 33 | exclusiveMaximum?: boolean | number; 34 | multipleOf?: number; 35 | required?: string[]; 36 | $ref?: string; 37 | anyOf?: JSONSchemaRef[]; 38 | allOf?: JSONSchemaRef[]; 39 | oneOf?: JSONSchemaRef[]; 40 | not?: JSONSchemaRef; 41 | enum?: any[]; 42 | format?: string; 43 | 44 | // schema draft 06 45 | const?: any; 46 | contains?: JSONSchemaRef; 47 | propertyNames?: JSONSchemaRef; 48 | examples?: any[]; 49 | 50 | // schema draft 07 51 | $comment?: string; 52 | if?: JSONSchemaRef; 53 | then?: JSONSchemaRef; 54 | else?: JSONSchemaRef; 55 | 56 | // VSCode extensions 57 | 58 | defaultSnippets?: { 59 | label?: string; 60 | description?: string; 61 | markdownDescription?: string; 62 | body?: any; 63 | bodyText?: string; 64 | }[]; // VSCode extension: body: a object that will be converted to a JSON string. bodyText: text with \t and \n 65 | errorMessage?: string; // VSCode extension 66 | patternErrorMessage?: string; // VSCode extension 67 | deprecationMessage?: string; // VSCode extension 68 | enumDescriptions?: string[]; // VSCode extension 69 | markdownEnumDescriptions?: string[]; // VSCode extension 70 | markdownDescription?: string; // VSCode extension 71 | doNotSuggest?: boolean; // VSCode extension 72 | suggestSortText?: string; // VSCode extension 73 | allowComments?: boolean; // VSCode extension 74 | allowTrailingCommas?: boolean; // VSCode extension 75 | 76 | // local extensions 77 | $$labeledType?: string; 78 | $$refName?: string; 79 | } 80 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/Linter.ts: -------------------------------------------------------------------------------- 1 | import { linter } from "@codemirror/lint"; 2 | import { produceLintValidations } from "./vendored/validator"; 3 | import { codeString } from "./utils"; 4 | import { cmStatePlugin } from "./cmState"; 5 | import { parser } from "@lezer/json"; 6 | 7 | export interface Diagonstic { 8 | location: { 9 | offset: number; 10 | length: number; 11 | }; 12 | message: string; 13 | code: keyof typeof errorCodeToErrorType; 14 | source?: string; 15 | expected?: string[]; 16 | } 17 | 18 | function validateCode(code: string): Promise { 19 | const errors: Diagonstic[] = []; 20 | parser.parse(code).iterate({ 21 | enter: (node) => { 22 | if (node.node.type.name === "⚠") { 23 | errors.push({ 24 | location: { offset: node.from, length: node.to - node.from }, 25 | message: "Parse Error", 26 | code: 1, 27 | source: "parser", 28 | }); 29 | } 30 | }, 31 | }); 32 | return Promise.resolve(errors); 33 | } 34 | 35 | const errorCodeToErrorType = { 36 | 1: "error", 37 | 2: "warning", 38 | 3: "info", 39 | 4: "info", 40 | }; 41 | 42 | export const jsonLinter = linter((source) => { 43 | return lintCode( 44 | source.state.field(cmStatePlugin).schema, 45 | codeString(source, 0) 46 | ); 47 | }); 48 | 49 | export const lintCode = (schema: any, code: string): Promise => { 50 | return Promise.all([ 51 | produceLintValidations(schema, code), 52 | validateCode(code), 53 | ]).then(([x, parsedValidations]) => { 54 | const problems = [...x.problems, ...parsedValidations] as Diagonstic[]; 55 | return problems.map((problem) => { 56 | const { location, message, code, source, expected } = problem; 57 | return { 58 | from: location.offset, 59 | to: location.offset + location.length, 60 | severity: code ? errorCodeToErrorType[code] : "info", 61 | source: source || "Schema Validation", 62 | message, 63 | expected, 64 | } as LintError; 65 | }); 66 | }); 67 | }; 68 | 69 | export interface LintError { 70 | from: number; 71 | to: number; 72 | severity: "error" | "warning" | "info"; 73 | source: string; 74 | message: string; 75 | expected?: string[]; 76 | } 77 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/Query.test.ts: -------------------------------------------------------------------------------- 1 | import { keyPathMatchesQuery } from "./query"; 2 | 3 | test("keyPathMatchesQuery", () => { 4 | const basicQuery = ["data", "values", "*"]; 5 | expect(keyPathMatchesQuery(basicQuery, ["data", "values", 1])).toBe(true); 6 | expect(keyPathMatchesQuery(["data", "values"], ["data", "values", 1])).toBe( 7 | false 8 | ); 9 | expect( 10 | keyPathMatchesQuery(basicQuery, ["encoding", "x", "field___key"]) 11 | ).toBe(false); 12 | 13 | expect(keyPathMatchesQuery(basicQuery, [])).toBe(false); 14 | expect(keyPathMatchesQuery(basicQuery, ["$schema"])).toBe(false); 15 | expect(keyPathMatchesQuery(basicQuery, ["$schema", "$schema___key"])).toBe( 16 | false 17 | ); 18 | expect(keyPathMatchesQuery(basicQuery, ["data"])).toBe(false); 19 | expect(keyPathMatchesQuery(basicQuery, ["data", "undefined___key"])).toBe( 20 | false 21 | ); 22 | expect(keyPathMatchesQuery(basicQuery, ["data", "values"])).toBe(false); 23 | expect(keyPathMatchesQuery(basicQuery, ["data", "values", 0])).toBe(true); 24 | expect(keyPathMatchesQuery(basicQuery, ["data", "values", "0___key"])).toBe( 25 | true 26 | ); 27 | expect( 28 | keyPathMatchesQuery(basicQuery, ["data", "values", 0, "a___key"]) 29 | ).toBe(false); 30 | expect( 31 | keyPathMatchesQuery(basicQuery, ["data", "values", 0, "b___key"]) 32 | ).toBe(false); 33 | expect(keyPathMatchesQuery(basicQuery, ["data", "values", 1])).toBe(true); 34 | expect( 35 | keyPathMatchesQuery(basicQuery, ["data", "values", 1, "a___key"]) 36 | ).toBe(false); 37 | expect( 38 | keyPathMatchesQuery(basicQuery, ["data", "values", 1, "b___key"]) 39 | ).toBe(false); 40 | expect(keyPathMatchesQuery(basicQuery, ["data", "values", 2])).toBe(true); 41 | 42 | expect(keyPathMatchesQuery(basicQuery, ["description"])).toBe(false); 43 | expect( 44 | keyPathMatchesQuery(basicQuery, ["description", "description___key"]) 45 | ).toBe(false); 46 | expect(keyPathMatchesQuery(basicQuery, ["encoding"])).toBe(false); 47 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "undefined___key"])).toBe( 48 | false 49 | ); 50 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "x"])).toBe(false); 51 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "Xaxis"])).toBe(false); 52 | expect( 53 | keyPathMatchesQuery(basicQuery, ["encoding", "Xaxis", "labelAngle"]) 54 | ).toBe(false); 55 | expect( 56 | keyPathMatchesQuery(basicQuery, [ 57 | "encoding", 58 | "Xaxis", 59 | "labelAngle", 60 | "labelAngle___key", 61 | ]) 62 | ).toBe(false); 63 | expect( 64 | keyPathMatchesQuery(basicQuery, ["encoding", "Xaxis", "undefined___key"]) 65 | ).toBe(false); 66 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "Xfield"])).toBe(false); 67 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "Xfield___key"])).toBe( 68 | false 69 | ); 70 | expect( 71 | keyPathMatchesQuery(basicQuery, ["encoding", "Xfield", "field___key"]) 72 | ).toBe(false); 73 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "Xtype"])).toBe(false); 74 | expect( 75 | keyPathMatchesQuery(basicQuery, ["encoding", "Xtype", "type___key"]) 76 | ).toBe(false); 77 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "y"])).toBe(false); 78 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "y", "field"])).toBe( 79 | false 80 | ); 81 | expect( 82 | keyPathMatchesQuery(basicQuery, ["encoding", "y", "field", "field___key"]) 83 | ).toBe(false); 84 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "y", "type"])).toBe( 85 | false 86 | ); 87 | expect(keyPathMatchesQuery(basicQuery, ["encoding", "y", "type___key"])).toBe( 88 | false 89 | ); 90 | expect( 91 | keyPathMatchesQuery(basicQuery, ["encoding", "y", "type", "type___key"]) 92 | ).toBe(false); 93 | expect(keyPathMatchesQuery(basicQuery, ["mark"])).toBe(false); 94 | expect(keyPathMatchesQuery(basicQuery, ["mark", "mark___key"])).toBe(false); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/Search.test.ts: -------------------------------------------------------------------------------- 1 | import { filterContents } from "./search"; 2 | import { findNodeByText } from "./test-utils"; 3 | import { generateMenuContent } from "./compute-menu-contents"; 4 | import VegaLiteV5Schema from "../../../../sites/docs/src/constants/vega-lite-v5-schema.json"; 5 | import { createNodeMap } from "./utils"; 6 | const updatedSchema = { 7 | ...VegaLiteV5Schema, 8 | $ref: "#/definitions/Config", 9 | }; 10 | 11 | test("filterContents", async () => { 12 | const exampleString = `{ "axis": { } }`; 13 | const schemaMap = await createNodeMap(updatedSchema, exampleString); 14 | const target = findNodeByText(exampleString, `{ }`)!; 15 | const generatedContent = generateMenuContent( 16 | target, 17 | schemaMap, 18 | exampleString 19 | ); 20 | expect(filterContents("gr", generatedContent)).toMatchSnapshot(); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/Utils.test.ts: -------------------------------------------------------------------------------- 1 | import { setIn, syntaxNodeToKeyPath, syntaxNodeToAbsPath } from "./utils"; 2 | import { vegaCode } from "../../../../sites/docs/src/examples/example-data"; 3 | import { findNodeByText } from "./test-utils"; 4 | const exampleData = `{ 5 | "a": { 6 | "b": [1, 2, 7 | 3], 8 | "c": true, 9 | }, 10 | "d": null, 11 | "e": [{ "f": 4, "g": 5 }], 12 | "I": "example", 13 | }`; 14 | 15 | test("setIn", () => { 16 | const result1 = `{ 17 | "a": false, 18 | "d": null, 19 | "e": [{ "f": 4, "g": 5 }], 20 | "I": "example", 21 | }`; 22 | expect(setIn(["a"], false, exampleData)).toBe(result1); 23 | const result2 = `{ 24 | "a": { 25 | "b": [false, 2, 26 | 3], 27 | "c": true, 28 | }, 29 | "d": null, 30 | "e": [{ "f": 4, "g": 5 }], 31 | "I": "example", 32 | }`; 33 | expect(setIn(["a", "b", 0], false, exampleData)).toBe(result2); 34 | const result3 = `{ 35 | "a": { 36 | "b": [1, 2, 37 | 3], 38 | "c": true, 39 | }, 40 | "d": null, 41 | "e": [{ "f": false, "g": 5 }], 42 | "I": "example", 43 | }`; 44 | expect(setIn(["e", 0, "f"], false, exampleData)).toBe(result3); 45 | expect(setIn(["e", "f"], false, exampleData)).toBe("error"); 46 | expect(setIn(["x", "f"], false, exampleData)).toBe("error"); 47 | 48 | const result4 = `{ 49 | "a": { 50 | "x": [1, 2, 51 | 3], 52 | "c": true, 53 | }, 54 | "d": null, 55 | "e": [{ "f": 4, "g": 5 }], 56 | "I": "example", 57 | }`; 58 | expect(setIn(["a", "b", "b___key"], '"x"', exampleData)).toBe(result4); 59 | const result5 = `{ 60 | "a": { 61 | "b": [1, 2, 62 | 3], 63 | "c": true, 64 | }, 65 | "d": null, 66 | "e": [{ "f": 4, "g": 5 }], 67 | "I": "big darn squid holy shit", 68 | }`; 69 | expect(setIn(["I"], '"big darn squid holy shit"', exampleData)).toBe(result5); 70 | }); 71 | 72 | test("setIn - vegaCode", () => { 73 | const a = setIn( 74 | ["marks", 0, "encode", "hover", "stroke", "value", "value___value"], 75 | '"green"', 76 | vegaCode 77 | ); 78 | expect(a).toMatchSnapshot(); 79 | const b = setIn( 80 | ["marks", 0, "encode", "update", "stroke___val"], 81 | '"purple"', 82 | a 83 | ); 84 | expect(b).toMatchSnapshot(); 85 | }); 86 | 87 | test("syntaxNodeToKeyPath", () => { 88 | ["firebrick", "steelblue", "marks", "extent[1]"].forEach((key) => { 89 | const node = findNodeByText(vegaCode, `"${key}"`)!; 90 | expect(syntaxNodeToKeyPath(node, vegaCode)).toMatchSnapshot(); 91 | }); 92 | }); 93 | 94 | test("syntaxNodeToAbsPath", () => { 95 | ["firebrick", "extent[1]"].forEach((key) => { 96 | const node = findNodeByText(vegaCode, `"${key}"`)!; 97 | expect( 98 | syntaxNodeToAbsPath(node).map((x) => [x.index, x.nodeType]) 99 | ).toMatchSnapshot(); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/__snapshots__/Search.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`filterContents 1`] = ` 4 | [ 5 | { 6 | "elements": [ 7 | { 8 | "content": "grid", 9 | "onSelect": { 10 | "nodeId": "10-13", 11 | "payload": { 12 | "key": ""grid"", 13 | "value": "true", 14 | }, 15 | "type": "addObjectKey", 16 | }, 17 | "type": "button", 18 | }, 19 | { 20 | "content": "gridCap", 21 | "onSelect": { 22 | "nodeId": "10-13", 23 | "payload": { 24 | "key": ""gridCap"", 25 | "value": """", 26 | }, 27 | "type": "addObjectKey", 28 | }, 29 | "type": "button", 30 | }, 31 | { 32 | "content": "gridColor", 33 | "onSelect": { 34 | "nodeId": "10-13", 35 | "payload": { 36 | "key": ""gridColor"", 37 | "value": "null", 38 | }, 39 | "type": "addObjectKey", 40 | }, 41 | "type": "button", 42 | }, 43 | { 44 | "content": "gridDash", 45 | "onSelect": { 46 | "nodeId": "10-13", 47 | "payload": { 48 | "key": ""gridDash"", 49 | "value": "{}", 50 | }, 51 | "type": "addObjectKey", 52 | }, 53 | "type": "button", 54 | }, 55 | { 56 | "content": "gridDashOffset", 57 | "onSelect": { 58 | "nodeId": "10-13", 59 | "payload": { 60 | "key": ""gridDashOffset"", 61 | "value": "0", 62 | }, 63 | "type": "addObjectKey", 64 | }, 65 | "type": "button", 66 | }, 67 | { 68 | "content": "gridOpacity", 69 | "onSelect": { 70 | "nodeId": "10-13", 71 | "payload": { 72 | "key": ""gridOpacity"", 73 | "value": "0", 74 | }, 75 | "type": "addObjectKey", 76 | }, 77 | "type": "button", 78 | }, 79 | { 80 | "content": "gridWidth", 81 | "onSelect": { 82 | "nodeId": "10-13", 83 | "payload": { 84 | "key": ""gridWidth"", 85 | "value": "0", 86 | }, 87 | "type": "addObjectKey", 88 | }, 89 | "type": "button", 90 | }, 91 | ], 92 | "label": "Add Field", 93 | }, 94 | ] 95 | `; 96 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/__snapshots__/Utils.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`setIn - vegaCode 1`] = ` 4 | " 5 | { 6 | "$schema": "https://vega.github.io/schema/vega/v5.json", 7 | "description": "A wheat plot example, which combines elements of dot plots and histograms.", 8 | "width": 500, 9 | "padding": 5, 10 | 11 | "signals": [ 12 | { "name": "symbolDiameter", "value": 4, 13 | "bind": {"input": "range", "min": 1, "max": 8, "step": 0.25} }, 14 | { "name": "binOffset", "value": 0, 15 | "bind": {"input": "range", "min": -0.1, "max": 0.1} }, 16 | { "name": "binStep", "value": 0.075, 17 | "bind": {"input": "range", "min": 0.001, "max": 0.2, "step": 0.001} }, 18 | { "name": "height", "update": "extent[1] * (1 + symbolDiameter)" } 19 | ], 20 | 21 | "data": [ 22 | { 23 | "name": "points", 24 | "url": "data/normal-2d.json", 25 | "transform": [ 26 | { 27 | "type": "bin", "field": "u", 28 | "extent": [-1, 1], 29 | "anchor": {"signal": "binOffset"}, 30 | "step": {"signal": "binStep"}, 31 | "nice": false, 32 | "signal": "bins" 33 | }, 34 | { 35 | "type": "stack", 36 | "groupby": ["bin0"], 37 | "sort": {"field": "u"} 38 | }, 39 | { 40 | "type": "extent", "signal": "extent", 41 | "field": "y1" 42 | } 43 | ] 44 | } 45 | ], 46 | 47 | "scales": [ 48 | { 49 | "name": "xscale", 50 | "type": "linear", 51 | "range": "width", 52 | "domain": [-1, 1] 53 | }, 54 | { 55 | "name": "yscale", 56 | "type": "linear", 57 | "range": "height", 58 | "domain": [0, {"signal": "extent[1]"}] 59 | } 60 | ], 61 | 62 | "axes": [ 63 | { "orient": "bottom", "scale": "xscale", 64 | "values": {"signal": "sequence(bins.start, bins.stop + bins.step, bins.step)"}, 65 | "domain": false, "ticks": false, "labels": false, "grid": true, 66 | "zindex": 0 }, 67 | {"orient": "bottom", "scale": "xscale", "zindex": 1} 68 | ], 69 | 70 | "marks": [ 71 | { 72 | "type": "symbol", 73 | "from": {"data": "points"}, 74 | "encode": { 75 | "enter": { 76 | "fill": {"value": "transparent"}, 77 | "strokeWidth": {"value": 0.5} 78 | }, 79 | "update": { 80 | "x": {"scale": "xscale", "field": "u"}, 81 | "y": {"scale": "yscale", "field": "y0"}, 82 | "size": {"signal": "symbolDiameter * symbolDiameter"}, 83 | "stroke": {"value": "steelblue"} 84 | }, 85 | "hover": { 86 | "stroke": {"value": "green"} 87 | } 88 | } 89 | } 90 | ] 91 | } 92 | " 93 | `; 94 | 95 | exports[`setIn - vegaCode 2`] = ` 96 | " 97 | { 98 | "$schema": "https://vega.github.io/schema/vega/v5.json", 99 | "description": "A wheat plot example, which combines elements of dot plots and histograms.", 100 | "width": 500, 101 | "padding": 5, 102 | 103 | "signals": [ 104 | { "name": "symbolDiameter", "value": 4, 105 | "bind": {"input": "range", "min": 1, "max": 8, "step": 0.25} }, 106 | { "name": "binOffset", "value": 0, 107 | "bind": {"input": "range", "min": -0.1, "max": 0.1} }, 108 | { "name": "binStep", "value": 0.075, 109 | "bind": {"input": "range", "min": 0.001, "max": 0.2, "step": 0.001} }, 110 | { "name": "height", "update": "extent[1] * (1 + symbolDiameter)" } 111 | ], 112 | 113 | "data": [ 114 | { 115 | "name": "points", 116 | "url": "data/normal-2d.json", 117 | "transform": [ 118 | { 119 | "type": "bin", "field": "u", 120 | "extent": [-1, 1], 121 | "anchor": {"signal": "binOffset"}, 122 | "step": {"signal": "binStep"}, 123 | "nice": false, 124 | "signal": "bins" 125 | }, 126 | { 127 | "type": "stack", 128 | "groupby": ["bin0"], 129 | "sort": {"field": "u"} 130 | }, 131 | { 132 | "type": "extent", "signal": "extent", 133 | "field": "y1" 134 | } 135 | ] 136 | } 137 | ], 138 | 139 | "scales": [ 140 | { 141 | "name": "xscale", 142 | "type": "linear", 143 | "range": "width", 144 | "domain": [-1, 1] 145 | }, 146 | { 147 | "name": "yscale", 148 | "type": "linear", 149 | "range": "height", 150 | "domain": [0, {"signal": "extent[1]"}] 151 | } 152 | ], 153 | 154 | "axes": [ 155 | { "orient": "bottom", "scale": "xscale", 156 | "values": {"signal": "sequence(bins.start, bins.stop + bins.step, bins.step)"}, 157 | "domain": false, "ticks": false, "labels": false, "grid": true, 158 | "zindex": 0 }, 159 | {"orient": "bottom", "scale": "xscale", "zindex": 1} 160 | ], 161 | 162 | "marks": [ 163 | { 164 | "type": "symbol", 165 | "from": {"data": "points"}, 166 | "encode": { 167 | "enter": { 168 | "fill": {"value": "transparent"}, 169 | "strokeWidth": {"value": 0.5} 170 | }, 171 | "update": "purple", 172 | "hover": { 173 | "stroke": {"value": "green"} 174 | } 175 | } 176 | } 177 | ] 178 | } 179 | " 180 | `; 181 | 182 | exports[`syntaxNodeToAbsPath 1`] = ` 183 | [ 184 | [ 185 | -1, 186 | "JsonText", 187 | ], 188 | [ 189 | 0, 190 | "Object", 191 | ], 192 | [ 193 | 8, 194 | "Property", 195 | ], 196 | [ 197 | 1, 198 | "Array", 199 | ], 200 | [ 201 | 0, 202 | "Object", 203 | ], 204 | [ 205 | 2, 206 | "Property", 207 | ], 208 | [ 209 | 1, 210 | "Object", 211 | ], 212 | [ 213 | 2, 214 | "Property", 215 | ], 216 | [ 217 | 1, 218 | "Object", 219 | ], 220 | [ 221 | 0, 222 | "Property", 223 | ], 224 | [ 225 | 1, 226 | "Object", 227 | ], 228 | [ 229 | 0, 230 | "Property", 231 | ], 232 | [ 233 | 1, 234 | "String", 235 | ], 236 | ] 237 | `; 238 | 239 | exports[`syntaxNodeToAbsPath 2`] = ` 240 | [ 241 | [ 242 | -1, 243 | "JsonText", 244 | ], 245 | [ 246 | 0, 247 | "Object", 248 | ], 249 | [ 250 | 6, 251 | "Property", 252 | ], 253 | [ 254 | 1, 255 | "Array", 256 | ], 257 | [ 258 | 1, 259 | "Object", 260 | ], 261 | [ 262 | 3, 263 | "Property", 264 | ], 265 | [ 266 | 1, 267 | "Array", 268 | ], 269 | [ 270 | 1, 271 | "Object", 272 | ], 273 | [ 274 | 0, 275 | "Property", 276 | ], 277 | [ 278 | 1, 279 | "String", 280 | ], 281 | ] 282 | `; 283 | 284 | exports[`syntaxNodeToKeyPath 1`] = ` 285 | [ 286 | "marks", 287 | 0, 288 | "encode", 289 | "hover", 290 | "stroke", 291 | "value", 292 | "value___value", 293 | ] 294 | `; 295 | 296 | exports[`syntaxNodeToKeyPath 2`] = ` 297 | [ 298 | "marks", 299 | 0, 300 | "encode", 301 | "update", 302 | "stroke", 303 | "value", 304 | "value___value", 305 | ] 306 | `; 307 | 308 | exports[`syntaxNodeToKeyPath 3`] = ` 309 | [ 310 | "marks___key", 311 | ] 312 | `; 313 | 314 | exports[`syntaxNodeToKeyPath 4`] = ` 315 | [ 316 | "scales", 317 | 1, 318 | "domain", 319 | 1, 320 | "signal", 321 | "signal___value", 322 | ] 323 | `; 324 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/cmState.ts: -------------------------------------------------------------------------------- 1 | import { StateEffect, StateField } from "@codemirror/state"; 2 | import { ViewPlugin, EditorView, ViewUpdate } from "@codemirror/view"; 3 | import { JSONSchema } from "./JSONSchemaTypes"; 4 | import { Projection } from "./projections"; 5 | import { createNodeMap } from "./utils"; 6 | import { lintCode, LintError } from "./Linter"; 7 | import isEqual from "lodash.isequal"; 8 | 9 | export const setSchema = StateEffect.define(); 10 | export const setProjections = StateEffect.define(); 11 | export const setSchemaTypings = StateEffect.define>(); 12 | export const setDiagnostics = StateEffect.define(); 13 | export const setUpdateHook = StateEffect.define(); 14 | 15 | export const initialCmState = { 16 | schema: {} as JSONSchema, 17 | projections: [] as Projection[], 18 | schemaTypings: {} as Record, 19 | diagnostics: [] as LintError[], 20 | codeUpdateHook: (_code: string) => {}, 21 | }; 22 | 23 | const simpleSet = ( 24 | key: keyof typeof initialCmState, 25 | value: any, 26 | state: typeof initialCmState 27 | ) => ({ ...state, [key]: value }); 28 | 29 | export const cmStatePlugin = StateField.define({ 30 | create: () => initialCmState, 31 | update(state, tr) { 32 | let didUpdate = false; 33 | let newState = state; 34 | for (const effect of tr.effects) { 35 | if (effect.is(setSchema)) { 36 | didUpdate = true; 37 | newState = simpleSet("schema", effect.value, newState); 38 | } 39 | if (effect.is(setProjections)) { 40 | didUpdate = true; 41 | newState = simpleSet( 42 | "projections", 43 | effect.value.map((x, id) => ({ ...x, id })), 44 | newState 45 | ); 46 | } 47 | if (effect.is(setSchemaTypings)) { 48 | didUpdate = true; 49 | newState = simpleSet("schemaTypings", effect.value, newState); 50 | } 51 | if (effect.is(setDiagnostics)) { 52 | didUpdate = true; 53 | newState = simpleSet("diagnostics", effect.value, newState); 54 | } 55 | if (effect.is(setUpdateHook)) { 56 | didUpdate = true; 57 | newState = simpleSet("codeUpdateHook", effect.value[0], newState); 58 | } 59 | } 60 | if (didUpdate) { 61 | return newState; 62 | } 63 | return state; 64 | }, 65 | }); 66 | 67 | export const cmStateView = ViewPlugin.fromClass( 68 | class { 69 | constructor() { 70 | this.run = this.run.bind(this); 71 | } 72 | 73 | run(view: EditorView) { 74 | const { schema } = view.state.field(cmStatePlugin); 75 | const code = view.state.doc.toString(); 76 | Promise.all([ 77 | createNodeMap(schema, code).then((schemaMap) => 78 | setSchemaTypings.of(schemaMap) 79 | ), 80 | lintCode(schema, code).then((diagnostics) => 81 | setDiagnostics.of(diagnostics) 82 | ), 83 | ]) 84 | .then((effects) => view.dispatch({ effects })) 85 | .catch((e) => { 86 | console.error(e); 87 | }); 88 | } 89 | 90 | update(update: ViewUpdate) { 91 | const stateValuesChanged = !isEqual( 92 | update.startState.field(cmStatePlugin), 93 | update.state.field(cmStatePlugin) 94 | ); 95 | if (stateValuesChanged || update.docChanged) { 96 | this.run(update.view); 97 | } 98 | } 99 | } 100 | ); 101 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/compute-menu-contents.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxNode } from "@lezer/common"; 2 | import { MenuEvent } from "./modify-json"; 3 | import { SchemaMap } from "../components/Editor"; 4 | import { JSONSchema7 } from "json-schema"; 5 | import { Projection } from "./projections"; 6 | 7 | import { LintError } from "./Linter"; 8 | 9 | import { 10 | evalTypeBasedContent, 11 | evalParentBasedContent, 12 | } from "./menu-content/type-based"; 13 | import { evalSchemaChunks } from "./menu-content/schema-based"; 14 | 15 | export type MenuRow = { label: string; elements: MenuElement[] }; 16 | export type MenuElement = 17 | | { 18 | type: "button"; 19 | label?: string; 20 | content: string; 21 | onSelect: MenuEvent; 22 | } 23 | | { type: "display"; label?: string; content: string } 24 | | { type: "free-input"; label: string } 25 | | { 26 | type: "projection"; 27 | projectionType: Projection["type"]; 28 | takeOverMenu?: boolean; 29 | label?: string; 30 | element: JSX.Element; 31 | }; 32 | 33 | export const nodeToId = (node: SyntaxNode): `${number}-${number}` => 34 | `${node.from}-${node.to}`; 35 | 36 | export function prepDiagnostics( 37 | diagnostics: LintError[], 38 | targetNode: SyntaxNode 39 | ) { 40 | return diagnostics 41 | .filter( 42 | (x) => 43 | (x.from === targetNode.from || x.from === targetNode.from - 1) && 44 | (x.to === targetNode.to || x.to === targetNode.to + 1) 45 | // more generous than we need to be with the linter errors 46 | ) 47 | .map((lint) => ({ 48 | label: "Lint error", 49 | elements: [ 50 | { type: "display", content: lint.message }, 51 | ...(lint.expected || []).map((expectation: string) => { 52 | return { 53 | type: "button", 54 | content: `Switch to ${expectation}`, 55 | onSelect: { 56 | type: "simpleSwap", 57 | payload: 58 | expectation in simpleTypes 59 | ? simpleTypes[expectation] 60 | : `"${expectation}"`, 61 | nodeId: nodeToId(targetNode), 62 | }, 63 | }; 64 | }), 65 | ], 66 | })) 67 | .filter((x) => x); 68 | } 69 | export const simpleTypes: Record = { 70 | string: "", 71 | object: `{ } `, 72 | number: "0", 73 | boolean: true, 74 | array: "[ ] ", 75 | null: "null", 76 | }; 77 | export const literalTypes: Record = { 78 | string: '""', 79 | integer: "0", 80 | number: "0", 81 | boolean: "true", 82 | null: "null", 83 | }; 84 | 85 | export const liminalNodeTypes = new Set(["⚠", "{", "}", "[", "]"]); 86 | export function retargetToAppropriateNode(node: SyntaxNode): SyntaxNode { 87 | let targetNode = node; 88 | if (liminalNodeTypes.has(node.type.name)) { 89 | targetNode = node.parent!; 90 | } else if (node.type.name === "PropertyName") { 91 | targetNode = node.nextSibling!; 92 | } 93 | return targetNode; 94 | } 95 | 96 | function getSchemaForRetargetedNode( 97 | node: SyntaxNode, 98 | schemaMap: SchemaMap 99 | ): JSONSchema7[] { 100 | const targetNode = retargetToAppropriateNode(node); 101 | const from = targetNode.from; 102 | const to = targetNode.to; 103 | 104 | const schemaChunk: JSONSchema7[] = schemaMap[`${from}-${to}`]; 105 | return schemaChunk; 106 | // todo remove below 107 | // if (schemaChunk?.length > 1) { 108 | // return { anyOf: schemaChunk }; 109 | // } else if (schemaChunk?.length === 1) { 110 | // return schemaChunk[0]; 111 | // } 112 | // // implying that its not an array? 113 | // return schemaChunk as any as JSONSchema7; 114 | } 115 | 116 | const safeStringify = (obj: any, indent = 2) => { 117 | let cache: any = []; 118 | const retVal = JSON.stringify( 119 | obj, 120 | (_key, value) => 121 | typeof value === "object" && value !== null 122 | ? cache.includes(value) 123 | ? undefined // Duplicate reference found, discard key 124 | : cache.push(value) && value // Store value in our collection 125 | : value, 126 | indent 127 | ); 128 | cache = null; 129 | return retVal; 130 | }; 131 | 132 | function getCacheKeyForElement(el: MenuElement): string { 133 | switch (el.type) { 134 | case "free-input": 135 | return "free-input"; 136 | case "button": 137 | return el.content; 138 | case "display": 139 | case "projection": 140 | default: 141 | return safeStringify(el); 142 | } 143 | } 144 | 145 | function deduplicate(rows: MenuElement[]): any[] { 146 | const hasSeen: Set = new Set([]); 147 | return rows.filter((x) => { 148 | const key = getCacheKeyForElement(x); 149 | if (hasSeen.has(key)) { 150 | return false; 151 | } 152 | hasSeen.add(key); 153 | return true; 154 | }); 155 | } 156 | 157 | export function simpleMerge(content: MenuRow[]): MenuRow[] { 158 | const groups = content.reduce((acc: Record, row) => { 159 | acc[row.label] = (acc[row.label] || []).concat(row.elements); 160 | return acc; 161 | }, {}); 162 | 163 | return Object.entries(groups).map( 164 | ([label, elements]) => 165 | ({ label, elements: deduplicate(elements).filter((x) => x) } as MenuRow) 166 | ); 167 | } 168 | 169 | function getCompareString(element: MenuElement): string { 170 | switch (element.type) { 171 | case "button": 172 | case "display": 173 | return element.content; 174 | case "free-input": 175 | case "projection": 176 | default: 177 | return "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"; 178 | } 179 | } 180 | 181 | function sortMenuContents(content: MenuRow[]): MenuRow[] { 182 | return content.map((row) => ({ 183 | ...row, 184 | elements: row.elements.sort((a, b) => 185 | getCompareString(a).localeCompare(getCompareString(b)) 186 | ), 187 | })); 188 | } 189 | 190 | export function generateMenuContent( 191 | syntaxNode: SyntaxNode, 192 | schemaMap: SchemaMap, 193 | fullCode: string 194 | ): MenuRow[] { 195 | const schemaChunk = getSchemaForRetargetedNode(syntaxNode, schemaMap); 196 | 197 | const content: MenuRow[] = [ 198 | { name: "evalSchemaChunks", fun: evalSchemaChunks }, 199 | { name: "evalTypeBasedContent", fun: evalTypeBasedContent }, 200 | { name: "evalParentBasedContent", fun: evalParentBasedContent }, 201 | ] 202 | .flatMap(({ fun, name }) => { 203 | try { 204 | return fun(syntaxNode, schemaChunk, fullCode); 205 | } catch (e) { 206 | console.log("error in ", name); 207 | console.error(e); 208 | return []; 209 | } 210 | }) 211 | .filter((x) => x); 212 | 213 | let computedMenuContents = simpleMerge(content).filter( 214 | (x) => x.elements.length 215 | ); 216 | computedMenuContents = sortMenuContents(computedMenuContents); 217 | return computedMenuContents; 218 | } 219 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/local-utils.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from "react"; 2 | 3 | export const usePersistedState = (name: string, defaultValue: any) => { 4 | const [value, setValue] = useState(defaultValue); 5 | const nameRef = useRef(name); 6 | 7 | useEffect(() => { 8 | try { 9 | const storedValue = localStorage.getItem(name); 10 | if (storedValue !== null) setValue(storedValue); 11 | else localStorage.setItem(name, defaultValue); 12 | } catch { 13 | setValue(defaultValue); 14 | } 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, []); 17 | 18 | useEffect(() => { 19 | try { 20 | localStorage.setItem(nameRef.current, value); 21 | } catch (e) { 22 | console.error(e); 23 | } 24 | }, [value]); 25 | 26 | useEffect(() => { 27 | const lastName = nameRef.current; 28 | if (name !== lastName) { 29 | try { 30 | localStorage.setItem(name, value); 31 | nameRef.current = name; 32 | localStorage.removeItem(lastName); 33 | } catch (e) { 34 | console.error(e); 35 | } 36 | } 37 | // eslint-disable-next-line react-hooks/exhaustive-deps 38 | }, [name]); 39 | 40 | return [value, setValue]; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/popover-menu.tsx: -------------------------------------------------------------------------------- 1 | import { Extension } from "@codemirror/state"; 2 | import { keymap } from "@codemirror/view"; 3 | import { popOverState } from "./popover-menu/PopoverState"; 4 | import { popOverCompletionKeymap } from "./popover-menu/KeyboardControls"; 5 | 6 | export default function popoverPlugin(): Extension { 7 | return [keymap.of(popOverCompletionKeymap), popOverState]; 8 | } 9 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/popover-menu/KeyboardControls.ts: -------------------------------------------------------------------------------- 1 | import { KeyBinding, EditorView } from "@codemirror/view"; 2 | import { simpleUpdate, codeString, getCursorPos } from "../utils"; 3 | 4 | import { 5 | popOverState, 6 | popoverEffectDispatch, 7 | setRouting, 8 | SelectionRoute, 9 | popoverSMEvent, 10 | } from "./PopoverState"; 11 | 12 | import { MenuRow, MenuElement } from "../compute-menu-contents"; 13 | import { modifyCodeByCommand } from "../modify-json"; 14 | 15 | type dir = "left" | "right" | "down" | "up"; 16 | const changeSelectionRoute = (direction: dir) => (view: EditorView) => { 17 | const { menuState, selectedRouting, menuContents } = 18 | view.state.field(popOverState); 19 | // pop over not actively in use 20 | if (menuState !== "tooltipInUse") { 21 | return false; 22 | } 23 | 24 | const updatedCursor = buildMoveCursor( 25 | direction, 26 | menuContents, 27 | selectedRouting 28 | ); 29 | const effect = updatedCursor 30 | ? setRouting.of(updatedCursor) 31 | : popoverEffectDispatch.of("stopUsingTooltip"); 32 | view.dispatch({ effects: [effect] }); 33 | 34 | return true; 35 | }; 36 | 37 | function buildMoveCursor( 38 | dir: dir, 39 | content: MenuRow[], 40 | route: SelectionRoute 41 | ): SelectionRoute | false { 42 | let row = route[0]; 43 | let col = route[1]; 44 | 45 | const leafGroupSize = content[row].elements?.length; 46 | const numRows = content.length; 47 | 48 | // bail out of menu use 49 | if (dir === "up" && row - 1 < 0) { 50 | return false; 51 | } 52 | // modify menu selection 53 | if (dir === "up" && row - 1 >= 0) { 54 | row -= 1; 55 | col = 0; 56 | } 57 | if (dir === "down" && row < numRows - 1) { 58 | row += 1; 59 | col = 0; 60 | } 61 | if (dir === "left") { 62 | col = Math.max(col - 1, 0); 63 | } 64 | if (dir === "right") { 65 | col = Math.min(col + 1, leafGroupSize); 66 | } 67 | 68 | return [row, col]; 69 | } 70 | 71 | const traverseContentTreeToNode: ( 72 | tree: MenuRow[], 73 | path: SelectionRoute 74 | ) => MenuElement | MenuRow | null = (tree, [row, col]) => { 75 | if (!tree.length) { 76 | return null; 77 | } 78 | return tree[row].elements[col - 1]; 79 | }; 80 | 81 | function runSelection(view: EditorView) { 82 | const { menuContents, selectedRouting, targetNode } = 83 | view.state.field(popOverState); 84 | 85 | let target = traverseContentTreeToNode(menuContents, selectedRouting); 86 | if (!target) { 87 | return false; 88 | } 89 | if ((target as MenuRow).label) { 90 | return false; 91 | } 92 | target = target as MenuElement; 93 | if (target.type === "button") { 94 | const update = modifyCodeByCommand( 95 | target.onSelect, 96 | targetNode!, 97 | codeString(view, 0), 98 | getCursorPos(view.state) 99 | ); 100 | if (update) { 101 | simpleUpdate(view, update.from, update.to, update.value); 102 | } 103 | 104 | view.dispatch({ 105 | effects: [ 106 | popoverEffectDispatch.of("closeTooltip"), 107 | setRouting.of([0, 0]), 108 | ], 109 | }); 110 | } 111 | return true; 112 | } 113 | const simpleDispatch = (view: EditorView, action: popoverSMEvent) => 114 | view.dispatch({ effects: [popoverEffectDispatch.of(action)] }); 115 | 116 | function engageWithPopover(view: EditorView) { 117 | simpleDispatch(view, "useTooltip"); 118 | return true; 119 | } 120 | 121 | function toggleForce(view: EditorView) { 122 | const { menuState } = view.state.field(popOverState); 123 | const action = 124 | menuState === "monocleOpen" ? "switchToTooltip" : "switchToMonocle"; 125 | simpleDispatch(view, action); 126 | return true; 127 | } 128 | 129 | export const popOverCompletionKeymap: readonly KeyBinding[] = [ 130 | { key: "Cmd-.", run: toggleForce }, 131 | { key: "Escape", run: toggleForce }, 132 | { key: "Cmd-ArrowDown", run: engageWithPopover, preventDefault: true }, 133 | { key: "ArrowDown", run: changeSelectionRoute("down") }, 134 | { key: "ArrowUp", run: changeSelectionRoute("up") }, 135 | { key: "ArrowLeft", run: changeSelectionRoute("left") }, 136 | { key: "ArrowRight", run: changeSelectionRoute("right") }, 137 | { key: "Enter", run: runSelection }, 138 | ]; 139 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/popover-menu/PopoverMenuElement.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import ReactMarkdown from "react-markdown"; 3 | import { classNames } from "../utils"; 4 | import { MenuEvent } from "../modify-json"; 5 | import { MenuRow } from "../compute-menu-contents"; 6 | import { parseTree, Node } from "jsonc-parser"; 7 | import { colorNames } from "../utils"; 8 | 9 | type MenuElementRenderer = (props: { 10 | eventDispatch: (menuEvent: MenuEvent, shouldCloseMenu?: boolean) => void; 11 | // TODO fix this type; 12 | menuElement: T; 13 | isSelected: boolean; 14 | allElementsInGroupAreOfThisType: boolean; 15 | parentGroup: MenuRow; 16 | }) => JSX.Element; 17 | 18 | const MarkDownMask = ReactMarkdown as any; 19 | const DisplayElement: MenuElementRenderer = (props) => { 20 | const isLintError = props.parentGroup.label === "Lint error"; 21 | return ( 22 |
30 | {props.menuElement.content.trim()} 31 |
32 | ); 33 | }; 34 | 35 | const InputElement: MenuElementRenderer = (props) => { 36 | const { isSelected, menuElement, eventDispatch } = props; 37 | const ref = useRef(null); 38 | useEffect(() => { 39 | if (isSelected) { 40 | ref.current?.focus(); 41 | } 42 | }, [isSelected]); 43 | const onSubmit = () => { 44 | const response = { 45 | ...menuElement.onSelect, 46 | payload: { 47 | key: `"${ref.current?.value!}"`, 48 | value: menuElement.onSelect.payload.value, 49 | }, 50 | }; 51 | eventDispatch(response, true); 52 | }; 53 | return ( 54 |
60 | { 64 | if (e.key === "Enter") { 65 | e.stopPropagation(); 66 | onSubmit(); 67 | } 68 | }} 69 | /> 70 | 71 |
72 | ); 73 | }; 74 | 75 | function treeToShortString(node: Node): string { 76 | if (!node.children) { 77 | if (node.type === "string" && !node.value.length) { 78 | return '""'; 79 | } 80 | return `${node.value as string}`; 81 | } 82 | if (node.type === "property") { 83 | const key = treeToShortString(node.children[0]); 84 | const value = treeToShortString(node.children[1]); 85 | return `${key}: ${value}`; 86 | } 87 | const innerContent = node.children 88 | .flatMap((child) => treeToShortString(child)) 89 | .join(", "); 90 | if (node.type === "array") { 91 | return `[${innerContent}]`; 92 | } 93 | if (node.type === "object") { 94 | return `{${innerContent}}`; 95 | } 96 | return ""; 97 | } 98 | 99 | function maybeGenerateShortRep(content: string) { 100 | const parsed = parseTree(content); 101 | if (!parsed || parsed.type !== "object") { 102 | return content; 103 | } 104 | return treeToShortString(parsed); 105 | } 106 | 107 | const ButtonElement: MenuElementRenderer = ({ 108 | isSelected, 109 | menuElement: { onSelect, content, label }, 110 | eventDispatch, 111 | allElementsInGroupAreOfThisType, 112 | }) => { 113 | const reformattedContent = !label ? maybeGenerateShortRep(content) : content; 114 | 115 | return ( 116 |
124 | 135 | {label &&
{label}
} 136 |
137 | ); 138 | }; 139 | 140 | const ProjectionElement: MenuElementRenderer = ({ 141 | isSelected, 142 | menuElement: { element }, 143 | }) => ( 144 |
150 | {element} 151 |
152 | ); 153 | 154 | const dispatch: Record> = { 155 | display: DisplayElement, 156 | button: ButtonElement, 157 | projection: ProjectionElement, 158 | "free-input": InputElement, 159 | }; 160 | const RenderMenuElement: MenuElementRenderer = (props) => { 161 | return dispatch[props.menuElement.type](props); 162 | }; 163 | 164 | export default RenderMenuElement; 165 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/query.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema } from "./JSONSchemaTypes"; 2 | import { SyntaxNode } from "@lezer/common"; 3 | 4 | type NodeType = SyntaxNode["type"]["name"]; 5 | type functionQueryType = ( 6 | value: string, 7 | nodeType: NodeType, 8 | keyPath: (string | number)[], 9 | cursorPosition: number, 10 | nodePos: { start: number; end: number } 11 | ) => boolean; 12 | export type ProjectionQuery = 13 | | { type: "function"; query: functionQueryType } 14 | | { type: "index"; query: (number | string)[] } 15 | | { type: "multi-index"; query: (number | string)[][] } 16 | | { type: "regex"; query: RegExp } 17 | | { type: "value"; query: string[] } 18 | | { type: "schemaMatch"; query: string[] } 19 | | { type: "nodeType"; query: NodeType[] }; 20 | 21 | export function keyPathMatchesQuery( 22 | query: (string | number)[], 23 | keyPath: (string | number)[] 24 | ): boolean { 25 | if (query.length !== keyPath.length) { 26 | return false; 27 | } 28 | for (let idx = 0; idx < query.length; idx++) { 29 | if (query[idx] === "*") { 30 | continue; 31 | } 32 | if (query[idx] !== keyPath[idx]) { 33 | return false; 34 | } 35 | } 36 | 37 | return true; 38 | } 39 | 40 | function valueQuery(query: string[], nodeValue: string): boolean { 41 | const strippedVal = nodeValue.slice(1, nodeValue.length - 1); 42 | return !!query.find((x) => x === strippedVal); 43 | } 44 | 45 | const functionQuery = ( 46 | query: functionQueryType, 47 | ...args: Parameters 48 | ) => query(...args); 49 | 50 | function regexQuery(query: RegExp, nodeValue: string) { 51 | return !!nodeValue.match(query); 52 | } 53 | 54 | function schemaMatchQuery(query: string[], typings: any): boolean { 55 | let refNames = []; 56 | if (Array.isArray(typings)) { 57 | refNames = typings 58 | .flatMap((type: JSONSchema) => [type?.$$refName, type?.$$labeledType]) 59 | .filter((x) => x); 60 | } else if (typeof typings === "object") { 61 | refNames = [typings?.$$refName, typings?.$$labeledType].filter((x) => x); 62 | } 63 | const downcasedQuery = query.map((x) => x.toLowerCase()); 64 | const result = refNames 65 | .map((x) => x.toLowerCase()) 66 | .some((type) => { 67 | const last = type?.split("/").at(-1); 68 | return downcasedQuery.some((queryKey) => queryKey === last); 69 | }); 70 | return result; 71 | } 72 | 73 | function nodeTypeMatch(query: NodeType[], nodeType: NodeType): boolean { 74 | return query.some((x) => x === nodeType); 75 | } 76 | 77 | const simpleMatchers = { 78 | regex: regexQuery, 79 | value: valueQuery, 80 | function: functionQuery, 81 | }; 82 | const cache: Record = {}; 83 | function buildCacheKey( 84 | query: ProjectionQuery, 85 | keyPath: (string | number)[], 86 | nodeValue: any, 87 | projId: number, 88 | cursorPos: number 89 | ) { 90 | const keyPathStr = JSON.stringify(keyPath); 91 | 92 | switch (query.type) { 93 | case "function": { 94 | const queryStr = JSON.stringify(query); 95 | return `${queryStr}-${keyPathStr}-${nodeValue}}-${projId}-${cursorPos}`; 96 | } 97 | case "regex": { 98 | const queryReg = JSON.stringify({ 99 | ...query, 100 | query: query.query.toString(), 101 | }); 102 | return `${queryReg}-${keyPathStr}-${nodeValue}}-${projId}`; 103 | } 104 | case "multi-index": 105 | case "index": 106 | case "nodeType": 107 | case "value": 108 | case "schemaMatch": 109 | default: { 110 | const queryStr = JSON.stringify(query); 111 | return `${queryStr}-${keyPathStr}-${nodeValue}}-${projId}`; 112 | } 113 | } 114 | } 115 | export function runProjectionQuery(props: { 116 | query: ProjectionQuery; 117 | keyPath: (string | number)[]; 118 | nodeValue: string; 119 | typings: any[]; 120 | nodeType: SyntaxNode["type"]["name"]; 121 | projId: number; 122 | cursorPosition: number; 123 | nodePos: { start: number; end: number }; 124 | }): boolean { 125 | const { 126 | query, 127 | keyPath, 128 | nodeValue, 129 | projId, 130 | nodeType, 131 | typings, 132 | cursorPosition, 133 | nodePos, 134 | } = props; 135 | 136 | const cacheKey = buildCacheKey( 137 | query, 138 | keyPath, 139 | nodeValue, 140 | projId, 141 | cursorPosition 142 | ); 143 | if (cache[cacheKey]) { 144 | return cache[cacheKey]; 145 | } 146 | let pass = false; 147 | switch (query.type) { 148 | case "multi-index": 149 | pass = query.query.some((q) => keyPathMatchesQuery(q, keyPath)); 150 | break; 151 | case "index": 152 | pass = keyPathMatchesQuery(query.query, keyPath); 153 | break; 154 | case "function": 155 | pass = functionQuery( 156 | query.query as any, 157 | nodeValue, 158 | nodeType, 159 | keyPath, 160 | cursorPosition, 161 | nodePos 162 | ); 163 | break; 164 | case "nodeType": 165 | pass = nodeTypeMatch(query.query, nodeType); 166 | break; 167 | case "value": 168 | case "regex": 169 | pass = simpleMatchers[query.type](query.query as any, nodeValue); 170 | break; 171 | case "schemaMatch": 172 | pass = schemaMatchQuery(query.query, typings); 173 | break; 174 | default: 175 | return false; 176 | } 177 | cache[cacheKey] = pass; 178 | return pass; 179 | } 180 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/search.ts: -------------------------------------------------------------------------------- 1 | import { MenuRow } from "./compute-menu-contents"; 2 | import { SyntaxNode } from "@lezer/common"; 3 | // import levenshtein from 'js-levenshtein'; 4 | // todo https://itnext.io/string-similarity-the-basic-know-your-algorithms-guide-3de3d7346227 5 | 6 | export function filterContents(searchTerm: string, rows: MenuRow[]): MenuRow[] { 7 | const term = searchTerm.toLowerCase(); 8 | const result = rows 9 | .map((row) => { 10 | return { 11 | ...row, 12 | elements: row.elements.filter((el) => { 13 | switch (el.type) { 14 | case "button": 15 | return ((el.label || "") + el.content) 16 | .toLowerCase() 17 | .includes(term); 18 | case "display": 19 | return ((el.label || "") + el.content) 20 | .toLowerCase() 21 | .includes(term); 22 | case "free-input": 23 | return false; 24 | default: 25 | return true; 26 | } 27 | }), 28 | }; 29 | }) 30 | .filter((row) => row.elements.length > 0); 31 | return result; 32 | } 33 | 34 | /** 35 | * This function potentially filters the content depending on some heuristics 36 | * @param targetNode 37 | * @param fullCode 38 | * @param contents 39 | * @returns MenuRow[] 40 | */ 41 | export function potentiallyFilterContentForGesture( 42 | targetNode: SyntaxNode, 43 | fullCode: string, 44 | contents: MenuRow[] 45 | ) { 46 | // this suggests that this MAY be an autocomplete gesture 47 | const targetNodeIsError = targetNode.type.name === "⚠"; 48 | const targNodeContent = fullCode.slice(targetNode.from, targetNode.to); 49 | const useContentAsFilter = 50 | targetNodeIsError && !targNodeContent.includes(" "); 51 | 52 | // console.log( 53 | // targetNode.type, 54 | // targNodeContent, 55 | // contents 56 | // ); 57 | 58 | if (useContentAsFilter) { 59 | return filterContents(targNodeContent, contents); 60 | } else { 61 | return contents; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/syntax-highlighting.ts: -------------------------------------------------------------------------------- 1 | import { tags } from "@lezer/highlight"; 2 | import { HighlightStyle } from "@codemirror/language"; 3 | 4 | const blue = "#0551A5"; 5 | const green = "#17885C"; 6 | const red = "#A21615"; 7 | const black = "#000"; 8 | export default HighlightStyle.define([ 9 | { tag: tags.string, color: blue }, 10 | { tag: tags.number, color: green }, 11 | { tag: tags.bool, color: blue }, 12 | { tag: tags.propertyName, color: red }, 13 | { tag: tags.null, color: blue }, 14 | { tag: tags.separator, color: black }, 15 | { tag: tags.squareBracket, color: black }, 16 | { tag: tags.brace, color: black }, 17 | ]); 18 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxNode } from "@lezer/common"; 2 | import { parser } from "@lezer/json"; 3 | // could also write one that is find by text content? 4 | export function findNodeByLocation( 5 | text: string, 6 | from: number, 7 | to: number 8 | ): SyntaxNode | null { 9 | let foundNode: SyntaxNode | null = null; 10 | parser.parse(text).iterate({ 11 | enter: (node) => { 12 | // console.log(node.from, node.to, node.node.type); 13 | if (node.from === from && node.to === to) { 14 | foundNode = node.node as unknown as SyntaxNode; 15 | } 16 | }, 17 | }); 18 | return foundNode; 19 | } 20 | 21 | export function findNodeByText( 22 | text: string, 23 | matchText: string 24 | ): SyntaxNode | null { 25 | let foundNode: SyntaxNode | null = null; 26 | parser.parse(text).iterate({ 27 | enter: (node) => { 28 | // console.log(node.from, node.to, node.node.type); 29 | const testText = text.slice(node.from, node.to); 30 | if (testText === matchText) { 31 | foundNode = node.node as unknown as SyntaxNode; 32 | } 33 | }, 34 | }); 35 | return foundNode; 36 | } 37 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/vendored/prettifier.ts: -------------------------------------------------------------------------------- 1 | // Forked from https://github.com/lydell/json-stringify-pretty-compact 2 | 3 | // Note: This regex matches even invalid JSON strings, but since we’re 4 | // working on the output of `JSON.stringify` we know that only valid strings 5 | // are present (unless the user supplied a weird `options.indent` but in 6 | // that case we don’t care since the output would be invalid anyway). 7 | const stringOrChar = /("(?:[^\\"]|\\.)*")|[:,]/g; 8 | 9 | export default function stringify( 10 | passedObj: any, 11 | options: { 12 | indent?: string; 13 | maxLength?: number; 14 | replacer?: (this: any, key: string, value: any) => any; 15 | } = {} 16 | ) { 17 | const indent = JSON.stringify( 18 | [1], 19 | undefined, 20 | options.indent === undefined ? 2 : options.indent 21 | ).slice(2, -3); 22 | 23 | const maxLength = 24 | indent === "" 25 | ? Infinity 26 | : options.maxLength === undefined 27 | ? 80 28 | : options.maxLength; 29 | 30 | let { replacer } = options; 31 | 32 | return (function _stringify(obj, currentIndent, reserved): string { 33 | if (obj && typeof obj.toJSON === "function") { 34 | obj = obj.toJSON(); 35 | } 36 | 37 | const string = JSON.stringify(obj, replacer); 38 | 39 | if (string === undefined) { 40 | return string; 41 | } 42 | 43 | const length = maxLength - currentIndent.length - reserved; 44 | 45 | if (string.length <= length) { 46 | const prettified = string.replace( 47 | stringOrChar, 48 | (match, stringLiteral) => { 49 | return stringLiteral || `${match} `; 50 | } 51 | ); 52 | if (prettified.length <= length) { 53 | return prettified; 54 | } 55 | } 56 | 57 | if (replacer != null) { 58 | obj = JSON.parse(string); 59 | replacer = undefined; 60 | } 61 | 62 | if (typeof obj === "object" && obj !== null) { 63 | const nextIndent = currentIndent + indent; 64 | const items = []; 65 | let index = 0; 66 | let start; 67 | let end; 68 | 69 | if (Array.isArray(obj)) { 70 | start = "["; 71 | end = "]"; 72 | const { length } = obj; 73 | for (; index < length; index++) { 74 | items.push( 75 | _stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) || 76 | "null" 77 | ); 78 | } 79 | } else { 80 | start = "{"; 81 | end = "}"; 82 | const keys = Object.keys(obj); 83 | const { length } = keys; 84 | for (; index < length; index++) { 85 | const key = keys[index]; 86 | const keyPart = `${JSON.stringify(key)}: `; 87 | const value = _stringify( 88 | obj[key], 89 | nextIndent, 90 | keyPart.length + (index === length - 1 ? 0 : 1) 91 | ); 92 | if (value !== undefined) { 93 | items.push(keyPart + value); 94 | } 95 | } 96 | } 97 | 98 | if (items.length > 0) { 99 | return [start, indent + items.join(`,\n${nextIndent}`), end].join( 100 | `\n${currentIndent}` 101 | ); 102 | } 103 | } 104 | 105 | return string; 106 | })(passedObj, "", 0); 107 | } 108 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/vendored/utils.ts: -------------------------------------------------------------------------------- 1 | import * as Json from "jsonc-parser"; 2 | import { JSONSchemaRef, JSONSchema } from "../JSONSchemaTypes"; 3 | import { ASTNode } from "./parser"; 4 | 5 | export function isNumber(val: any): val is number { 6 | return typeof val === "number"; 7 | } 8 | 9 | export function isDefined(val: any): val is object { 10 | return typeof val !== "undefined"; 11 | } 12 | 13 | export function isBoolean(val: any): val is boolean { 14 | return typeof val === "boolean"; 15 | } 16 | 17 | export function isString(val: any): val is string { 18 | return typeof val === "string"; 19 | } 20 | 21 | export function getNodeValue(node: ASTNode): any { 22 | return Json.getNodeValue(node); 23 | } 24 | 25 | export function asSchema( 26 | schema: JSONSchemaRef | undefined 27 | ): JSONSchema | undefined { 28 | if (isBoolean(schema)) { 29 | return schema ? {} : { not: {} }; 30 | } 31 | return schema; 32 | } 33 | 34 | export function contains( 35 | node: ASTNode, 36 | offset: number, 37 | includeRightBound = false 38 | ): boolean { 39 | return ( 40 | (offset >= node.offset && offset < node.offset + node.length) || 41 | (includeRightBound && offset === node.offset + node.length) 42 | ); 43 | } 44 | 45 | export function equals(one: any, other: any): boolean { 46 | if (one === other) { 47 | return true; 48 | } 49 | if ( 50 | one === null || 51 | one === undefined || 52 | other === null || 53 | other === undefined 54 | ) { 55 | return false; 56 | } 57 | if (typeof one !== typeof other) { 58 | return false; 59 | } 60 | if (typeof one !== "object") { 61 | return false; 62 | } 63 | if (Array.isArray(one) !== Array.isArray(other)) { 64 | return false; 65 | } 66 | 67 | let i: number; 68 | let key: string; 69 | 70 | if (Array.isArray(one)) { 71 | if (one.length !== other.length) { 72 | return false; 73 | } 74 | for (i = 0; i < one.length; i++) { 75 | if (!equals(one[i], other[i])) { 76 | return false; 77 | } 78 | } 79 | } else { 80 | const oneKeys: string[] = []; 81 | 82 | for (key in one) { 83 | oneKeys.push(key); 84 | } 85 | oneKeys.sort(); 86 | const otherKeys: string[] = []; 87 | for (key in other) { 88 | otherKeys.push(key); 89 | } 90 | otherKeys.sort(); 91 | if (!equals(oneKeys, otherKeys)) { 92 | return false; 93 | } 94 | for (i = 0; i < oneKeys.length; i++) { 95 | if (!equals(one[oneKeys[i]], other[oneKeys[i]])) { 96 | return false; 97 | } 98 | } 99 | } 100 | return true; 101 | } 102 | 103 | export enum ErrorCode { 104 | Undefined = 0, 105 | EnumValueMismatch = 1, 106 | Deprecated = 2, 107 | UnexpectedEndOfComment = 0x101, 108 | UnexpectedEndOfString = 0x102, 109 | UnexpectedEndOfNumber = 0x103, 110 | InvalidUnicode = 0x104, 111 | InvalidEscapeCharacter = 0x105, 112 | InvalidCharacter = 0x106, 113 | PropertyExpected = 0x201, 114 | CommaExpected = 0x202, 115 | ColonExpected = 0x203, 116 | ValueExpected = 0x204, 117 | CommaOrCloseBacketExpected = 0x205, 118 | CommaOrCloseBraceExpected = 0x206, 119 | TrailingComma = 0x207, 120 | DuplicateKey = 0x208, 121 | CommentNotPermitted = 0x209, 122 | SchemaResolveError = 0x300, 123 | } 124 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/widgets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Decoration, 3 | DecorationSet, 4 | EditorView, 5 | ViewPlugin, 6 | ViewUpdate, 7 | } from "@codemirror/view"; 8 | import { syntaxTree } from "@codemirror/language"; 9 | import { NodeType, SyntaxNode } from "@lezer/common"; 10 | 11 | import { Range, EditorState } from "@codemirror/state"; 12 | import isEqual from "lodash.isequal"; 13 | 14 | import { cmStatePlugin } from "./cmState"; 15 | import { popOverState } from "./popover-menu/PopoverState"; 16 | 17 | import Highlighter from "./widgets/highlighter"; 18 | import { getInUseRanges, projectionState } from "./projections"; 19 | 20 | type EventSubs = { [x: string]: (e: MouseEvent, view: EditorView) => any }; 21 | export interface SimpleWidget { 22 | checkForAdd: ( 23 | type: NodeType, 24 | view: EditorView, 25 | currentNode: SyntaxNode 26 | ) => boolean; 27 | addNode: ( 28 | view: EditorView, 29 | from: number, 30 | to: number, 31 | currentNode: SyntaxNode 32 | ) => Range[]; 33 | eventSubscriptions: EventSubs; 34 | } 35 | export interface SimpleWidgetStateVersion { 36 | checkForAdd: ( 37 | type: NodeType, 38 | state: EditorState, 39 | currentNode: SyntaxNode 40 | ) => boolean; 41 | addNode: ( 42 | state: EditorState, 43 | from: number, 44 | to: number, 45 | currentNode: SyntaxNode 46 | ) => Range[]; 47 | eventSubscriptions: EventSubs; 48 | } 49 | 50 | function createWidgets(view: EditorView) { 51 | const widgets: Range[] = []; 52 | // todo maybe this won't break? 53 | const { projectionsInUse } = view.state.field(projectionState); 54 | const blockedRanges = getInUseRanges(projectionsInUse); 55 | for (const { from, to } of view.visibleRanges) { 56 | syntaxTree(view.state).iterate({ 57 | from, 58 | to, 59 | enter: ({ node, from, to, type }) => { 60 | if (blockedRanges.has(`${from}-${to}`)) { 61 | return false; 62 | } 63 | [Highlighter].forEach(({ checkForAdd, addNode }) => { 64 | if (!checkForAdd(type, view, node)) { 65 | return; 66 | } 67 | addNode(view, from, to, node).forEach((w) => widgets.push(w)); 68 | }); 69 | }, 70 | }); 71 | } 72 | try { 73 | return Decoration.set( 74 | widgets.sort((a, b) => { 75 | const delta = a.from - b.from; 76 | const relWidth = 0; 77 | // const relWidth = a.to - a.from - (b.to - b.from); 78 | return delta || relWidth; 79 | }) 80 | ); 81 | } catch (e) { 82 | console.log(e); 83 | console.log("problem creating widgets"); 84 | return Decoration.set([]); 85 | } 86 | } 87 | 88 | // create event handler for all in play widgets 89 | // const subscriptions = simpleWidgets.reduce((acc, row) => { 90 | // Object.entries(row.eventSubscriptions).forEach(([eventName, sub]) => { 91 | // acc[eventName] = (acc[eventName] || []).concat(sub); 92 | // }); 93 | // return acc; 94 | // }, {} as { [eventName: string]: any[] }); 95 | // const eventHandlers = Object.entries(subscriptions).reduce( 96 | // (handlers: EventSubs, [eventName, subs]) => { 97 | // handlers[eventName] = (event, view) => { 98 | // subs.forEach((sub) => sub(event, view)); 99 | // }; 100 | 101 | // return handlers; 102 | // }, 103 | // {} 104 | // ); 105 | // build the widgets 106 | export const widgetsPlugin = ViewPlugin.fromClass( 107 | class { 108 | decorations: DecorationSet; 109 | 110 | constructor(view: EditorView) { 111 | this.decorations = createWidgets(view); 112 | } 113 | 114 | update(update: ViewUpdate) { 115 | const stateValuesChanged = !isEqual( 116 | update.startState.field(cmStatePlugin), 117 | update.state.field(cmStatePlugin) 118 | ); 119 | const targetChanged = !isEqual( 120 | update.startState.field(popOverState).targetNode, 121 | update.state.field(popOverState).targetNode 122 | ); 123 | if ( 124 | update.docChanged || 125 | update.viewportChanged || 126 | stateValuesChanged || 127 | targetChanged 128 | ) { 129 | this.decorations = createWidgets(update.view); 130 | } 131 | } 132 | 133 | // todo maybe need to add destroy and force 134 | }, 135 | { 136 | decorations: (v) => v.decorations, 137 | // eventHandlers, 138 | } 139 | ); 140 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/widgets/highlighter.ts: -------------------------------------------------------------------------------- 1 | import { Decoration } from "@codemirror/view"; 2 | import { SimpleWidget } from "../widgets"; 3 | import { cmStatePlugin } from "../cmState"; 4 | import { popOverState } from "../popover-menu/PopoverState"; 5 | import { classNames, syntaxNodeToKeyPath, codeString } from "../utils"; 6 | import { SyntaxNode } from "@lezer/common"; 7 | import { runProjectionQuery } from "../query"; 8 | import { EditorView } from "@codemirror/view"; 9 | 10 | const simpleTypes = new Set([ 11 | // "{", 12 | // "}", 13 | // "[", 14 | // "]", 15 | "String", 16 | "PropertyName", 17 | "Number", 18 | "Boolean", 19 | "Null", 20 | "True", 21 | "False", 22 | ]); 23 | 24 | const toParents = new Set([ 25 | "[", 26 | "]", 27 | "{", 28 | "}", 29 | "⚠", 30 | // "PropertyName", 31 | "PropertyValue", 32 | ]); 33 | export const targTypes = new Set([ 34 | "Object", 35 | "Property", 36 | "Array", 37 | "String", 38 | "Number", 39 | "Null", 40 | "False", 41 | "True", 42 | ]); 43 | export function pickNodeToHighlight(node: SyntaxNode): SyntaxNode { 44 | const type = node.type.name; 45 | if (toParents.has(type)) { 46 | return node.parent!; 47 | } 48 | 49 | return node; 50 | } 51 | 52 | function prepareHighlightString(view: EditorView, node: SyntaxNode) { 53 | const { schemaTypings, projections } = view.state.field(cmStatePlugin); 54 | const keyPath = syntaxNodeToKeyPath(node, codeString(view, 0)); 55 | const highlights = projections 56 | .filter( 57 | (proj) => 58 | proj.type === "highlight" && // todo covert these args to named args 59 | runProjectionQuery({ 60 | query: proj.query, 61 | keyPath, 62 | nodeValue: codeString(view, node.from, node.to), 63 | typings: schemaTypings[`${node.from}-${node.to}`], 64 | nodeType: node.type.name, 65 | // @ts-ignore 66 | projId: proj.id, 67 | cursorPosition: view.state.selection.ranges[0].from, 68 | nodePos: { start: node.from, end: node.to }, 69 | }) 70 | ) 71 | .map((x: any) => x.class); 72 | return highlights.join(" "); 73 | } 74 | 75 | const Highlighter: SimpleWidget = { 76 | checkForAdd: (_type, view, node) => { 77 | // return false; 78 | const { schemaTypings, diagnostics } = view.state.field(cmStatePlugin); 79 | 80 | const { highlightNode } = view.state.field(popOverState); 81 | const isTargetableType = simpleTypes.has(node.type.name); 82 | const hasTyping = schemaTypings[`${node.from}-${node.to}`]; 83 | const hasDiagnosticError = !!diagnostics.find( 84 | (x) => x.from === node.from && x.to === node.to 85 | ); 86 | const isTarget = 87 | !!highlightNode && 88 | highlightNode.from === node.from && 89 | highlightNode.to === node.to; 90 | const highlights = prepareHighlightString(view, node); 91 | return ( 92 | highlights.length || 93 | (hasTyping && hasTyping.length) || 94 | isTargetableType || 95 | hasDiagnosticError || 96 | isTarget 97 | ); 98 | }, 99 | addNode: (view, from, to, node) => { 100 | const { diagnostics } = view.state.field(cmStatePlugin); 101 | const { highlightNode } = view.state.field(popOverState); 102 | const highlights = prepareHighlightString(view, node); 103 | 104 | const hasDiagnosticError = !!diagnostics.find( 105 | (x) => x.from === node.from && x.to === node.to 106 | ); 107 | const l2 = new Set(["x"]); 108 | // const l2 = new Set(["Property"]); 109 | // const l3 = new Set(["Object", "Array"]); 110 | const l3 = new Set(["X"]); 111 | const levelNumber = [simpleTypes, l2, l3].findIndex((x) => 112 | x.has(node.type.name) 113 | ); 114 | const level = `${levelNumber >= 0 ? levelNumber + 1 : 4}`; 115 | const isHighlightNode = 116 | !!highlightNode && 117 | highlightNode.from === node.from && 118 | highlightNode.to === node.to; 119 | if (level === "4" && !hasDiagnosticError && !isHighlightNode) { 120 | return []; 121 | } 122 | const highlight = Decoration.mark({ 123 | attributes: { 124 | class: classNames({ 125 | [highlights]: true, 126 | "cm-annotation-highlighter": true, 127 | "cm-annotation-highlighter-selected": isHighlightNode, 128 | [`cm-annotation-highlighter-${level}`]: true, 129 | "cm-linter-highlight": hasDiagnosticError, 130 | }), 131 | }, 132 | }); 133 | 134 | return from !== to ? [highlight.range(from, to)] : []; 135 | }, 136 | eventSubscriptions: {}, 137 | }; 138 | export default Highlighter; 139 | -------------------------------------------------------------------------------- /packages/prong-editor/src/lib/widgets/inline-projection-widget.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from "react-dom"; 2 | import { createElement } from "react"; 3 | import { WidgetType, Decoration } from "@codemirror/view"; 4 | import { EditorState } from "@codemirror/state"; 5 | import { SyntaxNode } from "@lezer/common"; 6 | import { syntaxNodeToKeyPath, codeStringState } from "../utils"; 7 | import { runProjectionQuery } from "../query"; 8 | import { ProjectionInline } from "../projections"; 9 | import { SimpleWidgetStateVersion } from "../widgets"; 10 | import isEqual from "lodash.isequal"; 11 | import { cmStatePlugin } from "../cmState"; 12 | 13 | class InlineProjectionWidget extends WidgetType { 14 | widgetContainer: HTMLDivElement | null; 15 | constructor( 16 | readonly from: number, 17 | readonly to: number, 18 | readonly projection: ProjectionInline, 19 | readonly syntaxNode: SyntaxNode, 20 | readonly state: EditorState, 21 | readonly currentCodeSlice: string, 22 | readonly setCode: (code: string) => void 23 | ) { 24 | super(); 25 | this.widgetContainer = null; 26 | } 27 | 28 | eq(other: InlineProjectionWidget): boolean { 29 | // const nameTheSame = other.projection.name === this.projection.name; 30 | const codeTheSame = this.currentCodeSlice === other.currentCodeSlice; 31 | if (!isEqual(other.projection, this.projection)) { 32 | return false; 33 | } 34 | // is this wrong? 35 | return this.projection.hasInternalState ? codeTheSame : false; 36 | } 37 | 38 | toDOM(): HTMLDivElement { 39 | const wrap = document.createElement("div"); 40 | wrap.className = "cm-projection-widget position-relative"; 41 | wrap.innerText = this.currentCodeSlice; 42 | this.widgetContainer = wrap; 43 | const { schemaTypings, diagnostics } = this.state.field(cmStatePlugin); 44 | const from = this.from; 45 | const to = this.to; 46 | const element = createElement(this.projection.projection, { 47 | keyPath: syntaxNodeToKeyPath( 48 | this.syntaxNode, 49 | codeStringState(this.state, 0) 50 | ), 51 | node: this.syntaxNode, 52 | currentValue: this.currentCodeSlice, 53 | setCode: (code) => this.setCode(code), 54 | fullCode: this.state.doc.toString(), 55 | diagnosticErrors: diagnostics.filter( 56 | (x) => x.from === from && x.to === to 57 | ), 58 | typings: schemaTypings[`${from}-${to}`], 59 | cursorPositions: [...this.state.selection.ranges], 60 | }); 61 | 62 | ReactDOM.render(element, wrap); 63 | 64 | return wrap; 65 | } 66 | 67 | ignoreEvent(): boolean { 68 | return true; 69 | } 70 | destroy() { 71 | if (this.widgetContainer) { 72 | ReactDOM.unmountComponentAtNode(this.widgetContainer); 73 | this.widgetContainer = null; 74 | } 75 | } 76 | } 77 | 78 | const ProjectionWidgetFactory = ( 79 | projection: ProjectionInline, 80 | currentCodeSlice: string, 81 | syntaxNode: SyntaxNode, 82 | typings: any, 83 | setCode: (code: string) => void 84 | ): SimpleWidgetStateVersion => ({ 85 | checkForAdd: (_type, state, currentNode) => { 86 | const keyPath = syntaxNodeToKeyPath(syntaxNode, codeStringState(state, 0)); 87 | const currentCodeSlice = codeStringState( 88 | state, 89 | currentNode.from, 90 | currentNode.to 91 | ); 92 | return runProjectionQuery({ 93 | query: projection.query, 94 | keyPath, 95 | nodeValue: currentCodeSlice, 96 | typings, 97 | nodeType: currentNode.type.name, 98 | // @ts-ignore 99 | projId: projection.id, 100 | cursorPosition: state.selection.ranges[0].from, 101 | nodePos: { start: syntaxNode.from, end: syntaxNode.to }, 102 | }); 103 | }, 104 | addNode: (state, from, to) => { 105 | const widget = new InlineProjectionWidget( 106 | from, 107 | to, 108 | projection, 109 | syntaxNode, 110 | state, 111 | currentCodeSlice, 112 | setCode 113 | ); 114 | if ( 115 | projection.mode === "replace" || 116 | projection.mode === "replace-multiline" 117 | ) { 118 | return [Decoration.replace({ widget }).range(from, to)]; 119 | } else { 120 | const target = projection.mode === "prefix" ? from : to; 121 | return [Decoration.widget({ widget }).range(target)]; 122 | } 123 | }, 124 | eventSubscriptions: { 125 | mousedown: (e) => { 126 | console.log("what", e); 127 | }, 128 | }, 129 | }); 130 | export default ProjectionWidgetFactory; 131 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/Boolean.tsx: -------------------------------------------------------------------------------- 1 | import { Projection } from "../lib/projections"; 2 | import { setIn } from "../lib/utils"; 3 | const BooleanTarget: Projection = { 4 | query: { type: "nodeType", query: ["True", "False"] }, 5 | type: "inline", 6 | mode: "prefix", 7 | name: "boolean", 8 | projection: (props) => { 9 | const isChecked = props.node.type.name === "True"; 10 | return ( 11 |
12 | { 17 | props.setCode( 18 | setIn(props.keyPath, isChecked ? "false" : "true", props.fullCode) 19 | ); 20 | }} 21 | /> 22 |
23 | ); 24 | }, 25 | hasInternalState: false, 26 | }; 27 | 28 | export default BooleanTarget; 29 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/CleanUp.tsx: -------------------------------------------------------------------------------- 1 | import { Projection } from "../lib/projections"; 2 | import prettifier from "../lib/vendored/prettifier"; 3 | import { simpleParse } from "../lib/utils"; 4 | 5 | const CleanUp: Projection = { 6 | query: { type: "nodeType", query: ["Object", "Array", "[", "]", "{", "}"] }, 7 | type: "tooltip", 8 | name: "Clean Up", 9 | group: "Utils", 10 | projection: (props) => { 11 | return ( 12 | 24 | ); 25 | }, 26 | }; 27 | 28 | export default CleanUp; 29 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/ClickTarget.tsx: -------------------------------------------------------------------------------- 1 | import { Projection } from "../lib/projections"; 2 | const ClickTarget: Projection = { 3 | name: "click target", 4 | query: { type: "nodeType", query: ["[", "{"] }, 5 | type: "inline", 6 | mode: "suffix", 7 | projection: () => , 8 | hasInternalState: false, 9 | }; 10 | 11 | export default ClickTarget; 12 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/ColorChip.tsx: -------------------------------------------------------------------------------- 1 | import { Projection } from "../lib/projections"; 2 | import { colorNames, colorRegex, maybeTrim } from "../lib/utils"; 3 | 4 | const colorNameSet = new Set(Object.keys(colorNames)); 5 | const ColorChip: Projection = { 6 | // query: { type: "regex", query: colorRegex }, 7 | query: { 8 | type: "function", 9 | query: (value, type) => { 10 | if (type !== "String") { 11 | return false; 12 | } 13 | const val = maybeTrim(value.toLowerCase()); 14 | return !!(colorNameSet.has(val) || val.match(colorRegex)); 15 | }, 16 | }, 17 | type: "inline", 18 | mode: "prefix", 19 | name: "color chip", 20 | projection: (props) => { 21 | const value = maybeTrim(props.currentValue); 22 | return ( 23 |
24 | ); 25 | }, 26 | hasInternalState: false, 27 | }; 28 | 29 | export default ColorChip; 30 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/ColorNamePicker.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Projection } from "../lib/projections"; 3 | import { colorNames, setIn } from "../lib/utils"; 4 | 5 | // https://www.w3schools.com/colors/colors_groups.asp 6 | 7 | const titleCase = (x: string) => 8 | `${x[0].toUpperCase()}${x.slice(1).toLowerCase()}`; 9 | 10 | export const colorGroups: Record = { 11 | blue: [ 12 | "navy", 13 | "darkblue", 14 | "midnightblue", 15 | "mediumblue", 16 | "blue", 17 | "royalblue", 18 | "steelblue", 19 | "dodgerblue", 20 | "cadetblue", 21 | "cornflowerblue", 22 | "deepskyblue", 23 | "darkturquoise", 24 | "mediumturquoise", 25 | "lightsteelblue", 26 | "skyblue", 27 | "lightskyblue", 28 | "turquoise", 29 | "lightblue", 30 | "powderblue", 31 | "paleturquoise", 32 | "cyan", 33 | "aquamarine", 34 | "lightcyan", 35 | "aqua", 36 | ], 37 | brown: [ 38 | "maroon", 39 | "saddlebrown", 40 | "brown", 41 | "sienna", 42 | "olive", 43 | "chocolate", 44 | "darkgoldenrod", 45 | "peru", 46 | "rosybrown", 47 | "goldenrod", 48 | "sandybrown", 49 | "tan", 50 | "burlywood", 51 | "wheat", 52 | "navajowhite", 53 | "bisque", 54 | "blanchedalmond", 55 | "cornsilk", 56 | ], 57 | gray: [ 58 | "black", 59 | "darkslategray", 60 | "dimgray", 61 | "slategray", 62 | "gray", 63 | "lightslategray", 64 | "darkgray", 65 | "silver", 66 | "lightgray", 67 | "gainsboro", 68 | ], 69 | green: [ 70 | "darkgreen", 71 | "darkolivegreen", 72 | "green", 73 | "teal", 74 | "forestgreen", 75 | "seagreen", 76 | "darkcyan", 77 | "olivedrab", 78 | "mediumseagreen", 79 | "lightseagreen", 80 | "darkseagreen", 81 | "limegreen", 82 | "mediumaquamarine", 83 | "yellowgreen", 84 | "lightgreen", 85 | "mediumspringgreen", 86 | "lime", 87 | "springgreen", 88 | "lawngreen", 89 | "chartreuse", 90 | "palegreen", 91 | "greenyellow", 92 | ], 93 | purple: [ 94 | "indigo", 95 | "purple", 96 | "darkslateblue", 97 | "rebeccapurple", 98 | "darkmagenta", 99 | "darkviolet", 100 | "blueviolet", 101 | "darkorchid", 102 | "slateblue", 103 | "mediumslateblue", 104 | "mediumorchid", 105 | "mediumpurple", 106 | "magenta", 107 | "fuchsia", 108 | "orchid", 109 | "violet", 110 | "plum", 111 | "thistle", 112 | "lavender", 113 | ], 114 | red: [ 115 | "darkred", 116 | "firebrick", 117 | "mediumvioletred", 118 | "crimson", 119 | "indianred", 120 | "red", 121 | "deeppink", 122 | "orangered", 123 | "palevioletred", 124 | "tomato", 125 | "hotpink", 126 | "lightcoral", 127 | "salmon", 128 | "coral", 129 | "darkorange", 130 | "darksalmon", 131 | "lightsalmon", 132 | "orange", 133 | "lightpink", 134 | "pink", 135 | ], 136 | white: [ 137 | "mistyrose", 138 | "antiquewhite", 139 | "linen", 140 | "beige", 141 | "lavenderblush", 142 | "whitesmoke", 143 | "oldlace", 144 | "aliceblue", 145 | "seashell", 146 | "ghostwhite", 147 | "floralwhite", 148 | "honeydew", 149 | "snow", 150 | "azure", 151 | "mintcream", 152 | "ivory", 153 | "white", 154 | ], 155 | yellow: [ 156 | "darkkhaki", 157 | "gold", 158 | "peachpuff", 159 | "khaki", 160 | "palegoldenrod", 161 | "moccasin", 162 | "papayawhip", 163 | "lightgoldenrodyellow", 164 | "yellow", 165 | "lemonchiffon", 166 | "lightyellow", 167 | ], 168 | }; 169 | 170 | //stackoverflow.com/questions/1573053/javascript-function-to-convert-color-names-to-hex-codes 171 | 172 | function hexToRgb(hex: string) { 173 | const bigint = parseInt(hex, 16); 174 | const r = (bigint >> 16) & 255; 175 | const g = (bigint >> 8) & 255; 176 | const b = bigint & 255; 177 | 178 | return [r, g, b]; 179 | } 180 | 181 | export function isTooDark(color: string): boolean { 182 | // https://stackoverflow.com/a/41491220 183 | const hex = colorNames[color]; 184 | const [r, g, b] = hexToRgb(hex.slice(1)); 185 | const rgb = [r / 255, g / 255, b / 255]; 186 | const c = rgb.map((col) => { 187 | if (col <= 0.03928) { 188 | return col / 12.92; 189 | } 190 | return Math.pow((col + 0.055) / 1.055, 2.4); 191 | }); 192 | const L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; 193 | return L <= 0.179; 194 | } 195 | 196 | const initialState = Object.fromEntries( 197 | Object.keys(colorGroups).map((k) => [k, false]) 198 | ); 199 | 200 | const stripColor = (color: string) => color.slice(1, color.length - 1); 201 | 202 | function ColorNamePicker(props: { 203 | changeColor: (color: string | null) => void; 204 | initColor: string; 205 | }): JSX.Element { 206 | const { changeColor, initColor } = props; 207 | const [state, setState] = useState({ ...initialState }); 208 | 209 | return ( 210 |
211 |
    212 | {Object.entries(colorGroups).map(([groupName, colors]) => ( 213 |
  • 214 |
    215 | 218 | setState({ ...state, [groupName]: !state[groupName] }) 219 | } 220 | > 221 | {/* https://en.wikipedia.org/wiki/Geometric_Shapes */} 222 | {(state[groupName] ? "▲" : "▼") + titleCase(groupName)} 223 | 224 | 225 | {colors.map((color) => ( 226 | changeColor(color)} 230 | title={color} 231 | style={{ 232 | background: color, 233 | border: "none", 234 | // borderColor: color === initColor ? "black" : "none", 235 | }} 236 | > 237 | ))} 238 | 239 |
    240 | {state[groupName] && ( 241 |
      242 | {colors.map((color) => ( 243 |
    • changeColor(color)} 249 | style={{ 250 | background: color, 251 | color: isTooDark(color) ? "white" : "black", 252 | }} 253 | > 254 | {color} 255 |
    • 256 | ))} 257 |
    258 | )} 259 |
  • 260 | ))} 261 |
262 |
263 | ); 264 | } 265 | 266 | export const ColorNameProjection: Projection = { 267 | query: { type: "value", query: Object.keys(colorNames) }, 268 | type: "tooltip", 269 | projection: ({ keyPath, currentValue, setCode, fullCode }) => { 270 | return ( 271 | { 273 | setCode(setIn(keyPath, `"${newColor!}"`, fullCode)); 274 | }} 275 | initColor={currentValue} 276 | /> 277 | ); 278 | }, 279 | name: "Color Name Picker", 280 | group: "Color Name Picker", 281 | }; 282 | 283 | export const HexConversionProject: Projection = { 284 | query: { type: "value", query: Object.keys(colorNames) }, 285 | type: "tooltip", 286 | projection: ({ keyPath, currentValue, setCode, fullCode }) => { 287 | return ( 288 |
289 | 298 |
299 | ); 300 | }, 301 | name: "Hex Conversion", 302 | group: "Utils", 303 | }; 304 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/ColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { color as d3Color, hsl } from "d3-color"; 3 | import { setIn } from "../lib/utils"; 4 | import { Projection } from "../lib/projections"; 5 | import { colorGroups } from "./ColorNamePicker"; 6 | 7 | const colorRegex = /"#([a-fA-F0-9]){3}"$|[a-fA-F0-9]{6}"$/i; 8 | 9 | type HSLObj = { h: number; s: number; l: number }; 10 | const getHSLObject = (color: string): HSLObj => { 11 | const hslColor = hsl(d3Color(color) as any) as any; 12 | ["h", "s", "l"].forEach((key) => { 13 | hslColor[key] = hslColor[key] || 0; 14 | }); 15 | return { h: hslColor.h, s: hslColor.s, l: hslColor.l }; 16 | }; 17 | const hslObjectToHex = (hslObject: HSLObj): string => { 18 | const s = Math.round(hslObject.s * 100); 19 | const l = Math.round(hslObject.l * 100); 20 | const hslString = `hsl(${hslObject.h}, ${s}%, ${l}%)`; 21 | return hsl(hslString).formatHex(); 22 | }; 23 | 24 | const colorNames = Object.values(colorGroups).flat(); 25 | type HexObj = { r: number; g: number; b: number; opacity: number }; 26 | const hexDistance = (a: HexObj, b: HexObj) => 27 | Math.sqrt( 28 | Math.pow(a.r - b.r, 2) + Math.pow(a.g - b.g, 2) + Math.pow(a.b - b.b, 2) 29 | ); 30 | function convertToClosestNameColor(color: string) { 31 | const hex = d3Color(color) as HexObj; 32 | const bestColorName = colorNames.reduce( 33 | (acc, name) => { 34 | const color = d3Color(name) as HexObj; 35 | const distance = hexDistance(hex, color); 36 | if (distance < acc.score) { 37 | return { score: distance, name }; 38 | } 39 | return acc; 40 | }, 41 | { score: Infinity, name: "" } 42 | ); 43 | return bestColorName.name; 44 | } 45 | 46 | function ColorPicker(props: { 47 | onChange: (color: string) => void; 48 | initialColor: string; 49 | }) { 50 | const { onChange, initialColor } = props; 51 | const [color, setColor] = useState(getHSLObject(initialColor)); 52 | 53 | const hexColor = hslObjectToHex(color); 54 | return ( 55 |
56 |
57 |
58 | Old 59 |
63 |
64 |
65 | New 66 |
70 |
71 |
72 | 73 |
74 |
75 | 78 |
79 |
80 |
81 | {[ 82 | { label: "hue", key: "h", max: 360 }, 83 | { label: "saturation", key: "s", max: 1 }, 84 | { label: "lightness", key: "l", max: 1 }, 85 | ].map(({ label, max, key }) => ( 86 | 87 | 94 | setColor({ ...color, [key]: Number(e.target.value) }) 95 | } 96 | value={(color as any)[key] || 0} 97 | /> 98 | {" "} 99 | 100 | ))} 101 |
102 |
103 | ); 104 | } 105 | 106 | const ColorProjection: Projection = { 107 | query: { type: "regex", query: colorRegex }, 108 | type: "tooltip", 109 | name: "Color Picker", 110 | group: "Color Picker", 111 | projection: ({ keyPath, currentValue, setCode, fullCode }) => { 112 | return ( 113 | 115 | setCode(setIn(keyPath, `"${newColor}"`, fullCode)) 116 | } 117 | initialColor={currentValue.slice(1, currentValue.length - 1)} 118 | /> 119 | ); 120 | }, 121 | }; 122 | export default ColorProjection; 123 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/Debugger.tsx: -------------------------------------------------------------------------------- 1 | import { Projection } from "../lib/projections"; 2 | const Debugger: Projection = { 3 | type: "tooltip", 4 | name: "debug", 5 | group: "Utils", 6 | projection: (props) => { 7 | const types = (props.typings || []) 8 | .flatMap((typ) => [typ.$$labeledType, typ.type, typ.$$refName]) 9 | .filter((x) => x); 10 | return ( 11 |
12 | Node Type: "{props.node.type.name}" 13 | {`, Schema Types: ${JSON.stringify(types)}, `} 14 | KeyPath: {JSON.stringify(props.keyPath)} 15 |
16 | ); 17 | }, 18 | query: { type: "function", query: () => true }, 19 | }; 20 | 21 | export default Debugger; 22 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/HeuristicJSONFixes.tsx: -------------------------------------------------------------------------------- 1 | import { Projection } from "../lib/projections"; 2 | import { setIn } from "../lib/utils"; 3 | 4 | const quotes = new Set(['"', "'"]); 5 | const looksLikeItMightBeAQuotesError = (value: string) => { 6 | let startsOrEndsWithAQuote = false; 7 | if (quotes.has(value[0]) || quotes.has(value.at(-1) || "")) { 8 | startsOrEndsWithAQuote = true; 9 | } 10 | return startsOrEndsWithAQuote; 11 | }; 12 | const HeuristicJSONFixes: Projection = { 13 | type: "tooltip", 14 | name: "JSON Fixes", 15 | group: "Utils", 16 | projection: (props) => { 17 | const val = props.currentValue; 18 | if (looksLikeItMightBeAQuotesError(val)) { 19 | return ( 20 | 36 | ); 37 | } 38 | return <>; 39 | }, 40 | query: { 41 | type: "function", 42 | query: (value, nodeType) => { 43 | if (nodeType === "⚠" && looksLikeItMightBeAQuotesError(value)) { 44 | return true; 45 | } 46 | return false; 47 | }, 48 | }, 49 | }; 50 | 51 | export default HeuristicJSONFixes; 52 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/NumberSlider.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Projection, ProjectionProps } from "../lib/projections"; 3 | import { setIn, maybeTrim } from "../lib/utils"; 4 | 5 | // https://stackoverflow.com/questions/23917074/javascript-flooring-number-to-order-of-magnitude 6 | function orderOfMag(n: number) { 7 | return Math.floor(Math.log(Math.abs(n)) / Math.LN10 + 0.000000001); 8 | } 9 | 10 | function FancySlider(props: ProjectionProps) { 11 | const value = maybeTrim(props.currentValue) || 0; 12 | const [dragging, setDragging] = useState(false); 13 | const [val, setVal] = useState(value); 14 | const [min, setMin] = useState(0); 15 | const [max, setMax] = useState(0); 16 | const order = orderOfMag(Number(val) || 0); 17 | 18 | useEffect(() => { 19 | const localVal = maybeTrim(props.currentValue) || 0; 20 | const isNegative = Number(localVal) < 0; 21 | const localOrder = orderOfMag(Number(localVal) || 0); 22 | let localMin = Math.pow(10, localOrder - 1); 23 | let localMax = Math.pow(10, localOrder + 1); 24 | const isZero = localMin === localMax && localMax === 0; 25 | if (isZero) { 26 | localMax = 1; 27 | } 28 | if (isNegative) { 29 | const temp = localMin; 30 | localMin = -localMax; 31 | localMax = -temp; 32 | } 33 | setMax(localMax); 34 | setMin(localMin); 35 | }, [props.currentValue]); 36 | const pos = ((Number(val) - min) / (max - min)) * 90; 37 | 38 | return ( 39 |
40 |
41 |
42 | 46 | {min} 47 | 48 | 52 | {max} 53 | 54 | 58 | {val} 59 | 60 | { 68 | setDragging(false); 69 | props.setCode(setIn(props.keyPath, val, props.fullCode)); 70 | }} 71 | onMouseDown={() => setDragging(true)} 72 | onChange={(e) => { 73 | if (dragging) { 74 | setVal(e.target.value); 75 | } else { 76 | props.setCode(setIn(props.keyPath, e.target.value, props.fullCode)); 77 | } 78 | }} 79 | /> 80 |
81 | ); 82 | } 83 | 84 | const NumberSlider: Projection = { 85 | query: { type: "nodeType", query: ["Number"] }, 86 | type: "inline", 87 | mode: "prefix", 88 | name: "number slider", 89 | projection: (props) => , 90 | hasInternalState: false, 91 | }; 92 | 93 | export default NumberSlider; 94 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/SortObject.tsx: -------------------------------------------------------------------------------- 1 | import { Projection } from "../lib/projections"; 2 | import prettifier from "../lib/vendored/prettifier"; 3 | import { simpleParse, setIn } from "../lib/utils"; 4 | 5 | const SortObject: Projection = { 6 | query: { type: "nodeType", query: ["Object"] }, 7 | type: "tooltip", 8 | projection: (props) => { 9 | const value = simpleParse(props.currentValue, {}); 10 | if (value.length === 2) { 11 | return <>; 12 | } 13 | return ( 14 | 27 | ); 28 | }, 29 | name: "Sort Object", 30 | group: "Utils", 31 | }; 32 | 33 | export default SortObject; 34 | -------------------------------------------------------------------------------- /packages/prong-editor/src/projections/standard-bundle.ts: -------------------------------------------------------------------------------- 1 | import { ColorNameProjection, HexConversionProject } from "./ColorNamePicker"; 2 | import TooltipHexColorPicker from "./ColorPicker"; 3 | import { Projection } from "../lib/projections"; 4 | import ClickTarget from "./ClickTarget"; 5 | import BooleanTarget from "./Boolean"; 6 | import ColorChip from "./ColorChip"; 7 | import CleanUp from "./CleanUp"; 8 | import NumberSlider from "./NumberSlider"; 9 | import SortObject from "./SortObject"; 10 | // import Debugger from "./Debugger"; 11 | // import HeuristicJSONFixes from "./HeuristicJSONFixes"; 12 | 13 | const bundle = { 14 | BooleanTarget, 15 | CleanUp, 16 | ClickTarget, 17 | ColorChip, 18 | ConvertHex: HexConversionProject, 19 | // Debugger, 20 | // HeuristicJSONFixes, 21 | NumberSlider, 22 | SortObject, 23 | TooltipColorNamePicker: ColorNameProjection, 24 | TooltipHexColorPicker, 25 | } as Record; 26 | 27 | export default bundle; 28 | -------------------------------------------------------------------------------- /packages/prong-editor/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /packages/prong-editor/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/prong-editor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2022", "ES2020", "DOM", "DOM.Iterable"], 6 | "skipLibCheck": true, 7 | "declaration": true, 8 | 9 | /* Bundler mode */ 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | "allowSyntheticDefaultImports": true, 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": false 23 | }, 24 | "include": ["src"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /packages/prong-editor/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/prong-editor/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import path from "node:path"; 3 | import { defineConfig } from "vite"; 4 | import dts from "vite-plugin-dts"; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | react(), 9 | dts({ 10 | logDiagnostics: true, 11 | insertTypesEntry: true, 12 | }), 13 | ], 14 | build: { 15 | lib: { 16 | entry: path.resolve(__dirname, "src/index.ts"), 17 | name: "prong-editor", 18 | formats: ["es", "umd"], 19 | fileName: (format) => `prong.${format}.js`, 20 | }, 21 | rollupOptions: { 22 | external: ["react", "react-dom"], 23 | output: { 24 | globals: { 25 | react: "React", 26 | "react-dom": "ReactDOM", 27 | }, 28 | }, 29 | }, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /sites/docs/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | root: true, 5 | env: { browser: true, es2020: true }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "plugin:react-hooks/recommended", 11 | ], 12 | parser: "@typescript-eslint/parser", 13 | parserOptions: { 14 | ecmaVersion: "latest", 15 | sourceType: "module", 16 | project: true, 17 | tsconfigRootDir: __dirname, 18 | }, 19 | plugins: ["react-refresh"], 20 | rules: { 21 | "react-refresh/only-export-components": [ 22 | "warn", 23 | { allowConstantExport: true }, 24 | ], 25 | "@typescript-eslint/no-redundant-type-constituents": 0, 26 | "@typescript-eslint/no-non-null-assertion": "off", 27 | "@typescript-eslint/no-unsafe-call": "off", 28 | "@typescript-eslint/ban-ts-comment": 0, 29 | "@typescript-eslint/no-explicit-any": 0, 30 | "@typescript-eslint/no-unsafe-argument": 0, 31 | "@typescript-eslint/no-unsafe-assignment": 0, 32 | "@typescript-eslint/no-unsafe-member-access": 0, 33 | "@typescript-eslint/no-unsafe-return": 0, 34 | "@typescript-eslint/no-non-null-asserted-optional-chain": 0, 35 | "react-refresh/only-export-components": 0, 36 | "no-unused-vars": "off", 37 | "@typescript-eslint/no-empty-function": 0, 38 | "@typescript-eslint/no-unused-vars": [ 39 | "warn", // or "error" 40 | { 41 | argsIgnorePattern: "^_", 42 | varsIgnorePattern: "^_", 43 | caughtErrorsIgnorePattern: "^_", 44 | }, 45 | ], 46 | "@typescript-eslint/restrict-template-expressions": 0, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /sites/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /sites/docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prong 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sites/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prong-docs", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 8 | "preview": "vite preview", 9 | "cp-quiets": "cp ./src/examples/Quiet* ./public/", 10 | "cp-docs": "cp ../../README.md ./public/", 11 | "prep-data-prod": "mkdir -p dist/data && rm -rf dist/data && mkdir dist/data && cp ./node_modules/vega-datasets/data/* ./dist/data/", 12 | "prep-data": "rm -rf public/data && mkdir public/data && cp ./node_modules/vega-datasets/data/* ./public/data/" 13 | }, 14 | "dependencies": { 15 | "@codemirror/lang-javascript": "^6.1.4", 16 | "@codemirror/lang-json": "^6.0.1", 17 | "@json-schema-tools/traverse": "^1.10.1", 18 | "codemirror": "^6.0.1", 19 | "d3-array": "^3.2.4", 20 | "d3-scale": "^4.0.2", 21 | "d3-scale-chromatic": "^3.0.0", 22 | "d3-shape": "^3.2.0", 23 | "estree-walker": "^3.0.3", 24 | "friendly-words": "^1.2.3", 25 | "jsonc-parser": "^3.2.0", 26 | "lodash.merge": "^4.6.2", 27 | "react": "^17.0.2", 28 | "react-dnd": "^16.0.1", 29 | "react-dnd-html5-backend": "^16.0.1", 30 | "react-dom": "^17.0.2", 31 | "react-markdown": "^7.1.0", 32 | "react-router": "^6.4.2", 33 | "react-router-dom": "^6.4.2", 34 | "react-syntax-highlighter": "^15.5.0", 35 | "react-vega": "^7.6.0", 36 | "seedrandom": "^3.0.5", 37 | "vega": "^5.25.0", 38 | "vega-datasets": "^2.7.0", 39 | "vega-expression": "^5.1.0", 40 | "vega-lite": "^5.13.0", 41 | "vega-themes": "^2.13.0" 42 | }, 43 | "devDependencies": { 44 | "@testing-library/react": "^11.1.0", 45 | "@testing-library/user-event": "^12.1.10", 46 | "@types/d3": "^7.4.0", 47 | "@types/friendly-words": "^1.2.0", 48 | "@types/json-schema": "^7.0.12", 49 | "@types/lodash.merge": "^4.6.7", 50 | "@types/node": "^12.0.0", 51 | "@types/react": "^17.0.38", 52 | "@types/react-dom": "^17.0.11", 53 | "@types/react-syntax-highlighter": "^15.5.7", 54 | "@types/seedrandom": "^3.0.5", 55 | "@typescript-eslint/eslint-plugin": "^5.61.0", 56 | "@typescript-eslint/parser": "^5.61.0", 57 | "@vitejs/plugin-react-swc": "^3.3.2", 58 | "eslint": "^8.44.0", 59 | "eslint-plugin-react-hooks": "^4.6.0", 60 | "eslint-plugin-react-refresh": "^0.4.1", 61 | "typescript": "^5.0.2", 62 | "vite": "^4.4.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /sites/docs/public/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/prong/f9731444079f6477d620a13a11b1a40b652dbcfe/sites/docs/public/example.png -------------------------------------------------------------------------------- /sites/docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/prong/f9731444079f6477d620a13a11b1a40b652dbcfe/sites/docs/public/favicon.ico -------------------------------------------------------------------------------- /sites/docs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Prong 25 | 26 | 27 | 28 |
29 | 39 | <% if (process.env.REACT_APP_DISABLE_LIVE_RELOAD === "true") { %> 40 | 44 | 59 | <% } %> 60 | 61 | 62 | -------------------------------------------------------------------------------- /sites/docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/prong/f9731444079f6477d620a13a11b1a40b652dbcfe/sites/docs/public/logo.png -------------------------------------------------------------------------------- /sites/docs/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/prong/f9731444079f6477d620a13a11b1a40b652dbcfe/sites/docs/public/logo192.png -------------------------------------------------------------------------------- /sites/docs/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/prong/f9731444079f6477d620a13a11b1a40b652dbcfe/sites/docs/public/logo512.png -------------------------------------------------------------------------------- /sites/docs/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "prong-editor", 3 | "name": "prong-editor", 4 | "icons": [ 5 | { 6 | "src": "/logo512.png", 7 | "sizes": "512x512", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#ffffff", 14 | "background_color": "#3333" 15 | } -------------------------------------------------------------------------------- /sites/docs/scripts/do-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | yarn 3 | 4 | # prep assets 5 | mkdir -p public/data 6 | cp ./node_modules/vega-datasets/data/* ./public/data/ 7 | cp ./src/examples/Quiet* ./public/ 8 | cp ../../README.md ./public/ 9 | mkdir ./public/public 10 | cp ../../example.png ./public/ 11 | 12 | # swap to using the published version of prong edtior 13 | yarn add prong-editor && 14 | npx vite build && 15 | yarn remove prong-editor -------------------------------------------------------------------------------- /sites/docs/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | overflow: hidden; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | 16 | 17 | 18 | 19 | /* We have our own custom color picker button */ 20 | .flex { 21 | display: flex; 22 | } 23 | 24 | .flex-down { 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | 29 | .position-relative { 30 | position: relative; 31 | } 32 | 33 | 34 | 35 | .centering { 36 | align-items: center; 37 | justify-content: center; 38 | } 39 | 40 | .space-between { 41 | justify-content: space-between; 42 | } 43 | 44 | 45 | 46 | 47 | .App, .styler-app, .editor-container, .tracery-app-root, .vl-demo, #quiet-root { 48 | overflow: auto; 49 | flex: 1; 50 | } 51 | 52 | /* EDITOR STUFF */ 53 | 54 | .editor-container { 55 | position: relative; 56 | } 57 | 58 | .editor-target { 59 | height: 100%; 60 | } 61 | 62 | 63 | body, html, #root, .proot { 64 | height: 100%; 65 | } 66 | 67 | .proot .link-container h1 { 68 | left: 29%; 69 | top: 51px; 70 | color: white; 71 | text-decoration: none; 72 | } 73 | 74 | #header { 75 | background: #333; 76 | color: white; 77 | padding: 10px 20px; 78 | display: flex; 79 | flex-direction: row; 80 | justify-content: space-between; 81 | } 82 | 83 | .root { 84 | display: flex; 85 | flex-direction: column; 86 | height: 100%; 87 | padding: 50px; 88 | width: 100%; 89 | } 90 | 91 | .root img { 92 | max-width: 100%; 93 | } 94 | 95 | .link-container img { 96 | width: 300px; 97 | } 98 | 99 | .md-container { 100 | max-width: 600px; 101 | height: calc(100% - 100px); 102 | overflow-y: scroll; 103 | } 104 | 105 | .md-container a, .md-container a:visited { 106 | color: #009979; 107 | } 108 | 109 | .link-container a:visited, .link-container a, #header a, #header a:visited { 110 | color: white; 111 | } 112 | 113 | .link-container { 114 | align-items: center; 115 | background: #222222ef; 116 | color: white; 117 | height: 100%; 118 | width: 320px; 119 | padding: 10px; 120 | display: flex; 121 | flex-direction: column; 122 | position: relative; 123 | text-align: center; 124 | } 125 | 126 | .cm-editor { 127 | height: 100% 128 | } 129 | 130 | .cm-scroller { 131 | overflow: auto 132 | } 133 | 134 | 135 | 136 | /* TODO make more specific */ 137 | div[tabindex="-1"]:focus { 138 | outline: 0; 139 | } 140 | 141 | .inner-link-container { 142 | margin-top: 15px; 143 | display: flex; 144 | flex-direction: column; 145 | align-items: center; 146 | } 147 | 148 | .explanation-container { 149 | margin-top: 30px; 150 | display: flex; 151 | flex-direction: column; 152 | align-items: center 153 | } 154 | 155 | /* Handle Mobile stuff */ 156 | 157 | .mobile-proot { 158 | display: none; 159 | width: 100%; 160 | overflow-x: none; 161 | overflow-y: scroll; 162 | height: 100%; 163 | } 164 | 165 | .mobile-proot .root { 166 | padding: 20px; 167 | width: calc(100% - 40px); 168 | } 169 | 170 | .mobile-header { 171 | display: flex; 172 | flex-direction: row; 173 | justify-content: space-between; 174 | align-items: center; 175 | padding: 10px 20px; 176 | background: #333; 177 | color: white; 178 | } 179 | 180 | .mobile-header a:visited, .mobile-header a { 181 | color: white; 182 | } 183 | 184 | @media (max-width: 600px) { 185 | .proot, #cm-monocle { 186 | display: none !important; 187 | } 188 | 189 | .mobile-proot { 190 | display: block 191 | } 192 | } -------------------------------------------------------------------------------- /sites/docs/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { HashRouter, Route, Routes, Link } from "react-router-dom"; 4 | import ReactMarkdown from "react-markdown"; 5 | 6 | import "../../../packages/prong-editor/src/stylesheets/style.css"; 7 | 8 | import "./App.css"; 9 | import VegaLiteExampleApp from "./examples/VegaLiteDebug"; 10 | import SimpleExample from "./examples/SimpleExample"; 11 | import ProduceExample from "./examples/ProduceExample"; 12 | import InSituVis from "./examples/InSituVis"; 13 | import VegaLiteStyler from "./examples/VegaLiteStyler"; 14 | import Tracery from "./examples/TraceryExample"; 15 | import VegaLiteUseCase from "./examples/VegaLiteUseCase"; 16 | import VegaUseCase from "./examples/VegaUseCase"; 17 | import QuietModeCompare from "./examples/QuietModeCompare"; 18 | 19 | import StyledMarkdown from "./examples/StyledMarkdown"; 20 | 21 | const routes: { 22 | name: string; 23 | Component: () => JSX.Element; 24 | zone: "Case Studies" | "Debugging" | "Examples"; 25 | explanation: string; 26 | }[] = [ 27 | { 28 | name: "vega-lite", 29 | Component: VegaLiteUseCase, 30 | zone: "Case Studies", 31 | explanation: 32 | "This case study explores adding views that are similar to familiar GUI applications, such at Tableau and Excel, but adapated to the particular domain of [Vega-Lite](https://vega.github.io/vega-lite/examples/). On particular display here are the editable data table (which will modify the data underneath) and the Tableau-style drag-and-drop of data columns onto encoding drop targets. ", 33 | }, 34 | { 35 | name: "vega", 36 | Component: VegaUseCase, 37 | zone: "Case Studies", 38 | explanation: 39 | "This case study explore adding functionality to [Vega](https://vega.github.io/vega/examples/). Most prominent here are the sparklines (the inline charts decorated through the code) as well as the signal editor, which allows for more dynamic interaction with [Vega Expressions](https://vega.github.io/vega/docs/expressions/)", 40 | }, 41 | { 42 | name: "produce", 43 | Component: ProduceExample, 44 | zone: "Examples", 45 | explanation: 46 | "This simple example looks at a toy schema for organizing some fruits and vegetables. It demonstrates how views can be used to to parts of the code, as well as how they can be used to modify elements (here removing all the unnecessary double quotes). ", 47 | }, 48 | { 49 | name: "vega-lite-debugging", 50 | Component: VegaLiteExampleApp, 51 | zone: "Debugging", 52 | explanation: 53 | "This is a debugging example meant to help with the development of the system. It shows how views can have state and in several different fashions.", 54 | }, 55 | 56 | { 57 | name: "simple", 58 | Component: SimpleExample, 59 | zone: "Debugging", 60 | explanation: 61 | "This is a debugging example that shows a bunch of basic data types (booleans, numbers, etc), which makes it easy to see the way in which the standard bundle interacts with those components. ", 62 | }, 63 | { 64 | name: "in-situ-vis", 65 | Component: InSituVis, 66 | zone: "Examples", 67 | explanation: 68 | "This is an alternate version of the Vega Case Study that allows the user to fiddle with the rendering modes and positioning of the sparklines. The in-situ paper describes a number of additional modes and placements, and while these are valuable they are not fundamentally different from what is presented here, and so we forwent them as an implementation detail.", 69 | }, 70 | { 71 | name: "vega-styler", 72 | Component: VegaLiteStyler, 73 | zone: "Case Studies", 74 | explanation: 75 | "This case study explores the [Vega Configuration language](https://vega.github.io/vega/docs/config/), which is similar to a CSS file but for charts. It explores the role of search in supporting DSL. The notable feature that is shown here through a custom view is the doc search, which allows you to type in a term you might want to use (perhaps scheme) or might be close to a term you want and the populates suggestions into the text editor. You can dismiss them (X) or accept them (check mark). When you are done click 'dismiss suggestions' to return to normal editing. ", 76 | }, 77 | { 78 | name: "tracery", 79 | Component: Tracery, 80 | zone: "Case Studies", 81 | explanation: 82 | "This example looks at the procedural generative language [Tracery](https://tracery.io/), which supports automatic narrative generation for contexts like [twitter bots](https://cheapbotsdonequick.com/) (rip). The rules define a simple generative grammar which is then randomly unfolded to get a given iteration. The custom views here support some highlighting which elements were drawn on as well as a debugging view inspired by the [Tracery Editor](http://tracery.io/editor/). This example also supports limited bidirectional manipulation, such that the you can modify the output and have those edits propagate to the grammar.", 83 | }, 84 | { 85 | name: "quiet-mode", 86 | Component: QuietModeCompare, 87 | zone: "Examples", 88 | explanation: 89 | 'This example compares how long a minimal instantiation of the "quiet mode" view (which removes double quotes) is in both Prong as well as base Code Mirror. ', 90 | }, 91 | ]; 92 | 93 | function Root(props: { mobileWarning: boolean }) { 94 | const [docs, setDocs] = useState(""); 95 | 96 | useEffect(() => { 97 | fetch("./README.md") 98 | .then((x) => x.text()) 99 | .then((x) => { 100 | setDocs(x); 101 | }) 102 | .catch((e) => console.error(e)); 103 | }, []); 104 | 105 | let modifiedDocs = docs; 106 | if (props.mobileWarning) { 107 | modifiedDocs = modifiedDocs.replaceAll( 108 | "# Prong", 109 | ` 110 | # Prong 111 | 112 | ⚠️ ⚠️ This site and tool are not optimized for mobile. Please view on a desktop. ⚠️ ⚠️ 113 | ` 114 | ); 115 | } 116 | return ( 117 |
118 |
119 | 120 |
121 |
122 | ); 123 | } 124 | 125 | function Explanation(props: { explanation: string }) { 126 | const { explanation } = props; 127 | if (!explanation) { 128 | return <>; 129 | } 130 | return ( 131 |
132 |

Example Explanation

133 |
134 | {/* @ts-ignore */} 135 | {explanation} 136 |
137 |
138 | ); 139 | } 140 | 141 | function MobilePage() { 142 | return ( 143 |
144 |
145 |
Prong
146 |
147 | GitHub 148 |
149 |
150 | 151 |
152 | ); 153 | } 154 | 155 | function App() { 156 | const groups = routes.reduce((acc, row) => { 157 | acc[row.zone] = (acc[row.zone] || []).concat(row); 158 | return acc; 159 | }, {} as Record); 160 | return ( 161 | 162 | 163 | 164 |
165 |
166 | {/* @ts-ignore */} 167 | 168 | logo for prong. black and white bird face surrounded by white splatters. 172 | 173 |

Prong

174 |
175 | GitHub 176 | Paper 177 |
178 | {Object.entries(groups).map(([name, groupRoutes]) => { 179 | return ( 180 |
181 |

{name}

182 | {groupRoutes.map(({ name }) => ( 183 |
184 | {/* @ts-ignore */} 185 | {name} 186 |
187 | ))} 188 |
189 | ); 190 | })} 191 | {/* @ts-ignore */} 192 | 193 | {routes.map(({ name, explanation }) => ( 194 | //@ts-ignore 195 | } 197 | path={name} 198 | key={name} 199 | /> 200 | ))} 201 | 202 |
203 | {/* @ts-ignore */} 204 | 205 | {routes.map(({ name, Component }) => ( 206 | // @ts-ignore 207 | } path={name} key={name} /> 208 | ))} 209 | {/* @ts-ignore */} 210 | } path="/" /> 211 | 212 |
213 |
214 | ); 215 | } 216 | 217 | export default App; 218 | -------------------------------------------------------------------------------- /sites/docs/src/constants/tracery-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$ref": "#/definitions/Tracery", 4 | "definitions": { 5 | "Tracery": { 6 | "type": "object", 7 | "additionalProperties": { 8 | "$ref": "#/definitions/Descriptor" 9 | }, 10 | "properties": { 11 | "origin": { 12 | "$ref": "#/definitions/Descriptor" 13 | } 14 | }, 15 | "required": [ 16 | "origin" 17 | ], 18 | "title": "Tracery" 19 | }, 20 | "Descriptor": { 21 | "type": "array", 22 | "items": { 23 | "type": "string" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /sites/docs/src/examples/DataTable.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { 3 | ProjectionProps, 4 | utils, 5 | } from "../../../../packages/prong-editor/src/index"; 6 | import { extractFieldNames, Table } from "./example-utils"; 7 | 8 | const letters = ["penguins", "flowers", "wheat", "squids", "dough", "bags"]; 9 | 10 | function RenderCell(props: { 11 | contents: string; 12 | onUpdate: (newValue: string | number) => void; 13 | }) { 14 | const { contents, onUpdate } = props; 15 | const [active, setActive] = useState(false); 16 | const [currentValue, setCurrentValue] = useState(contents); 17 | if (!active) { 18 | return setActive(true)}>{contents}; 19 | } 20 | return ( 21 | 22 | setCurrentValue(e.target.value)} 26 | /> 27 |
{ 29 | setActive(false); 30 | onUpdate( 31 | isNaN(currentValue as any) ? currentValue : Number(currentValue) 32 | ); 33 | }} 34 | > 35 | ✓ 36 |
37 | 38 | ); 39 | } 40 | 41 | function sortData( 42 | data: Table, 43 | sortedBy: string | false, 44 | forwardSort: boolean 45 | ): Table { 46 | return data.sort((a, b) => { 47 | if (!sortedBy) { 48 | return 0; 49 | } 50 | const aVal = a[sortedBy]; 51 | const bVal = b[sortedBy]; 52 | return ( 53 | (forwardSort ? 1 : -1) * 54 | (typeof aVal === "string" 55 | ? aVal.localeCompare(bVal) 56 | : (aVal as number) - (bVal as number)) 57 | ); 58 | }); 59 | } 60 | 61 | const PAGE_SIZE = 5; 62 | function DataTableComponent(props: { 63 | data: Table; 64 | updateData: (newData: Table) => void; 65 | hideTable: () => void; 66 | }) { 67 | const { data, updateData, hideTable } = props; 68 | const keys: string[] = extractFieldNames(data); 69 | const [visibleData, setVisibleData] = useState([]); 70 | const [page, setPage] = useState(0); 71 | const [sortedBy, setSortedBy] = useState(false); 72 | const [forwardSort, setForwardSort] = useState(true); 73 | const [maxPages, setMaxPages] = useState(0); 74 | useEffect(() => { 75 | setMaxPages(Math.floor(data.length / PAGE_SIZE)); 76 | setPage(0); 77 | }, [data]); 78 | useEffect(() => { 79 | setVisibleData( 80 | sortData(data, sortedBy, forwardSort).slice( 81 | page * PAGE_SIZE, 82 | (page + 1) * PAGE_SIZE 83 | ) 84 | ); 85 | }, [data, page, sortedBy, forwardSort]); 86 | return ( 87 |
88 |
89 | 90 | 91 | 97 | {keys.map((key, idx) => ( 98 | 111 | ))} 112 | 120 | 126 | 127 | 128 | 129 | {visibleData.map((row, idx) => ( 130 | 131 | 132 | {keys.map((key, jdx) => ( 133 | { 137 | updateData( 138 | setValueInTable(data, idx + page * PAGE_SIZE, key, x) 139 | ); 140 | }} 141 | /> 142 | ))} 143 | 144 | 145 | 146 | ))} 147 | {/* pad to keep a consistent height */} 148 | {[...new Array(Math.max(0, PAGE_SIZE - visibleData.length))].map( 149 | (_, idx) => { 150 | return ( 151 | 152 | 153 | 154 | ); 155 | } 156 | )} 157 | 158 | 159 | 160 | 168 | 169 | 170 |
page > 0 && setPage(page - 1)} 94 | > 95 | ◀ 96 | { 101 | if (key === sortedBy) { 102 | setForwardSort(!forwardSort); 103 | } else { 104 | setSortedBy(key); 105 | } 106 | }} 107 | > 108 | {key} 109 | {sortedBy === key ? (forwardSort ? "▼" : "▲") : ""} 110 | { 114 | const newKey = letters[keys.length]; 115 | updateData(data.map((row) => ({ ...row, [newKey]: 0 }))); 116 | }} 117 | > 118 | + 119 | page < maxPages && setPage(page + 1)} 123 | > 124 | ▶ 125 |
{"\t"}
{ 162 | updateData(sortData(data, sortedBy, forwardSort)); 163 | hideTable(); 164 | }} 165 | > 166 | Hide 167 |
171 |
172 | ); 173 | } 174 | 175 | function setValueInTable( 176 | table: Table, 177 | rowNumber: number, 178 | key: string, 179 | newVal: string | number 180 | ): Table { 181 | return table.map((row, idx) => { 182 | if (idx !== rowNumber) { 183 | return row; 184 | } 185 | return { ...row, [key]: newVal }; 186 | }); 187 | } 188 | 189 | interface ExtendedProjectionProps extends ProjectionProps { 190 | externalUpdate: (code: string) => void; 191 | hideTable: () => void; 192 | } 193 | 194 | export default function DataTable(props: ExtendedProjectionProps): JSX.Element { 195 | const { externalUpdate, keyPath, fullCode, hideTable } = props; 196 | const parsed = utils.simpleParse(props.currentValue); 197 | if (!Array.isArray(parsed) || !parsed.length) { 198 | return
Loading data table...
; 199 | } else { 200 | return ( 201 | { 205 | externalUpdate( 206 | utils.setIn(keyPath, utils.prettifier(newData), fullCode) 207 | ); 208 | }} 209 | /> 210 | ); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /sites/docs/src/examples/InSituVis.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { 3 | Editor, 4 | StandardBundle, 5 | } from "../../../../packages/prong-editor/src/index"; 6 | import VegaSchema from "../constants/vega-schema.json"; 7 | import { analyzeVegaCode } from "./example-utils"; 8 | import { 9 | SparkType, 10 | sparkTypes, 11 | sparkPositions, 12 | SparkPos, 13 | PreComputedHistograms, 14 | createHistograms, 15 | DataTable, 16 | isDataTable, 17 | buildSparkProjection, 18 | } from "./histograms"; 19 | 20 | const connectedScatterPlotSpec = `{ 21 | "marks": [ 22 | { 23 | "type": "text", 24 | "from": {"data": "drive"}, 25 | "encode": { 26 | "enter": { 27 | "x": {"scale": "x", "field": "miles"}, 28 | "y": {"scale": "y", "field": "gas"}, 29 | "dx": {"scale": "dx", "field": "side"}, 30 | "dy": {"scale": "dy", "field": "side"}, 31 | "fill": {"value": "#000"}, 32 | "text": {"field": "year"}, 33 | "align": {"scale": "align", "field": "side"}, 34 | "baseline": {"scale": "base", "field": "side"} 35 | } 36 | } 37 | } 38 | ], 39 | "$schema": "https://vega.github.io/schema/vega/v3.0.json", 40 | "width": 800, 41 | "height": 500, 42 | "padding": 5, 43 | 44 | "data": [{ "name": "drive", "url": "data/driving.json"}], 45 | "scales": [ 46 | { 47 | "name": "x", 48 | "type": "linear", 49 | "domain": {"data": "drive", "field": "miles"}, 50 | "range": "width", 51 | "nice": true, 52 | "zero": false, 53 | "round": true 54 | }, 55 | { 56 | "name": "y", 57 | "type": "linear", 58 | "domain": {"data": "drive", "field": "gas"}, 59 | "range": "height", 60 | "nice": true, 61 | "zero": false, 62 | "round": true 63 | }, 64 | { 65 | "name": "align", 66 | "type": "ordinal", 67 | "domain": ["left", "right", "top", "bottom"], 68 | "range": ["right", "left", "center", "center"] 69 | }, 70 | { 71 | "name": "base", 72 | "type": "ordinal", 73 | "domain": ["left", "right", "top", "bottom"], 74 | "range": ["middle", "middle", "bottom", "top"] 75 | }, 76 | { 77 | "name": "dx", 78 | "type": "ordinal", 79 | "domain": ["left", "right", "top", "bottom"], 80 | "range": [-7, 6, 0, 0] 81 | }, 82 | { 83 | "name": "dy", 84 | "type": "ordinal", 85 | "domain": ["left", "right", "top", "bottom"], 86 | "range": [1, 1, -5, 6] 87 | } 88 | ] 89 | }`; 90 | 91 | function InSituFigure1() { 92 | const [currentCode, setCurrentCode] = useState(connectedScatterPlotSpec); 93 | const [sparkPosition, setSparkPosition] = useState("right"); 94 | const [sparkType, setSparkType] = useState("line"); 95 | const [preComputedHistograms, setPrecomputedHistograms] = 96 | useState({}); 97 | 98 | useEffect(() => { 99 | analyzeVegaCode(currentCode, ({ data }) => { 100 | const namedPairs = Object.entries(data) 101 | .filter(([_key, dataSet]) => isDataTable(dataSet)) 102 | .map(([key, data]) => [key, createHistograms(data as DataTable)]); 103 | setPrecomputedHistograms(Object.fromEntries(namedPairs)); 104 | }); 105 | }, [currentCode]); 106 | 107 | return ( 108 |
109 |
110 |

Sparkline Config

111 |
112 |
113 | 114 | 124 |
125 |
126 | 127 | 137 |
138 |
139 |
140 | setCurrentCode(x)} 144 | projections={[ 145 | ...Object.values(StandardBundle), 146 | buildSparkProjection(preComputedHistograms, sparkPosition, sparkType), 147 | ]} 148 | />{" "} 149 |
150 | ); 151 | } 152 | 153 | export default InSituFigure1; 154 | -------------------------------------------------------------------------------- /sites/docs/src/examples/ProduceExample.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { 3 | Editor, 4 | StandardBundle, 5 | Projection, 6 | utils, 7 | } from "../../../../packages/prong-editor/src/index"; 8 | import { produceSchema, produceExample } from "./example-data"; 9 | 10 | const blue = "#0551A5"; 11 | const green = "#17885C"; 12 | const red = "#A21615"; 13 | const coloring: Record = { 14 | String: blue, 15 | Number: green, 16 | Boolean: blue, 17 | PropertyName: red, 18 | Null: blue, 19 | }; 20 | 21 | const DestringProjection: Projection = { 22 | type: "inline", 23 | mode: "replace", 24 | name: "destring", 25 | query: { 26 | type: "nodeType", 27 | query: ["PropertyName", "Number", "String", "Null", "False", "True"], 28 | }, 29 | projection: (props) => { 30 | const val = utils.maybeTrim(props.currentValue); 31 | return ( 32 |
38 | {val.length ? val : '""'} 39 |
40 | ); 41 | }, 42 | hasInternalState: false, 43 | }; 44 | 45 | const HideMeta: Projection = { 46 | type: "inline", 47 | name: "hide meta", 48 | mode: "replace", 49 | query: { type: "index", query: ["meta"] }, 50 | projection: () =>
, 51 | hasInternalState: false, 52 | }; 53 | 54 | function ProduceExample() { 55 | const [currentCode, setCurrentCode] = useState(produceExample); 56 | 57 | return ( 58 | setCurrentCode(x)} 62 | projections={[ 63 | ...Object.values(StandardBundle), 64 | DestringProjection, 65 | HideMeta, 66 | ]} 67 | /> 68 | ); 69 | } 70 | 71 | export default ProduceExample; 72 | -------------------------------------------------------------------------------- /sites/docs/src/examples/QuietModeCodeMirror.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { syntaxTree } from "@codemirror/language"; 3 | import { WidgetType } from "@codemirror/view"; 4 | import { json } from "@codemirror/lang-json"; 5 | import { basicSetup } from "codemirror"; 6 | import { 7 | EditorView, 8 | ViewUpdate, 9 | ViewPlugin, 10 | DecorationSet, 11 | Decoration, 12 | } from "@codemirror/view"; 13 | import { EditorState, Range } from "@codemirror/state"; 14 | 15 | const coloring: Record = { 16 | String: "#0551A5", 17 | Number: "#17885C", 18 | Boolean: "#0551A5", 19 | PropertyName: "#A21615", 20 | Null: "#0551A5", 21 | }; 22 | 23 | const trim = (x: string) => 24 | x.at(0) === '"' && x.at(-1) === '"' ? x.slice(1, x.length - 1) : x; 25 | 26 | function QuietModeCodeMirror(props: { 27 | onChange: (code: string) => void; 28 | code: string; 29 | }) { 30 | const { code, onChange } = props; 31 | const cmParent = useRef(null); 32 | 33 | const [view, setView] = useState(null); 34 | useEffect(() => { 35 | const localExtension = EditorView.updateListener.of((v: ViewUpdate) => { 36 | if (v.docChanged) { 37 | onChange(v.state.doc.toString()); 38 | } 39 | }); 40 | 41 | const editorState = EditorState.create({ 42 | extensions: [basicSetup, quietMode, json(), localExtension], 43 | doc: code, 44 | })!; 45 | const view = new EditorView({ 46 | state: editorState, 47 | parent: cmParent.current, 48 | }); 49 | setView(view); 50 | return () => view.destroy(); 51 | // eslint-disable-next-line 52 | }, []); 53 | 54 | useEffect(() => { 55 | if (view && view.state.doc.toString() !== code) { 56 | view.dispatch( 57 | view.state.update({ 58 | changes: { from: 0, to: view.state.doc.length, insert: code }, 59 | }) 60 | ); 61 | } 62 | }, [code, view]); 63 | return ( 64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | class QuietWidget extends WidgetType { 71 | constructor( 72 | readonly content: string, 73 | readonly nodeType: string 74 | ) { 75 | super(); 76 | } 77 | 78 | eq(other: QuietWidget): boolean { 79 | return this.content === other.content; 80 | } 81 | 82 | toDOM(): HTMLDivElement { 83 | const wrap = document.createElement("div"); 84 | wrap.setAttribute( 85 | "style", 86 | `display: inline-block; color:${coloring[this.nodeType]}` 87 | ); 88 | wrap.innerText = trim(this.content); 89 | return wrap; 90 | } 91 | } 92 | const quietMode = ViewPlugin.fromClass( 93 | class { 94 | decorations: DecorationSet; 95 | constructor(view: EditorView) { 96 | this.decorations = this.makeQuietRepresentation(view); 97 | } 98 | 99 | makeQuietRepresentation(view: EditorView) { 100 | const widgets: Range[] = []; 101 | const code = view.state.doc.sliceString(0); 102 | syntaxTree(view.state).iterate({ 103 | from: 0, 104 | to: code.length, 105 | enter: ({ node, from, to, type }) => { 106 | if (coloring[node.type.name]) { 107 | const widget = new QuietWidget(code.slice(from, to), type.name); 108 | widgets.push(Decoration.replace({ widget }).range(from, to)); 109 | } 110 | }, 111 | }); 112 | try { 113 | return Decoration.set(widgets.sort((a, b) => a.from - b.from)); 114 | } catch (e) { 115 | console.log("problem creating widgets", e); 116 | return Decoration.set([]); 117 | } 118 | } 119 | 120 | update(update: ViewUpdate) { 121 | if (update.docChanged || update.viewportChanged) { 122 | this.decorations = this.makeQuietRepresentation(update.view); 123 | } 124 | } 125 | }, 126 | { decorations: (v) => v.decorations } 127 | ); 128 | 129 | export default QuietModeCodeMirror; 130 | -------------------------------------------------------------------------------- /sites/docs/src/examples/QuietModeCompare.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import QuietModeUs from "./QuietModeUs"; 3 | import QuietModeCodeMirror from "./QuietModeCodeMirror"; 4 | import { produceExample } from "./example-data"; 5 | import "../stylesheets/quiet-styles.css"; 6 | import StyledMarkdown from "./StyledMarkdown"; 7 | 8 | function QuietModeCompare() { 9 | const [code, setCode] = useState(produceExample); 10 | const [examples, setExamples] = useState({ codeMirror: "", us: "" }); 11 | useEffect(() => { 12 | Promise.all( 13 | ["QuietModeCodeMirror.tsx", "QuietModeUs.tsx"].map((el) => 14 | fetch(el) 15 | .then((x) => x.text()) 16 | .then((x) => { 17 | return ( 18 | "```tsx" + 19 | x 20 | .replace( 21 | /\n/g, 22 | ` 23 | ` 24 | ) 25 | .split("\n") 26 | .map((x) => x.slice(8)) 27 | .join("\n") 28 | ); 29 | }) 30 | ) 31 | ) 32 | .then(([codeMirror, us]) => setExamples({ codeMirror, us })) 33 | .catch((e) => console.error(e)); 34 | }, []); 35 | return ( 36 |
37 |
46 |

Prong

47 |

Lines of code {examples.us.split("\n").length}

48 |
49 | 50 |
51 |

Raw

52 |
58 | 59 |
60 |
61 |
69 |

Vanilla Code Mirror

70 |

Lines of code {examples.codeMirror.split("\n").length}

71 |
72 | 73 |
74 |

Raw

75 |
81 | 82 |
83 |
84 |
85 | ); 86 | } 87 | 88 | export default QuietModeCompare; 89 | -------------------------------------------------------------------------------- /sites/docs/src/examples/QuietModeUs.tsx: -------------------------------------------------------------------------------- 1 | import { Editor } from "../../../../packages/prong-editor/src/index"; 2 | import { produceSchema } from "./example-data"; 3 | 4 | const coloring: Record = { 5 | String: "#0551A5", 6 | Number: "#17885C", 7 | Boolean: "#0551A5", 8 | PropertyName: "#A21615", 9 | Null: "#0551A5", 10 | }; 11 | const nodeTypes = ["PropertyName", "Number", "String", "Null", "False", "True"]; 12 | 13 | const trim = (x: string) => 14 | x.at(0) === '"' && x.at(-1) === '"' ? x.slice(1, x.length - 1) : x; 15 | 16 | const QuietModeUs = (props: { 17 | onChange: (code: string) => void; 18 | code: string; 19 | }) => ( 20 | ( 34 |
35 | {trim(props.currentValue).length ? trim(props.currentValue) : '""'} 36 |
37 | ), 38 | hasInternalState: false, 39 | }, 40 | ]} 41 | /> 42 | ); 43 | 44 | export default QuietModeUs; 45 | -------------------------------------------------------------------------------- /sites/docs/src/examples/RandomWord.tsx: -------------------------------------------------------------------------------- 1 | import { utils, Projection } from "../../../../packages/prong-editor/src/index"; 2 | import friendlyWords from "friendly-words"; 3 | 4 | const titleCase = (word: string) => `${word[0].toUpperCase()}${word.slice(1)}`; 5 | const pick = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)]; 6 | 7 | // borrowed from the p5 editor 8 | function generateName() { 9 | const adj = pick(friendlyWords.predicates); 10 | const obj = titleCase(pick(friendlyWords.objects)); 11 | return `${adj}${obj}`; 12 | } 13 | 14 | const RandomWordProjection: Projection = { 15 | query: { type: "regex", query: /".*"/ }, 16 | type: "tooltip", 17 | name: "Random Word", 18 | group: "Utils", 19 | projection: ({ keyPath, setCode, fullCode }) => { 20 | return ( 21 | 28 | ); 29 | }, 30 | }; 31 | 32 | export default RandomWordProjection; 33 | -------------------------------------------------------------------------------- /sites/docs/src/examples/SimpleExample.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import { 4 | Editor, 5 | StandardBundle, 6 | } from "../../../../packages/prong-editor/src/index"; 7 | 8 | const exampleData = `{ 9 | "a": { 10 | "b": [1, 2, 3], 11 | "c": true, 12 | }, 13 | "d": null, 14 | "e": [{ "f": -4, "g": 0 }], 15 | "I": "example", 16 | }`; 17 | 18 | function SimpleExample() { 19 | const [currentCode, setCurrentCode] = useState(exampleData); 20 | 21 | return ( 22 | { 26 | setCurrentCode(x); 27 | }} 28 | projections={Object.values(StandardBundle)} 29 | /> 30 | ); 31 | } 32 | 33 | export default SimpleExample; 34 | -------------------------------------------------------------------------------- /sites/docs/src/examples/StyledMarkdown.tsx: -------------------------------------------------------------------------------- 1 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 2 | import { xonokai } from "react-syntax-highlighter/dist/esm/styles/prism"; 3 | import ReactMarkdown from "react-markdown"; 4 | 5 | const MaskedMarkdown = ReactMarkdown as any; 6 | const MaskedHighlight = SyntaxHighlighter as any; 7 | function StyledMarkdown(props: { content: string }) { 8 | // @ts-ignore 9 | return ( 10 | 21 | ) : ( 22 | 23 | {children} 24 | 25 | ); 26 | }, 27 | }} 28 | > 29 | {props.content} 30 | 31 | ); 32 | } 33 | export default StyledMarkdown; 34 | -------------------------------------------------------------------------------- /sites/docs/src/examples/VegaExpressionEditor.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | import { javascript } from "@codemirror/lang-javascript"; 4 | import { basicSetup } from "codemirror"; 5 | import { 6 | EditorView, 7 | ViewUpdate, 8 | ViewPlugin, 9 | DecorationSet, 10 | Decoration, 11 | } from "@codemirror/view"; 12 | import { EditorState, Range } from "@codemirror/state"; 13 | import { parser } from "@lezer/javascript"; 14 | 15 | import * as vegaExpression from "vega-expression"; 16 | import { walk } from "estree-walker"; 17 | 18 | import { 19 | autocompletion, 20 | CompletionSource, 21 | snippet, 22 | } from "@codemirror/autocomplete"; 23 | 24 | export type SchemaMap = Record; 25 | // vegaExpression. 26 | // javascript. 27 | function tryExpression(code: string, signals: string[]): null | string { 28 | const signalSet = new Set(signals); 29 | try { 30 | walk(vegaExpression.parseExpression(code), { 31 | enter(node, parent) { 32 | const parentIsProperty = parent && parent.type === "MemberExpression"; 33 | if ( 34 | node.type === "Identifier" && 35 | !parentIsProperty && // to catch the padding.top type cases 36 | !(termSet.has(node.name) || signalSet.has(node.name)) 37 | ) { 38 | throw new Error(`${node.name} is not a recognized keyword`); 39 | } 40 | }, 41 | }); 42 | } catch (e) { 43 | return `${e}`; 44 | } 45 | return null; 46 | } 47 | 48 | export default function Editor(props: { 49 | onChange: (code: string) => void; 50 | code: string; 51 | terms: string[]; 52 | onError: (errorMessage: string | null) => void; 53 | }) { 54 | const { code, onChange, terms, onError } = props; 55 | const cmParent = useRef(null); 56 | 57 | const [view, setView] = useState(null); 58 | // primary effect, initialize the editor etc 59 | useEffect(() => { 60 | const localExtension = EditorView.updateListener.of((v: ViewUpdate) => { 61 | if (v.docChanged) { 62 | const newCode = v.state.doc.toString(); 63 | onChange(newCode); 64 | onError(tryExpression(newCode, props.terms)); 65 | } 66 | }); 67 | 68 | const editorState = EditorState.create({ 69 | extensions: [ 70 | basicSetup, 71 | analogSyntaxHighlighter, 72 | javascript(), 73 | localExtension, 74 | autocomplete(terms), 75 | ], 76 | doc: code, 77 | })!; 78 | const view = new EditorView({ 79 | state: editorState, 80 | parent: cmParent.current, 81 | }); 82 | setView(view); 83 | return () => view.destroy(); 84 | // eslint-disable-next-line 85 | }, []); 86 | 87 | useEffect(() => { 88 | if (view && view.state.doc.toString() !== code) { 89 | view.dispatch( 90 | view.state.update({ 91 | changes: { from: 0, to: view.state.doc.length, insert: code }, 92 | }) 93 | ); 94 | } 95 | }, [code, view]); 96 | return ( 97 |
98 |
99 |
100 | ); 101 | } 102 | 103 | function from(list: string[]): CompletionSource { 104 | return (cx) => { 105 | const word = cx.matchBefore(/\w+$/); 106 | if (!word && !cx.explicit) { 107 | return null; 108 | } 109 | 110 | return { 111 | from: word ? word.from : cx.pos, 112 | options: [...list, ...terms].map((label) => ({ 113 | label, 114 | apply: snippet(label), 115 | })), 116 | span: /\w*/, 117 | }; 118 | }; 119 | } 120 | 121 | function autocomplete(words: string[]) { 122 | return autocompletion({ 123 | activateOnTyping: true, 124 | override: [from(words)], 125 | closeOnBlur: false, 126 | }); 127 | } 128 | 129 | const terms = [ 130 | "E", 131 | "LN10", 132 | "LN2", 133 | "LOG10E", 134 | "LOG2E", 135 | "MAX_VALUE", 136 | "MIN_VALUE", 137 | "NaN", 138 | "PI", 139 | "SQRT1_2", 140 | "SQRT2", 141 | "abs", 142 | "acos", 143 | "asin", 144 | "atan", 145 | "atan2", 146 | "bandspace", 147 | "bandwidth", 148 | "ceil", 149 | "clamp", 150 | "clampRange", 151 | "containerSize", 152 | "contrast", 153 | "copy", 154 | "cos", 155 | "cumulativeLogNormal", 156 | "cumulativeNormal", 157 | "cumulativeUniform", 158 | "data", 159 | "date", 160 | "datetime", 161 | "datum", 162 | "day", 163 | "dayAbbrevFormat", 164 | "dayFormat", 165 | "dayofyear", 166 | "debug", 167 | "densityLogNormal", 168 | "densityNormal", 169 | "densityUniform", 170 | "domain", 171 | "event", 172 | "exp", 173 | "extent", 174 | "floor", 175 | "format", 176 | "geoArea", 177 | "geoBounds", 178 | "geoCentroid", 179 | "gradient", 180 | "group", 181 | "hcl", 182 | "hours", 183 | "hsl", 184 | "if", 185 | "inScope", 186 | "indata", 187 | "indexof", 188 | "indexof", 189 | "info", 190 | "inrange", 191 | "invert", 192 | "isArray", 193 | "isBoolean", 194 | "isDate", 195 | "isDefined", 196 | "isFinite", 197 | "isNaN", 198 | "isNumber", 199 | "isObject", 200 | "isRegExp", 201 | "isString", 202 | "isValid", 203 | "item", 204 | "join", 205 | "lab", 206 | "lastindexof", 207 | "lastindexof", 208 | "length", 209 | "length", 210 | "lerp", 211 | "log", 212 | "lower", 213 | "luminance", 214 | "max", 215 | "merge", 216 | "milliseconds", 217 | "min", 218 | "minutes", 219 | "month", 220 | "monthAbbrevFormat", 221 | "monthFormat", 222 | "now", 223 | "pad", 224 | "panLinear", 225 | "panLog", 226 | "panPow", 227 | "panSymlog", 228 | "parseFloat", 229 | "parseInt", 230 | "peek", 231 | "pinchAngle", 232 | "pinchDistance", 233 | "pluck", 234 | "pow", 235 | "quantileLogNormal", 236 | "quantileNormal", 237 | "quantileUniform", 238 | "quarter", 239 | "random", 240 | "range", 241 | "regexp", 242 | "replace", 243 | "reverse", 244 | "rgb", 245 | "round", 246 | "sampleLogNormal", 247 | "sampleNormal", 248 | "sampleUniform", 249 | "scale", 250 | "screen", 251 | "seconds", 252 | "sequence", 253 | "signal names", 254 | "sin", 255 | "slice", 256 | "slice", 257 | "span", 258 | "split", 259 | "sqrt", 260 | "substring", 261 | "tan", 262 | "test", 263 | "time", 264 | "timeFormat", 265 | "timeOffset", 266 | "timeParse", 267 | "timeSequence", 268 | "timeUnitSpecifier", 269 | "timezoneoffset", 270 | "toBoolean", 271 | "toDate", 272 | "toNumber", 273 | "toString", 274 | "treeAncestors", 275 | "treePath", 276 | "trim", 277 | "truncate", 278 | "upper", 279 | "utc", 280 | "utcFormat", 281 | "utcOffset", 282 | "utcParse", 283 | "utcSequence", 284 | "utcdate", 285 | "utcday", 286 | "utcdayofyear", 287 | "utchours", 288 | "utcmilliseconds", 289 | "utcminutes", 290 | "utcmonth", 291 | "utcquarter", 292 | "utcseconds", 293 | "utcweek", 294 | "utcyear", 295 | "warn", 296 | "week", 297 | "windowSize", 298 | "x", 299 | "xy", 300 | "y", 301 | "year", 302 | "zoomLinear", 303 | "zoomLog", 304 | "zoomPow", 305 | "zoomSymlog", 306 | ]; 307 | 308 | const termSet = new Set(terms); 309 | 310 | const blue = "#0551A5"; 311 | const green = "#17885C"; 312 | const red = "#A21615"; 313 | // const black = "#000"; 314 | const colorMap: Record = { 315 | Number: green, 316 | String: red, 317 | VariableName: blue, 318 | }; 319 | 320 | function doSyntaxHighlighting(view: EditorView) { 321 | const widgets: Range[] = []; 322 | const code = view.state.doc.sliceString(0); 323 | parser.parse(code).iterate({ 324 | from: 0, 325 | to: code.length, 326 | enter: ({ node, from, to }) => { 327 | if (colorMap[node.type.name]) { 328 | const style = `color: ${colorMap[node.type.name]}`; 329 | const syntaxHighlight = Decoration.mark({ 330 | attributes: { style }, 331 | }); 332 | widgets.push(syntaxHighlight.range(from, to)); 333 | } 334 | }, 335 | }); 336 | try { 337 | return Decoration.set(widgets.sort((a, b) => a.from - b.from)); 338 | } catch (e) { 339 | console.log(e); 340 | console.log("problem creating widgets"); 341 | return Decoration.set([]); 342 | } 343 | } 344 | 345 | const analogSyntaxHighlighter = ViewPlugin.fromClass( 346 | class { 347 | decorations: DecorationSet; 348 | 349 | constructor(view: EditorView) { 350 | this.decorations = doSyntaxHighlighting(view); 351 | } 352 | 353 | update(update: ViewUpdate) { 354 | if (update.docChanged || update.viewportChanged) { 355 | this.decorations = doSyntaxHighlighting(update.view); 356 | } 357 | } 358 | }, 359 | { decorations: (v) => v.decorations } 360 | ); 361 | -------------------------------------------------------------------------------- /sites/docs/src/examples/VegaLiteDebug.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import VegaLiteV5Schema from "../constants/vega-lite-v5-schema.json"; 3 | import { 4 | Editor, 5 | ProjectionProps, 6 | Projection, 7 | utils, 8 | StandardBundle, 9 | } from "../../../../packages/prong-editor/src/index"; 10 | 11 | export const vegaLiteCode = ` 12 | { 13 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 14 | "description": "A simple bar chart with embedded data.", 15 | "data": { 16 | "values": [ 17 | {"penguins": "A", "flowers": 28}, {"penguins": "B", "flowers": 55}, {"penguins": "C", "flowers": 43}, 18 | {"penguins": "D", "flowers": 91}, {"penguins": "E", "flowers": 81}, {"penguins": "F", "flowers": 53}, 19 | {"penguins": "G", "flowers": 19}, {"penguins": "H", "flowers": 87}, {"penguins": "I", "flowers": 52} 20 | ] 21 | }, 22 | "mark": {"type": "bar"}, 23 | "encoding": { 24 | "x": {"field": "penguins", "type": "nominal", "axis": {"labelAngle": 0}}, 25 | "y": {"field": "flowers", "type": "quantitative"}, 26 | "color": {"field": "penguins", "scale": { "scheme": "" } } 27 | } 28 | } 29 | `; 30 | 31 | function CounterProjection(_props: ProjectionProps) { 32 | const [count, setCount] = useState(0); 33 | return ( 34 |
setCount(count + 1)}> 35 | Clicked {count} Times 36 |
37 | ); 38 | } 39 | 40 | function VegaLiteExampleApp() { 41 | const [currentCode, setCurrentCode] = useState(vegaLiteCode); 42 | const [clockRunning, setClockRunning] = useState(false); 43 | const [timer, setTimer] = useState(0); 44 | 45 | useEffect(() => { 46 | setTimeout(() => { 47 | if (clockRunning) { 48 | setTimer(timer + 1); 49 | } 50 | }, 5000); 51 | }, [timer, clockRunning]); 52 | 53 | function DynamicProjection(_props: ProjectionProps) { 54 | return
Timer: ({timer})
; 55 | } 56 | const fields = ["penguins", "flowers", "wheat", "squids"]; 57 | 58 | return ( 59 |
60 |
61 | 62 | 65 |
66 | 67 | setCurrentCode(x)} 71 | projections={ 72 | [ 73 | ...Object.values(StandardBundle), 74 | { 75 | query: { 76 | type: "index", 77 | query: ["data", "values", "*"], 78 | }, 79 | type: "tooltip", 80 | projection: ({ keyPath }) => { 81 | return ( 82 |
83 |
hi annotation projection {keyPath.join(",")}
84 |
{`Timer value: ${timer}`}
85 |
86 | ); 87 | }, 88 | name: "popover example", 89 | } as Projection, 90 | { 91 | query: { 92 | type: "index", 93 | query: ["encoding", "*", "field", "field___value"], 94 | }, 95 | type: "tooltip", 96 | projection: (props) => { 97 | return ( 98 |
99 | {fields.map((x) => ( 100 | 110 | ))} 111 |
112 | ); 113 | }, 114 | name: "Switch to", 115 | } as Projection, 116 | 117 | { 118 | // query: ["data", "values", "*"], 119 | query: { 120 | type: "index", 121 | query: ["description"], 122 | }, 123 | type: "inline", 124 | projection: CounterProjection, 125 | hasInternalState: true, 126 | name: "counter", 127 | mode: "replace", 128 | }, 129 | clockRunning && { 130 | // query: ["data", "values", "*"], 131 | query: { 132 | type: "index", 133 | query: ["$schema"], 134 | }, 135 | type: "inline", 136 | projection: DynamicProjection, 137 | hasInternalState: true, 138 | name: "dynamic projection", 139 | mode: "replace", 140 | }, 141 | ].filter((x) => x) as Projection[] 142 | } 143 | /> 144 |
145 | ); 146 | } 147 | 148 | export default VegaLiteExampleApp; 149 | -------------------------------------------------------------------------------- /sites/docs/src/examples/VegaUseCase.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { 3 | Editor, 4 | StandardBundle, 5 | Projection, 6 | ProjectionProps, 7 | utils, 8 | } from "../../../../packages/prong-editor/src/index"; 9 | import VegaSchema from "../constants/vega-schema.json"; 10 | 11 | import { 12 | analyzeVegaCode, 13 | buttonListProjection, 14 | buildInlineDropDownProjection, 15 | } from "./example-utils"; 16 | import VegaExpressionEditor from "./VegaExpressionEditor"; 17 | import { 18 | createHistograms, 19 | PreComputedHistograms, 20 | extractFieldNames, 21 | extractScaleNames, 22 | DataTable, 23 | isDataTable, 24 | buildSparkProjection, 25 | } from "./histograms"; 26 | 27 | import "../stylesheets/vega-example.css"; 28 | 29 | const initialSpec = `{ 30 | "data": [ 31 | { 32 | "name": "states", 33 | "url": "data/us-10m.json", 34 | "format": {"type": "topojson", "feature": "states"} 35 | }, 36 | { 37 | "name": "obesity", 38 | "url": "data/obesity.json", 39 | "transform": [ 40 | { 41 | "type": "lookup", 42 | "from": "states", "key": "id", 43 | "fields": ["id"], "as": ["geo"] 44 | }, 45 | { 46 | "type": "filter", 47 | "expr": "datum.geo" 48 | }, 49 | { 50 | "type": "formula", "as": "centroid", 51 | "expr": "geoCentroid('projection', datum.geo)" 52 | } 53 | ] 54 | } 55 | ], 56 | 57 | "projections": [ 58 | { 59 | "name": "projection", 60 | "type": "albersUsa", 61 | "scale": 1100, 62 | "translate": [{"signal": "width / 2"}, {"signal": "height / 2"}] 63 | } 64 | ], 65 | 66 | "scales": [ 67 | { 68 | "name": "size", 69 | "domain": {"data": "obesity", "field": "rate"}, 70 | "zero": false, "range": [1000, 5000] 71 | }, 72 | { 73 | "name": "color", "type": "linear", "nice": true, 74 | "domain": {"data": "obesity", "field": "rate"}, 75 | "range": "ramp" 76 | } 77 | ], 78 | 79 | "marks": [ 80 | { 81 | "name": "circles", 82 | "type": "symbol", 83 | "from": {"data": "obesity"}, 84 | "encode": { 85 | "enter": { 86 | "size": {"scale": "size", "field": "rate"}, 87 | "fill": {"scale": "color", "field": "rate"}, 88 | "stroke": {"value": "white"}, 89 | "strokeWidth": {"value": 1.5}, 90 | "x": {"field": "centroid[0]"}, 91 | "y": {"field": "centroid[1]"}, 92 | "tooltip": {"signal": "'Obesity Rate: ' + format(datum.rate, '.1%')"} 93 | } 94 | }, 95 | "transform": [ 96 | { 97 | "type": "force", 98 | "static": true, 99 | "forces": [ 100 | {"force": "collide", "radius": {"expr": "1 + sqrt(datum.size) / 2"}}, 101 | {"force": "x", "x": "datum.centroid[0]"}, 102 | {"force": "y", "y": "datum.centroid[1]"} 103 | ] 104 | } 105 | ] 106 | }, 107 | { 108 | "type": "text", 109 | "interactive": false, 110 | "from": {"data": "circles"}, 111 | "encode": { 112 | "enter": { 113 | "align": {"value": "center"}, 114 | "baseline": {"value": "middle"}, 115 | "fontSize": {"value": 13}, 116 | "fontWeight": {"value": "bold"}, 117 | "text": {"field": "datum.state"} 118 | }, 119 | "update": { 120 | "x": {"field": "x"}, 121 | "y": {"field": "y"} 122 | } 123 | } 124 | } 125 | ], 126 | "$schema": "https://vega.github.io/schema/vega/v5.json", 127 | "description": "A Dorling cartogram depicting U.S. state obesity rates.", 128 | "width": 900, 129 | "height": 520, 130 | "autosize": "none" 131 | }`; 132 | 133 | interface EditorProps extends ProjectionProps { 134 | signals: any; 135 | } 136 | function ExpressionEditorProjection(props: EditorProps) { 137 | const [code, setCode] = useState(""); 138 | const [error, setError] = useState(null); 139 | useEffect(() => { 140 | setCode(props.currentValue.slice(1, props.currentValue.length - 1)); 141 | }, [props.currentValue]); 142 | return ( 143 |
144 |
145 | {Object.entries(props.signals).map(([key, value]) => ( 146 |
147 | {key}: {JSON.stringify(value)} 148 |
149 | ))} 150 |
151 |
152 | setCode(update)} 154 | code={code} 155 | terms={Object.keys(props.signals)} 156 | onError={(e) => setError(e)} 157 | /> 158 | 167 |
168 | {error &&
{error}
} 169 |
170 | ); 171 | } 172 | 173 | const mapProjections = [ 174 | "albers", 175 | "albersUsa", 176 | "azimuthalEqualArea", 177 | "azimuthalEquidistant", 178 | "conicConformal", 179 | "conicEqualArea", 180 | "conicEquidistant", 181 | "equalEarth", 182 | "equirectangular", 183 | "gnomonic", 184 | "identity", 185 | "mercator", 186 | "mollweide", 187 | "naturalEarth1", 188 | "orthographic", 189 | "stereographic", 190 | "transverseMercator", 191 | ]; 192 | function getMapProjectionTypes(currentCode: string): string[] { 193 | return ((utils.simpleParse(currentCode, {})?.projections || []) as any[]) 194 | .map((proj) => proj?.type || false) 195 | .filter((x) => x); 196 | } 197 | 198 | function VegaUseCase() { 199 | const [currentCode, setCurrentCode] = useState(initialSpec); 200 | const [preComputedHistograms, setPrecomputedHistograms] = 201 | useState({}); 202 | const [fieldNames, setFieldNames] = useState([]); 203 | const [scaleNames, setScales] = useState([]); 204 | const [signals, setSignals] = useState({}); 205 | const [mapProjectionTypes, setMapProjectionTypes] = useState([]); 206 | 207 | useEffect(() => { 208 | analyzeVegaCode(currentCode, ({ data, signals }) => { 209 | const namedPairs = Object.entries(data) 210 | .filter(([_key, dataSet]) => isDataTable(dataSet)) 211 | .map(([key, data]) => [key, createHistograms(data as DataTable)]); 212 | setPrecomputedHistograms(Object.fromEntries(namedPairs)); 213 | setSignals(signals); 214 | setFieldNames(extractFieldNames(data || {})); 215 | setMapProjectionTypes(getMapProjectionTypes(currentCode)); 216 | }); 217 | setScales(extractScaleNames(currentCode)); 218 | }, [currentCode]); 219 | return ( 220 | setCurrentCode(x)} 224 | projections={ 225 | [ 226 | ...Object.values(StandardBundle), 227 | buildSparkProjection(preComputedHistograms, "right", "bar"), 228 | { 229 | type: "tooltip", 230 | takeOverMenu: true, 231 | query: { 232 | type: "schemaMatch", 233 | query: ["exprString", "signal", "expr"], 234 | }, 235 | name: "Signal Editor", 236 | projection: (props: ProjectionProps) => ( 237 | 238 | ), 239 | }, 240 | ...mapProjectionTypes.map((_mapProj, idx) => 241 | buildInlineDropDownProjection( 242 | mapProjections, 243 | mapProjectionTypes[idx], 244 | ["projections", idx, "type", "type___value"], 245 | "Map Projection Dropdown" 246 | ) 247 | ), 248 | { 249 | type: "tooltip", 250 | query: { 251 | type: "schemaMatch", 252 | query: ["field", "stringOrSignal"], 253 | }, 254 | name: "Switch to", 255 | projection: buttonListProjection(fieldNames, currentCode), 256 | }, 257 | { 258 | type: "tooltip", 259 | query: { 260 | type: "schemaMatch", 261 | query: ["scale"], 262 | }, 263 | name: "Switch to", 264 | projection: buttonListProjection(scaleNames, currentCode), 265 | }, 266 | ] as Projection[] 267 | } 268 | /> 269 | ); 270 | } 271 | 272 | export default VegaUseCase; 273 | -------------------------------------------------------------------------------- /sites/docs/src/examples/example-data.ts: -------------------------------------------------------------------------------- 1 | export const vegaCode = ` 2 | { 3 | "$schema": "https://vega.github.io/schema/vega/v5.json", 4 | "description": "A wheat plot example, which combines elements of dot plots and histograms.", 5 | "width": 500, 6 | "padding": 5, 7 | 8 | "signals": [ 9 | { "name": "symbolDiameter", "value": 4, 10 | "bind": {"input": "range", "min": 1, "max": 8, "step": 0.25} }, 11 | { "name": "binOffset", "value": 0, 12 | "bind": {"input": "range", "min": -0.1, "max": 0.1} }, 13 | { "name": "binStep", "value": 0.075, 14 | "bind": {"input": "range", "min": 0.001, "max": 0.2, "step": 0.001} }, 15 | { "name": "height", "update": "extent[1] * (1 + symbolDiameter)" } 16 | ], 17 | 18 | "data": [ 19 | { 20 | "name": "points", 21 | "url": "data/normal-2d.json", 22 | "transform": [ 23 | { 24 | "type": "bin", "field": "u", 25 | "extent": [-1, 1], 26 | "anchor": {"signal": "binOffset"}, 27 | "step": {"signal": "binStep"}, 28 | "nice": false, 29 | "signal": "bins" 30 | }, 31 | { 32 | "type": "stack", 33 | "groupby": ["bin0"], 34 | "sort": {"field": "u"} 35 | }, 36 | { 37 | "type": "extent", "signal": "extent", 38 | "field": "y1" 39 | } 40 | ] 41 | } 42 | ], 43 | 44 | "scales": [ 45 | { 46 | "name": "xscale", 47 | "type": "linear", 48 | "range": "width", 49 | "domain": [-1, 1] 50 | }, 51 | { 52 | "name": "yscale", 53 | "type": "linear", 54 | "range": "height", 55 | "domain": [0, {"signal": "extent[1]"}] 56 | } 57 | ], 58 | 59 | "axes": [ 60 | { "orient": "bottom", "scale": "xscale", 61 | "values": {"signal": "sequence(bins.start, bins.stop + bins.step, bins.step)"}, 62 | "domain": false, "ticks": false, "labels": false, "grid": true, 63 | "zindex": 0 }, 64 | {"orient": "bottom", "scale": "xscale", "zindex": 1} 65 | ], 66 | 67 | "marks": [ 68 | { 69 | "type": "symbol", 70 | "from": {"data": "points"}, 71 | "encode": { 72 | "enter": { 73 | "fill": {"value": "transparent"}, 74 | "strokeWidth": {"value": 0.5} 75 | }, 76 | "update": { 77 | "x": {"scale": "xscale", "field": "u"}, 78 | "y": {"scale": "yscale", "field": "y0"}, 79 | "size": {"signal": "symbolDiameter * symbolDiameter"}, 80 | "stroke": {"value": "steelblue"} 81 | }, 82 | "hover": { 83 | "stroke": {"value": "firebrick"} 84 | } 85 | } 86 | } 87 | ] 88 | } 89 | `; 90 | 91 | export const vegaLiteCode = ` 92 | { 93 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 94 | "description": "A simple bar chart with embedded data.", 95 | "data": { 96 | "values": [ 97 | {"penguins": "A", "flowers": 28}, {"penguins": "B", "flowers": 55}, {"penguins": "C", "flowers": 43}, 98 | {"penguins": "D", "flowers": 91}, {"penguins": "E", "flowers": 81}, {"penguins": "F", "flowers": 53}, 99 | {"penguins": "G", "flowers": 19}, {"penguins": "H", "flowers": 87}, {"penguins": "I", "flowers": 52} 100 | ] 101 | }, 102 | "mark": "bar", 103 | "encoding": { 104 | "x": {"field": "penguins", "type": "nominal", "axis": {"labelAngle": 0}}, 105 | "y": {"field": "flowers", "type": "quantitative"} 106 | } 107 | } 108 | `; 109 | 110 | export const vegaLiteScatterPlot = ` 111 | { 112 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 113 | "description": "A scatterplot showing body mass and flipper lengths of penguins.", 114 | "data": { 115 | "url": "data/penguins.json" 116 | }, 117 | "mark": "point", 118 | "encoding": { 119 | "x": { 120 | "field": "Flipper Length (mm)", 121 | "type": "quantitative", 122 | "scale": {"zero": false} 123 | }, 124 | "y": { 125 | "field": "Body Mass (g)", 126 | "type": "quantitative", 127 | "scale": {"zero": false} 128 | }, 129 | "color": {"field": "Species", "type": "nominal"}, 130 | "shape": {"field": "Species", "type": "nominal"} 131 | } 132 | } 133 | `; 134 | export const vegaLiteHeatmap = ` 135 | { 136 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 137 | "data": {"url": "data/movies.json"}, 138 | "transform": [{ 139 | "filter": {"and": [ 140 | {"field": "IMDB Rating", "valid": true}, 141 | {"field": "Rotten Tomatoes Rating", "valid": true} 142 | ]} 143 | }], 144 | "mark": "rect", 145 | "width": 300, 146 | "height": 200, 147 | "encoding": { 148 | "x": { 149 | "bin": {"maxbins":60}, 150 | "field": "IMDB Rating", 151 | "type": "quantitative" 152 | }, 153 | "y": { 154 | "bin": {"maxbins": 40}, 155 | "field": "Rotten Tomatoes Rating", 156 | "type": "quantitative" 157 | }, 158 | "color": { 159 | "aggregate": "count", 160 | "type": "quantitative" 161 | } 162 | }, 163 | "config": { 164 | "view": { 165 | "stroke": "transparent" 166 | } 167 | } 168 | } 169 | `; 170 | 171 | export const vegaLiteStreamgraph = ` 172 | { 173 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 174 | "width": 300, "height": 200, 175 | "data": {"url": "data/unemployment-across-industries.json"}, 176 | "mark": "area", 177 | "encoding": { 178 | "x": { 179 | "timeUnit": "yearmonth", "field": "date", 180 | "axis": {"domain": false, "format": "%Y", "tickSize": 0} 181 | }, 182 | "y": { 183 | "aggregate": "sum", "field": "count", 184 | "axis": null, 185 | "stack": "center" 186 | }, 187 | "color": {"field":"series", "scale":{"scheme": "category20b"}} 188 | } 189 | }`; 190 | 191 | export const vegaLiteLinechart = ` 192 | { 193 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 194 | "description": "Stock prices of 5 Tech Companies over Time.", 195 | "data": {"url": "data/stocks.csv"}, 196 | "mark": { 197 | "type": "line", 198 | "point": { 199 | "filled": false, 200 | "fill": "white" 201 | } 202 | }, 203 | "encoding": { 204 | "x": {"timeUnit": "year", "field": "date"}, 205 | "y": {"aggregate":"mean", "field": "price", "type": "quantitative"}, 206 | "color": {"field": "symbol", "type": "nominal"} 207 | } 208 | } 209 | `; 210 | 211 | export const produceSchema: any = { 212 | $id: "https://example.com/arrays.schema.json", 213 | $schema: "https://json-schema.org/draft/2020-12/schema", 214 | description: "A representation of a person, company, organization, or place", 215 | type: "object", 216 | required: ["fruits", "vegetables", "meta"], 217 | properties: { 218 | fruits: { 219 | type: "array", 220 | description: "wowza what a list of fruit! awoooga", 221 | items: { $ref: "#/$defs/fruitie" }, 222 | }, 223 | vegetables: { 224 | type: "array", 225 | description: "just a boring ol list of vegetables", 226 | items: { $ref: "#/$defs/veggie" }, 227 | }, 228 | meta: { 229 | type: "string", 230 | description: "just meta data dont worry about it", 231 | }, 232 | }, 233 | $defs: { 234 | veggie: { 235 | type: "object", 236 | required: ["veggieName", "veggieLike"], 237 | properties: { 238 | veggieName: { 239 | type: "string", 240 | description: "The name of the vegetable.", 241 | }, 242 | veggieLike: { 243 | type: "boolean", 244 | description: "Do I like this vegetable?", 245 | }, 246 | veggieStarRating: { 247 | $ref: "#/$defs/veggieStar", 248 | }, 249 | }, 250 | }, 251 | veggieStar: { 252 | anyOf: [ 253 | { 254 | description: "Stars out of 5", 255 | maximum: 5, 256 | minimum: 0, 257 | type: "number", 258 | }, 259 | { 260 | enum: ["Thumbs Up", "Thumbs Down"], 261 | type: "string", 262 | }, 263 | ], 264 | }, 265 | fruitie: { 266 | enum: [ 267 | "Apple", 268 | "Apricot", 269 | "Avocado", 270 | "Banana", 271 | "Blackberry", 272 | "Blueberry", 273 | "Cherry", 274 | "Coconut", 275 | "Cucumber", 276 | "Durian", 277 | "Dragonfruit", 278 | "Fig", 279 | "Gooseberry", 280 | "Grape", 281 | "Guava", 282 | ], 283 | type: "string", 284 | description: "Options for fruit that are allowed", 285 | }, 286 | }, 287 | }; 288 | 289 | export const produceExample = `{ 290 | "fruits": [ "apple", "orange", "#c71585" ], 291 | "vegetables": [ 292 | { 293 | "veggieName": "potato", 294 | "veggieLike": true 295 | }, 296 | { 297 | "veggieName": "broccoli", 298 | "veggieLike": false 299 | } 300 | ], 301 | "meta": "ignore this meta data" 302 | }`; 303 | -------------------------------------------------------------------------------- /sites/docs/src/examples/example-utils.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | 3 | import * as vega from "vega"; 4 | import { parse, View } from "vega"; 5 | 6 | import { 7 | utils, 8 | ProjectionProps, 9 | Projection, 10 | } from "../../../../packages/prong-editor/src/index"; 11 | const { simpleParse, setIn } = utils; 12 | 13 | export function classNames(input: Record) { 14 | return Object.entries(input) 15 | .filter(([_key, value]) => value) 16 | .map(([key]) => key) 17 | .join(" "); 18 | } 19 | 20 | export const usePersistedState = (name: string, defaultValue: any) => { 21 | const [value, setValue] = useState(defaultValue); 22 | const nameRef = useRef(name); 23 | 24 | useEffect(() => { 25 | try { 26 | const storedValue = localStorage.getItem(name); 27 | if (storedValue !== null) setValue(storedValue); 28 | else localStorage.setItem(name, defaultValue); 29 | } catch { 30 | setValue(defaultValue); 31 | } 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, []); 34 | 35 | useEffect(() => { 36 | try { 37 | localStorage.setItem(nameRef.current, value); 38 | } catch (e) { 39 | console.error(e); 40 | } 41 | }, [value]); 42 | 43 | useEffect(() => { 44 | const lastName = nameRef.current; 45 | if (name !== lastName) { 46 | try { 47 | localStorage.setItem(name, value); 48 | nameRef.current = name; 49 | localStorage.removeItem(lastName); 50 | } catch (e) { 51 | console.error(e); 52 | } 53 | } 54 | // eslint-disable-next-line react-hooks/exhaustive-deps 55 | }, [name]); 56 | 57 | return [value, setValue]; 58 | }; 59 | export function analyzeVegaCode( 60 | currentCode: string, 61 | analysis: (viewState: { signals?: any; data?: any }) => void 62 | ) { 63 | try { 64 | const code = simpleParse(currentCode, {}); 65 | const spec = parse(code, {}, { ast: true }); 66 | const view = new View(spec).initialize(); 67 | view 68 | .runAsync() 69 | .then(() => { 70 | const x = view.getState({ 71 | signals: vega.truthy, 72 | data: vega.truthy, 73 | recurse: true, 74 | }); 75 | analysis(x); 76 | }) 77 | .catch((e) => { 78 | console.error(e); 79 | }); 80 | } catch (err) { 81 | console.log(err); 82 | } 83 | } 84 | 85 | export type Table = Record[]; 86 | export function extractFieldNames(data: Table) { 87 | const fieldNames = new Set([]); 88 | data.forEach((row) => { 89 | Object.keys(row).forEach((fieldName) => fieldNames.add(fieldName)); 90 | }); 91 | return Array.from(fieldNames); 92 | } 93 | 94 | export const buttonListProjection = 95 | (list: string[], currentCode: string) => (props: ProjectionProps) => { 96 | return ( 97 |
98 | {list.map((item) => { 99 | return ( 100 | 108 | ); 109 | })} 110 |
111 | ); 112 | }; 113 | 114 | export const buildInlineDropDownProjection = ( 115 | options: string[], 116 | currentValue: string, 117 | indexTarget: (string | number)[], 118 | name: string 119 | ): Projection => ({ 120 | query: { type: "index", query: indexTarget }, 121 | type: "inline", 122 | hasInternalState: false, 123 | mode: "replace", 124 | name, 125 | projection: ({ setCode, fullCode }) => { 126 | return ( 127 | 138 | ); 139 | }, 140 | }); 141 | -------------------------------------------------------------------------------- /sites/docs/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | ReactDOM.render( 6 | // @ts-ignore 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /sites/docs/src/stylesheets/quiet-styles.css: -------------------------------------------------------------------------------- 1 | h1, h3 { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | #quiet-root { 7 | padding: 20px 50px; 8 | } -------------------------------------------------------------------------------- /sites/docs/src/stylesheets/tracery-example.css: -------------------------------------------------------------------------------- 1 | .cascade-node { 2 | border: thin solid black; 3 | border-radius: 15px; 4 | padding: 5px 5px; 5 | align-items: center; 6 | justify-content: center; 7 | cursor: pointer; 8 | } 9 | 10 | .cascade-container { 11 | border: thin solid black; 12 | border-radius: 15px; 13 | padding: 5px 5px; 14 | align-items: center; 15 | justify-content: center; 16 | cursor: pointer; 17 | } 18 | 19 | .cascade-container--selected { 20 | background-color: #004a995d; 21 | } 22 | 23 | .tracery-in-use { 24 | background-color: #0099785d; 25 | } 26 | 27 | 28 | .example-highlighter { 29 | background: #004a995d; 30 | } 31 | 32 | .tracery-app-root { 33 | padding: 50px; 34 | } 35 | 36 | .tracery-bdx { 37 | max-width: 800px; 38 | } 39 | 40 | .tracery-bdx-container { 41 | position: absolute; 42 | pointer-Events: none; 43 | } 44 | 45 | .tracery-in-use-0 { 46 | background: #4c78a8; 47 | } 48 | 49 | .tracery-in-use>.ͼo { 50 | color: black !important 51 | } 52 | 53 | .tracery-in-use-0>.ͼo, 54 | .tracery-in-use-4>.ͼo, 55 | .tracery-in-use-8>.ͼo, 56 | .tracery-in-use-12>.ͼo, 57 | .tracery-in-use-18>.ͼo { 58 | color: white !important 59 | } 60 | 61 | 62 | .tracery-in-use-1 { 63 | background: #9ecae977; 64 | } 65 | 66 | .tracery-in-use-2 { 67 | background: #f5851877; 68 | } 69 | 70 | .tracery-in-use-3 { 71 | background: #ffbf7977; 72 | } 73 | 74 | .tracery-in-use-4 { 75 | background: #54a24b77; 76 | } 77 | 78 | .tracery-in-use-5 { 79 | background: #88d27a77; 80 | } 81 | 82 | .tracery-in-use-6 { 83 | background: #b79a2077; 84 | } 85 | 86 | .tracery-in-use-7 { 87 | background: #f2cf5b77; 88 | } 89 | 90 | .tracery-in-use-8 { 91 | background: #43989477; 92 | } 93 | 94 | 95 | .tracery-in-use-9 { 96 | background: #83bcb677; 97 | } 98 | 99 | .tracery-in-use-10 { 100 | background: #e4575677; 101 | } 102 | 103 | .tracery-in-use-11 { 104 | background: #ff9d9877; 105 | } 106 | 107 | .tracery-in-use-12 { 108 | background: #79706e77; 109 | } 110 | 111 | 112 | .tracery-in-use-13 { 113 | background: #bab0ac77; 114 | } 115 | 116 | .tracery-in-use-14 { 117 | background: #d6719577; 118 | } 119 | 120 | .tracery-in-use-15 { 121 | background: #fcbfd277; 122 | } 123 | 124 | .tracery-in-use-16 { 125 | background: #b279a277; 126 | } 127 | 128 | .tracery-in-use-17 { 129 | background: #d6a5c977; 130 | } 131 | 132 | .tracery-in-use-18 { 133 | background: #9e765f77; 134 | } 135 | 136 | .tracery-in-use-19 { 137 | background: #d8b5a577; 138 | } -------------------------------------------------------------------------------- /sites/docs/src/stylesheets/vega-example.css: -------------------------------------------------------------------------------- 1 | .visible-histogram { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | position: relative; 6 | } 7 | 8 | .visible-histogram>svg { 9 | overflow: visible; 10 | } 11 | 12 | /* */ 13 | 14 | .signal-editor { 15 | width: 405px; 16 | } 17 | 18 | .signal-editor-list { 19 | display: flex; 20 | flex-wrap: wrap; 21 | } 22 | 23 | .signal-editor-list--item { 24 | margin-right: 5px; 25 | } 26 | 27 | .signal-editor-error-message { 28 | color: red; 29 | } 30 | 31 | .signal-editor .expression-editor .cm-activeLine { 32 | background-color: rgba(102, 51, 153, 0); 33 | } 34 | 35 | .signal-editor .expression-editor .cm-gutters { 36 | display: none; 37 | } 38 | 39 | 40 | .signal-editor .cm-tooltip.cm-tooltip-autocomplete>ul>li { 41 | background: gainsboro; 42 | } -------------------------------------------------------------------------------- /sites/docs/src/stylesheets/vega-lite-example.css: -------------------------------------------------------------------------------- 1 | .vl-demo { 2 | padding: 50px; 3 | } 4 | 5 | .vl-demo .editor-container { 6 | width: 750px; 7 | max-height: 800px; 8 | } 9 | 10 | .vl-demo .chart-container { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | width: 100%; 15 | /* max-width: 600px; */ 16 | max-width: 30%; 17 | overflow: auto; 18 | } 19 | 20 | .pill, .shelf-content { 21 | background-color: white; 22 | color: white; 23 | /* border: 1px dashed gray; */ 24 | cursor: move; 25 | /* margin-bottom: 1.5rem; */ 26 | margin-right: 1.5rem; 27 | padding: 0.5rem 1rem; 28 | background: #009879; 29 | border-radius: 30px; 30 | } 31 | 32 | .shelf { 33 | align-items: center; 34 | /* border-radius: 20px; */ 35 | border: 1px dashed gray; 36 | color: white; 37 | display: flex; 38 | height: 1em; 39 | justify-content: center; 40 | padding: 1em; 41 | text-align: center; 42 | width: 12rem; 43 | } 44 | 45 | .shelf-content { 46 | align-items: center; 47 | border: 1px dashed gray; 48 | display: flex; 49 | justify-content: space-around; 50 | width: 100%; 51 | } 52 | 53 | .shelf.shelf-empty { 54 | background-color: gray; 55 | } 56 | 57 | .shelf.shelf-full { 58 | background-color: none; 59 | } 60 | 61 | .shelf.shelf-droppable { 62 | background-color: lightgray; 63 | } 64 | 65 | .counter { 66 | align-items: center; 67 | background: red; 68 | border-radius: 2px; 69 | color: white; 70 | display: flex; 71 | height: 100px; 72 | justify-content: center; 73 | width: 100px; 74 | white-space: normal; 75 | text-align: center; 76 | } 77 | 78 | .dynamic-projection-example { 79 | align-items: center; 80 | background: rebeccapurple; 81 | border-radius: 2px; 82 | color: white; 83 | display: flex; 84 | height: 100px; 85 | justify-content: center; 86 | width: 100px; 87 | white-space: normal; 88 | text-align: center; 89 | } 90 | 91 | /* .editor-container { 92 | max-width: 50%; 93 | } */ 94 | 95 | .data-container { 96 | border: thin solid black; 97 | border-radius: 5px; 98 | display: flex; 99 | /* align-items: ; */ 100 | padding: 5px; 101 | } 102 | 103 | /* styler example */ 104 | 105 | .styler-app { 106 | width: 100%; 107 | padding: 50px; 108 | } 109 | 110 | .styler-app .editor-container { 111 | min-width: 650px; 112 | width: 650px; 113 | 114 | } 115 | 116 | .styler-app .chart-container { 117 | display: flex; 118 | flex-wrap: wrap; 119 | padding: 20px; 120 | align-items: center; 121 | justify-content: center; 122 | } 123 | 124 | /* table */ 125 | 126 | .styled-table { 127 | border-collapse: collapse; 128 | margin: 25px 0; 129 | font-size: 0.9em; 130 | font-family: sans-serif; 131 | min-width: 400px; 132 | } 133 | 134 | .styled-table thead tr { 135 | background-color: #009879; 136 | color: #ffffff; 137 | text-align: left; 138 | } 139 | 140 | .styled-table tbody tr { 141 | border-bottom: 1px solid #dddddd; 142 | } 143 | 144 | .styled-table tbody tr:nth-of-type(even) { 145 | background-color: #f3f3f3; 146 | } 147 | 148 | .styled-table tbody tr:last-of-type { 149 | border-bottom: 2px solid #009879; 150 | } 151 | 152 | .styled-table tbody tr.active-row { 153 | font-weight: bold; 154 | color: #009879; 155 | } 156 | 157 | /* SUGGESTION STUFF */ 158 | 159 | .suggestion-taker { 160 | background: white; 161 | border: thin solid 009879; 162 | border-radius: 5px; 163 | border-left: 12px solid #009879; 164 | display: flex; 165 | } 166 | 167 | .suggestion-taker .accept-area { 168 | background: #009879; 169 | } 170 | 171 | .suggestion-taker .accept-area, .suggestion-taker .reject-area { 172 | display: inline; 173 | } 174 | 175 | .suggestion-taker button { 176 | border: none; 177 | cursor: pointer; 178 | display: inline; 179 | } 180 | 181 | .suggestion-taker .accept-button { 182 | background-color: #009879; 183 | color: white; 184 | } 185 | 186 | .suggestion-taker .reject-area, .suggestion-taker .reject-button { 187 | background-color: #dddddd; 188 | border-radius: 5px; 189 | } -------------------------------------------------------------------------------- /sites/docs/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /sites/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["es2022", "ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": false, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": false 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /sites/docs/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /sites/docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | // for production builds swap to using the published version of prong editor, rather than the local one 5 | function myPlugin() { 6 | return { 7 | name: "transform-file", 8 | 9 | transform(src, id) { 10 | if ( 11 | process.env.NODE_ENV === "production" && 12 | id.includes("sites/docs") && 13 | !id.includes("node_modules") && 14 | (id.endsWith(".ts") || id.endsWith(".tsx")) 15 | ) { 16 | return { 17 | code: src 18 | .replaceAll( 19 | "../../../../packages/prong-editor/src/index", 20 | "prong-editor" 21 | ) 22 | .replaceAll( 23 | "../../../packages/prong-editor/src/stylesheets/styles.css", 24 | "prong-editor/style.css" 25 | ), 26 | map: null, 27 | }; 28 | } 29 | }, 30 | }; 31 | } 32 | 33 | export default defineConfig({ 34 | plugins: [myPlugin(), react({})], 35 | }); 36 | --------------------------------------------------------------------------------