@johnhenry/sitefile 0.0.0-stage → 0.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +475 -2
  4. package/lib/src/archive-writers.d.ts +31 -0
  5. package/lib/src/archive-writers.js +197 -0
  6. package/lib/src/attest.d.ts +126 -0
  7. package/lib/src/attest.js +220 -0
  8. package/lib/src/bake.d.ts +74 -0
  9. package/lib/src/bake.js +372 -0
  10. package/lib/src/base-image.d.ts +76 -0
  11. package/lib/src/base-image.js +138 -0
  12. package/lib/src/build-args.d.ts +26 -0
  13. package/lib/src/build-args.js +164 -0
  14. package/lib/src/cache-limit.d.ts +54 -0
  15. package/lib/src/cache-limit.js +239 -0
  16. package/lib/src/casefold-data.d.ts +2 -0
  17. package/lib/src/casefold-data.js +217 -0
  18. package/lib/src/cbor.d.ts +39 -0
  19. package/lib/src/cbor.js +174 -0
  20. package/lib/src/checklinks.d.ts +42 -0
  21. package/lib/src/checklinks.js +118 -0
  22. package/lib/src/cli/bake.d.ts +2 -0
  23. package/lib/src/cli/bake.js +179 -0
  24. package/lib/src/cli/build-image.d.ts +21 -0
  25. package/lib/src/cli/build-image.js +42 -0
  26. package/lib/src/cli/cache.d.ts +8 -0
  27. package/lib/src/cli/cache.js +109 -0
  28. package/lib/src/cli/common.d.ts +29 -0
  29. package/lib/src/cli/common.js +100 -0
  30. package/lib/src/cli/git-context.d.ts +18 -0
  31. package/lib/src/cli/git-context.js +48 -0
  32. package/lib/src/cli/images.d.ts +6 -0
  33. package/lib/src/cli/images.js +285 -0
  34. package/lib/src/cli/index.d.ts +10 -0
  35. package/lib/src/cli/index.js +95 -0
  36. package/lib/src/cli/outdated.d.ts +5 -0
  37. package/lib/src/cli/outdated.js +94 -0
  38. package/lib/src/cli/registry.d.ts +2 -0
  39. package/lib/src/cli/registry.js +122 -0
  40. package/lib/src/cli/serve-worker.d.ts +2 -0
  41. package/lib/src/cli/serve-worker.js +42 -0
  42. package/lib/src/cli/servers.d.ts +15 -0
  43. package/lib/src/cli/servers.js +451 -0
  44. package/lib/src/cli/vendor.d.ts +2 -0
  45. package/lib/src/cli/vendor.js +92 -0
  46. package/lib/src/cli/watch.d.ts +26 -0
  47. package/lib/src/cli/watch.js +81 -0
  48. package/lib/src/cli.d.ts +12 -0
  49. package/lib/src/cli.js +486 -0
  50. package/lib/src/csv.d.ts +27 -0
  51. package/lib/src/csv.js +117 -0
  52. package/lib/src/deflate.d.ts +76 -0
  53. package/lib/src/deflate.js +604 -0
  54. package/lib/src/digest.d.ts +3 -0
  55. package/lib/src/digest.js +7 -0
  56. package/lib/src/errors.d.ts +34 -0
  57. package/lib/src/errors.js +20 -0
  58. package/lib/src/executor.d.ts +386 -0
  59. package/lib/src/executor.js +2193 -0
  60. package/lib/src/explain.d.ts +64 -0
  61. package/lib/src/explain.js +445 -0
  62. package/lib/src/export-formats.d.ts +16 -0
  63. package/lib/src/export-formats.js +52 -0
  64. package/lib/src/exporter.d.ts +103 -0
  65. package/lib/src/exporter.js +676 -0
  66. package/lib/src/fingerprint.d.ts +40 -0
  67. package/lib/src/fingerprint.js +168 -0
  68. package/lib/src/frontmatter.d.ts +22 -0
  69. package/lib/src/frontmatter.js +45 -0
  70. package/lib/src/index.d.ts +12 -0
  71. package/lib/src/index.js +12 -0
  72. package/lib/src/language/ast.d.ts +353 -0
  73. package/lib/src/language/ast.js +57 -0
  74. package/lib/src/language/parser.d.ts +3 -0
  75. package/lib/src/language/parser.js +612 -0
  76. package/lib/src/language/scanner.d.ts +110 -0
  77. package/lib/src/language/scanner.js +449 -0
  78. package/lib/src/language/validate.d.ts +13 -0
  79. package/lib/src/language/validate.js +367 -0
  80. package/lib/src/links.d.ts +79 -0
  81. package/lib/src/links.js +583 -0
  82. package/lib/src/lint.d.ts +10 -0
  83. package/lib/src/lint.js +223 -0
  84. package/lib/src/markdown.d.ts +29 -0
  85. package/lib/src/markdown.js +612 -0
  86. package/lib/src/media.d.ts +82 -0
  87. package/lib/src/media.js +650 -0
  88. package/lib/src/memo.d.ts +57 -0
  89. package/lib/src/memo.js +93 -0
  90. package/lib/src/oci.d.ts +178 -0
  91. package/lib/src/oci.js +380 -0
  92. package/lib/src/outdated.d.ts +46 -0
  93. package/lib/src/outdated.js +195 -0
  94. package/lib/src/pages.d.ts +10 -0
  95. package/lib/src/pages.js +54 -0
  96. package/lib/src/paths.d.ts +39 -0
  97. package/lib/src/paths.js +101 -0
  98. package/lib/src/planner.d.ts +297 -0
  99. package/lib/src/planner.js +410 -0
  100. package/lib/src/plugins/cache.d.ts +70 -0
  101. package/lib/src/plugins/cache.js +226 -0
  102. package/lib/src/plugins/exec.d.ts +51 -0
  103. package/lib/src/plugins/exec.js +144 -0
  104. package/lib/src/plugins/host.d.ts +75 -0
  105. package/lib/src/plugins/host.js +277 -0
  106. package/lib/src/plugins/wasm.d.ts +23 -0
  107. package/lib/src/plugins/wasm.js +249 -0
  108. package/lib/src/profile.d.ts +42 -0
  109. package/lib/src/profile.js +68 -0
  110. package/lib/src/ref.d.ts +40 -0
  111. package/lib/src/ref.js +79 -0
  112. package/lib/src/registry-auth.d.ts +32 -0
  113. package/lib/src/registry-auth.js +151 -0
  114. package/lib/src/registry-cache.d.ts +71 -0
  115. package/lib/src/registry-cache.js +333 -0
  116. package/lib/src/registry-ops.d.ts +51 -0
  117. package/lib/src/registry-ops.js +248 -0
  118. package/lib/src/registry.d.ts +109 -0
  119. package/lib/src/registry.js +505 -0
  120. package/lib/src/render.d.ts +134 -0
  121. package/lib/src/render.js +576 -0
  122. package/lib/src/resources/archive.d.ts +11 -0
  123. package/lib/src/resources/archive.js +306 -0
  124. package/lib/src/resources/bundle-data.d.ts +16 -0
  125. package/lib/src/resources/bundle-data.js +50 -0
  126. package/lib/src/resources/convert.d.ts +12 -0
  127. package/lib/src/resources/convert.js +63 -0
  128. package/lib/src/resources/documents.d.ts +35 -0
  129. package/lib/src/resources/documents.js +122 -0
  130. package/lib/src/resources/git.d.ts +103 -0
  131. package/lib/src/resources/git.js +720 -0
  132. package/lib/src/resources/https.d.ts +40 -0
  133. package/lib/src/resources/https.js +256 -0
  134. package/lib/src/resources/ignore.d.ts +33 -0
  135. package/lib/src/resources/ignore.js +119 -0
  136. package/lib/src/resources/local.d.ts +64 -0
  137. package/lib/src/resources/local.js +248 -0
  138. package/lib/src/resources/lockfile.d.ts +52 -0
  139. package/lib/src/resources/lockfile.js +172 -0
  140. package/lib/src/resources/netpolicy.d.ts +6 -0
  141. package/lib/src/resources/netpolicy.js +82 -0
  142. package/lib/src/resources/types.d.ts +63 -0
  143. package/lib/src/resources/types.js +13 -0
  144. package/lib/src/resources/vendor.d.ts +94 -0
  145. package/lib/src/resources/vendor.js +296 -0
  146. package/lib/src/resources/wbn-reader.d.ts +44 -0
  147. package/lib/src/resources/wbn-reader.js +302 -0
  148. package/lib/src/schema.d.ts +25 -0
  149. package/lib/src/schema.js +169 -0
  150. package/lib/src/secrets.d.ts +21 -0
  151. package/lib/src/secrets.js +103 -0
  152. package/lib/src/serve-rules.d.ts +29 -0
  153. package/lib/src/serve-rules.js +122 -0
  154. package/lib/src/server.d.ts +45 -0
  155. package/lib/src/server.js +246 -0
  156. package/lib/src/servers.d.ts +71 -0
  157. package/lib/src/servers.js +139 -0
  158. package/lib/src/site.d.ts +60 -0
  159. package/lib/src/site.js +169 -0
  160. package/lib/src/store.d.ts +151 -0
  161. package/lib/src/store.js +596 -0
  162. package/lib/src/tree.d.ts +50 -0
  163. package/lib/src/tree.js +134 -0
  164. package/lib/src/values.d.ts +7 -0
  165. package/lib/src/values.js +37 -0
  166. package/lib/src/vendor.d.ts +19 -0
  167. package/lib/src/vendor.js +145 -0
  168. package/lib/src/watch.d.ts +44 -0
  169. package/lib/src/watch.js +137 -0
  170. package/lib/src/wbn.d.ts +5 -0
  171. package/lib/src/wbn.js +147 -0
  172. package/lib/src/yaml.d.ts +34 -0
  173. package/lib/src/yaml.js +562 -0
  174. package/package.json +58 -4
