@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,110 @@
1
+ import { SitefileError, type ErrorCode, type SourceSpan } from '../errors.js';
2
+ import type { InlineBody, Operand } from './ast.js';
3
+ export interface Pos {
4
+ readonly line: number;
5
+ readonly column: number;
6
+ }
7
+ /** One instruction's text after continuations are joined, with a source position per character. */
8
+ export interface LogicalLine {
9
+ readonly text: string;
10
+ readonly positions: readonly Pos[];
11
+ /** Position just after the last character (for "expected X" errors at end of line). */
12
+ readonly end: Pos;
13
+ /** A heredoc body that followed this line (COPY, RENDER, TRANSFORM and html LET only). */
14
+ readonly heredoc?: Heredoc;
15
+ }
16
+ export interface Heredoc {
17
+ /** Index in `text` of the `<<` marker. */
18
+ readonly at: number;
19
+ /** Length of the marker text (`<<-NAME`). */
20
+ readonly length: number;
21
+ readonly delimiter: string;
22
+ readonly text: string;
23
+ readonly bodyLine: number;
24
+ }
25
+ /** A comment line outside heredoc bodies: `text` runs from its `#` to the end of the line. */
26
+ export interface CommentLine {
27
+ readonly line: number;
28
+ /** Column of the `#`. */
29
+ readonly column: number;
30
+ readonly text: string;
31
+ }
32
+ /**
33
+ * Whether a logical line declares an html fragment, `LET name: html …` (the type ASCII
34
+ * case-insensitive), whose value may be a heredoc. Other LET lines are not scanned for `<<`.
35
+ */
36
+ export declare function isHtmlLet(text: string): boolean;
37
+ /** The language version this implementation reads (`# sitefile=1`; a file without the directive is this version). */
38
+ export declare const SYNTAX_VERSION = "1";
39
+ /**
40
+ * The version directive `# sitefile=N` (name case-insensitive, spaces or tabs around `=`), matched on a
41
+ * comment line from its `#`. Group 1 is everything after `=`.
42
+ * Other comments, `# sitefile-lint: …` included, are ordinary comments here.
43
+ */
44
+ export declare const VERSION_DIRECTIVE: RegExp;
45
+ /**
46
+ * Split source into logical lines. Blank lines and comment lines (first non-blank
47
+ * char `#`) are skipped, also inside continuations. A line ending in an odd number
48
+ * of `\` continues; pieces are trimmed at the join and joined with one space.
49
+ * A `# sitefile=N` comment is the syntax version directive: it must come before
50
+ * the first instruction, at most once, and name a supported version.
51
+ * `comments`, when given, receives every comment line (for `# sitefile-lint:`).
52
+ */
53
+ export declare function logicalLines(source: string, file?: string, comments?: CommentLine[]): LogicalLine[];
54
+ /** Reads tokens from one logical line. All methods skip leading whitespace. */
55
+ export declare class Cursor {
56
+ private readonly line;
57
+ readonly file: string;
58
+ private i;
59
+ private inlineUsed;
60
+ private readonly text;
61
+ constructor(line: LogicalLine, file: string);
62
+ spanAt(i: number): SourceSpan;
63
+ /** Span of the next non-whitespace character (or end of line). */
64
+ get span(): SourceSpan;
65
+ error(code: ErrorCode, detail: string, at?: number | SourceSpan): SitefileError;
66
+ skipWs(): void;
67
+ atEnd(): boolean;
68
+ private wordEnd;
69
+ /** Consume raw text up to the next whitespace ('' at end of line). */
70
+ word(): string;
71
+ /** Consume `kw` (case-insensitive) if it is the next whole bare word. */
72
+ keyword(kw: string): boolean;
73
+ /** Whether `kw` (case-insensitive) is the next whole bare word, without consuming it. */
74
+ peekKeyword(kw: string): boolean;
75
+ /** The current position, for {@link reset} (one-token lookahead). */
76
+ mark(): number;
77
+ reset(mark: number): void;
78
+ expectKeyword(kw: string): void;
79
+ /** Consume `ch` if it is the next non-whitespace character. */
80
+ eat(ch: string): boolean;
81
+ /**
82
+ * Read an identifier. It must be followed by whitespace, end of line, or one
83
+ * of the characters in `stops`.
84
+ */
85
+ ident(what: string, stops?: string): string;
86
+ /** Read `--name` or `--name=value` if the next word starts with `--`. */
87
+ flag(): {
88
+ name: string;
89
+ value: string | undefined;
90
+ span: SourceSpan;
91
+ } | undefined;
92
+ expectEnd(): void;
93
+ /**
94
+ * Consume the rest of the line (from the next non-whitespace character, trailing whitespace
95
+ * dropped): its text, its span, and whether it continues onto another physical line.
96
+ */
97
+ rest(): {
98
+ text: string;
99
+ span: SourceSpan;
100
+ multiline: boolean;
101
+ };
102
+ /** Consume this line's heredoc marker if it is the next word. */
103
+ inline(): InlineBody | undefined;
104
+ /** Fail if the line has a heredoc that was not used as the source. */
105
+ expectInlineUsed(where: string): void;
106
+ /** Read a bare, "double-quoted" or 'single-quoted' operand. */
107
+ operand(what: string): Operand;
108
+ /** At `${`: read `${NAME}`. */
109
+ private interpolation;
110
+ }
@@ -0,0 +1,449 @@
1
+ import { SitefileError } from '../errors.js';
2
+ /** Instructions whose source may be a heredoc (and `LET name: html`, see {@link isHtmlLet}). */
3
+ const HEREDOC_INSTRUCTIONS = new Set(['COPY', 'RENDER', 'TRANSFORM']);
4
+ /**
5
+ * Whether a logical line declares an html fragment, `LET name: html …` (the type ASCII
6
+ * case-insensitive), whose value may be a heredoc. Other LET lines are not scanned for `<<`.
7
+ */
8
+ export function isHtmlLet(text) {
9
+ const m = /^(\S+)/.exec(text);
10
+ if (!m || m[1].toUpperCase() !== 'LET')
11
+ return false;
12
+ let i = m[1].length;
13
+ const skip = () => {
14
+ while (i < text.length && WS.test(text[i]))
15
+ i++;
16
+ };
17
+ skip();
18
+ if (!IDENT_START.test(text[i] ?? ''))
19
+ return false;
20
+ while (IDENT_CHAR.test(text[i] ?? ''))
21
+ i++;
22
+ skip();
23
+ if (text[i] !== ':')
24
+ return false;
25
+ i++;
26
+ skip();
27
+ if (text.slice(i, i + 4).toLowerCase() !== 'html')
28
+ return false;
29
+ const next = text[i + 4];
30
+ return next === undefined || next === '=' || WS.test(next);
31
+ }
32
+ /** `<<NAME` / `<<-NAME` markers that start a word outside quotes. */
33
+ function heredocMarkers(text) {
34
+ const out = [];
35
+ let quote;
36
+ for (let i = 0; i < text.length; i++) {
37
+ const ch = text[i];
38
+ if (quote) {
39
+ if (ch === '\\' && quote === '"')
40
+ i++;
41
+ else if (ch === quote)
42
+ quote = undefined;
43
+ continue;
44
+ }
45
+ if (ch === '"' || ch === "'") {
46
+ quote = ch;
47
+ continue;
48
+ }
49
+ if (ch === '\\') {
50
+ i++;
51
+ continue;
52
+ }
53
+ if (ch !== '<' || (i > 0 && !WS.test(text[i - 1])))
54
+ continue;
55
+ const m = /^<<(-?)([A-Za-z_][A-Za-z0-9_]*)(?=\s|$)/.exec(text.slice(i));
56
+ if (m)
57
+ out.push({ at: i, length: m[0].length, strip: m[1] === '-', delimiter: m[2] });
58
+ }
59
+ return out;
60
+ }
61
+ /** The language version this implementation reads (`# sitefile=1`; a file without the directive is this version). */
62
+ export const SYNTAX_VERSION = '1';
63
+ /**
64
+ * The version directive `# sitefile=N` (name case-insensitive, spaces or tabs around `=`), matched on a
65
+ * comment line from its `#`. Group 1 is everything after `=`.
66
+ * Other comments, `# sitefile-lint: …` included, are ordinary comments here.
67
+ */
68
+ export const VERSION_DIRECTIVE = /^#[ \t]*[Ss][Ii][Tt][Ee][Ff][Ii][Ll][Ee][ \t]*=([\s\S]*)$/;
69
+ const IDENT_START = /[A-Za-z_]/;
70
+ const IDENT_CHAR = /[A-Za-z0-9_]/;
71
+ const WS = /\s/;
72
+ function trailingBackslashes(s) {
73
+ let n = 0;
74
+ while (n < s.length && s[s.length - 1 - n] === '\\')
75
+ n++;
76
+ return n;
77
+ }
78
+ /**
79
+ * Split source into logical lines. Blank lines and comment lines (first non-blank
80
+ * char `#`) are skipped, also inside continuations. A line ending in an odd number
81
+ * of `\` continues; pieces are trimmed at the join and joined with one space.
82
+ * A `# sitefile=N` comment is the syntax version directive: it must come before
83
+ * the first instruction, at most once, and name a supported version.
84
+ * `comments`, when given, receives every comment line (for `# sitefile-lint:`).
85
+ */
86
+ export function logicalLines(source, file = 'Sitefile', comments) {
87
+ const out = [];
88
+ let sawVersion = false;
89
+ /** A comment line: check a `# sitefile=N` directive (only allowed before the first instruction). */
90
+ const directive = (raw, line, late) => {
91
+ const hash = raw.indexOf('#');
92
+ const m = VERSION_DIRECTIVE.exec(raw.slice(hash));
93
+ if (!m)
94
+ return;
95
+ const at = { file, line, column: hash + 1 };
96
+ if (late)
97
+ throw new SitefileError('E_SYNTAX', "the '# sitefile=' directive must come before the first instruction", at);
98
+ if (sawVersion)
99
+ throw new SitefileError('E_SYNTAX', "'# sitefile=' given twice", at);
100
+ sawVersion = true;
101
+ const lead = m[1].length - m[1].trimStart().length;
102
+ const version = m[1].trim();
103
+ const vat = { file, line, column: hash + m[0].length - m[1].length + lead + 1 };
104
+ if (!/^[1-9][0-9]*$/.test(version)) {
105
+ throw new SitefileError('E_SYNTAX', `invalid syntax version '${version}' in '# sitefile=' (expected a whole number such as ${SYNTAX_VERSION})`, vat);
106
+ }
107
+ if (version !== SYNTAX_VERSION) {
108
+ throw new SitefileError('E_UNSUPPORTED', `this Sitefile uses syntax version ${version}; this sitefile supports version ${SYNTAX_VERSION} (upgrade sitefile)`, vat);
109
+ }
110
+ };
111
+ let text = '';
112
+ let positions = [];
113
+ let end = { line: 1, column: 1 };
114
+ let continuing = false;
115
+ // Set while reading a heredoc body: lines are taken verbatim until the delimiter.
116
+ let heredocBody;
117
+ const physical = source.split(/\r?\n/);
118
+ // A trailing newline is not an extra (empty) line of a heredoc body.
119
+ if (physical.length > 1 && physical[physical.length - 1] === '')
120
+ physical.pop();
121
+ physical.forEach((raw, index) => {
122
+ const line = index + 1;
123
+ if (heredocBody) {
124
+ const candidate = heredocBody.marker.strip ? raw.replace(/^\t+/, '') : raw;
125
+ if (candidate === heredocBody.marker.delimiter) {
126
+ const heredoc = {
127
+ at: heredocBody.marker.at,
128
+ length: heredocBody.marker.length,
129
+ delimiter: heredocBody.marker.delimiter,
130
+ text: heredocBody.lines.map((l) => l + '\n').join(''),
131
+ bodyLine: heredocBody.start,
132
+ };
133
+ out[out.length - 1] = { ...heredocBody.owner, heredoc };
134
+ heredocBody = undefined;
135
+ }
136
+ else {
137
+ heredocBody.lines.push(candidate);
138
+ }
139
+ return;
140
+ }
141
+ const trimmed = raw.trim();
142
+ if (trimmed.startsWith('#')) {
143
+ comments?.push({ line, column: raw.indexOf('#') + 1, text: raw.slice(raw.indexOf('#')) });
144
+ directive(raw, line, out.length > 0 || continuing);
145
+ return;
146
+ }
147
+ if (trimmed === '')
148
+ return;
149
+ const cont = trailingBackslashes(raw) % 2 === 1;
150
+ const body = cont ? raw.slice(0, -1) : raw;
151
+ let start = 0;
152
+ if (continuing)
153
+ while (start < body.length && WS.test(body[start]))
154
+ start++;
155
+ let stop = body.length;
156
+ if (cont)
157
+ while (stop > start && WS.test(body[stop - 1]))
158
+ stop--;
159
+ if (continuing && text !== '' && stop > start) {
160
+ text += ' ';
161
+ positions.push(end);
162
+ }
163
+ for (let c = start; c < stop; c++) {
164
+ text += body[c];
165
+ positions.push({ line, column: c + 1 });
166
+ }
167
+ if (stop > start || !continuing)
168
+ end = { line, column: stop + 1 };
169
+ if (cont) {
170
+ continuing = true;
171
+ }
172
+ else {
173
+ const logical = { text, positions, end };
174
+ out.push(logical);
175
+ const keyword = /^\S+/.exec(text)?.[0]?.toUpperCase() ?? '';
176
+ if (HEREDOC_INSTRUCTIONS.has(keyword) || isHtmlLet(text)) {
177
+ const markers = heredocMarkers(text);
178
+ if (markers.length > 1) {
179
+ const p = positions[markers[1].at];
180
+ throw new SitefileError('E_SYNTAX', 'only one inline (<<) body is allowed per instruction', { file, ...p });
181
+ }
182
+ if (markers[0])
183
+ heredocBody = { marker: markers[0], lines: [], start: line + 1, owner: logical };
184
+ }
185
+ text = '';
186
+ positions = [];
187
+ continuing = false;
188
+ }
189
+ });
190
+ if (heredocBody) {
191
+ const p = heredocBody.owner.positions[heredocBody.marker.at];
192
+ throw new SitefileError('E_SYNTAX', `inline body is missing its closing '${heredocBody.marker.delimiter}' line`, { file, ...p });
193
+ }
194
+ if (continuing && text.trim() !== '')
195
+ out.push({ text, positions, end });
196
+ return out;
197
+ }
198
+ /** Reads tokens from one logical line. All methods skip leading whitespace. */
199
+ export class Cursor {
200
+ line;
201
+ file;
202
+ i = 0;
203
+ inlineUsed = false;
204
+ text;
205
+ constructor(line, file) {
206
+ this.line = line;
207
+ this.file = file;
208
+ this.text = line.text;
209
+ }
210
+ spanAt(i) {
211
+ const p = this.line.positions[i] ?? this.line.end;
212
+ return { file: this.file, line: p.line, column: p.column };
213
+ }
214
+ /** Span of the next non-whitespace character (or end of line). */
215
+ get span() {
216
+ this.skipWs();
217
+ return this.spanAt(this.i);
218
+ }
219
+ error(code, detail, at = this.i) {
220
+ return new SitefileError(code, detail, typeof at === 'number' ? this.spanAt(at) : at);
221
+ }
222
+ skipWs() {
223
+ while (this.i < this.text.length && WS.test(this.text[this.i]))
224
+ this.i++;
225
+ }
226
+ atEnd() {
227
+ this.skipWs();
228
+ return this.i >= this.text.length;
229
+ }
230
+ wordEnd() {
231
+ let j = this.i;
232
+ while (j < this.text.length && !WS.test(this.text[j]))
233
+ j++;
234
+ return j;
235
+ }
236
+ /** Consume raw text up to the next whitespace ('' at end of line). */
237
+ word() {
238
+ this.skipWs();
239
+ const j = this.wordEnd();
240
+ const w = this.text.slice(this.i, j);
241
+ this.i = j;
242
+ return w;
243
+ }
244
+ /** Consume `kw` (case-insensitive) if it is the next whole bare word. */
245
+ keyword(kw) {
246
+ this.skipWs();
247
+ const j = this.wordEnd();
248
+ if (this.text.slice(this.i, j).toLowerCase() !== kw.toLowerCase())
249
+ return false;
250
+ this.i = j;
251
+ return true;
252
+ }
253
+ /** Whether `kw` (case-insensitive) is the next whole bare word, without consuming it. */
254
+ peekKeyword(kw) {
255
+ const at = this.i;
256
+ const found = this.keyword(kw);
257
+ this.i = at;
258
+ return found;
259
+ }
260
+ /** The current position, for {@link reset} (one-token lookahead). */
261
+ mark() {
262
+ return this.i;
263
+ }
264
+ reset(mark) {
265
+ this.i = mark;
266
+ }
267
+ expectKeyword(kw) {
268
+ const at = this.span;
269
+ if (!this.keyword(kw))
270
+ throw this.error('E_SYNTAX', `expected ${kw.toUpperCase()}`, at);
271
+ }
272
+ /** Consume `ch` if it is the next non-whitespace character. */
273
+ eat(ch) {
274
+ this.skipWs();
275
+ if (this.text[this.i] !== ch)
276
+ return false;
277
+ this.i++;
278
+ return true;
279
+ }
280
+ /**
281
+ * Read an identifier. It must be followed by whitespace, end of line, or one
282
+ * of the characters in `stops`.
283
+ */
284
+ ident(what, stops = '') {
285
+ this.skipWs();
286
+ const start = this.i;
287
+ if (!IDENT_START.test(this.text[this.i] ?? ''))
288
+ throw this.error('E_SYNTAX', `expected ${what}`);
289
+ while (IDENT_CHAR.test(this.text[this.i] ?? ''))
290
+ this.i++;
291
+ const next = this.text[this.i];
292
+ if (next !== undefined && !WS.test(next) && !stops.includes(next)) {
293
+ throw this.error('E_SYNTAX', `invalid ${what}: unexpected '${next}'`);
294
+ }
295
+ return this.text.slice(start, this.i);
296
+ }
297
+ /** Read `--name` or `--name=value` if the next word starts with `--`. */
298
+ flag() {
299
+ this.skipWs();
300
+ if (!this.text.startsWith('--', this.i))
301
+ return undefined;
302
+ const span = this.spanAt(this.i);
303
+ const w = this.word().slice(2);
304
+ const eq = w.indexOf('=');
305
+ return eq < 0 ? { name: w, value: undefined, span } : { name: w.slice(0, eq), value: w.slice(eq + 1), span };
306
+ }
307
+ expectEnd() {
308
+ if (this.atEnd())
309
+ return;
310
+ const at = this.i;
311
+ throw this.error('E_SYNTAX', `unexpected '${this.word()}'`, at);
312
+ }
313
+ /**
314
+ * Consume the rest of the line (from the next non-whitespace character, trailing whitespace
315
+ * dropped): its text, its span, and whether it continues onto another physical line.
316
+ */
317
+ rest() {
318
+ this.skipWs();
319
+ const start = this.i;
320
+ let stop = this.text.length;
321
+ while (stop > start && WS.test(this.text[stop - 1]))
322
+ stop--;
323
+ this.i = this.text.length;
324
+ const first = this.line.positions[start];
325
+ const last = this.line.positions[stop - 1];
326
+ return { text: this.text.slice(start, stop), span: this.spanAt(start), multiline: first !== undefined && last !== undefined && last.line !== first.line };
327
+ }
328
+ /** Consume this line's heredoc marker if it is the next word. */
329
+ inline() {
330
+ this.skipWs();
331
+ const h = this.line.heredoc;
332
+ if (!h || h.at !== this.i)
333
+ return undefined;
334
+ const span = this.spanAt(this.i);
335
+ this.i += h.length;
336
+ this.inlineUsed = true;
337
+ return { kind: 'inline', delimiter: h.delimiter, text: h.text, span, bodyLine: h.bodyLine };
338
+ }
339
+ /** Fail if the line has a heredoc that was not used as the source. */
340
+ expectInlineUsed(where) {
341
+ const h = this.line.heredoc;
342
+ if (h && !this.inlineUsed) {
343
+ throw this.error('E_SYNTAX', `an inline <<${h.delimiter} body can only be ${where}`, h.at);
344
+ }
345
+ }
346
+ /** Read a bare, "double-quoted" or 'single-quoted' operand. */
347
+ operand(what) {
348
+ this.skipWs();
349
+ const start = this.i;
350
+ const t = this.text;
351
+ if (start >= t.length)
352
+ throw this.error('E_SYNTAX', `expected ${what}`);
353
+ const parts = [];
354
+ const addText = (s) => {
355
+ const last = parts[parts.length - 1];
356
+ if (last?.kind === 'text')
357
+ parts[parts.length - 1] = { kind: 'text', value: last.value + s };
358
+ else
359
+ parts.push({ kind: 'text', value: s });
360
+ };
361
+ const quote = t[start];
362
+ if (quote === "'") {
363
+ const close = t.indexOf("'", start + 1);
364
+ if (close < 0)
365
+ throw this.error('E_SYNTAX', 'unterminated single-quoted string', start);
366
+ if (close > start + 1)
367
+ addText(t.slice(start + 1, close));
368
+ this.i = close + 1;
369
+ }
370
+ else if (quote === '"') {
371
+ this.i++;
372
+ for (;;) {
373
+ const ch = t[this.i];
374
+ if (ch === undefined)
375
+ throw this.error('E_SYNTAX', 'unterminated double-quoted string', start);
376
+ if (ch === '"') {
377
+ this.i++;
378
+ break;
379
+ }
380
+ if (ch === '\\') {
381
+ const next = t[this.i + 1];
382
+ if (next === undefined)
383
+ throw this.error('E_SYNTAX', 'unterminated double-quoted string', start);
384
+ addText(next === 'n' ? '\n' : next === 't' ? '\t' : next);
385
+ this.i += 2;
386
+ }
387
+ else if (ch === '$' && t[this.i + 1] === '{') {
388
+ parts.push(this.interpolation());
389
+ }
390
+ else {
391
+ addText(ch);
392
+ this.i++;
393
+ }
394
+ }
395
+ }
396
+ else {
397
+ while (this.i < t.length && !WS.test(t[this.i])) {
398
+ const ch = t[this.i];
399
+ if (ch === '\\') {
400
+ const next = t[this.i + 1];
401
+ if (next === undefined)
402
+ throw this.error('E_SYNTAX', 'dangling escape at end of line');
403
+ addText(next);
404
+ this.i += 2;
405
+ }
406
+ else if (ch === '$' && t[this.i + 1] === '{') {
407
+ parts.push(this.interpolation());
408
+ }
409
+ else {
410
+ addText(ch);
411
+ this.i++;
412
+ }
413
+ }
414
+ return { parts, span: this.spanAt(start) };
415
+ }
416
+ const after = t[this.i];
417
+ if (after !== undefined && !WS.test(after))
418
+ throw this.error('E_SYNTAX', 'unexpected character after closing quote');
419
+ return { parts, span: this.spanAt(start) };
420
+ }
421
+ /** At `${`: read `${NAME}`. */
422
+ interpolation() {
423
+ const at = this.i;
424
+ const t = this.text;
425
+ this.i += 2;
426
+ if (!IDENT_START.test(t[this.i] ?? ''))
427
+ throw this.error('E_SYNTAX', 'expected a name after ${', at);
428
+ const nameStart = this.i;
429
+ while (IDENT_CHAR.test(t[this.i] ?? ''))
430
+ this.i++;
431
+ const name = t.slice(nameStart, this.i);
432
+ // Dotted paths are parsed here; the validator allows them only for a RENDER … FOR loop variable.
433
+ const path = [];
434
+ while (t[this.i] === '.') {
435
+ this.i++;
436
+ const segStart = this.i;
437
+ while (/[A-Za-z0-9_-]/.test(t[this.i] ?? ''))
438
+ this.i++;
439
+ if (this.i === segStart)
440
+ throw this.error('E_SYNTAX', `expected a key after '.' in \${${name}…}`, at);
441
+ path.push(t.slice(segStart, this.i));
442
+ }
443
+ if (t[this.i] !== '}')
444
+ throw this.error('E_SYNTAX', 'unterminated ${...}: expected }', at);
445
+ this.i++;
446
+ return { kind: 'var', name, path, span: this.spanAt(at) };
447
+ }
448
+ }
449
+ //# sourceMappingURL=scanner.js.map
@@ -0,0 +1,13 @@
1
+ import { type FragmentBody, type Instruction, type Operand, type Program } from './ast.js';
2
+ import { type TemplateNode } from '../render.js';
3
+ /** A fragment's template: a heredoc body or partial file without its final newline (it is inserted inline). */
4
+ export declare function fragmentText(text: string): string;
5
+ /** Parse the template of an html LET (errors point into the Sitefile). */
6
+ export declare function parseFragmentBody(body: FragmentBody, file: string): TemplateNode[];
7
+ export declare function operandsOf(ins: Instruction): Operand[];
8
+ /** The only placeholders the host replaces inside TOOL arguments; any other `{…}` stays literal (e.g. ffmpeg's `%{pts}`). */
9
+ export declare const TOOL_PLACEHOLDER_RE: RegExp;
10
+ /** Check semantic rules (see docs/LANGUAGE.md, "v0.1 implementation decisions"). */
11
+ export declare function validate(instructions: readonly Instruction[], file: string): Program;
12
+ /** Parse and validate a Sitefile. */
13
+ export declare function parseProgram(source: string, file?: string): Program;