@writedocs/generator 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/LICENSE +15 -0
- package/README.md +17 -0
- package/astro.config.mjs +419 -0
- package/bin/writedocs.js +73 -0
- package/package.json +79 -0
- package/src/assets/wd_watermark.png +0 -0
- package/src/assets/wd_watermark_dark.png +0 -0
- package/src/cli/build-auth.js +53 -0
- package/src/cli/build.js +40 -0
- package/src/cli/dev.js +12 -0
- package/src/cli/generate-api-pages.js +359 -0
- package/src/cli/init.js +81 -0
- package/src/cli/preflight.js +40 -0
- package/src/cli/run-astro.js +57 -0
- package/src/cli/run-pagefind.js +66 -0
- package/src/cli/write-redirects-file.js +80 -0
- package/src/components/Accordion.astro +164 -0
- package/src/components/AccordionGroup.astro +40 -0
- package/src/components/ApiLangSelect.astro +168 -0
- package/src/components/ApiPlayground.astro +281 -0
- package/src/components/ApiReferencePanel.astro +1754 -0
- package/src/components/ApiSchemaField.astro +54 -0
- package/src/components/AppIcon.astro +32 -0
- package/src/components/Badge.astro +128 -0
- package/src/components/Callout.astro +168 -0
- package/src/components/Card.astro +136 -0
- package/src/components/CardGroup.astro +20 -0
- package/src/components/CodeGroup.astro +184 -0
- package/src/components/CopyPageMenu.astro +246 -0
- package/src/components/Danger.astro +12 -0
- package/src/components/Expandable.astro +126 -0
- package/src/components/Frame.astro +102 -0
- package/src/components/Hint.astro +99 -0
- package/src/components/Icon.astro +70 -0
- package/src/components/Image.astro +147 -0
- package/src/components/Info.astro +12 -0
- package/src/components/Note.astro +12 -0
- package/src/components/Parameter.astro +119 -0
- package/src/components/RequestExample.astro +33 -0
- package/src/components/ResponseExample.astro +19 -0
- package/src/components/Searchbar.astro +117 -0
- package/src/components/Step.astro +10 -0
- package/src/components/Steps.astro +32 -0
- package/src/components/Tab.astro +9 -0
- package/src/components/Tabs.astro +52 -0
- package/src/components/Tip.astro +12 -0
- package/src/components/Video.astro +135 -0
- package/src/components/Warning.astro +12 -0
- package/src/components/index.ts +48 -0
- package/src/content.config.ts +223 -0
- package/src/layout/BaseLayout.astro +750 -0
- package/src/layout/components/AnalyticsScripts.astro +77 -0
- package/src/layout/components/AskAiWidget.astro +37 -0
- package/src/layout/components/Breadcrumbs.astro +97 -0
- package/src/layout/components/ImageZoom.astro +19 -0
- package/src/layout/components/MobileMenu.astro +200 -0
- package/src/layout/components/NavTree.astro +351 -0
- package/src/layout/components/SearchModal.astro +42 -0
- package/src/layout/components/Sidebar.astro +122 -0
- package/src/layout/components/SiteFooter.astro +85 -0
- package/src/layout/components/TableOfContents.astro +117 -0
- package/src/layout/components/TopBar.astro +311 -0
- package/src/layout/styles/banner.css +44 -0
- package/src/layout/styles/base.css +234 -0
- package/src/layout/styles/dropdown.css +133 -0
- package/src/layout/styles/footer.css +108 -0
- package/src/layout/styles/image-zoom.css +50 -0
- package/src/layout/styles/mobile-menu.css +258 -0
- package/src/layout/styles/search-modal.css +122 -0
- package/src/layout/styles/topbar.css +437 -0
- package/src/lib/config.ts +2131 -0
- package/src/lib/mdx-auto-hydrate.js +70 -0
- package/src/lib/mdx-inject-builtins.js +87 -0
- package/src/lib/mdx-substitute-variables.js +66 -0
- package/src/lib/mdx-title-anchor-ids.js +84 -0
- package/src/lib/mermaid-rehype.js +72 -0
- package/src/lib/openapi-render.ts +479 -0
- package/src/lib/shiki-code-block.js +102 -0
- package/src/lib/shiki-copy-button.js +45 -0
- package/src/lib/styles-asset-integration.js +210 -0
- package/src/lib/writedocs-temp-dir.js +93 -0
- package/src/pages/404.astro +62 -0
- package/src/pages/[...slug].astro +1270 -0
- package/src/pages/[...slug].md.ts +78 -0
- package/src/pages/llms-full.txt.ts +71 -0
- package/src/pages/llms.txt.ts +141 -0
- package/src/scripts/banner.ts +20 -0
- package/src/scripts/dropdowns.ts +61 -0
- package/src/scripts/image-zoom.ts +66 -0
- package/src/scripts/mobile-menu.ts +55 -0
- package/src/scripts/search.ts +155 -0
- package/src/scripts/sidebar-scroll.ts +65 -0
- package/src/scripts/theme-toggle.ts +35 -0
- package/src/scripts/topbar-offset.ts +141 -0
- package/src/styles/global.css +18 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
const CLIENT_ATTR_PREFIX = 'client:';
|
|
4
|
+
const REACT_SNIPPET_EXTENSION = /\.(jsx|tsx)$/i;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A remark plugin (registered via `mdx({ remarkPlugins: [...] })` in
|
|
8
|
+
* astro.config.mjs) that auto-adds `client:load` to every JSX usage of a
|
|
9
|
+
* component imported from a `.jsx`/`.tsx` file, unless that usage already
|
|
10
|
+
* has its own `client:*` attribute.
|
|
11
|
+
*
|
|
12
|
+
* Why this exists: Astro only ships a framework component's JS (and
|
|
13
|
+
* hydrates it in the browser) when the JSX using it carries an explicit
|
|
14
|
+
* `client:*` directive - with none, it still server-renders fine, but
|
|
15
|
+
* stays inert (no event handlers ever attach). That's the right default
|
|
16
|
+
* for Astro's own `.astro` components generally, but it means every React
|
|
17
|
+
* snippet with a hook would silently do nothing unless whoever *used* the
|
|
18
|
+
* snippet - not whoever *wrote* it - remembered to add `client:load` (or
|
|
19
|
+
* `client:visible`/`client:idle`) at each call site. Requiring every
|
|
20
|
+
* import site to know and repeat an Astro-specific detail isn't
|
|
21
|
+
* reasonable for a snippet meant to be dropped in and just work - so this
|
|
22
|
+
* makes `client:load` automatic, and an explicit `client:*` attribute (for
|
|
23
|
+
* a different hydration strategy - `client:visible`, `client:idle`,
|
|
24
|
+
* `client:only`) still works exactly as before, since this only adds the
|
|
25
|
+
* attribute when none is already present.
|
|
26
|
+
*
|
|
27
|
+
* Only affects components imported from `.jsx`/`.tsx` - `.astro` and
|
|
28
|
+
* `.mdx` imports (snippets or otherwise) are untouched, since Astro/MDX
|
|
29
|
+
* components have no hydration model to opt into in the first place;
|
|
30
|
+
* adding `client:*` to one is a compiler error, not a no-op.
|
|
31
|
+
*/
|
|
32
|
+
export function remarkAutoHydrateSnippets() {
|
|
33
|
+
return (tree) => {
|
|
34
|
+
const reactLocalNames = new Set();
|
|
35
|
+
|
|
36
|
+
visit(tree, 'mdxjsEsm', (node) => {
|
|
37
|
+
const body = node.data?.estree?.body;
|
|
38
|
+
if (!body) return;
|
|
39
|
+
for (const stmt of body) {
|
|
40
|
+
if (stmt.type !== 'ImportDeclaration') continue;
|
|
41
|
+
if (typeof stmt.source?.value !== 'string') continue;
|
|
42
|
+
if (!REACT_SNIPPET_EXTENSION.test(stmt.source.value)) continue;
|
|
43
|
+
for (const specifier of stmt.specifiers) {
|
|
44
|
+
// Default (`import Foo from '...'`) and named
|
|
45
|
+
// (`import { Foo } from '...'`) imports both bind a local JSX-
|
|
46
|
+
// usable identifier. A namespace import (`import * as Foo`)
|
|
47
|
+
// isn't used as a JSX tag by itself (`<Foo.Bar />` wouldn't
|
|
48
|
+
// match this node's `name` anyway - see the mdxJsxFlowElement/
|
|
49
|
+
// mdxJsxTextElement visit below), so it's left alone.
|
|
50
|
+
if (specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportSpecifier') {
|
|
51
|
+
reactLocalNames.add(specifier.local.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (reactLocalNames.size === 0) return;
|
|
58
|
+
|
|
59
|
+
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
|
|
60
|
+
if (!node.name || !reactLocalNames.has(node.name)) return;
|
|
61
|
+
const attributes = node.attributes ?? [];
|
|
62
|
+
const hasClientDirective = attributes.some(
|
|
63
|
+
(attr) => attr.type === 'mdxJsxAttribute' && typeof attr.name === 'string' && attr.name.startsWith(CLIENT_ATTR_PREFIX)
|
|
64
|
+
);
|
|
65
|
+
if (hasClientDirective) return;
|
|
66
|
+
attributes.push({ type: 'mdxJsxAttribute', name: 'client:load', value: null });
|
|
67
|
+
node.attributes = attributes;
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
import { parse as acornParse } from 'acorn';
|
|
3
|
+
|
|
4
|
+
// The exact same set [...slug].astro passes as the `components` prop to
|
|
5
|
+
// every page's own <Content components={components} /> call (see
|
|
6
|
+
// src/components/index.ts, the barrel this imports from) - kept as one
|
|
7
|
+
// literal list rather than importing that map here, since this runs inside
|
|
8
|
+
// astro.config.mjs's markdown pipeline, a different module graph than
|
|
9
|
+
// [...slug].astro's own.
|
|
10
|
+
const BUILTIN_COMPONENT_NAMES = [
|
|
11
|
+
'Callout',
|
|
12
|
+
'Note',
|
|
13
|
+
'Info',
|
|
14
|
+
'Tip',
|
|
15
|
+
'Warning',
|
|
16
|
+
'Danger',
|
|
17
|
+
'Card',
|
|
18
|
+
'CardGroup',
|
|
19
|
+
'Tabs',
|
|
20
|
+
'Tab',
|
|
21
|
+
'CodeGroup',
|
|
22
|
+
'Accordion',
|
|
23
|
+
'AccordionGroup',
|
|
24
|
+
'Steps',
|
|
25
|
+
'Step',
|
|
26
|
+
'Hint',
|
|
27
|
+
'Image',
|
|
28
|
+
'Frame',
|
|
29
|
+
'Video',
|
|
30
|
+
'Parameter',
|
|
31
|
+
'Expandable',
|
|
32
|
+
'Searchbar',
|
|
33
|
+
'Badge',
|
|
34
|
+
'Icon',
|
|
35
|
+
'RequestExample',
|
|
36
|
+
'ResponseExample',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const PACKAGE_SPECIFIER = 'writedocs/components';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A remark plugin that auto-imports any of Writedocs' built-in components
|
|
43
|
+
* (`Callout`, `Card`, ...) a `.mdx` file uses but never itself imported -
|
|
44
|
+
* from `writedocs/components` (see src/components/index.ts) - so a snippet
|
|
45
|
+
* (see docs/dev/docs/snippets.mdx) gets the exact same zero-import
|
|
46
|
+
* experience a page under docs/ already has via [...slug].astro's own
|
|
47
|
+
* `components` prop. Runs on every `.mdx` file, pages included - harmless
|
|
48
|
+
* there since a page's own `<Callout>` usage already resolves correctly via
|
|
49
|
+
* that prop; the injected import just gives it an equally-correct direct
|
|
50
|
+
* binding instead, and doesn't touch the page's *own* file on disk (this
|
|
51
|
+
* only ever mutates the in-memory AST used for rendering, not the source
|
|
52
|
+
* the "Copy page" .md route serves - see context-menu.mdx).
|
|
53
|
+
*
|
|
54
|
+
* Respects an explicit import: if a file already imports its own `Callout`
|
|
55
|
+
* (a custom one, or re-exporting the built-in on purpose), that name is
|
|
56
|
+
* left alone rather than double-imported or overridden.
|
|
57
|
+
*/
|
|
58
|
+
export function remarkInjectBuiltinComponents() {
|
|
59
|
+
return (tree) => {
|
|
60
|
+
const locallyBound = new Set();
|
|
61
|
+
visit(tree, 'mdxjsEsm', (node) => {
|
|
62
|
+
const body = node.data?.estree?.body;
|
|
63
|
+
if (!body) return;
|
|
64
|
+
for (const stmt of body) {
|
|
65
|
+
if (stmt.type !== 'ImportDeclaration') continue;
|
|
66
|
+
for (const specifier of stmt.specifiers) {
|
|
67
|
+
if (specifier.local?.name) locallyBound.add(specifier.local.name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const needed = new Set();
|
|
73
|
+
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
|
|
74
|
+
if (!node.name) return;
|
|
75
|
+
if (BUILTIN_COMPONENT_NAMES.includes(node.name) && !locallyBound.has(node.name)) {
|
|
76
|
+
needed.add(node.name);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (needed.size === 0) return;
|
|
81
|
+
|
|
82
|
+
const names = [...needed].sort();
|
|
83
|
+
const source = `import { ${names.join(', ')} } from '${PACKAGE_SPECIFIER}';\n`;
|
|
84
|
+
const estree = acornParse(source, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
85
|
+
tree.children.unshift({ type: 'mdxjsEsm', value: source, data: { estree } });
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
// Matches `[[key]]` - alphanumerics, dots, dashes, underscores in the key,
|
|
4
|
+
// with optional whitespace padding (`[[ key ]]` also matches). Kept
|
|
5
|
+
// deliberately simple/text-only: this is prose substitution, not an
|
|
6
|
+
// expression language, so there's no support for defaults, nesting, or
|
|
7
|
+
// anything beyond "look this key up in writedocs.json's `variables` map".
|
|
8
|
+
//
|
|
9
|
+
// Double square brackets, not double curly braces (`{{key}}`, the more
|
|
10
|
+
// familiar templating convention from Handlebars/Mintlify-alikes) -
|
|
11
|
+
// deliberately. MDX reserves single curly braces for embedded JS
|
|
12
|
+
// expressions (`{jsExpression}`), and micromark-extension-mdx-expression
|
|
13
|
+
// tokenizes `{...}` at *parse* time, before any remark plugin (this one
|
|
14
|
+
// included) ever sees the file as an AST. `{{productName}}` in an .mdx
|
|
15
|
+
// file parses as one MDX expression container whose content is the JS
|
|
16
|
+
// object literal `{productName}` (shorthand for `{ productName:
|
|
17
|
+
// productName }`), not as literal text at all - by the time this plugin
|
|
18
|
+
// runs, there's no `text` node containing `{{productName}}` left to find,
|
|
19
|
+
// and the page throws `ReferenceError: productName is not defined` at
|
|
20
|
+
// render time instead. Confirmed empirically while building this
|
|
21
|
+
// feature's own fixture (`25-site-config/`) - not a hypothetical concern.
|
|
22
|
+
// Square brackets carry no such special meaning in MDX/CommonMark outside
|
|
23
|
+
// of the `[text](url)`/`[text][ref]` link forms, both of which require a
|
|
24
|
+
// `(` or second `[...]` immediately after - `[[key]]` alone never matches
|
|
25
|
+
// either, so it round-trips as plain text the same way `{{key}}` would
|
|
26
|
+
// have in an ordinary (non-MDX) Markdown file.
|
|
27
|
+
const PLACEHOLDER = /\[\[\s*([\w.-]+)\s*\]\]/g;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A remark plugin that replaces `[[key]]` placeholders in a page's prose
|
|
31
|
+
* with the corresponding value from writedocs.json's `variables` map (see
|
|
32
|
+
* variables: z.record(...) on docsConfigSchema in src/lib/config.ts) -
|
|
33
|
+
* e.g. `[[productName]]` becomes `Acme` everywhere a site sets
|
|
34
|
+
* `variables: { productName: "Acme" }`.
|
|
35
|
+
*
|
|
36
|
+
* Only visits `text`-type AST nodes, deliberately - `code`/`inlineCode`
|
|
37
|
+
* nodes (fenced/inline code) are a structurally separate node type in the
|
|
38
|
+
* MDX AST, so they're never visited here and a `[[...]]` written inside a
|
|
39
|
+
* code fence is left completely untouched - the same "don't mangle code"
|
|
40
|
+
* property remarkAutoHydrateSnippets and remarkInjectBuiltinComponents get
|
|
41
|
+
* from visiting narrow node-type sets rather than working on raw source
|
|
42
|
+
* text.
|
|
43
|
+
*
|
|
44
|
+
* A placeholder with no matching key is left as literal text (`[[typo]]`
|
|
45
|
+
* stays `[[typo]]`) rather than silently becoming an empty string - an
|
|
46
|
+
* unresolved placeholder rendering visibly in the built page is a much
|
|
47
|
+
* easier bug to spot than prose that's quietly missing a word.
|
|
48
|
+
*
|
|
49
|
+
* Takes `variables` as an options argument, so this attacher has to be
|
|
50
|
+
* passed to unified/remark as a `[remarkSubstituteVariables, variables]`
|
|
51
|
+
* tuple, not pre-called - see the comment on its own usage in
|
|
52
|
+
* astro.config.mjs for why calling it directly and handing unified the
|
|
53
|
+
* resulting transformer breaks the build.
|
|
54
|
+
*/
|
|
55
|
+
export function remarkSubstituteVariables(variables) {
|
|
56
|
+
return (tree) => {
|
|
57
|
+
if (!variables || Object.keys(variables).length === 0) return;
|
|
58
|
+
|
|
59
|
+
visit(tree, 'text', (node) => {
|
|
60
|
+
if (!node.value.includes('[[')) return;
|
|
61
|
+
node.value = node.value.replace(PLACEHOLDER, (match, key) =>
|
|
62
|
+
Object.prototype.hasOwnProperty.call(variables, key) ? variables[key] : match
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
// Every JSX tag name that can carry a `title` prop and render a linkable
|
|
4
|
+
// heading of its own - the 6 Callout variants remarkInjectBuiltinComponents
|
|
5
|
+
// also auto-imports (see its own BUILTIN_COMPONENT_NAMES), plus Accordion.
|
|
6
|
+
// Kept as its own literal list rather than importing that one, since this
|
|
7
|
+
// only cares about the subset that actually has a `title`/anchor at all
|
|
8
|
+
// (Card, Tabs, AccordionGroup, etc. don't). AccordionGroup is deliberately
|
|
9
|
+
// excluded - it has no `title` of its own, just wraps Accordion children
|
|
10
|
+
// that each get their own entry here independently.
|
|
11
|
+
const TITLED_COMPONENT_NAMES = ['Callout', 'Note', 'Info', 'Tip', 'Warning', 'Danger', 'Accordion'];
|
|
12
|
+
|
|
13
|
+
// Deliberately the same slugify Callout.astro/Accordion.astro themselves
|
|
14
|
+
// fall back to (see their own comments) - kept as a literal duplicate
|
|
15
|
+
// rather than a shared import, since this file runs inside
|
|
16
|
+
// astro.config.mjs's markdown pipeline, a different module graph than
|
|
17
|
+
// either component's own (same reasoning mdx-inject-builtins.js gives for
|
|
18
|
+
// not importing src/components/index.ts).
|
|
19
|
+
function slugify(value) {
|
|
20
|
+
return value
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
23
|
+
.replace(/^-+|-+$/g, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A remark plugin that assigns a deduped `_titleId` attribute to every
|
|
28
|
+
* <Callout title="...">/<Note title="...">/<Accordion title="...">/etc.
|
|
29
|
+
* element on a page, mirroring the -2/-3 suffixing behavior Astro's own
|
|
30
|
+
* heading-id step (github-slugger, upstream in the pipeline) already gives
|
|
31
|
+
* duplicate headings - so two of these elements with the same title text
|
|
32
|
+
* get two distinct, independently-linkable ids instead of silently
|
|
33
|
+
* colliding on the same #slug (confirmed as a real bug: the Components
|
|
34
|
+
* page has two callouts both titled "Two ways to write this").
|
|
35
|
+
*
|
|
36
|
+
* One shared `seen` map across every tag name in TITLED_COMPONENT_NAMES,
|
|
37
|
+
* not one per tag - a Callout titled "Configuration" and an Accordion
|
|
38
|
+
* titled "Configuration" later on the same page are still two different
|
|
39
|
+
* elements racing for the same #configuration slug, so they need to be
|
|
40
|
+
* deduped against each other too, not just against their own tag name.
|
|
41
|
+
*
|
|
42
|
+
* Has to happen here, not inside Callout.astro/Accordion.astro
|
|
43
|
+
* themselves - neither one ever sees more than *one* of its own
|
|
44
|
+
* invocations at a time; it has no visibility into whether some earlier
|
|
45
|
+
* element on the same page already produced the same slug. Walking the
|
|
46
|
+
* whole page's MDX AST in one pass, in document order, is the only
|
|
47
|
+
* vantage point that can dedupe correctly - the exact same reason
|
|
48
|
+
* github-slugger itself runs once per document rather than being
|
|
49
|
+
* reimplemented per-heading.
|
|
50
|
+
*
|
|
51
|
+
* Only handles a literal string `title="..."` - mdast-util-mdx-jsx gives a
|
|
52
|
+
* plain JS string for that form's attribute value, vs. an
|
|
53
|
+
* mdxJsxAttributeValueExpression node for a dynamic `title={expr}`. Every
|
|
54
|
+
* real usage in this codebase passes a plain string; a dynamic one is left
|
|
55
|
+
* alone and falls back to that component's own un-deduped slugify(title) -
|
|
56
|
+
* same behavior as before this plugin existed.
|
|
57
|
+
*
|
|
58
|
+
* Scoped per MDX *file*, not per rendered page - a snippet imported into a
|
|
59
|
+
* page (see docs/dev/docs/snippets.mdx) is its own independently-compiled
|
|
60
|
+
* MDX module with its own AST, so a title duplicated between a page and a
|
|
61
|
+
* snippet it imports won't be caught here. Acceptable for the same reason
|
|
62
|
+
* remarkInjectBuiltinComponents' own per-file scope is: there's no single
|
|
63
|
+
* shared AST spanning both by the time either one's transform runs.
|
|
64
|
+
*/
|
|
65
|
+
export function remarkTitleAnchorIds() {
|
|
66
|
+
return (tree) => {
|
|
67
|
+
const seen = new Map();
|
|
68
|
+
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
|
|
69
|
+
if (!node.name || !TITLED_COMPONENT_NAMES.includes(node.name)) return;
|
|
70
|
+
const titleAttr = node.attributes?.find(
|
|
71
|
+
(attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'title'
|
|
72
|
+
);
|
|
73
|
+
if (!titleAttr || typeof titleAttr.value !== 'string') return;
|
|
74
|
+
|
|
75
|
+
const base = slugify(titleAttr.value);
|
|
76
|
+
if (!base) return;
|
|
77
|
+
const priorCount = seen.get(base) ?? 0;
|
|
78
|
+
seen.set(base, priorCount + 1);
|
|
79
|
+
const id = priorCount === 0 ? base : `${base}-${priorCount + 1}`;
|
|
80
|
+
|
|
81
|
+
node.attributes.push({ type: 'mdxJsxAttribute', name: '_titleId', value: id });
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Turns a ```mermaid fenced block into a plain <div class="mermaid">
|
|
2
|
+
// containing its raw (un-highlighted) source text, ready for mermaid.js
|
|
3
|
+
// to render into an SVG diagram client-side - see initMermaid() in
|
|
4
|
+
// [...slug].astro for the render/re-render-on-theme-change side of this.
|
|
5
|
+
//
|
|
6
|
+
// Registered in astro.config.mjs's markdown.rehypePlugins, which Astro
|
|
7
|
+
// runs *after* its own internal Shiki syntax-highlighting step (see
|
|
8
|
+
// @astrojs/markdown-remark's index.js) - astro.config.mjs also sets
|
|
9
|
+
// markdown.syntaxHighlight.excludeLangs: ['mermaid'], so a ```mermaid
|
|
10
|
+
// block reaches this plugin completely untouched by Shiki: a plain
|
|
11
|
+
// <pre><code class="language-mermaid">raw source</code></pre>, no
|
|
12
|
+
// tokenization, no theme-specific inline styles to strip back out. This
|
|
13
|
+
// plugin's only job is reshaping that into the flat container mermaid.js
|
|
14
|
+
// expects (a single element directly containing the diagram source, not
|
|
15
|
+
// nested in a <code>).
|
|
16
|
+
//
|
|
17
|
+
// Deliberately a hand-written recursive walk rather than pulling in
|
|
18
|
+
// unist-util-visit for one simple find-and-replace - mirrors
|
|
19
|
+
// shiki-code-block.js's own direct hast.children manipulation instead of
|
|
20
|
+
// reaching for a visitor library for something this small.
|
|
21
|
+
|
|
22
|
+
const LANGUAGE_MERMAID_RE = /\blanguage-mermaid\b/;
|
|
23
|
+
|
|
24
|
+
function hasClass(properties, re) {
|
|
25
|
+
const className = properties?.className;
|
|
26
|
+
if (Array.isArray(className)) return className.some((c) => typeof c === 'string' && re.test(c));
|
|
27
|
+
if (typeof className === 'string') return re.test(className);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A Shiki-excluded ```mermaid block always survives as exactly
|
|
32
|
+
* `<pre><code class="language-mermaid">...</code></pre>` (see
|
|
33
|
+
* highlightCodeBlocks() in @astrojs/markdown-remark - it only ever
|
|
34
|
+
* replaces the matched <code>'s parent when there's exactly one child),
|
|
35
|
+
* so both checks together are enough to identify one unambiguously. */
|
|
36
|
+
function isMermaidCodeBlock(node) {
|
|
37
|
+
if (node.type !== 'element' || node.tagName !== 'pre' || node.children.length !== 1) return false;
|
|
38
|
+
const code = node.children[0];
|
|
39
|
+
return code.type === 'element' && code.tagName === 'code' && hasClass(code.properties, LANGUAGE_MERMAID_RE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function textOf(node) {
|
|
43
|
+
if (node.type === 'text') return node.value;
|
|
44
|
+
if (!node.children) return '';
|
|
45
|
+
return node.children.map(textOf).join('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function walk(node) {
|
|
49
|
+
if (!node || !Array.isArray(node.children)) return;
|
|
50
|
+
node.children = node.children.map((child) => {
|
|
51
|
+
if (isMermaidCodeBlock(child)) {
|
|
52
|
+
const codeNode = child.children[0];
|
|
53
|
+
return {
|
|
54
|
+
type: 'element',
|
|
55
|
+
tagName: 'div',
|
|
56
|
+
// data-pagefind-ignore: raw mermaid syntax ("graph TD; A-->B;")
|
|
57
|
+
// makes for garbled, useless search excerpts - see the same
|
|
58
|
+
// attribute on [...slug].astro's .wd-prevnext nav.
|
|
59
|
+
properties: { className: ['mermaid'], 'data-pagefind-ignore': true },
|
|
60
|
+
children: [{ type: 'text', value: textOf(codeNode) }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
walk(child);
|
|
64
|
+
return child;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function rehypeMermaid() {
|
|
69
|
+
return (tree) => {
|
|
70
|
+
walk(tree);
|
|
71
|
+
};
|
|
72
|
+
}
|