diff --git a/doc/manual/souce/components.md b/doc/manual/souce/components.md index 7af1898..e01dbbf 100644 --- a/doc/manual/souce/components.md +++ b/doc/manual/souce/components.md @@ -45,11 +45,14 @@ Important paths: ```text editors/lezer-decodal/decodal.grammar -site/decodal-site/src/lib/codemirror/decodal-parser.js -site/decodal-site/src/lib/codemirror/decodal-parser.terms.js -site/decodal-site/src/lib/codemirror/decodal.js +packages/decodal-codemirror/src/decodal.js +packages/decodal-codemirror/src/decodal-parser.js +packages/decodal-codemirror/src/decodal-parser.terms.js ``` +The npm package is `decodal-codemirror`. +The JSR package is `@hare/decodal-codemirror`. + The Lezer grammar is derived from the canonical grammar documentation, but it is not a literal copy of the EBNF. Precedence and token conflict handling are represented in the Lezer grammar in the form CodeMirror needs. diff --git a/doc/manual/souce/development.md b/doc/manual/souce/development.md index b48b265..736fe6f 100644 --- a/doc/manual/souce/development.md +++ b/doc/manual/souce/development.md @@ -68,10 +68,10 @@ site/decodal-site/src/pages/playground.astro site/decodal-site/src/scripts/playground.js site/decodal-site/src/scripts/playground-examples.js site/decodal-site/src/layouts/ManualLayout.astro -site/decodal-site/src/lib/codemirror/decodal.js -site/decodal-site/src/lib/codemirror/decodal-parser.js site/decodal-site/src/lib/docs.js site/decodal-site/src/lib/highlight.js +packages/decodal-codemirror/src/decodal.js +packages/decodal-codemirror/src/decodal-parser.js crates/decodal-wasm/src/lib.rs ``` @@ -111,8 +111,30 @@ The npm package name is `decodal-wasm`. The JSR package name is `@hare/decodal-wasm`. JSR currently expects a single SPDX license identifier in `jsr.json`; the WebAssembly package metadata uses `MIT` while the Rust workspace remains dual licensed as `MIT OR Apache-2.0`. -The playground editor uses CodeMirror 6 with the generated Lezer parser in `src/lib/codemirror/decodal-parser.js`. +The playground editor uses the `decodal-codemirror` package with the generated Lezer parser in `packages/decodal-codemirror/src/decodal-parser.js`. The canonical grammar is documented in `doc/manual/souce/language/grammar.md`; regenerate the Lezer parser when that grammar or `editors/lezer-decodal/decodal.grammar` changes. + +Publish the CodeMirror package from its package directory: + +```sh +cd packages/decodal-codemirror +npm install +npm publish +npx jsr publish +``` + +Run dry-runs first when preparing a release: + +```sh +npm install +npm pack --dry-run +npx jsr publish --dry-run +``` + +The npm package name is `decodal-codemirror`. +The JSR package name is `@hare/decodal-codemirror`. +JSR currently expects a single SPDX license identifier in `jsr.json`; the CodeMirror package metadata uses `MIT` while the Rust workspace remains dual licensed as `MIT OR Apache-2.0`. + The documentation build still uses the lightweight JavaScript fallback highlighter so Astro can render Markdown without initializing WASM at build time. To run the site locally: @@ -194,7 +216,7 @@ When the Decodal syntax changes: ```sh cd site/decodal-site -npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o src/lib/codemirror/decodal-parser.js +npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o ../../packages/decodal-codemirror/src/decodal-parser.js ``` 5. Add or update corpus/tests/examples. diff --git a/editors/lezer-decodal/README.md b/editors/lezer-decodal/README.md index 4703182..a8ce2fe 100644 --- a/editors/lezer-decodal/README.md +++ b/editors/lezer-decodal/README.md @@ -12,7 +12,7 @@ Regenerate the CodeMirror parser from the site directory: ```sh cd ../../site/decodal-site -npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o src/lib/codemirror/decodal-parser.js +npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o ../../packages/decodal-codemirror/src/decodal-parser.js ``` -The generated parser is committed with the site because the playground imports it directly. +The generated parser is committed with the `decodal-codemirror` package because both the playground and external Web editors consume it. diff --git a/packages/decodal-codemirror/README.md b/packages/decodal-codemirror/README.md new file mode 100644 index 0000000..9442b28 --- /dev/null +++ b/packages/decodal-codemirror/README.md @@ -0,0 +1,54 @@ +# decodal-codemirror + +CodeMirror 6 language support for Decodal. + +This package provides the Lezer parser metadata, highlighting tags, indentation rules, and block folding used by the Decodal Web playground. +It does not include the Decodal runtime or WebAssembly evaluator. +Use `decodal-wasm` when you want to evaluate Decodal in a browser. + +## Install + +```sh +npm install decodal-codemirror +``` + +JSR: + +```sh +deno add jsr:@hare/decodal-codemirror +``` + +## Usage + +```js +import { basicSetup } from 'codemirror'; +import { EditorView } from '@codemirror/view'; +import { decodal } from 'decodal-codemirror'; + +new EditorView({ + doc: 'Server = { port = Int default 8080; };', + extensions: [ + basicSetup, + decodal(), + ], + parent: document.querySelector('#editor'), +}); +``` + +## Exports + +```js +import { + decodal, + decodalLanguage, + decodalHighlightStyle, +} from 'decodal-codemirror'; +``` + +`decodal()` accepts a small options object: + +```js +decodal({ highlight: false }) +``` + +Use this when you want the language support without the bundled highlight style. diff --git a/packages/decodal-codemirror/jsr.json b/packages/decodal-codemirror/jsr.json new file mode 100644 index 0000000..8bf6aa1 --- /dev/null +++ b/packages/decodal-codemirror/jsr.json @@ -0,0 +1,24 @@ +{ + "name": "@hare/decodal-codemirror", + "version": "0.1.2", + "license": "MIT", + "exports": "./src/mod.ts", + "imports": { + "@codemirror/language": "npm:@codemirror/language@^6.12.4", + "@lezer/highlight": "npm:@lezer/highlight@^1.2.3", + "@lezer/lr": "npm:@lezer/lr@^1.4.10" + }, + "publish": { + "include": [ + "README.md", + "src/mod.ts", + "src/decodal.js", + "src/decodal.d.ts", + "src/decodal.js.d.ts", + "src/decodal-parser-jsr.js", + "src/decodal-parser.js", + "src/decodal-parser.terms.js", + "package.json" + ] + } +} diff --git a/packages/decodal-codemirror/package-lock.json b/packages/decodal-codemirror/package-lock.json new file mode 100644 index 0000000..d7cd3cf --- /dev/null +++ b/packages/decodal-codemirror/package-lock.json @@ -0,0 +1,101 @@ +{ + "name": "decodal-codemirror", + "version": "0.1.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "decodal-codemirror", + "version": "0.1.2", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@codemirror/language": "^6.12.4", + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.10" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", + "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz", + "integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + } + } +} diff --git a/packages/decodal-codemirror/package.json b/packages/decodal-codemirror/package.json new file mode 100644 index 0000000..934ad75 --- /dev/null +++ b/packages/decodal-codemirror/package.json @@ -0,0 +1,35 @@ +{ + "name": "decodal-codemirror", + "version": "0.1.2", + "description": "CodeMirror 6 language support for Decodal.", + "type": "module", + "license": "MIT OR Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://gitea.hareworks.net/Hare/Decodal.git", + "directory": "packages/decodal-codemirror" + }, + "exports": { + ".": { + "types": "./src/decodal.d.ts", + "import": "./src/decodal.js" + }, + "./parser": "./src/decodal-parser.js", + "./terms": "./src/decodal-parser.terms.js" + }, + "types": "./src/decodal.d.ts", + "files": [ + "README.md", + "src/" + ], + "keywords": [ + "decodal", + "codemirror", + "lezer" + ], + "dependencies": { + "@codemirror/language": "^6.12.4", + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.10" + } +} diff --git a/packages/decodal-codemirror/src/decodal-parser-jsr.js b/packages/decodal-codemirror/src/decodal-parser-jsr.js new file mode 100644 index 0000000..5941995 --- /dev/null +++ b/packages/decodal-codemirror/src/decodal-parser-jsr.js @@ -0,0 +1,16 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import {LRParser} from "npm:@lezer/lr@^1.4.10" +export const parser = LRParser.deserialize({ + version: 14, + states: "7vQYQPOOO#nQPO'#CaOOQO'#Cr'#CrO$wQPO'#CyO&UQPO'#DOO&^QPO'#DSO&eQPO'#DWO&mQPO'#CqO$wQPO'#DcO&wQPO'#DhOOQO'#Cq'#CqOOQO'#Cp'#CpO&|QPO'#CoO$wQPO'#CoOOQO'#Cn'#CnO)fQPO'#CmO.XQPO'#ClO0]QPO'#CkOOQO'#Cj'#CjO2TQPO'#CiO3xQPO'#ChO5jQPO'#CgO7UQPO'#CfOOQO'#Ce'#CeO7`QPO'#C`O7eQPO'#C_OOQO'#Dz'#DzQYQPOOO8xQPO'#D{O8}QPO,58{OOQO,59e,59eO9VQPO'#CaOOQO,59j,59jO9_QPO,59jOOQO,59n,59nO9gQPO,59nO9oQPO'#EOO9tQPO'#DYO9|QPO,59rO:RQPO'#CqO:`QPO'#D^O:hQQO,59vO:mQPO,59vO:rQPO,59]O:wQPO,59}OOQO,5:S,5:SO:|QPO'#DjOOQO,59[,59[O;TQPO,59[OOQO,59Z,59ZO$wQPO,59YO$wQPO,59XO$wQPO,59WOOQO'#Dr'#DrO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zOOQO,58y,58yOOQO-E7x-E7xOOQO,5:g,5:gOOQO-E7y-E7yO;YQPO1G/UO;bQPO1G/UOOQO1G/U1G/UO;jQPO1G/YO;qQPO1G/YOOQO1G/Y1G/YOOQO,5:j,5:jOOQO-E7|-E7|O$wQPO1G/^O$wQPO,59yO;yQPO,59xOZAN>ZOJeQPOAN>ZOOQO-E8O-E8OOOQOG23uG23uP context.column(context.node.from) + context.unit, + Array: context => context.column(context.node.from) + context.unit, + MatchExpression: context => context.column(context.node.from) + context.unit, + LetExpression: context => context.column(context.node.from) + context.unit, + }), + foldNodeProp.add({ + Object: foldDelimited('{', '}'), + Array: foldDelimited('[', ']'), + MatchExpression: foldDelimited('{', '}'), + }), + ], +}); + +function foldDelimited(open: string, close: string) { + return (node: { from: number; to: number }, state: { doc: { sliceString(from: number, to: number): string } }) => { + const text = state.doc.sliceString(node.from, node.to); + const openIndex = text.indexOf(open); + const closeIndex = text.lastIndexOf(close); + if (openIndex < 0 || closeIndex <= openIndex) return null; + const from = node.from + openIndex + open.length; + const to = node.from + closeIndex; + return from < to ? { from, to } : null; + }; +} + +export const decodalLanguage: LRLanguage = LRLanguage.define({ + parser: parserWithMetadata, + languageData: { + commentTokens: { line: '#' }, + closeBrackets: { brackets: ['(', '[', '{', '"'] }, + }, +}); + +export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([ + { tag: t.keyword, color: '#93c5fd', fontWeight: '700' }, + { tag: t.variableName, color: 'inherit' }, + { tag: t.bool, color: '#fbbf24' }, + { tag: t.number, color: '#fbbf24' }, + { tag: t.string, color: '#86efac' }, + { tag: t.regexp, color: '#86efac' }, + { tag: t.lineComment, color: '#94a3b8', fontStyle: 'italic' }, + { tag: t.operator, color: '#f9a8d4' }, + { tag: [t.brace, t.squareBracket, t.paren, t.punctuation], color: '#f9a8d4' }, +]); + +export interface DecodalOptions { + highlight?: boolean; +} + +export function decodal(options: DecodalOptions = {}): LanguageSupport { + const { highlight = true } = options; + return new LanguageSupport( + decodalLanguage, + highlight ? [syntaxHighlighting(decodalHighlightStyle)] : [], + ); +} diff --git a/site/decodal-site/astro.config.mjs b/site/decodal-site/astro.config.mjs index 0654f13..a542924 100644 --- a/site/decodal-site/astro.config.mjs +++ b/site/decodal-site/astro.config.mjs @@ -2,4 +2,9 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ output: 'static', + vite: { + resolve: { + preserveSymlinks: true, + }, + }, }); diff --git a/site/decodal-site/package-lock.json b/site/decodal-site/package-lock.json index 24b45bb..cd9fdc8 100644 --- a/site/decodal-site/package-lock.json +++ b/site/decodal-site/package-lock.json @@ -10,12 +10,12 @@ "dependencies": { "@astrojs/check": "^0.9.4", "@codemirror/language": "^6.12.4", - "@codemirror/state": "^6.7.1", "@codemirror/view": "^6.43.6", "@lezer/highlight": "^1.2.3", "@lezer/lr": "^1.4.10", "astro": "^4.16.18", "codemirror": "^6.0.2", + "decodal-codemirror": "file:../../packages/decodal-codemirror", "marked": "^12.0.2" }, "devDependencies": { @@ -23,6 +23,15 @@ "wrangler": "^4.104.0" } }, + "../../packages/decodal-codemirror": { + "version": "0.1.2", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@codemirror/language": "^6.12.4", + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.10" + } + }, "node_modules/@astrojs/check": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.9.tgz", @@ -3237,6 +3246,10 @@ } } }, + "node_modules/decodal-codemirror": { + "resolved": "../../packages/decodal-codemirror", + "link": true + }, "node_modules/decode-named-character-reference": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", diff --git a/site/decodal-site/package.json b/site/decodal-site/package.json index aa289d4..0127f54 100644 --- a/site/decodal-site/package.json +++ b/site/decodal-site/package.json @@ -14,12 +14,12 @@ "dependencies": { "@astrojs/check": "^0.9.4", "@codemirror/language": "^6.12.4", - "@codemirror/state": "^6.7.1", "@codemirror/view": "^6.43.6", "@lezer/highlight": "^1.2.3", "@lezer/lr": "^1.4.10", "astro": "^4.16.18", "codemirror": "^6.0.2", + "decodal-codemirror": "file:../../packages/decodal-codemirror", "marked": "^12.0.2" }, "devDependencies": { diff --git a/site/decodal-site/src/pages/index.astro b/site/decodal-site/src/pages/index.astro index 4db3ba8..bc726d7 100644 --- a/site/decodal-site/src/pages/index.astro +++ b/site/decodal-site/src/pages/index.astro @@ -40,6 +40,14 @@ Production = Server & { JSR package jsr.io/@hare/decodal-wasm + + CodeMirror package + npmjs.com/package/decodal-codemirror + + + CodeMirror on JSR + jsr.io/@hare/decodal-codemirror + diff --git a/site/decodal-site/src/scripts/playground.js b/site/decodal-site/src/scripts/playground.js index 1fcfca2..bf9196b 100644 --- a/site/decodal-site/src/scripts/playground.js +++ b/site/decodal-site/src/scripts/playground.js @@ -1,7 +1,7 @@ import init, { evaluateProject } from '../wasm/decodal_wasm.js'; import { EditorView, basicSetup } from 'codemirror'; import { keymap } from '@codemirror/view'; -import { decodal } from '../lib/codemirror/decodal.js'; +import { decodal } from 'decodal-codemirror'; import { playgroundExamples } from './playground-examples.js'; const STORAGE_KEY = 'decodal-playground-project-v1';