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 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..eb4d119f --- /dev/null +++ b/website/plugins/markdown-source-docs.js @@ -0,0 +1,208 @@ +/** + * 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}`; + }); +} + +/** + * 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/, ''); + 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, ''); + // 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); + // 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})` + ); + 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/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() { diff --git a/website/src/components/docs/MarkdownActionsDropdown/index.js b/website/src/components/docs/MarkdownActionsDropdown/index.js new file mode 100644 index 00000000..802acb3b --- /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/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 ( {alt} { + 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 ( <>