blodemd 0.0.13 → 0.0.14
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/README.md +1 -1
- package/dev-server/lib/local-runtime.tsx +12 -15
- package/dev-server/package.json +5 -1
- package/dist/cli.mjs +1193 -978
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +1 -1
- package/docs/components/api/api-playground.tsx +2 -5
- package/docs/components/api/api-reference.tsx +29 -3
- package/docs/components/docs/contextual-menu.tsx +37 -33
- package/docs/components/docs/copy-page-menu.tsx +18 -0
- package/docs/components/docs/doc-header.tsx +4 -1
- package/docs/components/docs/doc-shell.tsx +48 -18
- package/docs/components/docs/doc-sidebar.tsx +131 -89
- package/docs/components/docs/doc-toc.tsx +1 -0
- package/docs/components/docs/mobile-nav.tsx +72 -68
- package/docs/components/docs/sidebar-active-highlight.tsx +34 -0
- package/docs/components/docs/sidebar-scroll-area.tsx +9 -4
- package/docs/components/git/repo-picker.tsx +5 -2
- package/docs/components/mdx/index.tsx +31 -21
- package/docs/components/posthog-provider.tsx +42 -0
- package/docs/components/tenant-analytics.tsx +49 -0
- package/docs/components/third-parties.tsx +4 -1
- package/docs/components/ui/copy-button.tsx +16 -2
- package/docs/components/ui/search-dialog.tsx +431 -0
- package/docs/components/ui/search.tsx +44 -427
- package/docs/components/web-mcp.tsx +115 -0
- package/docs/lib/agent-skills.ts +48 -0
- package/docs/lib/config.ts +4 -5
- package/docs/lib/docs-collection.ts +7 -22
- package/docs/lib/docs-runtime.tsx +160 -10
- package/docs/lib/llms-full.ts +82 -0
- package/docs/lib/marketing-markdown.ts +79 -0
- package/docs/lib/marketing-site.ts +31 -0
- package/docs/lib/mdx.ts +17 -6
- package/docs/lib/routes.ts +19 -3
- package/docs/lib/shiki.ts +4 -0
- package/docs/lib/tenancy.ts +15 -2
- package/docs/lib/tenant-headers.ts +29 -0
- package/docs/lib/tenant-static.ts +73 -99
- package/package.json +8 -5
- package/packages/@repo/common/package.json +1 -1
- package/packages/@repo/contracts/dist/analytics.d.ts +21 -0
- package/packages/@repo/contracts/dist/analytics.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/analytics.js +18 -0
- package/packages/@repo/contracts/dist/deployment.js +1 -1
- package/packages/@repo/contracts/dist/git.js +2 -2
- package/packages/@repo/contracts/dist/ids.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/ids.js +2 -1
- package/packages/@repo/contracts/dist/index.d.ts +1 -0
- package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/index.js +1 -0
- package/packages/@repo/contracts/dist/project.d.ts +18 -1
- package/packages/@repo/contracts/dist/project.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/project.js +3 -1
- package/packages/@repo/contracts/dist/tenant.d.ts +36 -0
- package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/tenant.js +2 -0
- package/packages/@repo/contracts/package.json +1 -1
- package/packages/@repo/contracts/src/analytics.ts +33 -0
- package/packages/@repo/contracts/src/deployment.ts +1 -1
- package/packages/@repo/contracts/src/git.ts +2 -2
- package/packages/@repo/contracts/src/ids.ts +2 -1
- package/packages/@repo/contracts/src/index.ts +1 -0
- package/packages/@repo/contracts/src/project.ts +3 -1
- package/packages/@repo/contracts/src/tenant.ts +2 -0
- package/packages/@repo/models/dist/docs-config.d.ts +0 -198
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -2
- package/packages/@repo/models/dist/tenant.d.ts +12 -0
- package/packages/@repo/models/dist/tenant.d.ts.map +1 -1
- package/packages/@repo/models/package.json +1 -1
- package/packages/@repo/models/src/docs-config.ts +3 -3
- package/packages/@repo/models/src/tenant.ts +15 -0
- package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -1
- package/packages/@repo/prebuild/dist/openapi.js +32 -4
- package/packages/@repo/prebuild/package.json +1 -1
- package/packages/@repo/prebuild/src/openapi.ts +42 -4
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +18 -5
- package/packages/@repo/previewing/dist/config-loader.d.ts +134 -0
- package/packages/@repo/previewing/dist/config-loader.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/config-loader.js +231 -0
- package/packages/@repo/previewing/dist/constants.d.ts +15 -0
- package/packages/@repo/previewing/dist/constants.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/constants.js +14 -0
- package/packages/@repo/previewing/dist/content-index.d.ts +7 -0
- package/packages/@repo/previewing/dist/content-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/content-index.js +291 -0
- package/packages/@repo/previewing/dist/index.d.ts +13 -121
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +11 -1003
- package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +3 -0
- package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/agent-markdown.js +176 -0
- package/packages/@repo/previewing/dist/markdown/format.d.ts +8 -0
- package/packages/@repo/previewing/dist/markdown/format.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/format.js +43 -0
- package/packages/@repo/previewing/dist/markdown/links.d.ts +3 -0
- package/packages/@repo/previewing/dist/markdown/links.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/links.js +31 -0
- package/packages/@repo/previewing/dist/openapi-pages.d.ts +7 -0
- package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/openapi-pages.js +200 -0
- package/packages/@repo/previewing/dist/search-index.d.ts +4 -0
- package/packages/@repo/previewing/dist/search-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/search-index.js +23 -0
- package/packages/@repo/previewing/dist/serialization.d.ts +13 -0
- package/packages/@repo/previewing/dist/serialization.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/serialization.js +108 -0
- package/packages/@repo/previewing/dist/toc-index.d.ts +5 -0
- package/packages/@repo/previewing/dist/toc-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/toc-index.js +53 -0
- package/packages/@repo/previewing/dist/types.d.ts +92 -0
- package/packages/@repo/previewing/dist/types.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/types.js +1 -0
- package/packages/@repo/previewing/dist/utility-index.d.ts +7 -0
- package/packages/@repo/previewing/dist/utility-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/utility-index.js +201 -0
- package/packages/@repo/previewing/package.json +1 -1
- package/packages/@repo/previewing/src/blob-source.ts +20 -5
- package/packages/@repo/previewing/src/config-loader.ts +316 -0
- package/packages/@repo/previewing/src/constants.ts +16 -0
- package/packages/@repo/previewing/src/content-index.ts +384 -0
- package/packages/@repo/previewing/src/index.ts +69 -1515
- package/packages/@repo/previewing/src/markdown/agent-markdown.ts +254 -0
- package/packages/@repo/previewing/src/markdown/format.ts +63 -0
- package/packages/@repo/previewing/src/markdown/links.ts +46 -0
- package/packages/@repo/previewing/src/openapi-pages.ts +319 -0
- package/packages/@repo/previewing/src/search-index.ts +38 -0
- package/packages/@repo/previewing/src/serialization.ts +191 -0
- package/packages/@repo/previewing/src/toc-index.ts +72 -0
- package/packages/@repo/previewing/src/types.ts +105 -0
- package/packages/@repo/previewing/src/utility-index.ts +304 -0
- package/packages/@repo/validation/package.json +1 -1
- package/scripts/repo-packages.mjs +8 -0
- package/docs/components/ui/site-footer.tsx +0 -41
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { NEWLINE_REGEX } from "../constants.js";
|
|
2
|
+
import { absolutiseInternalLinks, sanitizePlaceholderUrls } from "./links.js";
|
|
3
|
+
|
|
4
|
+
const FENCED_CODE_BLOCK_REGEX =
|
|
5
|
+
/(^|\n)(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n\2(?=\n|$)/g;
|
|
6
|
+
const TYPE_TABLE_REGEX = /<TypeTable\s+type=\{\{([\s\S]*?)\}\}\s*\/>/g;
|
|
7
|
+
const TYPE_TABLE_ROW_HEADER_REGEX =
|
|
8
|
+
/^\s*(["']?[\w$./<>|{}\-\s]+["']?)\s*:\s*\{\s*$/;
|
|
9
|
+
const STRING_FIELD_REGEX = (field: string) =>
|
|
10
|
+
new RegExp(`${field}:\\s*(?:"([^"]*)"|'([^']*)'|\`([^\`]*)\`)`, "s");
|
|
11
|
+
const BOOLEAN_FIELD_REGEX = (field: string) =>
|
|
12
|
+
new RegExp(`${field}:\\s*(true|false)`, "s");
|
|
13
|
+
|
|
14
|
+
const getStringProp = (attributes: string, name: string) => {
|
|
15
|
+
const match = new RegExp(
|
|
16
|
+
`${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|\\{\\s*"([^"]*)"\\s*\\}|\\{\\s*'([^']*)'\\s*\\})`
|
|
17
|
+
).exec(attributes);
|
|
18
|
+
return match?.slice(1).find((value) => typeof value === "string");
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const protectFencedCodeBlocks = (source: string) => {
|
|
22
|
+
const blocks: string[] = [];
|
|
23
|
+
const text = source.replace(FENCED_CODE_BLOCK_REGEX, (match) => {
|
|
24
|
+
const placeholder = `\n@@BLODEMD_CODE_BLOCK_${blocks.length}@@\n`;
|
|
25
|
+
blocks.push(match.trim());
|
|
26
|
+
return placeholder;
|
|
27
|
+
});
|
|
28
|
+
return { blocks, text };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const restoreFencedCodeBlocks = (source: string, blocks: string[]) => {
|
|
32
|
+
let restored = source;
|
|
33
|
+
for (const [index, block] of blocks.entries()) {
|
|
34
|
+
// Function replacer: code may contain `$&`, `$1`, `` $` `` etc. which a
|
|
35
|
+
// string replacement would interpret as substitution patterns.
|
|
36
|
+
restored = restored.replace(`@@BLODEMD_CODE_BLOCK_${index}@@`, () => block);
|
|
37
|
+
}
|
|
38
|
+
return restored;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const INLINE_CODE_REGEX = /(?<![\\`])`([^`\n]+)`(?!`)/g;
|
|
42
|
+
|
|
43
|
+
const protectInlineCode = (source: string) => {
|
|
44
|
+
const spans: string[] = [];
|
|
45
|
+
const text = source.replace(INLINE_CODE_REGEX, (match) => {
|
|
46
|
+
const placeholder = `@@BLODEMD_INLINE_CODE_${spans.length}@@`;
|
|
47
|
+
spans.push(match);
|
|
48
|
+
return placeholder;
|
|
49
|
+
});
|
|
50
|
+
return { spans, text };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const restoreInlineCode = (source: string, spans: string[]) => {
|
|
54
|
+
let restored = source;
|
|
55
|
+
for (const [index, span] of spans.entries()) {
|
|
56
|
+
restored = restored.replace(`@@BLODEMD_INLINE_CODE_${index}@@`, () => span);
|
|
57
|
+
}
|
|
58
|
+
return restored;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const compactMarkdown = (source: string) =>
|
|
62
|
+
source
|
|
63
|
+
.replaceAll(/[ \t]+\n/g, "\n")
|
|
64
|
+
.replaceAll(/\n{3,}/g, "\n\n")
|
|
65
|
+
.trim();
|
|
66
|
+
|
|
67
|
+
const getFieldStringValue = (block: string, field: string) => {
|
|
68
|
+
const match = STRING_FIELD_REGEX(field).exec(block);
|
|
69
|
+
return match?.slice(1).find((value) => typeof value === "string");
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const getFieldBooleanValue = (block: string, field: string) => {
|
|
73
|
+
const match = BOOLEAN_FIELD_REGEX(field).exec(block);
|
|
74
|
+
return match?.[1];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const renderTypeTable = (_match: string, body: string) => {
|
|
78
|
+
const rows: string[] = [];
|
|
79
|
+
const lines = body.split(NEWLINE_REGEX);
|
|
80
|
+
|
|
81
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
82
|
+
const header = TYPE_TABLE_ROW_HEADER_REGEX.exec(lines[index] ?? "");
|
|
83
|
+
if (!header) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const name = (header[1] ?? "").replaceAll(/^["']|["']$/g, "").trim();
|
|
88
|
+
const blockLines: string[] = [];
|
|
89
|
+
index += 1;
|
|
90
|
+
while (index < lines.length) {
|
|
91
|
+
const line = lines[index] ?? "";
|
|
92
|
+
if (/^\s*\},?\s*$/.test(line)) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
blockLines.push(line);
|
|
96
|
+
index += 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const block = blockLines.join("\n");
|
|
100
|
+
const type = getFieldStringValue(block, "type") ?? "";
|
|
101
|
+
const description = getFieldStringValue(block, "description") ?? "";
|
|
102
|
+
const required = getFieldBooleanValue(block, "required") ?? "";
|
|
103
|
+
const defaultValue = getFieldStringValue(block, "default") ?? "";
|
|
104
|
+
const meta = [
|
|
105
|
+
type ? `type: ${type}` : null,
|
|
106
|
+
required ? `required: ${required}` : null,
|
|
107
|
+
defaultValue ? `default: ${defaultValue}` : null,
|
|
108
|
+
]
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join(", ");
|
|
111
|
+
rows.push(`- \`${name}\`${meta ? ` (${meta})` : ""}: ${description}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return rows.length ? rows.join("\n") : "";
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const transformMdxComponents = (source: string) => {
|
|
118
|
+
let output = source;
|
|
119
|
+
|
|
120
|
+
output = output.replaceAll(TYPE_TABLE_REGEX, renderTypeTable);
|
|
121
|
+
output = output.replaceAll(
|
|
122
|
+
/<Installer\s+([^>]*)\/>/g,
|
|
123
|
+
(_match, attributes: string) => {
|
|
124
|
+
const command = getStringProp(attributes, "command");
|
|
125
|
+
return command ? `\`\`\`bash\n${command}\n\`\`\`` : "";
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
output = output.replaceAll(
|
|
129
|
+
/<Tree\.Folder\s+([^>]*)>/g,
|
|
130
|
+
(_match, attributes: string) => {
|
|
131
|
+
const name = getStringProp(attributes, "name") ?? "folder";
|
|
132
|
+
return `- ${name}/`;
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
output = output.replaceAll(
|
|
136
|
+
/<Tree\.File\s+([^>]*)\/>/g,
|
|
137
|
+
(_match, attributes: string) => {
|
|
138
|
+
const name = getStringProp(attributes, "name") ?? "file";
|
|
139
|
+
return `- ${name}`;
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
output = output.replaceAll("</Tree.Folder>", "");
|
|
143
|
+
output = output.replaceAll(/<\/?Tree[^>]*>/g, "");
|
|
144
|
+
output = output.replaceAll(
|
|
145
|
+
/<Frame(?=[\s>])\s*([^>]*)>([\s\S]*?)<\/Frame>/g,
|
|
146
|
+
(_match, attributes: string, children: string) => {
|
|
147
|
+
const caption = getStringProp(attributes, "caption");
|
|
148
|
+
const hint = getStringProp(attributes, "hint");
|
|
149
|
+
return compactMarkdown(
|
|
150
|
+
[hint, children.trim(), caption ? `Caption: ${caption}` : null]
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.join("\n\n")
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
output = output.replaceAll(
|
|
157
|
+
/<Callout(?=[\s>])\s*([^>]*)>([\s\S]*?)<\/Callout>/g,
|
|
158
|
+
(_match, attributes: string, children: string) => {
|
|
159
|
+
const type = (getStringProp(attributes, "type") ?? "note").toUpperCase();
|
|
160
|
+
return `> [!${type}]\n${children
|
|
161
|
+
.trim()
|
|
162
|
+
.split(NEWLINE_REGEX)
|
|
163
|
+
.map((line) => `> ${line}`)
|
|
164
|
+
.join("\n")}`;
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
for (const [tag, type] of [
|
|
168
|
+
["Note", "NOTE"],
|
|
169
|
+
["Warning", "WARNING"],
|
|
170
|
+
["Info", "INFO"],
|
|
171
|
+
["Tip", "TIP"],
|
|
172
|
+
["Check", "CHECK"],
|
|
173
|
+
["Danger", "DANGER"],
|
|
174
|
+
] as const) {
|
|
175
|
+
output = output.replaceAll(
|
|
176
|
+
new RegExp(`<${tag}(?=[\\s>])\\s*[^>]*>([\\s\\S]*?)</${tag}>`, "g"),
|
|
177
|
+
(_match, children: string) =>
|
|
178
|
+
`> [!${type}]\n${children
|
|
179
|
+
.trim()
|
|
180
|
+
.split(NEWLINE_REGEX)
|
|
181
|
+
.map((line) => `> ${line}`)
|
|
182
|
+
.join("\n")}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
output = output.replaceAll(
|
|
186
|
+
/<Accordion(?=[\s>])\s*([^>]*)>([\s\S]*?)<\/Accordion>/g,
|
|
187
|
+
(_match, attributes: string, children: string) => {
|
|
188
|
+
const title = getStringProp(attributes, "title") ?? "Details";
|
|
189
|
+
return `### ${title}\n\n${children.trim()}`;
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
output = output.replaceAll(
|
|
193
|
+
/<Tab\s+([^>]*)>([\s\S]*?)<\/Tab>/g,
|
|
194
|
+
(_match, attributes: string, children: string) => {
|
|
195
|
+
const title =
|
|
196
|
+
getStringProp(attributes, "title") ??
|
|
197
|
+
getStringProp(attributes, "label") ??
|
|
198
|
+
"Tab";
|
|
199
|
+
return `### ${title}\n\n${children.trim()}`;
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
output = output.replaceAll(
|
|
203
|
+
/<Step\s+([^>]*)>([\s\S]*?)<\/Step>/g,
|
|
204
|
+
(_match, attributes: string, children: string) => {
|
|
205
|
+
const title = getStringProp(attributes, "title") ?? "Step";
|
|
206
|
+
return `1. **${title}**\n\n${children.trim()}`;
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
output = output.replaceAll(
|
|
210
|
+
/<Expandable(?=[\s>])\s*([^>]*)>([\s\S]*?)<\/Expandable>/g,
|
|
211
|
+
(_match, attributes: string, children: string) => {
|
|
212
|
+
const title = getStringProp(attributes, "title") ?? "Details";
|
|
213
|
+
return `### ${title}\n\n${children.trim()}`;
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
output = output.replaceAll(
|
|
217
|
+
/<Card(?=[\s>])\s*([^>]*)>([\s\S]*?)<\/Card>/g,
|
|
218
|
+
(_match, attributes: string, children: string) => {
|
|
219
|
+
const title = getStringProp(attributes, "title");
|
|
220
|
+
const href = getStringProp(attributes, "href");
|
|
221
|
+
const heading = title
|
|
222
|
+
? `### ${href ? `[${title}](${href})` : title}`
|
|
223
|
+
: "";
|
|
224
|
+
return compactMarkdown(
|
|
225
|
+
[heading, children.trim()].filter(Boolean).join("\n\n")
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
output = output.replaceAll(
|
|
230
|
+
/<\/?(?:Columns|Column|CodeGroup|Tabs|Steps|AccordionGroup|CardGroup)[^>]*>/g,
|
|
231
|
+
""
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return compactMarkdown(output);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const toAgentMarkdown = (source: string): string => {
|
|
238
|
+
const { blocks, text: codeProtected } = protectFencedCodeBlocks(source);
|
|
239
|
+
const { spans, text: inlineProtected } = protectInlineCode(codeProtected);
|
|
240
|
+
const transformed = transformMdxComponents(inlineProtected);
|
|
241
|
+
const inlineRestored = restoreInlineCode(transformed, spans);
|
|
242
|
+
return compactMarkdown(restoreFencedCodeBlocks(inlineRestored, blocks));
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const prepareLlmsFullContent = (
|
|
246
|
+
source: string,
|
|
247
|
+
origin: string,
|
|
248
|
+
basePath: string
|
|
249
|
+
) =>
|
|
250
|
+
absolutiseInternalLinks(
|
|
251
|
+
sanitizePlaceholderUrls(toAgentMarkdown(source)),
|
|
252
|
+
origin,
|
|
253
|
+
basePath
|
|
254
|
+
);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { slugify } from "@repo/common";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
|
|
4
|
+
const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
5
|
+
const LEADING_H1_REGEX = /^#\s+([^\r\n]+)(?:\r?\n(?:\r?\n)?)?/;
|
|
6
|
+
|
|
7
|
+
export const parseFrontmatter = (source: string) => {
|
|
8
|
+
const match = FRONTMATTER_REGEX.exec(source);
|
|
9
|
+
if (!match) {
|
|
10
|
+
return { body: source, frontmatter: {} };
|
|
11
|
+
}
|
|
12
|
+
const raw = match[1] ?? "";
|
|
13
|
+
const data = YAML.parse(raw) ?? {};
|
|
14
|
+
const body = source.slice(match[0].length);
|
|
15
|
+
return { body, frontmatter: data };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const stripFrontmatter = (source: string) =>
|
|
19
|
+
parseFrontmatter(source).body.trim();
|
|
20
|
+
|
|
21
|
+
const stripMatchingLeadingH1 = (source: string, title: string) => {
|
|
22
|
+
const trimmed = source.trimStart();
|
|
23
|
+
const match = LEADING_H1_REGEX.exec(trimmed);
|
|
24
|
+
if (!match) {
|
|
25
|
+
return trimmed.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const [headingLine = "", headingTitle = ""] = match;
|
|
29
|
+
if (slugify(headingTitle) !== slugify(title)) {
|
|
30
|
+
return trimmed.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return trimmed.slice(headingLine.length).trim();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const formatMarkdownPage = (
|
|
37
|
+
title: string,
|
|
38
|
+
source: string,
|
|
39
|
+
description?: string
|
|
40
|
+
) => {
|
|
41
|
+
const content = stripMatchingLeadingH1(source, title);
|
|
42
|
+
const descriptionBlock = description ? `\n\n${description}` : "";
|
|
43
|
+
if (!content) {
|
|
44
|
+
return `# ${title}${descriptionBlock}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return `# ${title}${descriptionBlock}\n\n${content}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const formatMarkdownPageSection = (
|
|
51
|
+
title: string,
|
|
52
|
+
url: string,
|
|
53
|
+
source: string,
|
|
54
|
+
description?: string
|
|
55
|
+
) => {
|
|
56
|
+
const content = stripMatchingLeadingH1(source, title);
|
|
57
|
+
const descriptionBlock = description ? `\n\n${description}` : "";
|
|
58
|
+
if (!content) {
|
|
59
|
+
return `# ${title} (${url})${descriptionBlock}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return `# ${title} (${url})${descriptionBlock}\n\n${content}`;
|
|
63
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const PLACEHOLDER_URL_PATTERN = [
|
|
2
|
+
"https?://(?:[a-z0-9-]+\\.)*example\\.(?:com|org|net)\\b[^\\s)\\]\"'<>]*",
|
|
3
|
+
"https?://discord\\.gg/example\\b[^\\s)\\]\"'<>]*",
|
|
4
|
+
"https?://(?:[a-z0-9-]+\\.)+(?:test|invalid)(?:[/?#:][^\\s)\\]\"'<>]*)?",
|
|
5
|
+
"https?://localhost(?::\\d+)?[^\\s)\\]\"'<>]*",
|
|
6
|
+
"https?://(?:[a-z0-9-]+\\.)*your[-_]?domain\\.[a-z]+\\b[^\\s)\\]\"'<>]*",
|
|
7
|
+
"https?://acme\\.blode\\.md\\b[^\\s)\\]\"'<>]*",
|
|
8
|
+
"https?://github\\.com/example/[^\\s)\\]\"'<>]*",
|
|
9
|
+
"https?://(?:us|eu)\\.i\\.posthog\\.com\\b[^\\s)\\]\"'<>]*",
|
|
10
|
+
].join("|");
|
|
11
|
+
const PLACEHOLDER_URL_RE = new RegExp(PLACEHOLDER_URL_PATTERN, "gi");
|
|
12
|
+
const PLACEHOLDER_MARKDOWN_LINK_RE = new RegExp(
|
|
13
|
+
`\\[([^\\]]+)\\]\\((?:${PLACEHOLDER_URL_PATTERN})\\)`,
|
|
14
|
+
"gi"
|
|
15
|
+
);
|
|
16
|
+
const INTERNAL_MARKDOWN_LINK_RE = /(!?\[[^\]]+\])\((\/[^)\s]*)\)/g;
|
|
17
|
+
|
|
18
|
+
const defangUrl = (value: string) => value.replace(/^https?:\/\//i, "");
|
|
19
|
+
|
|
20
|
+
export const sanitizePlaceholderUrls = (text: string): string =>
|
|
21
|
+
text
|
|
22
|
+
.replace(PLACEHOLDER_MARKDOWN_LINK_RE, "$1")
|
|
23
|
+
.replace(PLACEHOLDER_URL_RE, (match) => `\`${defangUrl(match)}\``);
|
|
24
|
+
|
|
25
|
+
export const absolutiseInternalLinks = (
|
|
26
|
+
source: string,
|
|
27
|
+
origin: string,
|
|
28
|
+
basePath: string
|
|
29
|
+
): string => {
|
|
30
|
+
const normalizedBase = basePath
|
|
31
|
+
? `/${basePath}`.replaceAll(/\/+/g, "/").replace(/\/$/, "")
|
|
32
|
+
: "";
|
|
33
|
+
return source.replaceAll(
|
|
34
|
+
INTERNAL_MARKDOWN_LINK_RE,
|
|
35
|
+
(_match, label, linkPath) => {
|
|
36
|
+
const alreadyPrefixed =
|
|
37
|
+
normalizedBase &&
|
|
38
|
+
(linkPath === normalizedBase ||
|
|
39
|
+
linkPath.startsWith(`${normalizedBase}/`));
|
|
40
|
+
const absolutePath = alreadyPrefixed
|
|
41
|
+
? linkPath
|
|
42
|
+
: `${normalizedBase}${linkPath}`.replaceAll(/\/+/g, "/");
|
|
43
|
+
return `${label}(${origin}${absolutePath})`;
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { ensureArray, normalizePath } from "@repo/common";
|
|
2
|
+
import type {
|
|
3
|
+
CollectionConfig,
|
|
4
|
+
DocsOpenApiSource,
|
|
5
|
+
SiteConfig,
|
|
6
|
+
} from "@repo/models";
|
|
7
|
+
import {
|
|
8
|
+
extractOpenApiOperations,
|
|
9
|
+
openApiIdentifier,
|
|
10
|
+
openApiSlug,
|
|
11
|
+
parseOpenApiSpec,
|
|
12
|
+
} from "@repo/prebuild";
|
|
13
|
+
import type { OpenApiOperation } from "@repo/prebuild";
|
|
14
|
+
|
|
15
|
+
import { getDocsNavigation } from "./config-loader.js";
|
|
16
|
+
import type { ContentSource } from "./content-source.js";
|
|
17
|
+
import type { UtilityPage } from "./types.js";
|
|
18
|
+
|
|
19
|
+
interface UtilityOpenApiPage extends UtilityPage {
|
|
20
|
+
identifier: string;
|
|
21
|
+
sourceKey: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getOpenApiSourceKey = (source: DocsOpenApiSource): string =>
|
|
25
|
+
`${source.source}::${source.directory ?? ""}::${(source.include ?? []).join(
|
|
26
|
+
"|"
|
|
27
|
+
)}`;
|
|
28
|
+
|
|
29
|
+
const toOpenApiSourceObject = (
|
|
30
|
+
value: string | DocsOpenApiSource
|
|
31
|
+
): DocsOpenApiSource => {
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
return { source: value };
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const collectOpenApiSources = (collection?: CollectionConfig) => {
|
|
39
|
+
const sources: DocsOpenApiSource[] = [];
|
|
40
|
+
|
|
41
|
+
for (const item of ensureArray(collection?.openapi)) {
|
|
42
|
+
if (!item) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
sources.push(toOpenApiSourceObject(item));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const groups = collection?.navigation?.groups ?? [];
|
|
49
|
+
for (const group of groups) {
|
|
50
|
+
if (!group.openapi) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
sources.push(toOpenApiSourceObject(group.openapi));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const seen = new Set<string>();
|
|
57
|
+
return sources.filter((source) => {
|
|
58
|
+
const key = getOpenApiSourceKey(source);
|
|
59
|
+
if (seen.has(key)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
seen.add(key);
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getSchemaTypeLabel = (schema: unknown): string => {
|
|
68
|
+
if (!schema || typeof schema !== "object") {
|
|
69
|
+
return "any";
|
|
70
|
+
}
|
|
71
|
+
const record = schema as Record<string, unknown>;
|
|
72
|
+
if (typeof record.$ref === "string") {
|
|
73
|
+
const last = record.$ref.split("/").pop();
|
|
74
|
+
return last ?? "object";
|
|
75
|
+
}
|
|
76
|
+
if (typeof record.type === "string") {
|
|
77
|
+
if (record.type === "array" && record.items) {
|
|
78
|
+
return `${getSchemaTypeLabel(record.items)}[]`;
|
|
79
|
+
}
|
|
80
|
+
return record.type;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(record.oneOf) || Array.isArray(record.anyOf)) {
|
|
83
|
+
const variants = (record.oneOf ?? record.anyOf) as unknown[];
|
|
84
|
+
return variants.map(getSchemaTypeLabel).join(" | ");
|
|
85
|
+
}
|
|
86
|
+
return "object";
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const formatParameterLine = (parameter: Record<string, unknown>): string => {
|
|
90
|
+
const name = String(parameter.name ?? "");
|
|
91
|
+
const location = parameter.in ? ` (${parameter.in})` : "";
|
|
92
|
+
const required = parameter.required ? "required" : "optional";
|
|
93
|
+
const type = getSchemaTypeLabel(parameter.schema);
|
|
94
|
+
const description =
|
|
95
|
+
typeof parameter.description === "string" && parameter.description
|
|
96
|
+
? ` - ${parameter.description.split("\n")[0]}`
|
|
97
|
+
: "";
|
|
98
|
+
return `- \`${name}\`${location}: ${type}, ${required}${description}`;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const formatRequestBody = (body: Record<string, unknown>): string => {
|
|
102
|
+
const required = body.required ? "required" : "optional";
|
|
103
|
+
const content = (body.content as Record<string, unknown> | undefined) ?? {};
|
|
104
|
+
const mediaTypes = Object.keys(content);
|
|
105
|
+
if (mediaTypes.length === 0) {
|
|
106
|
+
return `${required} body`;
|
|
107
|
+
}
|
|
108
|
+
return mediaTypes
|
|
109
|
+
.map((mediaType) => {
|
|
110
|
+
const mediaEntry = content[mediaType] as
|
|
111
|
+
| Record<string, unknown>
|
|
112
|
+
| undefined;
|
|
113
|
+
const schemaLabel = getSchemaTypeLabel(mediaEntry?.schema);
|
|
114
|
+
return `- ${mediaType} (${required}): ${schemaLabel}`;
|
|
115
|
+
})
|
|
116
|
+
.join("\n");
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const formatResponses = (responses: Record<string, unknown>): string =>
|
|
120
|
+
Object.entries(responses)
|
|
121
|
+
.map(([status, value]) => {
|
|
122
|
+
const response = (value as Record<string, unknown>) ?? {};
|
|
123
|
+
const description =
|
|
124
|
+
typeof response.description === "string" && response.description
|
|
125
|
+
? response.description.split("\n")[0]
|
|
126
|
+
: "";
|
|
127
|
+
const content =
|
|
128
|
+
(response.content as Record<string, unknown> | undefined) ?? {};
|
|
129
|
+
const mediaTypes = Object.keys(content);
|
|
130
|
+
const mediaSuffix = mediaTypes.length
|
|
131
|
+
? ` [${mediaTypes.join(", ")}]`
|
|
132
|
+
: "";
|
|
133
|
+
return `- ${status}${mediaSuffix}${description ? ` - ${description}` : ""}`;
|
|
134
|
+
})
|
|
135
|
+
.join("\n");
|
|
136
|
+
|
|
137
|
+
export const formatOpenApiPageContent = (
|
|
138
|
+
operation: OpenApiOperation
|
|
139
|
+
): string => {
|
|
140
|
+
const parts = [`Method: ${operation.method}`, `Path: ${operation.path}`];
|
|
141
|
+
|
|
142
|
+
if (operation.description) {
|
|
143
|
+
parts.push(operation.description);
|
|
144
|
+
}
|
|
145
|
+
if (operation.tags.length) {
|
|
146
|
+
parts.push(`Tags: ${operation.tags.join(", ")}`);
|
|
147
|
+
}
|
|
148
|
+
if (operation.parameters.length) {
|
|
149
|
+
parts.push(
|
|
150
|
+
`## Parameters\n\n${operation.parameters
|
|
151
|
+
.map(formatParameterLine)
|
|
152
|
+
.join("\n")}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (operation.requestBody) {
|
|
156
|
+
parts.push(
|
|
157
|
+
`## Request body\n\n${formatRequestBody(operation.requestBody)}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (operation.responses) {
|
|
161
|
+
parts.push(`## Responses\n\n${formatResponses(operation.responses)}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return parts.join("\n\n");
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const getGroupedOpenApiSourceKey = (
|
|
168
|
+
source: string | DocsOpenApiSource
|
|
169
|
+
): string => getOpenApiSourceKey(toOpenApiSourceObject(source));
|
|
170
|
+
|
|
171
|
+
const collectUtilityOpenApiPages = (
|
|
172
|
+
pagesByIdentifier: Map<string, UtilityOpenApiPage>,
|
|
173
|
+
pagesBySource: Map<string, UtilityOpenApiPage[]>,
|
|
174
|
+
operations: OpenApiOperation[],
|
|
175
|
+
directory: string,
|
|
176
|
+
openApiSource: DocsOpenApiSource,
|
|
177
|
+
slugPrefix: string
|
|
178
|
+
) => {
|
|
179
|
+
const sourceKey = getOpenApiSourceKey(openApiSource);
|
|
180
|
+
const includeIdentifiers = openApiSource.include?.length
|
|
181
|
+
? new Set(openApiSource.include)
|
|
182
|
+
: null;
|
|
183
|
+
|
|
184
|
+
for (const operation of operations) {
|
|
185
|
+
const identifier = openApiIdentifier(operation.method, operation.path);
|
|
186
|
+
if (includeIdentifiers && !includeIdentifiers.has(identifier)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const baseSlug = normalizePath(
|
|
191
|
+
openApiSlug(operation.method, operation.path, directory)
|
|
192
|
+
);
|
|
193
|
+
const slug = slugPrefix
|
|
194
|
+
? normalizePath(`${slugPrefix}/${baseSlug}`)
|
|
195
|
+
: baseSlug;
|
|
196
|
+
const page = {
|
|
197
|
+
content: formatOpenApiPageContent(operation),
|
|
198
|
+
description: operation.description,
|
|
199
|
+
identifier,
|
|
200
|
+
slug,
|
|
201
|
+
sourceKey,
|
|
202
|
+
title: operation.summary ?? identifier,
|
|
203
|
+
} satisfies UtilityOpenApiPage;
|
|
204
|
+
|
|
205
|
+
pagesByIdentifier.set(identifier, page);
|
|
206
|
+
if (!pagesBySource.has(sourceKey)) {
|
|
207
|
+
pagesBySource.set(sourceKey, []);
|
|
208
|
+
}
|
|
209
|
+
pagesBySource.get(sourceKey)?.push(page);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const addUtilityPagesFromSourceKey = (
|
|
214
|
+
pages: Map<string, UtilityPage>,
|
|
215
|
+
pagesBySource: Map<string, UtilityOpenApiPage[]>,
|
|
216
|
+
sourceKey: string
|
|
217
|
+
) => {
|
|
218
|
+
for (const page of pagesBySource.get(sourceKey) ?? []) {
|
|
219
|
+
pages.set(page.slug, page);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const addReferencedUtilityPages = (
|
|
224
|
+
pages: Map<string, UtilityPage>,
|
|
225
|
+
pagesByIdentifier: Map<string, UtilityOpenApiPage>,
|
|
226
|
+
pageReferences: string[] | undefined,
|
|
227
|
+
hiddenPages: Set<string>,
|
|
228
|
+
groupHidden = false
|
|
229
|
+
) => {
|
|
230
|
+
for (const pageReference of pageReferences ?? []) {
|
|
231
|
+
if (groupHidden || hiddenPages.has(pageReference)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const page = pagesByIdentifier.get(pageReference);
|
|
236
|
+
if (page) {
|
|
237
|
+
pages.set(page.slug, page);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export const buildUtilityOpenApiPages = async (
|
|
243
|
+
config: SiteConfig,
|
|
244
|
+
collection: CollectionConfig | undefined,
|
|
245
|
+
source: ContentSource
|
|
246
|
+
) => {
|
|
247
|
+
if (!collection || collection.type !== "docs") {
|
|
248
|
+
return [] satisfies UtilityPage[];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const docsNavigation = getDocsNavigation(config);
|
|
252
|
+
const hiddenPages = new Set(docsNavigation?.hidden);
|
|
253
|
+
const slugPrefix = normalizePath(collection.slugPrefix ?? "");
|
|
254
|
+
const byIdentifier = new Map<string, UtilityOpenApiPage>();
|
|
255
|
+
const bySource = new Map<string, UtilityOpenApiPage[]>();
|
|
256
|
+
const pages = new Map<string, UtilityPage>();
|
|
257
|
+
const sources = collectOpenApiSources(collection);
|
|
258
|
+
|
|
259
|
+
const resolved = await Promise.all(
|
|
260
|
+
sources.map(async (item) => {
|
|
261
|
+
const rawSpec = await source.readFile(item.source);
|
|
262
|
+
const spec = parseOpenApiSpec(rawSpec, item.source);
|
|
263
|
+
const directory = item.directory ?? "api";
|
|
264
|
+
const { operations } = extractOpenApiOperations(spec, directory);
|
|
265
|
+
return { directory, operations, source: item };
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
for (const { directory, operations, source: openApiSource } of resolved) {
|
|
270
|
+
collectUtilityOpenApiPages(
|
|
271
|
+
byIdentifier,
|
|
272
|
+
bySource,
|
|
273
|
+
operations,
|
|
274
|
+
directory,
|
|
275
|
+
openApiSource,
|
|
276
|
+
slugPrefix
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const openApiSource of ensureArray(collection.openapi)) {
|
|
281
|
+
if (!openApiSource) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
addUtilityPagesFromSourceKey(
|
|
285
|
+
pages,
|
|
286
|
+
bySource,
|
|
287
|
+
getGroupedOpenApiSourceKey(openApiSource)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const group of docsNavigation?.groups ?? []) {
|
|
292
|
+
const groupHidden = group.hidden === true;
|
|
293
|
+
addReferencedUtilityPages(
|
|
294
|
+
pages,
|
|
295
|
+
byIdentifier,
|
|
296
|
+
group.pages,
|
|
297
|
+
hiddenPages,
|
|
298
|
+
groupHidden
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (groupHidden || !group.openapi) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
addUtilityPagesFromSourceKey(
|
|
305
|
+
pages,
|
|
306
|
+
bySource,
|
|
307
|
+
getGroupedOpenApiSourceKey(group.openapi)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
addReferencedUtilityPages(
|
|
312
|
+
pages,
|
|
313
|
+
byIdentifier,
|
|
314
|
+
docsNavigation?.pages,
|
|
315
|
+
hiddenPages
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return [...pages.values()];
|
|
319
|
+
};
|