@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,583 @@
1
+ // References between a site's files: a small, dependency-free scanner for the URLs that HTML and CSS
2
+ // files contain, how they resolve to tree paths, and how a reference is rewritten when its target is
3
+ // renamed. Used by FINGERPRINT (docs/LANGUAGE.md, "Fingerprinting"). Ported as rust/src/links.rs: both
4
+ // must find the same references (a differential fixture checks this, rust/tests/fixtures/links/).
5
+ //
6
+ // The scanner is deliberately not a full HTML or CSS parser. It finds:
7
+ // - HTML: the attributes `href` (every element but <base>), `src`, `srcset` and `imagesrcset` (each
8
+ // candidate URL), `poster`, `xlink:href`, and `data` on <object>; the CSS inside <style> elements and
9
+ // `style` attributes. Comments, <!…> and <?…> declarations, end tags, and the contents of <script>,
10
+ // <textarea>, <title> and <xmp> are skipped. Attribute values have `&amp;`, `&lt;`, `&gt;`, `&quot;`,
11
+ // `&apos;` and numeric character references (with their `;`) decoded, and are trimmed of ASCII
12
+ // whitespace; other references stay as written. `<base href>` is not honoured.
13
+ // - CSS: `url(…)` (quoted or not) and the string after `@import`; comments are skipped and CSS escapes
14
+ // in strings and URLs are decoded.
15
+ // All syntax is ASCII, so the TypeScript (UTF-16) and Rust (UTF-8) scanners agree; offsets are each
16
+ // implementation's own string indices, lines count `\n` (1-based).
17
+ const isWs = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f';
18
+ const isAlpha = (c) => c !== undefined && ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
19
+ const isHex = (c) => c !== undefined && ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));
20
+ /** CSS name characters (ASCII letters, digits, `-`, `_`, anything non-ASCII). */
21
+ const isNameChar = (c) => c !== undefined && (isAlpha(c) || (c >= '0' && c <= '9') || c === '-' || c === '_' || c.charCodeAt(0) >= 0x80);
22
+ const lowerAscii = (s) => s.replace(/[A-Z]/g, (c) => c.toLowerCase());
23
+ function trimWs(s) {
24
+ let a = 0;
25
+ let b = s.length;
26
+ while (a < b && isWs(s[a]))
27
+ a++;
28
+ while (b > a && isWs(s[b - 1]))
29
+ b--;
30
+ return [a, b];
31
+ }
32
+ /** A code point from a character reference or CSS escape: 0, surrogates and values above U+10FFFF become U+FFFD. */
33
+ function codePoint(n) {
34
+ return n === 0 || (n >= 0xd800 && n <= 0xdfff) || n > 0x10ffff ? '�' : String.fromCodePoint(n);
35
+ }
36
+ const NAMED = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
37
+ /** Decode the character references this scanner knows (see the top of the file); others stay as written. */
38
+ export function decodeHtml(s) {
39
+ let out = '';
40
+ const map = [];
41
+ let i = 0;
42
+ while (i < s.length) {
43
+ if (s[i] !== '&') {
44
+ map.push(i);
45
+ out += s[i++];
46
+ continue;
47
+ }
48
+ const semi = s.indexOf(';', i + 1);
49
+ const name = semi === -1 ? '' : s.slice(i + 1, semi);
50
+ let decoded;
51
+ if (Object.hasOwn(NAMED, name))
52
+ decoded = NAMED[name];
53
+ else if (/^#[0-9]{1,8}$/.test(name))
54
+ decoded = codePoint(parseInt(name.slice(1), 10));
55
+ else if (/^#[xX][0-9a-fA-F]{1,8}$/.test(name))
56
+ decoded = codePoint(parseInt(name.slice(2), 16));
57
+ if (decoded === undefined) {
58
+ map.push(i);
59
+ out += '&';
60
+ i++;
61
+ }
62
+ else {
63
+ for (let k = 0; k < decoded.length; k++)
64
+ map.push(i);
65
+ out += decoded;
66
+ i = semi + 1;
67
+ }
68
+ }
69
+ map.push(s.length);
70
+ return { text: out, map };
71
+ }
72
+ export const decodeHtmlText = (s) => (s.includes('&') ? decodeHtml(s).text : s);
73
+ /** Decode CSS escapes: `\` + newline is removed, `\` + 1-6 hex digits (and one following whitespace) is a code point, `\` + anything else is that character. */
74
+ export function decodeCss(s) {
75
+ let out = '';
76
+ const map = [];
77
+ let i = 0;
78
+ while (i < s.length) {
79
+ const c = s[i];
80
+ if (c !== '\\') {
81
+ map.push(i);
82
+ out += c;
83
+ i++;
84
+ continue;
85
+ }
86
+ const at = i;
87
+ i++;
88
+ if (i >= s.length)
89
+ break;
90
+ if (s[i] === '\n') {
91
+ i++;
92
+ continue;
93
+ }
94
+ if (isHex(s[i])) {
95
+ let j = i;
96
+ while (j < s.length && j - i < 6 && isHex(s[j]))
97
+ j++;
98
+ const ch = codePoint(parseInt(s.slice(i, j), 16));
99
+ for (let k = 0; k < ch.length; k++)
100
+ map.push(at);
101
+ out += ch;
102
+ i = j;
103
+ if (s[i] === '\r' && s[i + 1] === '\n')
104
+ i += 2;
105
+ else if (isWs(s[i]))
106
+ i++;
107
+ continue;
108
+ }
109
+ // The escaped character, whole (both halves of a surrogate pair).
110
+ const ch = String.fromCodePoint(s.codePointAt(i));
111
+ for (let k = 0; k < ch.length; k++)
112
+ map.push(at);
113
+ out += ch;
114
+ i += ch.length;
115
+ }
116
+ map.push(s.length);
117
+ return { text: out, map };
118
+ }
119
+ export const decodeCssText = (s) => (s.includes('\\') ? decodeCss(s).text : s);
120
+ /** Line numbers by offset. */
121
+ function lineIndex(text) {
122
+ const starts = [];
123
+ for (let i = text.indexOf('\n'); i !== -1; i = text.indexOf('\n', i + 1))
124
+ starts.push(i);
125
+ return (pos) => {
126
+ let lo = 0;
127
+ let hi = starts.length;
128
+ while (lo < hi) {
129
+ const mid = (lo + hi) >> 1;
130
+ if (starts[mid] < pos)
131
+ lo = mid + 1;
132
+ else
133
+ hi = mid;
134
+ }
135
+ return lo + 1;
136
+ };
137
+ }
138
+ function collector(text, out) {
139
+ const lineAt = lineIndex(text);
140
+ return (start, end, url, css) => out.push({ start, end, raw: text.slice(start, end), url, line: lineAt(start), css });
141
+ }
142
+ // ---------------------------------------------------------------- CSS
143
+ /** The end of a CSS string starting at the quote `at`: the index of its closing quote, or undefined (newline or end of input). */
144
+ function cssStringEnd(text, at, limit) {
145
+ const q = text[at];
146
+ let i = at + 1;
147
+ while (i < limit) {
148
+ const c = text[i];
149
+ if (c === '\\')
150
+ i += 2;
151
+ else if (c === q)
152
+ return i;
153
+ else if (c === '\n')
154
+ return undefined;
155
+ else
156
+ i++;
157
+ }
158
+ return undefined;
159
+ }
160
+ /** References in `text[from, to)` read as CSS. */
161
+ function scanCssRange(text, from, to, push) {
162
+ let i = from;
163
+ let afterImport = false;
164
+ while (i < to) {
165
+ const c = text[i];
166
+ if (c === '/' && text[i + 1] === '*' && i + 1 < to) {
167
+ const j = text.indexOf('*/', i + 2);
168
+ i = j === -1 || j + 2 > to ? to : j + 2;
169
+ continue;
170
+ }
171
+ if (c === '"' || c === "'") {
172
+ const end = cssStringEnd(text, i, to);
173
+ if (end === undefined) {
174
+ // An unterminated string ends at the newline (or the end).
175
+ const nl = text.indexOf('\n', i + 1);
176
+ i = nl === -1 || nl >= to ? to : nl;
177
+ afterImport = false;
178
+ continue;
179
+ }
180
+ if (afterImport)
181
+ push(i + 1, end, trimmed(decodeCssText(text.slice(i + 1, end))), true);
182
+ afterImport = false;
183
+ i = end + 1;
184
+ continue;
185
+ }
186
+ if (c === '\\') {
187
+ // The backslash and the whole character it escapes.
188
+ i += 1 + ((text.codePointAt(i + 1) ?? 0) > 0xffff ? 2 : 1);
189
+ continue;
190
+ }
191
+ if (c === '@') {
192
+ let j = i + 1;
193
+ while (j < to && isNameChar(text[j]))
194
+ j++;
195
+ afterImport = lowerAscii(text.slice(i + 1, j)) === 'import';
196
+ i = j;
197
+ continue;
198
+ }
199
+ if (isNameChar(c)) {
200
+ let j = i;
201
+ while (j < to && isNameChar(text[j]))
202
+ j++;
203
+ if (j < to && text[j] === '(' && lowerAscii(text.slice(i, j)) === 'url') {
204
+ i = cssUrl(text, j + 1, to, push);
205
+ afterImport = false;
206
+ continue;
207
+ }
208
+ i = j;
209
+ continue;
210
+ }
211
+ if (c === ';' || c === '{' || c === '}')
212
+ afterImport = false;
213
+ i++;
214
+ }
215
+ }
216
+ function trimmed(s) {
217
+ const [a, b] = trimWs(s);
218
+ return s.slice(a, b);
219
+ }
220
+ /** The contents of `url(` starting at `i` (after the parenthesis); returns where scanning continues. */
221
+ function cssUrl(text, i, to, push) {
222
+ while (i < to && isWs(text[i]))
223
+ i++;
224
+ if (i >= to)
225
+ return to;
226
+ const q = text[i];
227
+ if (q === '"' || q === "'") {
228
+ const end = cssStringEnd(text, i, to);
229
+ if (end === undefined)
230
+ return i + 1;
231
+ let k = end + 1;
232
+ while (k < to && isWs(text[k]))
233
+ k++;
234
+ if (text[k] === ')' && k < to) {
235
+ push(i + 1, end, trimmed(decodeCssText(text.slice(i + 1, end))), true);
236
+ return k + 1;
237
+ }
238
+ return end + 1;
239
+ }
240
+ const start = i;
241
+ while (i < to) {
242
+ const c = text[i];
243
+ if (c === ')' || isWs(c) || c === '"' || c === "'" || c === '(')
244
+ break;
245
+ i += c === '\\' ? 2 : 1;
246
+ }
247
+ if (i > to)
248
+ i = to;
249
+ const end = i;
250
+ while (i < to && isWs(text[i]))
251
+ i++;
252
+ if (i < to && text[i] === ')') {
253
+ if (end > start)
254
+ push(start, end, decodeCssText(text.slice(start, end)), true);
255
+ return i + 1;
256
+ }
257
+ return i;
258
+ }
259
+ /** The URLs a CSS file references, in order. */
260
+ export function scanCss(text) {
261
+ const out = [];
262
+ scanCssRange(text, 0, text.length, collector(text, out));
263
+ return out;
264
+ }
265
+ // ---------------------------------------------------------------- HTML
266
+ /** Elements whose contents are not markup. `<style>` is read as CSS. */
267
+ const RAW_TEXT = new Set(['script', 'style', 'textarea', 'title', 'xmp']);
268
+ /** srcset candidates: the offsets of each URL in `value` (spec "parse a srcset attribute", without descriptors). */
269
+ function srcsetUrls(value) {
270
+ const out = [];
271
+ let i = 0;
272
+ while (i < value.length) {
273
+ while (i < value.length && (isWs(value[i]) || value[i] === ','))
274
+ i++;
275
+ if (i >= value.length)
276
+ break;
277
+ const start = i;
278
+ while (i < value.length && !isWs(value[i]))
279
+ i++;
280
+ let end = i;
281
+ if (value[end - 1] === ',') {
282
+ while (end > start && value[end - 1] === ',')
283
+ end--;
284
+ }
285
+ else {
286
+ let depth = 0;
287
+ while (i < value.length) {
288
+ const c = value[i];
289
+ if (c === '(')
290
+ depth++;
291
+ else if (c === ')' && depth > 0)
292
+ depth--;
293
+ else if (c === ',' && depth === 0)
294
+ break;
295
+ i++;
296
+ }
297
+ i++;
298
+ }
299
+ if (end > start)
300
+ out.push([start, end]);
301
+ }
302
+ return out;
303
+ }
304
+ /** The URLs an HTML file references, in order (including those of its CSS). */
305
+ export function scanHtml(text) {
306
+ const out = [];
307
+ const push = collector(text, out);
308
+ const n = text.length;
309
+ const attribute = (tag, name, start, end) => {
310
+ const value = text.slice(start, end);
311
+ if (name === 'style') {
312
+ scanCssRange(text, start, end, push);
313
+ return;
314
+ }
315
+ if (name === 'srcset' || name === 'imagesrcset') {
316
+ for (const [a, b] of srcsetUrls(value))
317
+ push(start + a, start + b, decodeHtmlText(value.slice(a, b)), false);
318
+ return;
319
+ }
320
+ const url = name === 'src' || name === 'poster' || name === 'xlink:href' || (name === 'href' && tag !== 'base') || (name === 'data' && tag === 'object');
321
+ if (!url)
322
+ return;
323
+ const [a, b] = trimWs(value);
324
+ push(start + a, start + b, trimmed(decodeHtmlText(value.slice(a, b))), false);
325
+ };
326
+ let i = 0;
327
+ while (i < n) {
328
+ const lt = text.indexOf('<', i);
329
+ if (lt === -1)
330
+ break;
331
+ i = lt;
332
+ const next = text[i + 1];
333
+ if (text.startsWith('<!--', i)) {
334
+ const j = text.indexOf('-->', i + 4);
335
+ i = j === -1 ? n : j + 3;
336
+ continue;
337
+ }
338
+ if (next === '!' || next === '?' || next === '/') {
339
+ const j = text.indexOf('>', i + 2);
340
+ i = j === -1 ? n : j + 1;
341
+ continue;
342
+ }
343
+ if (!isAlpha(next)) {
344
+ i++;
345
+ continue;
346
+ }
347
+ // A start tag.
348
+ let j = i + 1;
349
+ while (j < n && !isWs(text[j]) && text[j] !== '/' && text[j] !== '>')
350
+ j++;
351
+ const tag = lowerAscii(text.slice(i + 1, j));
352
+ i = j;
353
+ for (;;) {
354
+ while (i < n && (isWs(text[i]) || text[i] === '/'))
355
+ i++;
356
+ if (i >= n)
357
+ break;
358
+ if (text[i] === '>') {
359
+ i++;
360
+ break;
361
+ }
362
+ const nameStart = i;
363
+ i++; // the first character may be `=`
364
+ while (i < n && !isWs(text[i]) && text[i] !== '/' && text[i] !== '>' && text[i] !== '=')
365
+ i++;
366
+ const name = lowerAscii(text.slice(nameStart, i));
367
+ let k = i;
368
+ while (k < n && isWs(text[k]))
369
+ k++;
370
+ if (text[k] !== '=')
371
+ continue;
372
+ i = k + 1;
373
+ while (i < n && isWs(text[i]))
374
+ i++;
375
+ const q = text[i];
376
+ let start;
377
+ let end;
378
+ if (q === '"' || q === "'") {
379
+ start = i + 1;
380
+ const close = text.indexOf(q, start);
381
+ end = close === -1 ? n : close;
382
+ i = close === -1 ? n : close + 1;
383
+ }
384
+ else {
385
+ start = i;
386
+ while (i < n && !isWs(text[i]) && text[i] !== '>')
387
+ i++;
388
+ end = i;
389
+ }
390
+ attribute(tag, name, start, end);
391
+ }
392
+ if (RAW_TEXT.has(tag)) {
393
+ const close = indexOfCloseTag(text, tag, i);
394
+ if (tag === 'style')
395
+ scanCssRange(text, i, close, push);
396
+ i = close;
397
+ }
398
+ }
399
+ return out;
400
+ }
401
+ /** Where `</tag` (ASCII case-insensitive, followed by whitespace, `/` or `>`) starts at or after `from`; the end of the text if nowhere. */
402
+ function indexOfCloseTag(text, tag, from) {
403
+ let i = from;
404
+ for (;;) {
405
+ const j = text.indexOf('</', i);
406
+ if (j === -1)
407
+ return text.length;
408
+ const after = j + 2 + tag.length;
409
+ if (lowerAscii(text.slice(j + 2, after)) === tag && (after >= text.length || isWs(text[after]) || text[after] === '/' || text[after] === '>'))
410
+ return j;
411
+ i = j + 2;
412
+ }
413
+ }
414
+ /** Whether a path is read by {@link scanFile}, and how. */
415
+ export function referenceKind(path) {
416
+ const lower = lowerAscii(path);
417
+ if (lower.endsWith('.html') || lower.endsWith('.htm'))
418
+ return 'html';
419
+ if (lower.endsWith('.css'))
420
+ return 'css';
421
+ return undefined;
422
+ }
423
+ const UTF8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
424
+ /** A file's text and references; undefined when it is not HTML or CSS, or not valid UTF-8. */
425
+ export function scanFile(path, bytes) {
426
+ const kind = referenceKind(path);
427
+ if (kind === undefined)
428
+ return undefined;
429
+ let text;
430
+ try {
431
+ text = UTF8.decode(bytes);
432
+ }
433
+ catch {
434
+ return undefined;
435
+ }
436
+ return { text, refs: kind === 'html' ? scanHtml(text) : scanCss(text) };
437
+ }
438
+ const SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/;
439
+ /** Percent-decode one path segment; a segment whose decoding is not valid UTF-8 stays as written. */
440
+ export function percentDecode(seg) {
441
+ if (!seg.includes('%'))
442
+ return seg;
443
+ const bytes = [];
444
+ const enc = new TextEncoder();
445
+ for (let i = 0; i < seg.length;) {
446
+ if (seg[i] === '%' && isHex(seg[i + 1]) && isHex(seg[i + 2])) {
447
+ bytes.push(parseInt(seg.slice(i + 1, i + 3), 16));
448
+ i += 3;
449
+ }
450
+ else {
451
+ const cp = seg.codePointAt(i);
452
+ const ch = String.fromCodePoint(cp);
453
+ bytes.push(...enc.encode(ch));
454
+ i += ch.length;
455
+ }
456
+ }
457
+ try {
458
+ return UTF8.decode(new Uint8Array(bytes));
459
+ }
460
+ catch {
461
+ return seg;
462
+ }
463
+ }
464
+ /**
465
+ * Resolve a reference's URL against the path of the file that contains it. `root` is the directory the
466
+ * site is served from (the EXPORT path): an absolute URL path (`/img/a.png`) names a file below it, and
467
+ * `..` never climbs above it (as in a browser), for references in files below it; elsewhere `..` stops at `/`.
468
+ */
469
+ export function resolveReference(url, from, root = '/') {
470
+ if (url === '' || url.startsWith('#') || url.startsWith('?'))
471
+ return { kind: 'none' };
472
+ if (url.startsWith('//'))
473
+ return { kind: 'external', url };
474
+ const scheme = SCHEME.exec(url);
475
+ if (scheme) {
476
+ const s = scheme[0].toLowerCase();
477
+ return s === 'http:' || s === 'https:' ? { kind: 'external', url } : { kind: 'none' };
478
+ }
479
+ const cut = url.search(/[?#]/);
480
+ const raw = cut === -1 ? url : url.slice(0, cut);
481
+ const rootSegs = root === '/' ? [] : root.split('/').slice(1);
482
+ const below = root === '/' || from.startsWith(root + '/');
483
+ const absolute = raw.startsWith('/');
484
+ const segs = absolute ? [...rootSegs] : from.split('/').slice(1, -1);
485
+ const floor = absolute || below ? rootSegs.length : 0;
486
+ const parts = absolute ? raw.split('/').slice(1) : raw.split('/');
487
+ let dir = false;
488
+ for (const [i, part] of parts.entries()) {
489
+ const last = i === parts.length - 1;
490
+ const seg = percentDecode(part);
491
+ if (seg === '' || seg === '.') {
492
+ if (last)
493
+ dir = true;
494
+ }
495
+ else if (seg === '..') {
496
+ if (segs.length > floor)
497
+ segs.pop();
498
+ if (last)
499
+ dir = true;
500
+ }
501
+ else {
502
+ segs.push(seg);
503
+ }
504
+ }
505
+ if (segs.length === 0)
506
+ dir = true;
507
+ const path = '/' + segs.join('/');
508
+ return { kind: 'internal', path: /^[\x00-\x7f]*$/.test(path) ? path : path.normalize('NFC'), dir };
509
+ }
510
+ // ---------------------------------------------------------------- rewriting
511
+ /** Percent-encode a path, keeping `/` and the URL-unreserved characters `A-Z a-z 0-9 - . _ ~`. */
512
+ export function encodePath(p) {
513
+ let out = '';
514
+ for (const b of new TextEncoder().encode(p)) {
515
+ const c = String.fromCharCode(b);
516
+ out += /[A-Za-z0-9\-._~/]/.test(c) ? c : '%' + b.toString(16).toUpperCase().padStart(2, '0');
517
+ }
518
+ return out;
519
+ }
520
+ function basename(p) {
521
+ return p.slice(p.lastIndexOf('/') + 1);
522
+ }
523
+ /** A relative URL from the file `from` to the file `to` (both absolute tree paths), percent-encoded. */
524
+ export function relativeUrl(from, to) {
525
+ const dir = from.split('/').slice(1, -1);
526
+ const target = to.split('/').slice(1);
527
+ let k = 0;
528
+ while (k < dir.length && k < target.length - 1 && dir[k] === target[k])
529
+ k++;
530
+ return '../'.repeat(dir.length - k) + target.slice(k).map(encodePath).join('/');
531
+ }
532
+ /**
533
+ * The reference `raw` (written in `from`, resolved to `oldPath`) pointing at `newPath` instead, where only the
534
+ * file name changed by the insertion of `.<hash>`. The hash is inserted into the name as written, before the
535
+ * dot of its extension (or after the name), so the directories, the query, the fragment and the way it is
536
+ * written (escapes, character references, percent-encoding) are kept. When the name can't be edited that way
537
+ * (its extension's dot is percent-encoded, say), the path becomes a relative URL from `from` to `newPath`.
538
+ */
539
+ export function renameReference(raw, css, from, oldPath, newPath) {
540
+ const { text, map } = css ? decodeCss(raw) : decodeHtml(raw);
541
+ const [t0, t1] = trimWs(text);
542
+ const url = text.slice(t0, t1);
543
+ const cut = url.search(/[?#]/);
544
+ const pathEnd = t0 + (cut === -1 ? url.length : cut);
545
+ const slash = pathEnd > t0 ? text.lastIndexOf('/', pathEnd - 1) : -1;
546
+ const segStart = slash < t0 ? t0 : slash + 1;
547
+ const seg = text.slice(segStart, pathEnd);
548
+ const oldBase = basename(oldPath);
549
+ const newBase = basename(newPath);
550
+ const insertAt = (offset, insert) => raw.slice(0, map[offset]) + insert + raw.slice(map[offset]);
551
+ if (percentDecode(seg) === oldBase) {
552
+ const oldDot = oldBase.lastIndexOf('.');
553
+ if (oldDot > 0) {
554
+ const ext = oldBase.slice(oldDot);
555
+ const d = seg.lastIndexOf('.');
556
+ if (d >= 0 && percentDecode(seg.slice(d)) === ext)
557
+ return insertAt(segStart + d, newBase.slice(oldDot, newBase.length - ext.length));
558
+ }
559
+ else {
560
+ return insertAt(pathEnd, newBase.slice(oldBase.length));
561
+ }
562
+ }
563
+ return raw.slice(0, map[t0]) + relativeUrl(from, newPath) + raw.slice(map[pathEnd]);
564
+ }
565
+ /** `text` with each reference whose target `rename` maps to a new path rewritten; undefined when nothing changed. */
566
+ export function rewriteReferences(text, refs, from, root, rename) {
567
+ let out = '';
568
+ let at = 0;
569
+ let changed = false;
570
+ for (const r of refs) {
571
+ const t = resolveReference(r.url, from, root);
572
+ if (t.kind !== 'internal' || t.dir)
573
+ continue;
574
+ const to = rename(t.path);
575
+ if (to === undefined)
576
+ continue;
577
+ out += text.slice(at, r.start) + renameReference(r.raw, r.css, from, t.path, to);
578
+ at = r.end;
579
+ changed = true;
580
+ }
581
+ return changed ? out + text.slice(at) : undefined;
582
+ }
583
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1,10 @@
1
+ import type { Program } from './language/ast.js';
2
+ import { type Plan } from './planner.js';
3
+ /** Every rule, in the order warnings of one line are reported. */
4
+ export declare const LINT_RULES: readonly ['unused-name', 'unpinned-import', 'unpinned-image', 'render-escape', 'sitemap-order', 'shadowed-write'];
5
+ export type LintRule = (typeof LINT_RULES)[number];
6
+ /**
7
+ * Lint a validated program and its plan. `source` is the Sitefile text (for comments),
8
+ * `disabled` the rule ids turned off by the caller (`--lint-disable`).
9
+ */
10
+ export declare function lint(program: Program, plan: Plan, source: string, disabled?: readonly string[]): string[];