@@ -0,0 +1,576 @@
1
+ import { SitefileError } from './errors.js';
2
+ import { FILE_HELPERS, renderFile, VALUE_HELPERS } from './media.js';
3
+ const REF = String.raw `[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_-]+)*`;
4
+ const VAR_RE = new RegExp(String.raw `^(${REF})(?:\|(raw))?$`);
5
+ const EACH_RE = new RegExp(String.raw `^#each\s+(${REF})\s+as\s+([A-Za-z_][A-Za-z0-9_]*)\s*$`);
6
+ const IF_RE = new RegExp(String.raw `^#if\s+(!?)\s*(${REF})\s*$`);
7
+ const BLOCK_RE = /^(#each|#if|else|\/each|\/if)(?![A-Za-z0-9_.])/;
8
+ /**
9
+ * Parse a RENDER template.
10
+ *
11
+ * - `${name}` / `${name.seg.seg}` substitutes a value (segments may be array
12
+ * indexes); `${name|raw}` substitutes it without escaping.
13
+ * - `${#each list as item}…${else}…${/each}` repeats for each element of a list
14
+ * (the `${else}` part renders when the list is empty). A missing key such as
15
+ * `post.tags` counts as an empty list; an unknown top-level name is an error.
16
+ * - `${#if x}…${else}…${/if}` / `${#if !x}`: false, null, missing, '', 0 and
17
+ * empty lists are falsy.
18
+ * - A block tag alone on its line removes that whole line from the output.
19
+ * - `$${` writes a literal `${`; a `$` not followed by `{` is literal.
20
+ * - Anything else inside `${…}`, or unbalanced blocks, fails with E_TEMPLATE.
21
+ */
22
+ export function parseRenderTemplate(text, file, firstLine = 1, firstColumn = 1) {
23
+ const tokens = [];
24
+ let buf = '';
25
+ // firstLine > 1 when the template is a heredoc body inside the Sitefile; firstColumn > 1 when it
26
+ // starts inside a Sitefile line (`LET name: html = …`).
27
+ let line = firstLine;
28
+ let lineStart = 0;
29
+ let i = 0;
30
+ const spanAt = (idx) => ({ file, line, column: idx - lineStart + 1 + (line === firstLine ? firstColumn - 1 : 0) });
31
+ const flush = () => {
32
+ if (buf !== '')
33
+ tokens.push({ kind: 'text', value: buf });
34
+ buf = '';
35
+ };
36
+ while (i < text.length) {
37
+ const ch = text[i];
38
+ if (ch === '\n') {
39
+ buf += ch;
40
+ i++;
41
+ line++;
42
+ lineStart = i;
43
+ continue;
44
+ }
45
+ if (ch !== '$') {
46
+ buf += ch;
47
+ i++;
48
+ continue;
49
+ }
50
+ if (text[i + 1] === '$' && text[i + 2] === '{') {
51
+ buf += '${';
52
+ i += 3;
53
+ continue;
54
+ }
55
+ if (text[i + 1] !== '{') {
56
+ buf += '$';
57
+ i++;
58
+ continue;
59
+ }
60
+ const span = spanAt(i);
61
+ if (/^file[ \t]/.test(text.slice(i + 2, i + 7))) {
62
+ // `${file …}`: strings in it may hold `}`, so it is read by its own scanner.
63
+ const r = parseFileExpr(text, i + 2, spanAt, span);
64
+ flush();
65
+ tokens.push(r.node);
66
+ i = r.end;
67
+ continue;
68
+ }
69
+ const close = text.indexOf('}', i + 2);
70
+ const nl = text.indexOf('\n', i + 2);
71
+ if (close === -1 || (nl !== -1 && nl < close)) {
72
+ throw new SitefileError('E_TEMPLATE', "unterminated '${' in template", span);
73
+ }
74
+ const expr = text.slice(i + 2, close);
75
+ if (BLOCK_RE.test(expr)) {
76
+ tokens.push(blockToken(expr, span));
77
+ // A block tag alone on its line: drop the line's indentation and newline.
78
+ const before = text.slice(lineStart, i);
79
+ const lineEnd = nl === -1 ? text.length : nl;
80
+ if (/^[ \t]*$/.test(before) && /^[ \t\r]*$/.test(text.slice(close + 1, lineEnd))) {
81
+ if (buf.endsWith(before))
82
+ buf = buf.slice(0, buf.length - before.length);
83
+ const last = tokens.pop();
84
+ flush();
85
+ tokens.push(last);
86
+ i = lineEnd;
87
+ if (i < text.length) {
88
+ i++; // the newline
89
+ line++;
90
+ lineStart = i;
91
+ }
92
+ continue;
93
+ }
94
+ const last = tokens.pop();
95
+ flush();
96
+ tokens.push(last);
97
+ i = close + 1;
98
+ continue;
99
+ }
100
+ const m = VAR_RE.exec(expr);
101
+ if (!m) {
102
+ throw new SitefileError('E_TEMPLATE', `invalid template expression '\${${expr}}' (expected \${name}, \${name.key}, \${name|raw} or a block like \${#each list as item}; write $\${ for a literal '\${')`, span);
103
+ }
104
+ flush();
105
+ const [name, ...path] = m[1].split('.');
106
+ tokens.push({ kind: 'var', name: name, path, span, ...(m[2] ? { raw: true } : {}) });
107
+ i = close + 1;
108
+ }
109
+ flush();
110
+ return buildTree(tokens, file, { file, line, column: text.length - lineStart + 1 });
111
+ }
112
+ const REF_AT = new RegExp(String.raw `^${REF}`);
113
+ /** The code point at `i`, for messages (whole, also when it is outside the BMP). */
114
+ function charAt(text, i) {
115
+ return String.fromCodePoint(text.codePointAt(i));
116
+ }
117
+ /**
118
+ * `${file <value> [| <helper> [<name>[=<value>]]…]}` from `start` (after `${`). A value is a
119
+ * "double-quoted" string (`\` escapes, `${name}` references), a 'single-quoted' one (literal), or a
120
+ * bare word (escapes and references, up to whitespace, `|` or `}`). Errors point at the `${`.
121
+ */
122
+ function parseFileExpr(text, start, spanAt, span) {
123
+ const fail = (detail) => new SitefileError('E_TEMPLATE', detail, span);
124
+ const unterminated = () => fail("unterminated '${' in template");
125
+ let i = start + 4;
126
+ const ws = () => {
127
+ while (text[i] === ' ' || text[i] === '\t')
128
+ i++;
129
+ };
130
+ const atEnd = () => i >= text.length || text[i] === '\n';
131
+ const value = () => {
132
+ const parts = [];
133
+ const add = (s) => {
134
+ const last = parts[parts.length - 1];
135
+ if (last?.kind === 'text')
136
+ parts[parts.length - 1] = { kind: 'text', value: last.value + s };
137
+ else
138
+ parts.push({ kind: 'text', value: s });
139
+ };
140
+ if (text[i] === "'") {
141
+ const close = text.indexOf("'", i + 1);
142
+ const nl = text.indexOf('\n', i + 1);
143
+ if (close === -1 || (nl !== -1 && nl < close))
144
+ throw fail('unterminated string in ${file …}');
145
+ add(text.slice(i + 1, close));
146
+ i = close + 1;
147
+ return parts.length ? parts : [{ kind: 'text', value: '' }];
148
+ }
149
+ const quoted = text[i] === '"';
150
+ if (quoted)
151
+ i++;
152
+ for (;;) {
153
+ if (atEnd()) {
154
+ if (quoted)
155
+ throw fail('unterminated string in ${file …}');
156
+ break;
157
+ }
158
+ const ch = text[i];
159
+ if (quoted && ch === '"') {
160
+ i++;
161
+ break;
162
+ }
163
+ if (!quoted && (ch === ' ' || ch === '\t' || ch === '|' || ch === '}' || ch === '"' || ch === "'"))
164
+ break;
165
+ if (ch === '\\') {
166
+ if (i + 1 >= text.length || text[i + 1] === '\n')
167
+ throw quoted ? fail('unterminated string in ${file …}') : unterminated();
168
+ const c = charAt(text, i + 1);
169
+ add(c);
170
+ i += 1 + c.length;
171
+ continue;
172
+ }
173
+ if (ch === '$' && text[i + 1] === '{') {
174
+ const m = REF_AT.exec(text.slice(i + 2));
175
+ if (!m || text[i + 2 + m[0].length] !== '}')
176
+ throw fail('expected ${name} or ${name.key} inside a ${file …} string');
177
+ const [name, ...path] = m[0].split('.');
178
+ parts.push({ kind: 'ref', name: name, path, span: spanAt(i) });
179
+ i += 3 + m[0].length;
180
+ continue;
181
+ }
182
+ add(ch);
183
+ i++;
184
+ }
185
+ return parts.length ? parts : [{ kind: 'text', value: '' }];
186
+ };
187
+ ws();
188
+ if (atEnd() || text[i] === '}' || text[i] === '|') {
189
+ if (atEnd())
190
+ throw unterminated();
191
+ throw fail('expected a path after ${file, as in ${file "/img/a.png"}');
192
+ }
193
+ const path = value();
194
+ ws();
195
+ let helper;
196
+ const attrs = [];
197
+ if (text[i] === '|') {
198
+ i++;
199
+ ws();
200
+ const m = /^[a-z][a-z0-9-]*/.exec(text.slice(i));
201
+ if (!m)
202
+ throw fail(`expected a helper name after '|' (${FILE_HELPERS.join(', ')})`);
203
+ helper = m[0];
204
+ i += helper.length;
205
+ if (!FILE_HELPERS.includes(helper))
206
+ throw fail(`unknown file helper '${helper}' (expected ${FILE_HELPERS.join(', ')})`);
207
+ for (;;) {
208
+ const before = i;
209
+ ws();
210
+ if (atEnd() || text[i] === '}')
211
+ break;
212
+ const n = /^[A-Za-z_:][A-Za-z0-9_:.-]*/.exec(text.slice(i));
213
+ if (i === before || !n)
214
+ throw fail(`expected an attribute (name or name="value") in \${file … | ${helper}}, got '${charAt(text, i)}'`);
215
+ const name = n[0];
216
+ i += name.length;
217
+ let v;
218
+ if (text[i] === '=') {
219
+ i++;
220
+ if (atEnd() || text[i] === ' ' || text[i] === '\t' || text[i] === '}')
221
+ throw fail(`expected a value after '${name}='`);
222
+ v = value();
223
+ }
224
+ if (attrs.some((a) => a.name.toLowerCase() === name.toLowerCase()))
225
+ throw fail(`attribute '${name}' given twice`);
226
+ attrs.push(v === undefined ? { name } : { name, value: v });
227
+ }
228
+ if (attrs.length > 0 && VALUE_HELPERS.includes(helper))
229
+ throw fail(`the ${helper} helper takes no attributes`);
230
+ }
231
+ ws();
232
+ if (atEnd())
233
+ throw unterminated();
234
+ if (text[i] !== '}')
235
+ throw fail(`expected | or } after the path in \${file …}, got '${charAt(text, i)}'`);
236
+ return { node: { kind: 'file', path, ...(helper !== undefined ? { helper } : {}), attrs, span }, end: i + 1 };
237
+ }
238
+ /** Every `${name}` reference of a `${file …}` expression, in order. */
239
+ export function fileRefs(n) {
240
+ return [n.path, ...n.attrs.map((a) => a.value ?? [])].flatMap((t) => t.filter((p) => p.kind === 'ref'));
241
+ }
242
+ /** The names a template reads that its own `${#each}` blocks do not bind, in order. */
243
+ export function freeRefs(nodes) {
244
+ const out = [];
245
+ const walkNodes = (list, bound) => {
246
+ for (const n of list) {
247
+ switch (n.kind) {
248
+ case 'text':
249
+ break;
250
+ case 'var':
251
+ if (!bound.includes(n.name))
252
+ out.push({ name: n.name, path: n.path, span: n.span, use: 'var' });
253
+ break;
254
+ case 'file':
255
+ for (const r of fileRefs(n))
256
+ if (!bound.includes(r.name))
257
+ out.push({ name: r.name, path: r.path, span: r.span, use: 'text' });
258
+ break;
259
+ case 'each':
260
+ if (!bound.includes(n.list.name))
261
+ out.push({ ...n.list, use: 'each' });
262
+ walkNodes(n.body, [...bound, n.as]);
263
+ walkNodes(n.empty, bound);
264
+ break;
265
+ case 'if':
266
+ if (!bound.includes(n.cond.name))
267
+ out.push({ ...n.cond, use: 'if' });
268
+ walkNodes(n.then, bound);
269
+ walkNodes(n.otherwise, bound);
270
+ break;
271
+ }
272
+ }
273
+ };
274
+ walkNodes(nodes, []);
275
+ return out;
276
+ }
277
+ /** E_TYPE for a fragment used where a value is needed. */
278
+ export function fragmentNotValue(name, span) {
279
+ return new SitefileError('E_TYPE', `'${name}' is an html fragment, not a value; insert it with \${${name}}`, span);
280
+ }
281
+ /** An error raised inside fragment `name`, with the place it was used added to the message. */
282
+ function inFragment(e, name, span) {
283
+ if (!(e instanceof SitefileError))
284
+ return e;
285
+ return new SitefileError(e.code, `${e.detail} (in html fragment '${name}' used at ${span.file}:${span.line}:${span.column})`, e.span ?? span, { cause: e });
286
+ }
287
+ function ref(text, span) {
288
+ const [name, ...path] = text.split('.');
289
+ return { name: name, path, span };
290
+ }
291
+ function blockToken(expr, span) {
292
+ const word = BLOCK_RE.exec(expr)[1];
293
+ if (word === '#each') {
294
+ const m = EACH_RE.exec(expr);
295
+ if (!m)
296
+ throw new SitefileError('E_TEMPLATE', `expected \${#each list as item}, got '\${${expr}}'`, span);
297
+ return { kind: 'open-each', list: ref(m[1], span), as: m[2], span };
298
+ }
299
+ if (word === '#if') {
300
+ const m = IF_RE.exec(expr);
301
+ if (!m)
302
+ throw new SitefileError('E_TEMPLATE', `expected \${#if value} or \${#if !value}, got '\${${expr}}'`, span);
303
+ return { kind: 'open-if', cond: ref(m[2], span), negate: m[1] === '!', span };
304
+ }
305
+ if (expr.trim() !== word)
306
+ throw new SitefileError('E_TEMPLATE', `unexpected text in '\${${expr}}'`, span);
307
+ return { kind: word === 'else' ? 'else' : word === '/each' ? 'close-each' : 'close-if', span };
308
+ }
309
+ function buildTree(tokens, file, end) {
310
+ const root = [];
311
+ const stack = [];
312
+ const target = () => {
313
+ const f = stack[stack.length - 1];
314
+ return f ? (f.second ?? f.first) : root;
315
+ };
316
+ for (const t of tokens) {
317
+ switch (t.kind) {
318
+ case 'text':
319
+ case 'var':
320
+ case 'file':
321
+ target().push(t);
322
+ break;
323
+ case 'open-each':
324
+ case 'open-if':
325
+ stack.push({ open: t, first: [], second: undefined });
326
+ break;
327
+ case 'else': {
328
+ const f = stack[stack.length - 1];
329
+ if (!f)
330
+ throw new SitefileError('E_TEMPLATE', '${else} outside of ${#if} or ${#each}', t.span);
331
+ if (f.second)
332
+ throw new SitefileError('E_TEMPLATE', 'a block can have only one ${else}', t.span);
333
+ f.second = [];
334
+ break;
335
+ }
336
+ case 'close-each':
337
+ case 'close-if': {
338
+ const f = stack.pop();
339
+ const want = t.kind === 'close-each' ? 'open-each' : 'open-if';
340
+ if (!f || f.open.kind !== want) {
341
+ throw new SitefileError('E_TEMPLATE', `\${${t.kind === 'close-each' ? '/each' : '/if'}} without a matching opening tag`, t.span);
342
+ }
343
+ const o = f.open;
344
+ const node = o.kind === 'open-each'
345
+ ? { kind: 'each', list: o.list, as: o.as, body: f.first, empty: f.second ?? [], span: o.span }
346
+ : { kind: 'if', cond: o.cond, negate: o.negate, then: f.first, otherwise: f.second ?? [], span: o.span };
347
+ target().push(node);
348
+ break;
349
+ }
350
+ }
351
+ }
352
+ const open = stack.pop();
353
+ if (open) {
354
+ const tag = open.open.kind === 'open-each' ? '${#each}' : '${#if}';
355
+ throw new SitefileError('E_TEMPLATE', `${tag} opened here is never closed (template ends at ${file}:${end.line})`, open.open.span);
356
+ }
357
+ return mergeText(root);
358
+ }
359
+ function mergeText(nodes) {
360
+ const out = [];
361
+ for (const n of nodes) {
362
+ const prev = out[out.length - 1];
363
+ if (n.kind === 'text' && prev?.kind === 'text')
364
+ out[out.length - 1] = { kind: 'text', value: prev.value + n.value };
365
+ else if (n.kind !== 'text' || n.value !== '')
366
+ out.push(n);
367
+ }
368
+ return out;
369
+ }
370
+ /**
371
+ * Render a parsed template. `lookup` resolves scalars; `values` resolves lists
372
+ * and conditions for blocks (needed only if the template has blocks). Loop
373
+ * variables are resolved here and shadow every other name. Escaping applies to
374
+ * substituted values only, never to template text or `|raw` values.
375
+ */
376
+ export function renderTemplate(nodes, lookup, escape, values, ctx = {}) {
377
+ const scope = new Map();
378
+ /** Fragments being rendered, outermost first (a fragment that reaches itself is an error). */
379
+ const using = [];
380
+ const isFragment = (name) => !scope.has(name) && (ctx.isFragment?.(name) ?? false);
381
+ const valueOf = (r, loose) => {
382
+ if (scope.has(r.name))
383
+ return walk(scope.get(r.name), r.path, r.name, r.span, loose);
384
+ if (isFragment(r.name))
385
+ throw fragmentNotValue(r.name, r.span);
386
+ if (!values)
387
+ throw new SitefileError('E_TEMPLATE', 'this template uses blocks, which need list values', r.span);
388
+ return values(r.name, r.path, r.span, loose);
389
+ };
390
+ // A fragment is rendered in place: same escape mode, same loop variables. Its markup is never
391
+ // escaped; the values it substitutes are.
392
+ const fragment = (name, path, span) => {
393
+ if (path.length > 0)
394
+ throw fragmentNotValue(name, span);
395
+ if (using.includes(name)) {
396
+ throw new SitefileError('E_TEMPLATE', `html fragment '${name}' uses itself (${[...using.slice(using.indexOf(name)), name].join(' → ')})`, span);
397
+ }
398
+ using.push(name);
399
+ try {
400
+ return run(ctx.fragment(name));
401
+ }
402
+ catch (e) {
403
+ throw inFragment(e, name, span);
404
+ }
405
+ finally {
406
+ using.pop();
407
+ }
408
+ };
409
+ const run = (list) => {
410
+ let out = '';
411
+ for (const n of list) {
412
+ switch (n.kind) {
413
+ case 'text':
414
+ out += n.value;
415
+ break;
416
+ case 'file': {
417
+ // References inside its strings are values (a fragment is not one).
418
+ const textOf = (t) => t
419
+ .map((p) => {
420
+ if (p.kind === 'text')
421
+ return p.value;
422
+ if (scope.has(p.name))
423
+ return String(resolveJsonPath(scope.get(p.name), p.path, p.name, p.span));
424
+ if (isFragment(p.name))
425
+ throw fragmentNotValue(p.name, p.span);
426
+ return String(lookup(p.name, p.path, p.span));
427
+ })
428
+ .join('');
429
+ if (!ctx.tree)
430
+ throw new SitefileError('E_TEMPLATE', '${file …} reads the tree of a RENDER, which this template has not', n.span);
431
+ const attrs = n.attrs.map((a) => (a.value === undefined ? { name: a.name } : { name: a.name, value: textOf(a.value) }));
432
+ const r = renderFile(textOf(n.path), n.helper, attrs, ctx.tree, { inlineSvg: ctx.inlineSvg ?? false }, n.span);
433
+ out += 'markup' in r ? r.markup : escape === 'html' ? escapeHtml(r.value) : r.value;
434
+ break;
435
+ }
436
+ case 'var': {
437
+ if (isFragment(n.name)) {
438
+ out += fragment(n.name, n.path, n.span);
439
+ break;
440
+ }
441
+ const v = scope.has(n.name)
442
+ ? resolveJsonPath(scope.get(n.name), n.path, n.name, n.span)
443
+ : lookup(n.name, n.path, n.span);
444
+ const s = String(v);
445
+ out += n.raw || escape !== 'html' ? s : escapeHtml(s);
446
+ break;
447
+ }
448
+ case 'each': {
449
+ // A missing key (post.tags on a post without tags) is an empty list;
450
+ // an unknown top-level name is still an error, so typos are caught.
451
+ const found = valueOf(n.list, n.list.path.length > 0);
452
+ const items = found === undefined || found === null ? [] : found;
453
+ if (!Array.isArray(items)) {
454
+ throw new SitefileError('E_TYPE', `${dotted(n.list)} is ${describe(found)}, not a list; \${#each} needs a list`, n.list.span);
455
+ }
456
+ if (items.length === 0) {
457
+ out += run(n.empty);
458
+ break;
459
+ }
460
+ const saved = scope.has(n.as) ? { v: scope.get(n.as) } : undefined;
461
+ for (const item of items) {
462
+ scope.set(n.as, item);
463
+ out += run(n.body);
464
+ }
465
+ if (saved)
466
+ scope.set(n.as, saved.v);
467
+ else
468
+ scope.delete(n.as);
469
+ break;
470
+ }
471
+ case 'if': {
472
+ const truthy = isTruthy(valueOf(n.cond, true));
473
+ out += run(truthy !== n.negate ? n.then : n.otherwise);
474
+ break;
475
+ }
476
+ }
477
+ }
478
+ return out;
479
+ };
480
+ return run(nodes);
481
+ }
482
+ function dotted(r) {
483
+ return `'${[r.name, ...r.path].join('.')}'`;
484
+ }
485
+ export function isTruthy(v) {
486
+ if (Array.isArray(v))
487
+ return v.length > 0;
488
+ return v !== undefined && v !== null && v !== false && v !== '' && v !== 0;
489
+ }
490
+ /**
491
+ * Walk `value` along `path`. Strict mode fails like resolveJsonPath on a
492
+ * missing key; loose mode returns undefined. Any value type may be returned.
493
+ */
494
+ export function walk(value, path, name, span, loose) {
495
+ let cur = value;
496
+ let walked = name;
497
+ for (const seg of path) {
498
+ const parent = walked;
499
+ walked += `.${seg}`;
500
+ if (Array.isArray(cur) && /^(0|[1-9][0-9]*)$/.test(seg) && Number(seg) < cur.length) {
501
+ cur = cur[Number(seg)];
502
+ }
503
+ else if (cur !== null && typeof cur === 'object' && !Array.isArray(cur) && Object.prototype.hasOwnProperty.call(cur, seg)) {
504
+ cur = cur[seg];
505
+ }
506
+ else {
507
+ if (loose)
508
+ return undefined;
509
+ throw new SitefileError('E_UNDEFINED', `'${walked}' is undefined ('${parent}' is ${describe(cur)})`, span);
510
+ }
511
+ }
512
+ return cur;
513
+ }
514
+ const HTML_ESCAPES = {
515
+ '&': '&amp;',
516
+ '<': '&lt;',
517
+ '>': '&gt;',
518
+ '"': '&quot;',
519
+ "'": '&#39;',
520
+ };
521
+ export function escapeHtml(s) {
522
+ return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
523
+ }
524
+ /** Decode strict UTF-8 (E_ENCODING on invalid bytes), stripping a leading BOM. */
525
+ export function decodeUtf8(bytes, what, span) {
526
+ try {
527
+ // ignoreBOM: false (default) strips a leading BOM.
528
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
529
+ }
530
+ catch (e) {
531
+ throw new SitefileError('E_ENCODING', `${what} is not valid UTF-8`, span, { cause: e });
532
+ }
533
+ }
534
+ /**
535
+ * Walk a parsed JSON value along `path` and return the scalar found there.
536
+ * Missing keys/indexes fail with E_UNDEFINED; null, objects and arrays fail
537
+ * with E_TYPE. Messages name the full dotted path.
538
+ */
539
+ export function resolveJsonPath(value, path, name, span) {
540
+ let cur = value;
541
+ let walked = name;
542
+ for (const seg of path) {
543
+ const parent = walked;
544
+ walked += `.${seg}`;
545
+ if (Array.isArray(cur)) {
546
+ if (!/^(0|[1-9][0-9]*)$/.test(seg) || Number(seg) >= cur.length) {
547
+ throw new SitefileError('E_UNDEFINED', `'${walked}' is undefined: '${parent}' is an array of length ${cur.length}`, span);
548
+ }
549
+ cur = cur[Number(seg)];
550
+ }
551
+ else if (cur !== null && typeof cur === 'object') {
552
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
553
+ throw new SitefileError('E_UNDEFINED', `'${walked}' is undefined: '${parent}' has no key '${seg}'`, span);
554
+ }
555
+ cur = cur[seg];
556
+ }
557
+ else {
558
+ throw new SitefileError('E_UNDEFINED', `'${walked}' is undefined: '${parent}' is ${describe(cur)}, not an object or array`, span);
559
+ }
560
+ }
561
+ if (typeof cur === 'string' || typeof cur === 'number' || typeof cur === 'boolean')
562
+ return cur;
563
+ throw new SitefileError('E_TYPE', `'${walked}' is ${describe(cur)}; templates can only substitute strings, numbers and booleans`, span);
564
+ }
565
+ function describe(v) {
566
+ if (v === null)
567
+ return 'null';
568
+ if (Array.isArray(v))
569
+ return 'an array';
570
+ if (typeof v === 'object')
571
+ return 'an object';
572
+ if (v === undefined)
573
+ return 'undefined';
574
+ return `a ${typeof v}`;
575
+ }
576
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1,11 @@
1
+ import { type SourceSpan } from '../errors.js';
2
+ import { Tree } from '../tree.js';
3
+ import { type ArchiveLimits } from './types.js';
4
+ /**
5
+ * Extract a tar, tar.gz/tgz, zip or Web Bundle (b1/b2) archive into a Tree. The format is detected
6
+ * from magic bytes, never from the name. Nothing is inflated beyond the limits,
7
+ * and unsafe entries (absolute or `..` paths, backslashes, NUL, symlinks, hard
8
+ * links, devices) are rejected, not skipped. A single top-level directory is
9
+ * kept as-is.
10
+ */
11
+ export declare function extractArchive(bytes: Uint8Array, origin: string, limits?: ArchiveLimits, span?: SourceSpan): Promise<Tree>;