Add canonical grammar and CodeMirror playground

This commit is contained in:
Keisuke Hirata 2026-07-08 23:41:26 +09:00
parent a07f4c48aa
commit ee031bd2e8
No known key found for this signature in database
15 changed files with 709 additions and 81 deletions

View File

@ -58,7 +58,11 @@ Important files:
```text
site/decodal-site/src/pages/docs/[...slug].astro
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
crates/decodal-wasm/src/lib.rs
@ -81,7 +85,8 @@ site/decodal-site/src/wasm/
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm package first.
The playground editor uses `tokenizeSource` from `decodal-wasm` for token spans and maps those tokens to HTML classes in `src/lib/highlight.js`.
The playground editor uses CodeMirror 6 with the generated Lezer parser in `src/lib/codemirror/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.
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:
@ -156,12 +161,18 @@ The generated parser files under `editors/tree-sitter-decodal/src/` are committe
When the Decodal syntax changes:
1. Update `grammar.js`.
2. Run `npx tree-sitter generate`.
3. Add or update corpus tests in `corpus/`.
4. Update highlight queries in `queries/highlights.scm` if token names changed.
5. Run `npx tree-sitter test`.
6. Run Rust checks from the repository root if the language parser or examples also changed.
1. Update the canonical EBNF in `doc/manual/souce/language/grammar.md`.
2. Update the Rust parser/tokenizer as needed.
3. Update `editors/tree-sitter-decodal/grammar.js` and run `npx tree-sitter generate` / `npx tree-sitter test`.
4. Update `editors/lezer-decodal/decodal.grammar` and regenerate the CodeMirror parser:
```sh
cd site/decodal-site
npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o src/lib/codemirror/decodal-parser.js
```
5. Add or update corpus/tests/examples.
6. Run Rust and site checks from the repository root.
## Development shell

View File

@ -0,0 +1,112 @@
# Grammar
This page is the canonical grammar reference for Decodal syntax.
Parser implementations such as the Rust parser, Tree-sitter grammar, and Lezer grammar should follow this grammar and may add implementation-specific precedence annotations where needed.
## Lexical grammar
```ebnf
source_character = ? any Unicode scalar value ? ;
newline = "\n" | "\r\n" | "\r" ;
space = " " | "\t" | newline ;
comment = "#" , { ? any character except newline ? } ;
digit = "0" … "9" ;
letter = "A" … "Z" | "a" … "z" ;
identifier = letter , { letter | digit | "_" } ;
integer = digit , { digit } ;
float = digit , { digit } , "." , digit , { digit } ;
string = '"' , { string_character | escape } , '"' ;
string_character = ? any character except '"', "\\", or newline ? ;
escape = "\\" , source_character ;
regex = "/" , regex_character , { regex_character } , "/" ;
regex_character = escape | ? any character except "/", "\\", or newline ? ;
```
Whitespace and comments separate tokens and are otherwise ignored by the parser.
Comments are retained by public tokenizer APIs for editor tooling.
## Syntactic grammar
```ebnf
module = { statement } ;
statement = field_definition , [ ";" ]
| expression , [ ";" ] ;
expression = default_expression ;
default_expression = patch_expression , [ "default" , default_expression ] ;
patch_expression = compose_expression , { "//" , compose_expression } ;
compose_expression = logical_or_expression , { "&" , logical_or_expression } ;
logical_or_expression = logical_and_expression , { "||" , logical_and_expression } ;
logical_and_expression = comparison_expression , { "&&" , comparison_expression } ;
comparison_expression = concat_expression , [ comparison_operator , concat_expression ] ;
concat_expression = additive_expression , { "++" , additive_expression } ;
additive_expression = multiplicative_expression , { ( "+" | "-" ) , multiplicative_expression } ;
multiplicative_expression = unary_expression , { ( "*" | "/" ) , unary_expression } ;
unary_expression = [ "!" | "-" ] , postfix_expression ;
postfix_expression = primary_expression , { call_suffix | path_suffix } ;
call_suffix = "(" , [ argument_list ] , ")" ;
path_suffix = "." , identifier ;
primary_expression = literal
| identifier
| comparison_constraint
| object
| array
| let_expression
| function_expression
| match_expression
| import_expression
| "(" , expression , ")" ;
literal = string | integer | float | "true" | "false" | regex ;
comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ;
object = "{" , [ field_definition , { ";" , field_definition } , [ ";" ] ] , "}" ;
field_definition = field_path , "=" , expression ;
field_path = identifier , { "." , identifier } ;
array = "[" , [ expression , { "," , expression } , [ "," ] ] , "]" ;
let_expression = "let" , { field_definition , ";" } , "in" , expression ;
function_expression = "(" , [ parameter_list ] , ")" , "=>" , expression ;
parameter_list = parameter , { "," , parameter } , [ "," ] ;
parameter = identifier , [ ":" , expression ] ;
match_expression = "match" , expression , "{" , [ match_arm , { ";" , match_arm } , [ ";" ] ] , "}" ;
match_arm = pattern , ":" , expression ;
pattern = "_" | expression ;
import_expression = "import" , string ;
argument_list = expression , { "," , expression } , [ "," ] ;
```
## Precedence
Precedence is highest first.
1. function call and field path reference
2. unary `!` and `-`
3. `*` and `/`
4. `+` and `-`
5. `++`
6. `==`, `!=`, `<`, `<=`, `>`, `>=`
7. `&&`
8. `||`
9. `&`
10. `//`
11. `default`
Binary operators are left-associative except `default`, which is right-associative.
## Tooling mapping
Syntax tooling should derive token categories from this grammar rather than making a tool-specific grammar canonical.
Tree-sitter and Lezer grammars are implementation artifacts that follow this page.

