@writedocs/generator 0.4.7 → 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 +7 -0
- package/package.json +1 -1
- package/src/components/Color.astro +61 -0
- package/src/components/ColorItem.astro +93 -0
- package/src/components/ColorRow.astro +39 -0
- package/src/components/GitHubRepo.astro +156 -0
- package/src/components/Panel.astro +11 -0
- package/src/components/Prompt.astro +143 -0
- package/src/components/Tile.astro +65 -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 +7 -0
- package/src/lib/inline-markdown.js +29 -0
- package/src/lib/mdx-inject-builtins.js +15 -2
- package/src/lib/mdx-mintlify.js +99 -0
- package/src/lib/mdx-title-anchor-ids.js +7 -2
- package/src/lib/visibility.js +29 -0
- package/src/pages/[...slug].astro +23 -1
- package/src/pages/[...slug].md.ts +4 -1
- package/src/pages/llms-full.txt.ts +3 -1
|
@@ -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
|
+
}
|
|
@@ -39,6 +39,16 @@ const BUILTIN_COMPONENT_NAMES = [
|
|
|
39
39
|
'ResponseField',
|
|
40
40
|
'Columns',
|
|
41
41
|
'Tooltip',
|
|
42
|
+
'Update',
|
|
43
|
+
'Tile',
|
|
44
|
+
'Panel',
|
|
45
|
+
'Prompt',
|
|
46
|
+
'View',
|
|
47
|
+
'Visibility',
|
|
48
|
+
'Tree',
|
|
49
|
+
'FileTree',
|
|
50
|
+
'Color',
|
|
51
|
+
'GitHub',
|
|
42
52
|
];
|
|
43
53
|
|
|
44
54
|
const PACKAGE_SPECIFIER = 'writedocs/components';
|
|
@@ -77,8 +87,11 @@ export function remarkInjectBuiltinComponents() {
|
|
|
77
87
|
const needed = new Set();
|
|
78
88
|
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
|
|
79
89
|
if (!node.name) return;
|
|
80
|
-
|
|
81
|
-
|
|
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);
|
|
82
95
|
}
|
|
83
96
|
});
|
|
84
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', 'Check', '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
|
|
|
@@ -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
|
+
}
|
|
@@ -56,6 +56,13 @@ import ParamField from "../components/ParamField.astro";
|
|
|
56
56
|
import ResponseField from "../components/ResponseField.astro";
|
|
57
57
|
import Columns from "../components/Columns.astro";
|
|
58
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";
|
|
59
66
|
import ApiPlayground from "../components/ApiPlayground.astro";
|
|
60
67
|
import ApiReferencePanel from "../components/ApiReferencePanel.astro";
|
|
61
68
|
|
|
@@ -438,6 +445,16 @@ const components = {
|
|
|
438
445
|
ResponseField,
|
|
439
446
|
Columns,
|
|
440
447
|
Tooltip,
|
|
448
|
+
Update,
|
|
449
|
+
Tile,
|
|
450
|
+
Panel,
|
|
451
|
+
Prompt,
|
|
452
|
+
View,
|
|
453
|
+
Visibility,
|
|
454
|
+
Tree,
|
|
455
|
+
FileTree,
|
|
456
|
+
Color,
|
|
457
|
+
GitHub,
|
|
441
458
|
};
|
|
442
459
|
---
|
|
443
460
|
|
|
@@ -727,7 +744,12 @@ const components = {
|
|
|
727
744
|
const tocCol = document.querySelector<HTMLElement>(".wd-toc-col");
|
|
728
745
|
if (!tocCol) return; // no toc column on this page (mode !== 'default') - nothing to dock into
|
|
729
746
|
|
|
730
|
-
|
|
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
|
+
);
|
|
731
753
|
if (panels.length === 0) return;
|
|
732
754
|
if (tocCol.dataset.wdExampleInit) return;
|
|
733
755
|
tocCol.dataset.wdExampleInit = "true";
|
|
@@ -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.
|
|
@@ -73,7 +74,9 @@ export const GET: APIRoute = ({ props }) => {
|
|
|
73
74
|
// it here means this route needs no separate file read/parse of its
|
|
74
75
|
// own, and always matches exactly what [...slug].astro rendered from
|
|
75
76
|
// (same entry, same collection query).
|
|
76
|
-
|
|
77
|
+
// Mintlify's <Visibility>: this is the agents' view of the page - see
|
|
78
|
+
// lib/visibility.js.
|
|
79
|
+
const body = applyVisibilityForAgents(entry.body ?? '');
|
|
77
80
|
const markdown = `# ${entry.data.title}\n\n${body}`;
|
|
78
81
|
return new Response(markdown, {
|
|
79
82
|
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
|
@@ -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 "everything, concatenated" half of the llms.txt pair - see
|
|
9
10
|
// llms.txt.ts (right next to this file) for the lightweight index half,
|
|
@@ -53,7 +54,8 @@ export const GET: APIRoute = async () => {
|
|
|
53
54
|
.filter((entry) => !entry.data.url)
|
|
54
55
|
.map((entry: DocsEntry) => {
|
|
55
56
|
const slug = normalizeEntryId(entry.id);
|
|
56
|
-
|
|
57
|
+
// Agents' view of <Visibility> blocks, same as the .md route.
|
|
58
|
+
const body = applyVisibilityForAgents(entry.body ?? '');
|
|
57
59
|
return { slug, title: entry.data.title, body };
|
|
58
60
|
})
|
|
59
61
|
.sort((a, b) => a.slug.localeCompare(b.slug));
|