create-zudo-doc 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 +21 -0
- package/README.md +146 -0
- package/bin/create-zudo-doc.js +2 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.js +13 -0
- package/dist/claude-md-gen.d.ts +2 -0
- package/dist/claude-md-gen.js +113 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +157 -0
- package/dist/compose.d.ts +95 -0
- package/dist/compose.js +206 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.js +224 -0
- package/dist/features/body-foot-util.d.ts +10 -0
- package/dist/features/body-foot-util.js +12 -0
- package/dist/features/claude-resources.d.ts +2 -0
- package/dist/features/claude-resources.js +6 -0
- package/dist/features/design-token-panel.d.ts +14 -0
- package/dist/features/design-token-panel.js +27 -0
- package/dist/features/doc-history.d.ts +9 -0
- package/dist/features/doc-history.js +11 -0
- package/dist/features/doc-tags.d.ts +19 -0
- package/dist/features/doc-tags.js +33 -0
- package/dist/features/footer-taglist.d.ts +14 -0
- package/dist/features/footer-taglist.js +17 -0
- package/dist/features/footer.d.ts +8 -0
- package/dist/features/footer.js +10 -0
- package/dist/features/i18n.d.ts +22 -0
- package/dist/features/i18n.js +41 -0
- package/dist/features/image-enlarge.d.ts +11 -0
- package/dist/features/image-enlarge.js +13 -0
- package/dist/features/index.d.ts +15 -0
- package/dist/features/index.js +53 -0
- package/dist/features/llms-txt.d.ts +11 -0
- package/dist/features/llms-txt.js +13 -0
- package/dist/features/search.d.ts +9 -0
- package/dist/features/search.js +11 -0
- package/dist/features/sidebar-resizer.d.ts +14 -0
- package/dist/features/sidebar-resizer.js +16 -0
- package/dist/features/sidebar-toggle.d.ts +13 -0
- package/dist/features/sidebar-toggle.js +15 -0
- package/dist/features/tag-governance.d.ts +14 -0
- package/dist/features/tag-governance.js +16 -0
- package/dist/features/tauri-dev.d.ts +2 -0
- package/dist/features/tauri-dev.js +25 -0
- package/dist/features/tauri.d.ts +11 -0
- package/dist/features/tauri.js +52 -0
- package/dist/features/versioning.d.ts +27 -0
- package/dist/features/versioning.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +150 -0
- package/dist/preset.d.ts +37 -0
- package/dist/preset.js +156 -0
- package/dist/prompts.d.ts +32 -0
- package/dist/prompts.js +248 -0
- package/dist/scaffold.d.ts +4 -0
- package/dist/scaffold.js +344 -0
- package/dist/settings-gen.d.ts +2 -0
- package/dist/settings-gen.js +237 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +34 -0
- package/dist/zfb-config-gen.d.ts +19 -0
- package/dist/zfb-config-gen.js +222 -0
- package/package.json +65 -0
- package/templates/base/.htmlvalidate.json +5 -0
- package/templates/base/.zfb/doc-history-meta.json +1 -0
- package/templates/base/pages/404.tsx +55 -0
- package/templates/base/pages/_data.ts +179 -0
- package/templates/base/pages/_mdx-components.ts +249 -0
- package/templates/base/pages/docs/[...slug].tsx +448 -0
- package/templates/base/pages/index.tsx +158 -0
- package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
- package/templates/base/pages/lib/_category-nav.tsx +148 -0
- package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
- package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
- package/templates/base/pages/lib/_details.tsx +30 -0
- package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
- package/templates/base/pages/lib/_extract-headings.ts +81 -0
- package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
- package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
- package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
- package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
- package/templates/base/pages/lib/_math-block.tsx +63 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
- package/templates/base/pages/lib/_preset-generator.tsx +81 -0
- package/templates/base/pages/lib/_search-widget-script.ts +388 -0
- package/templates/base/pages/lib/_search-widget.tsx +196 -0
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
- package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
- package/templates/base/pages/lib/locale-merge.ts +58 -0
- package/templates/base/pages/lib/route-enumerators.ts +302 -0
- package/templates/base/pages/sitemap.xml.tsx +51 -0
- package/templates/base/plugins/connect-adapter.mjs +144 -0
- package/templates/base/plugins/copy-public-plugin.mjs +50 -0
- package/templates/base/plugins/search-index-plugin.mjs +54 -0
- package/templates/base/scripts/run-b4push.sh +102 -0
- package/templates/base/src/components/ai-chat-modal.tsx +15 -0
- package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
- package/templates/base/src/components/content/component-map.ts +25 -0
- package/templates/base/src/components/content/content-blockquote.tsx +16 -0
- package/templates/base/src/components/content/content-code.tsx +117 -0
- package/templates/base/src/components/content/content-link.tsx +83 -0
- package/templates/base/src/components/content/content-ol.tsx +19 -0
- package/templates/base/src/components/content/content-paragraph.tsx +10 -0
- package/templates/base/src/components/content/content-strong.tsx +16 -0
- package/templates/base/src/components/content/content-table.tsx +18 -0
- package/templates/base/src/components/content/content-ul.tsx +18 -0
- package/templates/base/src/components/content/heading-h2.tsx +26 -0
- package/templates/base/src/components/content/heading-h3.tsx +26 -0
- package/templates/base/src/components/content/heading-h4.tsx +26 -0
- package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
- package/templates/base/src/components/doc-history.tsx +18 -0
- package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
- package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
- package/templates/base/src/components/html-preview/preflight.ts +112 -0
- package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
- package/templates/base/src/components/image-enlarge.tsx +19 -0
- package/templates/base/src/components/mobile-toc.tsx +94 -0
- package/templates/base/src/components/preset-generator.tsx +14 -0
- package/templates/base/src/components/sidebar-toggle.tsx +98 -0
- package/templates/base/src/components/sidebar-tree.tsx +543 -0
- package/templates/base/src/components/site-tree-nav.tsx +233 -0
- package/templates/base/src/components/theme-toggle.tsx +93 -0
- package/templates/base/src/components/toc.tsx +63 -0
- package/templates/base/src/components/tree-nav-shared.tsx +71 -0
- package/templates/base/src/config/color-scheme-utils.ts +182 -0
- package/templates/base/src/config/color-schemes.ts +128 -0
- package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
- package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
- package/templates/base/src/config/i18n.ts +225 -0
- package/templates/base/src/config/settings-types.ts +162 -0
- package/templates/base/src/config/sidebars.ts +66 -0
- package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
- package/templates/base/src/config/tag-vocabulary.ts +20 -0
- package/templates/base/src/hooks/use-active-heading.ts +133 -0
- package/templates/base/src/plugins/docs-source-map.ts +103 -0
- package/templates/base/src/plugins/hast-utils.ts +10 -0
- package/templates/base/src/plugins/rehype-code-title.ts +50 -0
- package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
- package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
- package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
- package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
- package/templates/base/src/plugins/remark-admonitions.ts +99 -0
- package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
- package/templates/base/src/plugins/url-utils.ts +4 -0
- package/templates/base/src/styles/global.css +1066 -0
- package/templates/base/src/types/docs-entry.ts +39 -0
- package/templates/base/src/types/heading.ts +5 -0
- package/templates/base/src/types/locale.ts +10 -0
- package/templates/base/src/utils/base.ts +139 -0
- package/templates/base/src/utils/content-files.ts +106 -0
- package/templates/base/src/utils/dedent.ts +24 -0
- package/templates/base/src/utils/docs.ts +335 -0
- package/templates/base/src/utils/git-info.ts +70 -0
- package/templates/base/src/utils/github.ts +19 -0
- package/templates/base/src/utils/header-right-items.ts +38 -0
- package/templates/base/src/utils/nav-scope.ts +63 -0
- package/templates/base/src/utils/sidebar.ts +104 -0
- package/templates/base/src/utils/slug.ts +10 -0
- package/templates/base/src/utils/smart-break.tsx +126 -0
- package/templates/base/src/utils/tags.ts +126 -0
- package/templates/base/tsconfig.json +36 -0
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
- package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
- package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
- package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
- package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
- package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
- package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
- package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
- package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
- package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
- package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
- package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
- package/templates/features/tauri/files/src-tauri/build.rs +3 -0
- package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
- package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
- package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
- package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
- package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
- package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
- package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
- package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
- package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
- package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
- package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
- package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
- package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
- package/templates/features/versioning/files/pages/v/[version]/ja/docs/[...slug].tsx +490 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// zfb plugin module: claude-resources.
|
|
2
|
+
//
|
|
3
|
+
// Wires `runClaudeResourcesPreStep` (from
|
|
4
|
+
// `@takazudo/zudo-doc/integrations/claude-resources`) into zfb's
|
|
5
|
+
// `preBuild` lifecycle hook. Replaces the npm `prebuild`-script glue
|
|
6
|
+
// in `scripts/zfb-prebuild.mjs` for this step (the script remains in
|
|
7
|
+
// place during the merge window — T6 retires it).
|
|
8
|
+
//
|
|
9
|
+
// Why this shim exists (and is not the v2 integration module directly):
|
|
10
|
+
//
|
|
11
|
+
// 1. zfb's plugin host runs `node` without any TypeScript loader, so
|
|
12
|
+
// it cannot import `.ts` source files. The v2 integration package
|
|
13
|
+
// (`@takazudo/zudo-doc/integrations/claude-resources`) currently
|
|
14
|
+
// ships only TypeScript source through its `exports` map and has
|
|
15
|
+
// no build step.
|
|
16
|
+
//
|
|
17
|
+
// 2. The `runClaudeResourcesPreStep` runner pulls in `gray-matter`,
|
|
18
|
+
// which performs a CJS `require("fs")` that esbuild's
|
|
19
|
+
// configuration-loader bundle (ESM-only) cannot satisfy. Loading
|
|
20
|
+
// it inline at the top of `zfb.config.ts` therefore breaks config
|
|
21
|
+
// parsing entirely.
|
|
22
|
+
//
|
|
23
|
+
// Both problems are solved by isolating the runner behind a child
|
|
24
|
+
// process: this shim spawns `tsx` (the project's existing TS-aware
|
|
25
|
+
// Node runner, pinned via `tsx` in `package.json`) on a tiny inline
|
|
26
|
+
// script that imports the runner. tsx handles `.ts` resolution and
|
|
27
|
+
// Node's CJS↔ESM interop for gray-matter; the parent plugin host stays
|
|
28
|
+
// in plain Node and only sees a child-process exit code.
|
|
29
|
+
//
|
|
30
|
+
// Inline functions are not supported by zfb's plugin runtime — see
|
|
31
|
+
// `@takazudo/zfb/plugins` source comment. This file is the plugin-host
|
|
32
|
+
// equivalent once the npm script is retired (T6).
|
|
33
|
+
|
|
34
|
+
import { spawn } from "node:child_process";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
import { dirname, resolve } from "node:path";
|
|
37
|
+
|
|
38
|
+
const PLUGIN_NAME = "@takazudo/zudo-doc-claude-resources";
|
|
39
|
+
|
|
40
|
+
// `tsx` is a workspace dependency of the host project and resolves to
|
|
41
|
+
// `<projectRoot>/node_modules/.bin/tsx` from this file. We resolve the
|
|
42
|
+
// binary explicitly so the shim does not depend on the parent shell's
|
|
43
|
+
// `PATH` (the plugin host is spawned by zfb without sourcing the
|
|
44
|
+
// user's profile — Node's `PATH` is whatever zfb itself was launched
|
|
45
|
+
// with).
|
|
46
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
const TSX_BIN = resolve(HERE, "..", "node_modules", ".bin", "tsx");
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run the v2 claude-resources runner under tsx.
|
|
51
|
+
*
|
|
52
|
+
* Inlines a minimal ESM script via `tsx -e` so we don't have to ship a
|
|
53
|
+
* second `.ts` file just to host the call site. The runner returns a
|
|
54
|
+
* `{ claudemd, commands, skills, agents }` summary which we re-emit on
|
|
55
|
+
* the child's stdout — the parent (this shim) parses it and forwards
|
|
56
|
+
* the summary to the plugin host's logger.
|
|
57
|
+
*/
|
|
58
|
+
function runRunnerUnderTsx({ claudeDir, projectRoot, docsDir }) {
|
|
59
|
+
// The runner accepts `projectRoot` and `docsDir` as relative paths
|
|
60
|
+
// (resolved against `process.cwd()` in the runner). To insulate the
|
|
61
|
+
// child from whatever cwd zfb spawned us with, we set cwd explicitly
|
|
62
|
+
// and pass absolute paths through.
|
|
63
|
+
// tsx's `-e` flag emits CJS by default (it picks the format from the
|
|
64
|
+
// entry's extension, and an inline `-e` script has none), so we wrap
|
|
65
|
+
// the body in an `async`-IIFE rather than relying on top-level await.
|
|
66
|
+
const childScript = `
|
|
67
|
+
(async () => {
|
|
68
|
+
const { runClaudeResourcesPreStep } = await import("@takazudo/zudo-doc/integrations/claude-resources");
|
|
69
|
+
const result = await runClaudeResourcesPreStep({
|
|
70
|
+
claudeDir: ${JSON.stringify(claudeDir)},
|
|
71
|
+
projectRoot: ${JSON.stringify(projectRoot)},
|
|
72
|
+
docsDir: ${JSON.stringify(docsDir)},
|
|
73
|
+
});
|
|
74
|
+
process.stdout.write(JSON.stringify(result));
|
|
75
|
+
})().catch((err) => {
|
|
76
|
+
process.stderr.write(err && err.stack ? err.stack : String(err));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
return new Promise((resolveCall, reject) => {
|
|
82
|
+
const child = spawn(TSX_BIN, ["-e", childScript], {
|
|
83
|
+
cwd: projectRoot,
|
|
84
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
85
|
+
});
|
|
86
|
+
let stdout = "";
|
|
87
|
+
let stderr = "";
|
|
88
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
89
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
90
|
+
child.on("error", reject);
|
|
91
|
+
child.on("close", (code) => {
|
|
92
|
+
if (code !== 0) {
|
|
93
|
+
reject(
|
|
94
|
+
new Error(
|
|
95
|
+
`[${PLUGIN_NAME}] runner exited with code ${code}\n${stderr}`,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
resolveCall(JSON.parse(stdout));
|
|
102
|
+
} catch (err) {
|
|
103
|
+
reject(
|
|
104
|
+
new Error(
|
|
105
|
+
`[${PLUGIN_NAME}] failed to parse runner stdout: ${err.message}\nstdout: ${stdout}\nstderr: ${stderr}`,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default {
|
|
114
|
+
name: PLUGIN_NAME,
|
|
115
|
+
async preBuild(ctx) {
|
|
116
|
+
const claudeDir = ctx.options.claudeDir;
|
|
117
|
+
if (typeof claudeDir !== "string" || claudeDir.length === 0) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`[${PLUGIN_NAME}] preBuild: options.claudeDir must be a non-empty string (got ${JSON.stringify(claudeDir)})`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const projectRootOpt = ctx.options.projectRoot;
|
|
123
|
+
const docsDirOpt = ctx.options.docsDir;
|
|
124
|
+
const result = await runRunnerUnderTsx({
|
|
125
|
+
claudeDir,
|
|
126
|
+
projectRoot:
|
|
127
|
+
typeof projectRootOpt === "string" ? projectRootOpt : ctx.projectRoot,
|
|
128
|
+
docsDir: typeof docsDirOpt === "string" ? docsDirOpt : "src/content/docs",
|
|
129
|
+
});
|
|
130
|
+
// Surface a one-line summary so build logs make the migration
|
|
131
|
+
// observable (mirrors the `[zfb-prebuild]` lines from the legacy
|
|
132
|
+
// npm-script glue).
|
|
133
|
+
ctx.logger.info(
|
|
134
|
+
`claude-resources: ${result.claudemd} CLAUDE.md, ${result.commands} commands, ${result.skills} skills, ${result.agents} agents`,
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { escapeForMdx } from "../escape-for-mdx";
|
|
3
|
+
|
|
4
|
+
describe("escapeForMdx", () => {
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Path-like angle bracket sequences (not self-closing JSX)
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
it("escapes path-like <name>/suffix sequences", () => {
|
|
10
|
+
expect(escapeForMdx("<foo>/bar")).toBe("<foo>/bar");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("escapes path-like angle brackets in prose", () => {
|
|
14
|
+
expect(escapeForMdx("binary at <repo>/target/release/zfb")).toBe(
|
|
15
|
+
"binary at <repo>/target/release/zfb",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Allowlisted HTML tags must pass through unchanged
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
it("preserves allowlisted HTML tags followed by a path separator", () => {
|
|
24
|
+
expect(escapeForMdx("<div>/path")).toBe("<div>/path");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Self-closing non-allowlisted tags must still be escaped
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
it("escapes self-closing non-allowlisted component tags", () => {
|
|
32
|
+
expect(escapeForMdx("<Component />")).toMatch(/<Component\s*\/>/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
import { generateClaudeResourcesDocs } from "../generate";
|
|
7
|
+
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let claudeDir: string;
|
|
10
|
+
let docsDir: string;
|
|
11
|
+
|
|
12
|
+
function createFixture() {
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-res-test-"));
|
|
14
|
+
|
|
15
|
+
// .claude/ directory
|
|
16
|
+
claudeDir = path.join(tmpDir, ".claude");
|
|
17
|
+
docsDir = path.join(tmpDir, "docs");
|
|
18
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
// Commands
|
|
21
|
+
const commandsDir = path.join(claudeDir, "commands");
|
|
22
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
path.join(commandsDir, "test-cmd.md"),
|
|
25
|
+
'---\ndescription: "A test command"\n---\n\nThis is a test command body.',
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Skills
|
|
29
|
+
const skillDir = path.join(claudeDir, "skills", "test-skill");
|
|
30
|
+
fs.mkdirSync(path.join(skillDir, "references"), { recursive: true });
|
|
31
|
+
fs.mkdirSync(path.join(skillDir, "scripts"), { recursive: true });
|
|
32
|
+
fs.mkdirSync(path.join(skillDir, "assets"), { recursive: true });
|
|
33
|
+
|
|
34
|
+
fs.writeFileSync(
|
|
35
|
+
path.join(skillDir, "SKILL.md"),
|
|
36
|
+
'---\nname: test-skill\ndescription: "A test skill"\n---\n\nSkill instructions here.\n\nSee [references/guide.md](references/guide.md) for details.',
|
|
37
|
+
);
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(skillDir, "references", "guide.md"),
|
|
40
|
+
"# Guide\n\nSome guide content",
|
|
41
|
+
);
|
|
42
|
+
fs.writeFileSync(
|
|
43
|
+
path.join(skillDir, "scripts", "run.sh"),
|
|
44
|
+
"#!/bin/bash\n# Run the test",
|
|
45
|
+
);
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
path.join(skillDir, "assets", "template.md"),
|
|
48
|
+
"# Template\n\nA template",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Agents
|
|
52
|
+
const agentsDir = path.join(claudeDir, "agents");
|
|
53
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(agentsDir, "test-agent.md"),
|
|
56
|
+
'---\nname: test-agent\ndescription: "A test agent"\nmodel: sonnet\n---\n\nAgent instructions here.',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Root CLAUDE.md
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(tmpDir, "CLAUDE.md"),
|
|
62
|
+
"# Project\n\nProject instructions",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("generateClaudeResourcesDocs", () => {
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
createFixture();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// File structure tests
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
describe("file structure", () => {
|
|
80
|
+
it("generates correct directory structure", () => {
|
|
81
|
+
generateClaudeResourcesDocs({
|
|
82
|
+
claudeDir,
|
|
83
|
+
projectRoot: tmpDir,
|
|
84
|
+
docsDir,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(fs.existsSync(path.join(docsDir, "claude"))).toBe(true);
|
|
88
|
+
expect(fs.existsSync(path.join(docsDir, "claude-md"))).toBe(true);
|
|
89
|
+
expect(fs.existsSync(path.join(docsDir, "claude-commands"))).toBe(true);
|
|
90
|
+
expect(fs.existsSync(path.join(docsDir, "claude-skills"))).toBe(true);
|
|
91
|
+
expect(fs.existsSync(path.join(docsDir, "claude-agents"))).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("generates _category_.json with noPage for sub-categories", () => {
|
|
95
|
+
generateClaudeResourcesDocs({
|
|
96
|
+
claudeDir,
|
|
97
|
+
projectRoot: tmpDir,
|
|
98
|
+
docsDir,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const dirs = ["claude-md", "claude-commands", "claude-skills", "claude-agents"];
|
|
102
|
+
for (const dir of dirs) {
|
|
103
|
+
const catPath = path.join(docsDir, dir, "_category_.json");
|
|
104
|
+
expect(fs.existsSync(catPath)).toBe(true);
|
|
105
|
+
|
|
106
|
+
const cat = JSON.parse(fs.readFileSync(catPath, "utf8"));
|
|
107
|
+
expect(cat).toHaveProperty("label");
|
|
108
|
+
expect(cat).toHaveProperty("position");
|
|
109
|
+
expect(cat).toHaveProperty("description");
|
|
110
|
+
expect(cat.noPage).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("generates skill as flat .mdx file", () => {
|
|
115
|
+
generateClaudeResourcesDocs({
|
|
116
|
+
claudeDir,
|
|
117
|
+
projectRoot: tmpDir,
|
|
118
|
+
docsDir,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const flatPath = path.join(docsDir, "claude-skills", "test-skill.mdx");
|
|
122
|
+
expect(fs.existsSync(flatPath)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Content tests
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
describe("content", () => {
|
|
131
|
+
it("generates overview page with CategoryNav", () => {
|
|
132
|
+
generateClaudeResourcesDocs({
|
|
133
|
+
claudeDir,
|
|
134
|
+
projectRoot: tmpDir,
|
|
135
|
+
docsDir,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const overview = fs.readFileSync(
|
|
139
|
+
path.join(docsDir, "claude", "index.mdx"),
|
|
140
|
+
"utf8",
|
|
141
|
+
);
|
|
142
|
+
expect(overview).toContain('<CategoryNav categories={');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("skill page has correct frontmatter", () => {
|
|
146
|
+
generateClaudeResourcesDocs({
|
|
147
|
+
claudeDir,
|
|
148
|
+
projectRoot: tmpDir,
|
|
149
|
+
docsDir,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const skillPage = fs.readFileSync(
|
|
153
|
+
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
154
|
+
"utf8",
|
|
155
|
+
);
|
|
156
|
+
const parsed = matter(skillPage);
|
|
157
|
+
|
|
158
|
+
expect(parsed.data.title).toBe("test-skill");
|
|
159
|
+
expect(parsed.data.description).toBe("A test skill");
|
|
160
|
+
expect(parsed.data.sidebar_label).toBe("test-skill");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("skill page has file tree", () => {
|
|
164
|
+
generateClaudeResourcesDocs({
|
|
165
|
+
claudeDir,
|
|
166
|
+
projectRoot: tmpDir,
|
|
167
|
+
docsDir,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const skillPage = fs.readFileSync(
|
|
171
|
+
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
172
|
+
"utf8",
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Should contain tree-drawing characters
|
|
176
|
+
expect(skillPage).toContain("├── ");
|
|
177
|
+
expect(skillPage).toContain("└── ");
|
|
178
|
+
expect(skillPage).toContain("test-skill/");
|
|
179
|
+
expect(skillPage).toContain("SKILL.md");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("skill page has links to sub-files that resolve correctly from the page URL", () => {
|
|
183
|
+
generateClaudeResourcesDocs({
|
|
184
|
+
claudeDir,
|
|
185
|
+
projectRoot: tmpDir,
|
|
186
|
+
docsDir,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const skillPage = fs.readFileSync(
|
|
190
|
+
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
191
|
+
"utf8",
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Links use ./<subpage> format (relative to the skill page URL which
|
|
195
|
+
// already includes the skill dir, e.g. /docs/claude-skills/test-skill/)
|
|
196
|
+
expect(skillPage).toContain("./ref-guide");
|
|
197
|
+
expect(skillPage).toContain("./asset-template");
|
|
198
|
+
|
|
199
|
+
// Must NOT contain the double-dir pattern ./<dir>/<subpage>
|
|
200
|
+
expect(skillPage).not.toContain("./test-skill/ref-guide");
|
|
201
|
+
expect(skillPage).not.toContain("./test-skill/asset-template");
|
|
202
|
+
|
|
203
|
+
// Each linked sub-page must exist as a generated flat .mdx file
|
|
204
|
+
// The file is flat (test-skill--ref-guide.mdx) but slug is nested
|
|
205
|
+
const linkPattern = /\]\(\.\/([\w-]+)\)/g;
|
|
206
|
+
let match;
|
|
207
|
+
while ((match = linkPattern.exec(skillPage)) !== null) {
|
|
208
|
+
const subPage = match[1];
|
|
209
|
+
const targetFile = path.join(
|
|
210
|
+
docsDir,
|
|
211
|
+
"claude-skills",
|
|
212
|
+
`test-skill--${subPage}.mdx`,
|
|
213
|
+
);
|
|
214
|
+
expect(
|
|
215
|
+
fs.existsSync(targetFile),
|
|
216
|
+
`Link target "test-skill--${subPage}.mdx" should exist`,
|
|
217
|
+
).toBe(true);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("skill body references/scripts/assets links are rewritten to doc site format", () => {
|
|
222
|
+
generateClaudeResourcesDocs({
|
|
223
|
+
claudeDir,
|
|
224
|
+
projectRoot: tmpDir,
|
|
225
|
+
docsDir,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const skillPage = fs.readFileSync(
|
|
229
|
+
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
230
|
+
"utf8",
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Body links like (references/guide.md) should be rewritten to (./ref-guide)
|
|
234
|
+
expect(skillPage).toContain("](./ref-guide)");
|
|
235
|
+
expect(skillPage).not.toContain("](references/guide.md)");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("agent page has model badge", () => {
|
|
239
|
+
generateClaudeResourcesDocs({
|
|
240
|
+
claudeDir,
|
|
241
|
+
projectRoot: tmpDir,
|
|
242
|
+
docsDir,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const agentPage = fs.readFileSync(
|
|
246
|
+
path.join(docsDir, "claude-agents", "test-agent.mdx"),
|
|
247
|
+
"utf8",
|
|
248
|
+
);
|
|
249
|
+
expect(agentPage).toContain("**Model:** `sonnet`");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Sub-file page tests
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
describe("sub-file pages", () => {
|
|
258
|
+
it("generates unlisted reference page", () => {
|
|
259
|
+
generateClaudeResourcesDocs({
|
|
260
|
+
claudeDir,
|
|
261
|
+
projectRoot: tmpDir,
|
|
262
|
+
docsDir,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const refPage = path.join(docsDir, "claude-skills", "test-skill--ref-guide.mdx");
|
|
266
|
+
expect(fs.existsSync(refPage)).toBe(true);
|
|
267
|
+
|
|
268
|
+
const parsed = matter(fs.readFileSync(refPage, "utf8"));
|
|
269
|
+
expect(parsed.data.unlisted).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("generates unlisted asset page for .md files", () => {
|
|
273
|
+
generateClaudeResourcesDocs({
|
|
274
|
+
claudeDir,
|
|
275
|
+
projectRoot: tmpDir,
|
|
276
|
+
docsDir,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const assetPage = path.join(docsDir, "claude-skills", "test-skill--asset-template.mdx");
|
|
280
|
+
expect(fs.existsSync(assetPage)).toBe(true);
|
|
281
|
+
|
|
282
|
+
const parsed = matter(fs.readFileSync(assetPage, "utf8"));
|
|
283
|
+
expect(parsed.data.unlisted).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("does NOT generate page for non-.md scripts", () => {
|
|
287
|
+
generateClaudeResourcesDocs({
|
|
288
|
+
claudeDir,
|
|
289
|
+
projectRoot: tmpDir,
|
|
290
|
+
docsDir,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const scriptPage = path.join(docsDir, "claude-skills", "test-skill--script-run.mdx");
|
|
294
|
+
expect(fs.existsSync(scriptPage)).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("sub-pages have custom slug for nested breadcrumbs", () => {
|
|
298
|
+
generateClaudeResourcesDocs({
|
|
299
|
+
claudeDir,
|
|
300
|
+
projectRoot: tmpDir,
|
|
301
|
+
docsDir,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const refPage = fs.readFileSync(
|
|
305
|
+
path.join(docsDir, "claude-skills", "test-skill--ref-guide.mdx"),
|
|
306
|
+
"utf8",
|
|
307
|
+
);
|
|
308
|
+
const parsed = matter(refPage);
|
|
309
|
+
expect(parsed.data.slug).toBe("claude-skills/test-skill/ref-guide");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("reference page content is correct", () => {
|
|
313
|
+
generateClaudeResourcesDocs({
|
|
314
|
+
claudeDir,
|
|
315
|
+
projectRoot: tmpDir,
|
|
316
|
+
docsDir,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const refPage = fs.readFileSync(
|
|
320
|
+
path.join(docsDir, "claude-skills", "test-skill--ref-guide.mdx"),
|
|
321
|
+
"utf8",
|
|
322
|
+
);
|
|
323
|
+
const parsed = matter(refPage);
|
|
324
|
+
|
|
325
|
+
expect(parsed.data.title).toBe("Guide");
|
|
326
|
+
expect(parsed.content).toContain("Some guide content");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Category metadata tests
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
describe("category metadata", () => {
|
|
335
|
+
it("_category_.json positions are ordered correctly", () => {
|
|
336
|
+
generateClaudeResourcesDocs({
|
|
337
|
+
claudeDir,
|
|
338
|
+
projectRoot: tmpDir,
|
|
339
|
+
docsDir,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const readPos = (dir: string) => {
|
|
343
|
+
const cat = JSON.parse(
|
|
344
|
+
fs.readFileSync(path.join(docsDir, dir, "_category_.json"), "utf8"),
|
|
345
|
+
);
|
|
346
|
+
return cat.position;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
expect(readPos("claude-md")).toBe(900);
|
|
350
|
+
expect(readPos("claude-commands")).toBe(901);
|
|
351
|
+
expect(readPos("claude-skills")).toBe(902);
|
|
352
|
+
expect(readPos("claude-agents")).toBe(903);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Return value test
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
describe("return value", () => {
|
|
361
|
+
it("returns correct counts", () => {
|
|
362
|
+
const result = generateClaudeResourcesDocs({
|
|
363
|
+
claudeDir,
|
|
364
|
+
projectRoot: tmpDir,
|
|
365
|
+
docsDir,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(result).toEqual({
|
|
369
|
+
claudemd: 1,
|
|
370
|
+
commands: 1,
|
|
371
|
+
skills: 1,
|
|
372
|
+
agents: 1,
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const htmlTags = new Set([
|
|
2
|
+
"div", "span", "p", "a", "img", "br", "hr", "ul", "ol", "li",
|
|
3
|
+
"h1", "h2", "h3", "h4", "h5", "h6", "code", "pre", "blockquote",
|
|
4
|
+
"table", "tr", "td", "th", "thead", "tbody", "tfoot", "colgroup", "col",
|
|
5
|
+
"strong", "em", "b", "i", "u", "s", "del", "ins", "sub", "sup",
|
|
6
|
+
"details", "summary", "figure", "figcaption", "mark", "small",
|
|
7
|
+
"cite", "q", "abbr", "dfn", "time", "var", "samp", "kbd",
|
|
8
|
+
"section", "article", "aside", "header", "footer", "nav", "main",
|
|
9
|
+
"form", "input", "button", "select", "option", "textarea", "label",
|
|
10
|
+
"fieldset", "legend", "dl", "dt", "dd", "caption",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Escape angle brackets and curly braces in content for MDX compatibility.
|
|
15
|
+
* Preserves content inside code blocks (``` ... ```) and inline code (` ... `).
|
|
16
|
+
* Handles 3+ backtick fenced blocks correctly.
|
|
17
|
+
*/
|
|
18
|
+
export function escapeForMdx(content: string): string {
|
|
19
|
+
// Extract code blocks (supports 3+ backtick fences via backreference)
|
|
20
|
+
const codeBlocks: string[] = [];
|
|
21
|
+
const placeholder = "\x00CODEBLOCK_";
|
|
22
|
+
const codeBlockRegex = /(`{3,})[^\n]*\n[\s\S]*?\1/g;
|
|
23
|
+
const withPlaceholders = content.replace(codeBlockRegex, (match) => {
|
|
24
|
+
const idx = codeBlocks.length;
|
|
25
|
+
codeBlocks.push(match);
|
|
26
|
+
return `${placeholder}${idx}\x00`;
|
|
27
|
+
});
|
|
28
|
+
const parts = withPlaceholders.split(
|
|
29
|
+
new RegExp(`(${placeholder}\\d+\x00)`, "g"),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return parts
|
|
33
|
+
.map((part) => {
|
|
34
|
+
const placeholderMatch = part.match(
|
|
35
|
+
new RegExp(`^${placeholder}(\\d+)\x00$`),
|
|
36
|
+
);
|
|
37
|
+
if (placeholderMatch) return codeBlocks[Number(placeholderMatch[1])];
|
|
38
|
+
|
|
39
|
+
// For non-code-block text, split on inline code to preserve it.
|
|
40
|
+
// Supports multi-backtick inline code (`` `code` ``, ``` ``code`` ```).
|
|
41
|
+
const inlineCodeRegex = /(`{1,3})(?!`)([\s\S]*?[^`])\1(?!`)/g;
|
|
42
|
+
const inlineCodes: string[] = [];
|
|
43
|
+
const inlinePlaceholder = "\x00INLINE_";
|
|
44
|
+
const withInlinePlaceholders = part.replace(
|
|
45
|
+
inlineCodeRegex,
|
|
46
|
+
(match) => {
|
|
47
|
+
const idx = inlineCodes.length;
|
|
48
|
+
inlineCodes.push(match);
|
|
49
|
+
return `${inlinePlaceholder}${idx}\x00`;
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let escaped = withInlinePlaceholders
|
|
54
|
+
// Escape opening tags: <Name>, <Name attr="val">
|
|
55
|
+
.replace(
|
|
56
|
+
/<([A-Za-z][A-Za-z0-9_-]*)(\s[^>]*)?>/g,
|
|
57
|
+
(match, name: string) => {
|
|
58
|
+
if (htmlTags.has(name.toLowerCase())) return match;
|
|
59
|
+
return match.replace(/</g, "<").replace(/>/g, ">");
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
// Escape closing tags: </Name>
|
|
63
|
+
.replace(
|
|
64
|
+
/<\/([A-Za-z][A-Za-z0-9_-]*)>/g,
|
|
65
|
+
(match, name: string) => {
|
|
66
|
+
if (htmlTags.has(name.toLowerCase())) return match;
|
|
67
|
+
return `</${name}>`;
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
// Escape self-closing tags: <Name />
|
|
71
|
+
.replace(
|
|
72
|
+
/<([A-Za-z][A-Za-z0-9_-]*)(\s[^>]*)?\s*\/>/g,
|
|
73
|
+
(match, name: string) => {
|
|
74
|
+
if (htmlTags.has(name.toLowerCase())) return match;
|
|
75
|
+
return match.replace(/</g, "<").replace(/>/g, ">");
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
.replace(/<(-+|=+)/g, "<$1")
|
|
79
|
+
.replace(/<(\d)/g, "<$1")
|
|
80
|
+
// Escape curly braces (MDX interprets them as JSX expressions)
|
|
81
|
+
.replace(/\{/g, "{")
|
|
82
|
+
.replace(/\}/g, "}");
|
|
83
|
+
|
|
84
|
+
// Restore inline code placeholders
|
|
85
|
+
escaped = escaped.replace(
|
|
86
|
+
new RegExp(`${inlinePlaceholder}(\\d+)\x00`, "g"),
|
|
87
|
+
(_, idx: string) => inlineCodes[Number(idx)],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return escaped;
|
|
91
|
+
})
|
|
92
|
+
.join("");
|
|
93
|
+
}
|