@johnhenry/sitefile 0.0.0-stage → 0.0.1

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 (178) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +21 -0
  3. package/README.md +513 -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 +120 -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 +119 -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/network.d.ts +34 -0
  37. package/lib/src/cli/network.js +56 -0
  38. package/lib/src/cli/outdated.d.ts +5 -0
  39. package/lib/src/cli/outdated.js +99 -0
  40. package/lib/src/cli/registry.d.ts +2 -0
  41. package/lib/src/cli/registry.js +127 -0
  42. package/lib/src/cli/serve-worker.d.ts +2 -0
  43. package/lib/src/cli/serve-worker.js +42 -0
  44. package/lib/src/cli/servers.d.ts +15 -0
  45. package/lib/src/cli/servers.js +467 -0
  46. package/lib/src/cli/vendor.d.ts +2 -0
  47. package/lib/src/cli/vendor.js +97 -0
  48. package/lib/src/cli/watch.d.ts +26 -0
  49. package/lib/src/cli/watch.js +81 -0
  50. package/lib/src/cli.d.ts +12 -0
  51. package/lib/src/cli.js +522 -0
  52. package/lib/src/csv.d.ts +27 -0
  53. package/lib/src/csv.js +117 -0
  54. package/lib/src/deflate.d.ts +76 -0
  55. package/lib/src/deflate.js +604 -0
  56. package/lib/src/digest.d.ts +3 -0
  57. package/lib/src/digest.js +7 -0
  58. package/lib/src/errors.d.ts +56 -0
  59. package/lib/src/errors.js +29 -0
  60. package/lib/src/executor.d.ts +393 -0
  61. package/lib/src/executor.js +2201 -0
  62. package/lib/src/explain.d.ts +64 -0
  63. package/lib/src/explain.js +445 -0
  64. package/lib/src/export-formats.d.ts +23 -0
  65. package/lib/src/export-formats.js +63 -0
  66. package/lib/src/exporter.d.ts +103 -0
  67. package/lib/src/exporter.js +676 -0
  68. package/lib/src/fingerprint.d.ts +40 -0
  69. package/lib/src/fingerprint.js +168 -0
  70. package/lib/src/frontmatter.d.ts +22 -0
  71. package/lib/src/frontmatter.js +45 -0
  72. package/lib/src/index.d.ts +13 -0
  73. package/lib/src/index.js +13 -0
  74. package/lib/src/language/ast.d.ts +360 -0
  75. package/lib/src/language/ast.js +57 -0
  76. package/lib/src/language/parser.d.ts +3 -0
  77. package/lib/src/language/parser.js +612 -0
  78. package/lib/src/language/scanner.d.ts +110 -0
  79. package/lib/src/language/scanner.js +449 -0
  80. package/lib/src/language/validate.d.ts +13 -0
  81. package/lib/src/language/validate.js +367 -0
  82. package/lib/src/links.d.ts +79 -0
  83. package/lib/src/links.js +583 -0
  84. package/lib/src/lint.d.ts +10 -0
  85. package/lib/src/lint.js +223 -0
  86. package/lib/src/markdown.d.ts +29 -0
  87. package/lib/src/markdown.js +612 -0
  88. package/lib/src/media.d.ts +82 -0
  89. package/lib/src/media.js +650 -0
  90. package/lib/src/memo.d.ts +57 -0
  91. package/lib/src/memo.js +93 -0
  92. package/lib/src/oci.d.ts +178 -0
  93. package/lib/src/oci.js +380 -0
  94. package/lib/src/outdated.d.ts +46 -0
  95. package/lib/src/outdated.js +195 -0
  96. package/lib/src/pages.d.ts +10 -0
  97. package/lib/src/pages.js +54 -0
  98. package/lib/src/paths.d.ts +39 -0
  99. package/lib/src/paths.js +101 -0
  100. package/lib/src/planner.d.ts +297 -0
  101. package/lib/src/planner.js +410 -0
  102. package/lib/src/plugins/cache.d.ts +96 -0
  103. package/lib/src/plugins/cache.js +325 -0
  104. package/lib/src/plugins/exec.d.ts +51 -0
  105. package/lib/src/plugins/exec.js +144 -0
  106. package/lib/src/plugins/host.d.ts +75 -0
  107. package/lib/src/plugins/host.js +277 -0
  108. package/lib/src/plugins/wasm.d.ts +23 -0
  109. package/lib/src/plugins/wasm.js +249 -0
  110. package/lib/src/profile.d.ts +42 -0
  111. package/lib/src/profile.js +68 -0
  112. package/lib/src/ref.d.ts +40 -0
  113. package/lib/src/ref.js +79 -0
  114. package/lib/src/registry-auth.d.ts +32 -0
  115. package/lib/src/registry-auth.js +151 -0
  116. package/lib/src/registry-cache.d.ts +71 -0
  117. package/lib/src/registry-cache.js +333 -0
  118. package/lib/src/registry-ops.d.ts +51 -0
  119. package/lib/src/registry-ops.js +248 -0
  120. package/lib/src/registry.d.ts +115 -0
  121. package/lib/src/registry.js +524 -0
  122. package/lib/src/render.d.ts +134 -0
  123. package/lib/src/render.js +576 -0
  124. package/lib/src/resources/archive.d.ts +11 -0
  125. package/lib/src/resources/archive.js +306 -0
  126. package/lib/src/resources/bundle-data.d.ts +16 -0
  127. package/lib/src/resources/bundle-data.js +50 -0
  128. package/lib/src/resources/convert.d.ts +12 -0
  129. package/lib/src/resources/convert.js +63 -0
  130. package/lib/src/resources/documents.d.ts +35 -0
  131. package/lib/src/resources/documents.js +122 -0
  132. package/lib/src/resources/git.d.ts +103 -0
  133. package/lib/src/resources/git.js +720 -0
  134. package/lib/src/resources/https.d.ts +74 -0
  135. package/lib/src/resources/https.js +413 -0
  136. package/lib/src/resources/ignore.d.ts +33 -0
  137. package/lib/src/resources/ignore.js +119 -0
  138. package/lib/src/resources/local.d.ts +64 -0
  139. package/lib/src/resources/local.js +248 -0
  140. package/lib/src/resources/lockfile.d.ts +52 -0
  141. package/lib/src/resources/lockfile.js +172 -0
  142. package/lib/src/resources/netpolicy.d.ts +6 -0
  143. package/lib/src/resources/netpolicy.js +82 -0
  144. package/lib/src/resources/network.d.ts +70 -0
  145. package/lib/src/resources/network.js +304 -0
  146. package/lib/src/resources/types.d.ts +69 -0
  147. package/lib/src/resources/types.js +13 -0
  148. package/lib/src/resources/vendor.d.ts +94 -0
  149. package/lib/src/resources/vendor.js +296 -0
  150. package/lib/src/resources/wbn-reader.d.ts +44 -0
  151. package/lib/src/resources/wbn-reader.js +302 -0
  152. package/lib/src/schema.d.ts +25 -0
  153. package/lib/src/schema.js +169 -0
  154. package/lib/src/secrets.d.ts +21 -0
  155. package/lib/src/secrets.js +103 -0
  156. package/lib/src/serve-rules.d.ts +29 -0
  157. package/lib/src/serve-rules.js +123 -0
  158. package/lib/src/server.d.ts +45 -0
  159. package/lib/src/server.js +246 -0
  160. package/lib/src/servers.d.ts +85 -0
  161. package/lib/src/servers.js +225 -0
  162. package/lib/src/site.d.ts +60 -0
  163. package/lib/src/site.js +169 -0
  164. package/lib/src/store.d.ts +151 -0
  165. package/lib/src/store.js +596 -0
  166. package/lib/src/tree.d.ts +50 -0
  167. package/lib/src/tree.js +134 -0
  168. package/lib/src/values.d.ts +7 -0
  169. package/lib/src/values.js +37 -0
  170. package/lib/src/vendor.d.ts +19 -0
  171. package/lib/src/vendor.js +145 -0
  172. package/lib/src/watch.d.ts +44 -0
  173. package/lib/src/watch.js +137 -0
  174. package/lib/src/wbn.d.ts +26 -0
  175. package/lib/src/wbn.js +241 -0
  176. package/lib/src/yaml.d.ts +34 -0
  177. package/lib/src/yaml.js +562 -0
  178. package/package.json +59 -4
