/** * 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 headings from './remark/headings'; import contentTitle from './remark/contentTitle'; import toc from './remark/toc'; import transformImage from './remark/transformImage'; import transformLinks from './remark/transformLinks'; import resolveMarkdownLinks from './remark/resolveMarkdownLinks'; import details from './remark/details'; import head from './remark/head'; import mermaid from './remark/mermaid'; import transformAdmonitions from './remark/admonitions'; import unusedDirectivesWarning from './remark/unusedDirectives'; import codeCompatPlugin from './remark/mdx1Compat/codeCompatPlugin'; import {getFormat} from './format'; import type {WebpackCompilerName} from '@docusaurus/utils'; import type {MDXFrontMatter} from './frontMatter'; import type {Options} from './loader'; import type {AdmonitionOptions} from './remark/admonitions'; // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721 import type {ProcessorOptions} from '@mdx-js/mdx'; // TODO as of April 2023, no way to import/re-export this ESM type easily :/ // This might change soon, likely after TS 5.2 // See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391 type Pluggable = any; // TODO fix this asap type SimpleProcessorResult = {content: string; data: {[key: string]: unknown}}; // TODO alt interface because impossible to import type Processor (ESM + TS :/) type SimpleProcessor = { process: ({ content, filePath, frontMatter, compilerName, }: { content: string; filePath: string; frontMatter: {[key: string]: unknown}; compilerName: WebpackCompilerName; }) => Promise; }; export type MDXPlugin = Pluggable; export type MDXOptions = { admonitions: boolean | Partial; remarkPlugins: MDXPlugin[]; rehypePlugins: MDXPlugin[]; recmaPlugins: MDXPlugin[]; beforeDefaultRemarkPlugins: MDXPlugin[]; beforeDefaultRehypePlugins: MDXPlugin[]; }; function getAdmonitionsPlugins( admonitionsOption: MDXOptions['admonitions'], ): MDXPlugin[] { if (admonitionsOption) { const plugin: MDXPlugin = admonitionsOption === true ? transformAdmonitions : [transformAdmonitions, admonitionsOption]; return [plugin]; } return []; } // Need to be async due to ESM dynamic imports... async function createProcessorFactory() { const {createProcessor: createMdxProcessor} = await import('@mdx-js/mdx'); const {default: frontmatter} = await import('remark-frontmatter'); const {default: rehypeRaw} = await import('rehype-raw'); const {default: gfm} = await import('remark-gfm'); // TODO using fork until PR merged: https://github.com/leebyron/remark-comment/pull/3 const {default: comment} = await import('@slorber/remark-comment'); const {default: directive} = await import('remark-directive'); const {VFile} = await import('vfile'); const {default: emoji} = await import('remark-emoji'); function getDefaultRemarkPlugins({options}: {options: Options}): MDXPlugin[] { return [ [ headings, {anchorsMaintainCase: options.markdownConfig.anchors.maintainCase}, ], emoji, toc, ]; } // /!\ this method is synchronous on purpose // Using async code here can create cache entry race conditions! function createProcessorSync({ options, format, }: { options: Options; format: 'md' | 'mdx'; }): SimpleProcessor { const remarkPlugins: MDXPlugin[] = [ ...(options.beforeDefaultRemarkPlugins ?? []), frontmatter, directive, [contentTitle, {removeContentTitle: options.removeContentTitle}], ...getAdmonitionsPlugins(options.admonitions ?? false), ...getDefaultRemarkPlugins({options}), details, head, ...(options.markdownConfig.mermaid ? [mermaid] : []), [ transformImage, { staticDirs: options.staticDirs, siteDir: options.siteDir, }, ], // TODO merge this with transformLinks? options.resolveMarkdownLink ? [ resolveMarkdownLinks, {resolveMarkdownLink: options.resolveMarkdownLink}, ] : undefined, [ transformLinks, { staticDirs: options.staticDirs, siteDir: options.siteDir, }, ], gfm, options.markdownConfig.mdx1Compat.comments ? comment : null, ...(options.remarkPlugins ?? []), unusedDirectivesWarning, ].filter((plugin): plugin is MDXPlugin => Boolean(plugin)); // codeCompatPlugin needs to be applied last after user-provided plugins // (after npm2yarn for example) remarkPlugins.push(codeCompatPlugin); const rehypePlugins: MDXPlugin[] = [ ...(options.beforeDefaultRehypePlugins ?? []), ...(options.rehypePlugins ?? []), ]; // Maybe we'll want to introduce default recma plugins later? // For example https://github.com/domdomegg/recma-mdx-displayname ? const recmaPlugins = [...(options.recmaPlugins ?? [])]; if (format === 'md') { // This is what permits to embed HTML elements with format 'md' // See https://github.com/facebook/docusaurus/pull/8960 // See https://github.com/mdx-js/mdx/pull/2295#issuecomment-1540085960 const rehypeRawPlugin: MDXPlugin = [ rehypeRaw, { passThrough: [ 'mdxFlowExpression', 'mdxJsxFlowElement', 'mdxJsxTextElement', 'mdxTextExpression', 'mdxjsEsm', ], }, ]; rehypePlugins.unshift(rehypeRawPlugin); } const processorOptions: ProcessorOptions & Options = { ...options, remarkPlugins, rehypePlugins, recmaPlugins, providerImportSource: '@mdx-js/react', }; const mdxProcessor = createMdxProcessor({ ...processorOptions, remarkRehypeOptions: options.markdownConfig.remarkRehypeOptions, format, }); return { process: async ({content, filePath, frontMatter, compilerName}) => { const vfile = new VFile({ value: content, path: filePath, data: { frontMatter, compilerName, }, }); return mdxProcessor.process(vfile).then((result) => ({ content: result.toString(), data: result.data, })); }, }; } return {createProcessorSync}; } // Will be useful for tests export async function createProcessorUncached(parameters: { options: Options; format: 'md' | 'mdx'; }): Promise { const {createProcessorSync} = await createProcessorFactory(); return createProcessorSync(parameters); } // We use different compilers depending on the file type (md vs mdx) type ProcessorsCacheEntry = { mdProcessor: SimpleProcessor; mdxProcessor: SimpleProcessor; }; // Compilers are cached so that Remark/Rehype plugins can run // expensive code during initialization const ProcessorsCache = new Map(); async function createProcessorsCacheEntry({ options, }: { options: Options; }): Promise { const {createProcessorSync} = await createProcessorFactory(); const compilers = ProcessorsCache.get(options); if (compilers) { return compilers; } const compilerCacheEntry: ProcessorsCacheEntry = { mdProcessor: createProcessorSync({ options, format: 'md', }), mdxProcessor: createProcessorSync({ options, format: 'mdx', }), }; ProcessorsCache.set(options, compilerCacheEntry); return compilerCacheEntry; } export async function createProcessorCached({ filePath, mdxFrontMatter, options, }: { filePath: string; mdxFrontMatter: MDXFrontMatter; options: Options; }): Promise { const compilers = await createProcessorsCacheEntry({options}); const format = getFormat({ filePath, frontMatterFormat: mdxFrontMatter.format, markdownConfigFormat: options.markdownConfig.format, }); return format === 'md' ? compilers.mdProcessor : compilers.mdxProcessor; }