From 7be3d3d4c6909d8c7d533928b19b1d078dbcf4fb Mon Sep 17 00:00:00 2001 From: maxnorm Date: Sat, 21 Feb 2026 17:02:38 -0500 Subject: [PATCH 1/5] add markdown page for llms --- website/docusaurus.config.js | 6 + website/package-lock.json | 60 +++---- website/package.json | 1 + website/plugins/markdown-source-docs.js | 156 ++++++++++++++++++ .../docs/MarkdownActionsDropdown/index.js | 127 ++++++++++++++ website/src/css/breadcrumbs.css | 7 + website/src/css/custom.css | 3 +- website/src/css/markdown-source-plugin.css | 125 ++++++++++++++ .../hooks/useInjectMarkdownActionsDropdown.js | 42 +++++ website/src/theme/Root.js | 4 +- 10 files changed, 489 insertions(+), 42 deletions(-) create mode 100644 website/plugins/markdown-source-docs.js create mode 100644 website/src/components/docs/MarkdownActionsDropdown/index.js create mode 100644 website/src/css/markdown-source-plugin.css create mode 100644 website/src/hooks/useInjectMarkdownActionsDropdown.js diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 42e1f4eb..b88dfc94 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -4,9 +4,13 @@ // There are various equivalent ways to declare your Docusaurus config. // See: https://docusaurus.io/docs/api/docusaurus-config +import path from 'path'; +import {fileURLToPath} from 'url'; import dotenv from 'dotenv'; import {themes as prismThemes} from 'prism-react-renderer'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + dotenv.config(); // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) @@ -289,6 +293,8 @@ const config = { }), }), plugins: [ + path.join(__dirname, 'plugins', 'markdown-source-docs.js'), + process.env.POSTHOG_API_KEY && [ "posthog-docusaurus", { diff --git a/website/package-lock.json b/website/package-lock.json index f96dc3b1..665162cd 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -14,6 +14,7 @@ "@giscus/react": "^3.1.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "docusaurus-markdown-source-plugin": "^2.0.1", "dotenv": "^17.2.3", "gsap": "^3.13.0", "posthog-docusaurus": "^2.0.5", @@ -237,7 +238,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.43.0.tgz", "integrity": "sha512-wKy6x6fKcnB1CsfeNNdGp4dzLzz04k8II3JLt6Sp81F8s57Ks3/K9qsysmL9SJa8P486s719bBttVLE8JJYurQ==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.43.0", "@algolia/requester-browser-xhr": "5.43.0", @@ -385,7 +385,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2220,7 +2219,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2243,7 +2241,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2353,7 +2350,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2775,7 +2771,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3662,7 +3657,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4436,7 +4430,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -4764,7 +4757,6 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5395,7 +5387,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5752,7 +5743,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5838,7 +5828,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5884,7 +5873,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.43.0.tgz", "integrity": "sha512-hbkK41JsuGYhk+atBDxlcKxskjDCh3OOEDpdKZPtw+3zucBqhlojRG5e5KtCmByGyYvwZswVeaSWglgLn2fibg==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.9.0", "@algolia/client-abtesting": "5.43.0", @@ -6348,7 +6336,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6648,7 +6635,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -7347,7 +7333,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -7660,15 +7645,13 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cytoscape": { "version": "3.33.1", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -8090,7 +8073,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -8449,6 +8431,23 @@ "node": ">=6" } }, + "node_modules/docusaurus-markdown-source-plugin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/docusaurus-markdown-source-plugin/-/docusaurus-markdown-source-plugin-2.0.1.tgz", + "integrity": "sha512-RiKMi+SDlrQQ25VkbOvz9M7glH9lcpAsz0WzepilnC2MgXA6uLNpl7PezrlY6nvW7MnhB4Aakwwonz+0WVBQAQ==", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@docusaurus/core": "^3.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -9286,7 +9285,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13981,7 +13979,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14542,7 +14539,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15446,7 +15442,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -16266,7 +16261,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -16276,7 +16270,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -16349,7 +16342,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" }, @@ -16378,7 +16370,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -17112,13 +17103,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/search-insights": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", - "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", - "license": "MIT", - "peer": true - }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -18205,8 +18189,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/type-fest": { "version": "2.19.0", @@ -18602,7 +18585,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -18859,7 +18841,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -19446,7 +19427,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/website/package.json b/website/package.json index 3a38812b..b6a93ef0 100644 --- a/website/package.json +++ b/website/package.json @@ -20,6 +20,7 @@ "@giscus/react": "^3.1.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "docusaurus-markdown-source-plugin": "^2.0.1", "dotenv": "^17.2.3", "gsap": "^3.13.0", "posthog-docusaurus": "^2.0.5", diff --git a/website/plugins/markdown-source-docs.js b/website/plugins/markdown-source-docs.js new file mode 100644 index 00000000..2374ccca --- /dev/null +++ b/website/plugins/markdown-source-docs.js @@ -0,0 +1,156 @@ +/** + * Local plugin: extends docusaurus-markdown-source-plugin to also expose + * .mdx docs as .md URLs (plugin only copies .md by default). + */ +import { createRequire } from 'module'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); +const fs = require('fs-extra'); +const basePlugin = require('docusaurus-markdown-source-plugin'); + +// --- Copied from docusaurus-markdown-source-plugin (for .mdx support) --- +function convertTabsToMarkdown(content) { + const tabsPattern = /]*>([\s\S]*?)<\/Tabs>/g; + return content.replace(tabsPattern, (fullMatch, tabsContent) => { + const tabItemPattern = /]*value="([^"]*)"[^>]*label="([^"]*)"[^>]*>([\s\S]*?)<\/TabItem>/g; + let result = []; + let match; + while ((match = tabItemPattern.exec(tabsContent)) !== null) { + const [, , label, itemContent] = match; + const cleanContent = itemContent + .split('\n') + .map((line) => line.replace(/^\s{4}/, '')) + .join('\n') + .trim(); + result.push(`**${label}:**\n\n${cleanContent}`); + } + return result.join('\n\n---\n\n'); + }); +} + +function convertDetailsToMarkdown(content) { + const detailsPattern = /
\s*()?([^<]+)(<\/strong>)?<\/summary>([\s\S]*?)<\/details>/g; + return content.replace(detailsPattern, (fullMatch, strongOpen, summaryText, strongClose, detailsContent) => { + const cleanContent = detailsContent + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .join('\n') + .trim(); + return `### ${summaryText.trim()}\n\n${cleanContent}`; + }); +} + +function cleanMarkdownForDisplay(content, filepath) { + const fileDir = filepath.replace(/[^/]*$/, ''); + content = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ''); + content = content.replace(/^import\s+.*?from\s+['"].*?['"];?\s*$/gm, ''); + content = content.replace( + /

\s*\n?\s*([^\s*\n?\s*<\/p>/g, + (match, imagePath, alt) => { + const cleanPath = imagePath.replace('@site/static/', '/'); + return `![${alt}](${cleanPath})`; + } + ); + content = content.replace( + /]*src="https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]+)[^"]*"[^>]*title="([^"]*)"[^>]*>[\s\S]*?<\/iframe>/g, + 'Watch the video: [$2](https://www.youtube.com/watch?v=$1)' + ); + content = content.replace( + /]*>\s*]*>\s*<\/video>/g, + '

Video demonstration: $1

\n' + ); + content = content.replace(/[\s\S]*?<\/Head>/g, ''); + content = convertTabsToMarkdown(content); + content = convertDetailsToMarkdown(content); + content = content.replace(/<[A-Z][a-zA-Z]*[\s\S]*?(?:\/>|<\/[A-Z][a-zA-Z]*>)/g, ''); + content = content.replace( + /!\[([^\]]*)\]\((\.\/)?img\/([^)]+)\)/g, + (match, alt, relPrefix, filename) => `![${alt}](/docs/${fileDir}img/${filename})` + ); + content = content.replace(/^\s*\n/, ''); + return content; +} + +function findDocFiles(dir, fileList = [], baseDir = dir) { + const files = fs.readdirSync(dir); + files.forEach((file) => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + if (stat.isDirectory()) { + findDocFiles(filePath, fileList, baseDir); + } else if (file.endsWith('.md') || file.endsWith('.mdx')) { + const relativePath = path.relative(baseDir, filePath); + fileList.push(relativePath); + } + }); + return fileList; +} + +async function copyImageDirectories(docsDir, buildDir) { + const imageDirs = []; + function findImgDirs(dir, baseDir = dir) { + const files = fs.readdirSync(dir); + files.forEach((file) => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + if (stat.isDirectory()) { + if (file === 'img') { + const relativePath = path.relative(baseDir, dir); + imageDirs.push({ source: filePath, relativePath }); + } else { + findImgDirs(filePath, baseDir); + } + } + }); + } + findImgDirs(docsDir); + let copiedCount = 0; + for (const { source, relativePath } of imageDirs) { + const destination = path.join(buildDir, relativePath, 'img'); + try { + await fs.copy(source, destination); + const imageCount = fs.readdirSync(source).length; + console.log(` ✓ Copied: ${relativePath}/img/ (${imageCount} images)`); + copiedCount++; + } catch (error) { + console.error(` ✗ Failed to copy ${relativePath}/img/:`, error.message); + } + } + return copiedCount; +} + +export default function markdownSourceDocsPlugin(context, options) { + const base = basePlugin(context, options); + return { + ...base, + async postBuild({ outDir }) { + const docsDir = path.join(context.siteDir, 'docs'); + const buildDocsDir = path.join(outDir, 'docs'); + console.log('[markdown-source-plugin] Copying markdown source files (.md and .mdx)...'); + const docFiles = findDocFiles(docsDir); + let copiedCount = 0; + for (const docFile of docFiles) { + const sourcePath = path.join(docsDir, docFile); + const destPath = path.join(buildDocsDir, docFile.replace(/\.mdx?$/, '.md')); + try { + await fs.ensureDir(path.dirname(destPath)); + const content = await fs.readFile(sourcePath, 'utf8'); + const cleanedContent = cleanMarkdownForDisplay(content, docFile); + await fs.writeFile(destPath, cleanedContent, 'utf8'); + copiedCount++; + console.log(` ✓ Processed: ${docFile} → docs/${path.relative(buildDocsDir, destPath)}`); + } catch (error) { + console.error(` ✗ Failed to process ${docFile}:`, error.message); + } + } + console.log(`[markdown-source-plugin] Successfully copied ${copiedCount} doc files`); + console.log('[markdown-source-plugin] Copying image directories...'); + const imgDirCount = await copyImageDirectories(docsDir, buildDocsDir); + console.log(`[markdown-source-plugin] Successfully copied ${imgDirCount} image directories`); + }, + }; +} diff --git a/website/src/components/docs/MarkdownActionsDropdown/index.js b/website/src/components/docs/MarkdownActionsDropdown/index.js new file mode 100644 index 00000000..a2483eeb --- /dev/null +++ b/website/src/components/docs/MarkdownActionsDropdown/index.js @@ -0,0 +1,127 @@ +/** + * Markdown actions dropdown (view/copy as .md). + * Local override of docusaurus-markdown-source-plugin's dropdown so that + * category index pages (e.g. /docs/foundations/) resolve to index.md instead + * of intro.md (which only exists for the root /docs/ page). + */ +import React, { useState, useRef, useEffect } from 'react'; + +function getMarkdownUrl(currentPath) { + if (!currentPath.startsWith('/docs/')) return null; + if (currentPath.endsWith('/')) { + // Root docs index is intro; all other category indexes are index.md + return currentPath === '/docs/' + ? `${currentPath}intro.md` + : `${currentPath}index.md`; + } + return `${currentPath}.md`; +} + +export default function MarkdownActionsDropdown() { + const [copied, setCopied] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const currentPath = typeof window !== 'undefined' ? window.location.pathname : ''; + const isDocsPage = currentPath.startsWith('/docs/'); + const markdownUrl = getMarkdownUrl(currentPath); + + useEffect(() => { + if (!isOpen) return; + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [isOpen]); + + if (!isDocsPage || !markdownUrl) { + return null; + } + + const handleOpenMarkdown = () => { + window.open(markdownUrl, '_blank'); + setIsOpen(false); + }; + + const handleCopyMarkdown = async () => { + try { + const response = await fetch(markdownUrl); + if (!response.ok) { + throw new Error('Failed to fetch markdown'); + } + const markdown = await response.text(); + await navigator.clipboard.writeText(markdown); + + setCopied(true); + setTimeout(() => { + setCopied(false); + setIsOpen(false); + }, 2000); + } catch (error) { + console.error('Failed to copy markdown:', error); + alert('Failed to copy markdown. Please try again.'); + } + }; + + return ( +
+ + +
    +
  • + +
  • +
  • + +
  • +
+
+ ); +} diff --git a/website/src/css/breadcrumbs.css b/website/src/css/breadcrumbs.css index 3ba0262a..77176bc0 100644 --- a/website/src/css/breadcrumbs.css +++ b/website/src/css/breadcrumbs.css @@ -3,6 +3,13 @@ * Breadcrumb navigation styling */ +/* Remove bottom margin from breadcrumbs nav (override theme's .breadcrumbsContainer) */ +.theme-doc-breadcrumbs, +nav[aria-label="Breadcrumbs"], +[class*="breadcrumbsContainer"] { + margin-bottom: 0; +} + /* Light mode breadcrumbs */ .breadcrumbs__link { color: #64748b; diff --git a/website/src/css/custom.css b/website/src/css/custom.css index b2a417e4..5190121c 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -87,7 +87,8 @@ /* Import responsive breakpoints */ @import url('./responsive.css'); - +/* Import markdown source plugin dropdown styles */ +@import url('./markdown-source-plugin.css'); /* Make accordion body white in both light and dark mode */ [class^="accordionContent_"] { diff --git a/website/src/css/markdown-source-plugin.css b/website/src/css/markdown-source-plugin.css new file mode 100644 index 00000000..d2ccdde3 --- /dev/null +++ b/website/src/css/markdown-source-plugin.css @@ -0,0 +1,125 @@ +/** + * Markdown Actions Dropdown Styles + * For docusaurus-markdown-source-plugin + * @see https://github.com/FlyNumber/markdown_docusaurus_plugin + */ + +/* Row: breadcrumbs (left) + markdown dropdown (right) */ +.markdown-actions-breadcrumbs-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; + width: 100%; +} + +.markdown-actions-breadcrumbs-row nav { + flex: 1 1 auto; + min-width: 0; +} + +/* Container for the markdown actions dropdown (right side of row) */ +.markdown-actions-breadcrumbs-row .markdown-actions-container { + flex-shrink: 0; + position: relative; +} + +/* Style the dropdown button to match the active breadcrumb (light and dark mode) */ +.markdown-actions-breadcrumbs-row .markdown-actions-container .button { + display: inline-flex; + align-items: center; + background: var(--ifm-breadcrumb-item-background-active); + color: var(--ifm-breadcrumb-color-active); + border: none; + border-radius: var(--ifm-breadcrumb-border-radius); + padding: calc(var(--ifm-breadcrumb-padding-vertical) * var(--ifm-breadcrumb-size-multiplier)) + calc(var(--ifm-breadcrumb-padding-horizontal) * var(--ifm-breadcrumb-size-multiplier)); + font-weight: inherit; + box-shadow: none; +} + +.markdown-actions-breadcrumbs-row .markdown-actions-container .button:hover { + background: var(--ifm-breadcrumb-item-background-active); + color: var(--ifm-breadcrumb-color-active); + opacity: 0.9; +} + +.markdown-actions-breadcrumbs-row .markdown-actions-container .button[aria-expanded='true'] { + background: var(--ifm-breadcrumb-item-background-active); + color: var(--ifm-breadcrumb-color-active); +} + +/* Match project's dark mode active breadcrumb color (breadcrumbs.css) */ +[data-theme='dark'] .markdown-actions-breadcrumbs-row .markdown-actions-container .button { + color: #d0d0d0; +} + +[data-theme='dark'] .markdown-actions-breadcrumbs-row .markdown-actions-container .button:hover, +[data-theme='dark'] .markdown-actions-breadcrumbs-row .markdown-actions-container .button[aria-expanded='true'] { + color: #d0d0d0; +} + +/* Ensure dropdown wrapper has proper positioning */ +.markdown-actions-container .dropdown { + position: relative; +} + +/* Base dropdown menu styles */ +.markdown-actions-container .dropdown__menu { + z-index: 1000; + min-width: 220px; + right: auto; + left: 0; +} + +/* Add hover effect for dropdown items */ +.dropdown__link:hover { + background-color: var(--ifm-hover-overlay); +} + +/* Responsive adjustments for mobile */ +@media (max-width: 768px) { + .markdown-actions-breadcrumbs-row { + margin-bottom: 0.75rem; + } + + .markdown-actions-breadcrumbs-row .markdown-actions-container { + margin-right: 0; + } + + .markdown-actions-breadcrumbs-row .markdown-actions-container .button { + padding: 0.35rem 0.7rem; + } + + /* Right-align menu on mobile to prevent cutoff */ + .markdown-actions-container .dropdown__menu { + right: 0; + left: auto; + min-width: min(220px, calc(100vw - 2rem)); + max-width: calc(100vw - 2rem); + padding-bottom: 0.75rem; + } +} + +/* RTL language support */ +[dir="rtl"] .markdown-actions-breadcrumbs-row { + flex-direction: row-reverse; +} + +[dir="rtl"] .markdown-actions-breadcrumbs-row nav { + text-align: right; +} + +[dir="rtl"] .markdown-actions-container .dropdown__menu { + right: auto; + left: 0; +} + +@media (max-width: 768px) { + [dir="rtl"] .markdown-actions-container .dropdown__menu { + left: 0; + right: auto; + } +} diff --git a/website/src/hooks/useInjectMarkdownActionsDropdown.js b/website/src/hooks/useInjectMarkdownActionsDropdown.js new file mode 100644 index 00000000..655cd00b --- /dev/null +++ b/website/src/hooks/useInjectMarkdownActionsDropdown.js @@ -0,0 +1,42 @@ +/** + * Injects the markdown actions dropdown on doc pages: same row as breadcrumbs + * (breadcrumbs left, dropdown right). Runs at 0ms, 100ms, 300ms to handle + * async DOM from Docusaurus. + */ +import React, { useEffect } from 'react'; +import { useLocation } from '@docusaurus/router'; +import { createRoot } from 'react-dom/client'; +import MarkdownActionsDropdown from '@site/src/components/docs/MarkdownActionsDropdown'; + +export default function useInjectMarkdownActionsDropdown() { + const { pathname } = useLocation(); + + useEffect(() => { + const injectDropdown = () => { + if (!pathname.startsWith('/docs/')) return; + const article = document.querySelector('article'); + const breadcrumbsNav = article?.querySelector( + 'nav.theme-doc-breadcrumbs, nav[aria-label="Breadcrumbs"]' + ); + if (!article || !breadcrumbsNav) return; + if (document.querySelector('.markdown-actions-breadcrumbs-row')) return; + + const row = document.createElement('div'); + row.className = 'markdown-actions-breadcrumbs-row'; + article.insertBefore(row, article.firstChild); + row.appendChild(breadcrumbsNav); + + const container = document.createElement('div'); + container.className = 'markdown-actions-container'; + row.appendChild(container); + + const root = createRoot(container); + root.render(); + }; + + const timeouts = [0, 100, 300].map((delay) => + setTimeout(injectDropdown, delay) + ); + return () => timeouts.forEach(clearTimeout); + }, [pathname]); +} diff --git a/website/src/theme/Root.js b/website/src/theme/Root.js index 15af11bf..67f64354 100644 --- a/website/src/theme/Root.js +++ b/website/src/theme/Root.js @@ -2,12 +2,14 @@ * Root component wrapper * Adds global enhancements and effects */ - import React from 'react'; import { Toaster } from 'react-hot-toast'; import NavbarEnhancements from '@site/src/components/navigation/NavbarEnhancements'; +import useInjectMarkdownActionsDropdown from '@site/src/hooks/useInjectMarkdownActionsDropdown'; export default function Root({children}) { + useInjectMarkdownActionsDropdown(); + return ( <> From 64564a9b9f0cbdd40ecb7d2a7182025b783e2f26 Mon Sep 17 00:00:00 2001 From: maxnorm Date: Sat, 21 Feb 2026 17:03:10 -0500 Subject: [PATCH 2/5] adjust diamond animation facet badge --- .../components/DiamondScene/useFacetBadges.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/website/src/components/DiamondScene/useFacetBadges.js b/website/src/components/DiamondScene/useFacetBadges.js index b1049498..d2cbe387 100644 --- a/website/src/components/DiamondScene/useFacetBadges.js +++ b/website/src/components/DiamondScene/useFacetBadges.js @@ -2,18 +2,18 @@ import { useState, useCallback } from 'react'; // A list of realistic facet names related to EIP-2535 Diamond Standard const FACET_NAMES = [ - "DiamondCutFacet", - "DiamondLoupeFacet", + "DiamondUpgradeFacet", + "DiamondInspectFacet", "OwnerFacet", "AccessControlFacet", - "ERC20Facet", - "ERC721Facet", + "ERC20DataFacet", + "ERC20MetadataFacet", + "ERC20MintFacet", + "ERC721DataFacet", + "ERC721MetadataFacet", "ERC721EnumerableFacet", - "ERC1155Facet", - "RoyaltyFacet", "ERC165Facet", - "AccessControlPausableFacet", - "AccessControlTemporalFacet" + "RoyaltyFacet", ]; export function useFacetBadges() { From e3c4e2057a620d64aeaf8ddfb101dfc7b97a4f64 Mon Sep 17 00:00:00 2001 From: maxnorm Date: Sat, 21 Feb 2026 17:03:34 -0500 Subject: [PATCH 3/5] fix doc ToC --- website/src/css/blog-sidebar.css | 11 ++++------- website/src/css/documentation.css | 3 +-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/website/src/css/blog-sidebar.css b/website/src/css/blog-sidebar.css index 78f065be..2a9ce1c5 100644 --- a/website/src/css/blog-sidebar.css +++ b/website/src/css/blog-sidebar.css @@ -6,17 +6,15 @@ /* Target blog sidebar with more specific selectors */ .theme-blog-wrapper .col--3, .theme-blog-wrapper [class*="col--3"], -[class*="blog"] [class*="col--3"], -.col--3:has(strong) { +[class*="blog"] [class*="col--3"] { position: relative; padding: 1.5rem 0 1.5rem 2rem !important; } -/* Timeline vertical line */ +/* Timeline vertical line (blog sidebar only) */ .theme-blog-wrapper .col--3::before, .theme-blog-wrapper [class*="col--3"]::before, -[class*="blog"] [class*="col--3"]::before, -.col--3:has(strong)::before { +[class*="blog"] [class*="col--3"]::before { content: ''; position: absolute; left: 0.625rem; @@ -33,8 +31,7 @@ [data-theme='dark'] .theme-blog-wrapper .col--3::before, [data-theme='dark'] .theme-blog-wrapper [class*="col--3"]::before, -[data-theme='dark'] [class*="blog"] [class*="col--3"]::before, -[data-theme='dark'] .col--3:has(strong)::before { +[data-theme='dark'] [class*="blog"] [class*="col--3"]::before { background: linear-gradient(180deg, #60a5fa 0%, #93c5fd 10%, diff --git a/website/src/css/documentation.css b/website/src/css/documentation.css index 831d401e..573a9616 100644 --- a/website/src/css/documentation.css +++ b/website/src/css/documentation.css @@ -278,11 +278,10 @@ .table-of-contents__link { display: block; - padding: 0.375rem 0; + padding: 0 0 0 0.75rem; color: var(--ifm-color-emphasis-700); transition: all 0.2s ease; border-left: 2px solid transparent; - padding-left: 0.75rem; margin-left: -0.75rem; } From 31d67dc398b71c565b8ee50afbb69c055a65602a Mon Sep 17 00:00:00 2001 From: maxnorm Date: Sat, 21 Feb 2026 17:10:06 -0500 Subject: [PATCH 4/5] add legacy peer deps --- website/.npmrc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 website/.npmrc diff --git a/website/.npmrc b/website/.npmrc new file mode 100644 index 00000000..9fc4f0a6 --- /dev/null +++ b/website/.npmrc @@ -0,0 +1,3 @@ +# Required until docusaurus-markdown-source-plugin declares React 19 in peerDependencies. +# Project uses React 19; the plugin only lists react@^18.0.0. +legacy-peer-deps=true From cec7f021a7d75a6c33dbe43c48cd2bcf9fd39f9c Mon Sep 17 00:00:00 2001 From: maxnorm Date: Sat, 21 Feb 2026 17:34:40 -0500 Subject: [PATCH 5/5] md action style, image support, fix react md rendering --- website/plugins/markdown-source-docs.js | 54 ++++++++++++++++++- .../docs/MarkdownActionsDropdown/index.js | 4 +- .../theme/SvgThemeRenderer/index.js | 5 +- website/src/css/markdown-source-plugin.css | 4 +- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/website/plugins/markdown-source-docs.js b/website/plugins/markdown-source-docs.js index 2374ccca..eb4d119f 100644 --- a/website/plugins/markdown-source-docs.js +++ b/website/plugins/markdown-source-docs.js @@ -44,6 +44,46 @@ function convertDetailsToMarkdown(content) { }); } +/** + * Convert DocCardGrid + DocCard blocks to markdown list of links so the + * generated .md is readable and we avoid leaving JSX fragments. + */ +function convertDocCardGridToMarkdown(content) { + return content.replace( + /]*>([\s\S]*?)<\/DocCardGrid>/g, + (_, inner) => { + // Remove icon={...} so nested JSX doesn't break the DocCard match + const withoutIcon = inner.replace(/icon=\{[^}]*\}/g, 'icon=""'); + // DocCard attributes can be in any order; match full tag (no nested /> now) + const cardPattern = //g; + const items = []; + let m; + while ((m = cardPattern.exec(withoutIcon)) !== null) { + const attrs = m[1]; + const title = attrs.match(/title="([^"]*)"/)?.[1] ?? ''; + const description = attrs.match(/description="([^"]*)"/)?.[1] ?? ''; + const href = attrs.match(/href="([^"]*)"/)?.[1] ?? ''; + if (title && href) items.push(`- [${title}](${href}): ${description}`); + } + return items.length ? items.join('\n') + '\n' : ''; + } + ); +} + +/** + * Remove JSX tags (including nested ones) by repeatedly stripping the + * outermost component until no match. Avoids leaving fragments like " />" or "}". + */ +function stripJsxComponents(content) { + const componentTag = /<([A-Z][a-zA-Z]*)(?:\s[^>]*?)?(?:\/>|>[\s\S]*?<\/\1>)/g; + let prev; + do { + prev = content; + content = content.replace(componentTag, ''); + } while (content !== prev); + return content; +} + function cleanMarkdownForDisplay(content, filepath) { const fileDir = filepath.replace(/[^/]*$/, ''); content = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ''); @@ -64,9 +104,21 @@ function cleanMarkdownForDisplay(content, filepath) { '' ); content = content.replace(/[\s\S]*?<\/Head>/g, ''); + // Convert SvgThemeRenderer to markdown image so diagram appears in source .md view + content = content.replace(//g, (_, attrs) => { + const lightMatch = attrs.match(/lightSrc=\{?["']([^"']+)["']\}?/); + const darkMatch = attrs.match(/darkSrc=\{?["']([^"']+)["']\}?/); + const altMatch = attrs.match(/alt=["']([^"']*)["']/); + const src = (lightMatch && lightMatch[1]) || (darkMatch && darkMatch[1]); + const alt = (altMatch && altMatch[1]) || 'Diagram'; + return src ? `![${alt}](${src})\n\n` : ''; + }); content = convertTabsToMarkdown(content); content = convertDetailsToMarkdown(content); - content = content.replace(/<[A-Z][a-zA-Z]*[\s\S]*?(?:\/>|<\/[A-Z][a-zA-Z]*>)/g, ''); + // Convert DocCardGrid + DocCard to markdown links first so .md is readable + content = convertDocCardGridToMarkdown(content); + // Strip any remaining JSX (nested-aware) so we don't leave fragments + content = stripJsxComponents(content); content = content.replace( /!\[([^\]]*)\]\((\.\/)?img\/([^)]+)\)/g, (match, alt, relPrefix, filename) => `![${alt}](/docs/${fileDir}img/${filename})` diff --git a/website/src/components/docs/MarkdownActionsDropdown/index.js b/website/src/components/docs/MarkdownActionsDropdown/index.js index a2483eeb..802acb3b 100644 --- a/website/src/components/docs/MarkdownActionsDropdown/index.js +++ b/website/src/components/docs/MarkdownActionsDropdown/index.js @@ -93,7 +93,7 @@ export default function MarkdownActionsDropdown() { - View as Markdown + View
  • @@ -116,7 +116,7 @@ export default function MarkdownActionsDropdown() { - Copy as Markdown + Copy )} diff --git a/website/src/components/theme/SvgThemeRenderer/index.js b/website/src/components/theme/SvgThemeRenderer/index.js index 92db5ea4..8aa54f05 100644 --- a/website/src/components/theme/SvgThemeRenderer/index.js +++ b/website/src/components/theme/SvgThemeRenderer/index.js @@ -1,6 +1,7 @@ import React from 'react'; import clsx from 'clsx'; import {useColorMode} from '@docusaurus/theme-common'; +import useBaseUrl from '@docusaurus/useBaseUrl'; /** * SvgThemeRenderer Component @@ -30,9 +31,11 @@ export default function SvgThemeRenderer({ return null; } + const resolvedSrc = activeSrc.startsWith('/') ? useBaseUrl(activeSrc) : activeSrc; + return (