@@ -0,0 +1,650 @@
1
+ // Template helpers that write HTML for files of the tree (issue #105): `${file "/img/a.jpg" | img alt="…"}`.
2
+ //
3
+ // This module knows files, not templates: media types, image sizes read from file headers, URLs,
4
+ // subresource integrity, and the markup each helper writes. src/render.ts parses `${file …}` and
5
+ // calls renderFileHelper; rust/src/media.rs is the port (same output bytes, errors and messages).
6
+ import { createHash } from 'node:crypto';
7
+ import { SitefileError } from './errors.js';
8
+ import { escapeHtml } from './render.js';
9
+ import { normalizeVirtual } from './paths.js';
10
+ import { percentDecode } from './links.js';
11
+ /** Helpers that produce a value (escaped like any substituted value). */
12
+ export const VALUE_HELPERS = ['url', 'width', 'height', 'type', 'size', 'integrity', 'datauri', 'srcset'];
13
+ /** Helpers that write a tag (trusted markup: inserted as-is; every attribute value is escaped). */
14
+ export const TAG_HELPERS = ['img', 'video', 'audio', 'source', 'script', 'stylesheet', 'preload', 'modulepreload', 'pre', 'svg'];
15
+ export const FILE_HELPERS = [...VALUE_HELPERS, ...TAG_HELPERS];
16
+ /** The largest file `datauri` inlines. */
17
+ export const DATAURI_MAX = 32768;
18
+ const MIME = {
19
+ png: 'image/png',
20
+ jpg: 'image/jpeg',
21
+ jpeg: 'image/jpeg',
22
+ gif: 'image/gif',
23
+ webp: 'image/webp',
24
+ avif: 'image/avif',
25
+ svg: 'image/svg+xml',
26
+ ico: 'image/x-icon',
27
+ mp4: 'video/mp4',
28
+ m4v: 'video/mp4',
29
+ webm: 'video/webm',
30
+ ogv: 'video/ogg',
31
+ mov: 'video/quicktime',
32
+ mp3: 'audio/mpeg',
33
+ m4a: 'audio/mp4',
34
+ aac: 'audio/aac',
35
+ oga: 'audio/ogg',
36
+ ogg: 'audio/ogg',
37
+ opus: 'audio/opus',
38
+ wav: 'audio/wav',
39
+ flac: 'audio/flac',
40
+ css: 'text/css',
41
+ js: 'text/javascript',
42
+ mjs: 'text/javascript',
43
+ json: 'application/json',
44
+ html: 'text/html',
45
+ htm: 'text/html',
46
+ txt: 'text/plain',
47
+ xml: 'application/xml',
48
+ pdf: 'application/pdf',
49
+ wasm: 'application/wasm',
50
+ woff: 'font/woff',
51
+ woff2: 'font/woff2',
52
+ ttf: 'font/ttf',
53
+ otf: 'font/otf',
54
+ };
55
+ /** The lowercase extension of a path's last segment ('' if none). */
56
+ export function extensionOf(path) {
57
+ const base = path.slice(path.lastIndexOf('/') + 1);
58
+ const dot = base.lastIndexOf('.');
59
+ return dot <= 0 ? '' : base.slice(dot + 1).toLowerCase();
60
+ }
61
+ /** The media type of a path, by extension (`application/octet-stream` when unknown). */
62
+ export function mediaType(path) {
63
+ return MIME[extensionOf(path)] ?? 'application/octet-stream';
64
+ }
65
+ /** Extensions the image helpers accept (`img`, `width`, `height`, `source` writes `srcset`). */
66
+ const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg']);
67
+ export function isImagePath(path) {
68
+ return IMAGE_EXTENSIONS.has(extensionOf(path));
69
+ }
70
+ /** The URL of a tree path: each segment percent-encoded like `encodeURIComponent`. */
71
+ export function fileUrl(path) {
72
+ return path.split('/').map(encodeURIComponent).join('/');
73
+ }
74
+ /** `sha384-<base64>`, the subresource integrity of `bytes`. */
75
+ export function integrityOf(bytes) {
76
+ return 'sha384-' + createHash('sha384').update(bytes).digest('base64');
77
+ }
78
+ /** Which image format the bytes are, by their magic numbers. */
79
+ function sniff(b) {
80
+ const at = (i, s) => [...s].every((c, k) => b[i + k] === c.charCodeAt(0));
81
+ if (b.length >= 8 && b[0] === 0x89 && at(1, 'PNG\r\n\x1a\n'))
82
+ return 'PNG';
83
+ if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
84
+ return 'JPEG';
85
+ if (at(0, 'GIF87a') || at(0, 'GIF89a'))
86
+ return 'GIF';
87
+ if (at(0, 'RIFF') && at(8, 'WEBP'))
88
+ return 'WebP';
89
+ if (at(4, 'ftyp') && isAvifBrand(b))
90
+ return 'AVIF';
91
+ return undefined;
92
+ }
93
+ const u16be = (b, i) => (b[i] << 8) | b[i + 1];
94
+ const u16le = (b, i) => b[i] | (b[i + 1] << 8);
95
+ const u24le = (b, i) => b[i] | (b[i + 1] << 8) | (b[i + 2] << 16);
96
+ const u32be = (b, i) => ((b[i] << 24) >>> 0) + (b[i + 1] << 16) + (b[i + 2] << 8) + b[i + 3];
97
+ const u32le = (b, i) => b[i] + (b[i + 1] << 8) + (b[i + 2] << 16) + ((b[i + 3] << 24) >>> 0);
98
+ /** Thrown by the readers below; turned into one message per format. */
99
+ class Malformed extends Error {
100
+ }
101
+ const need = (b, end) => {
102
+ if (end > b.length)
103
+ throw new Malformed();
104
+ };
105
+ function pngSize(b) {
106
+ need(b, 24);
107
+ if (String.fromCharCode(b[12], b[13], b[14], b[15]) !== 'IHDR')
108
+ throw new Malformed();
109
+ return { width: u32be(b, 16), height: u32be(b, 20) };
110
+ }
111
+ function gifSize(b) {
112
+ need(b, 10);
113
+ return { width: u16le(b, 6), height: u16le(b, 8) };
114
+ }
115
+ /** JPEG: the first SOF segment; EXIF orientation 5 to 8 (a quarter turn) swaps width and height. */
116
+ function jpegSize(b) {
117
+ let i = 2;
118
+ let turned = false;
119
+ for (;;) {
120
+ need(b, i + 2);
121
+ if (b[i] !== 0xff)
122
+ throw new Malformed();
123
+ let m = b[i + 1];
124
+ while (m === 0xff) {
125
+ i++;
126
+ need(b, i + 2);
127
+ m = b[i + 1];
128
+ }
129
+ if (m === 0x01 || (m >= 0xd0 && m <= 0xd8)) {
130
+ i += 2;
131
+ continue;
132
+ }
133
+ if (m === 0xd9 || m === 0xda)
134
+ throw new Malformed(); // end of image or start of scan before any SOF
135
+ need(b, i + 4);
136
+ const len = u16be(b, i + 2);
137
+ if (len < 2)
138
+ throw new Malformed();
139
+ const end = i + 2 + len;
140
+ need(b, end);
141
+ const sof = (m >= 0xc0 && m <= 0xc3) || (m >= 0xc5 && m <= 0xc7) || (m >= 0xc9 && m <= 0xcb) || (m >= 0xcd && m <= 0xcf);
142
+ if (sof) {
143
+ if (len < 7)
144
+ throw new Malformed();
145
+ const height = u16be(b, i + 5);
146
+ const width = u16be(b, i + 7);
147
+ return turned ? { width: height, height: width } : { width, height };
148
+ }
149
+ if (m === 0xe1 && !turned)
150
+ turned = exifQuarterTurn(b.subarray(i + 4, end));
151
+ i = end;
152
+ }
153
+ }
154
+ /** An APP1 segment's EXIF orientation is 5, 6, 7 or 8. Anything unreadable counts as no orientation. */
155
+ function exifQuarterTurn(s) {
156
+ if (s.length < 14 || String.fromCharCode(...s.subarray(0, 6)) !== 'Exif\0\0')
157
+ return false;
158
+ const t = s.subarray(6);
159
+ const le = t[0] === 0x49 && t[1] === 0x49;
160
+ if (!le && !(t[0] === 0x4d && t[1] === 0x4d))
161
+ return false;
162
+ const u16 = (i) => (le ? u16le(t, i) : u16be(t, i));
163
+ const u32 = (i) => (le ? u32le(t, i) : u32be(t, i));
164
+ if (u16(2) !== 42)
165
+ return false;
166
+ const ifd = u32(4);
167
+ if (ifd + 2 > t.length)
168
+ return false;
169
+ const n = u16(ifd);
170
+ for (let k = 0; k < n; k++) {
171
+ const e = ifd + 2 + 12 * k;
172
+ if (e + 12 > t.length)
173
+ return false;
174
+ if (u16(e) === 0x0112) {
175
+ const v = u16(e + 2) === 3 ? u16(e + 8) : 0;
176
+ return v >= 5 && v <= 8;
177
+ }
178
+ }
179
+ return false;
180
+ }
181
+ function webpSize(b) {
182
+ need(b, 16);
183
+ const chunk = String.fromCharCode(b[12], b[13], b[14], b[15]);
184
+ if (chunk === 'VP8 ') {
185
+ need(b, 30);
186
+ if (b[23] !== 0x9d || b[24] !== 0x01 || b[25] !== 0x2a)
187
+ throw new Malformed();
188
+ return { width: u16le(b, 26) & 0x3fff, height: u16le(b, 28) & 0x3fff };
189
+ }
190
+ if (chunk === 'VP8L') {
191
+ need(b, 25);
192
+ if (b[20] !== 0x2f)
193
+ throw new Malformed();
194
+ const bits = u32le(b, 21);
195
+ return { width: (bits & 0x3fff) + 1, height: ((bits >>> 14) & 0x3fff) + 1 };
196
+ }
197
+ if (chunk === 'VP8X') {
198
+ need(b, 30);
199
+ return { width: u24le(b, 24) + 1, height: u24le(b, 27) + 1 };
200
+ }
201
+ throw new Malformed();
202
+ }
203
+ /** The boxes in b[from, to). */
204
+ function boxes(b, from, to) {
205
+ const out = [];
206
+ let i = from;
207
+ while (i < to) {
208
+ if (i + 8 > to)
209
+ throw new Malformed();
210
+ let size = u32be(b, i);
211
+ const type = String.fromCharCode(b[i + 4], b[i + 5], b[i + 6], b[i + 7]);
212
+ let start = i + 8;
213
+ if (size === 1) {
214
+ if (i + 16 > to || u32be(b, i + 8) !== 0)
215
+ throw new Malformed(); // a 64-bit size over 4 GiB
216
+ size = u32be(b, i + 12);
217
+ start = i + 16;
218
+ }
219
+ else if (size === 0) {
220
+ size = to - i;
221
+ }
222
+ if (size < start - i || i + size > to)
223
+ throw new Malformed();
224
+ out.push({ type, start, end: i + size });
225
+ i += size;
226
+ }
227
+ return out;
228
+ }
229
+ function isAvifBrand(b) {
230
+ if (b.length < 16)
231
+ return false;
232
+ const size = u32be(b, 0);
233
+ const end = Math.min(size, b.length);
234
+ for (let i = 8; i + 4 <= end; i += 4) {
235
+ if (i === 12)
236
+ continue; // minor version
237
+ const brand = String.fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3]);
238
+ if (brand === 'avif' || brand === 'avis')
239
+ return true;
240
+ }
241
+ return false;
242
+ }
243
+ function avifSize(b) {
244
+ const meta = boxes(b, 0, b.length).find((x) => x.type === 'meta');
245
+ if (!meta || meta.start + 4 > meta.end)
246
+ throw new Malformed();
247
+ const inner = boxes(b, meta.start + 4, meta.end); // a full box: version and flags first
248
+ const pitm = inner.find((x) => x.type === 'pitm');
249
+ const iprp = inner.find((x) => x.type === 'iprp');
250
+ if (!pitm || !iprp || pitm.start + 6 > pitm.end)
251
+ throw new Malformed();
252
+ const primary = b[pitm.start] === 0 ? u16be(b, pitm.start + 4) : (need(b, pitm.start + 8), u32be(b, pitm.start + 4));
253
+ const props = boxes(b, iprp.start, iprp.end);
254
+ const ipco = props.find((x) => x.type === 'ipco');
255
+ const ipma = props.find((x) => x.type === 'ipma');
256
+ if (!ipco || !ipma)
257
+ throw new Malformed();
258
+ const properties = boxes(b, ipco.start, ipco.end);
259
+ // ipma: version, flags, entry_count, then per item: id, count, associations.
260
+ need(b, ipma.start + 8);
261
+ const version = b[ipma.start];
262
+ const wide = (b[ipma.start + 3] & 1) === 1;
263
+ let i = ipma.start + 4;
264
+ const entries = u32be(b, i);
265
+ i += 4;
266
+ let associated;
267
+ for (let e = 0; e < entries; e++) {
268
+ const idLen = version < 1 ? 2 : 4;
269
+ if (i + idLen + 1 > ipma.end)
270
+ throw new Malformed();
271
+ const id = idLen === 2 ? u16be(b, i) : u32be(b, i);
272
+ i += idLen;
273
+ const count = b[i];
274
+ i += 1;
275
+ const list = [];
276
+ for (let a = 0; a < count; a++) {
277
+ if (wide) {
278
+ if (i + 2 > ipma.end)
279
+ throw new Malformed();
280
+ list.push(u16be(b, i) & 0x7fff);
281
+ i += 2;
282
+ }
283
+ else {
284
+ if (i + 1 > ipma.end)
285
+ throw new Malformed();
286
+ list.push(b[i] & 0x7f);
287
+ i += 1;
288
+ }
289
+ }
290
+ if (id === primary && associated === undefined)
291
+ associated = list;
292
+ }
293
+ if (!associated)
294
+ throw new Malformed();
295
+ let size;
296
+ let turned = false;
297
+ for (const index of associated) {
298
+ const p = properties[index - 1];
299
+ if (!p)
300
+ continue;
301
+ if (p.type === 'ispe' && !size) {
302
+ if (p.start + 12 > p.end)
303
+ throw new Malformed();
304
+ size = { width: u32be(b, p.start + 4), height: u32be(b, p.start + 8) };
305
+ }
306
+ else if (p.type === 'irot' && p.start < p.end) {
307
+ turned = (b[p.start] & 3) % 2 === 1;
308
+ }
309
+ }
310
+ if (!size)
311
+ throw new Malformed();
312
+ return turned ? { width: size.height, height: size.width } : size;
313
+ }
314
+ /**
315
+ * The width and height of an image file, from its header (PNG, JPEG, GIF, WebP, AVIF): bounded reads,
316
+ * no decoding. undefined for an SVG (its size is in its markup, often relative). E_DATA when the
317
+ * bytes are no supported image, or the header is truncated or malformed.
318
+ */
319
+ export function imageSize(bytes, path) {
320
+ if (extensionOf(path) === 'svg')
321
+ return undefined;
322
+ const format = sniff(bytes);
323
+ if (!format)
324
+ throw new SitefileError('E_DATA', `'${path}' is not a PNG, JPEG, GIF, WebP or AVIF image`);
325
+ let size;
326
+ try {
327
+ size =
328
+ format === 'PNG' ? pngSize(bytes) : format === 'JPEG' ? jpegSize(bytes) : format === 'GIF' ? gifSize(bytes) : format === 'WebP' ? webpSize(bytes) : avifSize(bytes);
329
+ }
330
+ catch (e) {
331
+ if (!(e instanceof Malformed))
332
+ throw e;
333
+ throw new SitefileError('E_DATA', `'${path}' is not a valid ${format} image: its header is truncated or malformed`);
334
+ }
335
+ if (size.width === 0 || size.height === 0) {
336
+ throw new SitefileError('E_DATA', `'${path}' is not a valid ${format} image: its header is truncated or malformed`);
337
+ }
338
+ return size;
339
+ }
340
+ function decodeText(f, helper, span) {
341
+ try {
342
+ return new TextDecoder('utf-8', { fatal: true }).decode(f.bytes);
343
+ }
344
+ catch (e) {
345
+ throw new SitefileError('E_ENCODING', `'${f.path}' is not valid UTF-8 (the ${helper} helper writes its text into the page)`, span, { cause: e });
346
+ }
347
+ }
348
+ /**
349
+ * `<name` and its attributes: the helper's own (`auto`, in order), then the template's, then the
350
+ * helper's defaults. A template attribute named like an automatic or default one (ASCII
351
+ * case-insensitive) replaces its value in place. Every value is HTML-escaped.
352
+ */
353
+ function tag(name, auto, attrs, defaults = []) {
354
+ const given = new Map();
355
+ for (const a of attrs)
356
+ given.set(a.name.toLowerCase(), a);
357
+ const own = new Set([...auto.map(([n]) => n), ...defaults.map(([n]) => n)]);
358
+ const one = (n, v) => (v === undefined ? ` ${n}` : ` ${n}="${escapeHtml(v)}"`);
359
+ const pick = (n, v) => {
360
+ const g = given.get(n);
361
+ return g ? one(g.name, g.value) : one(n, v);
362
+ };
363
+ let out = `<${name}`;
364
+ for (const [n, v] of auto)
365
+ out += pick(n, v);
366
+ for (const a of attrs)
367
+ if (!own.has(a.name.toLowerCase()))
368
+ out += one(a.name, a.value);
369
+ for (const [n, v] of defaults)
370
+ out += pick(n, v);
371
+ return out;
372
+ }
373
+ /** `as` of a preload, from the extension. */
374
+ const PRELOAD_AS = {
375
+ css: 'style', js: 'script', mjs: 'script', woff: 'font', woff2: 'font', ttf: 'font', otf: 'font', json: 'fetch',
376
+ png: 'image', jpg: 'image', jpeg: 'image', gif: 'image', webp: 'image', avif: 'image', svg: 'image',
377
+ };
378
+ /** The file `${file "<raw>"}` names: an absolute path in the tree (E_PATH, E_TYPE, E_NOT_FOUND otherwise). */
379
+ export function resolveFile(raw, tree, span) {
380
+ const p = normalizeVirtual(raw, span, '${file} path');
381
+ if (p.path === '/' || tree.isDirectory(p.path))
382
+ throw new SitefileError('E_TYPE', `'${p.path}' is a directory; \${file} needs a file`, span);
383
+ let path = p.path;
384
+ let bytes = tree.get(path);
385
+ // Renamed by FINGERPRINT: the hashed file, so the URL and integrity are those of what is served.
386
+ const moved = bytes ? undefined : tree.renamed?.(path);
387
+ if (moved !== undefined && tree.get(moved)) {
388
+ path = moved;
389
+ bytes = tree.get(moved);
390
+ }
391
+ if (!bytes) {
392
+ throw new SitefileError('E_NOT_FOUND', `'${p.path}' is not in the tree at this RENDER (write it with COPY, RENDER or TRANSFORM on an earlier line)`, span);
393
+ }
394
+ return { path, bytes };
395
+ }
396
+ /**
397
+ * What `${file "<raw>" | helper attrs…}` writes (`helper` undefined: the file's URL). A value is
398
+ * escaped by the caller like any substituted value; markup is inserted as it is.
399
+ */
400
+ export function renderFile(raw, helper, attrs, tree, opts, span) {
401
+ // The image plugin's metadata, when there is some, is what the image helpers read (#128).
402
+ if (helper === undefined || METADATA_HELPERS.has(helper)) {
403
+ const meta = findMetadata(normalizeVirtual(raw, span, '${file} path').path, helper, tree, span);
404
+ if (meta)
405
+ return renderMetadata(helper, meta, attrs);
406
+ }
407
+ return renderFileHelper(helper, resolveFile(raw, tree, span), attrs, opts, span);
408
+ }
409
+ /** What `helper` writes for file `f` (`undefined`: no helper, the file's URL). */
410
+ export function renderFileHelper(helper, f, attrs, opts, span) {
411
+ const url = fileUrl(f.path);
412
+ const ext = extensionOf(f.path);
413
+ const image = (what) => {
414
+ if (!IMAGE_EXTENSIONS.has(ext)) {
415
+ throw new SitefileError('E_TYPE', `'${f.path}' is not an image (${what} reads .png, .jpg, .jpeg, .gif, .webp, .avif and .svg files)`, span);
416
+ }
417
+ try {
418
+ return imageSize(f.bytes, f.path);
419
+ }
420
+ catch (e) {
421
+ if (e instanceof SitefileError && e.span === undefined)
422
+ throw new SitefileError(e.code, e.detail, span, { cause: e });
423
+ throw e;
424
+ }
425
+ };
426
+ switch (helper) {
427
+ case undefined:
428
+ case 'url':
429
+ return { value: url };
430
+ case 'type':
431
+ return { value: mediaType(f.path) };
432
+ case 'size':
433
+ return { value: String(f.bytes.length) };
434
+ case 'integrity':
435
+ return { value: integrityOf(f.bytes) };
436
+ case 'width':
437
+ case 'height': {
438
+ const size = image(helper);
439
+ if (!size)
440
+ throw new SitefileError('E_TYPE', `'${f.path}' is an SVG image; its ${helper} is in its markup, not in a header`, span);
441
+ return { value: String(helper === 'width' ? size.width : size.height) };
442
+ }
443
+ case 'srcset':
444
+ throw new SitefileError('E_TYPE', `'${f.path}' has no image metadata: srcset reads the <name>.json the image plugin writes (TRANSFORM … WITH image TO /dir/)`, span);
445
+ case 'datauri':
446
+ if (f.bytes.length > DATAURI_MAX) {
447
+ throw new SitefileError('E_TOO_LARGE', `'${f.path}' is ${f.bytes.length} bytes; datauri inlines files of at most ${DATAURI_MAX} bytes (link to it instead)`, span);
448
+ }
449
+ return { value: `data:${mediaType(f.path)};base64,${Buffer.from(f.bytes).toString('base64')}` };
450
+ case 'img': {
451
+ const size = image('img');
452
+ const auto = [['src', url]];
453
+ if (size)
454
+ auto.push(['width', String(size.width)], ['height', String(size.height)]);
455
+ return { markup: tag('img', auto, attrs, [['loading', 'lazy'], ['decoding', 'async']]) + '>' };
456
+ }
457
+ case 'video':
458
+ case 'audio':
459
+ return { markup: tag(helper, [['src', url]], attrs) + `></${helper}>` };
460
+ case 'source':
461
+ return { markup: tag('source', [[isImagePath(f.path) ? 'srcset' : 'src', url], ['type', mediaType(f.path)]], attrs) + '>' };
462
+ case 'script':
463
+ return { markup: tag('script', [['src', url], ['integrity', integrityOf(f.bytes)]], attrs) + '></script>' };
464
+ case 'stylesheet':
465
+ return { markup: tag('link', [['rel', 'stylesheet'], ['href', url], ['integrity', integrityOf(f.bytes)]], attrs) + '>' };
466
+ case 'modulepreload':
467
+ return { markup: tag('link', [['rel', 'modulepreload'], ['href', url], ['integrity', integrityOf(f.bytes)]], attrs) + '>' };
468
+ case 'preload': {
469
+ const as = attrs.find((a) => a.name.toLowerCase() === 'as')?.value ?? PRELOAD_AS[ext];
470
+ if (as === undefined)
471
+ throw new SitefileError('E_TYPE', `preload cannot tell what '${f.path}' is; give it as=… (style, script, font, image, fetch, …)`, span);
472
+ const auto = [['rel', 'preload'], ['href', url], ['as', as]];
473
+ // Browsers check integrity on preloaded scripts and stylesheets; fonts and fetches load in CORS mode.
474
+ if (as === 'script' || as === 'style')
475
+ auto.push(['integrity', integrityOf(f.bytes)]);
476
+ if (as === 'font' || as === 'fetch')
477
+ auto.push(['crossorigin', undefined]);
478
+ return { markup: tag('link', auto, attrs) + '>' };
479
+ }
480
+ case 'pre':
481
+ return { markup: tag('pre', [], attrs) + `>${escapeHtml(decodeText(f, 'pre', span))}</pre>` };
482
+ case 'svg': {
483
+ if (!opts.inlineSvg) {
484
+ throw new SitefileError('E_UNSUPPORTED', `the svg helper pastes '${f.path}' into the page, and SVG markup can run scripts; allow it with RENDER --inline-svg, or use the img helper`, span);
485
+ }
486
+ const text = stripSvgProlog(decodeText(f, 'svg', span));
487
+ if (!/^<svg[\t\n\f\r />]/.test(text))
488
+ throw new SitefileError('E_DATA', `'${f.path}' does not start with an <svg> element`, span);
489
+ return { markup: tag('svg', [], attrs) + text.slice(4) };
490
+ }
491
+ default:
492
+ throw new SitefileError('E_TEMPLATE', `unknown file helper '${helper}'`, span);
493
+ }
494
+ }
495
+ const SVG_WS = /^[\t\n\f\r ]+/;
496
+ /** SVG text without a BOM, XML declaration, comments before the root, doctype and surrounding whitespace. */
497
+ function stripSvgProlog(s) {
498
+ let t = s.replace(/^/, '').replace(SVG_WS, '');
499
+ for (;;) {
500
+ const end = t.startsWith('<?xml') ? t.indexOf('?>') + 2 : t.startsWith('<!--') ? t.indexOf('-->') + 3 : /^<!DOCTYPE/i.test(t) ? t.indexOf('>') + 1 : 0;
501
+ if (end <= 2)
502
+ break;
503
+ t = t.slice(end).replace(SVG_WS, '');
504
+ }
505
+ return t.replace(/[\t\n\f\r ]+$/, '');
506
+ }
507
+ /** The metadata version this sitefile reads. */
508
+ export const IMAGE_METADATA_VERSION = 1;
509
+ /** Helpers that read image metadata when there is some: the URL, type, size, srcset and img. */
510
+ const METADATA_HELPERS = new Set(['url', 'type', 'width', 'height', 'srcset', 'img']);
511
+ /** Looks like the plugin's metadata (an object with a number `version` and a `variants` list): otherwise it is some other JSON. */
512
+ function looksLikeMetadata(v) {
513
+ return v !== null && typeof v === 'object' && !Array.isArray(v) && typeof v['version'] === 'number' && Array.isArray(v['variants']);
514
+ }
515
+ function parseJson(bytes) {
516
+ try {
517
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
518
+ }
519
+ catch {
520
+ return undefined;
521
+ }
522
+ }
523
+ /** Check version 1 metadata; E_DATA naming the first field that is missing or of the wrong kind. */
524
+ function readMetadata(v, path, span) {
525
+ if (v['version'] !== IMAGE_METADATA_VERSION) {
526
+ throw new SitefileError('E_DATA', `'${path}' is image metadata version ${String(v['version'])}; this sitefile reads version ${IMAGE_METADATA_VERSION} (upgrade sitefile)`, span);
527
+ }
528
+ const bad = (field, what) => new SitefileError('E_DATA', `'${path}' is not valid image metadata: '${field}' must be ${what}`, span);
529
+ const str = (o, k, field) => {
530
+ const x = o[k];
531
+ if (typeof x !== 'string')
532
+ throw bad(field, 'a string');
533
+ return x;
534
+ };
535
+ const dim = (k) => {
536
+ const x = v[k];
537
+ if (typeof x !== 'number' || !Number.isInteger(x) || x <= 0)
538
+ throw bad(k, 'a positive whole number');
539
+ return x;
540
+ };
541
+ const src = str(v, 'src', 'src');
542
+ const type = str(v, 'type', 'type');
543
+ const width = dim('width');
544
+ const height = dim('height');
545
+ const srcset = str(v, 'srcset', 'srcset');
546
+ const list = v['sources'];
547
+ if (!Array.isArray(list))
548
+ throw bad('sources', 'a list');
549
+ const sources = list.map((s, i) => {
550
+ if (s === null || typeof s !== 'object' || Array.isArray(s))
551
+ throw bad(`sources.${i}`, 'an object');
552
+ return { type: str(s, 'type', `sources.${i}.type`), srcset: str(s, 'srcset', `sources.${i}.srcset`) };
553
+ });
554
+ return { path, src, type, width, height, srcset, sources };
555
+ }
556
+ /** `/dir/name.ext` -> `/dir/name.json` ('' for a path without an extension). */
557
+ export function metadataPathOf(path) {
558
+ const slash = path.lastIndexOf('/');
559
+ const dot = path.lastIndexOf('.');
560
+ return dot > slash + 1 ? path.slice(0, dot) + '.json' : '';
561
+ }
562
+ /**
563
+ * The image metadata a helper reads for `path`, if any. A `.json` path given to width, height,
564
+ * srcset or img must be metadata (E_DATA otherwise). An image path reads `<dir>/<name>.json` when
565
+ * that file is in the tree and is the plugin's metadata (another JSON file there is ignored).
566
+ */
567
+ function findMetadata(path, helper, tree, span) {
568
+ const ext = extensionOf(path);
569
+ if (ext === 'json') {
570
+ if (helper === undefined || !['width', 'height', 'srcset', 'img'].includes(helper))
571
+ return undefined;
572
+ const at = located(path, tree);
573
+ if (!at)
574
+ return undefined; // resolveFile reports it
575
+ const v = parseJson(at[1]);
576
+ if (!looksLikeMetadata(v))
577
+ throw new SitefileError('E_DATA', `'${path}' is not image metadata (the <name>.json the image plugin writes next to the files it makes)`, span);
578
+ return moved(readMetadata(v, path, span), tree);
579
+ }
580
+ if (!IMAGE_EXTENSIONS.has(ext))
581
+ return undefined;
582
+ const m = metadataPathOf(path);
583
+ const at = m ? located(m, tree) : undefined;
584
+ if (!at)
585
+ return undefined;
586
+ const v = parseJson(at[1]);
587
+ return looksLikeMetadata(v) ? moved(readMetadata(v, m, span), tree) : undefined;
588
+ }
589
+ /** A tree path and its bytes, or where a FINGERPRINT moved it. */
590
+ function located(path, tree) {
591
+ const bytes = tree.get(path);
592
+ if (bytes)
593
+ return [path, bytes];
594
+ const to = tree.renamed?.(path);
595
+ const b = to === undefined ? undefined : tree.get(to);
596
+ return b ? [to, b] : undefined;
597
+ }
598
+ /**
599
+ * Metadata read after a FINGERPRINT: its URLs (`src`, each `srcset` candidate) name the variants by
600
+ * their old paths, so a variant FINGERPRINT renamed gets the hashed file's URL.
601
+ */
602
+ function moved(m, tree) {
603
+ if (!tree.renamed)
604
+ return m;
605
+ const srcset = (set) => set.split(',').map((c) => {
606
+ const t = c.replace(/^[ \t\n\r\f]+/, '');
607
+ const end = t.search(/[ \t\n\r\f]/);
608
+ const u = end < 0 ? t : t.slice(0, end);
609
+ const to = movedUrl(u, tree);
610
+ return to === u ? c : c.replace(u, to);
611
+ }).join(',');
612
+ return { ...m, src: movedUrl(m.src, tree), srcset: srcset(m.srcset), sources: m.sources.map((x) => ({ type: x.type, srcset: srcset(x.srcset) })) };
613
+ }
614
+ /** An absolute URL path FINGERPRINT renamed: the hashed file's URL; any other URL as it is. */
615
+ function movedUrl(url, tree) {
616
+ if (!url.startsWith('/'))
617
+ return url;
618
+ const to = tree.renamed?.(url.split('/').map(percentDecode).join('/'));
619
+ return to !== undefined && tree.get(to) ? fileUrl(to) : url;
620
+ }
621
+ /** What a helper writes from image metadata: the largest fallback variant, its srcset, and `<picture>` for several formats. */
622
+ function renderMetadata(helper, m, attrs) {
623
+ switch (helper) {
624
+ case undefined:
625
+ case 'url':
626
+ return { value: m.src };
627
+ case 'type':
628
+ return { value: m.type };
629
+ case 'width':
630
+ return { value: String(m.width) };
631
+ case 'height':
632
+ return { value: String(m.height) };
633
+ case 'srcset':
634
+ return { value: m.srcset };
635
+ default: {
636
+ // img
637
+ const img = tag('img', [['src', m.src], ['srcset', m.srcset], ['width', String(m.width)], ['height', String(m.height)]], attrs, [['loading', 'lazy'], ['decoding', 'async']]) + '>';
638
+ if (m.sources.length <= 1)
639
+ return { markup: img };
640
+ // `sizes` applies to every <source> of the picture too.
641
+ const sizes = attrs.find((a) => a.name.toLowerCase() === 'sizes');
642
+ const sources = m.sources
643
+ .slice(0, -1)
644
+ .map((s) => tag('source', [['type', s.type], ['srcset', s.srcset], ...(sizes ? [['sizes', sizes.value]] : [])], []) + '>')
645
+ .join('');
646
+ return { markup: `<picture>${sources}${img}</picture>` };
647
+ }
648
+ }
649
+ }
650
+ //# sourceMappingURL=media.js.map