View File

@ -0,0 +1,18 @@
# lezer-decodal
Lezer implementation of the Decodal grammar for CodeMirror 6.
The canonical grammar is documented in:
```text
../../doc/manual/souce/language/grammar.md
```
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
```
The generated parser is committed with the site because the playground imports it directly.

View File

@ -0,0 +1,172 @@
@top Source { Statement* }
Statement {
FieldDefinition semicolon? |
Expression semicolon?
}
Expression { DefaultExpression }
DefaultExpression {
PatchExpression |
PatchExpression !default default DefaultExpression
}
PatchExpression {
ComposeExpression |
PatchExpression !patch slashSlash ComposeExpression
}
ComposeExpression {
LogicalOrExpression |
ComposeExpression !compose amp LogicalOrExpression
}
LogicalOrExpression {
LogicalAndExpression |
LogicalOrExpression !or pipePipe LogicalAndExpression
}
LogicalAndExpression {
ComparisonExpression |
LogicalAndExpression !and ampAmp ComparisonExpression
}
ComparisonExpression {
ConcatExpression |
ConcatExpression !compare CompareOperator ConcatExpression
}
ConcatExpression {
AdditiveExpression |
ConcatExpression !concat plusPlus AdditiveExpression
}
AdditiveExpression {
MultiplicativeExpression |
AdditiveExpression !add (plus | minus) MultiplicativeExpression
}
MultiplicativeExpression {
UnaryExpression |
MultiplicativeExpression !multiply (star | slash) UnaryExpression
}
UnaryExpression {
PostfixExpression |
(bang | minus) !unary UnaryExpression
}
PostfixExpression {
PrimaryExpression |
PostfixExpression !call CallSuffix |
PostfixExpression !path dot identifier
}
CallSuffix { lParen ArgumentList? rParen }
ArgumentList { Expression (comma Expression)* comma? }
PrimaryExpression {
Literal |
identifier |
ComparisonConstraint |
Object |
Array |
LetExpression |
FunctionExpression |
MatchExpression |
ImportExpression |
lParen Expression rParen
}
Literal { string | integer | float | true | false | regex }
CompareOperator { equalEqual | bangEqual | lt | lte | gt | gte }
ComparisonConstraint { (lt | lte | gt | gte) Expression }
Object { lBrace (FieldDefinition (semicolon FieldDefinition)* semicolon?)? rBrace }
FieldDefinition { FieldPath equal Expression }
FieldPath { identifier !fieldPath (dot identifier)* }
Array { lBracket (Expression (comma Expression)* comma?)? rBracket }
LetExpression { let FieldDefinitionList in Expression }
FieldDefinitionList { (FieldDefinition semicolon)* }
FunctionExpression { lParen ParameterList? rParen arrow Expression }
ParameterList { Parameter (comma Parameter)* comma? }
Parameter { identifier colon Expression }
MatchExpression { match Expression lBrace (MatchArm (semicolon MatchArm)* semicolon?)? rBrace }
MatchArm { Pattern colon Expression }
Pattern { underscore | Expression }
ImportExpression { import string }
@precedence {
fieldPath @left,
default @right,
patch @left,
compose @left,
or @left,
and @left,
compare @left,
concat @left,
add @left,
multiply @left,
unary,
call @left,
path @left
}
@tokens {
let { "let" }
in { "in" }
match { "match" }
import { "import" }
default { "default" }
true { "true" }
false { "false" }
identifier { $[A-Za-z_] $[A-Za-z0-9_]* }
integer { $[0-9]+ }
float { $[0-9]+ "." $[0-9]+ }
string { '"' (!["\\\n] | "\\" _)* '"' }
regex { "/" (![/\\\n] | "\\" _)+ "/" }
comment { "#" ![\n]* }
lBrace { "{" }
rBrace { "}" }
lBracket { "[" }
rBracket { "]" }
lParen { "(" }
rParen { ")" }
semicolon { ";" }
comma { "," }
dot { "." }
colon { ":" }
equal { "=" }
equalEqual { "==" }
bang { "!" }
bangEqual { "!=" }
arrow { "=>" }
amp { "&" }
ampAmp { "&&" }
pipePipe { "||" }
plus { "+" }
plusPlus { "++" }
minus { "-" }
star { "*" }
slash { "/" }
slashSlash { "//" }
gt { ">" }
gte { ">=" }
lt { "<" }
lte { "<=" }
underscore { "_" }
whitespace { @whitespace+ }
@precedence { let, in, match, import, default, true, false, float, integer, underscore, identifier }
}
@skip { whitespace | comment }

View File

@ -0,0 +1,12 @@
# tree-sitter-decodal
Tree-sitter implementation of the Decodal grammar.
The canonical grammar is documented in:
```text
../../doc/manual/souce/language/grammar.md
```
This grammar is an editor/tooling implementation of that EBNF reference.
Generated parser files under `src/` are committed.

View File

@ -1,3 +1,6 @@
// Tree-sitter implementation of the canonical EBNF grammar in
// doc/manual/souce/language/grammar.md.
const PREC = {
DEFAULT: 1,
PATCH: 2,

View File

@ -9,10 +9,17 @@
"version": "0.1.0",
"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",
"marked": "^12.0.2"
},
"devDependencies": {
"@lezer/generator": "^1.8.0",
"wrangler": "^4.104.0"
}
},
@ -560,6 +567,87 @@
"node": ">=16"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.3",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.7.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"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/lint": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.42.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.37.0",
"crelt": "^1.0.5"
}
},
"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/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -1632,6 +1720,50 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"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/generator": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.8.0.tgz",
"integrity": "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.1.0",
"@lezer/lr": "^1.3.0"
},
"bin": {
"lezer-generator": "src/lezer-generator.cjs"
}
},
"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/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2981,6 +3113,21 @@
"node": ">=6"
}
},
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@ -3055,6 +3202,12 @@
"node": ">= 0.6"
}
},
"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/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -6429,6 +6582,12 @@
"node": ">=0.10.0"
}
},
"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/supports-color": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
@ -7126,6 +7285,12 @@
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"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"
},
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",

View File

@ -13,10 +13,17 @@
},
"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",
"marked": "^12.0.2"
},
"devDependencies": {
"@lezer/generator": "^1.8.0",
"wrangler": "^4.104.0"
}
}

View File

@ -0,0 +1,16 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
export const parser = LRParser.deserialize({
version: 14,
states: "7vQYQPOOO#nQPO'#C`OOQO'#Cn'#CnO$wQPO'#CoO&UQPO'#CpO&^QPO'#CqO&eQPO'#CrO&mQPO'#CmO$wQPO'#CwO&wQPO'#CzOOQO'#Cm'#CmOOQO'#Cl'#ClO&|QPO'#CkO$wQPO'#CkOOQO'#Cj'#CjO)fQPO'#CiO.XQPO'#ChO0]QPO'#CgOOQO'#Cf'#CfO2TQPO'#CeO3xQPO'#CdO5jQPO'#CcO7UQPO'#CbOOQO'#Ca'#CaO7`QPO'#C_O7eQPO'#C^OOQO'#DO'#DOQYQPOOO8xQPO'#DPO8}QPO,58zOOQO,59Z,59ZO9VQPO'#C`OOQO,59[,59[O9_QPO,59[OOQO,59],59]O9gQPO,59]O9oQPO'#DSO9tQPO'#CsO9|QPO,59^O:RQPO'#CmO:`QPO'#CuO:hQQO,59`O:mQPO,59`O:rQPO,59XO:wQPO,59cOOQO,59f,59fO:|QPO'#C{OOQO,59W,59WO;TQPO,59WOOQO,59V,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SOOQO'#C}'#C}O$wQPO,59RO$wQPO,59QO$wQPO,59PO$wQPO,59OO$wQPO,58}O$wQPO,58|O$wQPO,58yOOQO,58x,58xOOQO-E6|-E6|OOQO,59k,59kOOQO-E6}-E6}O;YQPO1G.vO;bQPO1G.vOOQO1G.v1G.vO;jQPO1G.wO;qQPO1G.wOOQO1G.w1G.wOOQO,59n,59nOOQO-E7Q-E7QO$wQPO1G.xO$wQPO,59bO;yQPO,59aO<RQPO,59aO$wQPO1G.zO<ZQQO1G.zOOQO1G.s1G.sO<`QPO1G.}O<jQPO'#C|OOQO,59g,59gO<rQPO,59gOOQO1G.r1G.rOOQO1G.p1G.pO<wQPO1G.oO@_QPO1G.nOAyQPO1G.mOOQO1G.l1G.lOCqQPO1G.kOEfQPO1G.jOFxQPO1G.iOOQO1G.h1G.hOOQO1G.e1G.eOOQO1G.o1G.oOOQO1G.n1G.nOOQO1G.m1G.mOOQO1G.k1G.kOOQO1G.j1G.jOOQO1G.i1G.iOOQO,59l,59lOOQO7+$b7+$bOHfQPO7+$bOOQO-E7O-E7OOOQO,59m,59mOOQO7+$c7+$cOHnQPO7+$cOOQO-E7P-E7POOQO7+$d7+$dOOQO1G.|1G.|OHuQPO'#CvOOQO,59o,59oOHzQPO1G.{OOQO-E7R-E7ROOQO7+$f7+$fO$wQPO7+$fOOQO'#Cy'#CyOISQPO'#CxOOQO7+$i7+$iOIXQPO7+$iOIaQPO,59hOIhQPO,59hOOQO1G/R1G/ROOQO<<G|<<G|PIpQPO'#DQOOQO<<G}<<G}P$wQPO'#DRPIuQPO'#DTOOQO<<HQ<<HQO$wQPO,59dOIzQPO<<HTOJUQPO<<HTOOQO<<HT<<HTOJ^QPO1G/SOOQO1G/O1G/OOOQO,59p,59pOOQOAN=oAN=oOJeQPOAN=oOOQO-E7S-E7SOOQOG23ZG23ZP<cQPO'#DUO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59PO$wQPO,59OO$wQPO,58}OJoQPO1G.nOKVQPO'#ChOKjQPO'#CgOL^QPO'#CdOM^QPO'#CcONaQPO'#CbO$wQPO,59SO$wQPO,59QO$wQPO,58|O! jQPO1G.mO!!QQPO'#CgO!#_QPO'#CeO$wQPO,59RO$wQPO,59QO$wQPO,59PO$wQPO,59OO$wQPO,58}O$wQPO,58|O$wQPO,58yO!$[QPO1G.kO!$rQPO1G.jO!%YQPO1G.iO!%zQPO'#CeO!&rQPO'#CdO!'gQPO'#CcO!(XQPO'#CbO!(vQPO'#C_",
stateData: "!)Y~O{OS|OS~O}PO!QQO!RQO!SQO!TQO!UQO!VQO!WRO!XRO!YRO!ZRO![SO!_TO!bUO!dVO!hWO!jXO!k]O!l]O~O!WaX!XaX!YaX!ZaX!daX!laX!maX!naX!oaX!paX!qaX!raX!saX!taX!uaX!vaX!waX~O!OlOyaX}aX!PSX!QaX!RaX!SaX!TaX!UaX!VaX![aX!]aX!_aX!baX!haX!jaX!kaX~P!gO}YO!QQO!RQO!SQO!TQO!UQO!VQO!WRO!XRO!YRO!ZRO![SO!_TO!bUO!dVO!hWO!jXO!k]O!l]O~O}oO!^pO~O!arO~P$wO}oO!cgP~O}wO!fyO~P]O!Q}O~O!O!QO!d!OOy_X}_X!Q_X!R_X!S_X!T_X!U_X!V_X!W_X!X_X!Y_X!Z_X![_X!]_X!__X!b_X!h_X!j_X!k_X!l_X!m_X!n_X!o_X!p_X!q_X!r_X!s_X!t_X!u_X!v_X!w_X!`_X!a_X!f_X!^_X!e_X~O!m!SO!n!SOy]X}]X!Q]X!R]X!S]X!T]X!U]X!V]X!W]X!X]X!Y]X!Z]X![]X!]]X!_]X!b]X!d]X!h]X!j]X!k]X!l]X!o]X!p]X!q]X!r]X!s]X!t]X!u]X!v]X!w]X!O]X!`]X!a]X!f]X!^]X!e]X~Oy[X}[X!Q[X!R[X!S[X!T[X!U[X!V[X!W[X!X[X!Y[X!Z[X![[X!][X!_[X!b[X!d[X!h[X!j[X!k[X!p[X!q[X!r[X!s[X!t[X!u[X!v[X!w[X!`[X!a[X!f[X!^[X!e[X~O!l!TO!o!TO~P,OO!W!VO!X!VO!Y!VO!Z!VO!q!VO!r!VOyZX}ZX!QZX!RZX!SZX!TZX!UZX!VZX![ZX!]ZX!_ZX!bZX!dZX!hZX!jZX!kZX!lZX!sZX!tZX!uZX!vZX!wZX~O!p!UO~P.cOyXX}XX!QXX!RXX!SXX!TXX!UXX!VXX!WXX!XXX!YXX!ZXX![XX!]XX!_XX!bXX!dXX!hXX!jXX!kXX!lXX!tXX!uXX!vXX!wXX~O!s!XO~P0dOyWX}WX!QWX!RWX!SWX!TWX!UWX!VWX!WWX!XWX!YWX!ZWX![WX!]WX!_WX!bWX!dWX!hWX!jWX!kWX!lWX!uWX!vWX!wWX~O!t!YO~P2[OyVX}VX!QVX!RVX!SVX!TVX!UVX!VVX!WVX!XVX!YVX!ZVX![VX!]VX!_VX!bVX!dVX!hVX!jVX!kVX!lVX!vVX!wVX~O!u!ZO~P4POyUX}UX!QUX!RUX!SUX!TUX!UUX!VUX!WUX!XUX!YUX!ZUX![UX!]UX!_UX!bUX!dUX!hUX!jUX!kUX!lUX~O!v![O!w!]O~P5qO!P!^O~O!]!_OyQX}QX!QQX!RQX!SQX!TQX!UQX!VQX!WQX!XQX!YQX!ZQX![QX!_QX!bQX!dQX!hQX!jQX!kQX!lQX~O}!aO~O!OlO!PSa~O!OlO!PSX~O!]!cO!^!eO~O!`!fO!a!hO~O!]!iO~O}oO!cgX~O!c!kO~O!e!lO!OaX!faX~P!gO!`!mO!fiX~O!g!oO~O!f!pO~O!f!qO~O![!rO~O!f!tO~P$wO}!vO~O}oO!^#YO~O!]#ZO!^#YO~O!a#^O~P$wO!`#_O!a#^O~O}#cO!fia~O!`#eO!fia~O!g#hO~O!^#kO!i#iO~P$wO!`#mO!fpX~O!f#oO~O!m!SO!n!SOy]i}]i!Q]i!R]i!S]i!T]i!U]i!V]i!W]i!X]i!Y]i!Z]i![]i!]]i!_]i!b]i!d]i!h]i!j]i!k]i!l]i!o]i!p]i!q]i!r]i!s]i!t]i!u]i!v]i!w]i!`]i!a]i!f]i!^]i!e]i~O!o!TO!W[i!X[i!Y[i!Z[i![[i!][i!p[i!q[i!r[i!s[i!t[i!u[i!v[i!w[i~Oy[i}[i!Q[i!R[i!S[i!T[i!U[i!V[i!_[i!b[i!d[i!h[i!j[i!k[i!l[i~P?^O![Zi!]Zi!sZi!tZi!uZi!vZi!wZi~O!p!UOyZi}Zi!QZi!RZi!SZi!TZi!UZi!VZi!WZi!XZi!YZi!ZZi!_Zi!bZi!dZi!hZi!jZi!kZi!lZi~PAbO![Xi!]Xi!tXi!uXi!vXi!wXi~O!s!XOyXi}Xi!QXi!RXi!SXi!TXi!UXi!VXi!WXi!XXi!YXi!ZXi!_Xi!bXi!dXi!hXi!jXi!kXi!lXi~PC]O![Wi!]Wi!uWi!vWi!wWi~O!t!YOyWi}Wi!QWi!RWi!SWi!TWi!UWi!VWi!WWi!XWi!YWi!ZWi!_Wi!bWi!dWi!hWi!jWi!kWi!lWi~PETO!u!ZOyVi}Vi!QVi!RVi!SVi!TVi!UVi!VVi!WVi!XVi!YVi!ZVi![Vi!]Vi!_Vi!bVi!dVi!hVi!jVi!kVi!lVi!vVi!wVi~O}oO!^#pO~O!a#rO~P$wO!e!lO~O}#cO!fii~O!e#vO~O!]#wO!^#yO~O!fpa~P$wO!`#zO!fpa~O}oO~O}#cO~O!^#}O!i#iO~P$wO!]$OO!^#}O~O!fpi~P$wO!^$QO!i#iO~P$wO!l!TO!`[i!a[i!f[i!^[i!e[i~P?^O!l$SO!o$SO!O[X!m[X!n[X~P,OO!p$TO!OZX!mZX!nZX!oZX!`ZX!aZX!fZX!^ZX!eZX~P.cO!t$VO!OWX!mWX!nWX!oWX!pWX!qWX!rWX!sWX!`WX!aWX!fWX!^WX!eWX~P2[O!u$WO!OVX!mVX!nVX!oVX!pVX!qVX!rVX!sVX!tVX!`VX!aVX!fVX!^VX!eVX~P4PO!v$XO!w$bO!OUX!mUX!nUX!oUX!pUX!qUX!rUX!sUX!tUX!uUX!`UX!aUX!fUX!^UX!eUX~P5qO!p$`O!`Zi!aZi!fZi!^Zi!eZi~PAbO!W!VO!X!VO!Y!VO!Z!VO!p$`O!q!VO!r!VO!`ZX!aZX!sZX!tZX!uZX!vZX!wZX!fZX![ZX!]ZX!^ZX!eZX~O!s$aO!OXX!mXX!nXX!oXX!pXX!qXX!rXX!`XX!aXX!fXX!^XX!eXX~P0dO!s$gO!`Xi!aXi!fXi!^Xi!eXi~PC]O!t$hO!`Wi!aWi!fWi!^Wi!eWi~PETO!u$iO!`Vi!aVi!vVi!wVi!fVi![Vi!]Vi!^Vi!eVi~O!s$gO!`XX!aXX!tXX!uXX!vXX!wXX!fXX![XX!]XX!^XX!eXX~O!t$hO!`WX!aWX!uWX!vWX!wWX!fWX![WX!]WX!^WX!eWX~O!u$iO!`VX!aVX!vVX!wVX!fVX![VX!]VX!^VX!eVX~O!v$jO!w$kO!`UX!aUX!fUX![UX!]UX!^UX!eUX~O!P$lO~O!b!c!h!j!w!T!U!S!R!i}!T~",
goto: "2pyPPz!O!`!l#q$a%R%|&z'{(w*O+W,^-f.j/n/n/n/n/n0r/n0u0x/n1Q1Y/n1`1c1f1o1u1|2S2^2d2jTjOkSiOkQqSStUuV#X!c#Z#qShOk]$tSUu!c#Z#qSiOkQnRQsTQ{VQ|WQ!s!OS#Q!^$lY#]!f#_#m#s#zQ#a!kQ#b!lQ#g!oW#i!r#w$O$RQ#u#hR#{#v!OgORTVWk!O!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$lV#P!]$b$kWfOk!]!^Y$_R!k!o#h$bs$sTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lWeOk!]!^Q#O![Q#W$XY$^R!k!o#h$bQ$o$js$rTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lYdOk![!]!^Q!}!ZQ#V$W[$]R!k!o#h$X$bQ$n$iu$qTVW!O!f!l!r#_#m#s#v#w#z$O$R$j$k$l[cOk!Z![!]!^Q!|!YQ#U$V^$eR!k!o#h$W$X$bQ$m$hw$pTVW!O!f!l!r#_#m#s#v#w#z$O$R$i$j$k$l!hbORTVWk!O!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$V$W$X$b$h$i$j$k$lV!{!X$a$g`aOk!X!Y!Z![!]!^Q!z!WQ#T$Ub$[R!k!o#h$V$W$X$a$bQ$c$f{$dTVW!O!f!l!r#_#m#s#v#w#z$O$R$g$h$i$j$k$l!``OTVWk!O!W!X!Y!Z![!]!^!f!l!r#_#m#s#v#w#z$O$R$f$g$h$i$j$k$lQ!y!UQ#S$TQ$Y$`e$ZR!k!o#h$U$V$W$X$a$b!z_ORTVWk!O!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!x!TR#R$S#O^ORTVWk!O!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!R]R!w!S#T[ORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TZORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TYORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lRvURzVQxVV#d!m#e#tQ#l!rV#|#w$O$RX#j!r#w$O$RR!P[R!u!OQ!WaQ$U$[R$f$dQkOR!`kSmPoR!bmQ!dqR#[!dQ!gsS#`!g#nR#n!sQuUR!juQ!nxR#f!nQ#x#lR$P#x",
nodeNames: "⚠ Source Statement FieldDefinition FieldPath Expression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal ComparisonConstraint Object Array LetExpression FieldDefinitionList FunctionExpression ParameterList Parameter MatchExpression MatchArm Pattern ImportExpression CallSuffix ArgumentList CompareOperator",
maxTerm: 85,
skippedNodes: [0],
repeatNodeCount: 7,
tokenData: "=T~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q(m!Q![+T![!]+n!]!^+s!^!_+x!_!`,V!`!a,l!c!},y!}#O-[#P#Q-a#R#S-f#T#W,y#W#X-y#X#Y,y#Y#Z1Z#Z#],y#]#^3k#^#`,y#`#a6}#a#b8b#b#h,y#h#i:r#i#o,y#o#p<n#p#q<s#q#r=O#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY{~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!k~!_!`%R~%WO!r~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xO!Q~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PS|~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!u~vw'k~'pO!s~~'uO!d~~'zO!f~~(PO!m~~(UP!o~{|(X~(^O!p~~(cO!`~~(hO!l~~(mO!O~~(rW!n~OY)[Z!P)[!P!Q+O!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)_WOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)|O!V~~*PRO;'S)[;'S;=`*Y;=`O)[~*]XOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x;=`<%l)[<%lO)[~*{P;=`<%l)[~+TO!v~~+YQ!R~!O!P+`!Q![+T~+cP!Q![+f~+kP!S~!Q![+f~+sO!e~~+xO!]~~+}P!W~!_!`,Q~,VO!X~~,[Q!PP!_!`,b!`!a,g~,gO!q~Q,lO!gQ~,qP!Y~!_!`,t~,yO!Z~~-OS}~!Q![,y!c!},y#R#S,y#T#o,y~-aO!_~~-fO!a~~-mS!i~}~!Q![,y!c!},y#R#S,y#T#o,y~.OU}~!Q![,y!c!},y#R#S,y#T#X,y#X#Y.b#Y#o,y~.gU}~!Q![,y!c!},y#R#S,y#T#Y,y#Y#Z.y#Z#o,y~/OT}~!Q![,y!c!},y#R#S,y#T#U/_#U#o,y~/dU}~!Q![,y!c!},y#R#S,y#T#i,y#i#j/v#j#o,y~/{U}~!Q![,y!c!},y#R#S,y#T#`,y#`#a0_#a#o,y~0dU}~!Q![,y!c!},y#R#S,y#T#h,y#h#i0v#i#o,y~0}S!w~}~!Q![,y!c!},y#R#S,y#T#o,y~1`T}~!Q![,y!c!},y#R#S,y#T#U1o#U#o,y~1tU}~!Q![,y!c!},y#R#S,y#T#`,y#`#a2W#a#o,y~2]U}~!Q![,y!c!},y#R#S,y#T#g,y#g#h2o#h#o,y~2tU}~!Q![,y!c!},y#R#S,y#T#X,y#X#Y3W#Y#o,y~3_S!U~}~!Q![,y!c!},y#R#S,y#T#o,y~3pV}~!Q![,y!c!},y#R#S,y#T#a,y#a#b4V#b#c6j#c#o,y~4[U}~!Q![,y!c!},y#R#S,y#T#d,y#d#e4n#e#o,y~4sU}~!Q![,y!c!},y#R#S,y#T#c,y#c#d5V#d#o,y~5[U}~!Q![,y!c!},y#R#S,y#T#f,y#f#g5n#g#o,y~5sU}~!Q![,y!c!},y#R#S,y#T#h,y#h#i6V#i#o,y~6^S!j~}~!Q![,y!c!},y#R#S,y#T#o,y~6qS!c~}~!Q![,y!c!},y#R#S,y#T#o,y~7SU}~!Q![,y!c!},y#R#S,y#T#X,y#X#Y7f#Y#o,y~7kU}~!Q![,y!c!},y#R#S,y#T#h,y#h#i7}#i#o,y~8US!b~}~!Q![,y!c!},y#R#S,y#T#o,y~8gT}~!Q![,y!c!},y#R#S,y#T#U8v#U#o,y~8{U}~!Q![,y!c!},y#R#S,y#T#h,y#h#i9_#i#o,y~9dU}~!Q![,y!c!},y#R#S,y#T#V,y#V#W9v#W#o,y~9{U}~!Q![,y!c!},y#R#S,y#T#[,y#[#]:_#]#o,y~:fS!h~}~!Q![,y!c!},y#R#S,y#T#o,y~:wU}~!Q![,y!c!},y#R#S,y#T#f,y#f#g;Z#g#o,y~;`U}~!Q![,y!c!},y#R#S,y#T#i,y#i#j;r#j#o,y~;wU}~!Q![,y!c!},y#R#S,y#T#X,y#X#Y<Z#Y#o,y~<bS!T~}~!Q![,y!c!},y#R#S,y#T#o,y~<sO![~~<vP#p#q<y~=OO!t~~=TO!^~",
tokenizers: [0, 1],
topRules: {"Source":[0,1]},
tokenPrec: 2481
})

