@zvndev/darkmown 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +33 -0
- package/docs/cli.md +8 -0
- package/docs/spec-alignment.md +8 -0
- package/grammar/wd-directives.gbnf +58 -0
- package/package.json +3 -1
- package/site/pages/docs/index.wd +15 -0
- package/src/builder.js +5 -0
- package/src/catalog.js +586 -0
- package/src/cli.js +8 -0
- package/src/compiler/actions.js +45 -12
- package/src/compiler/context.js +41 -1
- package/src/compiler/fetch.js +12 -4
- package/src/compiler/forms.js +78 -24
- package/src/compiler/loops.js +15 -4
- package/src/compiler/media.js +21 -7
- package/src/compiler/predicates.js +25 -6
- package/src/compiler/state.js +67 -16
- package/src/compiler/structure.js +33 -10
- package/src/grammar.js +131 -0
- package/src/index.js +1 -0
package/src/compiler/state.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// while keeping the message text and `Use:` hints intact.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
|
-
import { at } from "./context.js";
|
|
11
|
+
import { at, lineOf, wdError } from "./context.js";
|
|
12
12
|
import { astOf, evalAst } from "./expr-ast.js";
|
|
13
13
|
import { escapeHtml, parseStateValue, safeScriptJson } from "./interpolation.js";
|
|
14
14
|
import { compileComputedExpr } from "./predicates.js";
|
|
@@ -17,6 +17,13 @@ import { compileComputedExpr } from "./predicates.js";
|
|
|
17
17
|
* @typedef {import("./context.js").Ctx} Ctx
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
// Concrete, compilable one-liners for the state-family hint tails + the catalog.
|
|
21
|
+
export const STATE_EXAMPLE = ":state count = 0";
|
|
22
|
+
export const STORE_EXAMPLE = ":store cart = []";
|
|
23
|
+
export const COMPUTED_EXAMPLE = ":computed total = items.length * 4";
|
|
24
|
+
export const THEME_EXAMPLE = ":theme";
|
|
25
|
+
const STATE_USE = `Use: :state name = value [persist] — e.g. ${STATE_EXAMPLE}`;
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* @param {string} line
|
|
22
29
|
* @param {Ctx} ctx
|
|
@@ -25,7 +32,13 @@ import { compileComputedExpr } from "./predicates.js";
|
|
|
25
32
|
*/
|
|
26
33
|
export function handleState(line, ctx, index) {
|
|
27
34
|
const match = line.match(/^:state\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)(\s+persist)?$/);
|
|
28
|
-
if (!match)
|
|
35
|
+
if (!match)
|
|
36
|
+
throw wdError(`Malformed :state in ${at(ctx, index)}: ${line}. ${STATE_USE}`, {
|
|
37
|
+
file: ctx.file,
|
|
38
|
+
line: lineOf(ctx, index),
|
|
39
|
+
hint: STATE_USE.slice("Use: ".length),
|
|
40
|
+
example: STATE_EXAMPLE
|
|
41
|
+
});
|
|
29
42
|
const value = parseStateValue(match[2], at(ctx, index));
|
|
30
43
|
const key = declareState(match[1], value, ctx);
|
|
31
44
|
const persistAttr = match[3] ? ` data-wd-persist="${key}"` : "";
|
|
@@ -43,8 +56,13 @@ export function declareState(name, value, ctx) {
|
|
|
43
56
|
if (ctx.loopItem)
|
|
44
57
|
throw new Error(`State cannot be declared inside a reactive @loop body (${ctx.file})`);
|
|
45
58
|
if (ctx.comp.stores.has(name))
|
|
46
|
-
throw
|
|
47
|
-
`State "${name}" collides with a :store of the same name in ${ctx.file}. Use: :store name = value for the global, or rename one
|
|
59
|
+
throw wdError(
|
|
60
|
+
`State "${name}" collides with a :store of the same name in ${ctx.file}. Use: :store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
|
|
61
|
+
{
|
|
62
|
+
file: ctx.file,
|
|
63
|
+
hint: `:store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
|
|
64
|
+
example: STORE_EXAMPLE
|
|
65
|
+
}
|
|
48
66
|
);
|
|
49
67
|
const key = ctx.sections.length ? `${ctx.sections.at(-1)}:${name}` : name;
|
|
50
68
|
if (ctx.comp.state.has(key))
|
|
@@ -63,8 +81,14 @@ export function declareState(name, value, ctx) {
|
|
|
63
81
|
export function handleStore(line, ctx, index) {
|
|
64
82
|
const match = line.match(/^:store\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)(\s+ephemeral)?$/);
|
|
65
83
|
if (!match)
|
|
66
|
-
throw
|
|
67
|
-
`Malformed :store in ${at(ctx, index)}: ${line}. Use: :store name = value [ephemeral]
|
|
84
|
+
throw wdError(
|
|
85
|
+
`Malformed :store in ${at(ctx, index)}: ${line}. Use: :store name = value [ephemeral] — e.g. ${STORE_EXAMPLE}`,
|
|
86
|
+
{
|
|
87
|
+
file: ctx.file,
|
|
88
|
+
line: lineOf(ctx, index),
|
|
89
|
+
hint: `:store name = value [ephemeral] — e.g. ${STORE_EXAMPLE}`,
|
|
90
|
+
example: STORE_EXAMPLE
|
|
91
|
+
}
|
|
68
92
|
);
|
|
69
93
|
const value = parseStateValue(match[2], at(ctx, index));
|
|
70
94
|
const name = declareStore(match[1], value, ctx);
|
|
@@ -83,10 +107,22 @@ export function handleStore(line, ctx, index) {
|
|
|
83
107
|
*/
|
|
84
108
|
export function declareStore(name, value, ctx) {
|
|
85
109
|
if (ctx.comp.stores.has(name))
|
|
86
|
-
throw
|
|
110
|
+
throw wdError(
|
|
111
|
+
`Store "${name}" is declared twice in ${ctx.file}. Use: :store name = value — e.g. ${STORE_EXAMPLE}`,
|
|
112
|
+
{
|
|
113
|
+
file: ctx.file,
|
|
114
|
+
hint: `:store name = value — e.g. ${STORE_EXAMPLE}`,
|
|
115
|
+
example: STORE_EXAMPLE
|
|
116
|
+
}
|
|
117
|
+
);
|
|
87
118
|
if (ctx.comp.state.has(name))
|
|
88
|
-
throw
|
|
89
|
-
`Store "${name}" collides with a :state of the same name in ${ctx.file}. Use: :store name = value for the global, or rename one
|
|
119
|
+
throw wdError(
|
|
120
|
+
`Store "${name}" collides with a :state of the same name in ${ctx.file}. Use: :store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
|
|
121
|
+
{
|
|
122
|
+
file: ctx.file,
|
|
123
|
+
hint: `:store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
|
|
124
|
+
example: STORE_EXAMPLE
|
|
125
|
+
}
|
|
90
126
|
);
|
|
91
127
|
ctx.comp.stores.add(name);
|
|
92
128
|
ctx.comp.state.set(name, value);
|
|
@@ -115,9 +151,12 @@ export function declareErrorState(key, ctx) {
|
|
|
115
151
|
export function handleComputed(line, ctx, index) {
|
|
116
152
|
const match = line.match(/^:computed\s+([A-Za-z_$][\w$]*)\s*=\s*(.+)$/);
|
|
117
153
|
if (!match)
|
|
118
|
-
throw
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
throw wdError(`Malformed :computed in ${at(ctx, index)}: ${line}. Use: ${COMPUTED_EXAMPLE}`, {
|
|
155
|
+
file: ctx.file,
|
|
156
|
+
line: lineOf(ctx, index),
|
|
157
|
+
hint: COMPUTED_EXAMPLE,
|
|
158
|
+
example: COMPUTED_EXAMPLE
|
|
159
|
+
});
|
|
121
160
|
const expr = compileComputedExpr(match[2].trim(), ctx);
|
|
122
161
|
// Parse the validated expression to the compact AST the runtime walks. A parse
|
|
123
162
|
// failure means the RHS is syntactically malformed (e.g. `|| <`) — a compile
|
|
@@ -127,8 +166,14 @@ export function handleComputed(line, ctx, index) {
|
|
|
127
166
|
try {
|
|
128
167
|
ast = astOf(expr);
|
|
129
168
|
} catch {
|
|
130
|
-
throw
|
|
131
|
-
`Malformed :computed expression in ${at(ctx, index)}: ${line}. Use:
|
|
169
|
+
throw wdError(
|
|
170
|
+
`Malformed :computed expression in ${at(ctx, index)}: ${line}. Use: ${COMPUTED_EXAMPLE}`,
|
|
171
|
+
{
|
|
172
|
+
file: ctx.file,
|
|
173
|
+
line: lineOf(ctx, index),
|
|
174
|
+
hint: COMPUTED_EXAMPLE,
|
|
175
|
+
example: COMPUTED_EXAMPLE
|
|
176
|
+
}
|
|
132
177
|
);
|
|
133
178
|
}
|
|
134
179
|
// Build-time mirror of the runtime: walk the AST (no eval) so `:computed total =
|
|
@@ -155,8 +200,14 @@ export function handleComputed(line, ctx, index) {
|
|
|
155
200
|
export function handleTheme(line, ctx, index) {
|
|
156
201
|
const match = line.match(/^:theme(?:\s+([A-Za-z_$][\w$]*))?(?:\s*=\s*(.+))?$/);
|
|
157
202
|
if (!match) {
|
|
158
|
-
throw
|
|
159
|
-
`Malformed :theme in ${at(ctx, index)}: ${line}. Use: :theme (or :theme name = "auto")
|
|
203
|
+
throw wdError(
|
|
204
|
+
`Malformed :theme in ${at(ctx, index)}: ${line}. Use: :theme (or :theme name = "auto") — e.g. ${THEME_EXAMPLE}`,
|
|
205
|
+
{
|
|
206
|
+
file: ctx.file,
|
|
207
|
+
line: lineOf(ctx, index),
|
|
208
|
+
hint: `:theme (or :theme name = "auto") — e.g. ${THEME_EXAMPLE}`,
|
|
209
|
+
example: THEME_EXAMPLE
|
|
210
|
+
}
|
|
160
211
|
);
|
|
161
212
|
}
|
|
162
213
|
const name = match[1] || "theme";
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// while keeping the message text and `Use:` hints intact.
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
|
|
14
|
-
import { at, createScope, LOOP_META, nestedCtx } from "./context.js";
|
|
14
|
+
import { at, createScope, LOOP_META, lineOf, nestedCtx, wdError } from "./context.js";
|
|
15
15
|
import { astOf, serializeExpr } from "./expr-ast.js";
|
|
16
16
|
import { resolveInclude, scopedSkinFor, stampScope } from "./includes.js";
|
|
17
17
|
import {
|
|
@@ -28,6 +28,13 @@ import { compileWhen, evalPredicate } from "./predicates.js";
|
|
|
28
28
|
* @typedef {import("./context.js").Ctx} Ctx
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
// Concrete, compilable structure examples for the hint tails + the catalog.
|
|
32
|
+
export const INCLUDE_EXAMPLE = "@include /header.wd";
|
|
33
|
+
export const CONTAINER_EXAMPLE = "::: card .featured";
|
|
34
|
+
export const IF_EXAMPLE = ":if count > 0";
|
|
35
|
+
export const CAROUSEL_EXAMPLE = ":carousel autoplay=3000";
|
|
36
|
+
const INCLUDE_USE = `Use: @include /partial.wd [with key="value"] — e.g. ${INCLUDE_EXAMPLE}`;
|
|
37
|
+
|
|
31
38
|
// Container names that emit their own semantic landmark element instead of a
|
|
32
39
|
// `<div>`, so scaffolds get real `<nav>`/`<main>` landmarks (accessibility: the
|
|
33
40
|
// skip link targets a genuine main-content region and navigation chrome sits in
|
|
@@ -44,7 +51,13 @@ const SEMANTIC_CONTAINER_TAGS = new Set(["nav", "main"]);
|
|
|
44
51
|
*/
|
|
45
52
|
export function handleInclude(line, ctx, index) {
|
|
46
53
|
const match = line.match(/^@include\s+(\S+)(?:\s+with\s+(.+))?$/);
|
|
47
|
-
if (!match)
|
|
54
|
+
if (!match)
|
|
55
|
+
throw wdError(`Malformed @include in ${at(ctx, index)}: ${line}. ${INCLUDE_USE}`, {
|
|
56
|
+
file: ctx.file,
|
|
57
|
+
line: lineOf(ctx, index),
|
|
58
|
+
hint: INCLUDE_USE.slice("Use: ".length),
|
|
59
|
+
example: INCLUDE_EXAMPLE
|
|
60
|
+
});
|
|
48
61
|
const target = resolveInclude(
|
|
49
62
|
match[1],
|
|
50
63
|
ctx.file,
|
|
@@ -215,10 +228,15 @@ export function handleCarousel(line, bodyLines, ctx, index) {
|
|
|
215
228
|
let autoplayAttr = "";
|
|
216
229
|
if (rest) {
|
|
217
230
|
const auto = rest.match(/^autoplay=(\d+)$/);
|
|
218
|
-
if (!auto)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
231
|
+
if (!auto) {
|
|
232
|
+
const hint = `:carousel [autoplay=3000] … :endcarousel — e.g. ${CAROUSEL_EXAMPLE}`;
|
|
233
|
+
throw wdError(`Malformed :carousel in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
234
|
+
file: ctx.file,
|
|
235
|
+
line: lineOf(ctx, index),
|
|
236
|
+
hint,
|
|
237
|
+
example: CAROUSEL_EXAMPLE
|
|
238
|
+
});
|
|
239
|
+
}
|
|
222
240
|
autoplayAttr = ` data-wd-carousel-autoplay="${auto[1]}"`;
|
|
223
241
|
}
|
|
224
242
|
ctx.comp.assets.behaviors.add("carousel");
|
|
@@ -323,10 +341,15 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
|
|
|
323
341
|
// whitelist as `@loop … where` / `.class when` (with `not`), so it folds at
|
|
324
342
|
// build when static, drives a per-row each-if when it reads the loop item, and
|
|
325
343
|
// a global if-region (evaluated each render) when it reads state. No raw eval.
|
|
326
|
-
if (!condition)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
344
|
+
if (!condition) {
|
|
345
|
+
const hint = `":if name" or ":if a <op> b [and|or|not …]" — e.g. ${IF_EXAMPLE}`;
|
|
346
|
+
throw wdError(`Malformed :if in ${at(ctx, index)}: ${line}. Use ${hint}`, {
|
|
347
|
+
file: ctx.file,
|
|
348
|
+
line: lineOf(ctx, index),
|
|
349
|
+
hint,
|
|
350
|
+
example: IF_EXAMPLE
|
|
351
|
+
});
|
|
352
|
+
}
|
|
330
353
|
const compiled = compileWhen(condition, ctx, '":if"');
|
|
331
354
|
if (compiled.static)
|
|
332
355
|
return ctx.compileBody(
|
package/src/grammar.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// GBNF grammar generator for `.wd` DIRECTIVE LINES.
|
|
3
|
+
//
|
|
4
|
+
// `generateGrammar()` emits a llama.cpp-compatible GBNF grammar that constrains
|
|
5
|
+
// a small model's decoding to well-formed Darkmown directive lines (plus the
|
|
6
|
+
// `{ path | pipe }` interpolation token). It is generated from the SAME catalog
|
|
7
|
+
// the cheatsheet uses, so the keyword set, `@loop` clauses, comparison operators,
|
|
8
|
+
// and format-pipe names can never drift from what compiles.
|
|
9
|
+
//
|
|
10
|
+
// Scope (stated deliberately): this grammar constrains the SHAPE of a directive
|
|
11
|
+
// line — the leading keyword must be a real directive, `@loop` must be
|
|
12
|
+
// `@loop <src> into <item> …clauses`, a `where`/`:if` condition must be
|
|
13
|
+
// `operand <op> operand`, a `:button` must be `"…" -> <action-chain>`, and an
|
|
14
|
+
// interpolation must pipe through a known formatter. It does NOT try to grammar-
|
|
15
|
+
// check free-form values (arbitrary JSON seeds, URLs, prose) — those are `text`.
|
|
16
|
+
// That is enough to delete the on-device failure class (JS/HTML muscle-memory
|
|
17
|
+
// openers like `::: card class=` or `cart.filter(...)` become unrepresentable)
|
|
18
|
+
// while staying small and maintainable.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
import { directiveCatalog } from "./catalog.js";
|
|
22
|
+
|
|
23
|
+
/** Slugify a directive token into a GBNF rule name (`@loop` → `dir-loop`). */
|
|
24
|
+
/** @type {Record<string, string>} */
|
|
25
|
+
const SLUG = {
|
|
26
|
+
"@include": "dir-include",
|
|
27
|
+
"@loop": "dir-loop",
|
|
28
|
+
":::": "dir-container",
|
|
29
|
+
":if": "dir-if",
|
|
30
|
+
":state": "dir-state",
|
|
31
|
+
":store": "dir-store",
|
|
32
|
+
":computed": "dir-computed",
|
|
33
|
+
":fetch": "dir-fetch",
|
|
34
|
+
":effect": "dir-effect",
|
|
35
|
+
":every": "dir-every",
|
|
36
|
+
":theme": "dir-theme",
|
|
37
|
+
":button": "dir-button",
|
|
38
|
+
":form": "dir-form",
|
|
39
|
+
":input": "dir-input",
|
|
40
|
+
":textarea": "dir-textarea",
|
|
41
|
+
":select": "dir-select",
|
|
42
|
+
":checkbox": "dir-checkbox",
|
|
43
|
+
":radio": "dir-radio",
|
|
44
|
+
":submit": "dir-submit",
|
|
45
|
+
":bind": "dir-bind",
|
|
46
|
+
":slider": "dir-slider",
|
|
47
|
+
":video": "dir-video",
|
|
48
|
+
":audio": "dir-audio",
|
|
49
|
+
":embed": "dir-embed",
|
|
50
|
+
":carousel": "dir-carousel"
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// The directives given a structured RHS below; every other directive gets the
|
|
54
|
+
// generic `"<keyword>" restofline` rule (keyword pinned, argument tail free).
|
|
55
|
+
const STRUCTURED = new Set(["@loop", ":if", ":button", ":::"]);
|
|
56
|
+
|
|
57
|
+
/** A GBNF alternation literal list, e.g. `"a" | "b"`. */
|
|
58
|
+
const alt = (/** @type {string[]} */ items) => items.map((s) => `"${s}"`).join(" | ");
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate the GBNF grammar for `.wd` directive lines from the directive catalog.
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
export function generateGrammar() {
|
|
65
|
+
const cat = directiveCatalog();
|
|
66
|
+
const dirRules = cat.directives.map((d) => SLUG[d.name]).filter(Boolean);
|
|
67
|
+
|
|
68
|
+
/** @type {string[]} */
|
|
69
|
+
const lines = [];
|
|
70
|
+
lines.push("# Darkmown .wd directive-line grammar — generated by src/grammar.js");
|
|
71
|
+
lines.push(
|
|
72
|
+
"# Constrains a directive line (or a { path | pipe } interpolation). Do not edit by hand."
|
|
73
|
+
);
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("root ::= directive | binding");
|
|
76
|
+
lines.push(`directive ::= ${dirRules.join(" | ")}`);
|
|
77
|
+
lines.push("");
|
|
78
|
+
|
|
79
|
+
// Structured directives.
|
|
80
|
+
lines.push('dir-loop ::= "@loop" sp operand sp "into" sp ident loop-clauses');
|
|
81
|
+
lines.push('dir-if ::= ":if" sp ( condition (sp joiner sp condition)* | path )');
|
|
82
|
+
lines.push('dir-button ::= ":button" sp dqstring sp "->" sp action-chain');
|
|
83
|
+
lines.push('dir-container ::= ":::" restofline');
|
|
84
|
+
lines.push("");
|
|
85
|
+
|
|
86
|
+
// Generic directives: keyword pinned, argument tail free to end of line.
|
|
87
|
+
for (const d of cat.directives) {
|
|
88
|
+
if (STRUCTURED.has(d.name)) continue;
|
|
89
|
+
lines.push(`${SLUG[d.name]} ::= "${d.name}" restofline`);
|
|
90
|
+
}
|
|
91
|
+
lines.push("");
|
|
92
|
+
|
|
93
|
+
// @loop clause vocabulary (mirrors the fixed clause order in the catalog).
|
|
94
|
+
lines.push("loop-clauses ::= ( sp loop-clause )*");
|
|
95
|
+
lines.push(
|
|
96
|
+
'loop-clause ::= where-clause | "sort by" sp path ( sp ( "asc" | "desc" ) )? | "reverse" | "offset" sp number | "limit" sp number | "paginate" sp number | "sortable"'
|
|
97
|
+
);
|
|
98
|
+
lines.push('where-clause ::= "where" sp condition ( sp joiner sp condition )*');
|
|
99
|
+
lines.push("condition ::= operand sp op sp operand");
|
|
100
|
+
lines.push(`op ::= ${alt(cat.predicateOps.map((o) => o.name))}`);
|
|
101
|
+
lines.push(`joiner ::= ${alt(cat.predicateJoiners.filter((j) => j !== "not"))}`);
|
|
102
|
+
lines.push("");
|
|
103
|
+
|
|
104
|
+
// Button-action vocabulary. Each operator token below is asserted present
|
|
105
|
+
// against the catalog's action tokens in tests/grammar.test.js.
|
|
106
|
+
lines.push('action-chain ::= action ( wsopt ";" wsopt action )*');
|
|
107
|
+
lines.push("action ::= ident action-op");
|
|
108
|
+
lines.push(
|
|
109
|
+
'action-op ::= "++" | "--" | sp "+=" sp value | sp "-=" sp value | sp "=" sp value | sp "toggle" ( sp value )? | sp "append" sp value | sp "prepend" sp value | sp "remove" sp value | sp "clear" | sp "merge" sp ident | sp "delete" sp value | sp "reset" | sp "refetch"'
|
|
110
|
+
);
|
|
111
|
+
lines.push("");
|
|
112
|
+
|
|
113
|
+
// Interpolation binding with format pipes.
|
|
114
|
+
lines.push('binding ::= "{" wsopt path pipe-chain wsopt "}"');
|
|
115
|
+
lines.push('pipe-chain ::= ( wsopt "|" wsopt pipe ( ":" value )* )*');
|
|
116
|
+
lines.push(`pipe ::= ${alt(cat.formatPipes.map((p) => p.name))}`);
|
|
117
|
+
lines.push("");
|
|
118
|
+
|
|
119
|
+
// Terminals.
|
|
120
|
+
lines.push("operand ::= path | dqstring | number");
|
|
121
|
+
lines.push("value ::= dqstring | number | path");
|
|
122
|
+
lines.push("ident ::= [a-zA-Z_$] [a-zA-Z0-9_$]*");
|
|
123
|
+
lines.push("path ::= [a-zA-Z0-9_$/.~-]+");
|
|
124
|
+
lines.push("number ::= [0-9]+");
|
|
125
|
+
lines.push('dqstring ::= "\\"" [^"]* "\\""');
|
|
126
|
+
lines.push("restofline ::= [^\\n]*");
|
|
127
|
+
lines.push('sp ::= " "+');
|
|
128
|
+
lines.push('wsopt ::= " "*');
|
|
129
|
+
lines.push("");
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|