@dogsbay/format-starlight 0.2.0-beta.115 → 0.2.0-beta.117
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/dist/adapter.d.ts +69 -1
- package/dist/adapters/cloudflare.d.ts +183 -1
- package/dist/adapters/cloudflare.js +1948 -57
- package/dist/cli.js +506 -15
- package/dist/fences.d.ts +16 -0
- package/dist/fences.js +49 -0
- package/dist/github-slug.d.ts +19 -0
- package/dist/github-slug.js +112 -0
- package/dist/js-literal.d.ts +12 -0
- package/dist/js-literal.js +249 -0
- package/dist/jsx-to-jinja.d.ts +62 -0
- package/dist/jsx-to-jinja.js +869 -0
- package/dist/parse-mdx.d.ts +38 -1
- package/dist/parse-mdx.js +433 -27
- package/dist/parse.js +7 -2
- package/dist/template-tokens.d.ts +63 -0
- package/dist/template-tokens.js +206 -0
- package/package.json +5 -3
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSX expressions over partial props → Jinja (template pages,
|
|
3
|
+
* plans/page-templates.md Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* Cloudflare partials read their parameters as `props.x` inside MDX
|
|
6
|
+
* expressions: `{props.x}`, `{props.a === "b" ? "c" : props.d}`,
|
|
7
|
+
* `{props.a === "b" && (<p>…</p>)}`, `{props.x ?? "default"}`, and
|
|
8
|
+
* `href={`/x/${props.y}/`}` in attributes. This module translates the subset
|
|
9
|
+
* that corpus actually uses into Jinja:
|
|
10
|
+
*
|
|
11
|
+
* {props.x} → {{ x }}
|
|
12
|
+
* {props.x ?? "d"} → {{ x | default("d") }}
|
|
13
|
+
* {A ? B : C} (text position) → {% if A %}B{% else %}C{% endif %}
|
|
14
|
+
* {A && B} (text position) → {% if A %}B{% endif %}
|
|
15
|
+
* attr={EXPR} → attr="{{ EXPR }}" (or the literal text)
|
|
16
|
+
*
|
|
17
|
+
* The expression grammar is deliberately small: `props.name`, string /
|
|
18
|
+
* number / boolean literals, template literals, `=== == !== != && || ! ??`,
|
|
19
|
+
* `?:`, parentheses, and `.includes(…)`. Anything else — a function call,
|
|
20
|
+
* an arrow function, an object literal, another identifier — is REFUSED:
|
|
21
|
+
* the original `{…}` stays byte-for-byte and is counted, so a conversion
|
|
22
|
+
* never produces a plausible wrong template (the format-vitepress rule).
|
|
23
|
+
*/
|
|
24
|
+
export function newStats() {
|
|
25
|
+
return { converted: 0, refused: 0, refusedSamples: [], names: new Set(), markdownInlined: 0 };
|
|
26
|
+
}
|
|
27
|
+
class Refuse extends Error {
|
|
28
|
+
}
|
|
29
|
+
const OPS = ["===", "!==", "==", "!=", "&&", "||", "??", "?.", "?", ":", "!", "(", ")", ".", ","];
|
|
30
|
+
function tokenize(src) {
|
|
31
|
+
const out = [];
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < src.length) {
|
|
34
|
+
const c = src[i];
|
|
35
|
+
if (/\s/.test(c)) {
|
|
36
|
+
i++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (c === '"' || c === "'") {
|
|
40
|
+
let j = i + 1;
|
|
41
|
+
let v = "";
|
|
42
|
+
while (j < src.length && src[j] !== c) {
|
|
43
|
+
if (src[j] === "\\" && j + 1 < src.length) {
|
|
44
|
+
v += src[j + 1];
|
|
45
|
+
j += 2;
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
v += src[j++];
|
|
49
|
+
}
|
|
50
|
+
if (j >= src.length)
|
|
51
|
+
throw new Refuse("unterminated string");
|
|
52
|
+
out.push({ t: "str", v });
|
|
53
|
+
i = j + 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (c === "`") {
|
|
57
|
+
const parts = [];
|
|
58
|
+
let j = i + 1;
|
|
59
|
+
let buf = "";
|
|
60
|
+
while (j < src.length && src[j] !== "`") {
|
|
61
|
+
if (src[j] === "$" && src[j + 1] === "{") {
|
|
62
|
+
const end = matchBrace(src, j + 1);
|
|
63
|
+
if (end < 0)
|
|
64
|
+
throw new Refuse("unterminated ${");
|
|
65
|
+
if (buf)
|
|
66
|
+
parts.push(buf);
|
|
67
|
+
buf = "";
|
|
68
|
+
parts.push(parseExpr(src.slice(j + 2, end)));
|
|
69
|
+
j = end + 1;
|
|
70
|
+
}
|
|
71
|
+
else if (src[j] === "\\" && j + 1 < src.length) {
|
|
72
|
+
buf += src[j + 1];
|
|
73
|
+
j += 2;
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
buf += src[j++];
|
|
77
|
+
}
|
|
78
|
+
if (j >= src.length)
|
|
79
|
+
throw new Refuse("unterminated template literal");
|
|
80
|
+
if (buf)
|
|
81
|
+
parts.push(buf);
|
|
82
|
+
out.push({ t: "tpl", parts });
|
|
83
|
+
i = j + 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (c === "<") {
|
|
87
|
+
// A JSX value (in a branch position): take it whole, balanced by tags.
|
|
88
|
+
const end = jsxEnd(src, i);
|
|
89
|
+
out.push({ t: "jsx", v: src.slice(i, end) });
|
|
90
|
+
i = end;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (/[0-9]/.test(c)) {
|
|
94
|
+
const m = /^[0-9]+(\.[0-9]+)?/.exec(src.slice(i));
|
|
95
|
+
out.push({ t: "num", v: m[0] });
|
|
96
|
+
i += m[0].length;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
100
|
+
const m = /^[A-Za-z_$][\w$]*/.exec(src.slice(i));
|
|
101
|
+
out.push({ t: "id", v: m[0] });
|
|
102
|
+
i += m[0].length;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const op = OPS.find((o) => src.startsWith(o, i));
|
|
106
|
+
if (!op)
|
|
107
|
+
throw new Refuse(`unsupported character '${c}'`);
|
|
108
|
+
out.push({ t: "op", v: op });
|
|
109
|
+
i += op.length;
|
|
110
|
+
}
|
|
111
|
+
out.push({ t: "end" });
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* End index (exclusive) of a JSX element or fragment starting at `i`.
|
|
116
|
+
*
|
|
117
|
+
* A tag ends at the first `>` OUTSIDE its attribute values: Cloudflare's
|
|
118
|
+
* `<Markdown text={`… **Insights** > **Network health** …`} />` carries `>`
|
|
119
|
+
* in a template literal, and ending there refused ~40 partial conditionals
|
|
120
|
+
* (their `<Markdown>` then leaked as raw tags). Braced expressions in
|
|
121
|
+
* children (`{" "}`, `{props.x}`) are skipped whole for the same reason.
|
|
122
|
+
*/
|
|
123
|
+
function jsxEnd(src, i) {
|
|
124
|
+
let depth = 0;
|
|
125
|
+
let j = i;
|
|
126
|
+
const skipBraced = (at) => {
|
|
127
|
+
const close = matchBrace(src, at);
|
|
128
|
+
if (close < 0)
|
|
129
|
+
throw new Refuse("unbalanced JSX expression");
|
|
130
|
+
return close + 1;
|
|
131
|
+
};
|
|
132
|
+
while (j < src.length) {
|
|
133
|
+
if (src[j] === "<") {
|
|
134
|
+
const closing = src[j + 1] === "/";
|
|
135
|
+
let k = j + 1;
|
|
136
|
+
for (;;) {
|
|
137
|
+
if (k >= src.length)
|
|
138
|
+
throw new Refuse("unterminated JSX");
|
|
139
|
+
const ch = src[k];
|
|
140
|
+
if (ch === "{")
|
|
141
|
+
k = skipBraced(k);
|
|
142
|
+
else if (ch === '"' || ch === "'") {
|
|
143
|
+
const q = src.indexOf(ch, k + 1);
|
|
144
|
+
if (q < 0)
|
|
145
|
+
throw new Refuse("unterminated JSX attribute");
|
|
146
|
+
k = q + 1;
|
|
147
|
+
}
|
|
148
|
+
else if (ch === ">")
|
|
149
|
+
break;
|
|
150
|
+
else
|
|
151
|
+
k++;
|
|
152
|
+
}
|
|
153
|
+
const selfClosing = src[k - 1] === "/";
|
|
154
|
+
if (closing)
|
|
155
|
+
depth--;
|
|
156
|
+
else if (!selfClosing)
|
|
157
|
+
depth++;
|
|
158
|
+
j = k + 1;
|
|
159
|
+
if (depth === 0)
|
|
160
|
+
return j;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (depth === 0)
|
|
164
|
+
break;
|
|
165
|
+
if (src[j] === "{") {
|
|
166
|
+
j = skipBraced(j);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
j++;
|
|
170
|
+
}
|
|
171
|
+
if (depth !== 0)
|
|
172
|
+
throw new Refuse("unbalanced JSX");
|
|
173
|
+
return j;
|
|
174
|
+
}
|
|
175
|
+
/** `bare`: identifiers that ARE props (a component's own destructured names), read as `props.<name>`. */
|
|
176
|
+
function parseExpr(src, bare) {
|
|
177
|
+
const toks = tokenize(src);
|
|
178
|
+
let p = 0;
|
|
179
|
+
const peek = () => toks[p];
|
|
180
|
+
const isOp = (v) => peek().t === "op" && peek().v === v;
|
|
181
|
+
const eat = (v) => {
|
|
182
|
+
if (!isOp(v))
|
|
183
|
+
throw new Refuse(`expected '${v}'`);
|
|
184
|
+
p++;
|
|
185
|
+
};
|
|
186
|
+
const primary = () => {
|
|
187
|
+
const t = toks[p++];
|
|
188
|
+
switch (t.t) {
|
|
189
|
+
case "str":
|
|
190
|
+
return { k: "str", v: t.v };
|
|
191
|
+
case "num":
|
|
192
|
+
return { k: "num", v: t.v };
|
|
193
|
+
case "tpl":
|
|
194
|
+
return { k: "tpl", parts: t.parts };
|
|
195
|
+
case "jsx":
|
|
196
|
+
return { k: "jsx", v: t.v };
|
|
197
|
+
case "id": {
|
|
198
|
+
if (t.v === "true" || t.v === "false")
|
|
199
|
+
return { k: "bool", v: t.v === "true" };
|
|
200
|
+
if (t.v === "null" || t.v === "undefined")
|
|
201
|
+
return { k: "none" };
|
|
202
|
+
if (bare?.has(t.v)) {
|
|
203
|
+
if (isOp(".") || isOp("?.") || isOp("("))
|
|
204
|
+
throw new Refuse("member access or call on a prop");
|
|
205
|
+
return { k: "prop", name: t.v };
|
|
206
|
+
}
|
|
207
|
+
// A component template has no `props` of its own: its names are `bare`.
|
|
208
|
+
if (t.v !== "props" || bare)
|
|
209
|
+
throw new Refuse(`identifier '${t.v}'`);
|
|
210
|
+
if (!(isOp(".") || isOp("?.")))
|
|
211
|
+
throw new Refuse("bare props");
|
|
212
|
+
p++;
|
|
213
|
+
const name = toks[p++];
|
|
214
|
+
if (name.t !== "id")
|
|
215
|
+
throw new Refuse("props.<name>");
|
|
216
|
+
let e = { k: "prop", name: name.v };
|
|
217
|
+
// props.x.includes(y) → y in x
|
|
218
|
+
if ((isOp(".") || isOp("?.")) && toks[p + 1]?.t === "id" && toks[p + 1].v === "includes") {
|
|
219
|
+
p += 2;
|
|
220
|
+
eat("(");
|
|
221
|
+
const needle = expr();
|
|
222
|
+
eat(")");
|
|
223
|
+
e = { k: "in", needle, hay: e };
|
|
224
|
+
}
|
|
225
|
+
if (isOp(".") || isOp("?.") || isOp("("))
|
|
226
|
+
throw new Refuse("member access or call beyond props.<name>");
|
|
227
|
+
return e;
|
|
228
|
+
}
|
|
229
|
+
case "op":
|
|
230
|
+
if (t.v === "(") {
|
|
231
|
+
const e = expr();
|
|
232
|
+
eat(")");
|
|
233
|
+
return e;
|
|
234
|
+
}
|
|
235
|
+
if (t.v === "!")
|
|
236
|
+
return { k: "not", e: primary() };
|
|
237
|
+
}
|
|
238
|
+
throw new Refuse("unexpected token");
|
|
239
|
+
};
|
|
240
|
+
const equality = () => {
|
|
241
|
+
let l = primary();
|
|
242
|
+
while (isOp("===") || isOp("==") || isOp("!==") || isOp("!=")) {
|
|
243
|
+
const op = toks[p++].v.startsWith("!") ? "!=" : "==";
|
|
244
|
+
l = { k: "bin", op, l, r: primary() };
|
|
245
|
+
}
|
|
246
|
+
return l;
|
|
247
|
+
};
|
|
248
|
+
const and = () => {
|
|
249
|
+
let l = equality();
|
|
250
|
+
while (isOp("&&")) {
|
|
251
|
+
p++;
|
|
252
|
+
l = { k: "bin", op: "and", l, r: equality() };
|
|
253
|
+
}
|
|
254
|
+
return l;
|
|
255
|
+
};
|
|
256
|
+
const or = () => {
|
|
257
|
+
let l = and();
|
|
258
|
+
while (isOp("||") || isOp("??")) {
|
|
259
|
+
const op = toks[p++].v === "??" ? "default" : "or";
|
|
260
|
+
l = { k: "bin", op, l, r: and() };
|
|
261
|
+
}
|
|
262
|
+
return l;
|
|
263
|
+
};
|
|
264
|
+
function expr() {
|
|
265
|
+
const test = or();
|
|
266
|
+
if (isOp("?")) {
|
|
267
|
+
p++;
|
|
268
|
+
const then = expr();
|
|
269
|
+
eat(":");
|
|
270
|
+
return { k: "cond", test, then, else: expr() };
|
|
271
|
+
}
|
|
272
|
+
return test;
|
|
273
|
+
}
|
|
274
|
+
const e = expr();
|
|
275
|
+
if (peek().t !== "end")
|
|
276
|
+
throw new Refuse("trailing input");
|
|
277
|
+
return e;
|
|
278
|
+
}
|
|
279
|
+
// ─── Jinja emission ───────────────────────────────────────────────────────
|
|
280
|
+
/**
|
|
281
|
+
* A Jinja string literal that needs no backslash escape for quotes.
|
|
282
|
+
*
|
|
283
|
+
* The directive text passes through the Markdown parser before it reaches
|
|
284
|
+
* the template engine, and Markdown reads `\"` as an escaped quote — it
|
|
285
|
+
* drops the backslash, and `"a \"b\" c"` arrived as `"a "b" c"`, a syntax
|
|
286
|
+
* error (the first Cloudflare conversion run failed on exactly this). So a
|
|
287
|
+
* value is quoted with whichever quote it does not contain, or split and
|
|
288
|
+
* joined with `~` when it contains both. Newlines and tabs stay `\n`/`\t`:
|
|
289
|
+
* Markdown leaves a backslash before a letter alone. A literal backslash
|
|
290
|
+
* cannot be written safely at all, so it is refused.
|
|
291
|
+
*/
|
|
292
|
+
function jinjaString(v) {
|
|
293
|
+
if (v.includes("\\"))
|
|
294
|
+
throw new Refuse("a backslash in a string value");
|
|
295
|
+
const body = (x) => x.replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
|
|
296
|
+
if (!v.includes('"'))
|
|
297
|
+
return `"${body(v)}"`;
|
|
298
|
+
if (!v.includes("'"))
|
|
299
|
+
return `'${body(v)}'`;
|
|
300
|
+
return v.split('"').map((part) => `"${body(part)}"`).join(` ~ '"' ~ `);
|
|
301
|
+
}
|
|
302
|
+
/** A value expression in Jinja source. JSX values are not values. */
|
|
303
|
+
function value(e, names) {
|
|
304
|
+
switch (e.k) {
|
|
305
|
+
case "prop":
|
|
306
|
+
names.add(e.name);
|
|
307
|
+
return e.name;
|
|
308
|
+
case "str":
|
|
309
|
+
return jinjaString(e.v);
|
|
310
|
+
case "num":
|
|
311
|
+
return e.v;
|
|
312
|
+
case "bool":
|
|
313
|
+
return e.v ? "true" : "false";
|
|
314
|
+
case "none":
|
|
315
|
+
return "none";
|
|
316
|
+
case "tpl":
|
|
317
|
+
return e.parts.length === 0 ? '""' : e.parts.map((x) => (typeof x === "string" ? jinjaString(x) : `(${value(x, names)})`)).join(" ~ ");
|
|
318
|
+
case "not":
|
|
319
|
+
return `not (${value(e.e, names)})`;
|
|
320
|
+
case "in":
|
|
321
|
+
return `(${value(e.needle, names)}) in (${value(e.hay, names)})`;
|
|
322
|
+
case "bin":
|
|
323
|
+
if (e.op === "default")
|
|
324
|
+
return `(${value(e.l, names)}) | default(${value(e.r, names)})`;
|
|
325
|
+
return `(${value(e.l, names)}) ${e.op} (${value(e.r, names)})`;
|
|
326
|
+
case "cond":
|
|
327
|
+
return `(${value(e.then, names)}) if (${value(e.test, names)}) else (${value(e.else, names)})`;
|
|
328
|
+
case "jsx":
|
|
329
|
+
throw new Refuse("JSX in a value position");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** A directive's source, as a token when a table is in use. */
|
|
333
|
+
function emit(src, stats) {
|
|
334
|
+
return stats.table ? stats.table.token(src) : src;
|
|
335
|
+
}
|
|
336
|
+
/** Output for a branch of a text-position conditional: literal text, JSX (converted recursively), or a value. */
|
|
337
|
+
function branch(e, stats, layout) {
|
|
338
|
+
if (e.k === "str")
|
|
339
|
+
return e.v;
|
|
340
|
+
if (e.k === "jsx") {
|
|
341
|
+
const inner = e.v.startsWith("<>") && e.v.endsWith("</>") ? e.v.slice(2, -3) : e.v;
|
|
342
|
+
const converted = convertText(layout.block ? dedent(inner) : inner, stats);
|
|
343
|
+
return layout.block ? converted.replace(/^\s*\n|\n\s*$/g, "") : converted;
|
|
344
|
+
}
|
|
345
|
+
if (e.k === "cond")
|
|
346
|
+
return statement(e, stats, layout);
|
|
347
|
+
if (e.k === "bin" && e.op === "and" && (e.r.k === "jsx" || e.r.k === "str"))
|
|
348
|
+
return statement(e, stats, layout);
|
|
349
|
+
return emit(`{{ ${value(e, stats.names)} }}`, stats);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Remove the common leading indentation of a JSX branch body. In JSX it
|
|
353
|
+
* means nothing; in Markdown four spaces make an indented CODE block, and a
|
|
354
|
+
* branch of `<strong>`/`<ul>` markup came out as a `plaintext` code sample.
|
|
355
|
+
*/
|
|
356
|
+
function dedent(s) {
|
|
357
|
+
const lines = s.split("\n");
|
|
358
|
+
// The first line is left out of the minimum: it follows `(` or `&&` on
|
|
359
|
+
// the expression's own line, so its column says nothing about the body's
|
|
360
|
+
// (`{props.a && (<div>\n body` must still lose the 8).
|
|
361
|
+
const widths = lines.slice(1).filter((l) => l.trim()).map((l) => /^[ \t]*/.exec(l)[0].length);
|
|
362
|
+
const min = widths.length ? Math.min(...widths) : 0;
|
|
363
|
+
return min ? lines.map((l) => l.slice(Math.min(min, /^[ \t]*/.exec(l)[0].length))).join("\n") : s;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* A text-position expression: `{% if %}` for conditionals, `{{ }}` for
|
|
367
|
+
* values. A block (multi-line) conditional puts each directive on its own
|
|
368
|
+
* line with blank lines around it, at the expression's indent — so no
|
|
369
|
+
* Markdown block (an HTML block, an unmapped component) can swallow the
|
|
370
|
+
* `{% endif %}` that follows its body.
|
|
371
|
+
*/
|
|
372
|
+
function statement(e, stats, layout) {
|
|
373
|
+
const sep = layout.block ? `\n\n${layout.indent}` : "";
|
|
374
|
+
const d = (src) => `${sep}${emit(src, stats)}${sep}`;
|
|
375
|
+
// No whitespace control: a conditional on a list or tab marker line is
|
|
376
|
+
// handled after rendering (TemplatePages joins an emptied marker line with
|
|
377
|
+
// its content). Trimming after the tags ate the NEXT line's indent whenever
|
|
378
|
+
// a conditional rendered nothing, and a tabs closer lost column 0.
|
|
379
|
+
if (e.k === "cond") {
|
|
380
|
+
const tail = e.else.k === "cond" ? `${d("{% else %}")}${statement(e.else, stats, layout)}` : `${d("{% else %}")}${branch(e.else, stats, layout)}`;
|
|
381
|
+
return `${d(`{% if ${value(e.test, stats.names)} %}`)}${branch(e.then, stats, layout)}${tail}${d("{% endif %}")}`;
|
|
382
|
+
}
|
|
383
|
+
if (e.k === "bin" && e.op === "and" && (e.r.k === "jsx" || e.r.k === "str")) {
|
|
384
|
+
return `${d(`{% if ${value(e.l, stats.names)} %}`)}${branch(e.r, stats, layout)}${d("{% endif %}")}`;
|
|
385
|
+
}
|
|
386
|
+
return emit(`{{ ${value(e, stats.names)} }}`, stats);
|
|
387
|
+
}
|
|
388
|
+
/** An attribute value: literal text when constant, else a `{{ }}` in quotes. */
|
|
389
|
+
function attribute(e, stats) {
|
|
390
|
+
const names = stats.names;
|
|
391
|
+
if (e.k === "str")
|
|
392
|
+
return JSON.stringify(e.v);
|
|
393
|
+
if (e.k === "tpl" && e.parts.every((x) => typeof x === "string"))
|
|
394
|
+
return JSON.stringify(e.parts.join(""));
|
|
395
|
+
if (e.k === "tpl") {
|
|
396
|
+
return JSON.stringify(e.parts.map((x) => (typeof x === "string" ? x : emit(`{{ ${value(x, names)} }}`, stats))).join(""));
|
|
397
|
+
}
|
|
398
|
+
return JSON.stringify(emit(`{{ ${value(e, names)} }}`, stats));
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Cloudflare's `<Markdown text={…} />` renders its text as Markdown. When the
|
|
402
|
+
* text is a string or template literal (the partials' shape), the tag is
|
|
403
|
+
* replaced by the text itself, dedented, with `${props.x}` as `{{ x }}`.
|
|
404
|
+
* Unmapped, it was dropped in both import modes. Returns undefined when the
|
|
405
|
+
* tag is not that shape.
|
|
406
|
+
*/
|
|
407
|
+
function markdownTag(src, at, stats) {
|
|
408
|
+
// `text="…"` (a plain attribute): its value IS the Markdown. Only the
|
|
409
|
+
// braced form was read, so `{ !props.x && <Markdown text="…" /> }` leaked.
|
|
410
|
+
const quoted = /^<Markdown\s+text=(?:"([^"]*)"|'([^']*)')((?:\s+[\w:-]+(?:=(?:"[^"]*"|'[^']*'|\{[^{}]*\}))?)*)\s*\/>/.exec(src.slice(at));
|
|
411
|
+
if (quoted) {
|
|
412
|
+
stats.markdownInlined++;
|
|
413
|
+
return { end: at + quoted[0].length, text: (quoted[1] ?? quoted[2]).trim() };
|
|
414
|
+
}
|
|
415
|
+
const head = /^<Markdown\s+text=\{/.exec(src.slice(at));
|
|
416
|
+
if (!head)
|
|
417
|
+
return undefined;
|
|
418
|
+
const open = at + head[0].length - 1;
|
|
419
|
+
const close = matchBrace(src, open);
|
|
420
|
+
if (close < 0)
|
|
421
|
+
return undefined;
|
|
422
|
+
const tagEnd = src.indexOf("/>", close);
|
|
423
|
+
if (tagEnd < 0 || /[<>]/.test(src.slice(close + 1, tagEnd)))
|
|
424
|
+
return undefined;
|
|
425
|
+
let e;
|
|
426
|
+
try {
|
|
427
|
+
e = parseExpr(src.slice(open + 1, close));
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
if (err instanceof Refuse)
|
|
431
|
+
return undefined;
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
// `text={props.x}` (and `props.x || ""`): the parameter's own Markdown, as
|
|
435
|
+
// a value. It is Markdown already, and the rendered page is parsed as
|
|
436
|
+
// Markdown, so `{{ x }}` renders as the component did. 150+ of these leaked
|
|
437
|
+
// as raw tags in partials.
|
|
438
|
+
if (e.k !== "str" && e.k !== "tpl") {
|
|
439
|
+
// Only what prints TEXT: a prop, or `a || "b"` / `a ?? "b"`. `a && b` and
|
|
440
|
+
// comparisons would print `false`/`true` (Jinja returns the operand).
|
|
441
|
+
const printsText = (x) => x.k === "prop" || x.k === "str" || (x.k === "bin" && (x.op === "or" || x.op === "default") && printsText(x.l) && printsText(x.r));
|
|
442
|
+
if (!printsText(e))
|
|
443
|
+
return undefined;
|
|
444
|
+
let v;
|
|
445
|
+
try {
|
|
446
|
+
v = value(e, stats.names);
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
if (err instanceof Refuse)
|
|
450
|
+
return undefined;
|
|
451
|
+
throw err;
|
|
452
|
+
}
|
|
453
|
+
stats.markdownInlined++;
|
|
454
|
+
return { end: tagEnd + 2, text: emit(`{{ ${v} }}`, stats) };
|
|
455
|
+
}
|
|
456
|
+
const raw = e.k === "str" ? e.v : e.parts.map((x) => (typeof x === "string" ? x : emit(`{{ ${value(x, stats.names)} }}`, stats))).join("");
|
|
457
|
+
const lines = raw.replace(/^\n+|\s+$/g, "").split("\n");
|
|
458
|
+
const indent = Math.min(...lines.filter((l) => l.trim()).map((l) => /^\s*/.exec(l)[0].length));
|
|
459
|
+
// A tag on its own indented line (inside a JSX branch): its continuation
|
|
460
|
+
// lines take that indent too, as the first line does by standing after it.
|
|
461
|
+
// At column 0 they broke the branch dedent — a table's first row kept a
|
|
462
|
+
// tab, its other rows had none, and the table was lost.
|
|
463
|
+
const lineStart = src.lastIndexOf("\n", at - 1) + 1;
|
|
464
|
+
const lead = /^[ \t]*$/.test(src.slice(lineStart, at)) ? src.slice(lineStart, at) : "";
|
|
465
|
+
stats.markdownInlined++;
|
|
466
|
+
return {
|
|
467
|
+
end: tagEnd + 2,
|
|
468
|
+
text: lines.map((l, n) => (n && l.trim() ? lead : "") + l.slice(Number.isFinite(indent) ? indent : 0)).join("\n"),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
// ─── Scanning the MDX body ────────────────────────────────────────────────
|
|
472
|
+
/** Index of the `}` matching the `{` at `open`, skipping strings, template literals and JSX values. */
|
|
473
|
+
function matchBrace(src, open) {
|
|
474
|
+
let depth = 0;
|
|
475
|
+
for (let i = open; i < src.length; i++) {
|
|
476
|
+
const c = src[i];
|
|
477
|
+
// Comments: `{/* note, <Aside> below */}` is not JSX, and `{/* don't */}`
|
|
478
|
+
// has no string in it.
|
|
479
|
+
if (c === "/" && src[i + 1] === "*") {
|
|
480
|
+
const end = src.indexOf("*/", i + 2);
|
|
481
|
+
if (end < 0)
|
|
482
|
+
return -1;
|
|
483
|
+
i = end + 1;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (c === "/" && src[i + 1] === "/" && i > open) {
|
|
487
|
+
const nl = src.indexOf("\n", i);
|
|
488
|
+
if (nl < 0)
|
|
489
|
+
return -1;
|
|
490
|
+
i = nl;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
494
|
+
const q = c;
|
|
495
|
+
i++;
|
|
496
|
+
while (i < src.length && src[i] !== q) {
|
|
497
|
+
if (src[i] === "\\")
|
|
498
|
+
i++;
|
|
499
|
+
else if (q === "`" && src[i] === "$" && src[i + 1] === "{") {
|
|
500
|
+
const end = matchBrace(src, i + 1);
|
|
501
|
+
if (end < 0)
|
|
502
|
+
return -1;
|
|
503
|
+
i = end;
|
|
504
|
+
}
|
|
505
|
+
i++;
|
|
506
|
+
}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
// JSX in a value position (`&& (<>…</>)`, `? <p>…</p> :`): its TEXT is
|
|
510
|
+
// not JavaScript. An apostrophe there ("Cloudflare's") was read as a
|
|
511
|
+
// string opener, the brace never closed, and the rest of the partial was
|
|
512
|
+
// copied through unconverted (its `<Markdown>`/`<AnchorHeading>` leaked).
|
|
513
|
+
if (c === "<" && /[A-Za-z>]/.test(src[i + 1] ?? "") && /[(?:,&|{[]$/.test(src.slice(open, i).trimEnd())) {
|
|
514
|
+
try {
|
|
515
|
+
i = jsxEnd(src, i) - 1;
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return -1;
|
|
519
|
+
}
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (c === "{")
|
|
523
|
+
depth++;
|
|
524
|
+
else if (c === "}") {
|
|
525
|
+
depth--;
|
|
526
|
+
if (depth === 0)
|
|
527
|
+
return i;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return -1;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Convert every `{…}` over props in a text span (no code fences). Inline code
|
|
534
|
+
* spans are skipped: `{props.x}` inside backticks is literal in MDX too.
|
|
535
|
+
* `<Render …/>` tags are left alone — their `params={{…}}` belong to the
|
|
536
|
+
* Render conversion.
|
|
537
|
+
*/
|
|
538
|
+
function convertText(src, stats) {
|
|
539
|
+
let out = "";
|
|
540
|
+
let i = 0;
|
|
541
|
+
while (i < src.length) {
|
|
542
|
+
const c = src[i];
|
|
543
|
+
if (c === "`") {
|
|
544
|
+
const run = /^`+/.exec(src.slice(i))[0];
|
|
545
|
+
const close = src.indexOf(run, i + run.length);
|
|
546
|
+
if (close < 0) {
|
|
547
|
+
out += src.slice(i);
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
out += src.slice(i, close + run.length);
|
|
551
|
+
i = close + run.length;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (src.startsWith("<Markdown", i)) {
|
|
555
|
+
const md = markdownTag(src, i, stats);
|
|
556
|
+
if (md) {
|
|
557
|
+
out += md.text;
|
|
558
|
+
i = md.end;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (src.startsWith("<Render", i)) {
|
|
563
|
+
const end = src.indexOf("/>", i);
|
|
564
|
+
if (end >= 0) {
|
|
565
|
+
out += src.slice(i, end + 2);
|
|
566
|
+
i = end + 2;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (c !== "{") {
|
|
571
|
+
out += c;
|
|
572
|
+
i++;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const close = matchBrace(src, i);
|
|
576
|
+
if (close < 0) {
|
|
577
|
+
out += src.slice(i);
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
const whole = src.slice(i, close + 1);
|
|
581
|
+
const inner = whole.slice(1, -1);
|
|
582
|
+
if (!/\bprops\b/.test(inner) || inner.trim().startsWith("/*")) {
|
|
583
|
+
out += whole;
|
|
584
|
+
i = close + 1;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
// Attribute position: `name={…}` inside an open JSX tag (`<a href={…}`).
|
|
588
|
+
// Prose like `CLOUDFLARE_ACCOUNT_ID={props.id}` is NOT an attribute: it
|
|
589
|
+
// would gain literal quotes the source never rendered.
|
|
590
|
+
const inTag = /<[A-Za-z][^<>]*$/.test(out);
|
|
591
|
+
const attr = inTag ? /([A-Za-z_:][\w:.-]*)\s*=\s*$/.exec(out) : null;
|
|
592
|
+
try {
|
|
593
|
+
const e = parseExpr(inner);
|
|
594
|
+
if (attr) {
|
|
595
|
+
out = out.slice(0, out.length - attr[0].length) + `${attr[1]}=` + attribute(e, stats);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
const lineStart = out.lastIndexOf("\n") + 1;
|
|
599
|
+
const indent = /^[ \t]*/.exec(out.slice(lineStart))[0];
|
|
600
|
+
// A conditional alone on its line with a JSX branch is a BLOCK too:
|
|
601
|
+
// `{ props.p === "x" && <><AnchorHeading … /></> }` kept its heading
|
|
602
|
+
// glued between `{% if %}` and `{% endif %}`, where it could not be
|
|
603
|
+
// a heading and leaked as a tag.
|
|
604
|
+
const alone = out.slice(lineStart) === indent && /^[ \t]*(\n|$)/.test(src.slice(close + 1));
|
|
605
|
+
// Only a BLOCK-level branch: `<Markdown text={`- item`}>` after a list
|
|
606
|
+
// item, or `<em>` mid-paragraph, must stay on the line (blank lines
|
|
607
|
+
// made the list loose and split the paragraph — review).
|
|
608
|
+
const blockJsx = /^(?:<>\s*)?<(?:AnchorHeading|Aside|Details|Tabs|Steps|CardGrid|LinkTitleCard|LinkCard|LinkButton|Card|Code|h[1-6]|table|ul|ol|div)\b/;
|
|
609
|
+
const jsxBranch = (x) => (x.k === "jsx" && blockJsx.test(x.v.trim())) ||
|
|
610
|
+
(x.k === "bin" && x.op === "and" && jsxBranch(x.r)) ||
|
|
611
|
+
(x.k === "cond" && (jsxBranch(x.then) || jsxBranch(x.else)));
|
|
612
|
+
out += statement(e, stats, { block: inner.includes("\n") || (alone && jsxBranch(e)), indent });
|
|
613
|
+
}
|
|
614
|
+
stats.converted++;
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
if (!(err instanceof Refuse))
|
|
618
|
+
throw err;
|
|
619
|
+
stats.refused++;
|
|
620
|
+
if (stats.refusedSamples.length < 20)
|
|
621
|
+
stats.refusedSamples.push(whole.replace(/\s+/g, " ").slice(0, 160));
|
|
622
|
+
out += whole;
|
|
623
|
+
}
|
|
624
|
+
i = close + 1;
|
|
625
|
+
}
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Convert a partial's MDX body. Fenced code blocks are copied verbatim;
|
|
630
|
+
* everything else goes through `convertText`.
|
|
631
|
+
*/
|
|
632
|
+
export function jsxPropsToJinja(body, stats) {
|
|
633
|
+
// Directive tokens are restored in the tree (template-tokens.ts).
|
|
634
|
+
const lines = body.split("\n");
|
|
635
|
+
const out = [];
|
|
636
|
+
let fence = null;
|
|
637
|
+
let buf = [];
|
|
638
|
+
const flush = () => {
|
|
639
|
+
if (buf.length)
|
|
640
|
+
out.push(convertText(buf.join("\n"), stats));
|
|
641
|
+
buf = [];
|
|
642
|
+
};
|
|
643
|
+
for (const line of lines) {
|
|
644
|
+
const m = /^\s*(`{3,}|~{3,})/.exec(line);
|
|
645
|
+
if (fence) {
|
|
646
|
+
out.push(line);
|
|
647
|
+
if (m && m[1][0] === fence[0] && m[1].length >= fence.length && line.trim() === m[1])
|
|
648
|
+
fence = null;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (m) {
|
|
652
|
+
flush();
|
|
653
|
+
fence = m[1];
|
|
654
|
+
out.push(line);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
buf.push(line);
|
|
658
|
+
}
|
|
659
|
+
flush();
|
|
660
|
+
return out.join("\n");
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* A JS literal from `params={{ … }}` as a Jinja expression, or undefined when
|
|
664
|
+
* it is outside the subset (the caller then inlines the partial as before).
|
|
665
|
+
*/
|
|
666
|
+
export function jsValueToJinja(src, names) {
|
|
667
|
+
try {
|
|
668
|
+
const trimmed = src.trim();
|
|
669
|
+
// Arrays of literals: ["a", "b"] → ["a", "b"]
|
|
670
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
671
|
+
const items = splitTopLevel(trimmed.slice(1, -1));
|
|
672
|
+
const vals = items.filter((x) => x.trim()).map((x) => jsValueToJinja(x, names));
|
|
673
|
+
return vals.some((v) => v === undefined) ? undefined : `[${vals.join(", ")}]`;
|
|
674
|
+
}
|
|
675
|
+
const e = parseExpr(trimmed);
|
|
676
|
+
if (e.k === "jsx")
|
|
677
|
+
return undefined;
|
|
678
|
+
return value(e, names ?? new Set());
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
if (err instanceof Refuse)
|
|
682
|
+
return undefined;
|
|
683
|
+
throw err;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/** Split `a, b, "c, d"` at top-level commas (strings, brackets and braces respected). */
|
|
687
|
+
export function splitTopLevel(src) {
|
|
688
|
+
const parts = [];
|
|
689
|
+
let depth = 0;
|
|
690
|
+
let cur = "";
|
|
691
|
+
let q = null;
|
|
692
|
+
for (let i = 0; i < src.length; i++) {
|
|
693
|
+
const c = src[i];
|
|
694
|
+
if (q) {
|
|
695
|
+
cur += c;
|
|
696
|
+
if (c === "\\")
|
|
697
|
+
cur += src[++i] ?? "";
|
|
698
|
+
else if (c === q)
|
|
699
|
+
q = null;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (c === '"' || c === "'" || c === "`")
|
|
703
|
+
q = c;
|
|
704
|
+
else if (c === "{" || c === "[" || c === "(")
|
|
705
|
+
depth++;
|
|
706
|
+
else if (c === "}" || c === "]" || c === ")")
|
|
707
|
+
depth--;
|
|
708
|
+
else if (c === "," && depth === 0) {
|
|
709
|
+
parts.push(cur);
|
|
710
|
+
cur = "";
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
cur += c;
|
|
714
|
+
}
|
|
715
|
+
if (cur.trim())
|
|
716
|
+
parts.push(cur);
|
|
717
|
+
return parts;
|
|
718
|
+
}
|
|
719
|
+
// ─── Static rendering of a component template ─────────────────────────────
|
|
720
|
+
/**
|
|
721
|
+
* Render an Astro component's TEMPLATE for known props — read, never run:
|
|
722
|
+
* `{cond && (<p>…</p>)}`, `{a === "x" ? (A) : (B)}`, `attr={cond ? "a" : "b"}`,
|
|
723
|
+
* `{"text"}`, fragments, comments — the JS subset `parseExpr` reads, with
|
|
724
|
+
* the component's own prop names (`cli`, not `props.cli`). `inline` may
|
|
725
|
+
* replace a self-closing sub-component with its own rendering. Undefined
|
|
726
|
+
* when anything is outside the subset (the caller leaves the tag reported).
|
|
727
|
+
*/
|
|
728
|
+
export function renderComponentTemplate(template, props, inline) {
|
|
729
|
+
// Prop names are resolved by the TOKENIZER (a regex rename also rewrote
|
|
730
|
+
// the word in a JSX branch's text).
|
|
731
|
+
const names = new Set(Object.keys(props));
|
|
732
|
+
const truthy = (v) => !(v === undefined || v === null || v === false || v === 0 || v === "");
|
|
733
|
+
const evalExpr = (e) => {
|
|
734
|
+
switch (e.k) {
|
|
735
|
+
case "prop":
|
|
736
|
+
return props[e.name];
|
|
737
|
+
case "str":
|
|
738
|
+
return e.v;
|
|
739
|
+
case "num":
|
|
740
|
+
return Number(e.v);
|
|
741
|
+
case "bool":
|
|
742
|
+
return e.v;
|
|
743
|
+
case "none":
|
|
744
|
+
return undefined;
|
|
745
|
+
case "tpl":
|
|
746
|
+
return e.parts.map((x) => (typeof x === "string" ? x : String(evalExpr(x) ?? ""))).join("");
|
|
747
|
+
case "jsx":
|
|
748
|
+
return { jsx: e.v };
|
|
749
|
+
case "not":
|
|
750
|
+
return !truthy(evalExpr(e.e));
|
|
751
|
+
case "in": {
|
|
752
|
+
const hay = evalExpr(e.hay);
|
|
753
|
+
return Array.isArray(hay) || typeof hay === "string" ? hay.includes(evalExpr(e.needle)) : false;
|
|
754
|
+
}
|
|
755
|
+
case "cond":
|
|
756
|
+
return truthy(evalExpr(e.test)) ? evalExpr(e.then) : evalExpr(e.else);
|
|
757
|
+
case "bin": {
|
|
758
|
+
const l = evalExpr(e.l);
|
|
759
|
+
if (e.op === "and")
|
|
760
|
+
return truthy(l) ? evalExpr(e.r) : l;
|
|
761
|
+
if (e.op === "or")
|
|
762
|
+
return truthy(l) ? l : evalExpr(e.r);
|
|
763
|
+
if (e.op === "default")
|
|
764
|
+
return l ?? evalExpr(e.r);
|
|
765
|
+
const r = evalExpr(e.r);
|
|
766
|
+
return e.op === "==" ? l === r : l !== r;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<");
|
|
771
|
+
const render = (src, depth) => {
|
|
772
|
+
if (depth > 8)
|
|
773
|
+
throw new Refuse("nesting");
|
|
774
|
+
let out = "";
|
|
775
|
+
let i = 0;
|
|
776
|
+
while (i < src.length) {
|
|
777
|
+
const c = src[i];
|
|
778
|
+
if (c !== "{") {
|
|
779
|
+
out += c;
|
|
780
|
+
i++;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const close = matchBrace(src, i);
|
|
784
|
+
if (close < 0)
|
|
785
|
+
throw new Refuse("unbalanced");
|
|
786
|
+
const inner = src.slice(i + 1, close).trim();
|
|
787
|
+
i = close + 1;
|
|
788
|
+
if (/^\/\*[\s\S]*\*\/$/.test(inner))
|
|
789
|
+
continue;
|
|
790
|
+
const attr = /(?:^|\s)([A-Za-z_:][\w:.-]*)\s*=\s*$/.exec(out);
|
|
791
|
+
// Inside an open tag, quoted values skipped (a `>` in `title="a>b"` is not the tag's end).
|
|
792
|
+
const inTag = /<[A-Za-z][\w:.-]*(?:\s+[^\s"'<>=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'))?)*\s+[^\s"'<>=]+\s*=\s*$/.test(out);
|
|
793
|
+
if (attr && !inTag && /<[A-Za-z][^<]*$/.test(out))
|
|
794
|
+
throw new Refuse("attribute position unclear");
|
|
795
|
+
const v = evalExpr(parseExpr(inner, names));
|
|
796
|
+
if (inTag && attr) {
|
|
797
|
+
// A falsy value drops the attribute, and the space before it.
|
|
798
|
+
if (attr[1].startsWith("set:"))
|
|
799
|
+
throw new Refuse("set: directive");
|
|
800
|
+
const sep = /^\s/.test(attr[0]) ? " " : "";
|
|
801
|
+
out = out.slice(0, out.length - attr[0].length);
|
|
802
|
+
// Astro drops an attribute only for false, null and undefined (0 and "" render).
|
|
803
|
+
if (v === true)
|
|
804
|
+
out += sep + attr[1];
|
|
805
|
+
else if (v !== false && v !== null && v !== undefined) {
|
|
806
|
+
if (typeof v === "object")
|
|
807
|
+
throw new Refuse("object attribute");
|
|
808
|
+
out += `${sep}${attr[1]}="${String(v).replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """)}"`;
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (v && typeof v === "object" && "jsx" in v) {
|
|
813
|
+
const jsx = String(v.jsx).replace(/^<>/, "").replace(/<\/>$/, "");
|
|
814
|
+
out += render(jsx, depth + 1);
|
|
815
|
+
}
|
|
816
|
+
else if (typeof v === "string" || typeof v === "number")
|
|
817
|
+
out += esc(String(v));
|
|
818
|
+
}
|
|
819
|
+
return out;
|
|
820
|
+
};
|
|
821
|
+
let html;
|
|
822
|
+
try {
|
|
823
|
+
html = render(template, 0);
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
if (err instanceof Refuse)
|
|
827
|
+
return undefined;
|
|
828
|
+
throw err;
|
|
829
|
+
}
|
|
830
|
+
html = html.replace(/<\/?>/g, "");
|
|
831
|
+
if (!inline)
|
|
832
|
+
return html;
|
|
833
|
+
const tagRe = /<([A-Z]\w*)\b((?:[^<>"'{]|"[^"]*"|'[^']*'|\{[^{}]*\})*)\/>/g;
|
|
834
|
+
html = html.replace(tagRe, (whole, tag, attrStr) => {
|
|
835
|
+
const attrs = {};
|
|
836
|
+
for (const m of attrStr.matchAll(/([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g))
|
|
837
|
+
attrs[m[1]] = m[2] ?? m[3] ?? true;
|
|
838
|
+
const r = inline(tag, attrs);
|
|
839
|
+
if (r === undefined)
|
|
840
|
+
return whole;
|
|
841
|
+
return r;
|
|
842
|
+
});
|
|
843
|
+
return html;
|
|
844
|
+
}
|
|
845
|
+
/** `const { a, b = false } = Astro.props;` → the prop names and their defaults (literals only). */
|
|
846
|
+
export function componentPropDefaults(frontmatter) {
|
|
847
|
+
const m = /const\s*\{([^}]*)\}\s*=\s*Astro\.props/.exec(frontmatter);
|
|
848
|
+
if (!m)
|
|
849
|
+
return {};
|
|
850
|
+
const out = {};
|
|
851
|
+
for (const part of splitTopLevel(m[1])) {
|
|
852
|
+
const eq = part.indexOf("=");
|
|
853
|
+
const name = (eq < 0 ? part : part.slice(0, eq)).trim();
|
|
854
|
+
const def = eq < 0 ? undefined : part.slice(eq + 1).trim();
|
|
855
|
+
if (!/^\w+$/.test(name))
|
|
856
|
+
return undefined;
|
|
857
|
+
if (def === undefined) {
|
|
858
|
+
out[name] = undefined;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
// A default it cannot read makes the whole component unreadable: a
|
|
862
|
+
// guessed `undefined` would render the wrong branch silently.
|
|
863
|
+
const lit = def === "false" ? false : def === "true" ? true : def === "null" ? null : /^"[^"]*"$|^'[^']*'$/.test(def) ? def.slice(1, -1) : /^-?\d+(\.\d+)?$/.test(def) ? Number(def) : Symbol.for("unreadable");
|
|
864
|
+
if (typeof lit === "symbol")
|
|
865
|
+
return undefined;
|
|
866
|
+
out[name] = lit;
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|