docusaurus/packages/docusaurus-mdx-loader/src/processor.ts

277 lines
8.2 KiB
TypeScript

/**
* 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<SimpleProcessorResult>;
};
export type MDXPlugin = Pluggable;
export type MDXOptions = {
admonitions: boolean | Partial<AdmonitionOptions>;
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<SimpleProcessor> {
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<string | Options, ProcessorsCacheEntry>();
async function createProcessorsCacheEntry({
options,
}: {
options: Options;
}): Promise<ProcessorsCacheEntry> {
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<SimpleProcessor> {
const compilers = await createProcessorsCacheEntry({options});
const format = getFormat({
filePath,
frontMatterFormat: mdxFrontMatter.format,
markdownConfigFormat: options.markdownConfig.format,
});
return format === 'md' ? compilers.mdProcessor : compilers.mdxProcessor;
}