@supatent/supatent-docs 0.1.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/chunk-QVZFIUSH.js +13777 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +75 -0
- package/dist/next-config.d.ts +20 -0
- package/dist/next-config.js +305 -0
- package/dist/types-Cf4pRODK.d.ts +60 -0
- package/package.json +49 -0
- package/runtime/middleware.ts +52 -0
- package/runtime/src/app/[locale]/docs-internal/[[...slug]]/page.tsx +91 -0
- package/runtime/src/app/[locale]/docs-internal/ai/all/route.ts +52 -0
- package/runtime/src/app/[locale]/docs-internal/ai/index/route.ts +52 -0
- package/runtime/src/app/[locale]/docs-internal/markdown/[...slug]/route.ts +50 -0
- package/runtime/src/app/globals.css +1082 -0
- package/runtime/src/app/layout.tsx +37 -0
- package/runtime/src/app/page.tsx +26 -0
- package/runtime/src/components/code-group-enhancer.tsx +128 -0
- package/runtime/src/components/docs-shell.tsx +140 -0
- package/runtime/src/components/header-dropdown.tsx +138 -0
- package/runtime/src/components/icons.tsx +58 -0
- package/runtime/src/components/locale-switcher.tsx +97 -0
- package/runtime/src/components/mobile-docs-menu.tsx +208 -0
- package/runtime/src/components/site-header.tsx +44 -0
- package/runtime/src/components/site-search.tsx +358 -0
- package/runtime/src/components/theme-toggle.tsx +74 -0
- package/runtime/src/docs.config.ts +91 -0
- package/runtime/src/framework/config.ts +76 -0
- package/runtime/src/framework/errors.ts +34 -0
- package/runtime/src/framework/locales.ts +45 -0
- package/runtime/src/framework/markdown-search-text.ts +11 -0
- package/runtime/src/framework/navigation.ts +58 -0
- package/runtime/src/framework/next-config.ts +160 -0
- package/runtime/src/framework/rendering.ts +445 -0
- package/runtime/src/framework/repository.ts +255 -0
- package/runtime/src/framework/runtime-locales.ts +85 -0
- package/runtime/src/framework/search-index-types.ts +34 -0
- package/runtime/src/framework/search-index.ts +271 -0
- package/runtime/src/framework/service.ts +302 -0
- package/runtime/src/framework/settings.ts +43 -0
- package/runtime/src/framework/site-title.ts +54 -0
- package/runtime/src/framework/theme.ts +17 -0
- package/runtime/src/framework/types.ts +66 -0
- package/runtime/src/framework/url.ts +78 -0
- package/runtime/src/supatent/client.ts +2 -0
- package/src/index.ts +11 -0
- package/src/next-config.ts +5 -0
- package/src/next.ts +10 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { unified } from "unified";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkDirective from "remark-directive";
|
|
5
|
+
import remarkRehype from "remark-rehype";
|
|
6
|
+
import rehypeRaw from "rehype-raw";
|
|
7
|
+
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
8
|
+
import rehypeStringify from "rehype-stringify";
|
|
9
|
+
import { visit } from "unist-util-visit";
|
|
10
|
+
import { markdownToSearchText } from "./markdown-search-text";
|
|
11
|
+
|
|
12
|
+
type MdastNode = {
|
|
13
|
+
type?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
lang?: string | null;
|
|
16
|
+
meta?: string | null;
|
|
17
|
+
data?: {
|
|
18
|
+
hName?: string;
|
|
19
|
+
hProperties?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
children?: unknown[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type HastNode = {
|
|
25
|
+
type?: string;
|
|
26
|
+
tagName?: string;
|
|
27
|
+
value?: string;
|
|
28
|
+
properties?: Record<string, unknown>;
|
|
29
|
+
children?: HastNode[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type SanitizeAttribute = string | [string, ...(string | RegExp | boolean)[]];
|
|
33
|
+
|
|
34
|
+
const sanitizeSchema = {
|
|
35
|
+
...defaultSchema,
|
|
36
|
+
attributes: {
|
|
37
|
+
...defaultSchema.attributes,
|
|
38
|
+
img: [
|
|
39
|
+
...(((defaultSchema.attributes?.img ?? []) as unknown[]) as SanitizeAttribute[]),
|
|
40
|
+
"width",
|
|
41
|
+
"height",
|
|
42
|
+
"loading",
|
|
43
|
+
"decoding",
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function slugify(value: string): string {
|
|
49
|
+
return value
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[`*_~()[\]]/g, "")
|
|
52
|
+
.replace(/[^\w\s-]/g, "")
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/\s+/g, "-");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function textFromNode(node: unknown): string {
|
|
58
|
+
if (!node || typeof node !== "object") {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const typedNode = node as {
|
|
63
|
+
value?: unknown;
|
|
64
|
+
children?: unknown[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (typeof typedNode.value === "string") {
|
|
68
|
+
return typedNode.value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(typedNode.children)) {
|
|
72
|
+
return typedNode.children.map(textFromNode).join("");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeTabLabel(value: string | undefined): string | null {
|
|
79
|
+
if (!value) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const normalized = value.trim().replace(/^["']|["']$/g, "");
|
|
84
|
+
return normalized.length > 0 ? normalized : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fallbackTabLabel(lang: string | null | undefined, index: number): string {
|
|
88
|
+
const normalized = typeof lang === "string" ? lang.trim() : "";
|
|
89
|
+
if (normalized.length > 0) {
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
return `Code ${index}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function labelFromMeta(meta: string | null | undefined): string | null {
|
|
96
|
+
if (typeof meta !== "string") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const bracket = meta.match(/\[([^\]]+)\]/);
|
|
101
|
+
if (bracket) {
|
|
102
|
+
return normalizeTabLabel(bracket[1]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const keyed = meta.match(
|
|
106
|
+
/\b(?:label|tab|title)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s]+))/i,
|
|
107
|
+
);
|
|
108
|
+
if (!keyed) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return normalizeTabLabel(keyed[1] ?? keyed[2] ?? keyed[3]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toClassNames(value: unknown): string[] {
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.filter((entry): entry is string => typeof entry === "string");
|
|
118
|
+
}
|
|
119
|
+
if (typeof value === "string") {
|
|
120
|
+
return value.split(/\s+/).filter(Boolean);
|
|
121
|
+
}
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function labelFromCodeNode(
|
|
126
|
+
codeNode: HastNode | undefined,
|
|
127
|
+
index: number,
|
|
128
|
+
): string {
|
|
129
|
+
if (!codeNode || !codeNode.properties) {
|
|
130
|
+
return `Code ${index}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const labeled = normalizeTabLabel(
|
|
134
|
+
typeof codeNode.properties.title === "string" ? codeNode.properties.title : undefined,
|
|
135
|
+
);
|
|
136
|
+
if (labeled) {
|
|
137
|
+
return labeled;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const classes = toClassNames(codeNode.properties.className);
|
|
141
|
+
const languageClass = classes.find((className) => className.startsWith("language-"));
|
|
142
|
+
if (languageClass) {
|
|
143
|
+
return languageClass.slice("language-".length);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `Code ${index}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function hasCodeGroupMarker(idValue: unknown): boolean {
|
|
150
|
+
if (typeof idValue === "string") {
|
|
151
|
+
return idValue.includes("docs-code-group-");
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(idValue)) {
|
|
154
|
+
return idValue.some(
|
|
155
|
+
(entry) => typeof entry === "string" && entry.includes("docs-code-group-"),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isElement(node: HastNode | undefined, tagName: string): boolean {
|
|
162
|
+
return node?.type === "element" && node.tagName === tagName;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function remarkCodeGroupDirectives() {
|
|
166
|
+
return (tree: unknown) => {
|
|
167
|
+
let groupIndex = 0;
|
|
168
|
+
|
|
169
|
+
visit(tree as never, "containerDirective", (node: unknown) => {
|
|
170
|
+
const typedNode = node as MdastNode;
|
|
171
|
+
if (typedNode.name !== "code-group") {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
groupIndex += 1;
|
|
176
|
+
|
|
177
|
+
const data = typedNode.data ?? (typedNode.data = {});
|
|
178
|
+
data.hName = "div";
|
|
179
|
+
data.hProperties = {
|
|
180
|
+
...(data.hProperties ?? {}),
|
|
181
|
+
id: `docs-code-group-${groupIndex}`,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (!Array.isArray(typedNode.children)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let codeIndex = 0;
|
|
189
|
+
for (const childNode of typedNode.children) {
|
|
190
|
+
const codeNode = childNode as MdastNode;
|
|
191
|
+
if (codeNode.type !== "code") {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
codeIndex += 1;
|
|
196
|
+
const label =
|
|
197
|
+
labelFromMeta(codeNode.meta) ?? fallbackTabLabel(codeNode.lang, codeIndex);
|
|
198
|
+
|
|
199
|
+
const codeData = codeNode.data ?? (codeNode.data = {});
|
|
200
|
+
codeData.hProperties = {
|
|
201
|
+
...(codeData.hProperties ?? {}),
|
|
202
|
+
title: label,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function rehypeCodeGroups() {
|
|
210
|
+
return (tree: unknown) => {
|
|
211
|
+
let renderedGroupCount = 0;
|
|
212
|
+
|
|
213
|
+
visit(tree as never, "element", (node: unknown) => {
|
|
214
|
+
const groupNode = node as HastNode;
|
|
215
|
+
if (groupNode.tagName !== "div") {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!hasCodeGroupMarker(groupNode.properties?.id)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const children = Array.isArray(groupNode.children) ? groupNode.children : [];
|
|
224
|
+
const codeBlocks = children.filter((child) => isElement(child, "pre"));
|
|
225
|
+
|
|
226
|
+
if (codeBlocks.length === 0) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
renderedGroupCount += 1;
|
|
231
|
+
|
|
232
|
+
const tabs: HastNode[] = [];
|
|
233
|
+
const panels: HastNode[] = [];
|
|
234
|
+
|
|
235
|
+
for (let index = 0; index < codeBlocks.length; index += 1) {
|
|
236
|
+
const preNode = codeBlocks[index];
|
|
237
|
+
const preChildren = Array.isArray(preNode.children) ? preNode.children : [];
|
|
238
|
+
const codeNode = preChildren.find((child) => isElement(child, "code"));
|
|
239
|
+
const label = labelFromCodeNode(codeNode, index + 1);
|
|
240
|
+
|
|
241
|
+
const tabId = `docs-code-tab-${renderedGroupCount}-${index + 1}`;
|
|
242
|
+
const panelId = `docs-code-panel-${renderedGroupCount}-${index + 1}`;
|
|
243
|
+
const active = index === 0;
|
|
244
|
+
|
|
245
|
+
tabs.push({
|
|
246
|
+
type: "element",
|
|
247
|
+
tagName: "button",
|
|
248
|
+
properties: {
|
|
249
|
+
type: "button",
|
|
250
|
+
className: active
|
|
251
|
+
? ["docs-code-tab", "docs-code-tab-active"]
|
|
252
|
+
: ["docs-code-tab"],
|
|
253
|
+
role: "tab",
|
|
254
|
+
id: tabId,
|
|
255
|
+
ariaControls: panelId,
|
|
256
|
+
ariaSelected: active ? "true" : "false",
|
|
257
|
+
tabIndex: active ? 0 : -1,
|
|
258
|
+
dataCodeTab: "",
|
|
259
|
+
dataCodeIndex: String(index),
|
|
260
|
+
},
|
|
261
|
+
children: [{ type: "text", value: label }],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (codeNode?.properties) {
|
|
265
|
+
delete codeNode.properties.title;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const preProperties = preNode.properties ?? (preNode.properties = {});
|
|
269
|
+
preProperties.className = [...toClassNames(preProperties.className), "docs-code-pre"];
|
|
270
|
+
|
|
271
|
+
panels.push({
|
|
272
|
+
type: "element",
|
|
273
|
+
tagName: "section",
|
|
274
|
+
properties: {
|
|
275
|
+
className: ["docs-code-panel"],
|
|
276
|
+
id: panelId,
|
|
277
|
+
role: "tabpanel",
|
|
278
|
+
ariaLabelledBy: tabId,
|
|
279
|
+
hidden: !active,
|
|
280
|
+
dataCodePanel: "",
|
|
281
|
+
dataCodeIndex: String(index),
|
|
282
|
+
},
|
|
283
|
+
children: [
|
|
284
|
+
{
|
|
285
|
+
type: "element",
|
|
286
|
+
tagName: "div",
|
|
287
|
+
properties: {
|
|
288
|
+
className: ["docs-code-window"],
|
|
289
|
+
},
|
|
290
|
+
children: [
|
|
291
|
+
{
|
|
292
|
+
type: "element",
|
|
293
|
+
tagName: "div",
|
|
294
|
+
properties: {
|
|
295
|
+
className: ["docs-code-window-chrome"],
|
|
296
|
+
ariaHidden: "true",
|
|
297
|
+
},
|
|
298
|
+
children: [
|
|
299
|
+
{
|
|
300
|
+
type: "element",
|
|
301
|
+
tagName: "span",
|
|
302
|
+
properties: {
|
|
303
|
+
className: ["docs-code-window-dot"],
|
|
304
|
+
},
|
|
305
|
+
children: [],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
type: "element",
|
|
309
|
+
tagName: "span",
|
|
310
|
+
properties: {
|
|
311
|
+
className: ["docs-code-window-dot"],
|
|
312
|
+
},
|
|
313
|
+
children: [],
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
type: "element",
|
|
317
|
+
tagName: "span",
|
|
318
|
+
properties: {
|
|
319
|
+
className: ["docs-code-window-dot"],
|
|
320
|
+
},
|
|
321
|
+
children: [],
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
type: "element",
|
|
327
|
+
tagName: "div",
|
|
328
|
+
properties: {
|
|
329
|
+
className: ["docs-code-window-content"],
|
|
330
|
+
},
|
|
331
|
+
children: [
|
|
332
|
+
preNode,
|
|
333
|
+
{
|
|
334
|
+
type: "element",
|
|
335
|
+
tagName: "button",
|
|
336
|
+
properties: {
|
|
337
|
+
type: "button",
|
|
338
|
+
className: ["docs-code-copy"],
|
|
339
|
+
dataCodeCopy: "",
|
|
340
|
+
dataCodeCopyLabel: "Copy",
|
|
341
|
+
dataCodeCopiedLabel: "Copied",
|
|
342
|
+
ariaLabel: "Copy code block",
|
|
343
|
+
},
|
|
344
|
+
children: [{ type: "text", value: "Copy" }],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
groupNode.properties = {
|
|
355
|
+
className: ["docs-code-group"],
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
groupNode.children = [
|
|
359
|
+
{
|
|
360
|
+
type: "element",
|
|
361
|
+
tagName: "div",
|
|
362
|
+
properties: {
|
|
363
|
+
className: ["docs-code-tabs"],
|
|
364
|
+
role: "tablist",
|
|
365
|
+
ariaLabel: "Code examples",
|
|
366
|
+
},
|
|
367
|
+
children: tabs,
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
type: "element",
|
|
371
|
+
tagName: "div",
|
|
372
|
+
properties: {
|
|
373
|
+
className: ["docs-code-panels"],
|
|
374
|
+
},
|
|
375
|
+
children: panels,
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function rehypeHeadingIds() {
|
|
383
|
+
return (tree: unknown) => {
|
|
384
|
+
visit(tree as never, "element", (node: unknown) => {
|
|
385
|
+
const typedNode = node as {
|
|
386
|
+
tagName?: string;
|
|
387
|
+
properties?: Record<string, unknown>;
|
|
388
|
+
children?: unknown[];
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (typedNode.tagName !== "h2" && typedNode.tagName !== "h3") {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const headingText = textFromNode({ children: typedNode.children ?? [] });
|
|
396
|
+
const id = slugify(headingText);
|
|
397
|
+
if (!id) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
typedNode.properties = {
|
|
402
|
+
...(typedNode.properties ?? {}),
|
|
403
|
+
id,
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function createMarkdownProcessor(enableDirectives: boolean) {
|
|
410
|
+
const processor = unified()
|
|
411
|
+
.use(remarkParse)
|
|
412
|
+
.use(remarkGfm);
|
|
413
|
+
|
|
414
|
+
if (enableDirectives) {
|
|
415
|
+
processor.use(remarkDirective).use(remarkCodeGroupDirectives);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return processor
|
|
419
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
|
420
|
+
.use(rehypeRaw)
|
|
421
|
+
.use(rehypeHeadingIds)
|
|
422
|
+
.use(rehypeSanitize, sanitizeSchema)
|
|
423
|
+
.use(rehypeCodeGroups)
|
|
424
|
+
.use(rehypeStringify);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const markdownProcessor = createMarkdownProcessor(false);
|
|
428
|
+
const markdownProcessorWithDirectives = createMarkdownProcessor(true);
|
|
429
|
+
const codeGroupDirectivePattern = /(^|\n):::\s*code-group\b/;
|
|
430
|
+
|
|
431
|
+
function selectMarkdownProcessor(markdown: string) {
|
|
432
|
+
if (codeGroupDirectivePattern.test(markdown)) {
|
|
433
|
+
return markdownProcessorWithDirectives;
|
|
434
|
+
}
|
|
435
|
+
return markdownProcessor;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function renderMarkdownToHtml(markdown: string): Promise<string> {
|
|
439
|
+
const result = await selectMarkdownProcessor(markdown).process(markdown);
|
|
440
|
+
return String(result);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function markdownToPlainText(markdown: string): string {
|
|
444
|
+
return markdownToSearchText(markdown);
|
|
445
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
createClient,
|
|
4
|
+
type ContentItem,
|
|
5
|
+
type SupatentClient,
|
|
6
|
+
} from "@supatent/client";
|
|
7
|
+
import type {
|
|
8
|
+
DocsCategory,
|
|
9
|
+
DocsDataMode,
|
|
10
|
+
DocsFrameworkConfig,
|
|
11
|
+
DocsPage,
|
|
12
|
+
DocsSettings,
|
|
13
|
+
} from "./types";
|
|
14
|
+
import { DocsFrameworkError, toDocsFrameworkError } from "./errors";
|
|
15
|
+
|
|
16
|
+
export type DocsReadOptions = {
|
|
17
|
+
publishedOnly?: boolean;
|
|
18
|
+
resolveMarkdownAssets?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type DocsRepository = {
|
|
22
|
+
getSettings(locale: string): Promise<DocsSettings | null>;
|
|
23
|
+
getCategories(locale: string, options?: DocsReadOptions): Promise<DocsCategory[]>;
|
|
24
|
+
getPages(locale: string, options?: DocsReadOptions): Promise<DocsPage[]>;
|
|
25
|
+
getPageBySlugLocale(
|
|
26
|
+
slug: string,
|
|
27
|
+
locale: string,
|
|
28
|
+
options?: DocsReadOptions,
|
|
29
|
+
): Promise<DocsPage | null>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type DocsRepositoryOptions = {
|
|
33
|
+
dataMode?: DocsDataMode;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const pageFieldsSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
title: z.string().min(1),
|
|
39
|
+
body: z.string(),
|
|
40
|
+
category: z.string().min(1),
|
|
41
|
+
order: z.preprocess(
|
|
42
|
+
(value) => {
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
return Number(value);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
},
|
|
48
|
+
z.number().finite(),
|
|
49
|
+
),
|
|
50
|
+
})
|
|
51
|
+
.passthrough();
|
|
52
|
+
|
|
53
|
+
const categoryFieldsSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
title: z.string().min(1),
|
|
56
|
+
order: z.preprocess(
|
|
57
|
+
(value) => {
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
return Number(value);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
z.number().finite(),
|
|
64
|
+
),
|
|
65
|
+
})
|
|
66
|
+
.passthrough();
|
|
67
|
+
|
|
68
|
+
function normalizePage(item: ContentItem<Record<string, unknown>>): DocsPage {
|
|
69
|
+
const parsedFields = pageFieldsSchema.safeParse(item.data);
|
|
70
|
+
if (!parsedFields.success) {
|
|
71
|
+
const issues = parsedFields.error.issues.map((issue) => issue.message).join("; ");
|
|
72
|
+
throw new DocsFrameworkError(
|
|
73
|
+
"DATA_ERROR",
|
|
74
|
+
`Invalid page content shape for slug "${item.slug}": ${issues}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
slug: item.slug,
|
|
80
|
+
title: parsedFields.data.title,
|
|
81
|
+
body: parsedFields.data.body,
|
|
82
|
+
category: parsedFields.data.category,
|
|
83
|
+
order: parsedFields.data.order,
|
|
84
|
+
locale: item.locale,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeCategory(item: ContentItem<Record<string, unknown>>): DocsCategory {
|
|
89
|
+
const parsedFields = categoryFieldsSchema.safeParse(item.data);
|
|
90
|
+
if (!parsedFields.success) {
|
|
91
|
+
const issues = parsedFields.error.issues.map((issue) => issue.message).join("; ");
|
|
92
|
+
throw new DocsFrameworkError(
|
|
93
|
+
"DATA_ERROR",
|
|
94
|
+
`Invalid category content shape for slug "${item.slug}": ${issues}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
slug: item.slug,
|
|
100
|
+
title: parsedFields.data.title,
|
|
101
|
+
order: parsedFields.data.order,
|
|
102
|
+
locale: item.locale,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sortPages(pages: DocsPage[]): DocsPage[] {
|
|
107
|
+
return [...pages].sort((a, b) => {
|
|
108
|
+
if (a.order === b.order) {
|
|
109
|
+
return a.slug.localeCompare(b.slug);
|
|
110
|
+
}
|
|
111
|
+
return a.order - b.order;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sortCategories(categories: DocsCategory[]): DocsCategory[] {
|
|
116
|
+
return [...categories].sort((a, b) => {
|
|
117
|
+
if (a.order === b.order) {
|
|
118
|
+
return a.slug.localeCompare(b.slug);
|
|
119
|
+
}
|
|
120
|
+
return a.order - b.order;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function resolvePageMarkdownAssets(
|
|
125
|
+
client: SupatentClient,
|
|
126
|
+
page: DocsPage,
|
|
127
|
+
options: DocsReadOptions,
|
|
128
|
+
): Promise<DocsPage> {
|
|
129
|
+
if (!options.resolveMarkdownAssets) {
|
|
130
|
+
return page;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const resolvedBody = await client.resolveMarkdown(page.body);
|
|
134
|
+
if (resolvedBody === page.body) {
|
|
135
|
+
return page;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...page,
|
|
140
|
+
body: resolvedBody,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createSupatentDocsRepository(
|
|
145
|
+
config: DocsFrameworkConfig,
|
|
146
|
+
options: DocsRepositoryOptions = {},
|
|
147
|
+
): DocsRepository {
|
|
148
|
+
const dataMode = options.dataMode ?? config.supatent.dataMode;
|
|
149
|
+
const clientByLocale = new Map<string, SupatentClient>();
|
|
150
|
+
|
|
151
|
+
function getClient(locale: string): SupatentClient {
|
|
152
|
+
const cacheKey = locale;
|
|
153
|
+
const existing = clientByLocale.get(cacheKey);
|
|
154
|
+
if (existing) {
|
|
155
|
+
return existing;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const client = createClient({
|
|
159
|
+
baseUrl: config.supatent.baseUrl,
|
|
160
|
+
projectSlug: config.supatent.projectSlug,
|
|
161
|
+
apiKey: config.supatent.apiKey,
|
|
162
|
+
preview: dataMode === "draft",
|
|
163
|
+
locale,
|
|
164
|
+
cache: {
|
|
165
|
+
maxAge: dataMode === "draft" ? 0 : config.supatent.cache.maxAge,
|
|
166
|
+
staleWhileRevalidate:
|
|
167
|
+
dataMode === "draft" ? 0 : config.supatent.cache.staleWhileRevalidate,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
clientByLocale.set(cacheKey, client);
|
|
172
|
+
return client;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function runSafely<T>(operation: () => Promise<T>): Promise<T> {
|
|
176
|
+
try {
|
|
177
|
+
return await operation();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw toDocsFrameworkError(error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
async getSettings(locale) {
|
|
185
|
+
return runSafely(async () => {
|
|
186
|
+
const content = await getClient(locale)
|
|
187
|
+
.schema<Record<string, unknown>>(config.schemas.settings)
|
|
188
|
+
.first();
|
|
189
|
+
|
|
190
|
+
if (!content) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return content.data as DocsSettings;
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async getCategories(locale, options = {}) {
|
|
199
|
+
return runSafely(async () => {
|
|
200
|
+
const items = await getClient(locale)
|
|
201
|
+
.schema<Record<string, unknown>>(config.schemas.category)
|
|
202
|
+
.all();
|
|
203
|
+
|
|
204
|
+
const filtered = options.publishedOnly
|
|
205
|
+
? items.filter((item) => item.publishedAt !== null)
|
|
206
|
+
: items;
|
|
207
|
+
|
|
208
|
+
return sortCategories(filtered.map(normalizeCategory));
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
async getPages(locale, options = {}) {
|
|
213
|
+
return runSafely(async () => {
|
|
214
|
+
const client = getClient(locale);
|
|
215
|
+
const items = await client
|
|
216
|
+
.schema<Record<string, unknown>>(config.schemas.page)
|
|
217
|
+
.all();
|
|
218
|
+
|
|
219
|
+
const filtered = options.publishedOnly
|
|
220
|
+
? items.filter((item) => item.publishedAt !== null)
|
|
221
|
+
: items;
|
|
222
|
+
|
|
223
|
+
const pages = filtered.map(normalizePage);
|
|
224
|
+
if (!options.resolveMarkdownAssets) {
|
|
225
|
+
return sortPages(pages);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const resolvedPages = await Promise.all(
|
|
229
|
+
pages.map((page) => resolvePageMarkdownAssets(client, page, options)),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
return sortPages(resolvedPages);
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
async getPageBySlugLocale(slug, locale, options = {}) {
|
|
237
|
+
return runSafely(async () => {
|
|
238
|
+
const client = getClient(locale);
|
|
239
|
+
const item = await client
|
|
240
|
+
.schema<Record<string, unknown>>(config.schemas.page)
|
|
241
|
+
.bySlug(slug);
|
|
242
|
+
|
|
243
|
+
if (!item) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (options.publishedOnly && item.publishedAt === null) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return resolvePageMarkdownAssets(client, normalizePage(item), options);
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|