@zvndev/darkmown 2.1.0 → 2.3.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 +22 -0
- package/README.md +65 -0
- package/docs/cli.md +8 -0
- package/docs/spec-alignment.md +18 -0
- package/grammar/wd-directives.gbnf +58 -0
- package/package.json +17 -10
- package/site/pages/docs/index.wd +29 -0
- package/site/pages/playground/index.js +47 -46
- package/src/builder.js +5 -0
- package/src/catalog.js +601 -0
- package/src/cli.js +8 -0
- package/src/compiler/actions.js +87 -34
- package/src/compiler/body.js +36 -14
- package/src/compiler/collections.js +16 -10
- package/src/compiler/context.js +70 -1
- package/src/compiler/fetch.js +51 -19
- package/src/compiler/format.js +5 -2
- package/src/compiler/forms.js +169 -50
- package/src/compiler/frontmatter.js +11 -2
- package/src/compiler/fs-reader.js +2 -1
- package/src/compiler/image-size.js +340 -0
- package/src/compiler/includes.js +8 -4
- package/src/compiler/interpolation.js +15 -7
- package/src/compiler/loops.js +78 -39
- package/src/compiler/markdown.js +4 -3
- package/src/compiler/media.js +38 -10
- package/src/compiler/page.js +8 -6
- package/src/compiler/predicates.js +74 -30
- package/src/compiler/reader.js +12 -2
- package/src/compiler/state.js +83 -18
- package/src/compiler/structure.js +57 -24
- package/src/deploy.js +8 -4
- package/src/errors.js +1156 -0
- package/src/grammar.js +131 -0
- package/src/index.js +1 -0
- package/src/router.js +7 -4
- package/src/scaffold.js +4 -2
- package/src/skin.js +17 -6
- package/types/api-runner.d.ts +94 -0
- package/types/builder.d.ts +141 -10
- package/types/catalog.d.ts +97 -0
- package/types/compiler/actions.d.ts +41 -0
- package/types/compiler/body.d.ts +11 -0
- package/types/compiler/collections.d.ts +76 -0
- package/types/compiler/context.d.ts +661 -0
- package/types/compiler/directives.d.ts +6 -0
- package/types/compiler/expr-ast.d.ts +20 -0
- package/types/compiler/fetch.d.ts +29 -0
- package/types/compiler/format.d.ts +49 -0
- package/types/compiler/forms.d.ts +94 -0
- package/types/compiler/frontmatter.d.ts +33 -0
- package/types/compiler/fs-reader.d.ts +7 -0
- package/types/compiler/image-size.d.ts +14 -0
- package/types/compiler/includes.d.ts +100 -0
- package/types/compiler/index.d.ts +5 -0
- package/types/compiler/interpolation.d.ts +115 -0
- package/types/compiler/loops.d.ts +36 -0
- package/types/compiler/markdown.d.ts +34 -0
- package/types/compiler/media.d.ts +32 -0
- package/types/compiler/page.d.ts +109 -0
- package/types/compiler/predicates.d.ts +50 -0
- package/types/compiler/reader.d.ts +56 -0
- package/types/compiler/state.d.ts +68 -0
- package/types/compiler/structure.d.ts +56 -0
- package/types/compiler.d.ts +35 -223
- package/types/dev.d.ts +47 -2
- package/types/errors.d.ts +100 -0
- package/types/feeds.d.ts +150 -0
- package/types/headers.d.ts +44 -0
- package/types/highlight.d.ts +24 -0
- package/types/router.d.ts +42 -3
- package/types/scaffold.d.ts +15 -3
- package/types/skin.d.ts +28 -1
- package/types/statics.d.ts +2 -1
package/src/compiler/reader.js
CHANGED
|
@@ -13,13 +13,18 @@
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
15
|
import path from "node:path";
|
|
16
|
+
import { normalizeNewlines } from "./context.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* The narrow filesystem surface the compiler needs. Every method takes an
|
|
19
20
|
* ABSOLUTE path (the compiler resolves includes/assets to absolute paths before
|
|
20
21
|
* reading), so a reader only has to map absolute paths to bytes/existence.
|
|
21
22
|
* @typedef {object} Reader
|
|
22
|
-
* @property {(absPath: string) => string} readText Read a UTF-8 text file.
|
|
23
|
+
* @property {(absPath: string) => string} readText Read a UTF-8 text file. The
|
|
24
|
+
* result is ALWAYS LF-normalized: every parser downstream is line-structured
|
|
25
|
+
* and LF-shaped, so a CRLF source (Windows, or any `core.autocrlf` checkout)
|
|
26
|
+
* would otherwise fail to match delimiters like `---`. A custom reader must
|
|
27
|
+
* uphold this.
|
|
23
28
|
* @property {(absPath: string) => Uint8Array} readBinary Read raw bytes (images).
|
|
24
29
|
* @property {(absPath: string) => boolean} exists Whether the path exists.
|
|
25
30
|
* @property {(absPath: string) => string} realpath Canonical path (symlinks
|
|
@@ -40,6 +45,11 @@ import path from "node:path";
|
|
|
40
45
|
*/
|
|
41
46
|
export function memoryReader(files, cwd = "/") {
|
|
42
47
|
const map = files instanceof Map ? files : new Map(Object.entries(files));
|
|
48
|
+
// Resolution deliberately uses the HOST `path`, because `cwd` is frequently a
|
|
49
|
+
// real OS directory (the fs-parity test compiles the same project from disk
|
|
50
|
+
// and from memory against one temp root, and a Windows temp root is
|
|
51
|
+
// `C:\…` — which `path.posix` does not recognize as absolute). Only the
|
|
52
|
+
// resulting KEY is forced to POSIX, matching the map's documented key shape.
|
|
43
53
|
const keyOf = (/** @type {string} */ absPath) =>
|
|
44
54
|
path.relative(cwd, path.resolve(absPath)).split(path.sep).join("/");
|
|
45
55
|
return {
|
|
@@ -49,7 +59,7 @@ export function memoryReader(files, cwd = "/") {
|
|
|
49
59
|
if (value === undefined) {
|
|
50
60
|
throw new Error(`ENOENT: no in-memory file "${key}" (from ${absPath})`);
|
|
51
61
|
}
|
|
52
|
-
return value;
|
|
62
|
+
return normalizeNewlines(value);
|
|
53
63
|
},
|
|
54
64
|
readBinary: () => {
|
|
55
65
|
throw new Error("memoryReader: binary reads are unsupported");
|
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,14 @@ 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
|
+
code: "WD201",
|
|
38
|
+
file: ctx.file,
|
|
39
|
+
line: lineOf(ctx, index),
|
|
40
|
+
hint: STATE_USE.slice("Use: ".length),
|
|
41
|
+
example: STATE_EXAMPLE
|
|
42
|
+
});
|
|
29
43
|
const value = parseStateValue(match[2], at(ctx, index));
|
|
30
44
|
const key = declareState(match[1], value, ctx);
|
|
31
45
|
const persistAttr = match[3] ? ` data-wd-persist="${key}"` : "";
|
|
@@ -41,14 +55,26 @@ export function handleState(line, ctx, index) {
|
|
|
41
55
|
*/
|
|
42
56
|
export function declareState(name, value, ctx) {
|
|
43
57
|
if (ctx.loopItem)
|
|
44
|
-
throw
|
|
58
|
+
throw wdError(`State cannot be declared inside a reactive @loop body (${ctx.file})`, {
|
|
59
|
+
code: "WD202",
|
|
60
|
+
file: ctx.file
|
|
61
|
+
});
|
|
45
62
|
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
|
|
63
|
+
throw wdError(
|
|
64
|
+
`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}`,
|
|
65
|
+
{
|
|
66
|
+
code: "WD203",
|
|
67
|
+
file: ctx.file,
|
|
68
|
+
hint: `:store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
|
|
69
|
+
example: STORE_EXAMPLE
|
|
70
|
+
}
|
|
48
71
|
);
|
|
49
72
|
const key = ctx.sections.length ? `${ctx.sections.at(-1)}:${name}` : name;
|
|
50
73
|
if (ctx.comp.state.has(key))
|
|
51
|
-
throw
|
|
74
|
+
throw wdError(`State "${name}" is declared twice in the same scope (${ctx.file})`, {
|
|
75
|
+
code: "WD204",
|
|
76
|
+
file: ctx.file
|
|
77
|
+
});
|
|
52
78
|
ctx.comp.state.set(key, value);
|
|
53
79
|
ctx.comp.assets.runtime = true;
|
|
54
80
|
return key;
|
|
@@ -63,8 +89,15 @@ export function declareState(name, value, ctx) {
|
|
|
63
89
|
export function handleStore(line, ctx, index) {
|
|
64
90
|
const match = line.match(/^:store\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)(\s+ephemeral)?$/);
|
|
65
91
|
if (!match)
|
|
66
|
-
throw
|
|
67
|
-
`Malformed :store in ${at(ctx, index)}: ${line}. Use: :store name = value [ephemeral]
|
|
92
|
+
throw wdError(
|
|
93
|
+
`Malformed :store in ${at(ctx, index)}: ${line}. Use: :store name = value [ephemeral] — e.g. ${STORE_EXAMPLE}`,
|
|
94
|
+
{
|
|
95
|
+
code: "WD205",
|
|
96
|
+
file: ctx.file,
|
|
97
|
+
line: lineOf(ctx, index),
|
|
98
|
+
hint: `:store name = value [ephemeral] — e.g. ${STORE_EXAMPLE}`,
|
|
99
|
+
example: STORE_EXAMPLE
|
|
100
|
+
}
|
|
68
101
|
);
|
|
69
102
|
const value = parseStateValue(match[2], at(ctx, index));
|
|
70
103
|
const name = declareStore(match[1], value, ctx);
|
|
@@ -83,10 +116,24 @@ export function handleStore(line, ctx, index) {
|
|
|
83
116
|
*/
|
|
84
117
|
export function declareStore(name, value, ctx) {
|
|
85
118
|
if (ctx.comp.stores.has(name))
|
|
86
|
-
throw
|
|
119
|
+
throw wdError(
|
|
120
|
+
`Store "${name}" is declared twice in ${ctx.file}. Use: :store name = value — e.g. ${STORE_EXAMPLE}`,
|
|
121
|
+
{
|
|
122
|
+
code: "WD206",
|
|
123
|
+
file: ctx.file,
|
|
124
|
+
hint: `:store name = value — e.g. ${STORE_EXAMPLE}`,
|
|
125
|
+
example: STORE_EXAMPLE
|
|
126
|
+
}
|
|
127
|
+
);
|
|
87
128
|
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
|
|
129
|
+
throw wdError(
|
|
130
|
+
`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}`,
|
|
131
|
+
{
|
|
132
|
+
code: "WD207",
|
|
133
|
+
file: ctx.file,
|
|
134
|
+
hint: `:store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
|
|
135
|
+
example: STORE_EXAMPLE
|
|
136
|
+
}
|
|
90
137
|
);
|
|
91
138
|
ctx.comp.stores.add(name);
|
|
92
139
|
ctx.comp.state.set(name, value);
|
|
@@ -115,9 +162,13 @@ export function declareErrorState(key, ctx) {
|
|
|
115
162
|
export function handleComputed(line, ctx, index) {
|
|
116
163
|
const match = line.match(/^:computed\s+([A-Za-z_$][\w$]*)\s*=\s*(.+)$/);
|
|
117
164
|
if (!match)
|
|
118
|
-
throw
|
|
119
|
-
|
|
120
|
-
|
|
165
|
+
throw wdError(`Malformed :computed in ${at(ctx, index)}: ${line}. Use: ${COMPUTED_EXAMPLE}`, {
|
|
166
|
+
code: "WD208",
|
|
167
|
+
file: ctx.file,
|
|
168
|
+
line: lineOf(ctx, index),
|
|
169
|
+
hint: COMPUTED_EXAMPLE,
|
|
170
|
+
example: COMPUTED_EXAMPLE
|
|
171
|
+
});
|
|
121
172
|
const expr = compileComputedExpr(match[2].trim(), ctx);
|
|
122
173
|
// Parse the validated expression to the compact AST the runtime walks. A parse
|
|
123
174
|
// failure means the RHS is syntactically malformed (e.g. `|| <`) — a compile
|
|
@@ -127,8 +178,15 @@ export function handleComputed(line, ctx, index) {
|
|
|
127
178
|
try {
|
|
128
179
|
ast = astOf(expr);
|
|
129
180
|
} catch {
|
|
130
|
-
throw
|
|
131
|
-
`Malformed :computed expression in ${at(ctx, index)}: ${line}. Use:
|
|
181
|
+
throw wdError(
|
|
182
|
+
`Malformed :computed expression in ${at(ctx, index)}: ${line}. Use: ${COMPUTED_EXAMPLE}`,
|
|
183
|
+
{
|
|
184
|
+
code: "WD209",
|
|
185
|
+
file: ctx.file,
|
|
186
|
+
line: lineOf(ctx, index),
|
|
187
|
+
hint: COMPUTED_EXAMPLE,
|
|
188
|
+
example: COMPUTED_EXAMPLE
|
|
189
|
+
}
|
|
132
190
|
);
|
|
133
191
|
}
|
|
134
192
|
// Build-time mirror of the runtime: walk the AST (no eval) so `:computed total =
|
|
@@ -155,8 +213,15 @@ export function handleComputed(line, ctx, index) {
|
|
|
155
213
|
export function handleTheme(line, ctx, index) {
|
|
156
214
|
const match = line.match(/^:theme(?:\s+([A-Za-z_$][\w$]*))?(?:\s*=\s*(.+))?$/);
|
|
157
215
|
if (!match) {
|
|
158
|
-
throw
|
|
159
|
-
`Malformed :theme in ${at(ctx, index)}: ${line}. Use: :theme (or :theme name = "auto")
|
|
216
|
+
throw wdError(
|
|
217
|
+
`Malformed :theme in ${at(ctx, index)}: ${line}. Use: :theme (or :theme name = "auto") — e.g. ${THEME_EXAMPLE}`,
|
|
218
|
+
{
|
|
219
|
+
code: "WD210",
|
|
220
|
+
file: ctx.file,
|
|
221
|
+
line: lineOf(ctx, index),
|
|
222
|
+
hint: `:theme (or :theme name = "auto") — e.g. ${THEME_EXAMPLE}`,
|
|
223
|
+
example: THEME_EXAMPLE
|
|
224
|
+
}
|
|
160
225
|
);
|
|
161
226
|
}
|
|
162
227
|
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,14 @@ 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
|
+
code: "WD603",
|
|
57
|
+
file: ctx.file,
|
|
58
|
+
line: lineOf(ctx, index),
|
|
59
|
+
hint: INCLUDE_USE.slice("Use: ".length),
|
|
60
|
+
example: INCLUDE_EXAMPLE
|
|
61
|
+
});
|
|
48
62
|
const target = resolveInclude(
|
|
49
63
|
match[1],
|
|
50
64
|
ctx.file,
|
|
@@ -94,8 +108,9 @@ function parseIncludeArgs(raw, ctx) {
|
|
|
94
108
|
const expr = value.slice(1, -1).trim();
|
|
95
109
|
const resolved = lookupPath(expr, ctx);
|
|
96
110
|
if (!resolved.found) {
|
|
97
|
-
throw
|
|
98
|
-
`@include argument ${match[1]}={ ${expr} } in ${ctx.file} does not match any value in scope
|
|
111
|
+
throw wdError(
|
|
112
|
+
`@include argument ${match[1]}={ ${expr} } in ${ctx.file} does not match any value in scope`,
|
|
113
|
+
{ code: "WD604", file: ctx.file }
|
|
99
114
|
);
|
|
100
115
|
}
|
|
101
116
|
args[match[1]] = resolved.value;
|
|
@@ -152,8 +167,9 @@ export function handleContainer(header, bodyLines, ctx, index) {
|
|
|
152
167
|
}
|
|
153
168
|
const cm = rest.match(/^\.([A-Za-z_][\w-]*)/);
|
|
154
169
|
if (!cm)
|
|
155
|
-
throw
|
|
156
|
-
`Unexpected token "${rest.split(/\s+/)[0]}" in container "::: ${header}" in ${at(ctx, index)}
|
|
170
|
+
throw wdError(
|
|
171
|
+
`Unexpected token "${rest.split(/\s+/)[0]}" in container "::: ${header}" in ${at(ctx, index)}`,
|
|
172
|
+
{ code: "WD605", file: ctx.file, line: lineOf(ctx, index) }
|
|
157
173
|
);
|
|
158
174
|
const cls = cm[1];
|
|
159
175
|
rest = rest.slice(cm[0].length).trim();
|
|
@@ -215,10 +231,16 @@ export function handleCarousel(line, bodyLines, ctx, index) {
|
|
|
215
231
|
let autoplayAttr = "";
|
|
216
232
|
if (rest) {
|
|
217
233
|
const auto = rest.match(/^autoplay=(\d+)$/);
|
|
218
|
-
if (!auto)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
234
|
+
if (!auto) {
|
|
235
|
+
const hint = `:carousel [autoplay=3000] … :endcarousel — e.g. ${CAROUSEL_EXAMPLE}`;
|
|
236
|
+
throw wdError(`Malformed :carousel in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
237
|
+
code: "WD606",
|
|
238
|
+
file: ctx.file,
|
|
239
|
+
line: lineOf(ctx, index),
|
|
240
|
+
hint,
|
|
241
|
+
example: CAROUSEL_EXAMPLE
|
|
242
|
+
});
|
|
243
|
+
}
|
|
222
244
|
autoplayAttr = ` data-wd-carousel-autoplay="${auto[1]}"`;
|
|
223
245
|
}
|
|
224
246
|
ctx.comp.assets.behaviors.add("carousel");
|
|
@@ -277,8 +299,9 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
|
|
|
277
299
|
// Per-row meta vars in :if — only valid inside a loop.
|
|
278
300
|
if (LOOP_META[head]) {
|
|
279
301
|
if (!ctx.loopMeta)
|
|
280
|
-
throw
|
|
281
|
-
`":if ${match[1]}" uses the loop meta variable "${head}" outside a @loop in ${ctx.file}. Use it inside a loop body
|
|
302
|
+
throw wdError(
|
|
303
|
+
`":if ${match[1]}" uses the loop meta variable "${head}" outside a @loop in ${ctx.file}. Use it inside a loop body.`,
|
|
304
|
+
{ code: "WD607", file: ctx.file }
|
|
282
305
|
);
|
|
283
306
|
if (ctx.loopItem) {
|
|
284
307
|
const truthy = ctx.compileBody(truthyLines, truthyCtx).trim();
|
|
@@ -304,8 +327,9 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
|
|
|
304
327
|
|
|
305
328
|
const key = resolveStateKey(head, ctx);
|
|
306
329
|
if (!key) {
|
|
307
|
-
throw
|
|
308
|
-
`:if ${match[1]} in ${ctx.file} does not match a :state or in-scope value. Declare it first
|
|
330
|
+
throw wdError(
|
|
331
|
+
`:if ${match[1]} in ${ctx.file} does not match a :state or in-scope value. Declare it first.`,
|
|
332
|
+
{ code: "WD608", file: ctx.file }
|
|
309
333
|
);
|
|
310
334
|
}
|
|
311
335
|
ctx.comp.assets.runtime = true;
|
|
@@ -323,10 +347,16 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
|
|
|
323
347
|
// whitelist as `@loop … where` / `.class when` (with `not`), so it folds at
|
|
324
348
|
// build when static, drives a per-row each-if when it reads the loop item, and
|
|
325
349
|
// a global if-region (evaluated each render) when it reads state. No raw eval.
|
|
326
|
-
if (!condition)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
350
|
+
if (!condition) {
|
|
351
|
+
const hint = `":if name" or ":if a <op> b [and|or|not …]" — e.g. ${IF_EXAMPLE}`;
|
|
352
|
+
throw wdError(`Malformed :if in ${at(ctx, index)}: ${line}. Use ${hint}`, {
|
|
353
|
+
code: "WD609",
|
|
354
|
+
file: ctx.file,
|
|
355
|
+
line: lineOf(ctx, index),
|
|
356
|
+
hint,
|
|
357
|
+
example: IF_EXAMPLE
|
|
358
|
+
});
|
|
359
|
+
}
|
|
330
360
|
const compiled = compileWhen(condition, ctx, '":if"');
|
|
331
361
|
if (compiled.static)
|
|
332
362
|
return ctx.compileBody(
|
|
@@ -380,13 +410,15 @@ function validateDemoHref(href, ctx) {
|
|
|
380
410
|
const value = href.trim();
|
|
381
411
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — rejects control characters in URLs/hrefs.
|
|
382
412
|
if (value !== href || /[\u0000-\u001F\u007F]/.test(value)) {
|
|
383
|
-
throw
|
|
384
|
-
`Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Use a relative URL starting with /, ./, ../, or #, or an http:, https:, or mailto: URL
|
|
413
|
+
throw wdError(
|
|
414
|
+
`Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Use a relative URL starting with /, ./, ../, or #, or an http:, https:, or mailto: URL.`,
|
|
415
|
+
{ code: "WD610", file: ctx.file }
|
|
385
416
|
);
|
|
386
417
|
}
|
|
387
418
|
if (value.startsWith("//")) {
|
|
388
|
-
throw
|
|
389
|
-
`Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Protocol-relative URLs are not allowed; use http: or https: explicitly
|
|
419
|
+
throw wdError(
|
|
420
|
+
`Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Protocol-relative URLs are not allowed; use http: or https: explicitly.`,
|
|
421
|
+
{ code: "WD611", file: ctx.file }
|
|
390
422
|
);
|
|
391
423
|
}
|
|
392
424
|
if (
|
|
@@ -401,7 +433,8 @@ function validateDemoHref(href, ctx) {
|
|
|
401
433
|
if (scheme && ["http", "https", "mailto"].includes(scheme[1].toLowerCase())) {
|
|
402
434
|
return value;
|
|
403
435
|
}
|
|
404
|
-
throw
|
|
405
|
-
`Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Use a relative URL starting with /, ./, ../, or #, or an http:, https:, or mailto: URL
|
|
436
|
+
throw wdError(
|
|
437
|
+
`Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Use a relative URL starting with /, ./, ../, or #, or an http:, https:, or mailto: URL.`,
|
|
438
|
+
{ code: "WD610", file: ctx.file }
|
|
406
439
|
);
|
|
407
440
|
}
|
package/src/deploy.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildSite } from "./builder.js";
|
|
5
|
+
import { wdError } from "./compiler/context.js";
|
|
5
6
|
import { BASE_SECURITY_HEADERS, REACTIVE_CSP } from "./headers.js";
|
|
6
7
|
|
|
7
8
|
/** The platforms `darkmown deploy` knows how to drive. */
|
|
@@ -25,7 +26,9 @@ export const DEPLOY_TARGETS = ["vercel", "cloudflare"];
|
|
|
25
26
|
*/
|
|
26
27
|
export async function deploy({ cwd, target, prod = false, log, run = spawnProcess, projectName }) {
|
|
27
28
|
if (!DEPLOY_TARGETS.includes(target)) {
|
|
28
|
-
throw
|
|
29
|
+
throw wdError(`Unknown deploy target "${target}". Use one of: ${DEPLOY_TARGETS.join(", ")}.`, {
|
|
30
|
+
code: "WD904"
|
|
31
|
+
});
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
ensurePlatformConfig(cwd, target);
|
|
@@ -46,11 +49,12 @@ export async function deploy({ cwd, target, prod = false, log, run = spawnProces
|
|
|
46
49
|
output
|
|
47
50
|
)
|
|
48
51
|
) {
|
|
49
|
-
throw
|
|
50
|
-
`The ${target} CLI is not signed in. Run \`${plan.loginHint}\` (try \`! ${plan.loginHint}\` in this session), then re-run \`darkmown deploy ${target}
|
|
52
|
+
throw wdError(
|
|
53
|
+
`The ${target} CLI is not signed in. Run \`${plan.loginHint}\` (try \`! ${plan.loginHint}\` in this session), then re-run \`darkmown deploy ${target}\`.`,
|
|
54
|
+
{ code: "WD905" }
|
|
51
55
|
);
|
|
52
56
|
}
|
|
53
|
-
throw
|
|
57
|
+
throw wdError(`${target} deploy failed (exit ${result.code}).`, { code: "WD906" });
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
const url = plan.parseUrl(output);
|