@dogsbay/format-starlight 0.2.0-beta.11 → 0.2.0-beta.110
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/adapters/cloudflare.js +48 -4
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +179 -5
- package/dist/parse-mdx.d.ts +23 -0
- package/dist/parse-mdx.js +34 -6
- package/dist/parse.js +4 -2
- package/package.json +7 -7
|
@@ -143,18 +143,27 @@ function resolveWranglerConfig(source) {
|
|
|
143
143
|
toml = toml.replace(/^(\s*)(.*compatibility_date.*)/m, "$1# Set this to today's date\n$1$2");
|
|
144
144
|
jsonc = jsonc.replace(/^(\s*)(.*compatibility_date.*)/m, "$1// Set this to today's date\n$1$2");
|
|
145
145
|
}
|
|
146
|
+
// A closing fence MUST be on its own line. It wasn't for the TOML
|
|
147
|
+
// half — `TOML.stringify` returns no trailing newline, so the fence
|
|
148
|
+
// was glued to the last config line (`remote = true```). The block
|
|
149
|
+
// then never closed and SWALLOWED THE REST OF THE PAGE: on
|
|
150
|
+
// ai-search/get-started/workers, everything after the binding config
|
|
151
|
+
// — prose, headings and three more components — became one giant code
|
|
152
|
+
// block. Normalising both halves means neither depends on what its
|
|
153
|
+
// serializer happens to append.
|
|
146
154
|
const result = `<Tabs>
|
|
147
155
|
<TabItem label="wrangler.jsonc">
|
|
148
156
|
|
|
149
157
|
\`\`\`jsonc
|
|
150
|
-
${jsonc}
|
|
158
|
+
${jsonc.replace(/\s*$/, "")}
|
|
151
159
|
\`\`\`
|
|
152
160
|
|
|
153
161
|
</TabItem>
|
|
154
162
|
<TabItem label="wrangler.toml">
|
|
155
163
|
|
|
156
164
|
\`\`\`toml
|
|
157
|
-
${toml}
|
|
165
|
+
${toml.replace(/\s*$/, "")}
|
|
166
|
+
\`\`\`
|
|
158
167
|
|
|
159
168
|
</TabItem>
|
|
160
169
|
</Tabs>`;
|
|
@@ -323,13 +332,34 @@ export const cloudflareAdapter = {
|
|
|
323
332
|
href: attrs.href,
|
|
324
333
|
}),
|
|
325
334
|
},
|
|
335
|
+
// Cloudflare's own alias: `export { default as LinkTitleCard } from
|
|
336
|
+
// "./components/cf/LinkCard.astro"` — the SAME component under a second
|
|
337
|
+
// name, used when the card wraps its description as children rather than
|
|
338
|
+
// passing it as an attribute. Unmapped, it leaked its opening tag onto the
|
|
339
|
+
// page as visible text (`<LinkTitleCard title="Get started" href=…`) and
|
|
340
|
+
// its body rendered as a code block. 300 uses across the corpus.
|
|
341
|
+
LinkTitleCard: {
|
|
342
|
+
type: "card",
|
|
343
|
+
container: true,
|
|
344
|
+
propsFromAttrs: (attrs) => ({
|
|
345
|
+
title: attrs.title || "",
|
|
346
|
+
href: attrs.href,
|
|
347
|
+
icon: attrs.icon,
|
|
348
|
+
}),
|
|
349
|
+
},
|
|
326
350
|
RelatedProduct: {
|
|
327
351
|
type: "card",
|
|
328
352
|
container: true,
|
|
329
353
|
propsFromAttrs: (attrs) => ({
|
|
330
354
|
title: attrs.header || "",
|
|
331
355
|
href: attrs.href,
|
|
332
|
-
|
|
356
|
+
// `/_assets/icons/…` — the mount convention, so the file the importer
|
|
357
|
+
// copies is the file this reference names. It used to be a bare
|
|
358
|
+
// `icons/x.svg`, which existed in neither output: Astro's <Icon> takes
|
|
359
|
+
// a registry NAME and silently rendered nothing, and Obsidian built an
|
|
360
|
+
// <img> against a vault path with no file behind it, so every related
|
|
361
|
+
// product showed a broken image with its title stretched down the page.
|
|
362
|
+
icon: attrs.product ? `/_assets/icons/${attrs.product}.svg` : undefined,
|
|
333
363
|
}),
|
|
334
364
|
},
|
|
335
365
|
Description: {
|
|
@@ -414,6 +444,20 @@ export const cloudflareAdapter = {
|
|
|
414
444
|
// <Plan type="all" /> → **Available on all plans**
|
|
415
445
|
[/<Plan\s+type="all"\s*\/?>/g, "**Available on all plans**"],
|
|
416
446
|
[/<Plan\s+type="([^"]*)"\s*\/?>/g, "**$1 plan**"],
|
|
447
|
+
// <Type text="string" /> → `string`
|
|
448
|
+
//
|
|
449
|
+
// An inline type-annotation badge next to a parameter or method — the
|
|
450
|
+
// component renders it as a styled <code>, so inline code is the faithful
|
|
451
|
+
// mapping, not an approximation. 249 uses in the AI Search partials alone
|
|
452
|
+
// (`string` ×93, `boolean` ×38, `object` ×28), all inside API reference
|
|
453
|
+
// parameter lists where the type IS the content.
|
|
454
|
+
[/<Type\s+text="([^"]*)"\s*\/>/g, "`$1`"],
|
|
455
|
+
// <MetaInfo text="optional" /> → optional
|
|
456
|
+
//
|
|
457
|
+
// The companion annotation: "optional", "default: 10", a units hint. It
|
|
458
|
+
// was being DELETED, which reads as though every parameter is required.
|
|
459
|
+
// The component is a muted inline span, so plain text is the mapping.
|
|
460
|
+
[/<MetaInfo\s+text="([^"]*)"\s*\/>/g, "$1"],
|
|
417
461
|
// <InlineBadge ... /> → strip
|
|
418
462
|
[/<InlineBadge\s[^>]*\/>/g, ""],
|
|
419
463
|
// Fallback strip for self-closing components that may appear deeply indented
|
|
@@ -422,7 +466,7 @@ export const cloudflareAdapter = {
|
|
|
422
466
|
[/<AutoconfigDiagram[\s\S]*?\/>/g, ""],
|
|
423
467
|
[/<GitHubCode\s[^>]*\/>/g, ""],
|
|
424
468
|
[/<ResourcesBySelector[\s\S]*?\/>/g, ""],
|
|
425
|
-
[/<MetaInfo\s[^>]*\/>/g, ""],
|
|
469
|
+
[/<MetaInfo\s[^>]*\/>/g, ""], // fallback: forms with no `text` attribute
|
|
426
470
|
[/<DirectoryListing[\s\S]*?\/>/g, ""],
|
|
427
471
|
[/<ExternalResources\s[^>]*\/>/g, ""],
|
|
428
472
|
[/<YouTubeVideos\s[^>]*\/>/g, ""],
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -5,9 +5,10 @@ import { starlightToTree as starlightToTreeLegacy, extractFrontmatter } from "./
|
|
|
5
5
|
import { starlightToTree as starlightToTreeMdx } from "./parse-mdx.js";
|
|
6
6
|
import { cloudflareAdapter } from "./adapters/cloudflare.js";
|
|
7
7
|
import { buildNavFromDirectory, buildNavFromConfig } from "./nav.js";
|
|
8
|
-
const
|
|
8
|
+
export const STARLIGHT_ADAPTERS = {
|
|
9
9
|
cloudflare: cloudflareAdapter,
|
|
10
10
|
};
|
|
11
|
+
const ADAPTERS = STARLIGHT_ADAPTERS;
|
|
11
12
|
/**
|
|
12
13
|
* Detect if a directory contains a Starlight docs source.
|
|
13
14
|
*
|
|
@@ -136,9 +137,96 @@ async function importStarlight(source, opts) {
|
|
|
136
137
|
if (opts.partialsDir) {
|
|
137
138
|
partialsDir = opts.partialsDir;
|
|
138
139
|
}
|
|
140
|
+
// Where the images live. Found the same way as partialsDir, because it is
|
|
141
|
+
// the same Astro layout: `src/content/docs` and `src/assets` are siblings
|
|
142
|
+
// under `src/`.
|
|
143
|
+
//
|
|
144
|
+
// Without this the conversion emitted `/_assets/images/x.png` refs and
|
|
145
|
+
// nothing ever copied a file there — every local image on a converted site
|
|
146
|
+
// was a 404. The reference survived and the file did not, which is why a
|
|
147
|
+
// page count, a retention score and a clean exit all looked fine.
|
|
148
|
+
let assetsDir;
|
|
149
|
+
searchDir = docsDir;
|
|
150
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
151
|
+
const candidate = join(searchDir, "src", "assets");
|
|
152
|
+
if (existsSync(candidate)) {
|
|
153
|
+
assetsDir = candidate;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
const sibling = join(dirname(searchDir), "assets");
|
|
157
|
+
if (existsSync(sibling)) {
|
|
158
|
+
assetsDir = sibling;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
const parent = dirname(searchDir);
|
|
162
|
+
if (parent === searchDir)
|
|
163
|
+
break;
|
|
164
|
+
searchDir = parent;
|
|
165
|
+
}
|
|
166
|
+
// Product/brand icons, mounted under `_assets/icons` to match the refs the
|
|
167
|
+
// adapters emit for them.
|
|
168
|
+
let iconsDir;
|
|
169
|
+
searchDir = docsDir;
|
|
170
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
171
|
+
const candidate = join(searchDir, "src", "icons");
|
|
172
|
+
if (existsSync(candidate)) {
|
|
173
|
+
iconsDir = candidate;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
const parent = dirname(searchDir);
|
|
177
|
+
if (parent === searchDir)
|
|
178
|
+
break;
|
|
179
|
+
searchDir = parent;
|
|
180
|
+
}
|
|
181
|
+
// Component sources, for recovering an accessible description from a
|
|
182
|
+
// component we cannot render. Same sibling layout as assets and partials.
|
|
183
|
+
let componentsDir;
|
|
184
|
+
searchDir = docsDir;
|
|
185
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
186
|
+
const candidate = join(searchDir, "src", "components");
|
|
187
|
+
if (existsSync(candidate)) {
|
|
188
|
+
componentsDir = candidate;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const parent = dirname(searchDir);
|
|
192
|
+
if (parent === searchDir)
|
|
193
|
+
break;
|
|
194
|
+
searchDir = parent;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The `aria-label` (or `<figcaption>`) a component declares about itself.
|
|
198
|
+
*
|
|
199
|
+
* Cloudflare's diagrams are Astro components — inline SVG, a web component,
|
|
200
|
+
* CSS animation — with no image file to carry across. But their `aria-label`
|
|
201
|
+
* is a full description written by their docs team, and it is the
|
|
202
|
+
* information the diagram exists to convey. Recovering it turns a silent
|
|
203
|
+
* hole into a described placeholder; nothing else about the component can
|
|
204
|
+
* survive a conversion to markdown.
|
|
205
|
+
*/
|
|
206
|
+
const describeComponent = (tag) => {
|
|
207
|
+
if (!componentsDir)
|
|
208
|
+
return undefined;
|
|
209
|
+
const file = join(componentsDir, `${tag}.astro`);
|
|
210
|
+
if (!existsSync(file))
|
|
211
|
+
return undefined;
|
|
212
|
+
const source = readFileSync(file, "utf-8");
|
|
213
|
+
const aria = source.match(/aria-label="([^"]{20,})"/);
|
|
214
|
+
if (aria)
|
|
215
|
+
return aria[1].replace(/\s+/g, " ").trim();
|
|
216
|
+
const caption = source.match(/<figcaption[^>]*>([^<]{20,})<\/figcaption>/);
|
|
217
|
+
return caption ? caption[1].replace(/\s+/g, " ").trim() : undefined;
|
|
218
|
+
};
|
|
139
219
|
// Parse all pages
|
|
140
220
|
const mdxFiles = walkMdxFiles(docsDir, docsDir);
|
|
141
221
|
const pages = [];
|
|
222
|
+
/** Components no builtin or adapter mapping claimed. */
|
|
223
|
+
const dropped = new Map();
|
|
224
|
+
const record = (tag, slug) => {
|
|
225
|
+
const entry = dropped.get(tag) ?? { count: 0, pages: new Set() };
|
|
226
|
+
entry.count++;
|
|
227
|
+
entry.pages.add(slug);
|
|
228
|
+
dropped.set(tag, entry);
|
|
229
|
+
};
|
|
142
230
|
// Custom taxonomy names from dogsbay.config.yml's `taxonomies:` block
|
|
143
231
|
// (threaded through cli's import-content.ts).
|
|
144
232
|
const taxonomyNames = Array.isArray(opts.taxonomyNames)
|
|
@@ -151,7 +239,24 @@ async function importStarlight(source, opts) {
|
|
|
151
239
|
// Fall back to legacy regex parser with --legacy-parser flag
|
|
152
240
|
const useLegacy = opts.legacyParser === true;
|
|
153
241
|
const starlightToTree = useLegacy ? starlightToTreeLegacy : starlightToTreeMdx;
|
|
154
|
-
const tree = starlightToTree(body, {
|
|
242
|
+
const tree = starlightToTree(body, {
|
|
243
|
+
adapter,
|
|
244
|
+
partialsDir,
|
|
245
|
+
onDrop: (tag) => record(tag, slug),
|
|
246
|
+
describeComponent,
|
|
247
|
+
});
|
|
248
|
+
// Catch what the callback cannot: a component nested inside a MAPPED
|
|
249
|
+
// container is not dropped, it LEAKS — its opening tag lands in the page
|
|
250
|
+
// as visible text and its body often becomes a code block, because nothing
|
|
251
|
+
// claimed the block. `<LinkTitleCard>` inside `<CardGrid>` did exactly
|
|
252
|
+
// that on Cloudflare's AI Search overview while the drop warning stayed
|
|
253
|
+
// silent.
|
|
254
|
+
//
|
|
255
|
+
// Scanning the finished tree catches every route into the output, present
|
|
256
|
+
// and future, in one place. `code` nodes are skipped: a code sample
|
|
257
|
+
// containing `<Response>` is content, not a leak.
|
|
258
|
+
for (const tag of leakedComponentTags(tree))
|
|
259
|
+
record(tag, slug);
|
|
155
260
|
// Extract headings from tree
|
|
156
261
|
const headings = tree
|
|
157
262
|
.filter((n) => n.type === "heading")
|
|
@@ -215,16 +320,85 @@ async function importStarlight(source, opts) {
|
|
|
215
320
|
opts.siteName = indexPage.title;
|
|
216
321
|
}
|
|
217
322
|
}
|
|
218
|
-
|
|
323
|
+
// Say what was dropped, loudly and countably.
|
|
324
|
+
//
|
|
325
|
+
// A component nothing claimed is CONTENT THAT IS NOT ON THE PAGE — the
|
|
326
|
+
// Cloudflare AI Search overview lost its architecture diagram this way, and
|
|
327
|
+
// the only way anyone found out was comparing the converted page with the
|
|
328
|
+
// live one by eye. There is no ledger on this path, so the warning IS the
|
|
329
|
+
// report: it names each tag, how many times it appeared and how many pages
|
|
330
|
+
// it touched, so `convert … 2>&1 | grep "dropped"` answers "how much of this
|
|
331
|
+
// corpus does this affect" without reading any output files.
|
|
332
|
+
if (dropped.size > 0) {
|
|
333
|
+
const rows = [...dropped.entries()]
|
|
334
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
335
|
+
.map(([tag, d]) => ` <${tag}> ×${d.count} on ${d.pages.size} page(s): ${[...d.pages].slice(0, 3).join(", ")}${d.pages.size > 3 ? ", …" : ""}`);
|
|
336
|
+
const total = [...dropped.values()].reduce((n, d) => n + d.count, 0);
|
|
337
|
+
console.warn(`warning: ${total} unmapped component(s) — dropped, or leaked into the ` +
|
|
338
|
+
`page as literal text:\n${rows.join("\n")}\n` +
|
|
339
|
+
` Map them with an adapter, or accept the loss knowingly.`);
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
pages,
|
|
343
|
+
nav,
|
|
344
|
+
// Contents land in `public/_assets/`, matching the `/_assets/…` refs the
|
|
345
|
+
// parser now emits for the `~/assets/` alias.
|
|
346
|
+
...(assetsDir || iconsDir
|
|
347
|
+
? {
|
|
348
|
+
assetMounts: [
|
|
349
|
+
...(assetsDir ? [{ dir: assetsDir }] : []),
|
|
350
|
+
...(iconsDir ? [{ dir: iconsDir, into: "icons" }] : []),
|
|
351
|
+
],
|
|
352
|
+
}
|
|
353
|
+
: {}),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Component tags left as literal text/HTML in a finished tree.
|
|
358
|
+
*
|
|
359
|
+
* A CamelCase tag only — `<ACCOUNT_ID>` and `<API_TOKEN>` are placeholders the
|
|
360
|
+
* docs write on purpose, and flagging those would bury the real signal.
|
|
361
|
+
*/
|
|
362
|
+
function leakedComponentTags(nodes) {
|
|
363
|
+
const found = [];
|
|
364
|
+
const scan = (list) => {
|
|
365
|
+
for (const node of list) {
|
|
366
|
+
if (node.type === "code" || node.type === "diagram")
|
|
367
|
+
continue;
|
|
368
|
+
for (const text of [node.html, ...(node.inline ?? []).map((i) => i.text)]) {
|
|
369
|
+
if (typeof text !== "string")
|
|
370
|
+
continue;
|
|
371
|
+
// `</?` — a leak is often only the CLOSING tag, when the opener was
|
|
372
|
+
// consumed by the container above it but the closer had nothing to
|
|
373
|
+
// pair with. Matching openers alone missed exactly that case.
|
|
374
|
+
for (const m of text.matchAll(/<\/?([A-Z][a-z0-9]+[A-Za-z0-9]*)[\s/>]/g))
|
|
375
|
+
found.push(m[1]);
|
|
376
|
+
}
|
|
377
|
+
if (node.children)
|
|
378
|
+
scan(node.children);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
scan(nodes);
|
|
382
|
+
return found;
|
|
219
383
|
}
|
|
220
384
|
export const plugin = {
|
|
221
385
|
name: "starlight",
|
|
222
386
|
canImport: true,
|
|
223
387
|
canExport: false,
|
|
224
388
|
detectSource: detectStarlightSource,
|
|
225
|
-
exportOptions:
|
|
226
|
-
|
|
389
|
+
// importOptions, not exportOptions: this plugin declares `canExport: false`,
|
|
390
|
+
// so an export-side declaration described a flag on a command that does not
|
|
391
|
+
// exist — and `convert`'s "unusable flag" warning reads the IMPORT list, so
|
|
392
|
+
// `--adapter cloudflare` was reported as ignored while being consumed. That
|
|
393
|
+
// warning is load-bearing on this corpus: the adapter is the difference
|
|
394
|
+
// between every `<Render>` partial resolving and none of them.
|
|
395
|
+
importOptions: [
|
|
396
|
+
{
|
|
397
|
+
flags: "--adapter <name>",
|
|
398
|
+
description: `Site adapter for custom components (${Object.keys(STARLIGHT_ADAPTERS).join(", ")})`,
|
|
399
|
+
},
|
|
227
400
|
],
|
|
401
|
+
exportOptions: [],
|
|
228
402
|
async import(source, opts) {
|
|
229
403
|
return importStarlight(source, opts);
|
|
230
404
|
},
|
package/dist/parse-mdx.d.ts
CHANGED
|
@@ -18,5 +18,28 @@ export interface ParseOptions {
|
|
|
18
18
|
adapter?: SiteAdapter;
|
|
19
19
|
/** Directory containing partial MDX files for <Render> resolution */
|
|
20
20
|
partialsDir?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Called with the tag name of every component DROPPED from the output —
|
|
23
|
+
* one that no builtin and no adapter mapping claimed.
|
|
24
|
+
*
|
|
25
|
+
* A dropped component is content that is not on the page. Cloudflare's
|
|
26
|
+
* `<AiSearchOverviewDiagram />` is the architecture diagram at the top of
|
|
27
|
+
* the AI Search overview; it vanished with no trace, and the only way to
|
|
28
|
+
* find out was to compare the converted page against the live one by eye.
|
|
29
|
+
* The importer knows at the moment it happens — this is how it says so.
|
|
30
|
+
*/
|
|
31
|
+
onDrop?: (tag: string) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Accessible description for a component we cannot render, if one can be
|
|
34
|
+
* found — used to leave a described placeholder instead of a hole.
|
|
35
|
+
*
|
|
36
|
+
* Cloudflare's diagrams are 478-line Astro components: inline SVG, a web
|
|
37
|
+
* component and CSS animation, with no image file anywhere. Nothing can
|
|
38
|
+
* convert that. But each carries an `aria-label` written by their docs team
|
|
39
|
+
* ("AI Search is a managed search primitive: you upload or connect your
|
|
40
|
+
* content…"), which IS the information the diagram conveys — and it was
|
|
41
|
+
* being thrown away with the markup.
|
|
42
|
+
*/
|
|
43
|
+
describeComponent?: (tag: string) => string | undefined;
|
|
21
44
|
}
|
|
22
45
|
export declare function starlightToTree(source: string, options?: ParseOptions): TreeNode[];
|
package/dist/parse-mdx.js
CHANGED
|
@@ -105,10 +105,21 @@ function applyInlineTransforms(source, adapter) {
|
|
|
105
105
|
return result;
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* Rewrite
|
|
108
|
+
* Rewrite the `~/assets/` Vite alias to the `/_assets/` mount convention.
|
|
109
|
+
*
|
|
110
|
+
* It used to produce a bare relative `assets/…`, which resolves to nothing:
|
|
111
|
+
* the page lives at `/ai-search/get-started/workers`, so a relative
|
|
112
|
+
* `assets/images/x.png` is looked for under that directory, and no exporter
|
|
113
|
+
* ever put a file there. Every local image on a converted Starlight site was a
|
|
114
|
+
* 404 — the reference survived the conversion and the file did not.
|
|
115
|
+
*
|
|
116
|
+
* `/_assets/…` is the repo-wide convention (see `assetMounts` in
|
|
117
|
+
* format-astro's project emitter): a mounted directory's CONTENTS land in
|
|
118
|
+
* `public/_assets/`, and importers rewrite refs to match — the same thing the
|
|
119
|
+
* Docusaurus importer does with a `static/` dir.
|
|
109
120
|
*/
|
|
110
121
|
function rewriteAssetPaths(source) {
|
|
111
|
-
return source.replace(/~\/assets\//g, "
|
|
122
|
+
return source.replace(/~\/assets\//g, "/_assets/");
|
|
112
123
|
}
|
|
113
124
|
/**
|
|
114
125
|
* Convert Starlight :::note / :::caution / :::tip / :::danger directives
|
|
@@ -149,7 +160,7 @@ function convertDirectives(source) {
|
|
|
149
160
|
* Convert markdown-it tokens (including jsx_* tokens) to TreeNode[].
|
|
150
161
|
* This replaces both segmentsToTree and tokensToTree from the old parser.
|
|
151
162
|
*/
|
|
152
|
-
function tokensToTree(tokens, componentMap) {
|
|
163
|
+
function tokensToTree(tokens, componentMap, onDrop, describeComponent) {
|
|
153
164
|
const tree = [];
|
|
154
165
|
let i = 0;
|
|
155
166
|
while (i < tokens.length) {
|
|
@@ -327,7 +338,7 @@ function tokensToTree(tokens, componentMap) {
|
|
|
327
338
|
const children = [];
|
|
328
339
|
i++;
|
|
329
340
|
while (i < tokens.length && tokens[i].type !== "blockquote_close") {
|
|
330
|
-
const nested = tokensToTree(tokens.slice(i, i + 1), componentMap);
|
|
341
|
+
const nested = tokensToTree(tokens.slice(i, i + 1), componentMap, onDrop, describeComponent);
|
|
331
342
|
children.push(...nested);
|
|
332
343
|
i++;
|
|
333
344
|
}
|
|
@@ -339,7 +350,22 @@ function tokensToTree(tokens, componentMap) {
|
|
|
339
350
|
// Check if this is a leftover JSX component that wasn't caught
|
|
340
351
|
const trimmed = token.content.trim();
|
|
341
352
|
if (trimmed.match(/^<[A-Z]/)) {
|
|
342
|
-
//
|
|
353
|
+
// A JSX component nothing claimed. Dropping it is the existing
|
|
354
|
+
// behaviour; reporting it is new — silence here is what let five
|
|
355
|
+
// diagrams disappear from a converted section unnoticed.
|
|
356
|
+
const tag = trimmed.match(/^<([A-Za-z0-9]+)/)?.[1] ?? "unknown";
|
|
357
|
+
const description = describeComponent?.(tag);
|
|
358
|
+
if (description) {
|
|
359
|
+
// Keep the MEANING even though the markup cannot travel.
|
|
360
|
+
tree.push({
|
|
361
|
+
type: "callout",
|
|
362
|
+
props: { variant: "note", title: "Diagram" },
|
|
363
|
+
children: [{ type: "paragraph", inline: [{ type: "text", text: description }] }],
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
onDrop?.(tag);
|
|
368
|
+
}
|
|
343
369
|
i++;
|
|
344
370
|
continue;
|
|
345
371
|
}
|
|
@@ -518,6 +544,8 @@ function groupConsecutiveCards(tree) {
|
|
|
518
544
|
}
|
|
519
545
|
export function starlightToTree(source, options) {
|
|
520
546
|
const adapter = options?.adapter;
|
|
547
|
+
const onDrop = options?.onDrop;
|
|
548
|
+
const describeComponent = options?.describeComponent;
|
|
521
549
|
const { components, selfClosing, map: componentMap } = buildComponentMap(adapter);
|
|
522
550
|
// Strip frontmatter block before parsing. Without this, markdown-it sees
|
|
523
551
|
// `---...---` as thematic-break + paragraph + thematic-break, which leaks the
|
|
@@ -548,7 +576,7 @@ export function starlightToTree(source, options) {
|
|
|
548
576
|
// Parse into tokens (including jsx_* tokens)
|
|
549
577
|
const tokens = md.parse(cleaned, {});
|
|
550
578
|
// Convert tokens to tree
|
|
551
|
-
const tree = tokensToTree(tokens, componentMap);
|
|
579
|
+
const tree = tokensToTree(tokens, componentMap, onDrop, describeComponent);
|
|
552
580
|
const unwrapped = unwrapStepsOrderedList(tree);
|
|
553
581
|
return groupConsecutiveCards(unwrapped);
|
|
554
582
|
}
|
package/dist/parse.js
CHANGED
|
@@ -140,11 +140,13 @@ function buildComponentMap(adapter) {
|
|
|
140
140
|
function stripImports(source) {
|
|
141
141
|
// Strip single-line imports: import { X } from "y";
|
|
142
142
|
// Strip multi-line imports: import {\n X,\n Y,\n} from "y";
|
|
143
|
-
// Rewrite ~/assets/ Vite alias to
|
|
143
|
+
// Rewrite the ~/assets/ Vite alias to the `/_assets/` mount convention —
|
|
144
|
+
// see the note on rewriteAssetPaths in parse-mdx.ts. A bare relative
|
|
145
|
+
// `assets/…` resolved against the page's own directory and 404'd.
|
|
144
146
|
return source
|
|
145
147
|
.replace(/^import\s+\{[^}]*\}\s+from\s+["'][^"']*["']\s*;?\s*$/gm, "")
|
|
146
148
|
.replace(/^import\s+\{[\s\S]*?\}\s+from\s+["'][^"']*["']\s*;?\s*$/gm, "")
|
|
147
|
-
.replace(/~\/assets\//g, "
|
|
149
|
+
.replace(/~\/assets\//g, "/_assets/")
|
|
148
150
|
.replace(/\{\/\*[\s\S]*?\*\/\}/g, "")
|
|
149
151
|
.replace(/\n{3,}/g, "\n\n");
|
|
150
152
|
}
|
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.110",
|
|
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": {
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
"@iarna/toml": "^2.2.5",
|
|
16
16
|
"gray-matter": "^4.0.3",
|
|
17
17
|
"jsonc-parser": "^3.3.1",
|
|
18
|
-
"markdown-it": "^14.
|
|
19
|
-
"@dogsbay/markdown-it-mdx-jsx": "0.2.0-beta.
|
|
20
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
18
|
+
"markdown-it": "^14.3.0",
|
|
19
|
+
"@dogsbay/markdown-it-mdx-jsx": "0.2.0-beta.110",
|
|
20
|
+
"@dogsbay/types": "0.2.0-beta.110"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/iarna__toml": "^2.0.5",
|
|
24
24
|
"@types/markdown-it": "^14.1.2",
|
|
25
|
-
"@types/node": "^22.
|
|
26
|
-
"typescript": "^5.
|
|
27
|
-
"vitest": "^
|
|
25
|
+
"@types/node": "^22.20.1",
|
|
26
|
+
"typescript": "^5.9.3",
|
|
27
|
+
"vitest": "^4.1.10"
|
|
28
28
|
},
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"repository": {
|