@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.
@@ -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, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ .replace(/"/g, '&quot;')
13
+ .replace(/'/g, '&#39;');
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
- if (BUILTIN_COMPONENT_NAMES.includes(node.name) && !locallyBound.has(node.name)) {
81
- needed.add(node.name);
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 === 'title'
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
- const panels = Array.from(root.querySelectorAll<HTMLElement>(".wd-example-panel"));
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
- const body = entry.body ?? '';
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
- const body = entry.body ?? '';
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));