@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.
- package/CHANGELOG.md +152 -0
- package/LICENSE +21 -0
- package/README.md +513 -2
- package/lib/src/archive-writers.d.ts +31 -0
- package/lib/src/archive-writers.js +197 -0
- package/lib/src/attest.d.ts +126 -0
- package/lib/src/attest.js +220 -0
- package/lib/src/bake.d.ts +74 -0
- package/lib/src/bake.js +372 -0
- package/lib/src/base-image.d.ts +76 -0
- package/lib/src/base-image.js +138 -0
- package/lib/src/build-args.d.ts +26 -0
- package/lib/src/build-args.js +164 -0
- package/lib/src/cache-limit.d.ts +54 -0
- package/lib/src/cache-limit.js +239 -0
- package/lib/src/casefold-data.d.ts +2 -0
- package/lib/src/casefold-data.js +217 -0
- package/lib/src/cbor.d.ts +39 -0
- package/lib/src/cbor.js +174 -0
- package/lib/src/checklinks.d.ts +42 -0
- package/lib/src/checklinks.js +120 -0
- package/lib/src/cli/bake.d.ts +2 -0
- package/lib/src/cli/bake.js +179 -0
- package/lib/src/cli/build-image.d.ts +21 -0
- package/lib/src/cli/build-image.js +42 -0
- package/lib/src/cli/cache.d.ts +8 -0
- package/lib/src/cli/cache.js +119 -0
- package/lib/src/cli/common.d.ts +29 -0
- package/lib/src/cli/common.js +100 -0
- package/lib/src/cli/git-context.d.ts +18 -0
- package/lib/src/cli/git-context.js +48 -0
- package/lib/src/cli/images.d.ts +6 -0
- package/lib/src/cli/images.js +285 -0
- package/lib/src/cli/index.d.ts +10 -0
- package/lib/src/cli/index.js +95 -0
- package/lib/src/cli/network.d.ts +34 -0
- package/lib/src/cli/network.js +56 -0
- package/lib/src/cli/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +99 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +127 -0
- package/lib/src/cli/serve-worker.d.ts +2 -0
- package/lib/src/cli/serve-worker.js +42 -0
- package/lib/src/cli/servers.d.ts +15 -0
- package/lib/src/cli/servers.js +467 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +97 -0
- package/lib/src/cli/watch.d.ts +26 -0
- package/lib/src/cli/watch.js +81 -0
- package/lib/src/cli.d.ts +12 -0
- package/lib/src/cli.js +522 -0
- package/lib/src/csv.d.ts +27 -0
- package/lib/src/csv.js +117 -0
- package/lib/src/deflate.d.ts +76 -0
- package/lib/src/deflate.js +604 -0
- package/lib/src/digest.d.ts +3 -0
- package/lib/src/digest.js +7 -0
- package/lib/src/errors.d.ts +56 -0
- package/lib/src/errors.js +29 -0
- package/lib/src/executor.d.ts +393 -0
- package/lib/src/executor.js +2201 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +23 -0
- package/lib/src/export-formats.js +63 -0
- package/lib/src/exporter.d.ts +103 -0
- package/lib/src/exporter.js +676 -0
- package/lib/src/fingerprint.d.ts +40 -0
- package/lib/src/fingerprint.js +168 -0
- package/lib/src/frontmatter.d.ts +22 -0
- package/lib/src/frontmatter.js +45 -0
- package/lib/src/index.d.ts +13 -0
- package/lib/src/index.js +13 -0
- package/lib/src/language/ast.d.ts +360 -0
- package/lib/src/language/ast.js +57 -0
- package/lib/src/language/parser.d.ts +3 -0
- package/lib/src/language/parser.js +612 -0
- package/lib/src/language/scanner.d.ts +110 -0
- package/lib/src/language/scanner.js +449 -0
- package/lib/src/language/validate.d.ts +13 -0
- package/lib/src/language/validate.js +367 -0
- package/lib/src/links.d.ts +79 -0
- package/lib/src/links.js +583 -0
- package/lib/src/lint.d.ts +10 -0
- package/lib/src/lint.js +223 -0
- package/lib/src/markdown.d.ts +29 -0
- package/lib/src/markdown.js +612 -0
- package/lib/src/media.d.ts +82 -0
- package/lib/src/media.js +650 -0
- package/lib/src/memo.d.ts +57 -0
- package/lib/src/memo.js +93 -0
- package/lib/src/oci.d.ts +178 -0
- package/lib/src/oci.js +380 -0
- package/lib/src/outdated.d.ts +46 -0
- package/lib/src/outdated.js +195 -0
- package/lib/src/pages.d.ts +10 -0
- package/lib/src/pages.js +54 -0
- package/lib/src/paths.d.ts +39 -0
- package/lib/src/paths.js +101 -0
- package/lib/src/planner.d.ts +297 -0
- package/lib/src/planner.js +410 -0
- package/lib/src/plugins/cache.d.ts +96 -0
- package/lib/src/plugins/cache.js +325 -0
- package/lib/src/plugins/exec.d.ts +51 -0
- package/lib/src/plugins/exec.js +144 -0
- package/lib/src/plugins/host.d.ts +75 -0
- package/lib/src/plugins/host.js +277 -0
- package/lib/src/plugins/wasm.d.ts +23 -0
- package/lib/src/plugins/wasm.js +249 -0
- package/lib/src/profile.d.ts +42 -0
- package/lib/src/profile.js +68 -0
- package/lib/src/ref.d.ts +40 -0
- package/lib/src/ref.js +79 -0
- package/lib/src/registry-auth.d.ts +32 -0
- package/lib/src/registry-auth.js +151 -0
- package/lib/src/registry-cache.d.ts +71 -0
- package/lib/src/registry-cache.js +333 -0
- package/lib/src/registry-ops.d.ts +51 -0
- package/lib/src/registry-ops.js +248 -0
- package/lib/src/registry.d.ts +115 -0
- package/lib/src/registry.js +524 -0
- package/lib/src/render.d.ts +134 -0
- package/lib/src/render.js +576 -0
- package/lib/src/resources/archive.d.ts +11 -0
- package/lib/src/resources/archive.js +306 -0
- package/lib/src/resources/bundle-data.d.ts +16 -0
- package/lib/src/resources/bundle-data.js +50 -0
- package/lib/src/resources/convert.d.ts +12 -0
- package/lib/src/resources/convert.js +63 -0
- package/lib/src/resources/documents.d.ts +35 -0
- package/lib/src/resources/documents.js +122 -0
- package/lib/src/resources/git.d.ts +103 -0
- package/lib/src/resources/git.js +720 -0
- package/lib/src/resources/https.d.ts +74 -0
- package/lib/src/resources/https.js +413 -0
- package/lib/src/resources/ignore.d.ts +33 -0
- package/lib/src/resources/ignore.js +119 -0
- package/lib/src/resources/local.d.ts +64 -0
- package/lib/src/resources/local.js +248 -0
- package/lib/src/resources/lockfile.d.ts +52 -0
- package/lib/src/resources/lockfile.js +172 -0
- package/lib/src/resources/netpolicy.d.ts +6 -0
- package/lib/src/resources/netpolicy.js +82 -0
- package/lib/src/resources/network.d.ts +70 -0
- package/lib/src/resources/network.js +304 -0
- package/lib/src/resources/types.d.ts +69 -0
- package/lib/src/resources/types.js +13 -0
- package/lib/src/resources/vendor.d.ts +94 -0
- package/lib/src/resources/vendor.js +296 -0
- package/lib/src/resources/wbn-reader.d.ts +44 -0
- package/lib/src/resources/wbn-reader.js +302 -0
- package/lib/src/schema.d.ts +25 -0
- package/lib/src/schema.js +169 -0
- package/lib/src/secrets.d.ts +21 -0
- package/lib/src/secrets.js +103 -0
- package/lib/src/serve-rules.d.ts +29 -0
- package/lib/src/serve-rules.js +123 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +85 -0
- package/lib/src/servers.js +225 -0
- package/lib/src/site.d.ts +60 -0
- package/lib/src/site.js +169 -0
- package/lib/src/store.d.ts +151 -0
- package/lib/src/store.js +596 -0
- package/lib/src/tree.d.ts +50 -0
- package/lib/src/tree.js +134 -0
- package/lib/src/values.d.ts +7 -0
- package/lib/src/values.js +37 -0
- package/lib/src/vendor.d.ts +19 -0
- package/lib/src/vendor.js +145 -0
- package/lib/src/watch.d.ts +44 -0
- package/lib/src/watch.js +137 -0
- package/lib/src/wbn.d.ts +26 -0
- package/lib/src/wbn.js +241 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +59 -4
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Whether a FROM base is `image <ref>`. */
|
|
2
|
+
export function isImageBase(b) {
|
|
3
|
+
return typeof b === 'object' && 'kind' in b && b.kind === 'image';
|
|
4
|
+
}
|
|
5
|
+
/** Whether a LET is an html fragment. */
|
|
6
|
+
export function isFragment(ins) {
|
|
7
|
+
return ins.kind === 'LET' && ins.type === 'html';
|
|
8
|
+
}
|
|
9
|
+
export function isInline(x) {
|
|
10
|
+
return 'kind' in x && x.kind === 'inline';
|
|
11
|
+
}
|
|
12
|
+
export const OCI_COMPRESSIONS = ['gzip', 'none'];
|
|
13
|
+
/** Parse a compression name (case-insensitive); undefined if unknown. */
|
|
14
|
+
export function parseOciCompression(name) {
|
|
15
|
+
const n = name.toLowerCase();
|
|
16
|
+
return OCI_COMPRESSIONS.includes(n) ? n : undefined;
|
|
17
|
+
}
|
|
18
|
+
export const EXPORT_FORMATS = ['directory', 'zip', 'tar', 'tgz', 'wbn', 'oci'];
|
|
19
|
+
/** Parse a format name (`tar.gz` is an alias for `tgz`); undefined if unknown. */
|
|
20
|
+
export function parseExportFormat(name) {
|
|
21
|
+
const n = name.toLowerCase();
|
|
22
|
+
if (n === 'tar.gz')
|
|
23
|
+
return 'tgz';
|
|
24
|
+
return EXPORT_FORMATS.includes(n) ? n : undefined;
|
|
25
|
+
}
|
|
26
|
+
/** The loop variable LINK of a FEED uses for the current element. */
|
|
27
|
+
export const FEED_ITEM = 'item';
|
|
28
|
+
export const REDIRECT_STATUSES = [301, 302, 303, 307, 308];
|
|
29
|
+
export function literalText(op) {
|
|
30
|
+
if (isInline(op))
|
|
31
|
+
return undefined;
|
|
32
|
+
let out = '';
|
|
33
|
+
for (const p of op.parts) {
|
|
34
|
+
if (p.kind !== 'text')
|
|
35
|
+
return undefined;
|
|
36
|
+
out += p.value;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** Names of ARG, LET, IMPORT and stages (`FROM … AS name`, `--from=name`). */
|
|
41
|
+
export const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
42
|
+
/** A stage of an imported Sitefile: `<import>+<stage>` (`IMPORT theme … AS sitefile`, then `FROM theme+assets`). */
|
|
43
|
+
export const IMPORTED_STAGE_RE = /^([A-Za-z_][A-Za-z0-9_]*)\+([A-Za-z_][A-Za-z0-9_]*)$/;
|
|
44
|
+
/** `theme+assets` -> `{ import: 'theme', stage: 'assets' }`; undefined for anything else. */
|
|
45
|
+
export function importedStage(name) {
|
|
46
|
+
const m = IMPORTED_STAGE_RE.exec(name);
|
|
47
|
+
return m ? { import: m[1], stage: m[2] } : undefined;
|
|
48
|
+
}
|
|
49
|
+
/** Every stage alias declared anywhere in the file. */
|
|
50
|
+
export function stageNames(instructions) {
|
|
51
|
+
const names = new Set();
|
|
52
|
+
for (const ins of instructions)
|
|
53
|
+
if (ins.kind === 'FROM' && ins.alias !== undefined)
|
|
54
|
+
names.add(ins.alias);
|
|
55
|
+
return names;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=ast.js.map
|
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { INTEGRITY_RE } from '../digest.js';
|
|
2
|
+
import { IDENT_RE, IMPORTED_STAGE_RE, isInline, literalText, parseExportFormat, parseOciCompression, REDIRECT_STATUSES, } from './ast.js';
|
|
3
|
+
import { isIgnorePattern } from '../resources/ignore.js';
|
|
4
|
+
import { Cursor, logicalLines } from './scanner.js';
|
|
5
|
+
const VALUE_TYPES = ['string', 'number', 'boolean'];
|
|
6
|
+
const IMPORT_KINDS = ['file', 'text', 'json', 'tree', 'markdown', 'yaml', 'csv', 'wbn', 'html', 'sitefile'];
|
|
7
|
+
const NOT_YET = new Set(['REMOVE', 'GENERATE', 'ASSERT', 'META', 'EXEC']);
|
|
8
|
+
/** Parse Sitefile source into instructions (syntax only; see validate.ts for semantics). */
|
|
9
|
+
export function parseSitefile(source, file = 'Sitefile') {
|
|
10
|
+
return logicalLines(source, file).map((line) => parseInstruction(new Cursor(line, file)));
|
|
11
|
+
}
|
|
12
|
+
function parseInstruction(c) {
|
|
13
|
+
const span = c.span;
|
|
14
|
+
const word = c.word();
|
|
15
|
+
const kw = word.toUpperCase();
|
|
16
|
+
switch (kw) {
|
|
17
|
+
case 'FROM':
|
|
18
|
+
return parseFrom(c, span);
|
|
19
|
+
case 'STAGE':
|
|
20
|
+
return parseStage(c, span);
|
|
21
|
+
case 'ARG':
|
|
22
|
+
case 'LET':
|
|
23
|
+
return parseBinding(c, span, kw);
|
|
24
|
+
case 'IMPORT':
|
|
25
|
+
return parseImport(c, span);
|
|
26
|
+
case 'COPY':
|
|
27
|
+
return parseCopy(c, span);
|
|
28
|
+
case 'RENDER':
|
|
29
|
+
return parseRender(c, span);
|
|
30
|
+
case 'EXPORT':
|
|
31
|
+
return parseExport(c, span);
|
|
32
|
+
case 'PLUGIN':
|
|
33
|
+
return parsePlugin(c, span);
|
|
34
|
+
case 'TOOL':
|
|
35
|
+
return parseTool(c, span);
|
|
36
|
+
case 'TRANSFORM':
|
|
37
|
+
return parseTransform(c, span);
|
|
38
|
+
case 'SITEMAP':
|
|
39
|
+
return parseSitemap(c, span);
|
|
40
|
+
case 'FEED':
|
|
41
|
+
return parseFeed(c, span);
|
|
42
|
+
case 'REDIRECT':
|
|
43
|
+
return parseRedirect(c, span);
|
|
44
|
+
case 'HEADER':
|
|
45
|
+
return parseHeader(c, span);
|
|
46
|
+
case 'FINGERPRINT':
|
|
47
|
+
return parseFingerprint(c, span);
|
|
48
|
+
case 'CHECKLINKS':
|
|
49
|
+
return parseCheckLinks(c, span);
|
|
50
|
+
case 'RUN':
|
|
51
|
+
throw c.error('E_UNSUPPORTED', 'RUN is not supported: Sitefile never runs shell commands', span);
|
|
52
|
+
}
|
|
53
|
+
if (NOT_YET.has(kw))
|
|
54
|
+
throw c.error('E_UNSUPPORTED', `${kw} is not supported yet`, span);
|
|
55
|
+
throw c.error('E_UNKNOWN_INSTRUCTION', `unknown instruction '${word}'`, span);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A FROM base: `scratch`, `image <ref> [AT <dir>]`, or an operand (stage or directory). `image` is the
|
|
59
|
+
* keyword only when something other than `AS` follows it, so `FROM image` and `FROM image AS x` still
|
|
60
|
+
* name a stage or directory called `image`.
|
|
61
|
+
*/
|
|
62
|
+
function fromBase(c) {
|
|
63
|
+
if (c.keyword('scratch'))
|
|
64
|
+
return 'scratch';
|
|
65
|
+
const mark = c.mark();
|
|
66
|
+
if (c.keyword('image') && !c.atEnd() && !c.peekKeyword('AS')) {
|
|
67
|
+
const ref = c.operand('image reference');
|
|
68
|
+
return c.keyword('AT') ? { kind: 'image', ref, at: c.operand('image directory') } : { kind: 'image', ref };
|
|
69
|
+
}
|
|
70
|
+
c.reset(mark);
|
|
71
|
+
return c.operand('base');
|
|
72
|
+
}
|
|
73
|
+
function parseFrom(c, span) {
|
|
74
|
+
const base = fromBase(c);
|
|
75
|
+
const alias = c.keyword('AS') ? c.ident('stage name') : undefined;
|
|
76
|
+
c.expectEnd();
|
|
77
|
+
return alias === undefined ? { kind: 'FROM', span, base } : { kind: 'FROM', span, base, alias };
|
|
78
|
+
}
|
|
79
|
+
/** `STAGE name [FROM base]`: the same IR as `FROM base AS name` (an omitted base is `scratch`). */
|
|
80
|
+
function parseStage(c, span) {
|
|
81
|
+
const alias = c.ident('stage name');
|
|
82
|
+
let base = 'scratch';
|
|
83
|
+
if (c.keyword('FROM'))
|
|
84
|
+
base = fromBase(c);
|
|
85
|
+
c.expectEnd();
|
|
86
|
+
return { kind: 'FROM', span, base, alias, keyword: 'STAGE' };
|
|
87
|
+
}
|
|
88
|
+
function parseBinding(c, span, kind) {
|
|
89
|
+
const name = c.ident(`${kind} name`, ':=');
|
|
90
|
+
let type = 'string';
|
|
91
|
+
if (c.eat(':')) {
|
|
92
|
+
const at = c.span;
|
|
93
|
+
const t = c.ident('type', '=').toLowerCase();
|
|
94
|
+
if (t === 'html') {
|
|
95
|
+
if (kind === 'ARG')
|
|
96
|
+
throw c.error('E_SYNTAX', `ARG ${name} cannot be html: an html value is markup written in the Sitefile (LET ${name}: html = …)`, at);
|
|
97
|
+
return parseFragment(c, span, name);
|
|
98
|
+
}
|
|
99
|
+
if (!VALUE_TYPES.includes(t))
|
|
100
|
+
throw c.error('E_SYNTAX', `unknown type '${t}' (expected string, number or boolean${kind === 'LET' ? ', or html' : ''})`, at);
|
|
101
|
+
type = t;
|
|
102
|
+
}
|
|
103
|
+
const value = c.eat('=') ? c.operand('value') : undefined;
|
|
104
|
+
c.expectEnd();
|
|
105
|
+
if (kind === 'ARG') {
|
|
106
|
+
return value === undefined ? { kind, span, name, type } : { kind, span, name, type, defaultValue: value };
|
|
107
|
+
}
|
|
108
|
+
if (value === undefined)
|
|
109
|
+
throw c.error('E_SYNTAX', `LET ${name} requires a value (LET ${name} = ...)`, span);
|
|
110
|
+
return { kind, span, name, type, value };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* `LET name: html = <markup>` (the rest of the line is the template) or `LET name: html <<HTML`
|
|
114
|
+
* (also `= <<HTML`). A one-line fragment cannot use `\` continuations, so its error positions are exact.
|
|
115
|
+
*/
|
|
116
|
+
function parseFragment(c, span, name) {
|
|
117
|
+
const eq = c.eat('=');
|
|
118
|
+
const inline = c.inline();
|
|
119
|
+
if (inline) {
|
|
120
|
+
c.expectEnd();
|
|
121
|
+
return { kind: 'LET', span, name, type: 'html', body: inline };
|
|
122
|
+
}
|
|
123
|
+
c.expectInlineUsed(`the value of LET ${name}: html`);
|
|
124
|
+
const at = c.span;
|
|
125
|
+
const rest = c.rest();
|
|
126
|
+
if (!eq || rest.text === '') {
|
|
127
|
+
throw c.error('E_SYNTAX', `LET ${name}: html requires a value (LET ${name}: html = <markup>, or a heredoc: LET ${name}: html <<HTML)`, eq ? at : rest.span);
|
|
128
|
+
}
|
|
129
|
+
if (rest.multiline) {
|
|
130
|
+
throw c.error('E_SYNTAX', `LET ${name}: html on one line cannot continue onto the next line; write it as a heredoc (LET ${name}: html <<HTML)`, rest.span);
|
|
131
|
+
}
|
|
132
|
+
return { kind: 'LET', span, name, type: 'html', body: { kind: 'line', text: rest.text, span: rest.span } };
|
|
133
|
+
}
|
|
134
|
+
function parseImport(c, span) {
|
|
135
|
+
const name = c.ident('import name');
|
|
136
|
+
c.expectKeyword('FROM');
|
|
137
|
+
const uri = c.operand('URI');
|
|
138
|
+
let as;
|
|
139
|
+
let integrity;
|
|
140
|
+
let sort;
|
|
141
|
+
let schema;
|
|
142
|
+
let drafts;
|
|
143
|
+
const secrets = [];
|
|
144
|
+
const args = [];
|
|
145
|
+
while (!c.atEnd()) {
|
|
146
|
+
const at = c.span;
|
|
147
|
+
if (c.keyword('AS')) {
|
|
148
|
+
if (as !== undefined)
|
|
149
|
+
throw c.error('E_SYNTAX', 'AS given twice', at);
|
|
150
|
+
const kindAt = c.span;
|
|
151
|
+
const k = c.ident('import kind').toLowerCase();
|
|
152
|
+
if (!IMPORT_KINDS.includes(k))
|
|
153
|
+
throw c.error('E_SYNTAX', `unknown import kind '${k}' (expected file, text, json, yaml, csv, markdown, wbn, html, tree or sitefile)`, kindAt);
|
|
154
|
+
as = k;
|
|
155
|
+
}
|
|
156
|
+
else if (c.keyword('INTEGRITY')) {
|
|
157
|
+
if (integrity !== undefined)
|
|
158
|
+
throw c.error('E_SYNTAX', 'INTEGRITY given twice', at);
|
|
159
|
+
const op = c.operand('integrity digest');
|
|
160
|
+
const text = literalText(op);
|
|
161
|
+
if (text === undefined || !INTEGRITY_RE.test(text)) {
|
|
162
|
+
throw c.error('E_SYNTAX', 'INTEGRITY must be sha256:<64 lowercase hex digits>', op.span);
|
|
163
|
+
}
|
|
164
|
+
integrity = text;
|
|
165
|
+
}
|
|
166
|
+
else if (c.keyword('SORT')) {
|
|
167
|
+
if (sort !== undefined)
|
|
168
|
+
throw c.error('E_SYNTAX', 'SORT given twice', at);
|
|
169
|
+
c.expectKeyword('BY');
|
|
170
|
+
const key = c.ident('sort key');
|
|
171
|
+
const descending = c.keyword('DESC') ? true : (c.keyword('ASC'), false);
|
|
172
|
+
sort = { key, descending, span: at };
|
|
173
|
+
}
|
|
174
|
+
else if (c.keyword('SCHEMA')) {
|
|
175
|
+
if (schema !== undefined)
|
|
176
|
+
throw c.error('E_SYNTAX', 'SCHEMA given twice', at);
|
|
177
|
+
schema = { path: c.operand('schema path'), span: at };
|
|
178
|
+
}
|
|
179
|
+
else if (c.keyword('DRAFTS')) {
|
|
180
|
+
if (drafts !== undefined)
|
|
181
|
+
throw c.error('E_SYNTAX', 'DRAFTS given twice', at);
|
|
182
|
+
drafts = { value: c.operand('true or false'), span: at };
|
|
183
|
+
}
|
|
184
|
+
else if (c.keyword('SECRET')) {
|
|
185
|
+
const s = parseSecretClause(c, at);
|
|
186
|
+
const prior = secrets.find((p) => p.header.toLowerCase() === s.header.toLowerCase());
|
|
187
|
+
if (prior)
|
|
188
|
+
throw c.error('E_SYNTAX', `header '${s.header}' is set by two SECRET clauses`, at);
|
|
189
|
+
secrets.push(s);
|
|
190
|
+
}
|
|
191
|
+
else if (c.keyword('ARG')) {
|
|
192
|
+
// `ARG NAME=value`: a build arg of the Sitefile imported AS sitefile.
|
|
193
|
+
const argName = c.ident('ARG name', '=');
|
|
194
|
+
if (!c.eat('='))
|
|
195
|
+
throw c.error('E_SYNTAX', `expected = after ARG ${argName} (ARG ${argName}=<value>)`);
|
|
196
|
+
if (args.some((a) => a.name === argName))
|
|
197
|
+
throw c.error('E_SYNTAX', `ARG ${argName} given twice`, at);
|
|
198
|
+
args.push({ name: argName, value: c.operand('ARG value'), span: at });
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
throw c.error('E_SYNTAX', `unexpected '${c.word()}' (expected AS, INTEGRITY, SORT BY, SCHEMA, DRAFTS, SECRET or ARG)`, at);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
kind: 'IMPORT',
|
|
206
|
+
span,
|
|
207
|
+
name,
|
|
208
|
+
uri,
|
|
209
|
+
...(as !== undefined && { as }),
|
|
210
|
+
...(integrity !== undefined && { integrity }),
|
|
211
|
+
...(sort !== undefined && { sort }),
|
|
212
|
+
...(schema !== undefined && { schema }),
|
|
213
|
+
...(drafts !== undefined && { drafts }),
|
|
214
|
+
...(secrets.length > 0 && { secrets }),
|
|
215
|
+
...(args.length > 0 && { args }),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const HEADER_TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
219
|
+
/** Headers the HTTP client sets itself (or that frame the request): a secret cannot replace them. */
|
|
220
|
+
const RESERVED_HEADERS = new Set([
|
|
221
|
+
'accept', 'accept-encoding', 'connection', 'content-encoding', 'content-length', 'content-type', 'git-protocol',
|
|
222
|
+
'host', 'keep-alive', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'user-agent',
|
|
223
|
+
]);
|
|
224
|
+
/** `SECRET <id> AS HEADER <Name>` or `SECRET <id> AS BEARER` (after the SECRET keyword). */
|
|
225
|
+
function parseSecretClause(c, span) {
|
|
226
|
+
const id = c.ident('secret id');
|
|
227
|
+
c.expectKeyword('AS');
|
|
228
|
+
if (c.keyword('BEARER'))
|
|
229
|
+
return { id, header: 'Authorization', bearer: true, span };
|
|
230
|
+
const at = c.span;
|
|
231
|
+
if (!c.keyword('HEADER'))
|
|
232
|
+
throw c.error('E_SYNTAX', 'expected HEADER <name> or BEARER', at);
|
|
233
|
+
const nameAt = c.span;
|
|
234
|
+
const header = c.word();
|
|
235
|
+
if (header === '')
|
|
236
|
+
throw c.error('E_SYNTAX', 'expected a header name', nameAt);
|
|
237
|
+
if (!HEADER_TOKEN.test(header))
|
|
238
|
+
throw c.error('E_SYNTAX', `invalid header name '${header}'`, nameAt);
|
|
239
|
+
if (RESERVED_HEADERS.has(header.toLowerCase()))
|
|
240
|
+
throw c.error('E_SYNTAX', `header '${header}' is set by sitefile and cannot carry a secret`, nameAt);
|
|
241
|
+
return { id, header, bearer: false, span };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Read leading `--flags`; returns name -> flag (value '' for a bare flag). A `repeat` flag may be
|
|
245
|
+
* given several times: each one is added to `repeated` (and the map holds the first).
|
|
246
|
+
*/
|
|
247
|
+
function parseFlags(c, allowed, repeated = []) {
|
|
248
|
+
const out = new Map();
|
|
249
|
+
for (let f = c.flag(); f; f = c.flag()) {
|
|
250
|
+
const shape = allowed[f.name];
|
|
251
|
+
if (shape === undefined)
|
|
252
|
+
throw c.error('E_SYNTAX', `unknown flag --${f.name}`, f.span);
|
|
253
|
+
if (out.has(f.name) && shape !== 'repeat')
|
|
254
|
+
throw c.error('E_SYNTAX', `--${f.name} given twice`, f.span);
|
|
255
|
+
if (shape === 'bare' && f.value !== undefined)
|
|
256
|
+
throw c.error('E_SYNTAX', `--${f.name} takes no value`, f.span);
|
|
257
|
+
if (shape !== 'bare' && !f.value)
|
|
258
|
+
throw c.error('E_SYNTAX', `--${f.name} requires a value (--${f.name}=...)`, f.span);
|
|
259
|
+
const flag = { value: f.value ?? '', span: f.span };
|
|
260
|
+
if (shape === 'repeat')
|
|
261
|
+
repeated.push({ ...flag, name: f.name });
|
|
262
|
+
if (!out.has(f.name))
|
|
263
|
+
out.set(f.name, flag);
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
function stageRef(c, flags) {
|
|
268
|
+
const f = flags.get('from');
|
|
269
|
+
if (!f)
|
|
270
|
+
return {};
|
|
271
|
+
// A stage of this Sitefile, or `<import>+<stage>` for a stage of a Sitefile imported AS sitefile.
|
|
272
|
+
if (!IDENT_RE.test(f.value) && !IMPORTED_STAGE_RE.test(f.value))
|
|
273
|
+
throw c.error('E_SYNTAX', `--from needs a stage name, got '${f.value}'`, f.span);
|
|
274
|
+
return { from: { name: f.value, span: f.span } };
|
|
275
|
+
}
|
|
276
|
+
function noInlineFrom(c, source, flags) {
|
|
277
|
+
const from = flags.get('from');
|
|
278
|
+
if (from && isInline(source))
|
|
279
|
+
throw c.error('E_SYNTAX', '--from cannot be combined with an inline (<<) body', from.span);
|
|
280
|
+
}
|
|
281
|
+
/** Repeatable pattern flags (`--exclude`, `--include`): each value must be a `.sitefileignore` rule. */
|
|
282
|
+
function checkPatterns(c, patterns) {
|
|
283
|
+
for (const e of patterns) {
|
|
284
|
+
if (!isIgnorePattern(e.value))
|
|
285
|
+
throw c.error('E_SYNTAX', `--${e.name}=${e.value} is not a pattern (it is blank or a # comment in .sitefileignore syntax)`, e.span);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function parseCopy(c, span) {
|
|
289
|
+
const excludes = [];
|
|
290
|
+
const flags = parseFlags(c, { overwrite: 'bare', from: 'value', exclude: 'repeat', chmod: 'value', parents: 'bare' }, excludes);
|
|
291
|
+
checkPatterns(c, excludes);
|
|
292
|
+
const chmodFlag = flags.get('chmod');
|
|
293
|
+
let chmod;
|
|
294
|
+
if (chmodFlag) {
|
|
295
|
+
chmod = Object.hasOwn(CHMOD_MODES, chmodFlag.value) ? CHMOD_MODES[chmodFlag.value] : undefined;
|
|
296
|
+
if (chmod === undefined) {
|
|
297
|
+
throw c.error('E_SYNTAX', `--chmod must be 644 or 755 (files in a Sitefile tree are 0644 or 0755), got '${chmodFlag.value}'`, chmodFlag.span);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const source = c.inline() ?? c.operand('source');
|
|
301
|
+
const dest = c.operand('destination');
|
|
302
|
+
c.expectEnd();
|
|
303
|
+
c.expectInlineUsed('the COPY source');
|
|
304
|
+
noInlineFrom(c, source, flags);
|
|
305
|
+
for (const name of ['exclude', 'parents']) {
|
|
306
|
+
const f = flags.get(name);
|
|
307
|
+
if (f && isInline(source))
|
|
308
|
+
throw c.error('E_SYNTAX', `--${name} cannot be combined with an inline (<<) body`, f.span);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
kind: 'COPY',
|
|
312
|
+
span,
|
|
313
|
+
source,
|
|
314
|
+
dest,
|
|
315
|
+
overwrite: flags.has('overwrite'),
|
|
316
|
+
...stageRef(c, flags),
|
|
317
|
+
...(excludes.length > 0 && { exclude: excludes.map((e) => e.value) }),
|
|
318
|
+
...(chmod !== undefined && { chmod }),
|
|
319
|
+
...(flags.has('parents') && { parents: true }),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/** `--chmod` values: Sitefile trees only hold 0644 and 0755 files. */
|
|
323
|
+
const CHMOD_MODES = { '644': 0o644, '0644': 0o644, '755': 0o755, '0755': 0o755 };
|
|
324
|
+
function parseRender(c, span) {
|
|
325
|
+
const flags = parseFlags(c, { escape: 'value', overwrite: 'bare', from: 'value', 'inline-svg': 'bare' });
|
|
326
|
+
const escapeFlag = flags.get('escape');
|
|
327
|
+
const escape = (escapeFlag?.value ?? 'none').toLowerCase();
|
|
328
|
+
if (escapeFlag && escape !== 'none' && escape !== 'html') {
|
|
329
|
+
throw c.error('E_SYNTAX', `--escape must be none or html, got '${escapeFlag.value}'`, escapeFlag.span);
|
|
330
|
+
}
|
|
331
|
+
const template = c.inline() ?? c.operand('template');
|
|
332
|
+
c.expectKeyword('TO');
|
|
333
|
+
const dest = c.operand('destination');
|
|
334
|
+
let each;
|
|
335
|
+
const forAt = c.span;
|
|
336
|
+
if (c.keyword('FOR')) {
|
|
337
|
+
const as = c.ident('loop variable');
|
|
338
|
+
c.expectKeyword('IN');
|
|
339
|
+
const list = c.ident('list name');
|
|
340
|
+
const byAt = c.span;
|
|
341
|
+
const by = c.keyword('BY') ? { key: c.ident('group key'), span: byAt } : undefined;
|
|
342
|
+
const pageAt = c.span;
|
|
343
|
+
let page;
|
|
344
|
+
if (c.keyword('PAGE')) {
|
|
345
|
+
const size = c.operand('page size');
|
|
346
|
+
page = c.keyword('FIRST') ? { size, first: c.operand('first page destination'), span: pageAt } : { size, span: pageAt };
|
|
347
|
+
}
|
|
348
|
+
each = { as, list, span: forAt, ...(by && { by }), ...(page && { page }) };
|
|
349
|
+
}
|
|
350
|
+
c.expectEnd();
|
|
351
|
+
c.expectInlineUsed('the RENDER template');
|
|
352
|
+
noInlineFrom(c, template, flags);
|
|
353
|
+
return {
|
|
354
|
+
kind: 'RENDER',
|
|
355
|
+
span,
|
|
356
|
+
template,
|
|
357
|
+
dest,
|
|
358
|
+
escape: escape,
|
|
359
|
+
overwrite: flags.has('overwrite'),
|
|
360
|
+
...stageRef(c, flags),
|
|
361
|
+
...(each && { each }),
|
|
362
|
+
...(flags.has('inline-svg') && { inlineSvg: true }),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function parsePlugin(c, span) {
|
|
366
|
+
const name = c.ident('plugin name');
|
|
367
|
+
c.expectKeyword('FROM');
|
|
368
|
+
const uri = c.operand('plugin path');
|
|
369
|
+
let integrity;
|
|
370
|
+
const at = c.span;
|
|
371
|
+
if (c.keyword('INTEGRITY')) {
|
|
372
|
+
const op = c.operand('integrity digest');
|
|
373
|
+
const text = literalText(op);
|
|
374
|
+
if (text === undefined || !INTEGRITY_RE.test(text)) {
|
|
375
|
+
throw c.error('E_SYNTAX', 'INTEGRITY must be sha256:<64 lowercase hex digits>', op.span);
|
|
376
|
+
}
|
|
377
|
+
integrity = text;
|
|
378
|
+
}
|
|
379
|
+
else if (!c.atEnd()) {
|
|
380
|
+
throw c.error('E_SYNTAX', `unexpected '${c.word()}' (expected INTEGRITY)`, at);
|
|
381
|
+
}
|
|
382
|
+
c.expectEnd();
|
|
383
|
+
return { kind: 'PLUGIN', span, name, uri, ...(integrity !== undefined && { integrity }) };
|
|
384
|
+
}
|
|
385
|
+
function parseTool(c, span) {
|
|
386
|
+
const name = c.ident('tool name');
|
|
387
|
+
c.expectEnd();
|
|
388
|
+
return { kind: 'TOOL', span, name };
|
|
389
|
+
}
|
|
390
|
+
function parseTransform(c, span) {
|
|
391
|
+
const patterns = [];
|
|
392
|
+
const flags = parseFlags(c, { overwrite: 'bare', from: 'value', include: 'repeat', exclude: 'repeat' }, patterns);
|
|
393
|
+
checkPatterns(c, patterns);
|
|
394
|
+
// The source is optional (only with FOR): `TRANSFORM WITH og TO … FOR post IN posts`.
|
|
395
|
+
const source = c.keyword('WITH') ? undefined : (c.inline() ?? c.operand('source'));
|
|
396
|
+
if (source !== undefined)
|
|
397
|
+
c.expectKeyword('WITH');
|
|
398
|
+
const pluginSpan = c.span;
|
|
399
|
+
const plugin = c.ident('plugin name');
|
|
400
|
+
c.expectKeyword('TO');
|
|
401
|
+
const dest = c.operand('destination');
|
|
402
|
+
const options = [];
|
|
403
|
+
let each;
|
|
404
|
+
let sawOptions = false;
|
|
405
|
+
let args;
|
|
406
|
+
const parseArgs = () => {
|
|
407
|
+
// ARGS takes the rest of the line: every word is one argv entry.
|
|
408
|
+
args = [];
|
|
409
|
+
while (!c.atEnd())
|
|
410
|
+
args.push(c.operand('argument'));
|
|
411
|
+
};
|
|
412
|
+
while (!c.atEnd()) {
|
|
413
|
+
const at = c.span;
|
|
414
|
+
if (c.keyword('OPTIONS')) {
|
|
415
|
+
if (sawOptions)
|
|
416
|
+
throw c.error('E_SYNTAX', 'OPTIONS given twice', at);
|
|
417
|
+
sawOptions = true;
|
|
418
|
+
// key=value pairs until FOR or the end of the line.
|
|
419
|
+
while (!c.atEnd()) {
|
|
420
|
+
const keyAt = c.span;
|
|
421
|
+
if (c.keyword('FOR')) {
|
|
422
|
+
each = parseFor(c, keyAt);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
if (c.keyword('ARGS')) {
|
|
426
|
+
parseArgs();
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
const key = c.ident('option name', '=');
|
|
430
|
+
if (!c.eat('='))
|
|
431
|
+
throw c.error('E_SYNTAX', `expected ${key}=value`, keyAt);
|
|
432
|
+
if (options.some((o) => o.key === key))
|
|
433
|
+
throw c.error('E_SYNTAX', `option '${key}' given twice`, keyAt);
|
|
434
|
+
options.push({ key, value: c.operand(`value of ${key}`), span: keyAt });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else if (c.keyword('FOR')) {
|
|
438
|
+
if (each)
|
|
439
|
+
throw c.error('E_SYNTAX', 'FOR given twice', at);
|
|
440
|
+
each = parseFor(c, at);
|
|
441
|
+
}
|
|
442
|
+
else if (c.keyword('ARGS')) {
|
|
443
|
+
parseArgs();
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
throw c.error('E_SYNTAX', `unexpected '${c.word()}' (expected OPTIONS, FOR or ARGS)`, at);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
c.expectEnd();
|
|
450
|
+
c.expectInlineUsed('the TRANSFORM source');
|
|
451
|
+
if (source !== undefined)
|
|
452
|
+
noInlineFrom(c, source, flags);
|
|
453
|
+
if (source === undefined && flags.has('from'))
|
|
454
|
+
throw c.error('E_SYNTAX', '--from needs a source path', flags.get('from').span);
|
|
455
|
+
// --include/--exclude pick the files of a directory source, which runs the plugin once per file.
|
|
456
|
+
const firstPattern = patterns[0];
|
|
457
|
+
if (firstPattern) {
|
|
458
|
+
if (source === undefined)
|
|
459
|
+
throw c.error('E_SYNTAX', `--${firstPattern.name} needs a source directory`, firstPattern.span);
|
|
460
|
+
if (isInline(source))
|
|
461
|
+
throw c.error('E_SYNTAX', `--${firstPattern.name} cannot be combined with an inline (<<) body`, firstPattern.span);
|
|
462
|
+
if (each)
|
|
463
|
+
throw c.error('E_SYNTAX', `--${firstPattern.name} filters a directory source, which cannot be combined with FOR`, firstPattern.span);
|
|
464
|
+
}
|
|
465
|
+
const include = patterns.filter((p) => p.name === 'include').map((p) => p.value);
|
|
466
|
+
const exclude = patterns.filter((p) => p.name === 'exclude').map((p) => p.value);
|
|
467
|
+
return {
|
|
468
|
+
kind: 'TRANSFORM',
|
|
469
|
+
span,
|
|
470
|
+
...(source !== undefined && { source }),
|
|
471
|
+
...stageRef(c, flags),
|
|
472
|
+
plugin,
|
|
473
|
+
pluginSpan,
|
|
474
|
+
dest,
|
|
475
|
+
options,
|
|
476
|
+
...(each && { each }),
|
|
477
|
+
...(args && { args }),
|
|
478
|
+
...(include.length > 0 && { include }),
|
|
479
|
+
...(exclude.length > 0 && { exclude }),
|
|
480
|
+
overwrite: flags.has('overwrite'),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function parseFor(c, span) {
|
|
484
|
+
const as = c.ident('loop variable');
|
|
485
|
+
c.expectKeyword('IN');
|
|
486
|
+
return { as, list: c.ident('list name'), span };
|
|
487
|
+
}
|
|
488
|
+
/** `SITEMAP [--overwrite] BASE <url> [TO <dest>]` */
|
|
489
|
+
function parseSitemap(c, span) {
|
|
490
|
+
const flags = parseFlags(c, { overwrite: 'bare' });
|
|
491
|
+
c.expectKeyword('BASE');
|
|
492
|
+
const base = c.operand('base URL');
|
|
493
|
+
const dest = c.keyword('TO') ? c.operand('destination') : undefined;
|
|
494
|
+
c.expectEnd();
|
|
495
|
+
return { kind: 'SITEMAP', span, base, ...(dest ? { dest } : {}), overwrite: flags.has('overwrite') };
|
|
496
|
+
}
|
|
497
|
+
/** `FEED [--overwrite] <import> BASE <url> LINK <path> [TO <dest>] [TITLE <text>]` (clauses in this order) */
|
|
498
|
+
function parseFeed(c, span) {
|
|
499
|
+
const flags = parseFlags(c, { overwrite: 'bare' });
|
|
500
|
+
const listSpan = c.span;
|
|
501
|
+
const list = c.ident('import name');
|
|
502
|
+
c.expectKeyword('BASE');
|
|
503
|
+
const base = c.operand('base URL');
|
|
504
|
+
c.expectKeyword('LINK');
|
|
505
|
+
const link = c.operand('link path');
|
|
506
|
+
const dest = c.keyword('TO') ? c.operand('destination') : undefined;
|
|
507
|
+
const title = c.keyword('TITLE') ? c.operand('feed title') : undefined;
|
|
508
|
+
c.expectEnd();
|
|
509
|
+
return {
|
|
510
|
+
kind: 'FEED',
|
|
511
|
+
span,
|
|
512
|
+
list,
|
|
513
|
+
listSpan,
|
|
514
|
+
base,
|
|
515
|
+
link,
|
|
516
|
+
...(dest ? { dest } : {}),
|
|
517
|
+
...(title ? { title } : {}),
|
|
518
|
+
overwrite: flags.has('overwrite'),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/** `REDIRECT [--overwrite] <from> <to> [STATUS 301|302|303|307|308]` */
|
|
522
|
+
function parseRedirect(c, span) {
|
|
523
|
+
const flags = parseFlags(c, { overwrite: 'bare' });
|
|
524
|
+
const from = c.operand('redirect source path');
|
|
525
|
+
const to = c.operand('redirect target');
|
|
526
|
+
let status = 301;
|
|
527
|
+
if (c.keyword('STATUS')) {
|
|
528
|
+
const at = c.span;
|
|
529
|
+
const w = c.word();
|
|
530
|
+
const n = REDIRECT_STATUSES.find((s) => String(s) === w);
|
|
531
|
+
if (n === undefined) {
|
|
532
|
+
throw c.error('E_SYNTAX', w === '' ? 'expected a status after STATUS (301, 302, 303, 307 or 308)' : `unknown redirect status '${w}' (expected 301, 302, 303, 307 or 308)`, at);
|
|
533
|
+
}
|
|
534
|
+
status = n;
|
|
535
|
+
}
|
|
536
|
+
c.expectEnd();
|
|
537
|
+
return { kind: 'REDIRECT', span, from, to, status, overwrite: flags.has('overwrite') };
|
|
538
|
+
}
|
|
539
|
+
/** `HEADER [--overwrite] <path-pattern> <Name>: <value …>` */
|
|
540
|
+
function parseHeader(c, span) {
|
|
541
|
+
const flags = parseFlags(c, { overwrite: 'bare' });
|
|
542
|
+
const pattern = c.operand('path pattern');
|
|
543
|
+
const raw = c.operand('header name');
|
|
544
|
+
const last = raw.parts[raw.parts.length - 1];
|
|
545
|
+
if (last?.kind !== 'text' || !last.value.endsWith(':')) {
|
|
546
|
+
throw c.error('E_SYNTAX', "expected 'Name:' (a header name and a colon, then the value: HEADER /* X-Frame-Options: DENY)", raw.span);
|
|
547
|
+
}
|
|
548
|
+
const head = last.value.slice(0, -1);
|
|
549
|
+
const parts = [...raw.parts.slice(0, -1), ...(head === '' ? [] : [{ kind: 'text', value: head }])];
|
|
550
|
+
if (parts.length === 0)
|
|
551
|
+
throw c.error('E_SYNTAX', 'expected a header name before the colon', raw.span);
|
|
552
|
+
const value = [];
|
|
553
|
+
while (!c.atEnd())
|
|
554
|
+
value.push(c.operand('header value'));
|
|
555
|
+
if (value.length === 0)
|
|
556
|
+
throw c.error('E_SYNTAX', 'expected header value');
|
|
557
|
+
return { kind: 'HEADER', span, pattern, name: { parts, span: raw.span }, value, overwrite: flags.has('overwrite') };
|
|
558
|
+
}
|
|
559
|
+
/** `FINGERPRINT [--overwrite] <pattern>… [MANIFEST <dest>]` */
|
|
560
|
+
function parseFingerprint(c, span) {
|
|
561
|
+
const flags = parseFlags(c, { overwrite: 'bare' });
|
|
562
|
+
const patterns = [];
|
|
563
|
+
while (!c.atEnd() && !c.peekKeyword('MANIFEST'))
|
|
564
|
+
patterns.push(c.operand('path pattern'));
|
|
565
|
+
if (patterns.length === 0)
|
|
566
|
+
throw c.error('E_SYNTAX', 'expected a path pattern (FINGERPRINT /assets/ or FINGERPRINT /**/*.css)');
|
|
567
|
+
const manifest = c.keyword('MANIFEST') ? c.operand('manifest destination') : undefined;
|
|
568
|
+
c.expectEnd();
|
|
569
|
+
return { kind: 'FINGERPRINT', span, patterns, ...(manifest ? { manifest } : {}), overwrite: flags.has('overwrite') };
|
|
570
|
+
}
|
|
571
|
+
/** `CHECKLINKS [--warn] [--external]` */
|
|
572
|
+
function parseCheckLinks(c, span) {
|
|
573
|
+
const flags = parseFlags(c, { warn: 'bare', external: 'bare' });
|
|
574
|
+
c.expectEnd();
|
|
575
|
+
return { kind: 'CHECKLINKS', span, warn: flags.has('warn'), external: flags.has('external') };
|
|
576
|
+
}
|
|
577
|
+
function parseExport(c, span) {
|
|
578
|
+
const path = c.operand('path');
|
|
579
|
+
let format = 'directory';
|
|
580
|
+
let base;
|
|
581
|
+
let at;
|
|
582
|
+
if (c.keyword('AS')) {
|
|
583
|
+
const at = c.span;
|
|
584
|
+
const name = c.word();
|
|
585
|
+
if (name === '')
|
|
586
|
+
throw c.error('E_SYNTAX', 'expected export format after AS', at);
|
|
587
|
+
const f = parseExportFormat(name);
|
|
588
|
+
if (f === undefined) {
|
|
589
|
+
throw c.error('E_UNSUPPORTED', `export format '${name}' is not supported (expected directory, zip, tar, tgz, wbn or oci)`, at);
|
|
590
|
+
}
|
|
591
|
+
format = f;
|
|
592
|
+
}
|
|
593
|
+
if (c.keyword('BASE')) {
|
|
594
|
+
base = c.operand('base URL');
|
|
595
|
+
}
|
|
596
|
+
if (c.keyword('AT')) {
|
|
597
|
+
at = c.operand('image root path');
|
|
598
|
+
}
|
|
599
|
+
let compression;
|
|
600
|
+
if (c.keyword('COMPRESSION')) {
|
|
601
|
+
const cspan = c.span;
|
|
602
|
+
const name = c.word();
|
|
603
|
+
const value = parseOciCompression(name);
|
|
604
|
+
if (value === undefined) {
|
|
605
|
+
throw c.error('E_SYNTAX', name === '' ? 'expected gzip or none after COMPRESSION' : `unknown compression '${name}' (expected gzip or none)`, cspan);
|
|
606
|
+
}
|
|
607
|
+
compression = { value, span: cspan };
|
|
608
|
+
}
|
|
609
|
+
c.expectEnd();
|
|
610
|
+
return { kind: 'EXPORT', span, path, format, ...(base ? { base } : {}), ...(at ? { at } : {}), ...(compression ? { compression } : {}) };
|
|
611
|
+
}
|
|
612
|
+
//# sourceMappingURL=parser.js.map
|