@zvndev/darkmown 2.2.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 +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 +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/state.js
CHANGED
|
@@ -34,6 +34,7 @@ export function handleState(line, ctx, index) {
|
|
|
34
34
|
const match = line.match(/^:state\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)(\s+persist)?$/);
|
|
35
35
|
if (!match)
|
|
36
36
|
throw wdError(`Malformed :state in ${at(ctx, index)}: ${line}. ${STATE_USE}`, {
|
|
37
|
+
code: "WD201",
|
|
37
38
|
file: ctx.file,
|
|
38
39
|
line: lineOf(ctx, index),
|
|
39
40
|
hint: STATE_USE.slice("Use: ".length),
|
|
@@ -54,11 +55,15 @@ export function handleState(line, ctx, index) {
|
|
|
54
55
|
*/
|
|
55
56
|
export function declareState(name, value, ctx) {
|
|
56
57
|
if (ctx.loopItem)
|
|
57
|
-
throw
|
|
58
|
+
throw wdError(`State cannot be declared inside a reactive @loop body (${ctx.file})`, {
|
|
59
|
+
code: "WD202",
|
|
60
|
+
file: ctx.file
|
|
61
|
+
});
|
|
58
62
|
if (ctx.comp.stores.has(name))
|
|
59
63
|
throw wdError(
|
|
60
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}`,
|
|
61
65
|
{
|
|
66
|
+
code: "WD203",
|
|
62
67
|
file: ctx.file,
|
|
63
68
|
hint: `:store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
|
|
64
69
|
example: STORE_EXAMPLE
|
|
@@ -66,7 +71,10 @@ export function declareState(name, value, ctx) {
|
|
|
66
71
|
);
|
|
67
72
|
const key = ctx.sections.length ? `${ctx.sections.at(-1)}:${name}` : name;
|
|
68
73
|
if (ctx.comp.state.has(key))
|
|
69
|
-
throw
|
|
74
|
+
throw wdError(`State "${name}" is declared twice in the same scope (${ctx.file})`, {
|
|
75
|
+
code: "WD204",
|
|
76
|
+
file: ctx.file
|
|
77
|
+
});
|
|
70
78
|
ctx.comp.state.set(key, value);
|
|
71
79
|
ctx.comp.assets.runtime = true;
|
|
72
80
|
return key;
|
|
@@ -84,6 +92,7 @@ export function handleStore(line, ctx, index) {
|
|
|
84
92
|
throw wdError(
|
|
85
93
|
`Malformed :store in ${at(ctx, index)}: ${line}. Use: :store name = value [ephemeral] — e.g. ${STORE_EXAMPLE}`,
|
|
86
94
|
{
|
|
95
|
+
code: "WD205",
|
|
87
96
|
file: ctx.file,
|
|
88
97
|
line: lineOf(ctx, index),
|
|
89
98
|
hint: `:store name = value [ephemeral] — e.g. ${STORE_EXAMPLE}`,
|
|
@@ -110,6 +119,7 @@ export function declareStore(name, value, ctx) {
|
|
|
110
119
|
throw wdError(
|
|
111
120
|
`Store "${name}" is declared twice in ${ctx.file}. Use: :store name = value — e.g. ${STORE_EXAMPLE}`,
|
|
112
121
|
{
|
|
122
|
+
code: "WD206",
|
|
113
123
|
file: ctx.file,
|
|
114
124
|
hint: `:store name = value — e.g. ${STORE_EXAMPLE}`,
|
|
115
125
|
example: STORE_EXAMPLE
|
|
@@ -119,6 +129,7 @@ export function declareStore(name, value, ctx) {
|
|
|
119
129
|
throw wdError(
|
|
120
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}`,
|
|
121
131
|
{
|
|
132
|
+
code: "WD207",
|
|
122
133
|
file: ctx.file,
|
|
123
134
|
hint: `:store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
|
|
124
135
|
example: STORE_EXAMPLE
|
|
@@ -152,6 +163,7 @@ export function handleComputed(line, ctx, index) {
|
|
|
152
163
|
const match = line.match(/^:computed\s+([A-Za-z_$][\w$]*)\s*=\s*(.+)$/);
|
|
153
164
|
if (!match)
|
|
154
165
|
throw wdError(`Malformed :computed in ${at(ctx, index)}: ${line}. Use: ${COMPUTED_EXAMPLE}`, {
|
|
166
|
+
code: "WD208",
|
|
155
167
|
file: ctx.file,
|
|
156
168
|
line: lineOf(ctx, index),
|
|
157
169
|
hint: COMPUTED_EXAMPLE,
|
|
@@ -169,6 +181,7 @@ export function handleComputed(line, ctx, index) {
|
|
|
169
181
|
throw wdError(
|
|
170
182
|
`Malformed :computed expression in ${at(ctx, index)}: ${line}. Use: ${COMPUTED_EXAMPLE}`,
|
|
171
183
|
{
|
|
184
|
+
code: "WD209",
|
|
172
185
|
file: ctx.file,
|
|
173
186
|
line: lineOf(ctx, index),
|
|
174
187
|
hint: COMPUTED_EXAMPLE,
|
|
@@ -203,6 +216,7 @@ export function handleTheme(line, ctx, index) {
|
|
|
203
216
|
throw wdError(
|
|
204
217
|
`Malformed :theme in ${at(ctx, index)}: ${line}. Use: :theme (or :theme name = "auto") — e.g. ${THEME_EXAMPLE}`,
|
|
205
218
|
{
|
|
219
|
+
code: "WD210",
|
|
206
220
|
file: ctx.file,
|
|
207
221
|
line: lineOf(ctx, index),
|
|
208
222
|
hint: `:theme (or :theme name = "auto") — e.g. ${THEME_EXAMPLE}`,
|
|
@@ -53,6 +53,7 @@ export function handleInclude(line, ctx, index) {
|
|
|
53
53
|
const match = line.match(/^@include\s+(\S+)(?:\s+with\s+(.+))?$/);
|
|
54
54
|
if (!match)
|
|
55
55
|
throw wdError(`Malformed @include in ${at(ctx, index)}: ${line}. ${INCLUDE_USE}`, {
|
|
56
|
+
code: "WD603",
|
|
56
57
|
file: ctx.file,
|
|
57
58
|
line: lineOf(ctx, index),
|
|
58
59
|
hint: INCLUDE_USE.slice("Use: ".length),
|
|
@@ -107,8 +108,9 @@ function parseIncludeArgs(raw, ctx) {
|
|
|
107
108
|
const expr = value.slice(1, -1).trim();
|
|
108
109
|
const resolved = lookupPath(expr, ctx);
|
|
109
110
|
if (!resolved.found) {
|
|
110
|
-
throw
|
|
111
|
-
`@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 }
|
|
112
114
|
);
|
|
113
115
|
}
|
|
114
116
|
args[match[1]] = resolved.value;
|
|
@@ -165,8 +167,9 @@ export function handleContainer(header, bodyLines, ctx, index) {
|
|
|
165
167
|
}
|
|
166
168
|
const cm = rest.match(/^\.([A-Za-z_][\w-]*)/);
|
|
167
169
|
if (!cm)
|
|
168
|
-
throw
|
|
169
|
-
`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) }
|
|
170
173
|
);
|
|
171
174
|
const cls = cm[1];
|
|
172
175
|
rest = rest.slice(cm[0].length).trim();
|
|
@@ -231,6 +234,7 @@ export function handleCarousel(line, bodyLines, ctx, index) {
|
|
|
231
234
|
if (!auto) {
|
|
232
235
|
const hint = `:carousel [autoplay=3000] … :endcarousel — e.g. ${CAROUSEL_EXAMPLE}`;
|
|
233
236
|
throw wdError(`Malformed :carousel in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
237
|
+
code: "WD606",
|
|
234
238
|
file: ctx.file,
|
|
235
239
|
line: lineOf(ctx, index),
|
|
236
240
|
hint,
|
|
@@ -295,8 +299,9 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
|
|
|
295
299
|
// Per-row meta vars in :if — only valid inside a loop.
|
|
296
300
|
if (LOOP_META[head]) {
|
|
297
301
|
if (!ctx.loopMeta)
|
|
298
|
-
throw
|
|
299
|
-
`":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 }
|
|
300
305
|
);
|
|
301
306
|
if (ctx.loopItem) {
|
|
302
307
|
const truthy = ctx.compileBody(truthyLines, truthyCtx).trim();
|
|
@@ -322,8 +327,9 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
|
|
|
322
327
|
|
|
323
328
|
const key = resolveStateKey(head, ctx);
|
|
324
329
|
if (!key) {
|
|
325
|
-
throw
|
|
326
|
-
`: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 }
|
|
327
333
|
);
|
|
328
334
|
}
|
|
329
335
|
ctx.comp.assets.runtime = true;
|
|
@@ -344,6 +350,7 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
|
|
|
344
350
|
if (!condition) {
|
|
345
351
|
const hint = `":if name" or ":if a <op> b [and|or|not …]" — e.g. ${IF_EXAMPLE}`;
|
|
346
352
|
throw wdError(`Malformed :if in ${at(ctx, index)}: ${line}. Use ${hint}`, {
|
|
353
|
+
code: "WD609",
|
|
347
354
|
file: ctx.file,
|
|
348
355
|
line: lineOf(ctx, index),
|
|
349
356
|
hint,
|
|
@@ -403,13 +410,15 @@ function validateDemoHref(href, ctx) {
|
|
|
403
410
|
const value = href.trim();
|
|
404
411
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — rejects control characters in URLs/hrefs.
|
|
405
412
|
if (value !== href || /[\u0000-\u001F\u007F]/.test(value)) {
|
|
406
|
-
throw
|
|
407
|
-
`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 }
|
|
408
416
|
);
|
|
409
417
|
}
|
|
410
418
|
if (value.startsWith("//")) {
|
|
411
|
-
throw
|
|
412
|
-
`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 }
|
|
413
422
|
);
|
|
414
423
|
}
|
|
415
424
|
if (
|
|
@@ -424,7 +433,8 @@ function validateDemoHref(href, ctx) {
|
|
|
424
433
|
if (scheme && ["http", "https", "mailto"].includes(scheme[1].toLowerCase())) {
|
|
425
434
|
return value;
|
|
426
435
|
}
|
|
427
|
-
throw
|
|
428
|
-
`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 }
|
|
429
439
|
);
|
|
430
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);
|