@zvndev/darkmown 2.2.0 → 2.3.1-next.1.g3a4a8dc
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 +13 -0
- package/README.md +34 -2
- package/docs/spec-alignment.md +10 -0
- package/package.json +16 -11
- package/site/pages/docs/index.wd +14 -0
- package/site/pages/playground/index.js +47 -46
- package/src/catalog.js +16 -1
- package/src/compiler/actions.js +42 -22
- package/src/compiler/body.js +36 -14
- package/src/compiler/collections.js +16 -10
- package/src/compiler/context.js +39 -10
- package/src/compiler/fetch.js +39 -15
- package/src/compiler/format.js +5 -2
- package/src/compiler/forms.js +91 -26
- 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 +63 -35
- package/src/compiler/markdown.js +4 -3
- package/src/compiler/media.js +17 -3
- package/src/compiler/page.js +8 -6
- package/src/compiler/predicates.js +49 -24
- package/src/compiler/reader.js +12 -2
- package/src/compiler/state.js +16 -2
- package/src/compiler/structure.js +24 -14
- package/src/deploy.js +8 -4
- package/src/errors.js +1156 -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 +647 -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 +48 -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/src/api-runner.d.ts +94 -0
- package/types/src/builder.d.ts +170 -0
- package/types/src/catalog.d.ts +97 -0
- package/types/src/compiler/actions.d.ts +41 -0
- package/types/src/compiler/body.d.ts +11 -0
- package/types/src/compiler/collections.d.ts +76 -0
- package/types/src/compiler/context.d.ts +647 -0
- package/types/src/compiler/directives.d.ts +6 -0
- package/types/src/compiler/expr-ast.d.ts +20 -0
- package/types/src/compiler/fetch.d.ts +29 -0
- package/types/src/compiler/format.d.ts +49 -0
- package/types/src/compiler/forms.d.ts +94 -0
- package/types/src/compiler/frontmatter.d.ts +33 -0
- package/types/src/compiler/fs-reader.d.ts +7 -0
- package/types/src/compiler/image-size.d.ts +14 -0
- package/types/src/compiler/includes.d.ts +100 -0
- package/types/src/compiler/index.d.ts +5 -0
- package/types/src/compiler/interpolation.d.ts +115 -0
- package/types/src/compiler/loops.d.ts +36 -0
- package/types/src/compiler/markdown.d.ts +34 -0
- package/types/src/compiler/media.d.ts +32 -0
- package/types/src/compiler/page.d.ts +109 -0
- package/types/src/compiler/predicates.d.ts +50 -0
- package/types/src/compiler/reader.d.ts +48 -0
- package/types/src/compiler/state.d.ts +68 -0
- package/types/src/compiler/structure.d.ts +56 -0
- package/types/src/compiler.d.ts +48 -0
- package/types/src/config.d.ts +45 -0
- package/types/src/dev.d.ts +58 -0
- package/types/src/errors.d.ts +100 -0
- package/types/src/feeds.d.ts +150 -0
- package/types/src/headers.d.ts +44 -0
- package/types/src/highlight.d.ts +24 -0
- package/types/src/router.d.ts +78 -0
- package/types/src/scaffold.d.ts +20 -0
- package/types/src/skin.d.ts +48 -0
- package/types/src/statics.d.ts +24 -0
- package/types/statics.d.ts +2 -1
package/src/compiler/includes.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { scopeIdFor } from "../skin.js";
|
|
10
|
-
import { pageIncludeExtensions } from "./context.js";
|
|
10
|
+
import { pageIncludeExtensions, wdError } from "./context.js";
|
|
11
11
|
import { stripQuotes } from "./interpolation.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -181,14 +181,18 @@ export function resolveInclude(spec, fromFile, context, allowAny, loc, reader) {
|
|
|
181
181
|
for (const candidate of candidates) {
|
|
182
182
|
const resolved = path.resolve(candidate);
|
|
183
183
|
if (!isAllowedInclude(resolved, context)) {
|
|
184
|
-
throw
|
|
184
|
+
throw wdError(`Include "${spec}" from ${loc} resolves outside site/pages or site/_`, {
|
|
185
|
+
code: "WD601",
|
|
186
|
+
file: fromFile
|
|
187
|
+
});
|
|
185
188
|
}
|
|
186
189
|
if (!reader.exists(resolved)) continue;
|
|
187
190
|
if (!allowAny && !pageIncludeExtensions.includes(path.extname(resolved))) continue;
|
|
188
191
|
return resolved;
|
|
189
192
|
}
|
|
190
|
-
throw
|
|
191
|
-
`Could not resolve include "${spec}" from ${loc}. Looked in site/pages and site/_ — check the path and leading slash
|
|
193
|
+
throw wdError(
|
|
194
|
+
`Could not resolve include "${spec}" from ${loc}. Looked in site/pages and site/_ — check the path and leading slash.`,
|
|
195
|
+
{ code: "WD602", file: fromFile }
|
|
192
196
|
);
|
|
193
197
|
}
|
|
194
198
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// Interpolation primitives: path/value resolution, state-key lookup, scalar
|
|
3
3
|
// parsing, and HTML escaping. These are the leaf utilities the directive
|
|
4
|
-
// handlers, loops, predicates, and markdown layer all build on —
|
|
5
|
-
//
|
|
4
|
+
// handlers, loops, predicates, and markdown layer all build on — it imports
|
|
5
|
+
// only `context.js` (which imports nothing), so it sits at the root of the DAG.
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
+
import { wdError } from "./context.js";
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* @typedef {import("./context.js").Scope} Scope
|
|
10
12
|
* @typedef {import("./context.js").Ctx} Ctx
|
|
@@ -66,7 +68,11 @@ export function getPath(value, segments) {
|
|
|
66
68
|
export function validatePath(path, ctx, use) {
|
|
67
69
|
const segs = path.split(".");
|
|
68
70
|
if (segs.some((seg) => ["constructor", "prototype", "__proto__"].includes(seg))) {
|
|
69
|
-
throw
|
|
71
|
+
throw wdError(`Path segment in "${path}" is not allowed in ${ctx.file}. Use: ${use}`, {
|
|
72
|
+
code: "WD002",
|
|
73
|
+
file: ctx.file,
|
|
74
|
+
hint: use
|
|
75
|
+
});
|
|
70
76
|
}
|
|
71
77
|
return segs;
|
|
72
78
|
}
|
|
@@ -98,8 +104,9 @@ export function interpolateLeaf(value, expr, ctx) {
|
|
|
98
104
|
if (value === null || value === undefined) return "";
|
|
99
105
|
if (Array.isArray(value)) return value.join(", ");
|
|
100
106
|
if (typeof value === "object") {
|
|
101
|
-
throw
|
|
102
|
-
`Cannot interpolate the object value "{ ${expr} }" in ${ctx.file}. Interpolate a field instead — e.g. { ${expr}.title } — or iterate it with @loop ${expr} into item
|
|
107
|
+
throw wdError(
|
|
108
|
+
`Cannot interpolate the object value "{ ${expr} }" in ${ctx.file}. Interpolate a field instead — e.g. { ${expr}.title } — or iterate it with @loop ${expr} into item.`,
|
|
109
|
+
{ code: "WD003", file: ctx.file }
|
|
103
110
|
);
|
|
104
111
|
}
|
|
105
112
|
return String(value);
|
|
@@ -143,11 +150,12 @@ export function parseStateValue(raw, where) {
|
|
|
143
150
|
const open = value[0];
|
|
144
151
|
const close = open === "[" ? "]" : "}";
|
|
145
152
|
const shown = value.length > 60 ? `${value.slice(0, 57)}…` : value;
|
|
146
|
-
throw
|
|
153
|
+
throw wdError(
|
|
147
154
|
`Unbalanced JSON value "${shown}"${where ? ` in ${where}` : ""}: this opens "${open}" but ` +
|
|
148
155
|
`never closes it. :state/:store/:theme accept a multi-line array/object, but the literal ` +
|
|
149
156
|
`must balance (a "${close}" is missing) with no blank lines inside it. ` +
|
|
150
|
-
`Quote it for literal bracket text (e.g. = "[draft]")
|
|
157
|
+
`Quote it for literal bracket text (e.g. = "[draft]").`,
|
|
158
|
+
{ code: "WD004" }
|
|
151
159
|
);
|
|
152
160
|
}
|
|
153
161
|
return value;
|
package/src/compiler/loops.js
CHANGED
|
@@ -61,7 +61,10 @@ function parseLoopClauses(tail, itemName, ctx) {
|
|
|
61
61
|
/** @type {number|null} */ let paginate = null;
|
|
62
62
|
/** @returns {never} */
|
|
63
63
|
const bad = () => {
|
|
64
|
-
throw
|
|
64
|
+
throw wdError(`Malformed @loop clause in ${ctx.file}: "${tail.trim()}". ${LOOP_USAGE}`, {
|
|
65
|
+
code: "WD102",
|
|
66
|
+
file: ctx.file
|
|
67
|
+
});
|
|
65
68
|
};
|
|
66
69
|
|
|
67
70
|
// `sortable` is a position-independent flag (drag-reorder); peel it wherever it
|
|
@@ -152,20 +155,25 @@ function parseSortClause(fieldTok, dirTok, itemName, ctx) {
|
|
|
152
155
|
if (fieldState) {
|
|
153
156
|
const resolved = resolveStateKey(fieldState[1], ctx);
|
|
154
157
|
if (!resolved)
|
|
155
|
-
throw
|
|
156
|
-
`@loop sort by { ${fieldState[1]} } references unknown :state/:store in ${ctx.file}. ${LOOP_USAGE}
|
|
158
|
+
throw wdError(
|
|
159
|
+
`@loop sort by { ${fieldState[1]} } references unknown :state/:store in ${ctx.file}. ${LOOP_USAGE}`,
|
|
160
|
+
{ code: "WD103", file: ctx.file }
|
|
157
161
|
);
|
|
158
162
|
key = resolved;
|
|
159
163
|
keyKind = "key";
|
|
160
164
|
} else {
|
|
161
165
|
const segs = fieldTok.split(".");
|
|
162
166
|
if (segs[0] !== itemName)
|
|
163
|
-
throw
|
|
164
|
-
`@loop sort key "${fieldTok}" must start with the loop item "${itemName}" in ${ctx.file}. ${LOOP_USAGE}
|
|
167
|
+
throw wdError(
|
|
168
|
+
`@loop sort key "${fieldTok}" must start with the loop item "${itemName}" in ${ctx.file}. ${LOOP_USAGE}`,
|
|
169
|
+
{ code: "WD104", file: ctx.file }
|
|
165
170
|
);
|
|
166
171
|
const rest = segs.slice(1);
|
|
167
172
|
if (rest.some((seg) => ["constructor", "prototype", "__proto__"].includes(seg)))
|
|
168
|
-
throw
|
|
173
|
+
throw wdError(`Sort key "${fieldTok}" is not allowed in @loop (${ctx.file})`, {
|
|
174
|
+
code: "WD105",
|
|
175
|
+
file: ctx.file
|
|
176
|
+
});
|
|
169
177
|
key = rest.join(".");
|
|
170
178
|
keyKind = "literal";
|
|
171
179
|
}
|
|
@@ -177,8 +185,9 @@ function parseSortClause(fieldTok, dirTok, itemName, ctx) {
|
|
|
177
185
|
if (dirState) {
|
|
178
186
|
const resolved = resolveStateKey(dirState[1], ctx);
|
|
179
187
|
if (!resolved)
|
|
180
|
-
throw
|
|
181
|
-
`@loop sort direction { ${dirState[1]} } references unknown :state/:store in ${ctx.file}. ${LOOP_USAGE}
|
|
188
|
+
throw wdError(
|
|
189
|
+
`@loop sort direction { ${dirState[1]} } references unknown :state/:store in ${ctx.file}. ${LOOP_USAGE}`,
|
|
190
|
+
{ code: "WD106", file: ctx.file }
|
|
182
191
|
);
|
|
183
192
|
dir = resolved;
|
|
184
193
|
dirKind = "key";
|
|
@@ -201,8 +210,9 @@ function parseNumArg(tok, ctx) {
|
|
|
201
210
|
if (/^\d+$/.test(tok)) return { kind: "literal", value: Number(tok) };
|
|
202
211
|
const key = resolveStateKey(tok, ctx);
|
|
203
212
|
if (!key)
|
|
204
|
-
throw
|
|
205
|
-
`@loop offset/limit "${tok}" in ${ctx.file} is neither a non-negative integer nor a declared :state. ${LOOP_USAGE}
|
|
213
|
+
throw wdError(
|
|
214
|
+
`@loop offset/limit "${tok}" in ${ctx.file} is neither a non-negative integer nor a declared :state. ${LOOP_USAGE}`,
|
|
215
|
+
{ code: "WD107", file: ctx.file }
|
|
206
216
|
);
|
|
207
217
|
return { kind: "key", value: key };
|
|
208
218
|
}
|
|
@@ -226,6 +236,7 @@ export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart =
|
|
|
226
236
|
const match = line.match(/^@loop\s+(.+?)\s+into\s+([A-Za-z_$][\w$]*)(\s+.+?)?\s*$/);
|
|
227
237
|
if (!match)
|
|
228
238
|
throw wdError(`Malformed @loop in ${at(ctx, index)}: ${line}. ${LOOP_USAGE}`, {
|
|
239
|
+
code: "WD101",
|
|
229
240
|
file: ctx.file,
|
|
230
241
|
line: lineOf(ctx, index),
|
|
231
242
|
hint: LOOP_USAGE.slice("Use: ".length),
|
|
@@ -258,16 +269,18 @@ export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart =
|
|
|
258
269
|
clauses.limit ||
|
|
259
270
|
clauses.paginate)
|
|
260
271
|
) {
|
|
261
|
-
throw
|
|
262
|
-
`@loop sortable cannot combine with where/sort/reverse/offset/limit/paginate in ${ctx.file}. ${LOOP_USAGE}
|
|
272
|
+
throw wdError(
|
|
273
|
+
`@loop sortable cannot combine with where/sort/reverse/offset/limit/paginate in ${ctx.file}. ${LOOP_USAGE}`,
|
|
274
|
+
{ code: "WD108", file: ctx.file }
|
|
263
275
|
);
|
|
264
276
|
}
|
|
265
277
|
// `paginate N` OWNS the page slice, so an explicit offset/limit alongside it is
|
|
266
278
|
// a conflict (which one wins?). Reject it with a clear hint rather than silently
|
|
267
279
|
// letting one override the other.
|
|
268
280
|
if (clauses.paginate && (clauses.offset || clauses.limit)) {
|
|
269
|
-
throw
|
|
270
|
-
`@loop paginate cannot combine with offset/limit in ${ctx.file} — paginate already slices each page. ${LOOP_USAGE}
|
|
281
|
+
throw wdError(
|
|
282
|
+
`@loop paginate cannot combine with offset/limit in ${ctx.file} — paginate already slices each page. ${LOOP_USAGE}`,
|
|
283
|
+
{ code: "WD109", file: ctx.file }
|
|
271
284
|
);
|
|
272
285
|
}
|
|
273
286
|
/** @type {LoopOpts} */
|
|
@@ -298,7 +311,7 @@ export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart =
|
|
|
298
311
|
source.startsWith("../") ||
|
|
299
312
|
source.endsWith(".json")
|
|
300
313
|
) {
|
|
301
|
-
if (opts.paginate) throw
|
|
314
|
+
if (opts.paginate) throw paginateOnlyCollections(ctx);
|
|
302
315
|
const dataFile = resolveInclude(
|
|
303
316
|
source,
|
|
304
317
|
ctx.file,
|
|
@@ -309,23 +322,28 @@ export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart =
|
|
|
309
322
|
);
|
|
310
323
|
ctx.comp.deps.add(dataFile);
|
|
311
324
|
const rows = JSON.parse(ctx.comp.reader.readText(dataFile));
|
|
312
|
-
if (!Array.isArray(rows))
|
|
325
|
+
if (!Array.isArray(rows))
|
|
326
|
+
throw wdError(`@loop data must be a JSON array: ${dataFile}`, {
|
|
327
|
+
code: "WD111",
|
|
328
|
+
file: ctx.file
|
|
329
|
+
});
|
|
313
330
|
return loopOverData(rows, itemName, bodyLines, bodyCtx, opts);
|
|
314
331
|
}
|
|
315
332
|
|
|
316
333
|
const resolved = lookupPath(source, ctx);
|
|
317
334
|
if (resolved.found) {
|
|
318
|
-
if (opts.paginate) throw
|
|
335
|
+
if (opts.paginate) throw paginateOnlyCollections(ctx);
|
|
319
336
|
// A missing/unset field (e.g. an optional frontmatter key) is an EMPTY
|
|
320
337
|
// list, not an error — the `@empty` branch renders, matching the runtime.
|
|
321
338
|
if (resolved.value === null || resolved.value === undefined) {
|
|
322
339
|
return loopOverData([], itemName, bodyLines, bodyCtx, opts);
|
|
323
340
|
}
|
|
324
341
|
if (!Array.isArray(resolved.value)) {
|
|
325
|
-
throw
|
|
342
|
+
throw wdError(
|
|
326
343
|
`@loop ${source} in ${at(ctx, index)} found an in-scope value, but it is not a list ` +
|
|
327
344
|
`(got ${typeof resolved.value}). Use: a list value (e.g. \`tags: [a, b]\` in frontmatter) — ` +
|
|
328
|
-
`or leave the field out entirely, which loops zero rows and renders the @empty branch
|
|
345
|
+
`or leave the field out entirely, which loops zero rows and renders the @empty branch.`,
|
|
346
|
+
{ code: "WD112", file: ctx.file, line: lineOf(ctx, index) }
|
|
329
347
|
);
|
|
330
348
|
}
|
|
331
349
|
return loopOverData(resolved.value, itemName, bodyLines, bodyCtx, opts);
|
|
@@ -335,11 +353,14 @@ export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart =
|
|
|
335
353
|
const segs = source.split(".");
|
|
336
354
|
if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(source)) {
|
|
337
355
|
if (segs.some((seg) => ["constructor", "prototype", "__proto__"].includes(seg))) {
|
|
338
|
-
throw
|
|
356
|
+
throw wdError(`@loop source "${source}" is not allowed in ${ctx.file}`, {
|
|
357
|
+
code: "WD113",
|
|
358
|
+
file: ctx.file
|
|
359
|
+
});
|
|
339
360
|
}
|
|
340
361
|
const key = resolveStateKey(segs[0], ctx);
|
|
341
362
|
if (key) {
|
|
342
|
-
if (opts.paginate) throw
|
|
363
|
+
if (opts.paginate) throw paginateOnlyCollections(ctx);
|
|
343
364
|
const fullKey = segs.length > 1 ? `${key}.${segs.slice(1).join(".")}` : key;
|
|
344
365
|
return reactiveLoop(fullKey, itemName, bodyLines, bodyCtx, opts);
|
|
345
366
|
}
|
|
@@ -355,16 +376,17 @@ export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart =
|
|
|
355
376
|
// the enclosing row item — an ITEM-RELATIVE loop the runtime fills per row.
|
|
356
377
|
if (ctx.loopItem && segs[0] === ctx.loopItem && segs.length > 1) {
|
|
357
378
|
if (opts.sortable) {
|
|
358
|
-
throw
|
|
359
|
-
`@loop sortable is not supported on a nested (item-relative) loop in ${ctx.file}. ${LOOP_USAGE}
|
|
379
|
+
throw wdError(
|
|
380
|
+
`@loop sortable is not supported on a nested (item-relative) loop in ${ctx.file}. ${LOOP_USAGE}`,
|
|
381
|
+
{ code: "WD114", file: ctx.file }
|
|
360
382
|
);
|
|
361
383
|
}
|
|
362
|
-
if (opts.paginate) throw
|
|
384
|
+
if (opts.paginate) throw paginateOnlyCollections(ctx);
|
|
363
385
|
return itemRelativeLoop(segs.slice(1).join("."), itemName, bodyLines, bodyCtx, opts);
|
|
364
386
|
}
|
|
365
387
|
}
|
|
366
388
|
|
|
367
|
-
throw
|
|
389
|
+
throw unresolvedSourceError(source, ctx);
|
|
368
390
|
}
|
|
369
391
|
|
|
370
392
|
/**
|
|
@@ -373,15 +395,16 @@ export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart =
|
|
|
373
395
|
* collection reference is immediately actionable.
|
|
374
396
|
* @param {string} source
|
|
375
397
|
* @param {Ctx} ctx
|
|
376
|
-
* @returns {
|
|
398
|
+
* @returns {Error}
|
|
377
399
|
*/
|
|
378
400
|
function unresolvedSourceError(source, ctx) {
|
|
379
401
|
const names = [...ctx.comp.collections.keys()].sort();
|
|
380
402
|
const collectionsHint = names.length ? ` Available collections: ${names.join(", ")}.` : "";
|
|
381
|
-
return (
|
|
403
|
+
return wdError(
|
|
382
404
|
`@loop source "${source}" in ${ctx.file} was not found. Loop over a collection ` +
|
|
383
|
-
|
|
384
|
-
|
|
405
|
+
`(a site/pages/<name>/ subdirectory, by its bare name), a JSON file ` +
|
|
406
|
+
`(@loop /data.json into row), an in-scope value, or a :state list.${collectionsHint}`,
|
|
407
|
+
{ code: "WD115", file: ctx.file }
|
|
385
408
|
);
|
|
386
409
|
}
|
|
387
410
|
|
|
@@ -389,10 +412,13 @@ function unresolvedSourceError(source, ctx) {
|
|
|
389
412
|
* The error for `paginate` on a non-collection source: pagination multiplies
|
|
390
413
|
* static routes, which only makes sense for a build-time collection listing.
|
|
391
414
|
* @param {Ctx} ctx
|
|
392
|
-
* @returns {
|
|
415
|
+
* @returns {Error}
|
|
393
416
|
*/
|
|
394
417
|
function paginateOnlyCollections(ctx) {
|
|
395
|
-
return
|
|
418
|
+
return wdError(
|
|
419
|
+
`@loop paginate requires a collection source (a site/pages/<name>/ subdirectory) in ${ctx.file}. ${LOOP_USAGE}`,
|
|
420
|
+
{ code: "WD110", file: ctx.file }
|
|
421
|
+
);
|
|
396
422
|
}
|
|
397
423
|
|
|
398
424
|
/**
|
|
@@ -410,9 +436,10 @@ function paginateOnlyCollections(ctx) {
|
|
|
410
436
|
function assertReactiveDepth(ctx) {
|
|
411
437
|
if ((ctx.reactiveDepth ?? 0) < 2) return;
|
|
412
438
|
const opener = ctx.loopOpener ?? { at: ctx.file, line: "" };
|
|
413
|
-
throw
|
|
439
|
+
throw wdError(
|
|
414
440
|
`Reactive @loop nesting is limited to one inner level in ${opener.at}: "${opener.line}". ` +
|
|
415
|
-
`Unroll the outer data at build time (JSON/frontmatter source) or move the innermost list into build-time data
|
|
441
|
+
`Unroll the outer data at build time (JSON/frontmatter source) or move the innermost list into build-time data.`,
|
|
442
|
+
{ code: "WD116", file: ctx.file }
|
|
416
443
|
);
|
|
417
444
|
}
|
|
418
445
|
|
|
@@ -522,8 +549,9 @@ function pipelineRows(rows, where, opts, ctx) {
|
|
|
522
549
|
*/
|
|
523
550
|
function loopOverData(rows, itemName, bodyLines, ctx, opts) {
|
|
524
551
|
if (opts.sortable) {
|
|
525
|
-
throw
|
|
526
|
-
`@loop sortable requires a :state or :store list to reorder, not a JSON file or in-scope value, in ${ctx.file}. ${LOOP_USAGE}
|
|
552
|
+
throw wdError(
|
|
553
|
+
`@loop sortable requires a :state or :store list to reorder, not a JSON file or in-scope value, in ${ctx.file}. ${LOOP_USAGE}`,
|
|
554
|
+
{ code: "WD117", file: ctx.file }
|
|
527
555
|
);
|
|
528
556
|
}
|
|
529
557
|
// Reactive when the where reads state OR an offset/limit references a state key.
|
package/src/compiler/markdown.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import MarkdownIt from "markdown-it";
|
|
9
9
|
import { highlightCode } from "../highlight.js";
|
|
10
|
-
import { LOOP_META } from "./context.js";
|
|
10
|
+
import { LOOP_META, wdError } from "./context.js";
|
|
11
11
|
import { applyPipeline, fmtAttr, validatePipes } from "./format.js";
|
|
12
12
|
import {
|
|
13
13
|
escapeHtml,
|
|
@@ -266,8 +266,9 @@ function resolveBindingHtml(expr, ctx) {
|
|
|
266
266
|
// Per-row meta vars ($index/$number/$first/$last/$count) — only inside a loop.
|
|
267
267
|
if (LOOP_META[head]) {
|
|
268
268
|
if (!ctx.loopMeta)
|
|
269
|
-
throw
|
|
270
|
-
`"{ ${expr} }" uses the loop meta variable "${head}" outside a @loop in ${ctx.file}. Move it into a loop body, or rename your value
|
|
269
|
+
throw wdError(
|
|
270
|
+
`"{ ${expr} }" uses the loop meta variable "${head}" outside a @loop in ${ctx.file}. Move it into a loop body, or rename your value.`,
|
|
271
|
+
{ code: "WD005", file: ctx.file }
|
|
271
272
|
);
|
|
272
273
|
if (ctx.loopItem) return `<span data-wd-each-meta="${LOOP_META[head]}"${fmt}></span>`; // reactive: filled per row
|
|
273
274
|
// static: the value is in scope (injected by staticUnroll); fall through.
|
package/src/compiler/media.js
CHANGED
|
@@ -49,6 +49,7 @@ export function handleMedia(line, kind, ctx, index) {
|
|
|
49
49
|
const example = kind === "video" ? VIDEO_EXAMPLE : AUDIO_EXAMPLE;
|
|
50
50
|
const hint = `:${kind} /clip [controls] [autoplay] [loop] [muted] — e.g. ${example}`;
|
|
51
51
|
throw wdError(`Malformed :${kind} in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
52
|
+
code: "WD701",
|
|
52
53
|
file: ctx.file,
|
|
53
54
|
line: lineOf(ctx, index),
|
|
54
55
|
hint,
|
|
@@ -65,11 +66,19 @@ export function handleMedia(line, kind, ctx, index) {
|
|
|
65
66
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
66
67
|
if (token[3]) {
|
|
67
68
|
if (!spec.flags.includes(token[3]))
|
|
68
|
-
throw
|
|
69
|
+
throw wdError(`Unknown :${kind} flag "${token[3]}" in ${at(ctx, index)}`, {
|
|
70
|
+
code: "WD702",
|
|
71
|
+
file: ctx.file,
|
|
72
|
+
line: lineOf(ctx, index)
|
|
73
|
+
});
|
|
69
74
|
flags.add(token[3]);
|
|
70
75
|
} else {
|
|
71
76
|
if (!spec.attrs.includes(token[1]))
|
|
72
|
-
throw
|
|
77
|
+
throw wdError(`Unknown :${kind} attribute "${token[1]}" in ${at(ctx, index)}`, {
|
|
78
|
+
code: "WD703",
|
|
79
|
+
file: ctx.file,
|
|
80
|
+
line: lineOf(ctx, index)
|
|
81
|
+
});
|
|
73
82
|
attrs.set(token[1], stripQuotes(token[2]));
|
|
74
83
|
}
|
|
75
84
|
}
|
|
@@ -105,6 +114,7 @@ export function handleEmbed(line, ctx, index) {
|
|
|
105
114
|
if (!match) {
|
|
106
115
|
const hint = `:embed https://youtu.be/ID [title="…"] — e.g. ${EMBED_EXAMPLE}`;
|
|
107
116
|
throw wdError(`Malformed :embed in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
117
|
+
code: "WD704",
|
|
108
118
|
file: ctx.file,
|
|
109
119
|
line: lineOf(ctx, index),
|
|
110
120
|
hint,
|
|
@@ -116,7 +126,11 @@ export function handleEmbed(line, ctx, index) {
|
|
|
116
126
|
const re = /([A-Za-z-]+)=("[^"]*"|\S+)/g;
|
|
117
127
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
118
128
|
if (token[1] !== "title")
|
|
119
|
-
throw
|
|
129
|
+
throw wdError(`Unknown :embed attribute "${token[1]}" in ${at(ctx, index)}`, {
|
|
130
|
+
code: "WD705",
|
|
131
|
+
file: ctx.file,
|
|
132
|
+
line: lineOf(ctx, index)
|
|
133
|
+
});
|
|
120
134
|
title = stripQuotes(token[2]);
|
|
121
135
|
}
|
|
122
136
|
const { url, label } = resolveEmbed(raw, ctx);
|
package/src/compiler/page.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import { imageSize } from "image-size";
|
|
10
9
|
import { createPaths } from "../config.js";
|
|
11
10
|
import { htmlHasHighlight } from "../highlight.js";
|
|
12
11
|
import { compileBody } from "./body.js";
|
|
13
|
-
import { createCompilation, createScope } from "./context.js";
|
|
12
|
+
import { createCompilation, createScope, wdError } from "./context.js";
|
|
14
13
|
import { parseFrontmatter, warnLikelyFrontmatter } from "./frontmatter.js";
|
|
14
|
+
import { imageSize } from "./image-size.js";
|
|
15
15
|
import {
|
|
16
16
|
collectColocatedAssets,
|
|
17
17
|
scanMarkdownHints,
|
|
@@ -279,8 +279,9 @@ function measureImage(src, paths, reader) {
|
|
|
279
279
|
return null; // page-relative: the source directory is lost after assembly
|
|
280
280
|
}
|
|
281
281
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
// `imageSize` itself never throws (an unsupported or malformed image is just
|
|
283
|
+
// null); the try/catch is for the read, which does throw on a missing file.
|
|
284
|
+
return imageSize(reader.readBinary(filePath));
|
|
284
285
|
} catch {
|
|
285
286
|
/* missing or unreadable — degrade to no dimensions */
|
|
286
287
|
}
|
|
@@ -363,8 +364,9 @@ export function compileFile(
|
|
|
363
364
|
) {
|
|
364
365
|
const real = comp.reader.realpath(file);
|
|
365
366
|
if (stack.includes(real)) {
|
|
366
|
-
throw
|
|
367
|
-
`Include cycle detected: ${[...stack, real].map((p) => path.basename(p)).join(" -> ")}
|
|
367
|
+
throw wdError(
|
|
368
|
+
`[WD612] Include cycle detected: ${[...stack, real].map((p) => path.basename(p)).join(" -> ")}`,
|
|
369
|
+
{ code: "WD612", file }
|
|
368
370
|
);
|
|
369
371
|
}
|
|
370
372
|
|
|
@@ -65,6 +65,7 @@ function compileCondition(cond, itemName, ctx) {
|
|
|
65
65
|
if (!m) {
|
|
66
66
|
const hint = `${itemName}.field contains state, or ${itemName}.field <op> value — e.g. ${itemName}.price < 50`;
|
|
67
67
|
throw wdError(`Malformed where-condition "${cond}" in ${ctx.file}. Use: ${hint}`, {
|
|
68
|
+
code: "WD220",
|
|
68
69
|
file: ctx.file,
|
|
69
70
|
hint,
|
|
70
71
|
example: `${itemName}.price < 50`
|
|
@@ -90,20 +91,25 @@ function compileOperand(tok, itemName, ctx) {
|
|
|
90
91
|
if (/^-?\d+(?:\.\d+)?$/.test(tok)) return { code: tok, usesState: false };
|
|
91
92
|
if (["true", "false", "null"].includes(tok)) return { code: tok, usesState: false };
|
|
92
93
|
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(tok)) {
|
|
93
|
-
throw
|
|
94
|
-
`Unsupported operand "${tok}" in @loop where (${ctx.file}). Use ${itemName}.field, a :state name, a number, or a "string"
|
|
94
|
+
throw wdError(
|
|
95
|
+
`Unsupported operand "${tok}" in @loop where (${ctx.file}). Use ${itemName}.field, a :state name, a number, or a "string".`,
|
|
96
|
+
{ code: "WD221", file: ctx.file }
|
|
95
97
|
);
|
|
96
98
|
}
|
|
97
99
|
const segs = tok.split(".");
|
|
98
100
|
if (segs.some((seg) => ["constructor", "prototype", "__proto__"].includes(seg))) {
|
|
99
|
-
throw
|
|
101
|
+
throw wdError(`Path "${tok}" is not allowed in @loop where (${ctx.file})`, {
|
|
102
|
+
code: "WD222",
|
|
103
|
+
file: ctx.file
|
|
104
|
+
});
|
|
100
105
|
}
|
|
101
106
|
if (segs[0] === itemName)
|
|
102
107
|
return { code: `I(${JSON.stringify(segs.slice(1).join("."))})`, usesState: false };
|
|
103
108
|
const key = resolveStateKey(segs[0], ctx);
|
|
104
109
|
if (!key) {
|
|
105
|
-
throw
|
|
106
|
-
`@loop where references unknown name "${segs[0]}" in ${ctx.file}. Use the loop item (${itemName}.field) or a declared :state
|
|
110
|
+
throw wdError(
|
|
111
|
+
`@loop where references unknown name "${segs[0]}" in ${ctx.file}. Use the loop item (${itemName}.field) or a declared :state.`,
|
|
112
|
+
{ code: "WD223", file: ctx.file }
|
|
107
113
|
);
|
|
108
114
|
}
|
|
109
115
|
const rest = segs.slice(1).join(".");
|
|
@@ -149,13 +155,17 @@ function compileWhenOperand(tok, ctx, what) {
|
|
|
149
155
|
if (/^-?\d+(?:\.\d+)?$/.test(tok)) return { code: tok, state: false, item: false };
|
|
150
156
|
if (["true", "false", "null"].includes(tok)) return { code: tok, state: false, item: false };
|
|
151
157
|
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(tok)) {
|
|
152
|
-
throw
|
|
153
|
-
`Unsupported operand "${tok}" in ${what} (${ctx.file}). Use item.field, a :state name, a number, or a "string"
|
|
158
|
+
throw wdError(
|
|
159
|
+
`Unsupported operand "${tok}" in ${what} (${ctx.file}). Use item.field, a :state name, a number, or a "string".`,
|
|
160
|
+
{ code: "WD224", file: ctx.file }
|
|
154
161
|
);
|
|
155
162
|
}
|
|
156
163
|
const segs = tok.split(".");
|
|
157
164
|
if (segs.some((seg) => ["constructor", "prototype", "__proto__"].includes(seg))) {
|
|
158
|
-
throw
|
|
165
|
+
throw wdError(`Path "${tok}" is not allowed in ${what} (${ctx.file})`, {
|
|
166
|
+
code: "WD225",
|
|
167
|
+
file: ctx.file
|
|
168
|
+
});
|
|
159
169
|
}
|
|
160
170
|
const scoped = lookupVar(ctx.scope, segs[0]);
|
|
161
171
|
if (scoped.found)
|
|
@@ -168,8 +178,9 @@ function compileWhenOperand(tok, ctx, what) {
|
|
|
168
178
|
return { code: `I(${JSON.stringify(segs.slice(1).join("."))})`, state: false, item: true };
|
|
169
179
|
const key = resolveStateKey(segs[0], ctx);
|
|
170
180
|
if (!key)
|
|
171
|
-
throw
|
|
172
|
-
`${what} references unknown name "${segs[0]}" in ${ctx.file}. Use a loop item field, a declared :state, a number, or a "string"
|
|
181
|
+
throw wdError(
|
|
182
|
+
`${what} references unknown name "${segs[0]}" in ${ctx.file}. Use a loop item field, a declared :state, a number, or a "string".`,
|
|
183
|
+
{ code: "WD226", file: ctx.file }
|
|
173
184
|
);
|
|
174
185
|
const rest = segs.slice(1).join(".");
|
|
175
186
|
return {
|
|
@@ -262,12 +273,16 @@ export function compileComputedExpr(raw, ctx) {
|
|
|
262
273
|
["constructor", "prototype", "__proto__"].includes(s)
|
|
263
274
|
)
|
|
264
275
|
) {
|
|
265
|
-
throw
|
|
276
|
+
throw wdError(`Path "${listPath}" is not allowed in :computed ${name}() (${ctx.file})`, {
|
|
277
|
+
code: "WD227",
|
|
278
|
+
file: ctx.file
|
|
279
|
+
});
|
|
266
280
|
}
|
|
267
281
|
const key = resolveStateKey(segs[0], ctx);
|
|
268
282
|
if (!key) {
|
|
269
|
-
throw
|
|
270
|
-
`:computed ${name}() references unknown state "${segs[0]}" in ${ctx.file}. Declare it with :state or :fetch first
|
|
283
|
+
throw wdError(
|
|
284
|
+
`:computed ${name}() references unknown state "${segs[0]}" in ${ctx.file}. Declare it with :state or :fetch first.`,
|
|
285
|
+
{ code: "WD228", file: ctx.file }
|
|
271
286
|
);
|
|
272
287
|
}
|
|
273
288
|
const rest = segs.slice(1).join(".");
|
|
@@ -279,15 +294,22 @@ export function compileComputedExpr(raw, ctx) {
|
|
|
279
294
|
}
|
|
280
295
|
);
|
|
281
296
|
if (/["'\\`]/.test(expr)) {
|
|
282
|
-
throw
|
|
297
|
+
throw wdError(`Unsupported string syntax in :computed expression "${raw}" (${ctx.file})`, {
|
|
298
|
+
code: "WD229",
|
|
299
|
+
file: ctx.file
|
|
300
|
+
});
|
|
283
301
|
}
|
|
284
302
|
if (!/^[\w$.\s+\-*/%()<>=!&|]*$/.test(expr)) {
|
|
285
|
-
throw
|
|
286
|
-
`Unsupported syntax in :computed expression "${raw}" (${ctx.file}). Allowed: state names, numbers, strings, + - * / % ( ), comparisons, && ||
|
|
303
|
+
throw wdError(
|
|
304
|
+
`Unsupported syntax in :computed expression "${raw}" (${ctx.file}). Allowed: state names, numbers, strings, + - * / % ( ), comparisons, && || !.`,
|
|
305
|
+
{ code: "WD230", file: ctx.file }
|
|
287
306
|
);
|
|
288
307
|
}
|
|
289
308
|
if (/(^|[^=!<>])=(?!=)/.test(expr)) {
|
|
290
|
-
throw
|
|
309
|
+
throw wdError(`Assignment is not allowed in :computed expressions ("${raw}" in ${ctx.file})`, {
|
|
310
|
+
code: "WD231",
|
|
311
|
+
file: ctx.file
|
|
312
|
+
});
|
|
291
313
|
}
|
|
292
314
|
// Reject function-call syntax: a `(` that directly follows an identifier, a
|
|
293
315
|
// string literal, or a closing `)` (e.g. `x()`, `x.valueOf()`, `(a)(b)`). Only
|
|
@@ -297,8 +319,9 @@ export function compileComputedExpr(raw, ctx) {
|
|
|
297
319
|
// SECURITY.md guarantee that function calls are compile errors. Runs BEFORE the
|
|
298
320
|
// identifier→S() mapping so it never trips on the emitted helper calls.
|
|
299
321
|
if (/[\w$)]\s*\(/.test(expr)) {
|
|
300
|
-
throw
|
|
301
|
-
`Function calls are not allowed in :computed expressions ("${raw}" in ${ctx.file})
|
|
322
|
+
throw wdError(
|
|
323
|
+
`Function calls are not allowed in :computed expressions ("${raw}" in ${ctx.file})`,
|
|
324
|
+
{ code: "WD232", file: ctx.file }
|
|
302
325
|
);
|
|
303
326
|
}
|
|
304
327
|
expr = expr.replace(/[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*/g, (ref) => {
|
|
@@ -306,14 +329,16 @@ export function compileComputedExpr(raw, ctx) {
|
|
|
306
329
|
if (["true", "false", "null"].includes(ref)) return ref;
|
|
307
330
|
const segs = ref.split(".");
|
|
308
331
|
if (segs.some((seg) => ["constructor", "prototype", "__proto__"].includes(seg))) {
|
|
309
|
-
throw
|
|
310
|
-
|
|
311
|
-
|
|
332
|
+
throw wdError(`Path segment "${ref}" is not allowed in :computed expressions (${ctx.file})`, {
|
|
333
|
+
code: "WD233",
|
|
334
|
+
file: ctx.file
|
|
335
|
+
});
|
|
312
336
|
}
|
|
313
337
|
const key = resolveStateKey(segs[0], ctx);
|
|
314
338
|
if (!key) {
|
|
315
|
-
throw
|
|
316
|
-
`:computed references unknown state "${segs[0]}" in ${ctx.file}. Declare it with :state or :fetch first
|
|
339
|
+
throw wdError(
|
|
340
|
+
`:computed references unknown state "${segs[0]}" in ${ctx.file}. Declare it with :state or :fetch first.`,
|
|
341
|
+
{ code: "WD234", file: ctx.file }
|
|
317
342
|
);
|
|
318
343
|
}
|
|
319
344
|
const rest = segs.slice(1).join(".");
|
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");
|