@zvndev/darkmown 0.19.0 → 1.0.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/AGENTS.md +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +115 -5
- package/docs/spec-alignment.md +16 -3
- package/package.json +22 -4
- package/site/_/base.skin +372 -0
- package/site/_/base.wd +4 -0
- package/site/_/footer.wd +14 -0
- package/site/_/metrics.json +13 -0
- package/site/_/nav.skin +35 -12
- package/site/_/nav.wd +8 -10
- package/site/_/products.json +10 -0
- package/site/pages/app.wd +1 -1
- package/site/pages/carousel/index.js +82 -0
- package/site/pages/carousel/index.skin +185 -0
- package/site/pages/carousel/index.wd +109 -0
- package/site/pages/carousel/slide-1.jpg +0 -0
- package/site/pages/carousel/slide-2.jpg +0 -0
- package/site/pages/carousel/slide-3.jpg +0 -0
- package/site/pages/carousel/slide-4.jpg +0 -0
- package/site/pages/carousel/slide-5.jpg +0 -0
- package/site/pages/compass/index.skin +247 -0
- package/site/pages/compass/index.wd +194 -0
- package/site/pages/docs/index.skin +48 -28
- package/site/pages/docs/index.wd +71 -5
- package/site/pages/folio/-style.skin +375 -0
- package/site/pages/folio/-style.wd +6 -0
- package/site/pages/folio/cart.skin +309 -0
- package/site/pages/folio/cart.wd +201 -0
- package/site/pages/folio/index.skin +24 -0
- package/site/pages/folio/index.wd +167 -0
- package/site/pages/forge/index.skin +676 -0
- package/site/pages/forge/index.wd +330 -0
- package/site/pages/index.skin +135 -144
- package/site/pages/index.wd +17 -5
- package/site/pages/ledger/index.skin +257 -0
- package/site/pages/ledger/index.wd +174 -0
- package/site/pages/media/clip-poster.jpg +0 -0
- package/site/pages/media/clip.mp4 +0 -0
- package/site/pages/media/index.skin +82 -0
- package/site/pages/media/index.wd +84 -0
- package/site/pages/media/tone.mp3 +0 -0
- package/site/pages/pulse/index.skin +434 -0
- package/site/pages/pulse/index.wd +178 -0
- package/site/pages/showcase/index.skin +130 -0
- package/site/pages/showcase/index.wd +168 -0
- package/src/builder.js +20 -14
- package/src/cli.js +216 -77
- package/src/compiler/body.js +457 -0
- package/src/compiler/context.js +160 -0
- package/src/compiler/directives.js +1345 -0
- package/src/compiler/format.js +244 -0
- package/src/compiler/frontmatter.js +117 -0
- package/src/compiler/includes.js +116 -0
- package/src/compiler/index.js +22 -0
- package/src/compiler/interpolation.js +223 -0
- package/src/compiler/loops.js +715 -0
- package/src/compiler/markdown.js +242 -0
- package/src/compiler/page.js +267 -0
- package/src/compiler/predicates.js +314 -0
- package/src/compiler.js +26 -2932
- package/src/dev.js +3 -1
- package/src/headers.js +8 -4
- package/src/index.js +22 -0
- package/src/router.js +3 -1
- package/src/runtime.js +168 -14
- package/src/scaffold.js +158 -131
- package/src/skin.js +11 -2
- package/site/_/og.png +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Interpolation primitives: path/value resolution, state-key lookup, scalar
|
|
3
|
+
// parsing, and HTML escaping. These are the leaf utilities the directive
|
|
4
|
+
// handlers, loops, predicates, and markdown layer all build on — this module
|
|
5
|
+
// imports only the shared typedefs, so it sits near the root of the DAG.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import("./context.js").Scope} Scope
|
|
10
|
+
* @typedef {import("./context.js").Ctx} Ctx
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Walk the scope chain for a top-level variable.
|
|
15
|
+
* @param {Scope} scope
|
|
16
|
+
* @param {string} name
|
|
17
|
+
* @returns {{ found: true, value: unknown } | { found: false }}
|
|
18
|
+
*/
|
|
19
|
+
export function lookupVar(scope, name) {
|
|
20
|
+
/** @type {Scope | null} */
|
|
21
|
+
let current = scope;
|
|
22
|
+
for (; current; current = current.parent) {
|
|
23
|
+
if (name in current.vars) return { found: true, value: current.vars[name] };
|
|
24
|
+
}
|
|
25
|
+
return { found: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a dotted path against the static scope chain.
|
|
30
|
+
* @param {string} expr
|
|
31
|
+
* @param {Ctx} ctx
|
|
32
|
+
* @returns {{ found: true, value: unknown } | { found: false }}
|
|
33
|
+
*/
|
|
34
|
+
export function lookupPath(expr, ctx) {
|
|
35
|
+
const segs = expr.split(".");
|
|
36
|
+
const head = lookupVar(ctx.scope, segs[0]);
|
|
37
|
+
if (!head.found) return { found: false };
|
|
38
|
+
return { found: true, value: getPath(head.value, segs.slice(1)) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Safely read a dotted path off a value, rejecting prototype-pollution segments.
|
|
43
|
+
* @param {unknown} value
|
|
44
|
+
* @param {string[]} segments
|
|
45
|
+
* @returns {unknown}
|
|
46
|
+
*/
|
|
47
|
+
export function getPath(value, segments) {
|
|
48
|
+
/** @type {any} */
|
|
49
|
+
let current = value;
|
|
50
|
+
for (const segment of segments) {
|
|
51
|
+
if (current == null) return undefined;
|
|
52
|
+
if (["constructor", "prototype", "__proto__"].includes(segment)) return undefined;
|
|
53
|
+
current = current[segment];
|
|
54
|
+
}
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate a dotted path shape, rejecting prototype-pollution segments at every
|
|
60
|
+
* level. Shared by actions/loops/fetch. Throws with a corrective `Use:` hint.
|
|
61
|
+
* @param {string} path Dotted path, e.g. `cart.items`.
|
|
62
|
+
* @param {Ctx} ctx
|
|
63
|
+
* @param {string} use Corrective suggestion for the error message.
|
|
64
|
+
* @returns {string[]} The validated segments.
|
|
65
|
+
*/
|
|
66
|
+
export function validatePath(path, ctx, use) {
|
|
67
|
+
const segs = path.split(".");
|
|
68
|
+
if (segs.some((seg) => ["constructor", "prototype", "__proto__"].includes(seg))) {
|
|
69
|
+
throw new Error(`Path segment in "${path}" is not allowed in ${ctx.file}. Use: ${use}`);
|
|
70
|
+
}
|
|
71
|
+
return segs;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a bare state name to its fully-qualified key, walking section scopes.
|
|
76
|
+
* @param {string} name
|
|
77
|
+
* @param {Ctx} ctx
|
|
78
|
+
* @returns {string | null}
|
|
79
|
+
*/
|
|
80
|
+
export function resolveStateKey(name, ctx) {
|
|
81
|
+
for (let i = ctx.sections.length - 1; i >= 0; i--) {
|
|
82
|
+
const key = `${ctx.sections[i]}:${name}`;
|
|
83
|
+
if (ctx.comp.state.has(key)) return key;
|
|
84
|
+
}
|
|
85
|
+
return ctx.comp.state.has(name) ? name : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Render an interpolated value as text. Arrays join with ", " (matching the
|
|
90
|
+
* documented `{ meta.tags }` behavior); a bare object is a mistake — fail
|
|
91
|
+
* loudly with a fix rather than emitting "[object Object]".
|
|
92
|
+
* @param {unknown} value
|
|
93
|
+
* @param {string} expr
|
|
94
|
+
* @param {Ctx} ctx
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export function interpolateLeaf(value, expr, ctx) {
|
|
98
|
+
if (value === null || value === undefined) return "";
|
|
99
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
100
|
+
if (typeof value === "object") {
|
|
101
|
+
throw new Error(
|
|
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.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return String(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Literals
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* JSON-encode a value for an inline `<script>`, escaping `<` to stay HTML-safe.
|
|
114
|
+
* @param {unknown} value
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function safeScriptJson(value) {
|
|
118
|
+
return JSON.stringify(value).replaceAll("<", "\\u003c");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse a single-line `:state`/`:store`/`:theme` value: quoted string, boolean,
|
|
123
|
+
* null, number, or JSON literal — falling back to the bare string. A value that
|
|
124
|
+
* opens a JSON array/object but won't parse throws (almost always a multi-line
|
|
125
|
+
* literal, which these single-line directives can't express).
|
|
126
|
+
* @param {string} raw
|
|
127
|
+
* @param {string} [where] `file:line` for the error message.
|
|
128
|
+
* @returns {unknown}
|
|
129
|
+
*/
|
|
130
|
+
export function parseStateValue(raw, where) {
|
|
131
|
+
const value = raw.trim();
|
|
132
|
+
if (/^["'].*["']$/.test(value)) return stripQuotes(value);
|
|
133
|
+
if (value === "true") return true;
|
|
134
|
+
if (value === "false") return false;
|
|
135
|
+
if (value === "null") return null;
|
|
136
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(value);
|
|
139
|
+
} catch {
|
|
140
|
+
// A value that opens a JSON array/object but won't parse is almost always a
|
|
141
|
+
// multi-line literal — :state/:store/:theme take a single-line value, so the
|
|
142
|
+
// unterminated "[" used to get stored verbatim as the string "[" and silently
|
|
143
|
+
// break the page. Fail loudly with the fix instead. Quote it to keep literal
|
|
144
|
+
// brackets (e.g. `= "[draft]"`).
|
|
145
|
+
if (/^[[{]/.test(value))
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Invalid value ${value}${where ? ` in ${where}` : ""}: this looks like the start of a ` +
|
|
148
|
+
`multi-line array/object, but :state/:store values must fit on one line. ` +
|
|
149
|
+
`Put the whole literal on one line, or quote it for literal text. ` +
|
|
150
|
+
`Use: name = [{"id":1,"label":"…"}]`
|
|
151
|
+
);
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} raw
|
|
158
|
+
* @returns {string | number | boolean}
|
|
159
|
+
*/
|
|
160
|
+
export function parseScalar(raw) {
|
|
161
|
+
const trimmed = raw.trim();
|
|
162
|
+
if (/^["']/.test(trimmed)) return stripQuotes(trimmed);
|
|
163
|
+
if (trimmed === "true") return true;
|
|
164
|
+
if (trimmed === "false") return false;
|
|
165
|
+
if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
|
166
|
+
return trimmed;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {string} [value]
|
|
171
|
+
* @returns {string}
|
|
172
|
+
*/
|
|
173
|
+
export function stripQuotes(value) {
|
|
174
|
+
return value?.replace(/^["']|["']$/g, "") ?? "";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Turn a field name / state key into a human-readable accessible name, e.g.
|
|
179
|
+
* `quest` → "Quest", `user-name` → "User name", `firstName` → "First name".
|
|
180
|
+
* Hyphens, underscores, and camelCase boundaries become spaces; the first letter
|
|
181
|
+
* is capitalized. Used to auto-derive aria-label on generated form inputs.
|
|
182
|
+
* @param {string} name
|
|
183
|
+
* @returns {string}
|
|
184
|
+
*/
|
|
185
|
+
export function humanizeName(name) {
|
|
186
|
+
const words = name
|
|
187
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2") // split camelCase
|
|
188
|
+
.replace(/[-_]+/g, " ") // hyphens/underscores → spaces
|
|
189
|
+
.trim()
|
|
190
|
+
.toLowerCase();
|
|
191
|
+
return words ? words.charAt(0).toUpperCase() + words.slice(1) : name;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Escape a value for safe inclusion in HTML text and attribute contexts.
|
|
196
|
+
* @param {unknown} value
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
export function escapeHtml(value) {
|
|
200
|
+
return String(value)
|
|
201
|
+
.replaceAll("&", "&")
|
|
202
|
+
.replaceAll("<", "<")
|
|
203
|
+
.replaceAll(">", ">")
|
|
204
|
+
.replaceAll('"', """)
|
|
205
|
+
.replaceAll("'", "'");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Reverse {@link escapeHtml}. Used by the loop initial-paint fill to read a
|
|
210
|
+
* `data-wd-fmt` attribute (HTML-escaped JSON) back into pipe stages at build
|
|
211
|
+
* time — the browser does the same decode for the runtime via getAttribute.
|
|
212
|
+
* `&` is decoded last so `&lt;` round-trips to `<`, not `<`.
|
|
213
|
+
* @param {string} value
|
|
214
|
+
* @returns {string}
|
|
215
|
+
*/
|
|
216
|
+
export function unescapeHtml(value) {
|
|
217
|
+
return String(value)
|
|
218
|
+
.replaceAll("<", "<")
|
|
219
|
+
.replaceAll(">", ">")
|
|
220
|
+
.replaceAll(""", '"')
|
|
221
|
+
.replaceAll("'", "'")
|
|
222
|
+
.replaceAll("&", "&");
|
|
223
|
+
}
|