@writedocs/generator 0.4.6 → 0.4.8
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/astro.config.mjs +37 -3
- package/package.json +1 -1
- package/src/components/Accordion.astro +22 -5
- package/src/components/AppIcon.astro +5 -0
- package/src/components/Callout.astro +15 -5
- package/src/components/Card.astro +64 -13
- package/src/components/Check.astro +13 -0
- package/src/components/Color.astro +61 -0
- package/src/components/ColorItem.astro +93 -0
- package/src/components/ColorRow.astro +39 -0
- package/src/components/Columns.astro +11 -0
- package/src/components/Frame.astro +10 -1
- package/src/components/GitHubRepo.astro +156 -0
- package/src/components/Hint.astro +50 -4
- package/src/components/Panel.astro +11 -0
- package/src/components/ParamField.astro +25 -0
- package/src/components/Parameter.astro +41 -4
- package/src/components/Prompt.astro +143 -0
- package/src/components/ResponseField.astro +18 -0
- package/src/components/Step.astro +22 -3
- package/src/components/Steps.astro +22 -0
- package/src/components/Tab.astro +7 -1
- package/src/components/Tabs.astro +27 -4
- package/src/components/Tile.astro +65 -0
- package/src/components/Tooltip.astro +13 -0
- package/src/components/Tree.astro +213 -0
- package/src/components/TreeFile.astro +17 -0
- package/src/components/TreeFolder.astro +28 -0
- package/src/components/Update.astro +171 -0
- package/src/components/View.astro +214 -0
- package/src/components/Visibility.astro +11 -0
- package/src/components/compound.ts +21 -0
- package/src/components/index.ts +12 -0
- package/src/content.config.ts +68 -2
- package/src/layout/components/NavTree.astro +34 -0
- package/src/layout/styles/base.css +12 -0
- package/src/lib/config.ts +1412 -1306
- package/src/lib/inline-markdown.js +29 -0
- package/src/lib/mdx-inject-builtins.js +20 -2
- package/src/lib/mdx-mintlify.js +99 -0
- package/src/lib/mdx-title-anchor-ids.js +7 -2
- package/src/lib/shiki-code-block.js +140 -26
- package/src/lib/visibility.js +29 -0
- package/src/pages/[...slug].astro +110 -7
- package/src/pages/[...slug].md.ts +7 -1
- package/src/pages/llms-full.txt.ts +5 -1
- package/src/pages/llms.txt.ts +3 -0
- package/src/styles/global.css +10 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// A string prop rendered with the small subset of inline Markdown Mintlify
|
|
2
|
+
// allows in some component props (Tile's `title`, Prompt's `description`,
|
|
3
|
+
// Color.Row's `title`): `code`, **bold**, and *italic* / _italic_. The
|
|
4
|
+
// input is HTML-escaped first, so this is always safe to hand to set:html -
|
|
5
|
+
// nothing in a prop value can produce markup other than these three.
|
|
6
|
+
|
|
7
|
+
function escapeHtml(value) {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>')
|
|
12
|
+
.replace(/"/g, '"')
|
|
13
|
+
.replace(/'/g, ''');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function inlineMarkdown(value) {
|
|
17
|
+
if (!value) return '';
|
|
18
|
+
// Code spans first, and set aside, so ** or _ inside them stay literal.
|
|
19
|
+
const codeSpans = [];
|
|
20
|
+
let html = escapeHtml(String(value)).replace(/`([^`]+)`/g, (_, code) => {
|
|
21
|
+
codeSpans.push(`<code>${code}</code>`);
|
|
22
|
+
return `\u0000${codeSpans.length - 1}\u0000`;
|
|
23
|
+
});
|
|
24
|
+
html = html
|
|
25
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
26
|
+
.replace(/(^|[^\w*])\*([^*]+)\*(?!\w)/g, '$1<em>$2</em>')
|
|
27
|
+
.replace(/(^|[^\w])_([^_]+)_(?!\w)/g, '$1<em>$2</em>');
|
|
28
|
+
return html.replace(/\u0000(\d+)\u0000/g, (_, i) => codeSpans[Number(i)]);
|
|
29
|
+
}
|
|
@@ -34,6 +34,21 @@ const BUILTIN_COMPONENT_NAMES = [
|
|
|
34
34
|
'Icon',
|
|
35
35
|
'RequestExample',
|
|
36
36
|
'ResponseExample',
|
|
37
|
+
'Check',
|
|
38
|
+
'ParamField',
|
|
39
|
+
'ResponseField',
|
|
40
|
+
'Columns',
|
|
41
|
+
'Tooltip',
|
|
42
|
+
'Update',
|
|
43
|
+
'Tile',
|
|
44
|
+
'Panel',
|
|
45
|
+
'Prompt',
|
|
46
|
+
'View',
|
|
47
|
+
'Visibility',
|
|
48
|
+
'Tree',
|
|
49
|
+
'FileTree',
|
|
50
|
+
'Color',
|
|
51
|
+
'GitHub',
|
|
37
52
|
];
|
|
38
53
|
|
|
39
54
|
const PACKAGE_SPECIFIER = 'writedocs/components';
|
|
@@ -72,8 +87,11 @@ export function remarkInjectBuiltinComponents() {
|
|
|
72
87
|
const needed = new Set();
|
|
73
88
|
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
|
|
74
89
|
if (!node.name) return;
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
// A dotted name (<Tree.File>, <GitHub.Repo>) needs its root imported -
|
|
91
|
+
// the parts hang off it (see src/components/compound.ts).
|
|
92
|
+
const root = node.name.split('.')[0];
|
|
93
|
+
if (BUILTIN_COMPONENT_NAMES.includes(root) && !locallyBound.has(root)) {
|
|
94
|
+
needed.add(root);
|
|
77
95
|
}
|
|
78
96
|
});
|
|
79
97
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
// Remark plugins for Mintlify components whose input can't be read from a
|
|
4
|
+
// component's own props/slot at render time. Registered in
|
|
5
|
+
// astro.config.mjs's markdown processor, next to the other remark plugins.
|
|
6
|
+
|
|
7
|
+
const TREE_NAMES = new Set(['Tree', 'FileTree']);
|
|
8
|
+
|
|
9
|
+
function textOf(node) {
|
|
10
|
+
if (!node) return '';
|
|
11
|
+
if (node.type === 'text' || node.type === 'inlineCode') return node.value;
|
|
12
|
+
return (node.children ?? []).map(textOf).join('');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function jsxAttr(name, value) {
|
|
16
|
+
return { type: 'mdxJsxAttribute', name, value };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function listToTreeNodes(list) {
|
|
20
|
+
return list.children
|
|
21
|
+
.filter((item) => item.type === 'listItem')
|
|
22
|
+
.map((item) => {
|
|
23
|
+
const label = textOf(item.children.find((c) => c.type === 'paragraph')).trim();
|
|
24
|
+
const nested = item.children.find((c) => c.type === 'list');
|
|
25
|
+
const isFolder = label.endsWith('/') || Boolean(nested);
|
|
26
|
+
const name = label.replace(/\/+$/, '');
|
|
27
|
+
if (!isFolder) {
|
|
28
|
+
return { type: 'mdxJsxFlowElement', name: 'Tree.File', attributes: [jsxAttr('name', name)], children: [] };
|
|
29
|
+
}
|
|
30
|
+
const children = nested ? listToTreeNodes(nested) : [];
|
|
31
|
+
const attributes = [jsxAttr('name', name)];
|
|
32
|
+
// "Folders that contain nested items expand by default" - Mintlify's
|
|
33
|
+
// rule for the list syntax.
|
|
34
|
+
if (children.length > 0) attributes.push(jsxAttr('defaultOpen', null));
|
|
35
|
+
return { type: 'mdxJsxFlowElement', name: 'Tree.Folder', attributes, children };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mintlify's Markdown-list form of <Tree>/<FileTree>:
|
|
41
|
+
*
|
|
42
|
+
* <FileTree>
|
|
43
|
+
*
|
|
44
|
+
* - docs/
|
|
45
|
+
* - index.mdx
|
|
46
|
+
* - docs.config.ts
|
|
47
|
+
*
|
|
48
|
+
* </FileTree>
|
|
49
|
+
*
|
|
50
|
+
* A trailing slash or nested items make an entry a folder; formatting in a
|
|
51
|
+
* name (`code`, **bold**) is dropped. Rewritten here into the same
|
|
52
|
+
* <Tree.Folder>/<Tree.File> elements the component form uses, so Tree.astro
|
|
53
|
+
* only ever renders one shape.
|
|
54
|
+
*/
|
|
55
|
+
export function remarkMintlifyTreeLists() {
|
|
56
|
+
return (tree) => {
|
|
57
|
+
visit(tree, 'mdxJsxFlowElement', (node) => {
|
|
58
|
+
if (!TREE_NAMES.has(node.name)) return;
|
|
59
|
+
if (!node.children.some((c) => c.type === 'list')) return;
|
|
60
|
+
node.children = node.children.flatMap((c) => (c.type === 'list' ? listToTreeNodes(c) : [c]));
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function dedent(text) {
|
|
66
|
+
const lines = text.split('\n');
|
|
67
|
+
const indents = lines.filter((l) => l.trim()).map((l) => l.match(/^[ \t]*/)[0].length);
|
|
68
|
+
const min = indents.length ? Math.min(...indents) : 0;
|
|
69
|
+
return lines
|
|
70
|
+
.map((l) => l.slice(min))
|
|
71
|
+
.join('\n')
|
|
72
|
+
.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Mintlify's <Prompt> copies (and sends to Cursor) the prompt's own source
|
|
77
|
+
* text - Markdown included, e.g. "- Use second-person voice" - not the
|
|
78
|
+
* rendered HTML's plain text. A component only receives the rendered slot,
|
|
79
|
+
* so the source is read here, from the file between the element's first
|
|
80
|
+
* and last child, and passed down as a hidden `_text` prop.
|
|
81
|
+
*/
|
|
82
|
+
export function remarkMintlifyPromptText() {
|
|
83
|
+
return (tree, file) => {
|
|
84
|
+
const source = String(file.value ?? '');
|
|
85
|
+
visit(tree, 'mdxJsxFlowElement', (node) => {
|
|
86
|
+
if (node.name !== 'Prompt' || node.children.length === 0) return;
|
|
87
|
+
if (node.attributes.some((a) => a.type === 'mdxJsxAttribute' && a.name === '_text')) return;
|
|
88
|
+
const start = node.children[0].position?.start?.offset;
|
|
89
|
+
const end = node.children[node.children.length - 1].position?.end?.offset;
|
|
90
|
+
if (typeof start !== 'number' || typeof end !== 'number') return;
|
|
91
|
+
// Back up to the start of the first child's line, so its own
|
|
92
|
+
// indentation is part of what dedent() measures - but only over
|
|
93
|
+
// whitespace, never into the <Prompt ...> tag on the same line.
|
|
94
|
+
const lineStart = source.lastIndexOf('\n', start - 1) + 1;
|
|
95
|
+
const from = /^[ \t]*$/.test(source.slice(lineStart, start)) ? lineStart : start;
|
|
96
|
+
node.attributes.push(jsxAttr('_text', dedent(source.slice(from, end))));
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -8,7 +8,11 @@ import { visit } from 'unist-util-visit';
|
|
|
8
8
|
// (Card, Tabs, AccordionGroup, etc. don't). AccordionGroup is deliberately
|
|
9
9
|
// excluded - it has no `title` of its own, just wraps Accordion children
|
|
10
10
|
// that each get their own entry here independently.
|
|
11
|
-
const TITLED_COMPONENT_NAMES = ['Callout', 'Note', 'Info', 'Tip', 'Warning', 'Danger', 'Accordion'];
|
|
11
|
+
const TITLED_COMPONENT_NAMES = ['Callout', 'Note', 'Info', 'Tip', 'Warning', 'Danger', 'Check', 'Accordion', 'Update'];
|
|
12
|
+
|
|
13
|
+
// The attribute each one's anchor text comes from - `title`, except
|
|
14
|
+
// Mintlify's <Update>, whose anchor is its `label`.
|
|
15
|
+
const ANCHOR_ATTRIBUTE = { Update: 'label' };
|
|
12
16
|
|
|
13
17
|
// Deliberately the same slugify Callout.astro/Accordion.astro themselves
|
|
14
18
|
// fall back to (see their own comments) - kept as a literal duplicate
|
|
@@ -67,8 +71,9 @@ export function remarkTitleAnchorIds() {
|
|
|
67
71
|
const seen = new Map();
|
|
68
72
|
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
|
|
69
73
|
if (!node.name || !TITLED_COMPONENT_NAMES.includes(node.name)) return;
|
|
74
|
+
const attrName = ANCHOR_ATTRIBUTE[node.name] ?? 'title';
|
|
70
75
|
const titleAttr = node.attributes?.find(
|
|
71
|
-
(attr) => attr.type === 'mdxJsxAttribute' && attr.name ===
|
|
76
|
+
(attr) => attr.type === 'mdxJsxAttribute' && attr.name === attrName
|
|
72
77
|
);
|
|
73
78
|
if (!titleAttr || typeof titleAttr.value !== 'string') return;
|
|
74
79
|
|
|
@@ -21,33 +21,124 @@
|
|
|
21
21
|
// at all, so this only ever touches genuine ```-fenced MDX content and
|
|
22
22
|
// never collides with the API playground's own bespoke copy buttons.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
import { iconSvg } from './config.ts';
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
// The fence's info string after the language, split into tokens: a
|
|
27
|
+
// `key=value` option (value quoted, in braces, or bare), a bare `{1,3-5}`
|
|
28
|
+
// range, a `/word/` pattern, or any other bare word.
|
|
29
|
+
const META_TOKEN_RE = /([A-Za-z][\w-]*)=(?:"([^"]*)"|'([^']*)'|\{([^}]*)\}|(\S+))|\{([^}]*)\}|(\/(?:\\.|[^/\s])+\/)(?=\s|$)|(\S+)/g;
|
|
30
|
+
|
|
31
|
+
// Bare words that are options, not title text. Case-sensitive on purpose:
|
|
32
|
+
// Mintlify's own docs write ```js Wrap example wrap, where the capitalized
|
|
33
|
+
// "Wrap" is part of the title and only the trailing "wrap" is the flag.
|
|
34
|
+
const META_FLAGS = new Set(['wrap', 'lines', 'showLineNumbers', 'expandable', 'nocopy', 'twoslash']);
|
|
35
|
+
|
|
36
|
+
function parseRanges(value) {
|
|
37
|
+
return value
|
|
38
|
+
.split(',')
|
|
39
|
+
.map((part) => part.trim())
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.flatMap((part) => {
|
|
42
|
+
const [start, end] = part.split('-').map((n) => Number.parseInt(n, 10));
|
|
43
|
+
if (Number.isNaN(start)) return [];
|
|
44
|
+
if (end === undefined || Number.isNaN(end)) return [start];
|
|
45
|
+
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Parses a fence's info string in both syntaxes writedocs accepts:
|
|
50
|
+
* its own (```js title="a.js" {1,3} wrap) and Mintlify's
|
|
51
|
+
* (```js a.js highlight={1,3} focus={2} icon="js" nocopy wrap) - the
|
|
52
|
+
* Mintlify title is the run of plain words right after the language,
|
|
53
|
+
* before the first option. `title="..."` wins over an inline title.
|
|
54
|
+
*
|
|
55
|
+
* `shikiMeta` is what the stock @shikijs/transformers meta transformers
|
|
56
|
+
* get to see instead of the raw string (see preprocess below): only the
|
|
57
|
+
* highlight range and the /word/ patterns. Read raw, those transformers
|
|
58
|
+
* would take focus={2} as a highlight range (they grab the first {...}
|
|
59
|
+
* anywhere) and a / inside a title or path as a word to highlight. */
|
|
60
|
+
export function parseMeta(raw) {
|
|
61
|
+
const meta = {
|
|
62
|
+
title: null,
|
|
63
|
+
icon: null,
|
|
64
|
+
wrap: false,
|
|
65
|
+
lines: false,
|
|
66
|
+
expandable: false,
|
|
67
|
+
nocopy: false,
|
|
68
|
+
twoslash: false,
|
|
69
|
+
highlight: [],
|
|
70
|
+
focus: [],
|
|
71
|
+
words: [],
|
|
32
72
|
};
|
|
73
|
+
const titleWords = [];
|
|
74
|
+
let sawOption = false;
|
|
75
|
+
for (const m of raw.matchAll(META_TOKEN_RE)) {
|
|
76
|
+
const [, key, dq, sq, braced, bare, bareRange, word, other] = m;
|
|
77
|
+
if (key !== undefined) {
|
|
78
|
+
sawOption = true;
|
|
79
|
+
const value = dq ?? sq ?? braced ?? bare ?? '';
|
|
80
|
+
if (key === 'title') meta.title = value;
|
|
81
|
+
else if (key === 'icon') meta.icon = value;
|
|
82
|
+
else if (key === 'highlight') meta.highlight.push(...parseRanges(value));
|
|
83
|
+
else if (key === 'focus') meta.focus.push(...parseRanges(value));
|
|
84
|
+
// Mintlify's `nocopy="false"` (explicitly keep the copy button).
|
|
85
|
+
else if (META_FLAGS.has(key)) meta[key === 'showLineNumbers' ? 'lines' : key] = value !== 'false';
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (bareRange !== undefined) {
|
|
89
|
+
sawOption = true;
|
|
90
|
+
meta.highlight.push(...parseRanges(bareRange));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (word !== undefined) {
|
|
94
|
+
sawOption = true;
|
|
95
|
+
meta.words.push(word);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (META_FLAGS.has(other)) {
|
|
99
|
+
sawOption = true;
|
|
100
|
+
meta[other === 'showLineNumbers' ? 'lines' : other] = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!sawOption) titleWords.push(other);
|
|
104
|
+
}
|
|
105
|
+
if (meta.title === null && titleWords.length > 0) meta.title = titleWords.join(' ');
|
|
106
|
+
const shikiParts = [];
|
|
107
|
+
if (meta.highlight.length > 0) shikiParts.push(`{${meta.highlight.join(',')}}`);
|
|
108
|
+
shikiParts.push(...meta.words);
|
|
109
|
+
meta.shikiMeta = shikiParts.join(' ');
|
|
110
|
+
return meta;
|
|
33
111
|
}
|
|
34
112
|
|
|
35
113
|
export function codeBlockTransformer() {
|
|
36
114
|
return {
|
|
37
115
|
name: 'writedocs:code-block',
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
116
|
+
// preprocess runs before every other hook of every transformer (see
|
|
117
|
+
// @shikijs/core's codeToTokens), so this is where the raw info string
|
|
118
|
+
// is parsed once and swapped for the cleaned-up `shikiMeta` the stock
|
|
119
|
+
// meta-highlight transformers read - see parseMeta() above. The full
|
|
120
|
+
// parse is kept on the same options object for the hooks below.
|
|
121
|
+
preprocess(code, options) {
|
|
122
|
+
if (options.meta) {
|
|
123
|
+
options.meta.__wdMeta = parseMeta(options.meta.__raw ?? '');
|
|
124
|
+
options.meta.__raw = options.meta.__wdMeta.shikiMeta;
|
|
125
|
+
}
|
|
126
|
+
return code;
|
|
127
|
+
},
|
|
128
|
+
line(node, lineNumber) {
|
|
129
|
+
if (this.options.meta?.__wdMeta?.focus.includes(lineNumber)) this.addClassToHast(node, 'focused');
|
|
130
|
+
return node;
|
|
131
|
+
},
|
|
132
|
+
// Same classes transformerNotationFocus (`// [!code focus]`) adds, so
|
|
133
|
+
// `focus={...}` reuses [...slug].astro's existing dimming CSS.
|
|
43
134
|
pre(node) {
|
|
44
|
-
|
|
135
|
+
if (this.options.meta?.__wdMeta?.focus.length) this.addClassToHast(node, 'has-focused');
|
|
45
136
|
return node;
|
|
46
137
|
},
|
|
47
138
|
root(hast) {
|
|
48
139
|
const pre = hast.children.find((child) => child.type === 'element' && child.tagName === 'pre');
|
|
49
140
|
if (!pre) return hast;
|
|
50
|
-
const meta = this.
|
|
141
|
+
const meta = this.options.meta?.__wdMeta ?? parseMeta(this.options.meta?.__raw ?? '');
|
|
51
142
|
|
|
52
143
|
const wrapperClass = ['wd-code-block'];
|
|
53
144
|
if (meta.wrap) wrapperClass.push('wd-code-wrap');
|
|
@@ -56,25 +147,48 @@ export function codeBlockTransformer() {
|
|
|
56
147
|
|
|
57
148
|
const children = [];
|
|
58
149
|
if (meta.title) {
|
|
150
|
+
const titleChildren = [];
|
|
151
|
+
// Drawn as a CSS mask over currentColor, so the icon takes the
|
|
152
|
+
// title bar's own text color in both themes. An icon string that
|
|
153
|
+
// isn't an installed icon (emoji, unknown name) is skipped rather
|
|
154
|
+
// than failing the build.
|
|
155
|
+
const svg = meta.icon ? iconSvg(meta.icon) : null;
|
|
156
|
+
if (svg) {
|
|
157
|
+
titleChildren.push({
|
|
158
|
+
type: 'element',
|
|
159
|
+
tagName: 'span',
|
|
160
|
+
properties: {
|
|
161
|
+
class: 'wd-code-title-icon',
|
|
162
|
+
'aria-hidden': 'true',
|
|
163
|
+
style: `--wd-code-icon: url("data:image/svg+xml,${encodeURIComponent(svg)}")`,
|
|
164
|
+
},
|
|
165
|
+
children: [],
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
titleChildren.push({ type: 'text', value: meta.title });
|
|
59
169
|
children.push({
|
|
60
170
|
type: 'element',
|
|
61
171
|
tagName: 'div',
|
|
62
172
|
properties: { class: 'wd-code-title' },
|
|
63
|
-
children:
|
|
173
|
+
children: titleChildren,
|
|
64
174
|
});
|
|
65
175
|
}
|
|
66
176
|
children.push(pre);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
type: '
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
177
|
+
// Mintlify's `nocopy` - for content where copying makes no sense
|
|
178
|
+
// (ASCII diagrams, sample output).
|
|
179
|
+
if (!meta.nocopy) {
|
|
180
|
+
children.push({
|
|
181
|
+
type: 'element',
|
|
182
|
+
tagName: 'button',
|
|
183
|
+
properties: {
|
|
184
|
+
type: 'button',
|
|
185
|
+
class: 'wd-code-copy-btn',
|
|
186
|
+
'data-role': 'copy-code',
|
|
187
|
+
'aria-label': 'Copy code',
|
|
188
|
+
},
|
|
189
|
+
children: [{ type: 'text', value: '⧉' }],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
78
192
|
if (meta.expandable) {
|
|
79
193
|
children.push({
|
|
80
194
|
type: 'element',
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Mintlify's <Visibility for="humans|agents"> in the page source text that
|
|
2
|
+
// writedocs serves to agents - the per-page `.md` route and llms-full.txt,
|
|
3
|
+
// which both hand out a page's raw MDX body rather than its rendered HTML.
|
|
4
|
+
// The rendered site shows `humans` content and hides `agents` content (see
|
|
5
|
+
// Visibility.astro); the text versions do the opposite: `humans` blocks are
|
|
6
|
+
// removed, and `agents` blocks are unwrapped so only their contents remain.
|
|
7
|
+
// A <Visibility> with no `for` counts as `humans`, like the component.
|
|
8
|
+
//
|
|
9
|
+
// Works on source text, not an AST, since that's what these routes serve.
|
|
10
|
+
// Nested <Visibility> blocks aren't supported (Mintlify doesn't document
|
|
11
|
+
// nesting either) - the regex pairs each opening tag with the first
|
|
12
|
+
// closing tag after it.
|
|
13
|
+
|
|
14
|
+
const VISIBILITY_BLOCK_RE =
|
|
15
|
+
/<Visibility(?:\s+for\s*=\s*(?:"(humans|agents)"|'(humans|agents)'|\{\s*["'](humans|agents)["']\s*\}))?\s*>([\s\S]*?)<\/Visibility>/g;
|
|
16
|
+
|
|
17
|
+
function dedent(text) {
|
|
18
|
+
const lines = text.replace(/^\n+|\s+$/g, '').split('\n');
|
|
19
|
+
const indents = lines.filter((l) => l.trim()).map((l) => l.match(/^[ \t]*/)[0].length);
|
|
20
|
+
const min = indents.length ? Math.min(...indents) : 0;
|
|
21
|
+
return lines.map((l) => l.slice(min)).join('\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function applyVisibilityForAgents(body) {
|
|
25
|
+
return body.replace(VISIBILITY_BLOCK_RE, (_, dq, sq, expr, inner) => {
|
|
26
|
+
const audience = dq ?? sq ?? expr ?? 'humans';
|
|
27
|
+
return audience === 'agents' ? dedent(inner) : '';
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -51,6 +51,18 @@ import Badge from "../components/Badge.astro";
|
|
|
51
51
|
import Icon from "../components/Icon.astro";
|
|
52
52
|
import RequestExample from "../components/RequestExample.astro";
|
|
53
53
|
import ResponseExample from "../components/ResponseExample.astro";
|
|
54
|
+
import Check from "../components/Check.astro";
|
|
55
|
+
import ParamField from "../components/ParamField.astro";
|
|
56
|
+
import ResponseField from "../components/ResponseField.astro";
|
|
57
|
+
import Columns from "../components/Columns.astro";
|
|
58
|
+
import Tooltip from "../components/Tooltip.astro";
|
|
59
|
+
import Update from "../components/Update.astro";
|
|
60
|
+
import Tile from "../components/Tile.astro";
|
|
61
|
+
import Panel from "../components/Panel.astro";
|
|
62
|
+
import Prompt from "../components/Prompt.astro";
|
|
63
|
+
import View from "../components/View.astro";
|
|
64
|
+
import Visibility from "../components/Visibility.astro";
|
|
65
|
+
import { Tree, FileTree, Color, GitHub } from "../components/compound";
|
|
54
66
|
import ApiPlayground from "../components/ApiPlayground.astro";
|
|
55
67
|
import ApiReferencePanel from "../components/ApiReferencePanel.astro";
|
|
56
68
|
|
|
@@ -156,6 +168,15 @@ export async function getStaticPaths() {
|
|
|
156
168
|
|
|
157
169
|
const pageRoutes = sections.flatMap((section, sectionIndex) => {
|
|
158
170
|
const flatNav = flattenNav(section.pages);
|
|
171
|
+
// Prev/next skip pages that aren't in the sidebar (frontmatter
|
|
172
|
+
// `hidden`) or aren't pages at all (frontmatter `url`, an external
|
|
173
|
+
// link) - the reader should land on the next page they could also
|
|
174
|
+
// have clicked in the sidebar. Inlined for the same reason as the
|
|
175
|
+
// error branch below.
|
|
176
|
+
const isPaginationStop = (slug: string) => {
|
|
177
|
+
const data = entryByFileId.get(slug)?.data;
|
|
178
|
+
return Boolean(data) && !data!.hidden && !data!.url;
|
|
179
|
+
};
|
|
159
180
|
return flatNav.map((navEntry, i) => {
|
|
160
181
|
const entry = entryByFileId.get(navEntry.slug);
|
|
161
182
|
if (!entry) {
|
|
@@ -184,9 +205,17 @@ export async function getStaticPaths() {
|
|
|
184
205
|
);
|
|
185
206
|
}
|
|
186
207
|
claimedFileIds.add(navEntry.slug);
|
|
187
|
-
const prev =
|
|
188
|
-
const next =
|
|
208
|
+
const prev = flatNav.slice(0, i).findLast((e) => isPaginationStop(e.slug)) ?? null;
|
|
209
|
+
const next = flatNav.slice(i + 1).find((e) => isPaginationStop(e.slug)) ?? null;
|
|
189
210
|
const urlSlug = normalizeEntryId(entry.id);
|
|
211
|
+
// A frontmatter `url` page (Mintlify's external link) exists only
|
|
212
|
+
// to put that link in the sidebar - its own URL redirects there.
|
|
213
|
+
if (entry.data.url) {
|
|
214
|
+
return {
|
|
215
|
+
params: { slug: urlSlug === "index" ? undefined : urlSlug },
|
|
216
|
+
props: { redirectTo: entry.data.url },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
190
219
|
return {
|
|
191
220
|
params: { slug: urlSlug === "index" ? undefined : urlSlug },
|
|
192
221
|
props: { entry, prev, next, sectionIndex },
|
|
@@ -276,6 +305,9 @@ const entries: DocsEntry[] = [...pagesEntries, ...generatedDocsEntries];
|
|
|
276
305
|
// themselves so hrefForSlug can resolve a writedocs.json file-id reference to
|
|
277
306
|
// wherever that page actually got routed.
|
|
278
307
|
const titleByFileId = new Map<string, string>();
|
|
308
|
+
// Navigation labels (sidebar, topbar dropdowns): a page's `sidebarTitle`
|
|
309
|
+
// when it has one - Mintlify's short nav label - else its `title`.
|
|
310
|
+
const navTitleByFileId = new Map<string, string>();
|
|
279
311
|
const entryByFileId = new Map<string, DocsEntry>();
|
|
280
312
|
// The HTTP method badge NavTree.astro shows next to an API operation
|
|
281
313
|
// page in the sidebar - read straight off the page's own `openapi:
|
|
@@ -286,13 +318,19 @@ const methodByFileId = new Map<string, string>();
|
|
|
286
318
|
for (const e of entries) {
|
|
287
319
|
const fileId = fileIdForEntry(contentDir, packageRoot, e);
|
|
288
320
|
titleByFileId.set(fileId, e.data.title);
|
|
321
|
+
navTitleByFileId.set(fileId, e.data.sidebarTitle ?? e.data.title);
|
|
289
322
|
entryByFileId.set(fileId, e);
|
|
290
|
-
|
|
323
|
+
// `hideApiMarker` (Mintlify's) drops the badge for this one page.
|
|
324
|
+
if (e.data.openapi && !e.data.hideApiMarker) {
|
|
291
325
|
methodByFileId.set(fileId, e.data.openapi.trim().split(/\s+/, 1)[0].toUpperCase());
|
|
292
326
|
}
|
|
293
327
|
}
|
|
294
328
|
const titleForSlug = (slug: string) => titleByFileId.get(slug) ?? slug;
|
|
329
|
+
const navTitleForSlug = (slug: string) => navTitleByFileId.get(slug) ?? slug;
|
|
295
330
|
const methodForSlug = (slug: string) => methodByFileId.get(slug) ?? null;
|
|
331
|
+
// Frontmatter that changes how a page appears in navigation (hidden, url,
|
|
332
|
+
// icon, tag, deprecated) - see buildNavTree() in lib/config.ts.
|
|
333
|
+
const navMetaForSlug = (slug: string) => entryByFileId.get(slug)?.data;
|
|
296
334
|
const hrefForSlug = (slug: string) => {
|
|
297
335
|
const raw = entryByFileId.get(slug)?.id ?? slug;
|
|
298
336
|
const effective = normalizeEntryId(raw);
|
|
@@ -303,7 +341,7 @@ const hrefForSlug = (slug: string) => {
|
|
|
303
341
|
// and what every "is this the active page" comparison below needs, since
|
|
304
342
|
// navTree/globalDropdowns are built from writedocs.json's file-id space.
|
|
305
343
|
const currentFileId = fileIdForEntry(contentDir, packageRoot, entry);
|
|
306
|
-
const navTree = buildNavTree(activeSection.pages,
|
|
344
|
+
const navTree = buildNavTree(activeSection.pages, navTitleForSlug, methodForSlug, navMetaForSlug);
|
|
307
345
|
// The ancestor sidebar-group trail above the auto <h1> (Breadcrumbs.astro)
|
|
308
346
|
// - empty for a page sitting at the top level of its Section with no
|
|
309
347
|
// enclosing group, in which case [...slug].astro below skips rendering
|
|
@@ -326,8 +364,9 @@ const selectors = buildSelectors(activeSection.path, hrefForSlug, activePagePosi
|
|
|
326
364
|
const globalDropdowns = buildGlobalDropdowns(
|
|
327
365
|
resolveGlobalDropdowns(config.navigation),
|
|
328
366
|
currentFileId,
|
|
329
|
-
|
|
367
|
+
navTitleForSlug,
|
|
330
368
|
hrefForSlug,
|
|
369
|
+
navMetaForSlug,
|
|
331
370
|
);
|
|
332
371
|
|
|
333
372
|
// Meta tags: this page's own frontmatter `seo` overrides writedocs.json's
|
|
@@ -401,6 +440,21 @@ const components = {
|
|
|
401
440
|
Icon,
|
|
402
441
|
RequestExample,
|
|
403
442
|
ResponseExample,
|
|
443
|
+
Check,
|
|
444
|
+
ParamField,
|
|
445
|
+
ResponseField,
|
|
446
|
+
Columns,
|
|
447
|
+
Tooltip,
|
|
448
|
+
Update,
|
|
449
|
+
Tile,
|
|
450
|
+
Panel,
|
|
451
|
+
Prompt,
|
|
452
|
+
View,
|
|
453
|
+
Visibility,
|
|
454
|
+
Tree,
|
|
455
|
+
FileTree,
|
|
456
|
+
Color,
|
|
457
|
+
GitHub,
|
|
404
458
|
};
|
|
405
459
|
---
|
|
406
460
|
|
|
@@ -430,13 +484,25 @@ const components = {
|
|
|
430
484
|
<article class={`wd-article ${pageMode === "wide" ? "wd-article-wide" : ""}`} data-pagefind-body>
|
|
431
485
|
{breadcrumbs.length > 0 && <Breadcrumbs items={breadcrumbs} />}
|
|
432
486
|
<div class="wd-article-header">
|
|
433
|
-
|
|
487
|
+
{
|
|
488
|
+
// Frontmatter `deprecated` puts a label beside the <h1>, not in
|
|
489
|
+
// it - the <h1>'s text is the search index's page title.
|
|
490
|
+
entry.data.deprecated ? (
|
|
491
|
+
<div class="wd-article-title">
|
|
492
|
+
<h1 data-pagefind-meta="title">{entry.data.title}</h1>
|
|
493
|
+
<span class="wd-deprecated-badge">Deprecated</span>
|
|
494
|
+
</div>
|
|
495
|
+
) : (
|
|
496
|
+
<h1 data-pagefind-meta="title">{entry.data.title}</h1>
|
|
497
|
+
)
|
|
498
|
+
}
|
|
434
499
|
{showCopyPageMenu && (
|
|
435
500
|
<CopyPageMenu currentPath={currentPath} siteUrl={siteUrl} contextMenu={config.contextMenu!} />
|
|
436
501
|
)}
|
|
437
502
|
</div>
|
|
438
503
|
<Content components={components} />
|
|
439
504
|
{entry.data.openapi && <ApiPlayground operation={entry.data.openapi} contentDir={contentDir} />}
|
|
505
|
+
{!entry.data.hideFooterPagination && (
|
|
440
506
|
<nav class="wd-prevnext" data-pagefind-ignore>
|
|
441
507
|
{prev && (
|
|
442
508
|
<a class="wd-prevnext-card wd-prevnext-prev" href={hrefForSlug(prev.slug)}>
|
|
@@ -485,6 +551,7 @@ const components = {
|
|
|
485
551
|
</a>
|
|
486
552
|
)}
|
|
487
553
|
</nav>
|
|
554
|
+
)}
|
|
488
555
|
</article>
|
|
489
556
|
)
|
|
490
557
|
}
|
|
@@ -677,7 +744,12 @@ const components = {
|
|
|
677
744
|
const tocCol = document.querySelector<HTMLElement>(".wd-toc-col");
|
|
678
745
|
if (!tocCol) return; // no toc column on this page (mode !== 'default') - nothing to dock into
|
|
679
746
|
|
|
680
|
-
|
|
747
|
+
// Top-level panels only: Mintlify's <Panel> (Panel.astro) is itself a
|
|
748
|
+
// .wd-example-panel, and a RequestExample/ResponseExample inside it
|
|
749
|
+
// moves with it rather than on its own.
|
|
750
|
+
const panels = Array.from(root.querySelectorAll<HTMLElement>(".wd-example-panel")).filter(
|
|
751
|
+
(panel) => !panel.parentElement?.closest(".wd-example-panel"),
|
|
752
|
+
);
|
|
681
753
|
if (panels.length === 0) return;
|
|
682
754
|
if (tocCol.dataset.wdExampleInit) return;
|
|
683
755
|
tocCol.dataset.wdExampleInit = "true";
|
|
@@ -777,6 +849,24 @@ const components = {
|
|
|
777
849
|
.wd-article-header h1 {
|
|
778
850
|
margin: 0;
|
|
779
851
|
}
|
|
852
|
+
/* Frontmatter `deprecated: true` (Mintlify's) - same amber as the
|
|
853
|
+
sidebar's own deprecated tag (NavTree.astro) and Parameter's
|
|
854
|
+
deprecated badge. */
|
|
855
|
+
.wd-article-title {
|
|
856
|
+
display: flex;
|
|
857
|
+
align-items: center;
|
|
858
|
+
flex-wrap: wrap;
|
|
859
|
+
gap: 0.6rem;
|
|
860
|
+
}
|
|
861
|
+
.wd-deprecated-badge {
|
|
862
|
+
display: inline-block;
|
|
863
|
+
padding: 0.15rem 0.55rem;
|
|
864
|
+
border-radius: 999px;
|
|
865
|
+
font-size: 0.8rem;
|
|
866
|
+
font-weight: 700;
|
|
867
|
+
color: #d97706;
|
|
868
|
+
background: color-mix(in srgb, #d97706 15%, transparent);
|
|
869
|
+
}
|
|
780
870
|
.wd-prevnext {
|
|
781
871
|
display: flex;
|
|
782
872
|
justify-content: space-between;
|
|
@@ -991,6 +1081,19 @@ const components = {
|
|
|
991
1081
|
background: var(--wd-surface);
|
|
992
1082
|
border-bottom: 1px solid var(--wd-border);
|
|
993
1083
|
}
|
|
1084
|
+
/* A fence's `icon="..."` (shiki-code-block.js) - the icon's own SVG
|
|
1085
|
+
arrives as a data URL in --wd-code-icon and is used as a mask, so it
|
|
1086
|
+
paints in the title's text color instead of the SVG's own fill. */
|
|
1087
|
+
.wd-code-title-icon {
|
|
1088
|
+
display: inline-block;
|
|
1089
|
+
width: 1em;
|
|
1090
|
+
height: 1em;
|
|
1091
|
+
margin-right: 0.45rem;
|
|
1092
|
+
vertical-align: -0.125em;
|
|
1093
|
+
background: currentColor;
|
|
1094
|
+
-webkit-mask: var(--wd-code-icon) center / contain no-repeat;
|
|
1095
|
+
mask: var(--wd-code-icon) center / contain no-repeat;
|
|
1096
|
+
}
|
|
994
1097
|
.wd-code-copy-btn {
|
|
995
1098
|
position: absolute;
|
|
996
1099
|
top: 0.6rem;
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import type { APIRoute } from 'astro';
|
|
5
5
|
import { loadDocsConfig, normalizeEntryId, findAllPages } from '../lib/config';
|
|
6
6
|
import { writedocsTempDir } from '../lib/writedocs-temp-dir.js';
|
|
7
|
+
import { applyVisibilityForAgents } from '../lib/visibility.js';
|
|
7
8
|
|
|
8
9
|
// The raw-Markdown twin of [...slug].astro: every content page is also
|
|
9
10
|
// reachable at the exact same slug with a literal ".md" suffix (e.g.
|
|
@@ -52,6 +53,9 @@ export async function getStaticPaths() {
|
|
|
52
53
|
// as an LLM-facing "view this page as text" export, so they're left
|
|
53
54
|
// out of this route entirely rather than serving something misleading.
|
|
54
55
|
.filter((entry) => !entry.data.openapi)
|
|
56
|
+
// A frontmatter `url` page's HTML route is a redirect to that link -
|
|
57
|
+
// there's no page content to serve as Markdown.
|
|
58
|
+
.filter((entry) => !entry.data.url)
|
|
55
59
|
.map((entry) => ({
|
|
56
60
|
params: { slug: normalizeEntryId(entry.id) },
|
|
57
61
|
props: { entry },
|
|
@@ -70,7 +74,9 @@ export const GET: APIRoute = ({ props }) => {
|
|
|
70
74
|
// it here means this route needs no separate file read/parse of its
|
|
71
75
|
// own, and always matches exactly what [...slug].astro rendered from
|
|
72
76
|
// (same entry, same collection query).
|
|
73
|
-
|
|
77
|
+
// Mintlify's <Visibility>: this is the agents' view of the page - see
|
|
78
|
+
// lib/visibility.js.
|
|
79
|
+
const body = applyVisibilityForAgents(entry.body ?? '');
|
|
74
80
|
const markdown = `# ${entry.data.title}\n\n${body}`;
|
|
75
81
|
return new Response(markdown, {
|
|
76
82
|
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|