@dogsbay/format-starlight 0.2.0-beta.114 → 0.2.0-beta.116
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
package/dist/parse.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - Cloudflare custom components (Feature, Plan, Description, etc.) via customization layer
|
|
10
10
|
* - Standard CommonMark (headings, paragraphs, lists, tables, code, links)
|
|
11
11
|
*/
|
|
12
|
+
import { assignHeadingSlugs } from "./github-slug.js";
|
|
12
13
|
import MarkdownIt from "markdown-it";
|
|
13
14
|
import matter from "gray-matter";
|
|
14
15
|
const md = new MarkdownIt({ html: true, linkify: true });
|
|
@@ -389,7 +390,8 @@ function segmentsToTree(segments, componentMap, adapter) {
|
|
|
389
390
|
continue;
|
|
390
391
|
}
|
|
391
392
|
const props = mapping.propsFromAttrs?.(seg.attrs || {}) || {};
|
|
392
|
-
|
|
393
|
+
// Nested: the outermost call assigns ids once, over the whole page.
|
|
394
|
+
const children = seg.inner ? starlightToTree(seg.inner, { adapter, nested: true }) : [];
|
|
393
395
|
tree.push({ type: mapping.type, props, children });
|
|
394
396
|
}
|
|
395
397
|
}
|
|
@@ -694,7 +696,10 @@ export function starlightToTree(source, options) {
|
|
|
694
696
|
const segments = segmentMdx(cleaned, componentMap, selfClosing);
|
|
695
697
|
const tree = segmentsToTree(segments, componentMap, adapter);
|
|
696
698
|
const unwrapped = unwrapStepsOrderedList(tree);
|
|
697
|
-
|
|
699
|
+
const grouped = groupConsecutiveCards(unwrapped);
|
|
700
|
+
if (!options?.nested)
|
|
701
|
+
assignHeadingSlugs(grouped);
|
|
702
|
+
return grouped;
|
|
698
703
|
}
|
|
699
704
|
/**
|
|
700
705
|
* Starlight's <Steps> wraps a markdown ordered list, producing:
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protecting template directives through the MDX parse (template mode,
|
|
3
|
+
* plans/page-templates.md).
|
|
4
|
+
*
|
|
5
|
+
* The directives the converter emits — `{% with %}{% include %}`,
|
|
6
|
+
* `{{ x }}`, `{% if %}` — pass through the Starlight MDX parser and the
|
|
7
|
+
* Dogsbay-MD serializer on their way out. As plain text they were damaged
|
|
8
|
+
* twice over on the first Cloudflare run: Markdown read `\"` inside a
|
|
9
|
+
* string literal as an escaped quote, the serializer escaped `_` and `<`
|
|
10
|
+
* inside a condition, and an unmapped component block swallowed the
|
|
11
|
+
* `{% endif %}` line after it.
|
|
12
|
+
*
|
|
13
|
+
* So each directive travels as an opaque ASCII token (a number between
|
|
14
|
+
* two marker words) and is restored in the TREE after parsing:
|
|
15
|
+
* - a paragraph holding only tokens becomes a `directive-raw` block;
|
|
16
|
+
* - a token inside text becomes a `directive-raw-inline` node;
|
|
17
|
+
* - a token in any other string (an attribute, an HTML block) is spliced
|
|
18
|
+
* back as text.
|
|
19
|
+
* Both directive-raw forms serialize byte-exact.
|
|
20
|
+
*
|
|
21
|
+
* The same pass ESCAPES template openers the converter did not emit:
|
|
22
|
+
* `headers={{ … }}` from a leaked component, a JSON sample in prose. In a
|
|
23
|
+
* template page they would be syntax; escaped as `{{ "{{" }}` they print
|
|
24
|
+
* as before. Code (blocks and inline spans) is never touched: template
|
|
25
|
+
* pages keep it literal anyway.
|
|
26
|
+
*/
|
|
27
|
+
import type { TreeNode } from "@dogsbay/types";
|
|
28
|
+
export declare class DirectiveTable {
|
|
29
|
+
private readonly directives;
|
|
30
|
+
/** Register a directive's source; returns the token that stands for it. */
|
|
31
|
+
token(source: string): string;
|
|
32
|
+
get size(): number;
|
|
33
|
+
/** Replace tokens in a string by their directive source. */
|
|
34
|
+
splice(s: string): string;
|
|
35
|
+
/** Split a string into text runs and directive sources, in order. */
|
|
36
|
+
parts(s: string): Array<{
|
|
37
|
+
text: string;
|
|
38
|
+
} | {
|
|
39
|
+
directive: string;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
/** Escape template openers in a plain string (a title) so a template page prints them literally. */
|
|
43
|
+
export declare function escapeStrayOpeners(s: string): string;
|
|
44
|
+
export interface RestoreStats {
|
|
45
|
+
/** Stray template openers escaped so they print literally. */
|
|
46
|
+
escaped: number;
|
|
47
|
+
/**
|
|
48
|
+
* Directives that ended up inside code (an indented code block, say):
|
|
49
|
+
* restored as text, where a template page leaves them literal. Reported,
|
|
50
|
+
* because the reader sees `{% include %}` instead of the partial.
|
|
51
|
+
*/
|
|
52
|
+
inCode: number;
|
|
53
|
+
/**
|
|
54
|
+
* Fenced code blocks whose value directives are real (`<Code
|
|
55
|
+
* code={props.x} />` → `{{ x }}`): marked `{minja}` so they render.
|
|
56
|
+
*/
|
|
57
|
+
fencesMarked?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Restore directive tokens in a converted tree, and escape stray openers.
|
|
61
|
+
* Mutates the tree IN PLACE (and returns it).
|
|
62
|
+
*/
|
|
63
|
+
export declare function restoreDirectives(tree: TreeNode[], table: DirectiveTable, stats: RestoreStats): TreeNode[];
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// ASCII letters and digits only: the Starlight pipeline strips private-use
|
|
2
|
+
// characters (a first version used U+E030/U+E031 and every directive came
|
|
3
|
+
// out as its bare number), and Markdown neither escapes nor splits a word.
|
|
4
|
+
const OPEN = "DBTPLDIRx";
|
|
5
|
+
const CLOSE = "xDBTPLDIR";
|
|
6
|
+
const TOKEN = /DBTPLDIRx(\d+)xDBTPLDIR/g;
|
|
7
|
+
const BRACES_BEFORE_TOKEN = /\{+(?=DBTPLDIRx\d+xDBTPLDIR)/g;
|
|
8
|
+
/**
|
|
9
|
+
* A template opener that is not a `{#id}` attribute block. The attribute
|
|
10
|
+
* rule is the build's (format-dogsbay-md ATTRIBUTE_BLOCK_OPEN): `{#` + a
|
|
11
|
+
* letter or digit in any script, `_` or `-`, reaching `}` with no `#}`.
|
|
12
|
+
*/
|
|
13
|
+
const STRAY_OPENER = /\{\{|\{%|\{#(?!(?=[\p{L}\p{N}_-][^}\n]*\})(?![^}\n]*#\}))/gu;
|
|
14
|
+
export class DirectiveTable {
|
|
15
|
+
directives = [];
|
|
16
|
+
/** Register a directive's source; returns the token that stands for it. */
|
|
17
|
+
token(source) {
|
|
18
|
+
this.directives.push(source);
|
|
19
|
+
return `${OPEN}${this.directives.length - 1}${CLOSE}`;
|
|
20
|
+
}
|
|
21
|
+
get size() {
|
|
22
|
+
return this.directives.length;
|
|
23
|
+
}
|
|
24
|
+
/** Replace tokens in a string by their directive source. */
|
|
25
|
+
splice(s) {
|
|
26
|
+
return s.replace(TOKEN, (_m, i) => this.directives[Number(i)]);
|
|
27
|
+
}
|
|
28
|
+
/** Split a string into text runs and directive sources, in order. */
|
|
29
|
+
parts(s) {
|
|
30
|
+
const out = [];
|
|
31
|
+
let last = 0;
|
|
32
|
+
for (const m of s.matchAll(TOKEN)) {
|
|
33
|
+
if (m.index > last)
|
|
34
|
+
out.push({ text: s.slice(last, m.index) });
|
|
35
|
+
out.push({ directive: this.directives[Number(m[1])] });
|
|
36
|
+
last = m.index + m[0].length;
|
|
37
|
+
}
|
|
38
|
+
if (last < s.length)
|
|
39
|
+
out.push({ text: s.slice(last) });
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** `{{ "{{" }}` — prints the opener literally in a template page. */
|
|
44
|
+
function literal(opener) {
|
|
45
|
+
return `{{ "${opener}" }}`;
|
|
46
|
+
}
|
|
47
|
+
/** Escape template openers in a plain string (a title) so a template page prints them literally. */
|
|
48
|
+
export function escapeStrayOpeners(s) {
|
|
49
|
+
return s.replace(STRAY_OPENER, (m) => literal(m));
|
|
50
|
+
}
|
|
51
|
+
const isCode = (type) => typeof type === "string" && /code/.test(type);
|
|
52
|
+
/**
|
|
53
|
+
* A directive the converter may evaluate inside a fence: a value or a
|
|
54
|
+
* conditional. An `{% include %}` or `{% with %}` in code is a tag that
|
|
55
|
+
* landed there by accident (an indented code block), and rendering a
|
|
56
|
+
* partial INTO code is no better than showing the tag.
|
|
57
|
+
*/
|
|
58
|
+
const FENCE_SAFE = /^(?:\{\{[\s\S]*\}\}|\{%-?\s*(?:if|elif|else|endif)\b[\s\S]*%\})$/;
|
|
59
|
+
/**
|
|
60
|
+
* Restore directive tokens in a converted tree, and escape stray openers.
|
|
61
|
+
* Mutates the tree IN PLACE (and returns it).
|
|
62
|
+
*/
|
|
63
|
+
export function restoreDirectives(tree, table, stats) {
|
|
64
|
+
/**
|
|
65
|
+
* Braces right before one of our directives join with it: `{"a": {{ x }}`
|
|
66
|
+
* reads as `{{{ x }}` and `{{% if %}` as an expression opener, and the
|
|
67
|
+
* page fails to render. Such a run becomes a literal-printing directive
|
|
68
|
+
* of its own, so every path below treats it like any other directive.
|
|
69
|
+
*/
|
|
70
|
+
const guard = (s) => s.replace(BRACES_BEFORE_TOKEN, (m) => table.token(`{{ "${m}" }}`));
|
|
71
|
+
const escapeString = (raw) => {
|
|
72
|
+
const s = guard(raw);
|
|
73
|
+
return table.splice(s.replace(STRAY_OPENER, (m) => {
|
|
74
|
+
stats.escaped++;
|
|
75
|
+
return literal(m);
|
|
76
|
+
}));
|
|
77
|
+
};
|
|
78
|
+
/** Inline array → inline array with tokens as directive-raw-inline and stray openers escaped. */
|
|
79
|
+
const inline = (nodes) => {
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const n of nodes) {
|
|
82
|
+
const node = n;
|
|
83
|
+
if (isCode(node.type)) {
|
|
84
|
+
out.push(code(node));
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (node.type === "text" && typeof node.text === "string") {
|
|
88
|
+
const marks = { ...(node.bold ? { bold: true } : {}), ...(node.italic ? { italic: true } : {}) };
|
|
89
|
+
for (const part of table.parts(guard(node.text))) {
|
|
90
|
+
if ("directive" in part) {
|
|
91
|
+
out.push({ type: "directive-raw-inline", source: part.directive, ...marks });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Stray openers become literal-printing directives too, so the
|
|
95
|
+
// serializer cannot escape their braces into something else.
|
|
96
|
+
let last = 0;
|
|
97
|
+
for (const m of part.text.matchAll(STRAY_OPENER)) {
|
|
98
|
+
if (m.index > last)
|
|
99
|
+
out.push({ ...node, text: part.text.slice(last, m.index) });
|
|
100
|
+
out.push({ type: "directive-raw-inline", source: literal(m[0]), ...marks });
|
|
101
|
+
stats.escaped++;
|
|
102
|
+
last = m.index + m[0].length;
|
|
103
|
+
}
|
|
104
|
+
if (last < part.text.length)
|
|
105
|
+
out.push({ ...node, text: part.text.slice(last) });
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
out.push(walk(node));
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
};
|
|
113
|
+
/** Every other string field: splice tokens back, escape stray openers. */
|
|
114
|
+
/** Code keeps its text; only our tokens are put back (and counted). */
|
|
115
|
+
const code = (node) => {
|
|
116
|
+
if (node.type === "code" && markFence(node))
|
|
117
|
+
return node;
|
|
118
|
+
for (const [k, v] of Object.entries(node)) {
|
|
119
|
+
if (typeof v === "string" && table.parts(v).some((p) => "directive" in p)) {
|
|
120
|
+
stats.inCode += table.parts(v).filter((p) => "directive" in p).length;
|
|
121
|
+
node[k] = table.splice(v);
|
|
122
|
+
}
|
|
123
|
+
else if (v && typeof v === "object" && !Array.isArray(v))
|
|
124
|
+
node[k] = code(v);
|
|
125
|
+
}
|
|
126
|
+
return node;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* A fenced block whose directives are all values or conditionals: splice
|
|
130
|
+
* them back, escape every other opener (it is code, and must stay
|
|
131
|
+
* literal), and mark the fence `{minja}` so the template page renders it.
|
|
132
|
+
*/
|
|
133
|
+
const markFence = (node) => {
|
|
134
|
+
const props = node.props;
|
|
135
|
+
const fields = [];
|
|
136
|
+
if (typeof node.html === "string")
|
|
137
|
+
fields.push([node, "html"]);
|
|
138
|
+
if (props && typeof props.code === "string")
|
|
139
|
+
fields.push([props, "code"]);
|
|
140
|
+
const directives = fields.flatMap(([o, k]) => table.parts(o[k]).filter((p) => "directive" in p));
|
|
141
|
+
if (!directives.length || !directives.every((d) => FENCE_SAFE.test(d.directive)))
|
|
142
|
+
return false;
|
|
143
|
+
for (const [o, k] of fields) {
|
|
144
|
+
o[k] = table
|
|
145
|
+
.parts(guard(o[k]))
|
|
146
|
+
.map((p) => ("directive" in p ? p.directive : p.text.replace(STRAY_OPENER, (m) => literal(m))))
|
|
147
|
+
.join("");
|
|
148
|
+
}
|
|
149
|
+
node.props = { ...(props ?? {}), minja: true };
|
|
150
|
+
stats.fencesMarked = (stats.fencesMarked ?? 0) + 1;
|
|
151
|
+
return true;
|
|
152
|
+
};
|
|
153
|
+
const walk = (node) => {
|
|
154
|
+
if (isCode(node.type))
|
|
155
|
+
return code(node);
|
|
156
|
+
for (const [k, v] of Object.entries(node)) {
|
|
157
|
+
if (k === "type")
|
|
158
|
+
continue;
|
|
159
|
+
if (typeof v === "string")
|
|
160
|
+
node[k] = escapeString(v);
|
|
161
|
+
else if (Array.isArray(v))
|
|
162
|
+
node[k] = k === "inline" ? inline(v) : blocks(v);
|
|
163
|
+
else if (v && typeof v === "object")
|
|
164
|
+
node[k] = walk(v);
|
|
165
|
+
}
|
|
166
|
+
return node;
|
|
167
|
+
};
|
|
168
|
+
/** A paragraph whose only content is directive tokens (and whitespace). */
|
|
169
|
+
const onlyTokens = (node) => {
|
|
170
|
+
if (node.type !== "paragraph")
|
|
171
|
+
return undefined;
|
|
172
|
+
const texts = [];
|
|
173
|
+
const collect = (n) => {
|
|
174
|
+
if (Array.isArray(n.inline)) {
|
|
175
|
+
for (const i of n.inline) {
|
|
176
|
+
if (i.type !== "text" || typeof i.text !== "string")
|
|
177
|
+
return false;
|
|
178
|
+
texts.push(i.text);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(n.children))
|
|
182
|
+
for (const c of n.children)
|
|
183
|
+
if (!collect(c))
|
|
184
|
+
return false;
|
|
185
|
+
return true;
|
|
186
|
+
};
|
|
187
|
+
if (!collect(node))
|
|
188
|
+
return undefined;
|
|
189
|
+
const joined = texts.join("");
|
|
190
|
+
if (!joined.trim() || joined.replace(TOKEN, "").trim() !== "")
|
|
191
|
+
return undefined;
|
|
192
|
+
return table.splice(joined.trim());
|
|
193
|
+
};
|
|
194
|
+
const blocks = (nodes) => nodes.map((n) => {
|
|
195
|
+
if (!n || typeof n !== "object")
|
|
196
|
+
return n;
|
|
197
|
+
const raw = onlyTokens(n);
|
|
198
|
+
if (raw !== undefined)
|
|
199
|
+
return { type: "directive-raw", props: { source: raw, kind: "template" } };
|
|
200
|
+
return walk(n);
|
|
201
|
+
});
|
|
202
|
+
// In place: callers hold the array (a returned copy was once ignored,
|
|
203
|
+
// and every top-level directive paragraph stayed a raw token).
|
|
204
|
+
tree.splice(0, tree.length, ...blocks(tree));
|
|
205
|
+
return tree;
|
|
206
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/format-starlight",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.116",
|
|
4
4
|
"description": "Starlight MDX importer for Dogsbay — converts Starlight sources (with JSX components, Render partials, and adapter-specific extensions) into TreeNode[]",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
"gray-matter": "^4.0.3",
|
|
17
17
|
"jsonc-parser": "^3.3.1",
|
|
18
18
|
"markdown-it": "^14.3.0",
|
|
19
|
-
"
|
|
20
|
-
"@dogsbay/
|
|
19
|
+
"markdown-it-footnote": "^4.0.0",
|
|
20
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.116",
|
|
21
|
+
"@dogsbay/types": "0.2.0-beta.116",
|
|
22
|
+
"@dogsbay/markdown-it-mdx-jsx": "0.2.0-beta.116"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
23
25
|
"@types/iarna__toml": "^2.0.5",
|