View File

@ -0,0 +1,35 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Source = 1,
Statement = 2,
FieldDefinition = 3,
FieldPath = 4,
Expression = 5,
DefaultExpression = 6,
PatchExpression = 7,
ComposeExpression = 8,
LogicalOrExpression = 9,
LogicalAndExpression = 10,
ComparisonExpression = 11,
ConcatExpression = 12,
AdditiveExpression = 13,
MultiplicativeExpression = 14,
UnaryExpression = 15,
PostfixExpression = 16,
PrimaryExpression = 17,
Literal = 18,
ComparisonConstraint = 19,
Object = 20,
Array = 21,
LetExpression = 22,
FieldDefinitionList = 23,
FunctionExpression = 24,
ParameterList = 25,
Parameter = 26,
MatchExpression = 27,
MatchArm = 28,
Pattern = 29,
ImportExpression = 30,
CallSuffix = 31,
ArgumentList = 32,
CompareOperator = 33

View File

@ -0,0 +1,57 @@
import { LRLanguage, LanguageSupport, HighlightStyle, syntaxHighlighting, foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language';
import { styleTags, tags as t } from '@lezer/highlight';
import { parser } from './decodal-parser.js';
const parserWithMetadata = parser.configure({
props: [
styleTags({
'let in match import default': t.keyword,
'true false': t.bool,
identifier: t.variableName,
string: t.string,
regex: t.regexp,
'integer float': t.number,
comment: t.lineComment,
'plus minus star slash plusPlus equal equalEqual bang bangEqual amp ampAmp pipePipe slashSlash gt gte lt lte arrow default': t.operator,
'lBrace rBrace': t.brace,
'lBracket rBracket': t.squareBracket,
'lParen rParen': t.paren,
'semicolon comma dot colon': t.punctuation,
}),
indentNodeProp.add({
Object: context => 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: foldInside,
Array: foldInside,
MatchExpression: foldInside,
}),
],
});
export const decodalLanguage = LRLanguage.define({
parser: parserWithMetadata,
languageData: {
commentTokens: { line: '#' },
closeBrackets: { brackets: ['(', '[', '{', '"'] },
},
});
export const decodalHighlightStyle = 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 function decodal() {
return new LanguageSupport(decodalLanguage, [syntaxHighlighting(decodalHighlightStyle)]);
}

View File

@ -24,6 +24,7 @@ export const nav = [
slug: 'language',
children: [
{ title: 'Syntax', slug: 'language/syntax' },
{ title: 'Grammar', slug: 'language/grammar' },
{
title: 'Value',
slug: 'language/value',

View File

@ -29,13 +29,10 @@ import ManualLayout from '../layouts/ManualLayout.astro';
<button id="delete-file" class="danger-button" type="button">Delete file</button>
</aside>
<label class="pane input-pane">
<section class="pane input-pane">
<span id="active-file">Input</span>
<div class="editor-wrap">
<pre id="source-highlight" aria-hidden="true"></pre>
<textarea id="source" spellcheck="false"></textarea>
</div>
</label>
<div id="editor" class="code-editor"></div>
</section>
<section class="pane output-pane">
<span>Output</span>

View File

@ -1,12 +1,13 @@
import init, { evaluateProject, tokenizeSource } from '../wasm/decodal_wasm.js';
import { highlightDecodal, highlightDecodalTokens } from '../lib/highlight.js';
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 { playgroundExamples } from './playground-examples.js';
const STORAGE_KEY = 'decodal-playground-project-v1';
const starterProject = playgroundExamples[0];
const source = document.getElementById('source');
const sourceHighlight = document.getElementById('source-highlight');
const editorHost = document.getElementById('editor');
const output = document.getElementById('output');
const run = document.getElementById('run');
const status = document.getElementById('status');
@ -18,7 +19,63 @@ const exampleSelect = document.getElementById('example-select');
const loadExample = document.getElementById('load-example');
const project = loadProject();
let wasmReady = false;
const editorTheme = EditorView.theme({
'&': {
backgroundColor: '#0f172a',
color: '#e5e7eb',
height: '100%',
},
'.cm-scroller': {
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
fontSize: '14px',
lineHeight: '1.5',
overflow: 'auto',
},
'.cm-content': {
caretColor: '#e5e7eb',
minHeight: '100%',
padding: '12px',
},
'.cm-gutters': {
backgroundColor: '#111827',
borderRight: '1px solid #334155',
color: '#94a3b8',
},
'&.cm-focused': {
outline: 'none',
},
'&.cm-focused .cm-cursor': {
borderLeftColor: '#e5e7eb',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
backgroundColor: 'rgb(59 130 246 / 0.35)',
},
}, { dark: true });
const editor = new EditorView({
doc: project.files[project.activePath] ?? '',
parent: editorHost,
extensions: [
basicSetup,
decodal(),
editorTheme,
keymap.of([
{
key: 'Mod-Enter',
run() {
execute();
return true;
},
},
]),
EditorView.updateListener.of((update) => {
if (!update.docChanged) return;
project.files[project.activePath] = update.state.doc.toString();
saveProject();
}),
],
});
for (const example of playgroundExamples) {
const option = document.createElement('option');
@ -30,7 +87,6 @@ exampleSelect.value = starterProject.id;
setActiveFile(project.activePath);
renderFileTree();
updateHighlight();
function loadProject() {
try {
@ -84,41 +140,36 @@ function normalizePath(path) {
return parts.join('/');
}
function getEditorText() {
return editor.state.doc.toString();
}
function setEditorText(value) {
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: value,
},
});
}
function setActiveFile(path) {
const normalized = normalizePath(path);
if (project.files[normalized] === undefined) return;
if (project.activePath && project.files[project.activePath] !== undefined) {
project.files[project.activePath] = getEditorText();
}
project.activePath = normalized;
source.value = project.files[normalized];
setEditorText(project.files[normalized]);
activeFile.textContent = normalized;
deleteFile.disabled = Object.keys(project.files).length <= 1;
updateHighlight();
syncHighlightScroll();
renderFileTree();
saveProject();
}
function updateHighlight() {
sourceHighlight.innerHTML = `${highlightSource(source.value)}\n`;
}
function highlightSource(value) {
if (!wasmReady) return highlightDecodal(value);
try {
const result = JSON.parse(tokenizeSource(value));
if (result.ok) return highlightDecodalTokens(value, result.tokens);
} catch (_error) {
// Fall back to the lightweight JavaScript highlighter.
}
return highlightDecodal(value);
}
function syncHighlightScroll() {
sourceHighlight.scrollTop = source.scrollTop;
sourceHighlight.scrollLeft = source.scrollLeft;
}
function execute() {
project.files[project.activePath] = source.value;
project.files[project.activePath] = getEditorText();
saveProject();
const result = JSON.parse(evaluateProject(project.activePath, JSON.stringify(project.files)));
output.textContent = result.ok ? result.output : result.error;
@ -177,8 +228,6 @@ function compareNodes(a, b) {
try {
await init();
wasmReady = true;
updateHighlight();
run.disabled = false;
status.textContent = '';
execute();
@ -209,13 +258,3 @@ deleteFile.addEventListener('click', () => {
delete project.files[project.activePath];
setActiveFile(Object.keys(project.files).sort()[0]);
});
source.addEventListener('input', () => {
project.files[project.activePath] = source.value;
updateHighlight();
syncHighlightScroll();
saveProject();
});
source.addEventListener('scroll', syncHighlightScroll);
source.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') execute();
});

View File

@ -419,43 +419,26 @@ button:disabled {
text-align: left;
}
.editor-wrap {
.code-editor {
background: var(--editor-bg);
flex: 1;
min-height: 0;
position: relative;
}
.editor-wrap pre,
.editor-wrap textarea {
border: 0;
box-sizing: border-box;
font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
inset: 0;
margin: 0;
overflow: auto;
padding: 12px;
position: absolute;
tab-size: 2;
white-space: pre;
.code-editor .cm-editor {
height: 100%;
}
.editor-wrap pre {
color: var(--editor-text);
pointer-events: none;
.code-editor .cm-line {
padding: 0;
}
.editor-wrap textarea {
background: transparent;
caret-color: var(--editor-text);
color: transparent;
outline: none;
resize: none;
width: 100%;
.code-editor .cm-activeLine {
background: rgb(59 130 246 / 0.08);
}
.editor-wrap textarea::selection {
background: var(--selection);
.code-editor .cm-activeLineGutter {
background: rgb(59 130 246 / 0.12);
}
.output-pane pre {