@dogsbay/format-starlight 0.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Site adapter — maps site-specific components to TreeNode types.
3
+ *
4
+ * The core Starlight parser handles built-in components (Tabs, Steps, Aside, etc.).
5
+ * Adapters handle custom components that specific sites add on top of Starlight.
6
+ *
7
+ * Usage:
8
+ * starlightToTree(source) // core only
9
+ * starlightToTree(source, { adapter: cloudflare }) // core + cloudflare
10
+ */
11
+ export interface ComponentMapping {
12
+ /** TreeNode type to produce (e.g. "card", "callout", "badge") */
13
+ type: string;
14
+ /** Extract props from JSX attributes */
15
+ propsFromAttrs?: (attrs: Record<string, string>) => Record<string, unknown>;
16
+ /** Whether this component wraps children */
17
+ container: boolean;
18
+ /** Strip the component entirely. If container=true, inner content is kept. */
19
+ strip?: boolean;
20
+ }
21
+ export interface SiteAdapter {
22
+ /** Adapter name (e.g. "cloudflare") */
23
+ name: string;
24
+ /** Block-level component mappings (components that appear on their own line) */
25
+ components: Record<string, ComponentMapping>;
26
+ /** Self-closing tags (no closing tag expected) */
27
+ selfClosingTags?: string[];
28
+ /**
29
+ * Inline component transformations applied to source text before parsing.
30
+ * These handle components that appear within prose text (not on their own line).
31
+ * Each entry: [regex, replacement string or function]
32
+ */
33
+ inlineTransforms?: [RegExp, string][];
34
+ /**
35
+ * Resolve include/partial component tags in source text.
36
+ * Called before all other processing (inline transforms, directives, etc.)
37
+ * so that inlined content gets full parser treatment.
38
+ *
39
+ * @param source - MDX source text
40
+ * @param partialsDir - Base directory for partial files (auto-detected or CLI flag)
41
+ * @returns Source with include tags replaced by partial content
42
+ */
43
+ resolveIncludes?: (source: string, partialsDir?: string) => string;
44
+ /** Frontmatter field name that carries the redirect/external link URL */
45
+ redirectField?: string;
46
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Site adapter — maps site-specific components to TreeNode types.
3
+ *
4
+ * The core Starlight parser handles built-in components (Tabs, Steps, Aside, etc.).
5
+ * Adapters handle custom components that specific sites add on top of Starlight.
6
+ *
7
+ * Usage:
8
+ * starlightToTree(source) // core only
9
+ * starlightToTree(source, { adapter: cloudflare }) // core + cloudflare
10
+ */
11
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Cloudflare adapter for format-starlight.
3
+ *
4
+ * Maps Cloudflare-specific custom components used across developers.cloudflare.com.
5
+ * These are layered on top of the core Starlight component mappings.
6
+ *
7
+ * Source: https://github.com/cloudflare/cloudflare-docs/tree/production/src/components
8
+ */
9
+ import type { SiteAdapter } from "../adapter.js";
10
+ export declare const cloudflareAdapter: SiteAdapter;
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Cloudflare adapter for format-starlight.
3
+ *
4
+ * Maps Cloudflare-specific custom components used across developers.cloudflare.com.
5
+ * These are layered on top of the core Starlight component mappings.
6
+ *
7
+ * Source: https://github.com/cloudflare/cloudflare-docs/tree/production/src/components
8
+ */
9
+ import { readFileSync, existsSync } from "node:fs";
10
+ import { join, dirname } from "node:path";
11
+ import TOML from "@iarna/toml";
12
+ import { parse as jsoncParse } from "jsonc-parser";
13
+ // ── Indentation helper ─────────────────────────────────
14
+ /**
15
+ * Indent all lines of content to match the indentation of the original tag.
16
+ * Critical for resolved content inside list items, Steps, etc. — without
17
+ * matching indentation, markdown-it treats the content as breaking out of
18
+ * the parent block context.
19
+ */
20
+ function indentContent(content, indent) {
21
+ if (!indent)
22
+ return content;
23
+ return content.split("\n").map((line) => line ? `${indent}${line}` : line).join("\n");
24
+ }
25
+ let dashRoutesCache = null;
26
+ function loadDashRoutes(partialsDir) {
27
+ if (dashRoutesCache)
28
+ return dashRoutesCache;
29
+ // Routes are at src/content/dash-routes/ — sibling of src/content/partials/
30
+ const contentDir = dirname(partialsDir);
31
+ const routesDir = join(contentDir, "dash-routes");
32
+ const routes = [];
33
+ for (const file of ["core.json", "core-manually-defined.json"]) {
34
+ const path = join(routesDir, file);
35
+ if (existsSync(path)) {
36
+ try {
37
+ const data = JSON.parse(readFileSync(path, "utf-8"));
38
+ routes.push(...data);
39
+ }
40
+ catch { /* ignore parse errors */ }
41
+ }
42
+ }
43
+ dashRoutesCache = routes;
44
+ return routes;
45
+ }
46
+ function resolveDashButton(source, partialsDir) {
47
+ if (!partialsDir)
48
+ return source.replace(/<DashButton\s[^>]*\/>/g, "");
49
+ const routes = loadDashRoutes(partialsDir);
50
+ return source.replace(/<DashButton\s+url="([^"]*)"[^>]*\/>/g, (_match, url) => {
51
+ const route = routes.find((r) => r.deeplink === url);
52
+ const name = route?.name || "dashboard";
53
+ const href = `https://dash.cloudflare.com${url}`;
54
+ return `<LinkButton href="${href}">Go to ${name}</LinkButton>`;
55
+ });
56
+ }
57
+ const commands = {
58
+ npm: { add: "npm i", create: "npm create", dlx: "npx", exec: "npx", install: "npm install", run: "npm run", remove: "npm uninstall", dev: "-D" },
59
+ yarn: { add: "yarn add", create: "yarn create", dlx: "yarn dlx", exec: "yarn", install: "yarn install", run: "yarn run", remove: "yarn remove", dev: "-D" },
60
+ pnpm: { add: "pnpm add", create: "pnpm create", dlx: "pnpx", exec: "pnpm", install: "pnpm install", run: "pnpm run", remove: "pnpm remove", dev: "-D" },
61
+ bun: { add: "bun add", install: "bun install", remove: "bun remove", dev: "-d" },
62
+ };
63
+ const MANAGERS = ["npm", "yarn", "pnpm", "bun"];
64
+ function getCommand(mgr, type, pkg, opts = {}) {
65
+ let cmd = commands[mgr][type];
66
+ if (cmd === undefined)
67
+ return undefined;
68
+ if (opts.prefix)
69
+ cmd = `${opts.prefix} ${cmd}`;
70
+ if (opts.dev && type === "add")
71
+ cmd += ` ${commands[mgr].dev}`;
72
+ if (pkg) {
73
+ const processedPkg = type === "create" && mgr === "yarn"
74
+ ? pkg.replace(/@(?![^@]*\/)[^\s]*$/, "") : pkg;
75
+ cmd += ` ${processedPkg}`;
76
+ }
77
+ if (opts.args) {
78
+ cmd += `${mgr === "npm" && !["dlx", "exec", "run"].includes(type) ? " --" : ""} ${opts.args}`;
79
+ }
80
+ return cmd;
81
+ }
82
+ /**
83
+ * Resolve `<PackageManagers>` to Tabs with npm/yarn/pnpm/bun commands.
84
+ */
85
+ function resolvePackageManagers(source) {
86
+ return source.replace(/^([ \t]*)<PackageManagers\s+([\s\S]*?)\/>/gm, (_match, indent, attrsStr) => {
87
+ // Parse attributes
88
+ const type = (attrsStr.match(/type\s*=\s*"([^"]*)"/)?.[1] || "add");
89
+ const pkg = attrsStr.match(/pkg\s*=\s*"([^"]*)"/)?.[1];
90
+ const args = attrsStr.match(/args\s*=\s*(?:"([^"]*)"|{["']([^"']*)['"]})/)?.[1]
91
+ || attrsStr.match(/args\s*=\s*\{?"?([^"}\s]*)"?\}?/)?.[1];
92
+ const dev = attrsStr.includes("dev");
93
+ const prefix = attrsStr.match(/prefix\s*=\s*"([^"]*)"/)?.[1];
94
+ const tabs = MANAGERS
95
+ .map((mgr) => {
96
+ const cmd = getCommand(mgr, type, pkg, { args, dev, prefix });
97
+ return cmd ? `<TabItem label="${mgr}">\n\n\`\`\`sh\n${cmd}\n\`\`\`\n\n</TabItem>` : null;
98
+ })
99
+ .filter(Boolean)
100
+ .join("\n");
101
+ return indentContent(`<Tabs>\n${tabs}\n</Tabs>`, indent);
102
+ });
103
+ }
104
+ // ── WranglerConfig resolution ───────────────────────────
105
+ /**
106
+ * Resolve `<WranglerConfig>` by extracting the inner code block,
107
+ * converting between JSONC and TOML, and outputting as Tabs.
108
+ *
109
+ * Handles:
110
+ * - `$today` magic string → current date (YYYY-MM-DD)
111
+ * - Bidirectional JSONC ↔ TOML conversion
112
+ * - "Set this to today's date" comment injection
113
+ * - `$schema` property addition for JSONC output
114
+ */
115
+ function resolveWranglerConfig(source) {
116
+ return source.replace(/^([ \t]*)<WranglerConfig[^>]*>([\s\S]*?)<\/WranglerConfig>/gm, (_match, indent, inner) => {
117
+ // Extract code block content: ```lang\ncode\n```
118
+ const codeMatch = inner.match(/```(\w+)\s*\n([\s\S]*?)```/);
119
+ if (!codeMatch)
120
+ return inner.trim();
121
+ const lang = codeMatch[1].toLowerCase();
122
+ let code = codeMatch[2].trim();
123
+ // Replace $today with current date
124
+ const hadToday = /\$today/i.test(code);
125
+ const today = new Date().toISOString().split("T")[0];
126
+ code = code.replace(/\$today/gi, today);
127
+ try {
128
+ let jsonc;
129
+ let toml;
130
+ if (lang === "toml") {
131
+ toml = code;
132
+ const parsed = TOML.parse(code);
133
+ jsonc = JSON.stringify(parsed, null, 4);
134
+ }
135
+ else {
136
+ // JSONC or JSON
137
+ const parsed = jsoncParse(code);
138
+ jsonc = JSON.stringify(parsed, null, 4);
139
+ toml = TOML.stringify(parsed);
140
+ }
141
+ // Inject compatibility_date comment when $today was used
142
+ if (hadToday) {
143
+ toml = toml.replace(/^(\s*)(.*compatibility_date.*)/m, "$1# Set this to today's date\n$1$2");
144
+ jsonc = jsonc.replace(/^(\s*)(.*compatibility_date.*)/m, "$1// Set this to today's date\n$1$2");
145
+ }
146
+ const result = `<Tabs>
147
+ <TabItem label="wrangler.jsonc">
148
+
149
+ \`\`\`jsonc
150
+ ${jsonc}
151
+ \`\`\`
152
+
153
+ </TabItem>
154
+ <TabItem label="wrangler.toml">
155
+
156
+ \`\`\`toml
157
+ ${toml}\`\`\`
158
+
159
+ </TabItem>
160
+ </Tabs>`;
161
+ return indentContent(result, indent);
162
+ }
163
+ catch {
164
+ // If conversion fails, output the original code block
165
+ return indentContent(`\`\`\`${lang}\n${code}\n\`\`\``, indent);
166
+ }
167
+ });
168
+ }
169
+ /**
170
+ * Resolve JSX switch expressions that remain after param substitution.
171
+ *
172
+ * Pattern: {(function() { switch('value') { case 'value': return (<content>); ... } })()}
173
+ *
174
+ * Extracts the switch value, finds the matching case, and returns its content.
175
+ * Falls back to stripping the entire expression if no match is found.
176
+ */
177
+ function resolveJsxSwitch(content) {
178
+ return content.replace(/\{\(function\s*\(\)\s*\{[\s\S]*?switch\s*\((?:'([^']*)'|"([^"]*)")\)\s*\{([\s\S]*?)\}\s*\}\)\(\)\}/g, (_match, val1, val2, cases) => {
179
+ const switchValue = val1 ?? val2 ?? "";
180
+ // Find the matching case
181
+ const caseRegex = new RegExp(`case\\s+['"]${switchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]\\s*:\\s*return\\s*\\(([\\s\\S]*?)\\);`);
182
+ const match = cases.match(caseRegex);
183
+ if (match) {
184
+ // Clean up the JSX: strip <ul>/<li> wrappers, convert to markdown
185
+ let html = match[1].trim();
186
+ // Convert <li>...</li> to markdown list items
187
+ html = html.replace(/<ul>\s*/g, "");
188
+ html = html.replace(/<\/ul>\s*/g, "");
189
+ html = html.replace(/<li>([\s\S]*?)<\/li>/g, "- $1");
190
+ // Convert <em>...</em> to *...*
191
+ html = html.replace(/<em>([\s\S]*?)<\/em>/g, "*$1*");
192
+ // Convert <code>...</code> to `...`
193
+ html = html.replace(/<code>([\s\S]*?)<\/code>/g, "`$1`");
194
+ return html.trim();
195
+ }
196
+ return ""; // No matching case — strip the expression
197
+ });
198
+ }
199
+ // ── Raw file import resolution ──────────────────────────
200
+ /**
201
+ * Resolve `import X from "~/path?raw"` by reading the file and
202
+ * substituting `{X}` with the file content in the source.
203
+ * Vite/Astro's ?raw imports file contents as strings.
204
+ */
205
+ function resolveRawImports(source, partialsDir) {
206
+ if (!partialsDir)
207
+ return source;
208
+ // src/content/partials/ → src/ (go up two levels to repo src/)
209
+ const srcDir = dirname(dirname(partialsDir));
210
+ const importRegex = /^import\s+(\w+)\s+from\s+["']~\/([^"']+)\?raw["']\s*;?\s*$/gm;
211
+ let result = source;
212
+ let match;
213
+ while ((match = importRegex.exec(source)) !== null) {
214
+ const varName = match[1];
215
+ const filePath = join(srcDir, match[2]);
216
+ if (existsSync(filePath)) {
217
+ try {
218
+ const content = readFileSync(filePath, "utf-8");
219
+ result = result.replace(match[0], ""); // remove import line
220
+ // If used in <Code code={X} lang="..." />, replace with a fenced code block
221
+ const codeTagRegex = new RegExp(`<Code\\s+code=\\{${varName}\\}[^>]*(?:lang=["']([^"']*)["'])?[^>]*/>`);
222
+ const codeMatch = result.match(codeTagRegex);
223
+ if (codeMatch) {
224
+ const lang = codeMatch[1] || "plaintext";
225
+ // Use a fence that doesn't appear in the content
226
+ let fence = "```";
227
+ const backtickRuns = content.match(/`{3,}/g);
228
+ if (backtickRuns) {
229
+ const maxLen = Math.max(...backtickRuns.map((r) => r.length));
230
+ fence = "`".repeat(maxLen + 1);
231
+ }
232
+ result = result.replace(codeMatch[0], `${fence}${lang}\n${content}\n${fence}`);
233
+ }
234
+ else {
235
+ // Fallback: substitute {X} directly
236
+ result = result.replace(new RegExp(`\\{${varName}\\}`, "g"), content);
237
+ }
238
+ }
239
+ catch { /* ignore read errors */ }
240
+ }
241
+ }
242
+ return result;
243
+ }
244
+ // ── Render partial resolution ──────────────────────────
245
+ /**
246
+ * Resolve `<Render>` tags by inlining partial MDX content.
247
+ *
248
+ * Cloudflare uses `<Render file="..." product="..." params={{...}} />`
249
+ * to include content from `src/content/partials/{product}/{file}.mdx`.
250
+ *
251
+ * This reads the partial, strips its frontmatter/imports, substitutes
252
+ * `{props.paramName}` with provided params, and returns the body.
253
+ */
254
+ function resolveRenderTags(source, partialsDir, depth = 0) {
255
+ if (!partialsDir || depth > 3)
256
+ return source; // guard against infinite recursion
257
+ return source.replace(/^([ \t]*)<Render\b([\s\S]*?)\/>/gm, (_match, indent, attrsStr) => {
258
+ const attrs = parseRenderAttrs(attrsStr);
259
+ if (!attrs.file || !attrs.product)
260
+ return "";
261
+ const partialPath = join(partialsDir, attrs.product, `${attrs.file}.mdx`);
262
+ if (!existsSync(partialPath))
263
+ return "";
264
+ try {
265
+ let content = readFileSync(partialPath, "utf-8");
266
+ // Strip frontmatter
267
+ if (content.startsWith("---")) {
268
+ const endIdx = content.indexOf("---", 3);
269
+ if (endIdx !== -1) {
270
+ content = content.slice(endIdx + 3).trim();
271
+ }
272
+ }
273
+ // Strip imports
274
+ content = content
275
+ .replace(/^import\s+\{[^}]*\}\s+from\s+["'][^"']*["']\s*;?\s*$/gm, "")
276
+ .replace(/^import\s+\{[\s\S]*?\}\s+from\s+["'][^"']*["']\s*;?\s*$/gm, "");
277
+ // Substitute props.paramName with values from params
278
+ // Handles both {props.key} (in JSX text) and bare props.key (in JS expressions)
279
+ if (attrs.params) {
280
+ for (const [key, value] of Object.entries(attrs.params)) {
281
+ // {props.key} → value (JSX text interpolation)
282
+ content = content.replace(new RegExp(`\\{props\\.${key}\\}`, "g"), value);
283
+ // props.key → 'value' (JS expression context — wrap in quotes)
284
+ content = content.replace(new RegExp(`props\\.${key}`, "g"), `'${value}'`);
285
+ }
286
+ // Resolve JSX switch expressions after param substitution
287
+ content = resolveJsxSwitch(content);
288
+ }
289
+ // Recursively resolve nested Render tags in the partial content
290
+ content = resolveRenderTags(content.trim(), partialsDir, depth + 1);
291
+ // Indent resolved content to match the original tag's position
292
+ return indentContent(content, indent);
293
+ }
294
+ catch {
295
+ return "";
296
+ }
297
+ });
298
+ }
299
+ function parseRenderAttrs(str) {
300
+ const file = str.match(/file\s*=\s*"([^"]*)"/)?.[1];
301
+ const product = str.match(/product\s*=\s*"([^"]*)"/)?.[1];
302
+ let params;
303
+ const paramsMatch = str.match(/params\s*=\s*\{\{([\s\S]*?)\}\}/);
304
+ if (paramsMatch) {
305
+ params = {};
306
+ const pairs = paramsMatch[1].matchAll(/(\w+)\s*:\s*"([^"]*)"/g);
307
+ for (const pair of pairs) {
308
+ params[pair[1]] = pair[2];
309
+ }
310
+ }
311
+ return { file, product, params };
312
+ }
313
+ // ── Adapter export ─────────────────────────────────────
314
+ export const cloudflareAdapter = {
315
+ name: "cloudflare",
316
+ components: {
317
+ // ── Mapped components ─────────────────────────────────
318
+ Feature: {
319
+ type: "card",
320
+ container: true,
321
+ propsFromAttrs: (attrs) => ({
322
+ title: attrs.header || "",
323
+ href: attrs.href,
324
+ }),
325
+ },
326
+ RelatedProduct: {
327
+ type: "card",
328
+ container: true,
329
+ propsFromAttrs: (attrs) => ({
330
+ title: attrs.header || "",
331
+ href: attrs.href,
332
+ icon: attrs.product ? `icons/${attrs.product}.svg` : undefined,
333
+ }),
334
+ },
335
+ Description: {
336
+ type: "description",
337
+ container: true,
338
+ },
339
+ Details: {
340
+ type: "details",
341
+ container: true,
342
+ propsFromAttrs: (attrs) => ({ title: attrs.header || "" }),
343
+ },
344
+ YouTube: {
345
+ type: "youtube",
346
+ container: false,
347
+ propsFromAttrs: (attrs) => ({ id: attrs.id || "" }),
348
+ },
349
+ Stream: {
350
+ type: "youtube",
351
+ container: false,
352
+ propsFromAttrs: (attrs) => ({ id: attrs.id || "" }),
353
+ },
354
+ // ── Stripped components (data-driven, can't resolve) ──
355
+ FeatureTable: { type: "", container: false, strip: true },
356
+ // PackageManagers resolved by resolveIncludes → Tabs with code blocks
357
+ // WranglerConfig resolved by resolveIncludes → Tabs with JSONC + TOML
358
+ WranglerCommand: { type: "", container: true, strip: true },
359
+ TypeScriptExample: { type: "", container: true, strip: true },
360
+ APIRequest: { type: "", container: true, strip: true },
361
+ AvailableNotifications: { type: "", container: false, strip: true },
362
+ CompatibilityFlags: { type: "", container: false, strip: true },
363
+ RSSButton: { type: "", container: false, strip: true },
364
+ ProductReleaseNotes: { type: "", container: false, strip: true },
365
+ // ── Workers/Pages-specific (strip) ───────────────────
366
+ AutoconfigDiagram: { type: "", container: false, strip: true },
367
+ GitHubCode: { type: "", container: false, strip: true },
368
+ ResourcesBySelector: { type: "", container: false, strip: true },
369
+ MetaInfo: { type: "", container: false, strip: true },
370
+ DirectoryListing: { type: "", container: false, strip: true },
371
+ ExternalResources: { type: "", container: false, strip: true },
372
+ YouTubeVideos: { type: "", container: false, strip: true },
373
+ ListTutorials: { type: "", container: false, strip: true },
374
+ WorkersTemplates: { type: "", container: false, strip: true },
375
+ // ── Heading variant (map to heading) ─────────────────
376
+ AnchorHeading: {
377
+ type: "heading",
378
+ container: true,
379
+ propsFromAttrs: (attrs) => ({
380
+ level: parseInt(attrs.level || "2", 10),
381
+ text: attrs.title || "",
382
+ slug: (attrs.title || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
383
+ }),
384
+ },
385
+ },
386
+ selfClosingTags: [
387
+ "FeatureTable",
388
+ "YouTube",
389
+ "Stream",
390
+ "WranglerCommand",
391
+ "TypeScriptExample",
392
+ "APIRequest",
393
+ "AvailableNotifications",
394
+ "RSSButton",
395
+ "ProductReleaseNotes",
396
+ "AutoconfigDiagram",
397
+ "GitHubCode",
398
+ "ResourcesBySelector",
399
+ "MetaInfo",
400
+ "DirectoryListing",
401
+ "ExternalResources",
402
+ "YouTubeVideos",
403
+ "ListTutorials",
404
+ "WorkersTemplates",
405
+ "CompatibilityFlags",
406
+ ],
407
+ // Inline components — transformed in source text before block parsing
408
+ inlineTransforms: [
409
+ // <GlossaryTooltip term="..." link="...">text</GlossaryTooltip> → [text](link) or just text
410
+ [
411
+ /<GlossaryTooltip\s+term="([^"]*)"(?:\s+link="([^"]*)")?[^>]*>([\s\S]*?)<\/GlossaryTooltip>/g,
412
+ "$3",
413
+ ],
414
+ // <Plan type="all" /> → **Available on all plans**
415
+ [/<Plan\s+type="all"\s*\/?>/g, "**Available on all plans**"],
416
+ [/<Plan\s+type="([^"]*)"\s*\/?>/g, "**$1 plan**"],
417
+ // <InlineBadge ... /> → strip
418
+ [/<InlineBadge\s[^>]*\/>/g, ""],
419
+ // Fallback strip for self-closing components that may appear deeply indented
420
+ // where the block rule can't reach (inside Steps > list items etc.)
421
+ // PackageManagers resolved by resolveIncludes (not stripped)
422
+ [/<AutoconfigDiagram[\s\S]*?\/>/g, ""],
423
+ [/<GitHubCode\s[^>]*\/>/g, ""],
424
+ [/<ResourcesBySelector[\s\S]*?\/>/g, ""],
425
+ [/<MetaInfo\s[^>]*\/>/g, ""],
426
+ [/<DirectoryListing[\s\S]*?\/>/g, ""],
427
+ [/<ExternalResources\s[^>]*\/>/g, ""],
428
+ [/<YouTubeVideos\s[^>]*\/>/g, ""],
429
+ [/<SUBCOMMAND\s[^>]*\/>/g, ""],
430
+ ],
431
+ // Resolve <Render> partials and <DashButton> links
432
+ resolveIncludes(source, partialsDir) {
433
+ let result = resolveRawImports(source, partialsDir);
434
+ result = resolveRenderTags(result, partialsDir);
435
+ result = resolveDashButton(result, partialsDir);
436
+ result = resolvePackageManagers(result);
437
+ result = resolveWranglerConfig(result);
438
+ return result;
439
+ },
440
+ // Redirect pages use external_link in frontmatter
441
+ redirectField: "external_link",
442
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { FormatPlugin } from "@dogsbay/types";
2
+ export declare const plugin: FormatPlugin;