|
| 1 | +import {visit} from 'unist-util-visit'; |
| 2 | + |
| 3 | +/** |
| 4 | + * A remark plugin that rewrites cross-documentation-root links in markdown files. |
| 5 | + * |
| 6 | + * In the source markdown, authors write relative links across doc roots like: |
| 7 | + * ../_documentation/3-architecture/10-dot Env.md#ANCHOR (from _changelog) |
| 8 | + * ../_changelog/2.4.0.md (from _documentation) |
| 9 | + * |
| 10 | + * These links work on GitHub but break in Docusaurus because `_changelog` and `_documentation` |
| 11 | + * are separate docs plugin instances with different base paths. |
| 12 | + * |
| 13 | + * This plugin rewrites those links to absolute Docusaurus URLs by: |
| 14 | + * 1. Detecting `../_documentation/` or `../_changelog/` prefixes |
| 15 | + * 2. Removing the `.md` extension |
| 16 | + * 3. Stripping Docusaurus-style number prefixes (e.g. `3-` from dirs, `10-` from files) |
| 17 | + * 4. Prepending the target routeBasePath (`/` for docs, `/changelog` for changelog) |
| 18 | + * |
| 19 | + * The result is a valid absolute URL like `/architecture/dot%20Env#anchor` |
| 20 | + * or `/changelog/2.4.0` |
| 21 | + */ |
| 22 | +function remarkRewriteCrossDocLinks() { |
| 23 | + /** |
| 24 | + * Strips a Docusaurus-style number prefix from a single path segment. |
| 25 | + * E.g. "3-architecture" -> "architecture", "10-dot Env" -> "dot Env" |
| 26 | + * But preserves prefixes like "10.1-Model Config" (version-like, ignored by Docusaurus). |
| 27 | + */ |
| 28 | + function stripNumberPrefix(segment) { |
| 29 | + // Docusaurus ignores prefixes that look like versions: \d+[-_.]\d+ |
| 30 | + if (/^\d+[-_.]\d+/.test(segment)) { |
| 31 | + return segment; |
| 32 | + } |
| 33 | + // Strip pattern: leading digits followed by separator(s) |
| 34 | + return segment.replace(/^\d+\s*[-_.]+\s*/, ''); |
| 35 | + } |
| 36 | + |
| 37 | + /** |
| 38 | + * Maps a `../<sourceDir>/` prefix to the corresponding Docusaurus routeBasePath. |
| 39 | + */ |
| 40 | + const prefixToRouteBase = { |
| 41 | + '_documentation': '', // routeBasePath: '/' |
| 42 | + '_changelog': 'changelog' // routeBasePath: 'changelog' |
| 43 | + }; |
| 44 | + |
| 45 | + const prefixPattern = new RegExp( |
| 46 | + '^\\.\\.\/(' + Object.keys(prefixToRouteBase).join('|') + ')\/(.*)' |
| 47 | + ); |
| 48 | + |
| 49 | + return (tree) => { |
| 50 | + visit(tree, 'link', (node) => { |
| 51 | + if (!node.url) return; |
| 52 | + |
| 53 | + const match = node.url.match(prefixPattern); |
| 54 | + if (!match) return; |
| 55 | + |
| 56 | + const sourceDir = match[1]; |
| 57 | + const routeBase = prefixToRouteBase[sourceDir]; |
| 58 | + let targetPath = match[2]; |
| 59 | + |
| 60 | + // Separate anchor from path |
| 61 | + let anchor = ''; |
| 62 | + const hashIndex = targetPath.indexOf('#'); |
| 63 | + if (hashIndex !== -1) { |
| 64 | + anchor = targetPath.substring(hashIndex).toLowerCase(); |
| 65 | + targetPath = targetPath.substring(0, hashIndex); |
| 66 | + } |
| 67 | + |
| 68 | + // Remove .md extension |
| 69 | + targetPath = targetPath.replace(/\.md$/, ''); |
| 70 | + |
| 71 | + // Decode URL encoding (e.g. %20 -> space) so we can process segments |
| 72 | + targetPath = decodeURIComponent(targetPath); |
| 73 | + |
| 74 | + // Strip number prefixes from each path segment |
| 75 | + const segments = targetPath.split('/').map(stripNumberPrefix); |
| 76 | + |
| 77 | + // Re-encode spaces and rebuild path |
| 78 | + const rewrittenPath = '/' + [routeBase, ...segments] |
| 79 | + .filter(Boolean) |
| 80 | + .map(s => encodeURIComponent(s)) |
| 81 | + .join('/'); |
| 82 | + |
| 83 | + node.url = rewrittenPath + anchor; |
| 84 | + }); |
| 85 | + }; |
| 86 | +} |
| 87 | + |
| 88 | +export default remarkRewriteCrossDocLinks; |
0 commit comments