@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.
- package/dist/adapter.d.ts +46 -0
- package/dist/adapter.js +11 -0
- package/dist/adapters/cloudflare.d.ts +10 -0
- package/dist/adapters/cloudflare.js +442 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +231 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/nav.d.ts +32 -0
- package/dist/nav.js +332 -0
- package/dist/parse-mdx.d.ts +22 -0
- package/dist/parse-mdx.js +554 -0
- package/dist/parse.d.ts +34 -0
- package/dist/parse.js +729 -0
- package/package.json +43 -0
|
@@ -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
|
+
}
|
package/dist/adapter.js
ADDED
|
@@ -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