From b1e9b37cc59e09f7916214a28f9f871f03968106 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 23 May 2024 19:00:00 +0200 Subject: [PATCH 01/11] POC of resolving Markdown links with Remark --- packages/docusaurus-mdx-loader/src/loader.ts | 2 + .../docusaurus-mdx-loader/src/processor.ts | 5 + .../remark/linkify/__tests__/index.test.ts | 67 ++++++++++ .../src/remark/linkify/index.ts | 120 ++++++++++++++++++ .../src/index.ts | 33 ++++- .../src/markdown/index.ts | 7 +- .../src/markdown/linkify.ts | 6 +- .../docusaurus-utils/src/markdownLinks.ts | 1 + 8 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts create mode 100644 packages/docusaurus-mdx-loader/src/remark/linkify/index.ts diff --git a/packages/docusaurus-mdx-loader/src/loader.ts b/packages/docusaurus-mdx-loader/src/loader.ts index bde02542beb3..00594108d1ed 100644 --- a/packages/docusaurus-mdx-loader/src/loader.ts +++ b/packages/docusaurus-mdx-loader/src/loader.ts @@ -17,6 +17,7 @@ import stringifyObject from 'stringify-object'; import preprocessor from './preprocessor'; import {validateMDXFrontMatter} from './frontMatter'; import {createProcessorCached} from './processor'; +import type {ResolveMarkdownLink} from './remark/linkify'; import type {MDXOptions} from './processor'; import type {MarkdownConfig} from '@docusaurus/types'; @@ -45,6 +46,7 @@ export type Options = Partial & { frontMatter: {[key: string]: unknown}; metadata: {[key: string]: unknown}; }) => {[key: string]: unknown}; + resolveMarkdownLink?: ResolveMarkdownLink; }; /** diff --git a/packages/docusaurus-mdx-loader/src/processor.ts b/packages/docusaurus-mdx-loader/src/processor.ts index 778bfce1ba47..90c2f17248ad 100644 --- a/packages/docusaurus-mdx-loader/src/processor.ts +++ b/packages/docusaurus-mdx-loader/src/processor.ts @@ -10,6 +10,7 @@ import contentTitle from './remark/contentTitle'; import toc from './remark/toc'; import transformImage from './remark/transformImage'; import transformLinks from './remark/transformLinks'; +import linkify from './remark/linkify'; import details from './remark/details'; import head from './remark/head'; import mermaid from './remark/mermaid'; @@ -120,6 +121,10 @@ async function createProcessorFactory() { siteDir: options.siteDir, }, ], + // TODO merge this with transformLinks? + options.resolveMarkdownLink + ? [linkify, {resolveMarkdownLink: options.resolveMarkdownLink}] + : undefined, [ transformLinks, { diff --git a/packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts new file mode 100644 index 000000000000..30e0c3531163 --- /dev/null +++ b/packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts @@ -0,0 +1,67 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import remark2rehype from 'remark-rehype'; +import stringify from 'rehype-stringify'; +import mermaid from '..'; + +async function process(content: string) { + const {remark} = await import('remark'); + + // const {default: mdx} = await import('remark-mdx'); + // const result = await remark().use(mermaid).use(mdx).process(content); + + const result = await remark() + .use(mermaid) + .use(remark2rehype) + .use(stringify) + .process(content); + + return result.value; +} + +describe('mermaid remark plugin', () => { + it("does nothing if there's no mermaid code block", async () => { + const result = await process( + `# Heading 1 + +No Mermaid diagram :( + +\`\`\`js +this is not mermaid +\`\`\` +`, + ); + + expect(result).toMatchInlineSnapshot(` + "

Heading 1

+

No Mermaid diagram :(

+
this is not mermaid
+      
" + `); + }); + + it('works for basic mermaid code blocks', async () => { + const result = await process(`# Heading 1 + +\`\`\`mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +\`\`\``); + expect(result).toMatchInlineSnapshot(` + "

Heading 1

+ " + `); + }); +}); diff --git a/packages/docusaurus-mdx-loader/src/remark/linkify/index.ts b/packages/docusaurus-mdx-loader/src/remark/linkify/index.ts new file mode 100644 index 000000000000..2907dd3dd3b9 --- /dev/null +++ b/packages/docusaurus-mdx-loader/src/remark/linkify/index.ts @@ -0,0 +1,120 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import {stringifyContent} from '../utils'; + +// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721 +import type {Transformer} from 'unified'; +import type {Link} from 'mdast'; + +export type ResolveMarkdownLink = ({ + link, + filePath, +}: { + link: string; + filePath: string; +}) => string | undefined; + +// TODO: this plugin shouldn't be in the core MDX loader +// After we allow plugins to provide Remark/Rehype plugins (see +// https://github.com/facebook/docusaurus/issues/6370), this should be provided +// by theme-mermaid itself +export interface PluginOptions { + resolveMarkdownLink: ResolveMarkdownLink; +} + +// TODO as of April 2023, no way to import/re-export this ESM type easily :/ +// TODO upgrade to TS 5.3 +// See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391 +// import type {Plugin} from 'unified'; +type Plugin = any; // TODO fix this asap + +const LINK_PATTERN = /\.mdx?$/g; + +function ignoreLink(link: Link) { + // Probably not 100% accurate, but this is faster than proper url parsing + // Historical code, nobody complained about it so far + const hasProtocol = + link.url.toLowerCase().startsWith('http://') || + link.url.toLowerCase().startsWith('https://'); + return hasProtocol || !LINK_PATTERN.test(link.url); +} + +export type BrokenMarkdownLink = { + /** Absolute path to the file containing this link. */ + filePath: string; + /** + * The content of the link, like `"./brokenFile.md"` + */ + link: Link; +}; + +/** + * A remark plugin to extract the h1 heading found in Markdown files + * This is exposed as "data.contentTitle" to the processed vfile + * Also gives the ability to strip that content title (used for the blog plugin) + */ +const plugin: Plugin = function plugin(options: PluginOptions): Transformer { + const {resolveMarkdownLink} = options; + return async (root, file) => { + const {toString} = await import('mdast-util-to-string'); + + const {visit} = await import('unist-util-visit'); + + const brokenMarkdownLinks: BrokenMarkdownLink[] = []; + + visit(root, 'link', (link: Link) => { + if (ignoreLink(link)) { + return; + } + const permalink = resolveMarkdownLink({ + link: link.url, + filePath: file.path, + }); + + /* + const sourcesToTry: string[] = []; + // ./file.md and ../file.md are always relative to the current file + if (!mdLink.startsWith('./') && !mdLink.startsWith('../')) { + sourcesToTry.push(...getContentPathList(contentPaths), siteDir); + } + // /file.md is always relative to the content path + if (!mdLink.startsWith('/')) { + sourcesToTry.push(path.dirname(filePath)); + } + + const aliasedSourceMatch = sourcesToTry + .map((p) => path.join(p, decodeURIComponent(mdLink))) + .map((source) => aliasedSitePath(source, siteDir)) + .find((source) => sourceToPermalink[source]); + + const permalink: string | undefined = aliasedSourceMatch + ? sourceToPermalink[aliasedSourceMatch] + : undefined; + + */ + if (permalink) { + console.log(`✅ Markdown link resolved: ${link.url} => ${permalink}`); + link.url = permalink; + } else { + const linkContent = stringifyContent(link, toString); + console.log(`❌ Markdown link broken: [${linkContent}](${link.url})`); + brokenMarkdownLinks.push({ + filePath: file.path, + link, + }); + } + }); + + if (brokenMarkdownLinks.length > 0) { + console.log( + `❌ ${brokenMarkdownLinks.length} broken Markdown links for ${file.path}\n`, + ); + } + }; +}; + +export default plugin; diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 18e43a7f6540..87f1557eac69 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -38,6 +38,8 @@ import { } from './translations'; import {createAllRoutes} from './routes'; import {createSidebarsUtils} from './sidebars/utils'; +import {getVersion} from './markdown/linkify'; +import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader'; import type { PluginOptions, @@ -251,6 +253,8 @@ export default async function pluginContentDocs( beforeDefaultRemarkPlugins, } = options; + // TODO this does not re-run when content gets updated in dev! + // it's probably better to restore a mutable cache in the plugin function getSourceToPermalink(): SourceToPermalink { const allDocs = content.loadedVersions.flatMap((v) => v.docs); return Object.fromEntries( @@ -310,7 +314,34 @@ export default async function pluginContentDocs( image: frontMatter.image, }), markdownConfig: siteConfig.markdown, - }, + resolveMarkdownLink: ({link, filePath}) => { + // TODO temporary, POC with historical code + const mdLink = link; + const {sourceToPermalink} = docsMarkdownOptions; + const version = getVersion(filePath, docsMarkdownOptions); + + const sourcesToTry: string[] = []; + // ./file.md and ../file.md are always relative to the current file + if (!mdLink.startsWith('./') && !mdLink.startsWith('../')) { + sourcesToTry.push(...getContentPathList(version), siteDir); + } + // /file.md is always relative to the content path + if (!mdLink.startsWith('/')) { + sourcesToTry.push(path.dirname(filePath)); + } + + const aliasedSourceMatch = sourcesToTry + .map((p) => path.join(p, decodeURIComponent(mdLink))) + .map((source) => aliasedSitePath(source, siteDir)) + .find((source) => sourceToPermalink[source]); + + const permalink: string | undefined = aliasedSourceMatch + ? sourceToPermalink[aliasedSourceMatch] + : undefined; + + return permalink; + }, + } satisfies MDXLoaderOptions, }, { loader: path.resolve(__dirname, './markdown/index.js'), diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/index.ts b/packages/docusaurus-plugin-content-docs/src/markdown/index.ts index 9d846ef8d5c5..caf1fed47bbd 100644 --- a/packages/docusaurus-plugin-content-docs/src/markdown/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/markdown/index.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {linkify} from './linkify'; +// import {linkify} from './linkify'; import type {DocsMarkdownOption} from '../types'; import type {LoaderContext} from 'webpack'; @@ -15,6 +15,7 @@ export default function markdownLoader( ): void { const fileString = source; const callback = this.async(); - const options = this.getOptions(); - return callback(null, linkify(fileString, this.resourcePath, options)); + // const options = this.getOptions(); + // return callback(null, linkify(fileString, this.resourcePath, options)); + return callback(null, fileString); } diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts b/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts index ae651cb316a0..cb5713ab47d2 100644 --- a/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts +++ b/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts @@ -7,8 +7,12 @@ import {replaceMarkdownLinks, getContentPathList} from '@docusaurus/utils'; import type {DocsMarkdownOption} from '../types'; +import type {VersionMetadata} from '@docusaurus/plugin-content-docs'; -function getVersion(filePath: string, options: DocsMarkdownOption) { +export function getVersion( + filePath: string, + options: DocsMarkdownOption, +): VersionMetadata { const versionFound = options.versionsMetadata.find((version) => getContentPathList(version).some((docsDirPath) => filePath.startsWith(docsDirPath), diff --git a/packages/docusaurus-utils/src/markdownLinks.ts b/packages/docusaurus-utils/src/markdownLinks.ts index 13afca33905e..6c26b12b6cbc 100644 --- a/packages/docusaurus-utils/src/markdownLinks.ts +++ b/packages/docusaurus-utils/src/markdownLinks.ts @@ -69,6 +69,7 @@ function parseCodeFence(line: string): CodeFence | null { * `/docs/tutorials/intro.md`). Links that contain the `http(s):` or * `@site/` prefix will always be ignored. */ +// TODO remove this export function replaceMarkdownLinks({ siteDir, fileString, From 38b101adaaf2b27d5a5088f0d471c5d658b2034e Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 09:47:26 +0200 Subject: [PATCH 02/11] improve urlUtils --- .../src/__tests__/urlUtils.test.ts | 49 +++++++++++++++++++ packages/docusaurus-utils/src/urlUtils.ts | 46 +++++++++-------- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts index 35b5ac79307c..b2fab8720537 100644 --- a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts @@ -17,6 +17,8 @@ import { hasSSHProtocol, parseURLPath, serializeURLPath, + parseURLOrPath, + toURLPath, } from '../urlUtils'; describe('normalizeUrl', () => { @@ -228,6 +230,53 @@ describe('isValidPathname', () => { }); }); +describe('toURLPath', () => { + it('url', () => { + const url = new URL('https://example.com/pathname?qs#hash'); + expect(toURLPath(url)).toEqual({ + pathname: '/pathname', + search: 'qs', + hash: 'hash', + }); + }); + + it('pathname + qs', () => { + const url = parseURLOrPath('/pathname?qs'); + expect(toURLPath(url)).toEqual({ + pathname: '/pathname', + search: 'qs', + hash: undefined, + }); + }); + + it('pathname + hash', () => { + const url = parseURLOrPath('/pathname#hash'); + expect(toURLPath(url)).toEqual({ + pathname: '/pathname', + search: undefined, + hash: 'hash', + }); + }); + + it('pathname + qs + hash', () => { + const url = parseURLOrPath('/pathname?qs#hash'); + expect(toURLPath(url)).toEqual({ + pathname: '/pathname', + search: 'qs', + hash: 'hash', + }); + }); + + it('pathname + empty qs + empty hash', () => { + const url = parseURLOrPath('/pathname?#'); + expect(toURLPath(url)).toEqual({ + pathname: '/pathname', + search: '', + hash: '', + }); + }); +}); + describe('parseURLPath', () => { it('parse and resolve pathname', () => { expect(parseURLPath('')).toEqual({ diff --git a/packages/docusaurus-utils/src/urlUtils.ts b/packages/docusaurus-utils/src/urlUtils.ts index f6b2de027cbd..d8c31cffb769 100644 --- a/packages/docusaurus-utils/src/urlUtils.ts +++ b/packages/docusaurus-utils/src/urlUtils.ts @@ -164,27 +164,22 @@ export function isValidPathname(str: string): boolean { } } -export type URLPath = {pathname: string; search?: string; hash?: string}; - -// Let's name the concept of (pathname + search + hash) as URLPath -// See also https://twitter.com/kettanaito/status/1741768992866308120 -// Note: this function also resolves relative pathnames while parsing! -export function parseURLPath(urlPath: string, fromPath?: string): URLPath { - function parseURL(url: string, base?: string | URL): URL { - try { - // A possible alternative? https://github.com/unjs/ufo#url - return new URL(url, base ?? 'https://example.com'); - } catch (e) { - throw new Error( - `Can't parse URL ${url}${base ? ` with base ${base}` : ''}`, - {cause: e}, - ); - } +export function parseURLOrPath(url: string, base?: string | URL): URL { + try { + // TODO when Node supports it, use URL.parse could be faster? + // see https://kilianvalkhof.com/2024/javascript/the-problem-with-new-url-and-how-url-parse-fixes-that/ + return new URL(url, base ?? 'https://example.com'); + } catch (e) { + throw new Error( + `Can't parse URL ${url}${base ? ` with base ${base}` : ''}`, + {cause: e}, + ); } +} - const base = fromPath ? parseURL(fromPath) : undefined; - const url = parseURL(urlPath, base); +export type URLPath = {pathname: string; search?: string; hash?: string}; +export function toURLPath(url: URL): URLPath { const {pathname} = url; // Fixes annoying url.search behavior @@ -193,17 +188,17 @@ export function parseURLPath(urlPath: string, fromPath?: string): URLPath { // "?param => "param" const search = url.search ? url.search.slice(1) - : urlPath.includes('?') + : url.href.includes('?') ? '' : undefined; // Fixes annoying url.hash behavior // "" => undefined // "#" => "" - // "?param => "param" + // "#param => "param" const hash = url.hash ? url.hash.slice(1) - : urlPath.includes('#') + : url.href.includes('#') ? '' : undefined; @@ -214,6 +209,15 @@ export function parseURLPath(urlPath: string, fromPath?: string): URLPath { }; } +// Let's name the concept of (pathname + search + hash) as URLPath +// See also https://twitter.com/kettanaito/status/1741768992866308120 +// Note: this function also resolves relative pathnames while parsing! +export function parseURLPath(urlPath: string, fromPath?: string): URLPath { + const base = fromPath ? parseURLOrPath(fromPath) : undefined; + const url = parseURLOrPath(urlPath, base); + return toURLPath(url); +} + export function serializeURLPath(urlPath: URLPath): string { const search = urlPath.search === undefined ? '' : `?${urlPath.search}`; const hash = urlPath.hash === undefined ? '' : `#${urlPath.hash}`; From bb33901f4e1a2fb5478a2cb6967292e5dd56287a Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 10:37:17 +0200 Subject: [PATCH 03/11] Create parseLocalURLPath() utils method --- .../src/__tests__/urlUtils.test.ts | 109 ++++++++++++++++++ packages/docusaurus-utils/src/index.ts | 3 + packages/docusaurus-utils/src/urlUtils.ts | 56 ++++++++- 3 files changed, 165 insertions(+), 3 deletions(-) diff --git a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts index b2fab8720537..ad292eef0ef4 100644 --- a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts @@ -19,6 +19,7 @@ import { serializeURLPath, parseURLOrPath, toURLPath, + parseLocalURLPath, } from '../urlUtils'; describe('normalizeUrl', () => { @@ -277,6 +278,114 @@ describe('toURLPath', () => { }); }); +describe('parseLocalURLPath', () => { + it('returns null for non-local URLs', () => { + expect(parseLocalURLPath('https://example')).toBeNull(); + expect(parseLocalURLPath('https://example:80')).toBeNull(); + expect(parseLocalURLPath('https://example.com/xyz')).toBeNull(); + expect(parseLocalURLPath('https://example.com/xyz?qs#hash')).toBeNull(); + expect(parseLocalURLPath('https://example.com:80/xyz?qs#hash')).toBeNull(); + expect(parseLocalURLPath('https://u:p@example:80/xyz?qs#hash')).toBeNull(); + }); + + it('parses pathname', () => { + expect(parseLocalURLPath('/pathname')).toEqual({ + pathname: '/pathname', + search: undefined, + hash: undefined, + }); + expect(parseLocalURLPath('pathname.md')).toEqual({ + pathname: 'pathname.md', + search: undefined, + hash: undefined, + }); + expect(parseLocalURLPath('./pathname')).toEqual({ + pathname: './pathname', + search: undefined, + hash: undefined, + }); + expect(parseLocalURLPath('../../pathname.mdx')).toEqual({ + pathname: '../../pathname.mdx', + search: undefined, + hash: undefined, + }); + }); + + it('parses qs', () => { + expect(parseLocalURLPath('?')).toEqual({ + pathname: '', + search: '', + hash: undefined, + }); + expect(parseLocalURLPath('?qs')).toEqual({ + pathname: '', + search: 'qs', + hash: undefined, + }); + expect(parseLocalURLPath('?age=42')).toEqual({ + pathname: '', + search: 'age=42', + hash: undefined, + }); + }); + + it('parses hash', () => { + expect(parseLocalURLPath('#')).toEqual({ + pathname: '', + search: undefined, + hash: '', + }); + expect(parseLocalURLPath('#hash')).toEqual({ + pathname: '', + search: undefined, + hash: 'hash', + }); + }); + + it('parses complex local paths', () => { + expect( + parseLocalURLPath('../../great/path name/doc.mdx?age=42#hash'), + ).toEqual({ + pathname: '../../great/path name/doc.mdx', + search: 'age=42', + hash: 'hash', + }); + expect(parseLocalURLPath('my great path?=42#hash?qsInHash')).toEqual({ + pathname: 'my great path', + search: '=42', + hash: 'hash?qsInHash', + }); + expect(parseLocalURLPath('?qs1#hash1?qs2#hash2')).toEqual({ + pathname: '', + search: 'qs1', + hash: 'hash1?qs2#hash2', + }); + }); + + it('parses is isomorphic with serialize', () => { + const testLocalPath = (url: string) => { + expect(serializeURLPath(parseLocalURLPath(url)!)).toBe(url); + }; + [ + '', + 'doc', + 'doc.mdx', + './doc.mdx', + '.././doc.mdx', + '/some pathname/.././doc.mdx', + '?', + '?qs', + '#', + '#hash', + '?qs#hash', + '?qs#hash', + 'doc.mdx?qs#hash', + '/some pathname/.././doc.mdx?qs#hash', + '/some pathname/.././doc.mdx?qs#hash?qs2#hash2', + ].forEach(testLocalPath); + }); +}); + describe('parseURLPath', () => { it('parse and resolve pathname', () => { expect(parseURLPath('')).toEqual({ diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 405da5258dd4..39a4ec6e6783 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -44,6 +44,9 @@ export { isValidPathname, resolvePathname, parseURLPath, + parseLocalURLPath, + parseURLOrPath, + toURLPath, serializeURLPath, hasSSHProtocol, buildHttpsUrl, diff --git a/packages/docusaurus-utils/src/urlUtils.ts b/packages/docusaurus-utils/src/urlUtils.ts index d8c31cffb769..0a882dcdc9a6 100644 --- a/packages/docusaurus-utils/src/urlUtils.ts +++ b/packages/docusaurus-utils/src/urlUtils.ts @@ -209,15 +209,65 @@ export function toURLPath(url: URL): URLPath { }; } -// Let's name the concept of (pathname + search + hash) as URLPath -// See also https://twitter.com/kettanaito/status/1741768992866308120 -// Note: this function also resolves relative pathnames while parsing! +/** + * Let's name the concept of (pathname + search + hash) as URLPath + * See also https://twitter.com/kettanaito/status/1741768992866308120 + * Note: this function also resolves relative pathnames while parsing! + */ export function parseURLPath(urlPath: string, fromPath?: string): URLPath { const base = fromPath ? parseURLOrPath(fromPath) : undefined; const url = parseURLOrPath(urlPath, base); return toURLPath(url); } +/** + * This returns results for strings like "foo", "../foo", "./foo.mdx?qs#hash" + * Unlike "parseURLPath()" above, this will not resolve the pathnames + * Te returned pathname of "../../foo.mdx" will be "../../foo.mdx", not "/foo" + * This returns null if the url is not "local" (contains domain/protocol etc) + */ +export function parseLocalURLPath(urlPath: string): URLPath | null { + // Workaround because URL("") requires a protocol + const unspecifiedProtocol = 'unspecified:'; + + const url = parseURLOrPath(urlPath, `${unspecifiedProtocol}//`); + // Ignore links with specified protocol / host + // (usually fully qualified links starting with https://) + if ( + url.protocol !== unspecifiedProtocol || + url.host !== '' || + url.username !== '' || + url.password !== '' + ) { + return null; + } + + // We can't use "new URL()" result because it always tries to resolve urls + // IE it will remove any "./" or "../" in the pathname, which we don't want + // We have to parse it manually... + let localUrlPath = urlPath; + + // Extract and remove the #hash part + const hashIndex = localUrlPath.indexOf('#'); + const hash = + hashIndex !== -1 ? localUrlPath.substring(hashIndex + 1) : undefined; + localUrlPath = + hashIndex !== -1 ? localUrlPath.substring(0, hashIndex) : localUrlPath; + + // Extract and remove ?search part + const searchIndex = localUrlPath.indexOf('?'); + const search = + searchIndex !== -1 ? localUrlPath.substring(searchIndex + 1) : undefined; + localUrlPath = + searchIndex !== -1 ? localUrlPath.substring(0, searchIndex) : localUrlPath; + + return { + pathname: localUrlPath, + search, + hash, + }; +} + export function serializeURLPath(urlPath: URLPath): string { const search = urlPath.search === undefined ? '' : `?${urlPath.search}`; const hash = urlPath.hash === undefined ? '' : `#${urlPath.hash}`; From f5d97343a762416771afc039fb66a8a1def4708c Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 13:33:20 +0200 Subject: [PATCH 04/11] Make it work! --- .../src/remark/linkify/index.ts | 97 ++++++++++--------- .../src/index.ts | 37 +++---- .../src/__tests__/urlUtils.test.ts | 5 + packages/docusaurus-utils/src/index.ts | 1 + .../docusaurus-utils/src/markdownLinks.ts | 58 +++++++---- 5 files changed, 110 insertions(+), 88 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/linkify/index.ts b/packages/docusaurus-mdx-loader/src/remark/linkify/index.ts index 2907dd3dd3b9..4edd10fd01fe 100644 --- a/packages/docusaurus-mdx-loader/src/remark/linkify/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/linkify/index.ts @@ -4,19 +4,33 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ + +import { + parseLocalURLPath, + serializeURLPath, + type URLPath, +} from '@docusaurus/utils'; import {stringifyContent} from '../utils'; // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721 import type {Transformer} from 'unified'; import type {Link} from 'mdast'; -export type ResolveMarkdownLink = ({ - link, - filePath, -}: { - link: string; - filePath: string; -}) => string | undefined; +type ResolveMarkdownLinkParams = { + /** + * Absolute path to the source file containing this Markdown link. + */ + sourceFilePath: string; + /** + * The Markdown link pathname to resolve, as found in the source file. + * If the link is "./myFile.mdx?qs#hash", this will be "./myFile.mdx" + */ + linkPathname: string; +}; + +export type ResolveMarkdownLink = ( + params: ResolveMarkdownLinkParams, +) => string | null; // TODO: this plugin shouldn't be in the core MDX loader // After we allow plugins to provide Remark/Rehype plugins (see @@ -32,22 +46,31 @@ export interface PluginOptions { // import type {Plugin} from 'unified'; type Plugin = any; // TODO fix this asap -const LINK_PATTERN = /\.mdx?$/g; +const HAS_MARKDOWN_EXTENSION = /\.mdx?$/i; + +function parseMarkdownLinkURLPath(link: string): URLPath | null { + const urlPath = parseLocalURLPath(link); + + // If it's not local, we don't resolve it even if it's a Markdown file + // Example, we don't resolve https://github.com/project/README.md + if (!urlPath) { + return null; + } -function ignoreLink(link: Link) { - // Probably not 100% accurate, but this is faster than proper url parsing - // Historical code, nobody complained about it so far - const hasProtocol = - link.url.toLowerCase().startsWith('http://') || - link.url.toLowerCase().startsWith('https://'); - return hasProtocol || !LINK_PATTERN.test(link.url); + // Ignore links without a Markdown file extension (ignoring qs/hash) + if (!HAS_MARKDOWN_EXTENSION.test(urlPath.pathname)) { + return null; + } + return urlPath; } -export type BrokenMarkdownLink = { - /** Absolute path to the file containing this link. */ +type BrokenMarkdownLink = { + /** + * Absolute path to the file containing this Markdown link. + */ filePath: string; /** - * The content of the link, like `"./brokenFile.md"` + * The broken Markdown link */ link: Link; }; @@ -67,38 +90,24 @@ const plugin: Plugin = function plugin(options: PluginOptions): Transformer { const brokenMarkdownLinks: BrokenMarkdownLink[] = []; visit(root, 'link', (link: Link) => { - if (ignoreLink(link)) { + const linkURLPath = parseMarkdownLinkURLPath(link.url); + if (!linkURLPath) { return; } + const permalink = resolveMarkdownLink({ - link: link.url, - filePath: file.path, + sourceFilePath: file.path, + linkPathname: linkURLPath.pathname, }); - /* - const sourcesToTry: string[] = []; - // ./file.md and ../file.md are always relative to the current file - if (!mdLink.startsWith('./') && !mdLink.startsWith('../')) { - sourcesToTry.push(...getContentPathList(contentPaths), siteDir); - } - // /file.md is always relative to the content path - if (!mdLink.startsWith('/')) { - sourcesToTry.push(path.dirname(filePath)); - } - - const aliasedSourceMatch = sourcesToTry - .map((p) => path.join(p, decodeURIComponent(mdLink))) - .map((source) => aliasedSitePath(source, siteDir)) - .find((source) => sourceToPermalink[source]); - - const permalink: string | undefined = aliasedSourceMatch - ? sourceToPermalink[aliasedSourceMatch] - : undefined; - - */ if (permalink) { - console.log(`✅ Markdown link resolved: ${link.url} => ${permalink}`); - link.url = permalink; + // This reapplies the link ?qs#hash part to the resolved pathname + const resolvedUrl = serializeURLPath({ + ...linkURLPath, + pathname: permalink, + }); + // console.log(`✅ Markdown link resolved: ${link.url} => ${resolvedUrl}`); + link.url = resolvedUrl; } else { const linkContent = stringifyContent(link, toString); console.log(`❌ Markdown link broken: [${linkContent}](${link.url})`); diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 87f1557eac69..4ecfdacb37ec 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -17,6 +17,7 @@ import { addTrailingPathSeparator, createAbsoluteFilePathMatcher, createSlugger, + resolveMarkdownLinkPathname, DEFAULT_PLUGIN_ID, } from '@docusaurus/utils'; import {loadSidebars, resolveSidebarPathOption} from './sidebars'; @@ -314,32 +315,16 @@ export default async function pluginContentDocs( image: frontMatter.image, }), markdownConfig: siteConfig.markdown, - resolveMarkdownLink: ({link, filePath}) => { - // TODO temporary, POC with historical code - const mdLink = link; - const {sourceToPermalink} = docsMarkdownOptions; - const version = getVersion(filePath, docsMarkdownOptions); - - const sourcesToTry: string[] = []; - // ./file.md and ../file.md are always relative to the current file - if (!mdLink.startsWith('./') && !mdLink.startsWith('../')) { - sourcesToTry.push(...getContentPathList(version), siteDir); - } - // /file.md is always relative to the content path - if (!mdLink.startsWith('/')) { - sourcesToTry.push(path.dirname(filePath)); - } - - const aliasedSourceMatch = sourcesToTry - .map((p) => path.join(p, decodeURIComponent(mdLink))) - .map((source) => aliasedSitePath(source, siteDir)) - .find((source) => sourceToPermalink[source]); - - const permalink: string | undefined = aliasedSourceMatch - ? sourceToPermalink[aliasedSourceMatch] - : undefined; - - return permalink; + resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { + return resolveMarkdownLinkPathname({ + linkPathname, + sourceFilePath, + sourceToPermalink: docsMarkdownOptions.sourceToPermalink, + siteDir, + contentPathRoots: getContentPathList( + getVersion(sourceFilePath, docsMarkdownOptions), + ), + }); }, } satisfies MDXLoaderOptions, }, diff --git a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts index ad292eef0ef4..97bfdccd4bf1 100644 --- a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts @@ -360,6 +360,11 @@ describe('parseLocalURLPath', () => { search: 'qs1', hash: 'hash1?qs2#hash2', }); + expect(parseLocalURLPath('../swizzling.mdx#wrapping')).toEqual({ + pathname: '../swizzling.mdx', + search: undefined, + hash: 'wrapping', + }); }); it('parses is isomorphic with serialize', () => { diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 39a4ec6e6783..8cf78740334d 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -78,6 +78,7 @@ export { type ContentPaths, type BrokenMarkdownLink, replaceMarkdownLinks, + resolveMarkdownLinkPathname, } from './markdownLinks'; export {type SluggerOptions, type Slugger, createSlugger} from './slugger'; export { diff --git a/packages/docusaurus-utils/src/markdownLinks.ts b/packages/docusaurus-utils/src/markdownLinks.ts index 6c26b12b6cbc..1f3ae7d5227b 100644 --- a/packages/docusaurus-utils/src/markdownLinks.ts +++ b/packages/docusaurus-utils/src/markdownLinks.ts @@ -58,6 +58,39 @@ function parseCodeFence(line: string): CodeFence | null { }; } +export function resolveMarkdownLinkPathname({ + linkPathname, + sourceFilePath, + sourceToPermalink, + contentPathRoots, + siteDir, +}: { + linkPathname: string; + sourceFilePath: string; + sourceToPermalink: {[aliasedFilePath: string]: string}; + contentPathRoots: string[]; + siteDir: string; +}): string | null { + const sourceDirsToTry: string[] = []; + // ./file.md and ../file.md are always relative to the current file + if (!linkPathname.startsWith('./') && !linkPathname.startsWith('../')) { + sourceDirsToTry.push(...contentPathRoots, siteDir); + } + // /file.md is never relative to the source file path + if (!linkPathname.startsWith('/')) { + sourceDirsToTry.push(path.dirname(sourceFilePath)); + } + + const aliasedSourceMatch = sourceDirsToTry + .map((sourceDir) => path.join(sourceDir, decodeURIComponent(linkPathname))) + .map((source) => aliasedSitePath(source, siteDir)) + .find((source) => sourceToPermalink[source]); + + return aliasedSourceMatch + ? sourceToPermalink[aliasedSourceMatch] ?? null + : null; +} + /** * Takes a Markdown file and replaces relative file references with their URL * counterparts, e.g. `[link](./intro.md)` => `[link](/docs/intro)`, preserving @@ -146,24 +179,13 @@ export function replaceMarkdownLinks({ continue; } - const sourcesToTry: string[] = []; - // ./file.md and ../file.md are always relative to the current file - if (!mdLink.startsWith('./') && !mdLink.startsWith('../')) { - sourcesToTry.push(...getContentPathList(contentPaths), siteDir); - } - // /file.md is always relative to the content path - if (!mdLink.startsWith('/')) { - sourcesToTry.push(path.dirname(filePath)); - } - - const aliasedSourceMatch = sourcesToTry - .map((p) => path.join(p, decodeURIComponent(mdLink))) - .map((source) => aliasedSitePath(source, siteDir)) - .find((source) => sourceToPermalink[source]); - - const permalink: string | undefined = aliasedSourceMatch - ? sourceToPermalink[aliasedSourceMatch] - : undefined; + const permalink: string | null = resolveMarkdownLinkPathname({ + siteDir, + linkPathname: mdLink, + sourceFilePath: filePath, + sourceToPermalink, + contentPathRoots: getContentPathList(contentPaths), + }); if (permalink) { // MDX won't be happy if the permalink contains a space, we need to From f039da2dc44fe20e544e765dfd8d92e72d426c93 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 14:11:09 +0200 Subject: [PATCH 05/11] unit tests --- .../remark/linkify/__tests__/index.test.ts | 148 +++++++++++++----- .../src/index.ts | 83 +++++----- 2 files changed, 149 insertions(+), 82 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts index 30e0c3531163..9c6f97586fd1 100644 --- a/packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts @@ -5,63 +5,131 @@ * LICENSE file in the root directory of this source tree. */ -import remark2rehype from 'remark-rehype'; -import stringify from 'rehype-stringify'; -import mermaid from '..'; +import linkify from '..'; +import type {PluginOptions} from '../index'; -async function process(content: string) { +async function process( + content: string, + pluginOptions?: Partial, +) { const {remark} = await import('remark'); - // const {default: mdx} = await import('remark-mdx'); - // const result = await remark().use(mermaid).use(mdx).process(content); + const options: PluginOptions = { + resolveMarkdownLink: ({linkPathname}) => `RESOLVED---${linkPathname}`, + ...pluginOptions, + }; - const result = await remark() - .use(mermaid) - .use(remark2rehype) - .use(stringify) - .process(content); + const result = await remark().use(linkify, options).process(content); return result.value; } -describe('mermaid remark plugin', () => { - it("does nothing if there's no mermaid code block", async () => { - const result = await process( - `# Heading 1 +describe('linkify remark plugin', () => { + it('resolves Markdown and MDX links', async () => { + /* language=markdown */ + const content = `[link1](link1.mdx) -No Mermaid diagram :( + [link2](../myLink2.md) [link3](myLink3.md) -\`\`\`js -this is not mermaid -\`\`\` -`, - ); + [link4](../myLink4.mdx?qs#hash) [link5](./../my/great/link5.md?#) + + [link6](../myLink6.mdx?qs#hash) + + [link7]() + + [link8](/link8.md) + + [**link** \`9\`](/link9.md) + `; + + const result = await process(content); expect(result).toMatchInlineSnapshot(` - "

Heading 1

-

No Mermaid diagram :(

-
this is not mermaid
-      
" + "[link1](RESOLVED---link1.mdx) + + \`\`\` + [link2](../myLink2.md) [link3](myLink3.md) + + [link4](../myLink4.mdx?qs#hash) [link5](./../my/great/link5.md?#) + + [link6](../myLink6.mdx?qs#hash) + + [link7]() + + [link8](/link8.md) + + [**link** \`9\`](/link9.md) + \`\`\` + " `); }); - it('works for basic mermaid code blocks', async () => { - const result = await process(`# Heading 1 + it('skips non-Markdown links', async () => { + /* language=markdown */ + const content = `[link1](./myLink1.m) + +[link2](../myLink2mdx) + +[link3](https://github.com/facebook/docusaurus/blob/main/README.md) + +[link4](ftp:///README.mdx) + +[link5](../link5.js) + +[link6](../link6.jsx) + +[link7](../link7.tsx) + + + +\`\`\`md +[link9](link9.md) +\`\`\` +`; + + const result = await process(content); -\`\`\`mermaid -graph TD; - A-->B; - A-->C; - B-->D; - C-->D; -\`\`\``); expect(result).toMatchInlineSnapshot(` - "

Heading 1

- " + "[link1](./myLink1.m) + + [link2](../myLink2mdx) + + [link3](https://github.com/facebook/docusaurus/blob/main/README.md) + + [link4](ftp:///README.mdx) + + [link5](../link5.js) + + [link6](../link6.jsx) + + [link7](../link7.tsx) + + + + \`\`\`md + [link9](link9.md) + \`\`\` + " `); }); + + it('keeps regular Markdown unmodified', async () => { + /* language=markdown */ + const content = `# Title + +Simple link + +\`\`\`js +this is a code block +\`\`\` +`; + + const result = await process(content); + + expect(result).toEqual(content); + }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 4ecfdacb37ec..61b35a84da2b 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -279,54 +279,53 @@ export default async function pluginContentDocs( .flatMap(getContentPathList) // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 .map(addTrailingPathSeparator); + + const loaderOptions: MDXLoaderOptions = { + admonitions: options.admonitions, + remarkPlugins, + rehypePlugins, + beforeDefaultRehypePlugins, + beforeDefaultRemarkPlugins, + staticDirs: siteConfig.staticDirectories.map((dir) => + path.resolve(siteDir, dir), + ), + siteDir, + isMDXPartial: createAbsoluteFilePathMatcher( + options.exclude, + contentDirs, + ), + metadataPath: (mdxPath: string) => { + // Note that metadataPath must be the same/in-sync as + // the path from createData for each MDX. + const aliasedPath = aliasedSitePath(mdxPath, siteDir); + return path.join(dataDir, `${docuHash(aliasedPath)}.json`); + }, + // Assets allow to convert some relative images paths to + // require(...) calls + createAssets: ({frontMatter}: {frontMatter: DocFrontMatter}) => ({ + image: frontMatter.image, + }), + markdownConfig: siteConfig.markdown, + resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { + return resolveMarkdownLinkPathname({ + linkPathname, + sourceFilePath, + sourceToPermalink: docsMarkdownOptions.sourceToPermalink, + siteDir, + contentPathRoots: getContentPathList( + getVersion(sourceFilePath, docsMarkdownOptions), + ), + }); + }, + }; + return { test: /\.mdx?$/i, include: contentDirs, use: [ { loader: require.resolve('@docusaurus/mdx-loader'), - options: { - admonitions: options.admonitions, - remarkPlugins, - rehypePlugins, - beforeDefaultRehypePlugins, - beforeDefaultRemarkPlugins, - staticDirs: siteConfig.staticDirectories.map((dir) => - path.resolve(siteDir, dir), - ), - siteDir, - isMDXPartial: createAbsoluteFilePathMatcher( - options.exclude, - contentDirs, - ), - metadataPath: (mdxPath: string) => { - // Note that metadataPath must be the same/in-sync as - // the path from createData for each MDX. - const aliasedPath = aliasedSitePath(mdxPath, siteDir); - return path.join(dataDir, `${docuHash(aliasedPath)}.json`); - }, - // Assets allow to convert some relative images paths to - // require(...) calls - createAssets: ({ - frontMatter, - }: { - frontMatter: DocFrontMatter; - }) => ({ - image: frontMatter.image, - }), - markdownConfig: siteConfig.markdown, - resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { - return resolveMarkdownLinkPathname({ - linkPathname, - sourceFilePath, - sourceToPermalink: docsMarkdownOptions.sourceToPermalink, - siteDir, - contentPathRoots: getContentPathList( - getVersion(sourceFilePath, docsMarkdownOptions), - ), - }); - }, - } satisfies MDXLoaderOptions, + options: loaderOptions, }, { loader: path.resolve(__dirname, './markdown/index.js'), From 668d39c2ec38e6133ea7c372730b305640912a26 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 16:20:09 +0200 Subject: [PATCH 06/11] add dogfood for linking tests --- .../_docs tests/tests/links/target.mdx | 9 +++ .../tests/links/test-markdown-links.mdx | 56 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 website/_dogfooding/_docs tests/tests/links/target.mdx create mode 100644 website/_dogfooding/_docs tests/tests/links/test-markdown-links.mdx diff --git a/website/_dogfooding/_docs tests/tests/links/target.mdx b/website/_dogfooding/_docs tests/tests/links/target.mdx new file mode 100644 index 000000000000..809de45801f7 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/links/target.mdx @@ -0,0 +1,9 @@ +--- +slug: target-doc-slug +--- + +# Target doc + +This is just a doc meant to be linked to by other docs. + +## Target heading {#target-heading} diff --git a/website/_dogfooding/_docs tests/tests/links/test-markdown-links.mdx b/website/_dogfooding/_docs tests/tests/links/test-markdown-links.mdx new file mode 100644 index 000000000000..536e27a41d67 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/links/test-markdown-links.mdx @@ -0,0 +1,56 @@ +# Test links + +These are dogfood tests showing that Markdown links with md/mdx file references are resolved correctly. + +Also proves that [#9048](https://github.com/facebook/docusaurus/issues/9048) linking bugs are solved. + +--- + +## Resolvable links + +[target.mdx](target.mdx) + +[./target.mdx](./target.mdx) + +[../links/target.mdx](../links/target.mdx) + +[./target.mdx?age=42#target-heading](./target.mdx?age=42#target-heading) + +## Complex resolvable links + +Some of those are edge cases reported in [#9048](https://github.com/facebook/docusaurus/issues/9048) + +{/* prettier-ignore */}```inline triple backticks code block, see https://github.com/facebook/docusaurus/issues/9048#issuecomment-1959199829``` + +

+ [./target.mdx](./target.mdx) bolded +

+ +[**./target.mdx** with _italic_ and `JSX`](./target.mdx) + +[`Type1`](target.mdx#target-heading)\<[`Type2`](target.mdx#target-heading)\> + +{/* prettier-ignore */}[./target.mdx link +declared +on +multiple +lines +](./target.mdx) + +[![Image with ./target.mdx link](/img/slash-introducing.svg)](./target.mdx) + +## Unresolvable links + +[https://github.com/facebook/docusaurus/blob/main/README.md](https://github.com/facebook/docusaurus/blob/main/README.md) + +[ftp:///README.mdx](ftp:///README.mdx) + +```markdown +[target.mdx](target.mdx) +``` + +## Links in comments + +{/* [doesNotExist.mdx](doesNotExist.mdx) */} + + From 7890eecd9e0ede247b78e3b3eee93ce181b723f9 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 17:35:33 +0200 Subject: [PATCH 07/11] do the refactor cleanup --- .../__snapshots__/blogUtils.test.ts.snap | 23 - .../src/__tests__/blogUtils.test.ts | 96 ---- .../src/blogUtils.ts | 32 +- .../src/index.ts | 150 +++--- .../src/markdownLoader.ts | 10 +- .../src/types.ts | 7 +- .../src/index.ts | 49 +- .../__fixtures__/docs/doc-localized.md | 1 - .../__tests__/__fixtures__/docs/doc1.md | 13 - .../__tests__/__fixtures__/docs/doc2.md | 12 - .../__tests__/__fixtures__/docs/doc4.md | 19 - .../__tests__/__fixtures__/docs/doc5.md | 6 - .../__fixtures__/docs/subdir/doc3.md | 3 - .../__tests__/__fixtures__/outside/doc1.md | 1 - .../versioned_docs/version-1.0.0/doc2.md | 7 - .../version-1.0.0/subdir/doc1.md | 3 - .../__snapshots__/linkify.test.ts.snap | 82 ---- .../src/markdown/__tests__/linkify.test.ts | 210 -------- .../src/markdown/index.ts | 21 - .../src/markdown/linkify.ts | 51 -- .../src/types.ts | 12 +- .../src/versions/index.ts | 23 +- .../__snapshots__/markdownLinks.test.ts.snap | 250 ---------- .../__snapshots__/markdownUtils.test.ts.snap | 214 --------- .../src/__tests__/markdownLinks.test.ts | 451 +++--------------- packages/docusaurus-utils/src/index.ts | 7 +- .../docusaurus-utils/src/markdownLinks.ts | 172 +------ .../tests/links/test-markdown-links.mdx | 30 ++ 28 files changed, 230 insertions(+), 1725 deletions(-) delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc-localized.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc1.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc2.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc4.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc5.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/subdir/doc3.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/versioned_docs/version-1.0.0/doc2.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/versioned_docs/version-1.0.0/subdir/doc1.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__snapshots__/linkify.test.ts.snap delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/index.ts delete mode 100644 packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts delete mode 100644 packages/docusaurus-utils/src/__tests__/__snapshots__/markdownLinks.test.ts.snap delete mode 100644 packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/blogUtils.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/blogUtils.test.ts.snap index b8ae6939f72d..038e71ca8f8e 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/blogUtils.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/blogUtils.test.ts.snap @@ -1,28 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`linkify reports broken markdown links 1`] = ` -"--- -title: This post links to another one! ---- - -[Good link 1](/blog/2018/12/14/Happy-First-Birthday-Slash) - -[Good link 2](/blog/2018/12/14/Happy-First-Birthday-Slash) - -[Bad link 1](postNotExist1.md) - -[Bad link 1](./postNotExist2.mdx) -" -`; - -exports[`linkify transforms to correct link 1`] = ` -"--- -title: This post links to another one! ---- - -[Linked post](/blog/2018/12/14/Happy-First-Birthday-Slash)" -`; - exports[`paginateBlogPosts generates a single page 1`] = ` [ { diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts index f6e34977e158..5b45f13a0823 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts @@ -5,20 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import {jest} from '@jest/globals'; -import fs from 'fs-extra'; -import path from 'path'; import {fromPartial} from '@total-typescript/shoehorn'; import { truncate, parseBlogFileName, - linkify, - getSourceToPermalink, paginateBlogPosts, applyProcessBlogPosts, - type LinkifyParams, } from '../blogUtils'; -import type {BlogBrokenMarkdownLink, BlogContentPaths} from '../types'; import type {BlogPost} from '@docusaurus/plugin-content-blog'; describe('truncate', () => { @@ -209,95 +202,6 @@ describe('parseBlogFileName', () => { }); }); -describe('linkify', () => { - const siteDir = path.join(__dirname, '__fixtures__', 'website'); - const contentPaths: BlogContentPaths = { - contentPath: path.join(siteDir, 'blog-with-ref'), - contentPathLocalized: path.join(siteDir, 'blog-with-ref-localized'), - }; - const pluginDir = 'blog-with-ref'; - - const blogPosts: BlogPost[] = [ - { - id: 'Happy 1st Birthday Slash!', - metadata: { - permalink: '/blog/2018/12/14/Happy-First-Birthday-Slash', - source: path.posix.join( - '@site', - pluginDir, - '2018-12-14-Happy-First-Birthday-Slash.md', - ), - title: 'Happy 1st Birthday Slash!', - description: `pattern name`, - date: new Date('2018-12-14'), - tags: [], - prevItem: { - permalink: '/blog/2019/01/01/date-matter', - title: 'date-matter', - }, - hasTruncateMarker: false, - frontMatter: {}, - authors: [], - unlisted: false, - }, - content: '', - }, - ]; - - async function transform(filePath: string, options?: Partial) { - const fileContent = await fs.readFile(filePath, 'utf-8'); - const transformedContent = linkify({ - filePath, - fileString: fileContent, - siteDir, - contentPaths, - sourceToPermalink: getSourceToPermalink(blogPosts), - onBrokenMarkdownLink: (brokenMarkdownLink) => { - throw new Error( - `Broken markdown link found: ${JSON.stringify(brokenMarkdownLink)}`, - ); - }, - ...options, - }); - return [fileContent, transformedContent]; - } - - it('transforms to correct link', async () => { - const post = path.join(contentPaths.contentPath, 'post.md'); - const [content, transformedContent] = await transform(post); - expect(transformedContent).toMatchSnapshot(); - expect(transformedContent).toContain( - '](/blog/2018/12/14/Happy-First-Birthday-Slash', - ); - expect(transformedContent).not.toContain( - '](2018-12-14-Happy-First-Birthday-Slash.md)', - ); - expect(content).not.toEqual(transformedContent); - }); - - it('reports broken markdown links', async () => { - const filePath = 'post-with-broken-links.md'; - const folderPath = contentPaths.contentPath; - const postWithBrokenLinks = path.join(folderPath, filePath); - const onBrokenMarkdownLink = jest.fn(); - const [, transformedContent] = await transform(postWithBrokenLinks, { - onBrokenMarkdownLink, - }); - expect(transformedContent).toMatchSnapshot(); - expect(onBrokenMarkdownLink).toHaveBeenCalledTimes(2); - expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(1, { - filePath: path.resolve(folderPath, filePath), - contentPaths, - link: 'postNotExist1.md', - } as BlogBrokenMarkdownLink); - expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(2, { - filePath: path.resolve(folderPath, filePath), - contentPaths, - link: './postNotExist2.mdx', - } as BlogBrokenMarkdownLink); - }); -}); - describe('processBlogPosts', () => { const blogPost2022: BlogPost = fromPartial({ metadata: {date: new Date('2022-01-01')}, diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index ff8b5b892915..89f1c8f36d4f 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -17,7 +17,6 @@ import { getEditUrl, getFolderContainingFile, posixPath, - replaceMarkdownLinks, Globby, normalizeFrontMatterTags, groupTaggedItems, @@ -38,7 +37,7 @@ import type { BlogTags, BlogPaginated, } from '@docusaurus/plugin-content-blog'; -import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types'; +import type {BlogContentPaths} from './types'; export function truncate(fileString: string, truncateMarker: RegExp): string { return fileString.split(truncateMarker, 1).shift()!; @@ -403,35 +402,6 @@ export async function generateBlogPosts( return blogPosts; } -export type LinkifyParams = { - filePath: string; - fileString: string; -} & Pick< - BlogMarkdownLoaderOptions, - 'sourceToPermalink' | 'siteDir' | 'contentPaths' | 'onBrokenMarkdownLink' ->; - -export function linkify({ - filePath, - contentPaths, - fileString, - siteDir, - sourceToPermalink, - onBrokenMarkdownLink, -}: LinkifyParams): string { - const {newContent, brokenMarkdownLinks} = replaceMarkdownLinks({ - siteDir, - fileString, - filePath, - contentPaths, - sourceToPermalink, - }); - - brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l)); - - return newContent; -} - export async function applyProcessBlogPosts({ blogPosts, processBlogPosts, diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 8906296a8f63..991d906d0572 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -18,6 +18,7 @@ import { getContentPathList, getDataFilePath, DEFAULT_PLUGIN_ID, + resolveMarkdownLinkPathname, } from '@docusaurus/utils'; import { getSourceToPermalink, @@ -43,6 +44,8 @@ import type { BlogContent, BlogPaginated, } from '@docusaurus/plugin-content-blog'; +import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader/lib/loader'; +import type {RuleSetUseItem} from 'webpack'; const PluginName = 'docusaurus-plugin-content-blog'; @@ -213,22 +216,81 @@ export default async function pluginContentBlog( beforeDefaultRehypePlugins, } = options; - const markdownLoaderOptions: BlogMarkdownLoaderOptions = { - siteDir, - contentPaths, - truncateMarker, - sourceToPermalink: getSourceToPermalink(content.blogPosts), - onBrokenMarkdownLink: (brokenMarkdownLink) => { - if (onBrokenMarkdownLinks === 'ignore') { - return; - } - logger.report( - onBrokenMarkdownLinks, - )`Blog markdown link couldn't be resolved: (url=${brokenMarkdownLink.link}) in path=${brokenMarkdownLink.filePath}`; - }, - }; - + const sourceToPermalink = getSourceToPermalink(content.blogPosts); const contentDirs = getContentPathList(contentPaths); + + function createMDXLoader(): RuleSetUseItem { + const loaderOptions: MDXLoaderOptions = { + admonitions, + remarkPlugins, + rehypePlugins, + beforeDefaultRemarkPlugins: [ + footnoteIDFixer, + ...beforeDefaultRemarkPlugins, + ], + beforeDefaultRehypePlugins, + staticDirs: siteConfig.staticDirectories.map((dir) => + path.resolve(siteDir, dir), + ), + siteDir, + isMDXPartial: createAbsoluteFilePathMatcher( + options.exclude, + contentDirs, + ), + metadataPath: (mdxPath: string) => { + // Note that metadataPath must be the same/in-sync as + // the path from createData for each MDX. + const aliasedPath = aliasedSitePath(mdxPath, siteDir); + return path.join(dataDir, `${docuHash(aliasedPath)}.json`); + }, + // For blog posts a title in markdown is always removed + // Blog posts title are rendered separately + removeContentTitle: true, + // Assets allow to convert some relative images paths to + // require() calls + // @ts-expect-error: TODO fix typing issue + createAssets: ({ + frontMatter, + metadata, + }: { + frontMatter: BlogPostFrontMatter; + metadata: BlogPostMetadata; + }): Assets => ({ + image: frontMatter.image, + authorsImageUrls: metadata.authors.map((author) => author.imageURL), + }), + markdownConfig: siteConfig.markdown, + resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { + const permalink = resolveMarkdownLinkPathname(linkPathname, { + sourceFilePath, + sourceToPermalink, + siteDir, + contentPaths, + }); + if (permalink === null) { + logger.report( + onBrokenMarkdownLinks, + )`Blog markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath}`; + } + return permalink; + }, + }; + return { + loader: require.resolve('@docusaurus/mdx-loader'), + options: loaderOptions, + }; + } + + function createBlogMarkdownLoader(): RuleSetUseItem { + const loaderOptions: BlogMarkdownLoaderOptions = { + truncateMarker, + }; + return { + loader: path.resolve(__dirname, './markdownLoader.js'), + options: loaderOptions, + }; + } + return { resolve: { alias: { @@ -242,61 +304,9 @@ export default async function pluginContentBlog( include: contentDirs // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 .map(addTrailingPathSeparator), - use: [ - { - loader: require.resolve('@docusaurus/mdx-loader'), - options: { - admonitions, - remarkPlugins, - rehypePlugins, - beforeDefaultRemarkPlugins: [ - footnoteIDFixer, - ...beforeDefaultRemarkPlugins, - ], - beforeDefaultRehypePlugins, - staticDirs: siteConfig.staticDirectories.map((dir) => - path.resolve(siteDir, dir), - ), - siteDir, - isMDXPartial: createAbsoluteFilePathMatcher( - options.exclude, - contentDirs, - ), - metadataPath: (mdxPath: string) => { - // Note that metadataPath must be the same/in-sync as - // the path from createData for each MDX. - const aliasedPath = aliasedSitePath(mdxPath, siteDir); - return path.join( - dataDir, - `${docuHash(aliasedPath)}.json`, - ); - }, - // For blog posts a title in markdown is always removed - // Blog posts title are rendered separately - removeContentTitle: true, - - // Assets allow to convert some relative images paths to - // require() calls - createAssets: ({ - frontMatter, - metadata, - }: { - frontMatter: BlogPostFrontMatter; - metadata: BlogPostMetadata; - }): Assets => ({ - image: frontMatter.image, - authorsImageUrls: metadata.authors.map( - (author) => author.imageURL, - ), - }), - markdownConfig: siteConfig.markdown, - }, - }, - { - loader: path.resolve(__dirname, './markdownLoader.js'), - options: markdownLoaderOptions, - }, - ].filter(Boolean), + use: [createMDXLoader(), createBlogMarkdownLoader()].filter( + Boolean, + ), }, ], }, diff --git a/packages/docusaurus-plugin-content-blog/src/markdownLoader.ts b/packages/docusaurus-plugin-content-blog/src/markdownLoader.ts index add3e16682c2..830989fe4dd7 100644 --- a/packages/docusaurus-plugin-content-blog/src/markdownLoader.ts +++ b/packages/docusaurus-plugin-content-blog/src/markdownLoader.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {truncate, linkify} from './blogUtils'; +import {truncate} from './blogUtils'; import type {BlogMarkdownLoaderOptions} from './types'; import type {LoaderContext} from 'webpack'; @@ -13,23 +13,19 @@ export default function markdownLoader( this: LoaderContext, source: string, ): void { - const filePath = this.resourcePath; const fileString = source; const callback = this.async(); const markdownLoaderOptions = this.getOptions(); // Linkify blog posts - let finalContent = linkify({ - fileString, - filePath, - ...markdownLoaderOptions, - }); + let finalContent = fileString; // Truncate content if requested (e.g: file.md?truncated=true). const truncated: boolean | undefined = this.resourceQuery ? !!new URLSearchParams(this.resourceQuery.slice(1)).get('truncated') : undefined; + // TODO truncate with the AST instead of the string ? if (truncated) { finalContent = truncate(finalContent, markdownLoaderOptions.truncateMarker); } diff --git a/packages/docusaurus-plugin-content-blog/src/types.ts b/packages/docusaurus-plugin-content-blog/src/types.ts index 9774a412300f..14820f32360e 100644 --- a/packages/docusaurus-plugin-content-blog/src/types.ts +++ b/packages/docusaurus-plugin-content-blog/src/types.ts @@ -5,15 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -import type {BrokenMarkdownLink, ContentPaths} from '@docusaurus/utils'; +import type {ContentPaths} from '@docusaurus/utils'; export type BlogContentPaths = ContentPaths; -export type BlogBrokenMarkdownLink = BrokenMarkdownLink; export type BlogMarkdownLoaderOptions = { - siteDir: string; - contentPaths: BlogContentPaths; truncateMarker: RegExp; - sourceToPermalink: {[aliasedPath: string]: string}; - onBrokenMarkdownLink: (brokenMarkdownLink: BlogBrokenMarkdownLink) => void; }; diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 61b35a84da2b..a4c1e44b5009 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -29,7 +29,11 @@ import { type DocEnv, createDocsByIdIndex, } from './docs'; -import {readVersionsMetadata, toFullVersion} from './versions'; +import { + getVersionFromSourceFilePath, + readVersionsMetadata, + toFullVersion, +} from './versions'; import {cliDocsVersionCommand} from './cli'; import {VERSIONS_JSON_FILE} from './constants'; import {toGlobalDataVersion} from './globalData'; @@ -39,7 +43,6 @@ import { } from './translations'; import {createAllRoutes} from './routes'; import {createSidebarsUtils} from './sidebars/utils'; -import {getVersion} from './markdown/linkify'; import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader'; import type { @@ -51,12 +54,7 @@ import type { LoadedVersion, } from '@docusaurus/plugin-content-docs'; import type {LoadContext, Plugin} from '@docusaurus/types'; -import type { - SourceToPermalink, - DocFile, - DocsMarkdownOption, - FullVersion, -} from './types'; +import type {SourceToPermalink, DocFile, FullVersion} from './types'; import type {RuleSetRule} from 'webpack'; export default async function pluginContentDocs( @@ -262,17 +260,7 @@ export default async function pluginContentDocs( allDocs.map(({source, permalink}) => [source, permalink]), ); } - - const docsMarkdownOptions: DocsMarkdownOption = { - siteDir, - sourceToPermalink: getSourceToPermalink(), - versionsMetadata, - onBrokenMarkdownLink: (brokenMarkdownLink) => { - logger.report( - siteConfig.onBrokenMarkdownLinks, - )`Docs markdown link couldn't be resolved: (url=${brokenMarkdownLink.link}) in path=${brokenMarkdownLink.filePath} for version number=${brokenMarkdownLink.contentPaths.versionName}`; - }, - }; + const sourceToPermalink = getSourceToPermalink(); function createMDXLoaderRule(): RuleSetRule { const contentDirs = versionsMetadata @@ -307,15 +295,22 @@ export default async function pluginContentDocs( }), markdownConfig: siteConfig.markdown, resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { - return resolveMarkdownLinkPathname({ - linkPathname, + const version = getVersionFromSourceFilePath( + sourceFilePath, + content.loadedVersions, + ); + const permalink = resolveMarkdownLinkPathname(linkPathname, { sourceFilePath, - sourceToPermalink: docsMarkdownOptions.sourceToPermalink, + sourceToPermalink, siteDir, - contentPathRoots: getContentPathList( - getVersion(sourceFilePath, docsMarkdownOptions), - ), + contentPaths: version, }); + if (permalink === null) { + logger.report( + siteConfig.onBrokenMarkdownLinks, + )`Docs markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath} for version number=${version.versionName}`; + } + return permalink; }, }; @@ -327,10 +322,6 @@ export default async function pluginContentDocs( loader: require.resolve('@docusaurus/mdx-loader'), options: loaderOptions, }, - { - loader: path.resolve(__dirname, './markdown/index.js'), - options: docsMarkdownOptions, - }, ].filter(Boolean), }; } diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc-localized.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc-localized.md deleted file mode 100644 index 63e38da76c0a..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc-localized.md +++ /dev/null @@ -1 +0,0 @@ -### localized doc diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc1.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc1.md deleted file mode 100644 index 92ecd85f9f3f..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc1.md +++ /dev/null @@ -1,13 +0,0 @@ -# Don't transform any link here - -![image1](assets/image1.png) - -# Don't replace inside fenced codeblock - -```md -![doc4](doc4.md) -``` - -### Non-existing Docs - -- [hahaha](hahaha.md) diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc2.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc2.md deleted file mode 100644 index 542405177b8c..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc2.md +++ /dev/null @@ -1,12 +0,0 @@ -### Existing Docs - -- [doc1](doc1.md) -- [doc2](./doc2.md) -- [doc3](subdir/doc3.md) - -## Repeating Docs - -- [doc1](doc1.md) -- [doc2](./doc2.md) - -- [doc-localized](/doc-localized.md) diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc4.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc4.md deleted file mode 100644 index 9cf111212a52..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc4.md +++ /dev/null @@ -1,19 +0,0 @@ -### Existing Docs - -- [doc1][doc1] -- [doc2][doc2] - -## Repeating Docs - -- [doc1][doc1] -- [doc2][doc2] - -## Do not replace this - -```md -![image1][image1] -``` - -[doc1]: doc1.md -[doc2]: ./doc2.md -[image1]: assets/image1.png diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc5.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc5.md deleted file mode 100644 index cea1e3ade8f9..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc5.md +++ /dev/null @@ -1,6 +0,0 @@ -### Not Existing Docs - -- [docNotExist1](docNotExist1.md) -- [docNotExist2](./docNotExist2.mdx) -- [docNotExist3](../docNotExist3.mdx) -- [docNotExist4](./subdir/docNotExist4.md) diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/subdir/doc3.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/subdir/doc3.md deleted file mode 100644 index 031c4c6d205d..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/subdir/doc3.md +++ /dev/null @@ -1,3 +0,0 @@ -### Relative linking - -- [doc1](../doc2.md) diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md deleted file mode 100644 index 4fd86e1c55ca..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md +++ /dev/null @@ -1 +0,0 @@ -[link](../docs/doc1.md) diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/versioned_docs/version-1.0.0/doc2.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/versioned_docs/version-1.0.0/doc2.md deleted file mode 100644 index 3dc22abb3e3b..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/versioned_docs/version-1.0.0/doc2.md +++ /dev/null @@ -1,7 +0,0 @@ -### Existing Docs - -- [doc1](subdir/doc1.md) - -### With hash - -- [doc2](doc2.md#existing-docs) diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/versioned_docs/version-1.0.0/subdir/doc1.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/versioned_docs/version-1.0.0/subdir/doc1.md deleted file mode 100644 index 031c4c6d205d..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/versioned_docs/version-1.0.0/subdir/doc1.md +++ /dev/null @@ -1,3 +0,0 @@ -### Relative linking - -- [doc1](../doc2.md) diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__snapshots__/linkify.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__snapshots__/linkify.test.ts.snap deleted file mode 100644 index 39d7880d48a1..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__snapshots__/linkify.test.ts.snap +++ /dev/null @@ -1,82 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`linkify transforms absolute links in versioned docs 1`] = ` -"### Existing Docs - -- [doc1](/docs/1.0.0/subdir/doc1) - -### With hash - -- [doc2](/docs/1.0.0/doc2#existing-docs) -" -`; - -exports[`linkify transforms nothing with no links 1`] = ` -"# Don't transform any link here - -![image1](assets/image1.png) - -# Don't replace inside fenced codeblock - -\`\`\`md -![doc4](doc4.md) -\`\`\` - -### Non-existing Docs - -- [hahaha](hahaha.md) -" -`; - -exports[`linkify transforms reference links 1`] = ` -"### Existing Docs - -- [doc1][doc1] -- [doc2][doc2] - -## Repeating Docs - -- [doc1][doc1] -- [doc2][doc2] - -## Do not replace this - -\`\`\`md -![image1][image1] -\`\`\` - -[doc1]: /docs/doc1 -[doc2]: /docs/doc2 -[image1]: assets/image1.png -" -`; - -exports[`linkify transforms relative links 1`] = ` -"### Relative linking - -- [doc1](/docs/doc2) -" -`; - -exports[`linkify transforms relative links in versioned docs 1`] = ` -"### Relative linking - -- [doc1](/docs/1.0.0/doc2) -" -`; - -exports[`linkify transforms to correct links 1`] = ` -"### Existing Docs - -- [doc1](/docs/doc1) -- [doc2](/docs/doc2) -- [doc3](/docs/subdir/doc3) - -## Repeating Docs - -- [doc1](/docs/doc1) -- [doc2](/docs/doc2) - -- [doc-localized](/fr/doc-localized) -" -`; diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts deleted file mode 100644 index 6d3f11a2c427..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {jest} from '@jest/globals'; -import fs from 'fs-extra'; -import path from 'path'; -import {linkify} from '../linkify'; -import {VERSIONED_DOCS_DIR, CURRENT_VERSION_NAME} from '../../constants'; -import type { - DocsMarkdownOption, - SourceToPermalink, - DocBrokenMarkdownLink, -} from '../../types'; -import type {VersionMetadata} from '@docusaurus/plugin-content-docs'; - -function createFakeVersion({ - versionName, - contentPath, - contentPathLocalized, -}: { - versionName: string; - contentPath: string; - contentPathLocalized: string; -}): VersionMetadata { - return { - versionName, - label: 'Any', - path: 'any', - badge: true, - banner: null, - tagsPath: '/tags/', - className: '', - contentPath, - contentPathLocalized, - sidebarFilePath: 'any', - routePriority: undefined, - isLast: false, - }; -} - -const siteDir = path.join(__dirname, '__fixtures__'); - -const versionCurrent = createFakeVersion({ - versionName: CURRENT_VERSION_NAME, - contentPath: path.join(siteDir, 'docs'), - contentPathLocalized: path.join( - siteDir, - 'i18n', - 'fr', - 'docusaurus-plugin-content-docs', - CURRENT_VERSION_NAME, - ), -}); - -const version100 = createFakeVersion({ - versionName: '1.0.0', - contentPath: path.join(siteDir, VERSIONED_DOCS_DIR, 'version-1.0.0'), - contentPathLocalized: path.join( - siteDir, - 'i18n', - 'fr', - 'docusaurus-plugin-content-docs', - 'version-1.0.0', - ), -}); - -const sourceToPermalink: SourceToPermalink = { - '@site/docs/doc1.md': '/docs/doc1', - '@site/docs/doc2.md': '/docs/doc2', - '@site/docs/subdir/doc3.md': '/docs/subdir/doc3', - '@site/docs/doc4.md': '/docs/doc4', - '@site/versioned_docs/version-1.0.0/doc2.md': '/docs/1.0.0/doc2', - '@site/versioned_docs/version-1.0.0/subdir/doc1.md': - '/docs/1.0.0/subdir/doc1', - - '@site/i18n/fr/docusaurus-plugin-content-docs/current/doc-localized.md': - '/fr/doc-localized', - '@site/docs/doc-localized.md': '/doc-localized', -}; - -function createMarkdownOptions( - options?: Partial, -): DocsMarkdownOption { - return { - sourceToPermalink, - onBrokenMarkdownLink: () => {}, - versionsMetadata: [versionCurrent, version100], - siteDir, - ...options, - }; -} - -const transform = async ( - filepath: string, - options?: Partial, -) => { - const markdownOptions = createMarkdownOptions(options); - const content = await fs.readFile(filepath, 'utf-8'); - const transformedContent = linkify(content, filepath, markdownOptions); - return [content, transformedContent]; -}; - -describe('linkify', () => { - it('transforms nothing with no links', async () => { - const doc1 = path.join(versionCurrent.contentPath, 'doc1.md'); - const [content, transformedContent] = await transform(doc1); - expect(transformedContent).toMatchSnapshot(); - expect(content).toEqual(transformedContent); - }); - - it('transforms to correct links', async () => { - const doc2 = path.join(versionCurrent.contentPath, 'doc2.md'); - const [content, transformedContent] = await transform(doc2); - expect(transformedContent).toMatchSnapshot(); - expect(transformedContent).toContain('](/docs/doc1'); - expect(transformedContent).toContain('](/docs/doc2'); - expect(transformedContent).toContain('](/docs/subdir/doc3'); - expect(transformedContent).toContain('](/fr/doc-localized'); - expect(transformedContent).not.toContain('](doc1.md)'); - expect(transformedContent).not.toContain('](./doc2.md)'); - expect(transformedContent).not.toContain('](subdir/doc3.md)'); - expect(transformedContent).not.toContain('](/doc-localized'); - expect(content).not.toEqual(transformedContent); - }); - - it('transforms relative links', async () => { - const doc3 = path.join(versionCurrent.contentPath, 'subdir', 'doc3.md'); - - const [content, transformedContent] = await transform(doc3); - expect(transformedContent).toMatchSnapshot(); - expect(transformedContent).toContain('](/docs/doc2'); - expect(transformedContent).not.toContain('](../doc2.md)'); - expect(content).not.toEqual(transformedContent); - }); - - it('transforms reference links', async () => { - const doc4 = path.join(versionCurrent.contentPath, 'doc4.md'); - const [content, transformedContent] = await transform(doc4); - expect(transformedContent).toMatchSnapshot(); - expect(transformedContent).toContain('[doc1]: /docs/doc1'); - expect(transformedContent).toContain('[doc2]: /docs/doc2'); - expect(transformedContent).not.toContain('[doc1]: doc1.md'); - expect(transformedContent).not.toContain('[doc2]: ./doc2.md'); - expect(content).not.toEqual(transformedContent); - }); - - it('reports broken markdown links', async () => { - const doc5 = path.join(versionCurrent.contentPath, 'doc5.md'); - const onBrokenMarkdownLink = jest.fn(); - const [content, transformedContent] = await transform(doc5, { - onBrokenMarkdownLink, - }); - expect(transformedContent).toEqual(content); - expect(onBrokenMarkdownLink).toHaveBeenCalledTimes(4); - expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(1, { - filePath: doc5, - link: 'docNotExist1.md', - contentPaths: versionCurrent, - } as DocBrokenMarkdownLink); - expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(2, { - filePath: doc5, - link: './docNotExist2.mdx', - contentPaths: versionCurrent, - } as DocBrokenMarkdownLink); - expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(3, { - filePath: doc5, - link: '../docNotExist3.mdx', - contentPaths: versionCurrent, - } as DocBrokenMarkdownLink); - expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(4, { - filePath: doc5, - link: './subdir/docNotExist4.md', - contentPaths: versionCurrent, - } as DocBrokenMarkdownLink); - }); - - it('transforms absolute links in versioned docs', async () => { - const doc2 = path.join(version100.contentPath, 'doc2.md'); - const [content, transformedContent] = await transform(doc2); - expect(transformedContent).toMatchSnapshot(); - expect(transformedContent).toContain('](/docs/1.0.0/subdir/doc1'); - expect(transformedContent).toContain('](/docs/1.0.0/doc2#existing-docs'); - expect(transformedContent).not.toContain('](subdir/doc1.md)'); - expect(transformedContent).not.toContain('](doc2.md#existing-docs)'); - expect(content).not.toEqual(transformedContent); - }); - - it('transforms relative links in versioned docs', async () => { - const doc1 = path.join(version100.contentPath, 'subdir', 'doc1.md'); - const [content, transformedContent] = await transform(doc1); - expect(transformedContent).toMatchSnapshot(); - expect(transformedContent).toContain('](/docs/1.0.0/doc2'); - expect(transformedContent).not.toContain('](../doc2.md)'); - expect(content).not.toEqual(transformedContent); - }); - - // See comment in linkify.ts - it('throws for file outside version', async () => { - const doc1 = path.join(__dirname, '__fixtures__/outside/doc1.md'); - await expect(() => - transform(doc1), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unexpected error: Markdown file at "/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md" does not belong to any docs version!"`, - ); - }); -}); diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/index.ts b/packages/docusaurus-plugin-content-docs/src/markdown/index.ts deleted file mode 100644 index caf1fed47bbd..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -// import {linkify} from './linkify'; -import type {DocsMarkdownOption} from '../types'; -import type {LoaderContext} from 'webpack'; - -export default function markdownLoader( - this: LoaderContext, - source: string, -): void { - const fileString = source; - const callback = this.async(); - // const options = this.getOptions(); - // return callback(null, linkify(fileString, this.resourcePath, options)); - return callback(null, fileString); -} diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts b/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts deleted file mode 100644 index cb5713ab47d2..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {replaceMarkdownLinks, getContentPathList} from '@docusaurus/utils'; -import type {DocsMarkdownOption} from '../types'; -import type {VersionMetadata} from '@docusaurus/plugin-content-docs'; - -export function getVersion( - filePath: string, - options: DocsMarkdownOption, -): VersionMetadata { - const versionFound = options.versionsMetadata.find((version) => - getContentPathList(version).some((docsDirPath) => - filePath.startsWith(docsDirPath), - ), - ); - // At this point, this should never happen, because the MDX loaders' paths are - // literally using the version content paths; but if we allow sourcing content - // from outside the docs directory (through the `include` option, for example; - // is there a compelling use-case?), this would actually be testable - if (!versionFound) { - throw new Error( - `Unexpected error: Markdown file at "${filePath}" does not belong to any docs version!`, - ); - } - return versionFound; -} - -export function linkify( - fileString: string, - filePath: string, - options: DocsMarkdownOption, -): string { - const {siteDir, sourceToPermalink, onBrokenMarkdownLink} = options; - - const {newContent, brokenMarkdownLinks} = replaceMarkdownLinks({ - siteDir, - fileString, - filePath, - contentPaths: getVersion(filePath, options), - sourceToPermalink, - }); - - brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l)); - - return newContent; -} diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index d78521c71684..c44c60896a80 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -5,9 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import type {BrokenMarkdownLink, Tag} from '@docusaurus/utils'; +import type {Tag} from '@docusaurus/utils'; import type { - VersionMetadata, LoadedVersion, CategoryGeneratedIndexMetadata, } from '@docusaurus/plugin-content-docs'; @@ -37,12 +36,3 @@ export type FullVersion = LoadedVersion & { sidebarsUtils: SidebarsUtils; categoryGeneratedIndices: CategoryGeneratedIndexMetadata[]; }; - -export type DocBrokenMarkdownLink = BrokenMarkdownLink; - -export type DocsMarkdownOption = { - versionsMetadata: VersionMetadata[]; - siteDir: string; - sourceToPermalink: SourceToPermalink; - onBrokenMarkdownLink: (brokenMarkdownLink: DocBrokenMarkdownLink) => void; -}; diff --git a/packages/docusaurus-plugin-content-docs/src/versions/index.ts b/packages/docusaurus-plugin-content-docs/src/versions/index.ts index f9be8149a853..87eb16cab5f5 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/index.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import {normalizeUrl, posixPath} from '@docusaurus/utils'; +import {getContentPathList, normalizeUrl, posixPath} from '@docusaurus/utils'; import {CURRENT_VERSION_NAME} from '../constants'; import {validateVersionsOptions} from './validation'; import { @@ -268,3 +268,24 @@ export function toFullVersion(version: LoadedVersion): FullVersion { }), }; } + +export function getVersionFromSourceFilePath( + filePath: string, + versionsMetadata: VersionMetadata[], +): VersionMetadata { + const versionFound = versionsMetadata.find((version) => + getContentPathList(version).some((docsDirPath) => + filePath.startsWith(docsDirPath), + ), + ); + // At this point, this should never happen, because the MDX loaders' paths are + // literally using the version content paths; but if we allow sourcing content + // from outside the docs directory (through the `include` option, for example; + // is there a compelling use-case?), this would actually be testable + if (!versionFound) { + throw new Error( + `Unexpected error: file at "${filePath}" does not belong to any docs version!`, + ); + } + return versionFound; +} diff --git a/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownLinks.test.ts.snap b/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownLinks.test.ts.snap deleted file mode 100644 index 3c4f732a82fd..000000000000 --- a/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownLinks.test.ts.snap +++ /dev/null @@ -1,250 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`replaceMarkdownLinks does basic replace 1`] = ` -{ - "brokenMarkdownLinks": [ - { - "contentPaths": { - "contentPath": "docs", - "contentPathLocalized": "i18n/docs-localized", - }, - "filePath": "docs/intro.md", - "link": "hmmm.md", - }, - ], - "newContent": " -[foo](/doc/foo) -[baz](/doc/baz) -[foo](/doc/foo) -[http](http://github.com/facebook/docusaurus/README.md) -[https](https://github.com/facebook/docusaurus/README.md) -[asset](./foo.js) -[asset as well](@site/docs/_partial.md) -[looks like http...](/doc/http) -[nonexistent](hmmm.md) -", -} -`; - -exports[`replaceMarkdownLinks handles link titles 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": " -[URL](/docs/file "title") -[URL](/docs/file 'title') -[URL](/docs/file (title)) -", -} -`; - -exports[`replaceMarkdownLinks handles stray spaces 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": " -[URL]( /docs/file ) -[ref]: /docs/file -", -} -`; - -exports[`replaceMarkdownLinks handles unpaired fences 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": " -\`\`\`foo -hello - -\`\`\`foo -hello -\`\`\` - -A [link](/docs/file) -", -} -`; - -exports[`replaceMarkdownLinks ignores links in HTML comments 1`] = ` -{ - "brokenMarkdownLinks": [ - { - "contentPaths": { - "contentPath": "docs", - "contentPathLocalized": "i18n/docs-localized", - }, - "filePath": "docs/intro.md", - "link": "./foo.md", - }, - { - "contentPaths": { - "contentPath": "docs", - "contentPathLocalized": "i18n/docs-localized", - }, - "filePath": "docs/intro.md", - "link": "./foo.md", - }, - ], - "newContent": " - - -", -} -`; - -exports[`replaceMarkdownLinks ignores links in fenced blocks 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": " -\`\`\` -[foo](foo.md) -\`\`\` - -\`\`\`\`js -[foo](foo.md) -\`\`\` -[foo](foo.md) -\`\`\` -[foo](foo.md) -\`\`\`\` - -\`\`\`\`js -[foo](foo.md) -\`\`\` -[foo](foo.md) -\`\`\`\` - -~~~js -[foo](foo.md) -~~~ - -~~~js -[foo](foo.md) -\`\`\` -[foo](foo.md) -\`\`\` -[foo](foo.md) -~~~ -", -} -`; - -exports[`replaceMarkdownLinks ignores links in inline code 1`] = ` -{ - "brokenMarkdownLinks": [ - { - "contentPaths": { - "contentPath": "docs", - "contentPathLocalized": "i18n/docs-localized", - }, - "filePath": "docs/intro.md", - "link": "foo.md", - }, - ], - "newContent": " -\`[foo](foo.md)\` -", -} -`; - -exports[`replaceMarkdownLinks preserves query/hash 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": " -[URL](/docs/file?foo=bar#baz) -[URL](/docs/file#a) -[URL](/docs/file?c) -", -} -`; - -exports[`replaceMarkdownLinks replaces Markdown links with spaces 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": " -[doc a](/docs/doc%20a) -[doc a]() -[doc b](/docs/my%20docs/doc%20b) -[doc b]() -[doc]: -", -} -`; - -exports[`replaceMarkdownLinks replaces links with same title as URL 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": " -[foo.md](/docs/foo) -[./foo.md]() -[./foo.md](/docs/foo) -[foo.md](/docs/foo) -[./foo.md](/docs/foo) -", -} -`; - -exports[`replaceMarkdownLinks replaces multiple links on same line 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": " -[a](/docs/a), [a](/docs/a), [b](/docs/b), [c](/docs/c) -", -} -`; - -exports[`replaceMarkdownLinks replaces reference style Markdown links 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": " -The following operations are defined for [URI]s: - -* [info]: Returns metadata about the resource, -* [list]: Returns metadata about the resource's children (like getting the content of a local directory). - -[URI]: /docs/api/classes/uri -[info]: /docs/api/classes/uri#info -[list]: /docs/api/classes/uri#list - ", -} -`; - -exports[`replaceMarkdownLinks replaces two links on the same line 1`] = ` -{ - "brokenMarkdownLinks": [], - "newContent": "[TypeScript](/programming-languages/typescript/) and [Go](/programming-languages/go/)", -} -`; - -exports[`replaceMarkdownLinks resolves absolute and relative links differently 1`] = ` -{ - "brokenMarkdownLinks": [ - { - "contentPaths": { - "contentPath": "docs", - "contentPathLocalized": "i18n/docs-localized", - }, - "filePath": "docs/intro/intro.md", - "link": "./api/classes/divine_uri.URI.md", - }, - { - "contentPaths": { - "contentPath": "docs", - "contentPathLocalized": "i18n/docs-localized", - }, - "filePath": "docs/intro/intro.md", - "link": "/another.md", - }, - ], - "newContent": " -[Relative link](/docs/another) -[Relative link 2](/docs/api/classes/uri) -[Relative link that should be absolute](./api/classes/divine_uri.URI.md) -[Absolute link](/docs/api/classes/uri) -[Absolute link from site dir](/docs/api/classes/uri) -[Absolute link that should be relative](/another.md) -[Relative link that acts as absolute](/docs/api/classes/uri) -[Relative link that acts as relative](/docs/another) -", -} -`; diff --git a/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap b/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap deleted file mode 100644 index 8fb7a03dfa2f..000000000000 --- a/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap +++ /dev/null @@ -1,214 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`parseMarkdownFile deletes only first heading 1`] = ` -{ - "content": "# Markdown Title - -test test test # test bar - -# Markdown Title 2 - -### Markdown Title h3", - "contentTitle": "Markdown Title", - "excerpt": "test test test # test bar", - "frontMatter": {}, -} -`; - -exports[`parseMarkdownFile deletes only first heading 2 1`] = ` -{ - "content": "# test - -test test test test test test -test test test # test bar -# test2 -### test -test3", - "contentTitle": "test", - "excerpt": "test test test test test test", - "frontMatter": {}, -} -`; - -exports[`parseMarkdownFile does not warn for duplicate title if markdown title is not at the top 1`] = ` -{ - "content": "foo - -# Markdown Title", - "contentTitle": undefined, - "excerpt": "foo", - "frontMatter": { - "title": "Frontmatter title", - }, -} -`; - -exports[`parseMarkdownFile handles code blocks 1`] = ` -{ - "content": "\`\`\`js -code -\`\`\` - -Content", - "contentTitle": undefined, - "excerpt": "Content", - "frontMatter": {}, -} -`; - -exports[`parseMarkdownFile handles code blocks 2`] = ` -{ - "content": "\`\`\`\`js -Foo -\`\`\`diff -code -\`\`\` -Bar -\`\`\`\` - -Content", - "contentTitle": undefined, - "excerpt": "Content", - "frontMatter": {}, -} -`; - -exports[`parseMarkdownFile handles code blocks 3`] = ` -{ - "content": "\`\`\`\`js -Foo -\`\`\`diff -code -\`\`\`\` - -Content", - "contentTitle": undefined, - "excerpt": "Content", - "frontMatter": {}, -} -`; - -exports[`parseMarkdownFile ignores markdown title if its not a first text 1`] = ` -{ - "content": "foo -# test", - "contentTitle": undefined, - "excerpt": "foo", - "frontMatter": {}, -} -`; - -exports[`parseMarkdownFile parse markdown with custom front matter parser 1`] = ` -{ - "content": "Some text", - "contentTitle": undefined, - "excerpt": "Some text", - "frontMatter": { - "age": 84, - "extra": "value", - "great": true, - "title": "Frontmatter title", - }, -} -`; - -exports[`parseMarkdownFile parse markdown with front matter 1`] = ` -{ - "content": "Some text", - "contentTitle": undefined, - "excerpt": "Some text", - "frontMatter": { - "title": "Frontmatter title", - }, -} -`; - -exports[`parseMarkdownFile parses first heading as contentTitle 1`] = ` -{ - "content": "# Markdown Title - -Some text", - "contentTitle": "Markdown Title", - "excerpt": "Some text", - "frontMatter": {}, -} -`; - -exports[`parseMarkdownFile parses front-matter and ignore h2 1`] = ` -{ - "content": "## test", - "contentTitle": undefined, - "excerpt": "test", - "frontMatter": { - "title": "Frontmatter title", - }, -} -`; - -exports[`parseMarkdownFile parses title only 1`] = ` -{ - "content": "# test", - "contentTitle": "test", - "excerpt": undefined, - "frontMatter": {}, -} -`; - -exports[`parseMarkdownFile parses title only alternate 1`] = ` -{ - "content": "test -===", - "contentTitle": "test", - "excerpt": undefined, - "frontMatter": {}, -} -`; - -exports[`parseMarkdownFile reads front matter only 1`] = ` -{ - "content": "", - "contentTitle": undefined, - "excerpt": undefined, - "frontMatter": { - "title": "test", - }, -} -`; - -exports[`parseMarkdownFile warns about duplicate titles (front matter + markdown alternate) 1`] = ` -{ - "content": "Markdown Title alternate -================ - -Some text", - "contentTitle": "Markdown Title alternate", - "excerpt": "Some text", - "frontMatter": { - "title": "Frontmatter title", - }, -} -`; - -exports[`parseMarkdownFile warns about duplicate titles (front matter + markdown) 1`] = ` -{ - "content": "# Markdown Title - -Some text", - "contentTitle": "Markdown Title", - "excerpt": "Some text", - "frontMatter": { - "title": "Frontmatter title", - }, -} -`; - -exports[`parseMarkdownFile warns about duplicate titles 1`] = ` -{ - "content": "# test", - "contentTitle": "test", - "excerpt": undefined, - "frontMatter": { - "title": "Frontmatter title", - }, -} -`; diff --git a/packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts b/packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts index ce0acfb3a99b..c9526c12dcd3 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts @@ -5,401 +5,70 @@ * LICENSE file in the root directory of this source tree. */ -import {replaceMarkdownLinks} from '../markdownLinks'; +import {resolveMarkdownLinkPathname} from '../markdownLinks'; -describe('replaceMarkdownLinks', () => { - it('does basic replace', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/intro.md': '/docs/intro', - '@site/docs/foo.md': '/doc/foo', - '@site/docs/bar/baz.md': '/doc/baz', - '@site/docs/http.foo.md': '/doc/http', - }, - fileString: ` -[foo](./foo.md) -[baz](./bar/baz.md) -[foo](foo.md) -[http](http://github.com/facebook/docusaurus/README.md) -[https](https://github.com/facebook/docusaurus/README.md) -[asset](./foo.js) -[asset as well](@site/docs/_partial.md) -[looks like http...](http.foo.md) -[nonexistent](hmmm.md) -`, - }), - ).toMatchSnapshot(); - }); - - it('replaces two links on the same line', () => { - // cSpell:ignore Goooooooooo - // This is a very arcane bug: if we continue matching using the previous - // matching index (as is the behavior of RegExp#exec), it will go right over - // the next Markdown link and fail to match the "Go" link. This only happens - // when: (1) the replaced link is much shorter than the Markdown path, (2) - // the next link is very close to the current one (e.g. here if it's not - // "Go" but "Goooooooooo", or if every link has the /docs/ prefix, the bug - // will not trigger because it won't overshoot) - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/intro.md': '/', - '@site/docs/programming-languages/typescript/typescript.md': - '/programming-languages/typescript/', - '@site/docs/programming-languages/go/go.md': - '/programming-languages/go/', - }, - fileString: `[TypeScript](programming-languages/typescript/typescript.md) and [Go](programming-languages/go/go.md)`, - }), - ).toMatchSnapshot(); - }); - - it('replaces reference style Markdown links', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, +describe('resolveMarkdownLinkPathname', () => { + type Context = Parameters[1]; - sourceToPermalink: { - '@site/docs/intro/intro.md': '/docs/intro', - '@site/docs/api/classes/divine_uri.URI.md': '/docs/api/classes/uri', - }, - - fileString: ` -The following operations are defined for [URI]s: - -* [info]: Returns metadata about the resource, -* [list]: Returns metadata about the resource's children (like getting the content of a local directory). - -[URI]: ../api/classes/divine_uri.URI.md -[info]: ../api/classes/divine_uri.URI.md#info -[list]: ../api/classes/divine_uri.URI.md#list - `, - }), - ).toMatchSnapshot(); + it('does basic replace', () => { + const context: Context = { + siteDir: '.', + sourceFilePath: 'docs/intro.md', + contentPaths: { + contentPath: 'docs', + contentPathLocalized: 'i18n/docs-localized', + }, + sourceToPermalink: { + '@site/docs/intro.md': '/docs/intro', + '@site/docs/foo.md': '/doc/foo', + '@site/docs/bar/baz.md': '/doc/baz', + '@site/docs/http.foo.md': '/doc/http', + }, + }; + + function test(linkPathname: string, expectedOutput: string) { + const output = resolveMarkdownLinkPathname(linkPathname, context); + expect(output).toEqual(expectedOutput); + } + + test('./foo.md', '/doc/foo'); + test('foo.md', '/doc/foo'); + test('./bar/baz.md', '/doc/baz'); + test('http.foo.md', '/doc/http'); + test('@site/docs/_partial.md', null); + test('foo.js', null); + test('nonexistent.md', null); + test('https://github.com/facebook/docusaurus/README.md', null); }); it('resolves absolute and relative links differently', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - - sourceToPermalink: { - '@site/docs/intro/intro.md': '/docs/intro', - '@site/docs/intro/another.md': '/docs/another', - '@site/docs/api/classes/divine_uri.URI.md': '/docs/api/classes/uri', - }, - - fileString: ` -[Relative link](./another.md) -[Relative link 2](../api/classes/divine_uri.URI.md) -[Relative link that should be absolute](./api/classes/divine_uri.URI.md) -[Absolute link](/api/classes/divine_uri.URI.md) -[Absolute link from site dir](/docs/api/classes/divine_uri.URI.md) -[Absolute link that should be relative](/another.md) -[Relative link that acts as absolute](api/classes/divine_uri.URI.md) -[Relative link that acts as relative](another.md) -`, - }), - ).toMatchSnapshot(); - }); - - // TODO bad - it('ignores links in HTML comments', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/intro.md': '/docs/intro', - }, - fileString: ` - - -`, - }), - ).toMatchSnapshot(); - }); - - it('ignores links in fenced blocks', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/intro.md': '/docs/intro', - }, - fileString: ` -\`\`\` -[foo](foo.md) -\`\`\` - -\`\`\`\`js -[foo](foo.md) -\`\`\` -[foo](foo.md) -\`\`\` -[foo](foo.md) -\`\`\`\` - -\`\`\`\`js -[foo](foo.md) -\`\`\` -[foo](foo.md) -\`\`\`\` - -~~~js -[foo](foo.md) -~~~ - -~~~js -[foo](foo.md) -\`\`\` -[foo](foo.md) -\`\`\` -[foo](foo.md) -~~~ -`, - }), - ).toMatchSnapshot(); - }); - - // FIXME - it('ignores links in inline code', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/intro.md': '/docs/intro', - }, - fileString: ` -\`[foo](foo.md)\` -`, - }), - ).toMatchSnapshot(); - }); - - it('replaces links with same title as URL', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/intro.md': '/docs/intro', - '@site/docs/foo.md': '/docs/foo', - }, - fileString: ` -[foo.md](foo.md) -[./foo.md](<./foo.md>) -[./foo.md](./foo.md) -[foo.md](./foo.md) -[./foo.md](foo.md) -`, - }), - ).toMatchSnapshot(); - }); - - it('replaces multiple links on same line', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/intro.md': '/docs/intro', - '@site/docs/a.md': '/docs/a', - '@site/docs/b.md': '/docs/b', - '@site/docs/c.md': '/docs/c', - }, - fileString: ` -[a](a.md), [a](a.md), [b](b.md), [c](c.md) -`, - }), - ).toMatchSnapshot(); - }); - - it('replaces Markdown links with spaces', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/intro.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/doc a.md': '/docs/doc%20a', - '@site/docs/my docs/doc b.md': '/docs/my%20docs/doc%20b', - }, - fileString: ` -[doc a](./doc%20a.md) -[doc a](<./doc a.md>) -[doc b](./my%20docs/doc%20b.md) -[doc b](<./my docs/doc b.md>) -[doc]: <./my docs/doc b.md> -`, - }), - ).toMatchSnapshot(); - }); - - it('does not replace non-Markdown links', () => { - const input = ` -[asset](./file.md_asset/1.png) -[URL]() -[not a link]((foo) -[not a link](foo bar) -[not a link]: foo bar -[not a link]: (foo -[not a link]: bar) -`; - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/file.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/file.md': '/docs/file', - }, - fileString: input, - }), - ).toEqual({ - newContent: input, - brokenMarkdownLinks: [], - }); - }); - - it('handles stray spaces', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/file.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/file.md': '/docs/file', - }, - fileString: ` -[URL]( ./file.md ) -[ref]: ./file.md -`, - }), - ).toMatchSnapshot(); - }); - - it('handles link titles', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/file.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/file.md': '/docs/file', - }, - fileString: ` -[URL](./file.md "title") -[URL](./file.md 'title') -[URL](./file.md (title)) -`, - }), - ).toMatchSnapshot(); - }); - - it('preserves query/hash', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/file.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/file.md': '/docs/file', - }, - fileString: ` -[URL](./file.md?foo=bar#baz) -[URL](./file.md#a) -[URL](./file.md?c) -`, - }), - ).toMatchSnapshot(); - }); - - it('handles unpaired fences', () => { - expect( - replaceMarkdownLinks({ - siteDir: '.', - filePath: 'docs/file.md', - contentPaths: { - contentPath: 'docs', - contentPathLocalized: 'i18n/docs-localized', - }, - sourceToPermalink: { - '@site/docs/file.md': '/docs/file', - }, - fileString: ` -\`\`\`foo -hello - -\`\`\`foo -hello -\`\`\` - -A [link](./file.md) -`, - }), - ).toMatchSnapshot(); + const context: Context = { + siteDir: '.', + sourceFilePath: 'docs/intro/intro.md', + contentPaths: { + contentPath: 'docs', + contentPathLocalized: 'i18n/docs-localized', + }, + + sourceToPermalink: { + '@site/docs/intro/intro.md': '/docs/intro', + '@site/docs/intro/another.md': '/docs/another', + '@site/docs/api/classes/divine_uri.URI.md': '/docs/api/classes/uri', + }, + }; + + function test(linkPathname: string, expectedOutput: string) { + const output = resolveMarkdownLinkPathname(linkPathname, context); + expect(output).toEqual(expectedOutput); + } + + test('./another.md', '/docs/another'); + test('../api/classes/divine_uri.URI.md', '/docs/api/classes/uri'); + test('./api/classes/divine_uri.URI.md', null); + test('/api/classes/divine_uri.URI.md', '/docs/api/classes/uri'); + test('/docs/api/classes/divine_uri.URI.md', '/docs/api/classes/uri'); + test('/another.md', null); + test('api/classes/divine_uri.URI.md', '/docs/api/classes/uri'); + test('another.md', '/docs/another'); }); }); diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 8cf78740334d..6e6c3c67188e 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -74,12 +74,7 @@ export { writeMarkdownHeadingId, type WriteHeadingIDOptions, } from './markdownUtils'; -export { - type ContentPaths, - type BrokenMarkdownLink, - replaceMarkdownLinks, - resolveMarkdownLinkPathname, -} from './markdownLinks'; +export {type ContentPaths, resolveMarkdownLinkPathname} from './markdownLinks'; export {type SluggerOptions, type Slugger, createSlugger} from './slugger'; export { isNameTooLong, diff --git a/packages/docusaurus-utils/src/markdownLinks.ts b/packages/docusaurus-utils/src/markdownLinks.ts index 1f3ae7d5227b..265cf28ba200 100644 --- a/packages/docusaurus-utils/src/markdownLinks.ts +++ b/packages/docusaurus-utils/src/markdownLinks.ts @@ -40,41 +40,20 @@ export type BrokenMarkdownLink = { link: string; }; -type CodeFence = { - type: '`' | '~'; - definitelyOpen: boolean; - count: number; -}; - -function parseCodeFence(line: string): CodeFence | null { - const match = line.trim().match(/^(?`{3,}|~{3,})(?.*)/); - if (!match) { - return null; - } - return { - type: match.groups!.fence![0]! as '`' | '~', - definitelyOpen: !!match.groups!.rest!, - count: match.groups!.fence!.length, - }; -} - -export function resolveMarkdownLinkPathname({ - linkPathname, - sourceFilePath, - sourceToPermalink, - contentPathRoots, - siteDir, -}: { - linkPathname: string; - sourceFilePath: string; - sourceToPermalink: {[aliasedFilePath: string]: string}; - contentPathRoots: string[]; - siteDir: string; -}): string | null { +export function resolveMarkdownLinkPathname( + linkPathname: string, + context: { + sourceFilePath: string; + sourceToPermalink: {[aliasedFilePath: string]: string}; + contentPaths: ContentPaths; + siteDir: string; + }, +): string | null { + const {sourceFilePath, sourceToPermalink, contentPaths, siteDir} = context; const sourceDirsToTry: string[] = []; // ./file.md and ../file.md are always relative to the current file if (!linkPathname.startsWith('./') && !linkPathname.startsWith('../')) { - sourceDirsToTry.push(...contentPathRoots, siteDir); + sourceDirsToTry.push(...getContentPathList(contentPaths), siteDir); } // /file.md is never relative to the source file path if (!linkPathname.startsWith('/')) { @@ -90,132 +69,3 @@ export function resolveMarkdownLinkPathname({ ? sourceToPermalink[aliasedSourceMatch] ?? null : null; } - -/** - * Takes a Markdown file and replaces relative file references with their URL - * counterparts, e.g. `[link](./intro.md)` => `[link](/docs/intro)`, preserving - * everything else. - * - * This method uses best effort to find a matching file. The file reference can - * be relative to the directory of the current file (most likely) or any of the - * content paths (so `/tutorials/intro.md` can be resolved as - * `/docs/tutorials/intro.md`). Links that contain the `http(s):` or - * `@site/` prefix will always be ignored. - */ -// TODO remove this -export function replaceMarkdownLinks({ - siteDir, - fileString, - filePath, - contentPaths, - sourceToPermalink, -}: { - /** Absolute path to the site directory, used to resolve aliased paths. */ - siteDir: string; - /** The Markdown file content to be processed. */ - fileString: string; - /** Absolute path to the current file containing `fileString`. */ - filePath: string; - /** The content paths which the file reference may live in. */ - contentPaths: T; - /** - * A map from source paths to their URLs. Source paths are `@site` aliased. - */ - sourceToPermalink: {[aliasedPath: string]: string}; -}): { - /** - * The content with all Markdown file references replaced with their URLs. - * Unresolved links are left as-is. - */ - newContent: string; - /** The list of broken links, */ - brokenMarkdownLinks: BrokenMarkdownLink[]; -} { - const brokenMarkdownLinks: BrokenMarkdownLink[] = []; - - // Replace internal markdown linking (except in fenced blocks). - let lastOpenCodeFence: CodeFence | null = null; - const lines = fileString.split('\n').map((line) => { - const codeFence = parseCodeFence(line); - if (codeFence) { - if (!lastOpenCodeFence) { - lastOpenCodeFence = codeFence; - } else if ( - !codeFence.definitelyOpen && - lastOpenCodeFence.type === codeFence.type && - lastOpenCodeFence.count <= codeFence.count - ) { - // All three conditions must be met in order for this to be considered - // a closing fence. - lastOpenCodeFence = null; - } - } - if (lastOpenCodeFence) { - return line; - } - - let modifiedLine = line; - // Replace inline-style links or reference-style links e.g: - // This is [Document 1](doc1.md) - // [doc1]: doc1.md - const linkTitlePattern = '(?:\\s+(?:\'.*?\'|".*?"|\\(.*?\\)))?'; - const linkSuffixPattern = '(?:\\?[^#>\\s]+)?(?:#[^>\\s]+)?'; - const linkCapture = (forbidden: string) => - `((?!https?://|@site/)[^${forbidden}#?]+)`; - const linkURLPattern = `(?:(?!<)${linkCapture( - '()\\s', - )}${linkSuffixPattern}|<${linkCapture('>')}${linkSuffixPattern}>)`; - const linkPattern = new RegExp( - `\\[(?:(?!\\]\\().)*\\]\\(\\s*${linkURLPattern}${linkTitlePattern}\\s*\\)|^\\s*\\[[^[\\]]*[^[\\]\\s][^[\\]]*\\]:\\s*${linkURLPattern}${linkTitlePattern}$`, - 'dgm', - ); - let mdMatch = linkPattern.exec(modifiedLine); - while (mdMatch !== null) { - // Replace it to correct html link. - const mdLink = mdMatch.slice(1, 5).find(Boolean)!; - const mdLinkRange = mdMatch.indices!.slice(1, 5).find(Boolean)!; - if (!/\.mdx?$/.test(mdLink)) { - mdMatch = linkPattern.exec(modifiedLine); - continue; - } - - const permalink: string | null = resolveMarkdownLinkPathname({ - siteDir, - linkPathname: mdLink, - sourceFilePath: filePath, - sourceToPermalink, - contentPathRoots: getContentPathList(contentPaths), - }); - - if (permalink) { - // MDX won't be happy if the permalink contains a space, we need to - // convert it to %20 - const encodedPermalink = permalink - .split('/') - .map((part) => part.replace(/\s/g, '%20')) - .join('/'); - modifiedLine = `${modifiedLine.slice( - 0, - mdLinkRange[0], - )}${encodedPermalink}${modifiedLine.slice(mdLinkRange[1])}`; - // Adjust the lastIndex to avoid passing over the next link if the - // newly replaced URL is shorter. - linkPattern.lastIndex += encodedPermalink.length - mdLink.length; - } else { - const brokenMarkdownLink: BrokenMarkdownLink = { - contentPaths, - filePath, - link: mdLink, - }; - - brokenMarkdownLinks.push(brokenMarkdownLink); - } - mdMatch = linkPattern.exec(modifiedLine); - } - return modifiedLine; - }); - - const newContent = lines.join('\n'); - - return {newContent, brokenMarkdownLinks}; -} diff --git a/website/_dogfooding/_docs tests/tests/links/test-markdown-links.mdx b/website/_dogfooding/_docs tests/tests/links/test-markdown-links.mdx index 536e27a41d67..e2ddf64da893 100644 --- a/website/_dogfooding/_docs tests/tests/links/test-markdown-links.mdx +++ b/website/_dogfooding/_docs tests/tests/links/test-markdown-links.mdx @@ -16,6 +16,10 @@ Also proves that [#9048](https://github.com/facebook/docusaurus/issues/9048) lin [./target.mdx?age=42#target-heading](./target.mdx?age=42#target-heading) +[\<./target.mdx?qs=value with space>](<./target.mdx?qs=value with space>) + +[target.mdx 'link title'](target.mdx 'link title') + ## Complex resolvable links Some of those are edge cases reported in [#9048](https://github.com/facebook/docusaurus/issues/9048) @@ -51,6 +55,32 @@ lines ## Links in comments +MDX/HTML comments with invalid file references should not be resolved nor reported by the broken link checker: + +```mdx +{/* [doesNotExist.mdx](doesNotExist.mdx) */} + + +``` + {/* [doesNotExist.mdx](doesNotExist.mdx) */} + +## Reference-style links + +The following should also work: + +```md +Testing some link refs: [link-ref1], [link-ref2], [link-ref3] + +[link-ref1]: target.mdx +[link-ref2]: ./target.mdx +[link-ref3]: ../links/target.mdx?qs#target-heading +``` + +Testing some link refs: [link-ref1], [link-ref2], [link-ref3] + +[link-ref1]: target.mdx +[link-ref2]: ./target.mdx +[link-ref3]: ../links/target.mdx?qs#target-heading From 9527c09699ed012c5106580e9f567b3dfa4531d5 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 17:46:44 +0200 Subject: [PATCH 08/11] more cleanup --- packages/docusaurus-mdx-loader/src/loader.ts | 2 +- .../docusaurus-mdx-loader/src/processor.ts | 7 +- .../__tests__/index.test.ts | 6 +- .../index.ts | 0 .../src/index.ts | 4 +- .../src/index.ts | 6 +- .../src/index.ts | 85 +++++++++---------- .../src/markdownLoader.ts | 22 ----- 8 files changed, 52 insertions(+), 80 deletions(-) rename packages/docusaurus-mdx-loader/src/remark/{linkify => resolveMarkdownLinks}/__tests__/index.test.ts (94%) rename packages/docusaurus-mdx-loader/src/remark/{linkify => resolveMarkdownLinks}/index.ts (100%) delete mode 100644 packages/docusaurus-plugin-content-pages/src/markdownLoader.ts diff --git a/packages/docusaurus-mdx-loader/src/loader.ts b/packages/docusaurus-mdx-loader/src/loader.ts index 00594108d1ed..7118c38e747c 100644 --- a/packages/docusaurus-mdx-loader/src/loader.ts +++ b/packages/docusaurus-mdx-loader/src/loader.ts @@ -17,7 +17,7 @@ import stringifyObject from 'stringify-object'; import preprocessor from './preprocessor'; import {validateMDXFrontMatter} from './frontMatter'; import {createProcessorCached} from './processor'; -import type {ResolveMarkdownLink} from './remark/linkify'; +import type {ResolveMarkdownLink} from './remark/resolveMarkdownLinks'; import type {MDXOptions} from './processor'; import type {MarkdownConfig} from '@docusaurus/types'; diff --git a/packages/docusaurus-mdx-loader/src/processor.ts b/packages/docusaurus-mdx-loader/src/processor.ts index 90c2f17248ad..c96bf168e954 100644 --- a/packages/docusaurus-mdx-loader/src/processor.ts +++ b/packages/docusaurus-mdx-loader/src/processor.ts @@ -10,7 +10,7 @@ import contentTitle from './remark/contentTitle'; import toc from './remark/toc'; import transformImage from './remark/transformImage'; import transformLinks from './remark/transformLinks'; -import linkify from './remark/linkify'; +import resolveMarkdownLinks from './remark/resolveMarkdownLinks'; import details from './remark/details'; import head from './remark/head'; import mermaid from './remark/mermaid'; @@ -123,7 +123,10 @@ async function createProcessorFactory() { ], // TODO merge this with transformLinks? options.resolveMarkdownLink - ? [linkify, {resolveMarkdownLink: options.resolveMarkdownLink}] + ? [ + resolveMarkdownLinks, + {resolveMarkdownLink: options.resolveMarkdownLink}, + ] : undefined, [ transformLinks, diff --git a/packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts similarity index 94% rename from packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts rename to packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts index 9c6f97586fd1..491d4b0aca66 100644 --- a/packages/docusaurus-mdx-loader/src/remark/linkify/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import linkify from '..'; +import plugin from '..'; import type {PluginOptions} from '../index'; async function process( @@ -19,12 +19,12 @@ async function process( ...pluginOptions, }; - const result = await remark().use(linkify, options).process(content); + const result = await remark().use(plugin, options).process(content); return result.value; } -describe('linkify remark plugin', () => { +describe('resolveMarkdownLinks remark plugin', () => { it('resolves Markdown and MDX links', async () => { /* language=markdown */ const content = `[link1](link1.mdx) diff --git a/packages/docusaurus-mdx-loader/src/remark/linkify/index.ts b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts similarity index 100% rename from packages/docusaurus-mdx-loader/src/remark/linkify/index.ts rename to packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 991d906d0572..dc64b8b45155 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -304,9 +304,7 @@ export default async function pluginContentBlog( include: contentDirs // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 .map(addTrailingPathSeparator), - use: [createMDXLoader(), createBlogMarkdownLoader()].filter( - Boolean, - ), + use: [createMDXLoader(), createBlogMarkdownLoader()], }, ], }, diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index a4c1e44b5009..67054ff7da8b 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -55,7 +55,7 @@ import type { } from '@docusaurus/plugin-content-docs'; import type {LoadContext, Plugin} from '@docusaurus/types'; import type {SourceToPermalink, DocFile, FullVersion} from './types'; -import type {RuleSetRule} from 'webpack'; +import type {RuleSetUseItem} from 'webpack'; export default async function pluginContentDocs( context: LoadContext, @@ -262,7 +262,7 @@ export default async function pluginContentDocs( } const sourceToPermalink = getSourceToPermalink(); - function createMDXLoaderRule(): RuleSetRule { + function createMDXLoader(): RuleSetUseItem { const contentDirs = versionsMetadata .flatMap(getContentPathList) // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 @@ -339,7 +339,7 @@ export default async function pluginContentDocs( }, }, module: { - rules: [createMDXLoaderRule()], + rules: [createMDXLoader()], }, }; }, diff --git a/packages/docusaurus-plugin-content-pages/src/index.ts b/packages/docusaurus-plugin-content-pages/src/index.ts index a9f7b3464293..f32dd74b74c4 100644 --- a/packages/docusaurus-plugin-content-pages/src/index.ts +++ b/packages/docusaurus-plugin-content-pages/src/index.ts @@ -26,6 +26,8 @@ import type { LoadedContent, PageFrontMatter, } from '@docusaurus/plugin-content-pages'; +import type {RuleSetUseItem} from 'webpack'; +import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader/lib/loader'; export default function pluginContentPages( context: LoadContext, @@ -74,6 +76,42 @@ export default function pluginContentPages( beforeDefaultRemarkPlugins, } = options; const contentDirs = getContentPathList(contentPaths); + + function createMDXLoader(): RuleSetUseItem { + const loaderOptions: MDXLoaderOptions = { + admonitions, + remarkPlugins, + rehypePlugins, + beforeDefaultRehypePlugins, + beforeDefaultRemarkPlugins, + staticDirs: siteConfig.staticDirectories.map((dir) => + path.resolve(siteDir, dir), + ), + siteDir, + isMDXPartial: createAbsoluteFilePathMatcher( + options.exclude, + contentDirs, + ), + metadataPath: (mdxPath: string) => { + // Note that metadataPath must be the same/in-sync as + // the path from createData for each MDX. + const aliasedSource = aliasedSitePath(mdxPath, siteDir); + return path.join(dataDir, `${docuHash(aliasedSource)}.json`); + }, + // Assets allow to convert some relative images paths to + // require(...) calls + createAssets: ({frontMatter}: {frontMatter: PageFrontMatter}) => ({ + image: frontMatter.image, + }), + markdownConfig: siteConfig.markdown, + }; + + return { + loader: require.resolve('@docusaurus/mdx-loader'), + options: loaderOptions, + }; + } + return { module: { rules: [ @@ -82,52 +120,7 @@ export default function pluginContentPages( include: contentDirs // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 .map(addTrailingPathSeparator), - use: [ - { - loader: require.resolve('@docusaurus/mdx-loader'), - options: { - admonitions, - remarkPlugins, - rehypePlugins, - beforeDefaultRehypePlugins, - beforeDefaultRemarkPlugins, - staticDirs: siteConfig.staticDirectories.map((dir) => - path.resolve(siteDir, dir), - ), - siteDir, - isMDXPartial: createAbsoluteFilePathMatcher( - options.exclude, - contentDirs, - ), - metadataPath: (mdxPath: string) => { - // Note that metadataPath must be the same/in-sync as - // the path from createData for each MDX. - const aliasedSource = aliasedSitePath(mdxPath, siteDir); - return path.join( - dataDir, - `${docuHash(aliasedSource)}.json`, - ); - }, - // Assets allow to convert some relative images paths to - // require(...) calls - createAssets: ({ - frontMatter, - }: { - frontMatter: PageFrontMatter; - }) => ({ - image: frontMatter.image, - }), - markdownConfig: siteConfig.markdown, - }, - }, - { - loader: path.resolve(__dirname, './markdownLoader.js'), - options: { - // siteDir, - // contentPath, - }, - }, - ].filter(Boolean), + use: [createMDXLoader()], }, ], }, diff --git a/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts b/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts deleted file mode 100644 index e5c91b7bf797..000000000000 --- a/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import type {LoaderContext} from 'webpack'; - -export default function markdownLoader( - this: LoaderContext, - fileString: string, -): void { - const callback = this.async(); - - // const options = this.getOptions(); - - // TODO provide additional md processing here? like interlinking pages? - // fileString = linkify(fileString) - - return callback(null, fileString); -} From 43ab19b1ed38d5921f96db0811b0b15e322cee12 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 18:01:43 +0200 Subject: [PATCH 09/11] resolveMarkdownLinks should support link references --- .../__tests__/index.test.ts | 67 ++++-- .../src/remark/resolveMarkdownLinks/index.ts | 35 +-- .../__snapshots__/markdownUtils.test.ts.snap | 214 ++++++++++++++++++ 3 files changed, 263 insertions(+), 53 deletions(-) create mode 100644 packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap diff --git a/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts index 491d4b0aca66..00a76da679ee 100644 --- a/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts @@ -8,15 +8,11 @@ import plugin from '..'; import type {PluginOptions} from '../index'; -async function process( - content: string, - pluginOptions?: Partial, -) { +async function process(content: string) { const {remark} = await import('remark'); const options: PluginOptions = { - resolveMarkdownLink: ({linkPathname}) => `RESOLVED---${linkPathname}`, - ...pluginOptions, + resolveMarkdownLink: ({linkPathname}) => `/RESOLVED---${linkPathname}`, }; const result = await remark().use(plugin, options).process(content); @@ -29,37 +25,35 @@ describe('resolveMarkdownLinks remark plugin', () => { /* language=markdown */ const content = `[link1](link1.mdx) - [link2](../myLink2.md) [link3](myLink3.md) +[link2](../myLink2.md) [link3](myLink3.md) - [link4](../myLink4.mdx?qs#hash) [link5](./../my/great/link5.md?#) +[link4](../myLink4.mdx?qs#hash) [link5](./../my/great/link5.md?#) - [link6](../myLink6.mdx?qs#hash) +[link6](../myLink6.mdx?qs#hash) - [link7]() +[link7]() - [link8](/link8.md) +[link8](/link8.md) - [**link** \`9\`](/link9.md) +[**link** \`9\`](/link9.md) `; const result = await process(content); expect(result).toMatchInlineSnapshot(` - "[link1](RESOLVED---link1.mdx) + "[link1](/RESOLVED---link1.mdx) - \`\`\` - [link2](../myLink2.md) [link3](myLink3.md) + [link2](/RESOLVED---../myLink2.md) [link3](/RESOLVED---myLink3.md) - [link4](../myLink4.mdx?qs#hash) [link5](./../my/great/link5.md?#) + [link4](/RESOLVED---../myLink4.mdx?qs#hash) [link5](/RESOLVED---./../my/great/link5.md?#) - [link6](../myLink6.mdx?qs#hash) + [link6](/RESOLVED---../myLink6.mdx?qs#hash) - [link7]() + [link7]() - [link8](/link8.md) + [link8](/RESOLVED---/link8.md) - [**link** \`9\`](/link9.md) - \`\`\` + [**link** \`9\`](/RESOLVED---/link9.md) " `); }); @@ -132,4 +126,35 @@ this is a code block expect(result).toEqual(content); }); + + it('supports link references', async () => { + /* language=markdown */ + const content = `Testing some link refs: + +* [link-ref1] +* [link-ref2] +* [link-ref3] + +[link-ref1]: target.mdx +[link-ref2]: ./target.mdx +[link-ref3]: ../links/target.mdx?qs#target-heading + `; + + const result = await process(content); + + expect(result).toMatchInlineSnapshot(` + "Testing some link refs: + + * [link-ref1] + * [link-ref2] + * [link-ref3] + + [link-ref1]: /RESOLVED---target.mdx + + [link-ref2]: /RESOLVED---./target.mdx + + [link-ref3]: /RESOLVED---../links/target.mdx?qs#target-heading + " + `); + }); }); diff --git a/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts index 4edd10fd01fe..420caa67f7a4 100644 --- a/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts @@ -10,11 +10,10 @@ import { serializeURLPath, type URLPath, } from '@docusaurus/utils'; -import {stringifyContent} from '../utils'; // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721 import type {Transformer} from 'unified'; -import type {Link} from 'mdast'; +import type {Definition, Link} from 'mdast'; type ResolveMarkdownLinkParams = { /** @@ -64,17 +63,6 @@ function parseMarkdownLinkURLPath(link: string): URLPath | null { return urlPath; } -type BrokenMarkdownLink = { - /** - * Absolute path to the file containing this Markdown link. - */ - filePath: string; - /** - * The broken Markdown link - */ - link: Link; -}; - /** * A remark plugin to extract the h1 heading found in Markdown files * This is exposed as "data.contentTitle" to the processed vfile @@ -83,13 +71,10 @@ type BrokenMarkdownLink = { const plugin: Plugin = function plugin(options: PluginOptions): Transformer { const {resolveMarkdownLink} = options; return async (root, file) => { - const {toString} = await import('mdast-util-to-string'); - const {visit} = await import('unist-util-visit'); - const brokenMarkdownLinks: BrokenMarkdownLink[] = []; - - visit(root, 'link', (link: Link) => { + visit(root, ['link', 'definition'], (node) => { + const link = node as unknown as Link | Definition; const linkURLPath = parseMarkdownLinkURLPath(link.url); if (!linkURLPath) { return; @@ -106,23 +91,9 @@ const plugin: Plugin = function plugin(options: PluginOptions): Transformer { ...linkURLPath, pathname: permalink, }); - // console.log(`✅ Markdown link resolved: ${link.url} => ${resolvedUrl}`); link.url = resolvedUrl; - } else { - const linkContent = stringifyContent(link, toString); - console.log(`❌ Markdown link broken: [${linkContent}](${link.url})`); - brokenMarkdownLinks.push({ - filePath: file.path, - link, - }); } }); - - if (brokenMarkdownLinks.length > 0) { - console.log( - `❌ ${brokenMarkdownLinks.length} broken Markdown links for ${file.path}\n`, - ); - } }; }; diff --git a/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap b/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap new file mode 100644 index 000000000000..8fb7a03dfa2f --- /dev/null +++ b/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap @@ -0,0 +1,214 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parseMarkdownFile deletes only first heading 1`] = ` +{ + "content": "# Markdown Title + +test test test # test bar + +# Markdown Title 2 + +### Markdown Title h3", + "contentTitle": "Markdown Title", + "excerpt": "test test test # test bar", + "frontMatter": {}, +} +`; + +exports[`parseMarkdownFile deletes only first heading 2 1`] = ` +{ + "content": "# test + +test test test test test test +test test test # test bar +# test2 +### test +test3", + "contentTitle": "test", + "excerpt": "test test test test test test", + "frontMatter": {}, +} +`; + +exports[`parseMarkdownFile does not warn for duplicate title if markdown title is not at the top 1`] = ` +{ + "content": "foo + +# Markdown Title", + "contentTitle": undefined, + "excerpt": "foo", + "frontMatter": { + "title": "Frontmatter title", + }, +} +`; + +exports[`parseMarkdownFile handles code blocks 1`] = ` +{ + "content": "\`\`\`js +code +\`\`\` + +Content", + "contentTitle": undefined, + "excerpt": "Content", + "frontMatter": {}, +} +`; + +exports[`parseMarkdownFile handles code blocks 2`] = ` +{ + "content": "\`\`\`\`js +Foo +\`\`\`diff +code +\`\`\` +Bar +\`\`\`\` + +Content", + "contentTitle": undefined, + "excerpt": "Content", + "frontMatter": {}, +} +`; + +exports[`parseMarkdownFile handles code blocks 3`] = ` +{ + "content": "\`\`\`\`js +Foo +\`\`\`diff +code +\`\`\`\` + +Content", + "contentTitle": undefined, + "excerpt": "Content", + "frontMatter": {}, +} +`; + +exports[`parseMarkdownFile ignores markdown title if its not a first text 1`] = ` +{ + "content": "foo +# test", + "contentTitle": undefined, + "excerpt": "foo", + "frontMatter": {}, +} +`; + +exports[`parseMarkdownFile parse markdown with custom front matter parser 1`] = ` +{ + "content": "Some text", + "contentTitle": undefined, + "excerpt": "Some text", + "frontMatter": { + "age": 84, + "extra": "value", + "great": true, + "title": "Frontmatter title", + }, +} +`; + +exports[`parseMarkdownFile parse markdown with front matter 1`] = ` +{ + "content": "Some text", + "contentTitle": undefined, + "excerpt": "Some text", + "frontMatter": { + "title": "Frontmatter title", + }, +} +`; + +exports[`parseMarkdownFile parses first heading as contentTitle 1`] = ` +{ + "content": "# Markdown Title + +Some text", + "contentTitle": "Markdown Title", + "excerpt": "Some text", + "frontMatter": {}, +} +`; + +exports[`parseMarkdownFile parses front-matter and ignore h2 1`] = ` +{ + "content": "## test", + "contentTitle": undefined, + "excerpt": "test", + "frontMatter": { + "title": "Frontmatter title", + }, +} +`; + +exports[`parseMarkdownFile parses title only 1`] = ` +{ + "content": "# test", + "contentTitle": "test", + "excerpt": undefined, + "frontMatter": {}, +} +`; + +exports[`parseMarkdownFile parses title only alternate 1`] = ` +{ + "content": "test +===", + "contentTitle": "test", + "excerpt": undefined, + "frontMatter": {}, +} +`; + +exports[`parseMarkdownFile reads front matter only 1`] = ` +{ + "content": "", + "contentTitle": undefined, + "excerpt": undefined, + "frontMatter": { + "title": "test", + }, +} +`; + +exports[`parseMarkdownFile warns about duplicate titles (front matter + markdown alternate) 1`] = ` +{ + "content": "Markdown Title alternate +================ + +Some text", + "contentTitle": "Markdown Title alternate", + "excerpt": "Some text", + "frontMatter": { + "title": "Frontmatter title", + }, +} +`; + +exports[`parseMarkdownFile warns about duplicate titles (front matter + markdown) 1`] = ` +{ + "content": "# Markdown Title + +Some text", + "contentTitle": "Markdown Title", + "excerpt": "Some text", + "frontMatter": { + "title": "Frontmatter title", + }, +} +`; + +exports[`parseMarkdownFile warns about duplicate titles 1`] = ` +{ + "content": "# test", + "contentTitle": "test", + "excerpt": undefined, + "frontMatter": { + "title": "Frontmatter title", + }, +} +`; From 98e393fb0aa9b3c0ba8ca996ba51c1b66b7f9289 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 18:31:22 +0200 Subject: [PATCH 10/11] fixes --- .../src/index.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 67054ff7da8b..baf8b8a4f05a 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -252,6 +252,11 @@ export default async function pluginContentDocs( beforeDefaultRemarkPlugins, } = options; + const contentDirs = versionsMetadata + .flatMap(getContentPathList) + // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 + .map(addTrailingPathSeparator); + // TODO this does not re-run when content gets updated in dev! // it's probably better to restore a mutable cache in the plugin function getSourceToPermalink(): SourceToPermalink { @@ -263,11 +268,6 @@ export default async function pluginContentDocs( const sourceToPermalink = getSourceToPermalink(); function createMDXLoader(): RuleSetUseItem { - const contentDirs = versionsMetadata - .flatMap(getContentPathList) - // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 - .map(addTrailingPathSeparator); - const loaderOptions: MDXLoaderOptions = { admonitions: options.admonitions, remarkPlugins, @@ -315,14 +315,8 @@ export default async function pluginContentDocs( }; return { - test: /\.mdx?$/i, - include: contentDirs, - use: [ - { - loader: require.resolve('@docusaurus/mdx-loader'), - options: loaderOptions, - }, - ].filter(Boolean), + loader: require.resolve('@docusaurus/mdx-loader'), + options: loaderOptions, }; } @@ -339,7 +333,13 @@ export default async function pluginContentDocs( }, }, module: { - rules: [createMDXLoader()], + rules: [ + { + test: /\.mdx?$/i, + include: contentDirs, + use: [createMDXLoader()], + }, + ], }, }; }, From ebe2aefee5c315a57f3071bbf92b316de9e06896 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 24 May 2024 18:47:03 +0200 Subject: [PATCH 11/11] polish before merge --- .../src/remark/resolveMarkdownLinks/index.ts | 4 ---- packages/docusaurus-plugin-content-docs/src/versions/index.ts | 4 ---- packages/docusaurus-utils/src/markdownLinks.ts | 3 +++ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts index 420caa67f7a4..0eeecb06a4d2 100644 --- a/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts @@ -31,10 +31,6 @@ export type ResolveMarkdownLink = ( params: ResolveMarkdownLinkParams, ) => string | null; -// TODO: this plugin shouldn't be in the core MDX loader -// After we allow plugins to provide Remark/Rehype plugins (see -// https://github.com/facebook/docusaurus/issues/6370), this should be provided -// by theme-mermaid itself export interface PluginOptions { resolveMarkdownLink: ResolveMarkdownLink; } diff --git a/packages/docusaurus-plugin-content-docs/src/versions/index.ts b/packages/docusaurus-plugin-content-docs/src/versions/index.ts index 87eb16cab5f5..bb77f8cbbdf4 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/index.ts @@ -278,10 +278,6 @@ export function getVersionFromSourceFilePath( filePath.startsWith(docsDirPath), ), ); - // At this point, this should never happen, because the MDX loaders' paths are - // literally using the version content paths; but if we allow sourcing content - // from outside the docs directory (through the `include` option, for example; - // is there a compelling use-case?), this would actually be testable if (!versionFound) { throw new Error( `Unexpected error: file at "${filePath}" does not belong to any docs version!`, diff --git a/packages/docusaurus-utils/src/markdownLinks.ts b/packages/docusaurus-utils/src/markdownLinks.ts index 265cf28ba200..1b65776187f8 100644 --- a/packages/docusaurus-utils/src/markdownLinks.ts +++ b/packages/docusaurus-utils/src/markdownLinks.ts @@ -40,6 +40,9 @@ export type BrokenMarkdownLink = { link: string; }; +// Note this is historical logic extracted during a 2024 refactor +// The algo has been kept exactly as before for retro compatibility +// See also https://github.com/facebook/docusaurus/pull/10168 export function resolveMarkdownLinkPathname( linkPathname: string, context: {