mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-22 02:57:45 +02:00
feat: upgrade to MDX v2 (#8288)
Co-authored-by: Armano <armano2@users.noreply.github.com>
This commit is contained in:
parent
10f161d578
commit
bf913aea2a
161 changed files with 4028 additions and 2821 deletions
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* 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 escapeStringRegexp from 'escape-string-regexp';
|
||||
import {
|
||||
validateMDXFrontMatter,
|
||||
DefaultMDXFrontMatter,
|
||||
type MDXFrontMatter,
|
||||
} from '../frontMatter';
|
||||
|
||||
function testField(params: {
|
||||
prefix: string;
|
||||
validFrontMatters: MDXFrontMatter[];
|
||||
convertibleFrontMatter?: [
|
||||
ConvertibleFrontMatter: {[key: string]: unknown},
|
||||
ConvertedFrontMatter: MDXFrontMatter,
|
||||
][];
|
||||
invalidFrontMatters?: [
|
||||
InvalidFrontMatter: {[key: string]: unknown},
|
||||
ErrorMessage: string,
|
||||
][];
|
||||
}) {
|
||||
// eslint-disable-next-line jest/require-top-level-describe
|
||||
test(`[${params.prefix}] accept valid values`, () => {
|
||||
params.validFrontMatters.forEach((frontMatter) => {
|
||||
expect(validateMDXFrontMatter(frontMatter)).toEqual(frontMatter);
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/require-top-level-describe
|
||||
test(`[${params.prefix}] convert valid values`, () => {
|
||||
params.convertibleFrontMatter?.forEach(
|
||||
([convertibleFrontMatter, convertedFrontMatter]) => {
|
||||
expect(validateMDXFrontMatter(convertibleFrontMatter)).toEqual(
|
||||
convertedFrontMatter,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/require-top-level-describe
|
||||
test(`[${params.prefix}] throw error for values`, () => {
|
||||
params.invalidFrontMatters?.forEach(([frontMatter, message]) => {
|
||||
try {
|
||||
validateMDXFrontMatter(frontMatter);
|
||||
throw new Error(
|
||||
`MDX front matter is expected to be rejected, but was accepted successfully:\n ${JSON.stringify(
|
||||
frontMatter,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect((err as Error).message).toMatch(
|
||||
new RegExp(escapeStringRegexp(message)),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('MDX front matter schema', () => {
|
||||
it('accepts empty object', () => {
|
||||
const frontMatter: Partial<MDXFrontMatter> = {};
|
||||
expect(validateMDXFrontMatter(frontMatter)).toEqual(DefaultMDXFrontMatter);
|
||||
});
|
||||
|
||||
it('accepts undefined object', () => {
|
||||
expect(validateMDXFrontMatter(undefined)).toEqual(DefaultMDXFrontMatter);
|
||||
});
|
||||
|
||||
it('rejects unknown field', () => {
|
||||
const frontMatter = {abc: '1'};
|
||||
expect(() =>
|
||||
validateMDXFrontMatter(frontMatter),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""abc" is not allowed"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateDocFrontMatter format', () => {
|
||||
testField({
|
||||
prefix: 'format',
|
||||
validFrontMatters: [{format: 'md'}, {format: 'mdx'}],
|
||||
invalidFrontMatters: [
|
||||
[{format: 'xdm'}, '"format" must be one of [md, mdx, detect]'],
|
||||
],
|
||||
});
|
||||
});
|
31
packages/docusaurus-mdx-loader/src/deps.d.ts
vendored
31
packages/docusaurus-mdx-loader/src/deps.d.ts
vendored
|
@ -1,31 +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.
|
||||
*/
|
||||
|
||||
// TODO Types provided by MDX 2.0 https://github.com/mdx-js/mdx/blob/main/packages/mdx/types/index.d.ts
|
||||
declare module '@mdx-js/mdx' {
|
||||
import type {Processor, Plugin} from 'unified';
|
||||
|
||||
type MDXPlugin =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[Plugin<any[]>, any] | Plugin<any[]>;
|
||||
|
||||
type Options = {
|
||||
filepath?: string;
|
||||
skipExport?: boolean;
|
||||
wrapExport?: string;
|
||||
remarkPlugins?: MDXPlugin[];
|
||||
rehypePlugins?: MDXPlugin[];
|
||||
};
|
||||
|
||||
export function sync(content: string, options?: Options): string;
|
||||
export function createMdxAstCompiler(options?: Options): Processor;
|
||||
export function createCompiler(options?: Options): Processor;
|
||||
export default function mdx(
|
||||
content: string,
|
||||
options?: Options,
|
||||
): Promise<string>;
|
||||
}
|
31
packages/docusaurus-mdx-loader/src/frontMatter.ts
Normal file
31
packages/docusaurus-mdx-loader/src/frontMatter.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* 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 {
|
||||
JoiFrontMatter as Joi,
|
||||
validateFrontMatter,
|
||||
} from '@docusaurus/utils-validation';
|
||||
|
||||
export type MDXFrontMatter = {
|
||||
format: 'md' | 'mdx' | 'detect';
|
||||
};
|
||||
|
||||
export const DefaultMDXFrontMatter: MDXFrontMatter = {
|
||||
format: 'detect',
|
||||
};
|
||||
|
||||
const MDXFrontMatterSchema = Joi.object<MDXFrontMatter>({
|
||||
format: Joi.string()
|
||||
.equal('md', 'mdx', 'detect')
|
||||
.default(DefaultMDXFrontMatter.format),
|
||||
}).default(DefaultMDXFrontMatter);
|
||||
|
||||
export function validateMDXFrontMatter(frontMatter: unknown): MDXFrontMatter {
|
||||
return validateFrontMatter(frontMatter, MDXFrontMatterSchema, {
|
||||
allowUnknown: false,
|
||||
});
|
||||
}
|
|
@ -7,13 +7,11 @@
|
|||
|
||||
import {mdxLoader} from './loader';
|
||||
|
||||
import type {TOCItem as TOCItemImported} from './remark/toc';
|
||||
|
||||
export default mdxLoader;
|
||||
|
||||
export type TOCItem = {
|
||||
readonly value: string;
|
||||
readonly id: string;
|
||||
readonly level: number;
|
||||
};
|
||||
export type TOCItem = TOCItemImported;
|
||||
|
||||
export type LoadedMDXContent<FrontMatter, Metadata, Assets = undefined> = {
|
||||
/** As verbatim declared in the MDX document. */
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {
|
||||
parseFrontMatter,
|
||||
|
@ -13,46 +14,73 @@ import {
|
|||
escapePath,
|
||||
getFileLoaderUtils,
|
||||
} from '@docusaurus/utils';
|
||||
import {createCompiler} from '@mdx-js/mdx';
|
||||
import emoji from 'remark-emoji';
|
||||
import stringifyObject from 'stringify-object';
|
||||
|
||||
import preprocessor from './preprocessor';
|
||||
import headings from './remark/headings';
|
||||
import toc from './remark/toc';
|
||||
import unwrapMdxCodeBlocks from './remark/unwrapMdxCodeBlocks';
|
||||
import transformImage from './remark/transformImage';
|
||||
import transformLinks from './remark/transformLinks';
|
||||
import details from './remark/details';
|
||||
import head from './remark/head';
|
||||
import mermaid from './remark/mermaid';
|
||||
|
||||
import transformAdmonitions from './remark/admonitions';
|
||||
import codeCompatPlugin from './remark/mdx1Compat/codeCompatPlugin';
|
||||
import {validateMDXFrontMatter} from './frontMatter';
|
||||
|
||||
import type {MarkdownConfig} from '@docusaurus/types';
|
||||
import type {LoaderContext} from 'webpack';
|
||||
import type {Processor, Plugin} from 'unified';
|
||||
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Processor} from 'unified';
|
||||
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
|
||||
|
||||
// Copied from https://mdxjs.com/packages/mdx/#optionsmdextensions
|
||||
// Although we are likely to only use .md / .mdx anyway...
|
||||
const mdFormatExtensions = [
|
||||
'.md',
|
||||
'.markdown',
|
||||
'.mdown',
|
||||
'.mkdn',
|
||||
'.mkd',
|
||||
'.mdwn',
|
||||
'.mkdown',
|
||||
'.ron',
|
||||
];
|
||||
|
||||
function isMDFormat(filepath: string) {
|
||||
return mdFormatExtensions.includes(path.extname(filepath));
|
||||
}
|
||||
|
||||
const {
|
||||
loaders: {inlineMarkdownImageFileLoader},
|
||||
} = getFileLoaderUtils();
|
||||
|
||||
const pragma = `
|
||||
/* @jsxRuntime classic */
|
||||
/* @jsx mdx */
|
||||
/* @jsxFrag React.Fragment */
|
||||
`;
|
||||
|
||||
const DEFAULT_OPTIONS: MDXOptions = {
|
||||
admonitions: true,
|
||||
rehypePlugins: [],
|
||||
remarkPlugins: [unwrapMdxCodeBlocks, emoji, headings, toc],
|
||||
remarkPlugins: [emoji, headings, toc],
|
||||
beforeDefaultRemarkPlugins: [],
|
||||
beforeDefaultRehypePlugins: [],
|
||||
};
|
||||
|
||||
const compilerCache = new Map<string | Options, [Processor, Options]>();
|
||||
type CompilerCacheEntry = {
|
||||
mdCompiler: Processor;
|
||||
mdxCompiler: Processor;
|
||||
options: Options;
|
||||
};
|
||||
|
||||
export type MDXPlugin =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[Plugin<any[]>, any] | Plugin<any[]>;
|
||||
const compilerCache = new Map<string | Options, CompilerCacheEntry>();
|
||||
|
||||
export type MDXPlugin = Pluggable;
|
||||
|
||||
export type MDXOptions = {
|
||||
admonitions: boolean | Partial<AdmonitionOptions>;
|
||||
|
@ -149,9 +177,21 @@ function getAdmonitionsPlugins(
|
|||
: [transformAdmonitions, admonitionsOption];
|
||||
return [plugin];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO temporary, remove this after v3.1?
|
||||
// Some plugin authors use our mdx-loader, despite it not being public API
|
||||
// see https://github.com/facebook/docusaurus/issues/8298
|
||||
function ensureMarkdownConfig(reqOptions: Options) {
|
||||
if (!reqOptions.markdownConfig) {
|
||||
throw new Error(
|
||||
'Docusaurus v3+ requires MDX loader options.markdownConfig - plugin authors using the MDX loader should make sure to provide that option',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function mdxLoader(
|
||||
this: LoaderContext<Options>,
|
||||
fileString: string,
|
||||
|
@ -159,21 +199,47 @@ export async function mdxLoader(
|
|||
const callback = this.async();
|
||||
const filePath = this.resourcePath;
|
||||
const reqOptions = this.getOptions();
|
||||
ensureMarkdownConfig(reqOptions);
|
||||
|
||||
const {createProcessor} = await import('@mdx-js/mdx');
|
||||
const {default: gfm} = await import('remark-gfm');
|
||||
const {default: comment} = await import('remark-comment');
|
||||
const {default: directives} = await import('remark-directive');
|
||||
|
||||
const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString);
|
||||
const mdxFrontMatter = validateMDXFrontMatter(frontMatter.mdx);
|
||||
|
||||
const {content, contentTitle} = parseMarkdownContentTitle(contentWithTitle, {
|
||||
removeContentTitle: reqOptions.removeContentTitle,
|
||||
const {content: contentUnprocessed, contentTitle} = parseMarkdownContentTitle(
|
||||
contentWithTitle,
|
||||
{
|
||||
removeContentTitle: reqOptions.removeContentTitle,
|
||||
},
|
||||
);
|
||||
|
||||
const content = preprocessor({
|
||||
fileContent: contentUnprocessed,
|
||||
filePath,
|
||||
admonitions: reqOptions.admonitions,
|
||||
markdownConfig: reqOptions.markdownConfig,
|
||||
});
|
||||
|
||||
const hasFrontMatter = Object.keys(frontMatter).length > 0;
|
||||
|
||||
if (!compilerCache.has(this.query)) {
|
||||
/*
|
||||
/!\ DO NOT PUT ANY ASYNC / AWAIT / DYNAMIC IMPORTS HERE
|
||||
This creates cache creation race conditions
|
||||
TODO extract this in a synchronous method
|
||||
*/
|
||||
|
||||
const remarkPlugins: MDXPlugin[] = [
|
||||
...(reqOptions.beforeDefaultRemarkPlugins ?? []),
|
||||
directives,
|
||||
...getAdmonitionsPlugins(reqOptions.admonitions ?? false),
|
||||
...DEFAULT_OPTIONS.remarkPlugins,
|
||||
...(reqOptions.markdownConfig?.mermaid ? [mermaid] : []),
|
||||
details,
|
||||
head,
|
||||
...(reqOptions.markdownConfig.mermaid ? [mermaid] : []),
|
||||
[
|
||||
transformImage,
|
||||
{
|
||||
|
@ -188,8 +254,14 @@ export async function mdxLoader(
|
|||
siteDir: reqOptions.siteDir,
|
||||
},
|
||||
],
|
||||
gfm,
|
||||
reqOptions.markdownConfig.mdx1Compat.comments ? comment : null,
|
||||
...(reqOptions.remarkPlugins ?? []),
|
||||
];
|
||||
].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[] = [
|
||||
...(reqOptions.beforeDefaultRehypePlugins ?? []),
|
||||
|
@ -197,26 +269,60 @@ export async function mdxLoader(
|
|||
...(reqOptions.rehypePlugins ?? []),
|
||||
];
|
||||
|
||||
const options: Options = {
|
||||
const options: ProcessorOptions & Options = {
|
||||
...reqOptions,
|
||||
remarkPlugins,
|
||||
rehypePlugins,
|
||||
providerImportSource: '@mdx-js/react',
|
||||
};
|
||||
compilerCache.set(this.query, [createCompiler(options), options]);
|
||||
|
||||
const compilerCacheEntry: CompilerCacheEntry = {
|
||||
mdCompiler: createProcessor({
|
||||
...options,
|
||||
format: 'md',
|
||||
}),
|
||||
mdxCompiler: createProcessor({
|
||||
...options,
|
||||
format: 'mdx',
|
||||
}),
|
||||
options,
|
||||
};
|
||||
|
||||
compilerCache.set(this.query, compilerCacheEntry);
|
||||
}
|
||||
|
||||
const [compiler, options] = compilerCache.get(this.query)!;
|
||||
const {mdCompiler, mdxCompiler, options} = compilerCache.get(this.query)!;
|
||||
|
||||
function getCompiler() {
|
||||
const format =
|
||||
mdxFrontMatter.format === 'detect'
|
||||
? isMDFormat(filePath)
|
||||
? 'md'
|
||||
: 'mdx'
|
||||
: mdxFrontMatter.format;
|
||||
|
||||
return format === 'md' ? mdCompiler : mdxCompiler;
|
||||
}
|
||||
|
||||
let result: string;
|
||||
try {
|
||||
result = await compiler
|
||||
result = await getCompiler()
|
||||
.process({
|
||||
contents: content,
|
||||
path: this.resourcePath,
|
||||
value: content,
|
||||
path: filePath,
|
||||
})
|
||||
.then((res) => res.toString());
|
||||
} catch (err) {
|
||||
return callback(err as Error);
|
||||
} catch (errorUnknown) {
|
||||
const error = errorUnknown as Error;
|
||||
return callback(
|
||||
new Error(
|
||||
`MDX compilation failed for file ${logger.path(filePath)}\nCause: ${
|
||||
error.message
|
||||
}\nDetails:\n${JSON.stringify(error, null, 2)}`,
|
||||
// TODO error cause doesn't seem to be used by Webpack stats.errors :s
|
||||
{cause: error},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// MDX partials are MDX files starting with _ or in a folder starting with _
|
||||
|
@ -265,6 +371,8 @@ ${JSON.stringify(frontMatter, null, 2)}`;
|
|||
? reqOptions.createAssets({frontMatter, metadata})
|
||||
: undefined;
|
||||
|
||||
// TODO use remark plugins to insert extra exports instead of string concat?
|
||||
// cf how the toc is exported
|
||||
const exportsCode = `
|
||||
export const frontMatter = ${stringifyObject(frontMatter)};
|
||||
export const contentTitle = ${stringifyObject(contentTitle)};
|
||||
|
@ -273,10 +381,6 @@ ${assets ? `export const assets = ${createAssetsExportCode(assets)};` : ''}
|
|||
`;
|
||||
|
||||
const code = `
|
||||
${pragma}
|
||||
import React from 'react';
|
||||
import { mdx } from '@mdx-js/react';
|
||||
|
||||
${exportsCode}
|
||||
${result}
|
||||
`;
|
||||
|
|
47
packages/docusaurus-mdx-loader/src/preprocessor.ts
Normal file
47
packages/docusaurus-mdx-loader/src/preprocessor.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* 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 {
|
||||
escapeMarkdownHeadingIds,
|
||||
unwrapMdxCodeBlocks,
|
||||
admonitionTitleToDirectiveLabel,
|
||||
} from '@docusaurus/utils';
|
||||
import {normalizeAdmonitionOptions} from './remark/admonitions';
|
||||
import type {Options} from './loader';
|
||||
|
||||
/**
|
||||
* Preprocess the string before passing it to MDX
|
||||
* This is not particularly recommended but makes it easier to upgrade to MDX 2
|
||||
*/
|
||||
export default function preprocessContent({
|
||||
fileContent: initialFileContent,
|
||||
filePath,
|
||||
markdownConfig,
|
||||
admonitions,
|
||||
}: {
|
||||
fileContent: string;
|
||||
filePath: string;
|
||||
markdownConfig: Options['markdownConfig'];
|
||||
admonitions: Options['admonitions'] | undefined;
|
||||
}): string {
|
||||
let fileContent = initialFileContent;
|
||||
if (markdownConfig.preprocessor) {
|
||||
fileContent = markdownConfig.preprocessor({
|
||||
fileContent,
|
||||
filePath,
|
||||
});
|
||||
}
|
||||
fileContent = unwrapMdxCodeBlocks(fileContent);
|
||||
if (markdownConfig.mdx1Compat.headingIds) {
|
||||
fileContent = escapeMarkdownHeadingIds(fileContent);
|
||||
}
|
||||
if (markdownConfig.mdx1Compat.admonitions && admonitions) {
|
||||
const {keywords} = normalizeAdmonitionOptions(admonitions);
|
||||
fileContent = admonitionTitleToDirectiveLabel(fileContent, keywords);
|
||||
}
|
||||
return fileContent;
|
||||
}
|
|
@ -22,25 +22,9 @@ exports[`admonitions remark plugin base 1`] = `
|
|||
<p>++++</p>"
|
||||
`;
|
||||
|
||||
exports[`admonitions remark plugin custom tag 1`] = `
|
||||
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
|
||||
<p>:::info Sample Title</p>
|
||||
<p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p>
|
||||
<p>:::</p>
|
||||
<h2>Initial setup {#initial-setup}</h2>
|
||||
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
|
||||
<p>:::tip</p>
|
||||
<p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p>
|
||||
<p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p>
|
||||
<p>:::</p>
|
||||
<admonition type="tip"><p>Admonition with different syntax</p></admonition>"
|
||||
`;
|
||||
|
||||
exports[`admonitions remark plugin default behavior for custom keyword 1`] = `
|
||||
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
|
||||
<p>:::info Sample Title</p>
|
||||
<p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p>
|
||||
<p>:::</p>
|
||||
<div><p>Sample Title</p><p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p></div>
|
||||
<h2>Initial setup {#initial-setup}</h2>
|
||||
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
|
||||
<admonition type="tip"><p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p><p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p></admonition>
|
||||
|
@ -61,9 +45,7 @@ exports[`admonitions remark plugin nesting 1`] = `
|
|||
|
||||
exports[`admonitions remark plugin replace custom keyword 1`] = `
|
||||
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
|
||||
<p>:::info Sample Title</p>
|
||||
<p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p>
|
||||
<p>:::</p>
|
||||
<div><p>Sample Title</p><p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p></div>
|
||||
<h2>Initial setup {#initial-setup}</h2>
|
||||
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
|
||||
<admonition type="tip"><p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p><p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p></admonition>
|
||||
|
|
|
@ -6,40 +6,68 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import remark from 'remark';
|
||||
import remark2rehype from 'remark-rehype';
|
||||
import stringify from 'rehype-stringify';
|
||||
|
||||
import vfile from 'to-vfile';
|
||||
import plugin from '../index';
|
||||
import preprocessor from '../../../preprocessor';
|
||||
import plugin, {DefaultAdmonitionOptions} from '../index';
|
||||
import type {AdmonitionOptions} from '../index';
|
||||
|
||||
const processFixture = async (
|
||||
name: string,
|
||||
options?: Partial<AdmonitionOptions>,
|
||||
) => {
|
||||
const {remark} = await import('remark');
|
||||
const {default: directives} = await import('remark-directive');
|
||||
|
||||
const filePath = path.join(__dirname, '__fixtures__', `${name}.md`);
|
||||
const file = await vfile.read(filePath);
|
||||
const fileContentPreprocessed = preprocessor({
|
||||
fileContent: file.toString(),
|
||||
filePath,
|
||||
admonitions: DefaultAdmonitionOptions,
|
||||
markdownConfig: {
|
||||
mermaid: false,
|
||||
mdx1Compat: {
|
||||
admonitions: true,
|
||||
comments: false,
|
||||
headingIds: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
// TODO we shouldn't use rehype in these tests
|
||||
// this requires to re-implement admonitions with mdxJsxFlowElement
|
||||
const {default: mdx} = await import('remark-mdx');
|
||||
const result = await remark()
|
||||
.use(directives)
|
||||
.use(plugin)
|
||||
.use(mdx)
|
||||
.process(fileContentPreprocessed);
|
||||
return result.value;
|
||||
*/
|
||||
|
||||
const result = await remark()
|
||||
.use(directives)
|
||||
.use(plugin, options)
|
||||
.use(remark2rehype)
|
||||
.use(stringify)
|
||||
.process(file);
|
||||
.process(fileContentPreprocessed);
|
||||
|
||||
return result.toString();
|
||||
return result.value;
|
||||
};
|
||||
|
||||
describe('admonitions remark plugin', () => {
|
||||
it('base', async () => {
|
||||
const result = await processFixture('base');
|
||||
expect(result).toMatchSnapshot();
|
||||
await expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('default behavior for custom keyword', async () => {
|
||||
const result = await processFixture('base', {
|
||||
keywords: ['tip'],
|
||||
// extendDefaults: false, // By default we don't extend
|
||||
extendDefaults: undefined, // By default we extend
|
||||
});
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
@ -60,13 +88,6 @@ describe('admonitions remark plugin', () => {
|
|||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('custom tag', async () => {
|
||||
const result = await processFixture('base', {
|
||||
tag: '++++',
|
||||
});
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('interpolation', async () => {
|
||||
const result = await processFixture('interpolation');
|
||||
expect(result).toMatchSnapshot();
|
||||
|
|
|
@ -4,25 +4,26 @@
|
|||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import visit from 'unist-util-visit';
|
||||
import type {Transformer, Processor, Plugin} from 'unified';
|
||||
import type {Literal} from 'mdast';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer, Processor} from 'unified';
|
||||
|
||||
const NEWLINE = '\n';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {ContainerDirective} from 'mdast-util-directive';
|
||||
import type {Parent} from 'mdast';
|
||||
|
||||
// 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
|
||||
// import type {Plugin} from 'unified';
|
||||
type Plugin = any; // TODO fix this asap
|
||||
|
||||
// TODO not ideal option shape
|
||||
// First let upgrade to MDX 2.0
|
||||
// Maybe we'll want to provide different tags for different admonition types?
|
||||
// Also maybe rename "keywords" to "types"?
|
||||
export type AdmonitionOptions = {
|
||||
tag: string;
|
||||
keywords: string[];
|
||||
extendDefaults: boolean;
|
||||
};
|
||||
|
||||
export const DefaultAdmonitionOptions: AdmonitionOptions = {
|
||||
tag: ':::',
|
||||
keywords: [
|
||||
'secondary',
|
||||
'info',
|
||||
|
@ -34,16 +35,16 @@ export const DefaultAdmonitionOptions: AdmonitionOptions = {
|
|||
'important',
|
||||
'caution',
|
||||
],
|
||||
extendDefaults: false, // TODO make it true by default: breaking change
|
||||
extendDefaults: true,
|
||||
};
|
||||
|
||||
function escapeRegExp(s: string): string {
|
||||
return s.replace(/[-[\]{}()*+?.\\^$|/]/g, '\\$&');
|
||||
}
|
||||
|
||||
function normalizeOptions(
|
||||
providedOptions: Partial<AdmonitionOptions>,
|
||||
export function normalizeAdmonitionOptions(
|
||||
providedOptions: Partial<AdmonitionOptions> | true,
|
||||
): AdmonitionOptions {
|
||||
if (providedOptions === true) {
|
||||
return DefaultAdmonitionOptions;
|
||||
}
|
||||
|
||||
const options = {...DefaultAdmonitionOptions, ...providedOptions};
|
||||
|
||||
// By default it makes more sense to append keywords to the default ones
|
||||
|
@ -58,181 +59,84 @@ function normalizeOptions(
|
|||
return options;
|
||||
}
|
||||
|
||||
// This string value does not matter much
|
||||
// It is ignored because nodes are using hName/hProperties coming from HAST
|
||||
const admonitionNodeType = 'admonitionHTML';
|
||||
type DirectiveLabel = Parent;
|
||||
type DirectiveContent = ContainerDirective['children'];
|
||||
|
||||
function parseDirective(directive: ContainerDirective): {
|
||||
directiveLabel: DirectiveLabel | undefined;
|
||||
contentNodes: DirectiveContent;
|
||||
} {
|
||||
const hasDirectiveLabel =
|
||||
directive.children?.[0]?.data?.directiveLabel === true;
|
||||
if (hasDirectiveLabel) {
|
||||
const [directiveLabel, ...contentNodes] = directive.children;
|
||||
return {directiveLabel: directiveLabel as DirectiveLabel, contentNodes};
|
||||
}
|
||||
return {directiveLabel: undefined, contentNodes: directive.children};
|
||||
}
|
||||
|
||||
function getTextOnlyTitle(directiveLabel: DirectiveLabel): string | undefined {
|
||||
const isTextOnlyTitle =
|
||||
directiveLabel?.children?.length === 1 &&
|
||||
directiveLabel?.children?.[0]?.type === 'text';
|
||||
return isTextOnlyTitle
|
||||
? // @ts-expect-error: todo type
|
||||
(directiveLabel?.children?.[0].value as string)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const plugin: Plugin = function plugin(
|
||||
this: Processor,
|
||||
optionsInput: Partial<AdmonitionOptions> = {},
|
||||
): Transformer {
|
||||
const options = normalizeOptions(optionsInput);
|
||||
const {keywords} = normalizeAdmonitionOptions(optionsInput);
|
||||
|
||||
const keywords = Object.values(options.keywords).map(escapeRegExp).join('|');
|
||||
const nestingChar = escapeRegExp(options.tag.slice(0, 1));
|
||||
const tag = escapeRegExp(options.tag);
|
||||
return async (root) => {
|
||||
visit(root, (node) => {
|
||||
if (node.type === 'containerDirective') {
|
||||
const directive = node as ContainerDirective;
|
||||
const isAdmonition = keywords.includes(directive.name);
|
||||
|
||||
// resolve th nesting level of an opening tag
|
||||
// ::: -> 0, :::: -> 1, ::::: -> 2 ...
|
||||
const nestingLevelRegex = new RegExp(
|
||||
`^${tag}(?<nestingLevel>${nestingChar}*)`,
|
||||
);
|
||||
|
||||
const regex = new RegExp(`${tag}${nestingChar}*(${keywords})(?: *(.*))?\n`);
|
||||
const escapeTag = new RegExp(
|
||||
escapeRegExp(`\\${options.tag}${options.tag.slice(0, 1)}*`),
|
||||
'g',
|
||||
);
|
||||
|
||||
// The tokenizer is called on blocks to determine if there is an admonition
|
||||
// present and create tags for it
|
||||
function blockTokenizer(this: any, eat: any, value: string, silent: boolean) {
|
||||
// Stop if no match or match does not start at beginning of line
|
||||
const match = regex.exec(value);
|
||||
if (!match || match.index !== 0) {
|
||||
return false;
|
||||
}
|
||||
// If silent return the match
|
||||
if (silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = eat.now();
|
||||
const [opening, keyword, title] = match as string[] as [
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
const food = [];
|
||||
const content = [];
|
||||
// get the nesting level of the opening tag
|
||||
const openingLevel =
|
||||
nestingLevelRegex.exec(opening)!.groups!.nestingLevel!.length;
|
||||
// used as a stack to keep track of nested admonitions
|
||||
const nestingLevels: number[] = [openingLevel];
|
||||
|
||||
let newValue = value;
|
||||
// consume lines until a closing tag
|
||||
let idx = newValue.indexOf(NEWLINE);
|
||||
while (idx !== -1) {
|
||||
// grab this line and eat it
|
||||
const next = newValue.indexOf(NEWLINE, idx + 1);
|
||||
const line =
|
||||
next !== -1 ? newValue.slice(idx + 1, next) : newValue.slice(idx + 1);
|
||||
food.push(line);
|
||||
newValue = newValue.slice(idx + 1);
|
||||
const nesting = nestingLevelRegex.exec(line);
|
||||
idx = newValue.indexOf(NEWLINE);
|
||||
if (!nesting) {
|
||||
content.push(line);
|
||||
continue;
|
||||
}
|
||||
const tagLevel = nesting.groups!.nestingLevel!.length;
|
||||
// first level
|
||||
if (nestingLevels.length === 0) {
|
||||
nestingLevels.push(tagLevel);
|
||||
content.push(line);
|
||||
continue;
|
||||
}
|
||||
const currentLevel = nestingLevels[nestingLevels.length - 1]!;
|
||||
if (tagLevel < currentLevel) {
|
||||
// entering a nested admonition block
|
||||
nestingLevels.push(tagLevel);
|
||||
} else if (tagLevel === currentLevel) {
|
||||
// closing a nested admonition block
|
||||
nestingLevels.pop();
|
||||
// the closing tag is NOT part of the content
|
||||
if (nestingLevels.length === 0) {
|
||||
break;
|
||||
if (!isAdmonition) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
content.push(line);
|
||||
}
|
||||
|
||||
// consume the processed tag and replace escape sequences
|
||||
const contentString = content.join(NEWLINE).replace(escapeTag, options.tag);
|
||||
const add = eat(opening + food.join(NEWLINE));
|
||||
const {directiveLabel, contentNodes} = parseDirective(directive);
|
||||
|
||||
// parse the content in block mode
|
||||
const exit = this.enterBlock();
|
||||
const contentNodes = this.tokenizeBlock(contentString, now);
|
||||
exit();
|
||||
const textOnlyTitle =
|
||||
directive.attributes?.title ??
|
||||
(directiveLabel ? getTextOnlyTitle(directiveLabel) : undefined);
|
||||
|
||||
const titleNodes = this.tokenizeInline(title, now);
|
||||
|
||||
const isSimpleTextTitle =
|
||||
titleNodes.length === 1 && titleNodes[0].type === 'text';
|
||||
|
||||
const element = {
|
||||
type: admonitionNodeType,
|
||||
data: {
|
||||
// hName/hProperties come from HAST
|
||||
// Transform the mdast directive node to a hast admonition node
|
||||
// See https://github.com/syntax-tree/mdast-util-to-hast#fields-on-nodes
|
||||
hName: 'admonition',
|
||||
hProperties: {
|
||||
...(title && isSimpleTextTitle && {title}),
|
||||
type: keyword,
|
||||
},
|
||||
},
|
||||
children: [
|
||||
// For titles containing MDX syntax: create a custom element. The theme
|
||||
// component will extract it and render it nicely.
|
||||
//
|
||||
// Temporary workaround, because it's complex in MDX v1 to emit
|
||||
// interpolated JSX prop syntax (title={<>my <code>title</code></>}).
|
||||
// For this reason, we use children instead of the title prop.
|
||||
title &&
|
||||
!isSimpleTextTitle && {
|
||||
type: admonitionNodeType,
|
||||
// TODO in MDX v2 we should transform the whole directive to
|
||||
// mdxJsxFlowElement instead of using hast
|
||||
directive.data = {
|
||||
hName: 'admonition',
|
||||
hProperties: {
|
||||
...(textOnlyTitle && {title: textOnlyTitle}),
|
||||
type: directive.name,
|
||||
},
|
||||
};
|
||||
directive.children = contentNodes;
|
||||
|
||||
// TODO legacy MDX v1 <mdxAdmonitionTitle> workaround
|
||||
// v1: not possible to inject complex JSX elements as props
|
||||
// v2: now possible: use a mdxJsxFlowElement element
|
||||
if (directiveLabel && !textOnlyTitle) {
|
||||
const complexTitleNode = {
|
||||
type: 'mdxAdmonitionTitle',
|
||||
data: {
|
||||
hName: 'mdxAdmonitionTitle',
|
||||
hProperties: {},
|
||||
},
|
||||
children: titleNodes,
|
||||
},
|
||||
...contentNodes,
|
||||
].filter(Boolean),
|
||||
};
|
||||
|
||||
return add(element);
|
||||
}
|
||||
|
||||
// add tokenizer to parser after fenced code blocks
|
||||
const Parser = this.Parser.prototype;
|
||||
Parser.blockTokenizers.admonition = blockTokenizer;
|
||||
Parser.blockMethods.splice(
|
||||
Parser.blockMethods.indexOf('fencedCode') + 1,
|
||||
0,
|
||||
'admonition',
|
||||
);
|
||||
Parser.interruptParagraph.splice(
|
||||
Parser.interruptParagraph.indexOf('fencedCode') + 1,
|
||||
0,
|
||||
['admonition'],
|
||||
);
|
||||
Parser.interruptList.splice(
|
||||
Parser.interruptList.indexOf('fencedCode') + 1,
|
||||
0,
|
||||
['admonition'],
|
||||
);
|
||||
Parser.interruptBlockquote.splice(
|
||||
Parser.interruptBlockquote.indexOf('fencedCode') + 1,
|
||||
0,
|
||||
['admonition'],
|
||||
);
|
||||
|
||||
return (root) => {
|
||||
// escape everything except admonitionHTML nodes
|
||||
visit(
|
||||
root,
|
||||
(node: unknown): node is Literal =>
|
||||
(node as Literal | undefined)?.type !== admonitionNodeType,
|
||||
(node: Literal) => {
|
||||
if (node.value) {
|
||||
node.value = node.value.replace(escapeTag, options.tag);
|
||||
children: directiveLabel.children,
|
||||
};
|
||||
// @ts-expect-error: invented node type
|
||||
directive.children.unshift(complexTitleNode);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* 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 details from '..';
|
||||
|
||||
async function process(content: string) {
|
||||
const {remark} = await import('remark');
|
||||
|
||||
const {default: mdx} = await import('remark-mdx');
|
||||
|
||||
const result = await remark().use(mdx).use(details).process(content);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
describe('details remark plugin', () => {
|
||||
it("does nothing if there's no details", async () => {
|
||||
const input = `# Heading 1
|
||||
|
||||
Some content
|
||||
`;
|
||||
const result = await process(input);
|
||||
expect(result).toEqual(result);
|
||||
});
|
||||
|
||||
it('can convert details', async () => {
|
||||
const input = `# Details element example
|
||||
|
||||
<details>
|
||||
<summary>Toggle me!</summary>
|
||||
<div>
|
||||
<div>This is the detailed content</div>
|
||||
<br/>
|
||||
<details>
|
||||
<summary>
|
||||
Nested toggle! Some surprise inside...
|
||||
</summary>
|
||||
<div>
|
||||
😲😲😲😲😲
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</details>`;
|
||||
|
||||
const result = await process(input);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"# Details element example
|
||||
|
||||
<Details>
|
||||
<summary>Toggle me!</summary>
|
||||
|
||||
<div>
|
||||
<div>This is the detailed content</div>
|
||||
|
||||
<br />
|
||||
|
||||
<Details>
|
||||
<summary>
|
||||
Nested toggle! Some surprise inside...
|
||||
</summary>
|
||||
|
||||
<div>
|
||||
😲😲😲😲😲
|
||||
</div>
|
||||
</Details>
|
||||
</div>
|
||||
</Details>
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
25
packages/docusaurus-mdx-loader/src/remark/details/index.ts
Normal file
25
packages/docusaurus-mdx-loader/src/remark/details/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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 visit from 'unist-util-visit';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
|
||||
// @ts-expect-error: ES support...
|
||||
import type {MdxJsxFlowElement} from 'mdast-util-mdx';
|
||||
|
||||
// Transform <details> to <Details>
|
||||
// MDX 2 doesn't allow to substitute html elements with the provider anymore
|
||||
export default function plugin(): Transformer {
|
||||
return (root) => {
|
||||
visit(root, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => {
|
||||
if (node.name === 'details') {
|
||||
node.name = 'Details';
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* 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 head from '..';
|
||||
|
||||
async function process(content: string) {
|
||||
const {remark} = await import('remark');
|
||||
|
||||
const {default: mdx} = await import('remark-mdx');
|
||||
|
||||
const result = await remark().use(mdx).use(head).process(content);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
describe('head remark plugin', () => {
|
||||
it("does nothing if there's no details", async () => {
|
||||
const input = `# Heading
|
||||
|
||||
<head>
|
||||
<html className="some-extra-html-class" />
|
||||
<body className="other-extra-body-class" />
|
||||
<title>Head Metadata customized title!</title>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="canonical" href="https://docusaurus.io/docs/markdown-features/head-metadata" />
|
||||
</head>
|
||||
|
||||
Some content
|
||||
`;
|
||||
const result = await process(input);
|
||||
expect(result).toEqual(result);
|
||||
});
|
||||
|
||||
it('can convert head', async () => {
|
||||
const input = `# Heading
|
||||
|
||||
<head>
|
||||
<html className="some-extra-html-class" />
|
||||
<body className="other-extra-body-class" />
|
||||
<title>Head Metadata customized title!</title>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="canonical" href="https://docusaurus.io/docs/markdown-features/head-metadata" />
|
||||
</head>
|
||||
|
||||
Some content;`;
|
||||
|
||||
const result = await process(input);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"# Heading
|
||||
|
||||
<Head>
|
||||
<html className="some-extra-html-class" />
|
||||
|
||||
<body className="other-extra-body-class" />
|
||||
|
||||
<title>Head Metadata customized title!</title>
|
||||
|
||||
<meta charSet="utf-8" />
|
||||
|
||||
<meta name="twitter:card" content="summary" />
|
||||
|
||||
<link rel="canonical" href="https://docusaurus.io/docs/markdown-features/head-metadata" />
|
||||
</Head>
|
||||
|
||||
Some content;
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
25
packages/docusaurus-mdx-loader/src/remark/head/index.ts
Normal file
25
packages/docusaurus-mdx-loader/src/remark/head/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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 visit from 'unist-util-visit';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
|
||||
// @ts-expect-error: ES support...
|
||||
import type {MdxJsxFlowElement} from 'mdast-util-mdx';
|
||||
|
||||
// Transform <head> to <Head>
|
||||
// MDX 2 doesn't allow to substitute html elements with the provider anymore
|
||||
export default function plugin(): Transformer {
|
||||
return (root) => {
|
||||
visit(root, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => {
|
||||
if (node.name === 'head') {
|
||||
node.name = 'Head';
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
|
@ -7,18 +7,18 @@
|
|||
|
||||
/* Based on remark-slug (https://github.com/remarkjs/remark-slug) and gatsby-remark-autolink-headers (https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-remark-autolink-headers) */
|
||||
|
||||
import remark from 'remark';
|
||||
import u from 'unist-builder';
|
||||
import removePosition from 'unist-util-remove-position';
|
||||
import toString from 'mdast-util-to-string';
|
||||
import {toString} from 'mdast-util-to-string';
|
||||
import visit from 'unist-util-visit';
|
||||
import slug from '../index';
|
||||
import type {Plugin} from 'unified';
|
||||
import type {Parent} from 'unist';
|
||||
|
||||
function process(doc: string, plugins: Plugin[] = []) {
|
||||
const processor = remark().use({plugins: [...plugins, slug]});
|
||||
return removePosition(processor.runSync(processor.parse(doc)), true);
|
||||
async function process(doc: string, plugins: Plugin[] = []) {
|
||||
const {remark} = await import('remark');
|
||||
const processor = await remark().use({plugins: [...plugins, slug]});
|
||||
return removePosition(await processor.run(processor.parse(doc)), true);
|
||||
}
|
||||
|
||||
function heading(label: string | null, id: string) {
|
||||
|
@ -30,8 +30,8 @@ function heading(label: string | null, id: string) {
|
|||
}
|
||||
|
||||
describe('headings remark plugin', () => {
|
||||
it('patches `id`s and `data.hProperties.id', () => {
|
||||
const result = process('# Normal\n\n## Table of Contents\n\n# Baz\n');
|
||||
it('patches `id`s and `data.hProperties.id', async () => {
|
||||
const result = await process('# Normal\n\n## Table of Contents\n\n# Baz\n');
|
||||
const expected = u('root', [
|
||||
u(
|
||||
'heading',
|
||||
|
@ -57,8 +57,8 @@ describe('headings remark plugin', () => {
|
|||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('does not overwrite `data` on headings', () => {
|
||||
const result = process('# Normal\n', [
|
||||
it('does not overwrite `data` on headings', async () => {
|
||||
const result = await process('# Normal\n', [
|
||||
() => (root) => {
|
||||
(root as Parent).children[0]!.data = {foo: 'bar'};
|
||||
},
|
||||
|
@ -77,8 +77,8 @@ describe('headings remark plugin', () => {
|
|||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('does not overwrite `data.hProperties` on headings', () => {
|
||||
const result = process('# Normal\n', [
|
||||
it('does not overwrite `data.hProperties` on headings', async () => {
|
||||
const result = await process('# Normal\n', [
|
||||
() => (root) => {
|
||||
(root as Parent).children[0]!.data = {
|
||||
hProperties: {className: ['foo']},
|
||||
|
@ -99,8 +99,8 @@ describe('headings remark plugin', () => {
|
|||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('generates `id`s and `hProperties.id`s, based on `hProperties.id` if they exist', () => {
|
||||
const result = process(
|
||||
it('generates `id`s and `hProperties.id`s, based on `hProperties.id` if they exist', async () => {
|
||||
const result = await process(
|
||||
[
|
||||
'## Something',
|
||||
'## Something here',
|
||||
|
@ -152,8 +152,8 @@ describe('headings remark plugin', () => {
|
|||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('creates GitHub-style headings ids', () => {
|
||||
const result = process(
|
||||
it('creates GitHub-style headings ids', async () => {
|
||||
const result = await process(
|
||||
[
|
||||
'## I ♥ unicode',
|
||||
'',
|
||||
|
@ -223,8 +223,10 @@ describe('headings remark plugin', () => {
|
|||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('generates id from only text contents of headings if they contains HTML tags', () => {
|
||||
const result = process('# <span class="normal-header">Normal</span>\n');
|
||||
it('generates id from only text contents of headings if they contains HTML tags', async () => {
|
||||
const result = await process(
|
||||
'# <span class="normal-header">Normal</span>\n',
|
||||
);
|
||||
const expected = u('root', [
|
||||
u(
|
||||
'heading',
|
||||
|
@ -243,8 +245,8 @@ describe('headings remark plugin', () => {
|
|||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('creates custom headings ids', () => {
|
||||
const result = process(`
|
||||
it('creates custom headings ids', async () => {
|
||||
const result = await process(`
|
||||
# Heading One {#custom_h1}
|
||||
|
||||
## Heading Two {#custom-heading-two}
|
||||
|
|
|
@ -9,12 +9,14 @@
|
|||
|
||||
import {parseMarkdownHeadingId, createSlugger} from '@docusaurus/utils';
|
||||
import visit from 'unist-util-visit';
|
||||
import mdastToString from 'mdast-util-to-string';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
import type {Heading, Text} from 'mdast';
|
||||
|
||||
export default function plugin(): Transformer {
|
||||
return (root) => {
|
||||
return async (root) => {
|
||||
const {toString} = await import('mdast-util-to-string');
|
||||
|
||||
const slugs = createSlugger();
|
||||
visit(root, 'heading', (headingNode: Heading) => {
|
||||
const data = headingNode.data ?? (headingNode.data = {});
|
||||
|
@ -29,7 +31,7 @@ export default function plugin(): Transformer {
|
|||
const headingTextNodes = headingNode.children.filter(
|
||||
({type}) => !['html', 'jsx'].includes(type),
|
||||
);
|
||||
const heading = mdastToString(
|
||||
const heading = toString(
|
||||
headingTextNodes.length > 0 ? headingTextNodes : headingNode,
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 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 visit from 'unist-util-visit';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer, Processor} from 'unified';
|
||||
import type {Code} from 'mdast';
|
||||
|
||||
// Solution inspired by https://github.com/pomber/docusaurus-mdx-2/blob/main/packages/mdx-loader/src/remark/codeCompat/index.ts
|
||||
// TODO after MDX 2 we probably don't need this - remove soon?
|
||||
// Only fenced code blocks are swapped by pre/code MDX components
|
||||
// Using <pre><code> in JSX shouldn't use our MDX components anymore
|
||||
|
||||
// To make theme-classic/src/theme/MDXComponents/Pre work
|
||||
// we need to fill two properties that mdx v2 doesn't provide anymore
|
||||
export default function codeCompatPlugin(this: Processor): Transformer {
|
||||
return (root) => {
|
||||
visit(root, 'code', (node: Code) => {
|
||||
node.data = node.data || {};
|
||||
node.data.hProperties = node.data.hProperties || {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(node.data.hProperties as any).metastring = node.meta;
|
||||
|
||||
// Retrocompatible support for live codeblock metastring
|
||||
// Not really the appropriate place to handle that :s
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(node.data.hProperties as any).live = node.meta
|
||||
?.split(' ')
|
||||
.includes('live');
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`mermaid remark plugin does nothing if there's no mermaid code block 1`] = `
|
||||
"
|
||||
|
||||
|
||||
const layoutProps = {
|
||||
|
||||
};
|
||||
const MDXLayout = "wrapper"
|
||||
export default function MDXContent({
|
||||
components,
|
||||
...props
|
||||
}) {
|
||||
return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout">
|
||||
<h1>{\`Heading 1\`}</h1>
|
||||
<p>{\`No Mermaid diagram :(\`}</p>
|
||||
<pre><code parentName="pre" {...{
|
||||
"className": "language-js"
|
||||
}}>{\`this is not mermaid
|
||||
\`}</code></pre>
|
||||
</MDXLayout>;
|
||||
}
|
||||
;
|
||||
MDXContent.isMDXComponent = true;"
|
||||
`;
|
||||
|
||||
exports[`mermaid remark plugin works for basic mermaid code blocks 1`] = `
|
||||
"
|
||||
|
||||
|
||||
const layoutProps = {
|
||||
|
||||
};
|
||||
const MDXLayout = "wrapper"
|
||||
export default function MDXContent({
|
||||
components,
|
||||
...props
|
||||
}) {
|
||||
return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout">
|
||||
<h1>{\`Heading 1\`}</h1>
|
||||
<mermaid {...{
|
||||
"value": "graph TD;/n A-->B;/n A-->C;/n B-->D;/n C-->D;"
|
||||
}}></mermaid>
|
||||
</MDXLayout>;
|
||||
}
|
||||
;
|
||||
MDXContent.isMDXComponent = true;"
|
||||
`;
|
|
@ -5,19 +5,28 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {createCompiler} from '@mdx-js/mdx';
|
||||
import remark2rehype from 'remark-rehype';
|
||||
import stringify from 'rehype-stringify';
|
||||
import mermaid from '..';
|
||||
|
||||
describe('mermaid remark plugin', () => {
|
||||
function createTestCompiler() {
|
||||
return createCompiler({
|
||||
remarkPlugins: [mermaid],
|
||||
});
|
||||
}
|
||||
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 mdxCompiler = createTestCompiler();
|
||||
const result = await mdxCompiler.process(
|
||||
const result = await process(
|
||||
`# Heading 1
|
||||
|
||||
No Mermaid diagram :(
|
||||
|
@ -27,12 +36,17 @@ this is not mermaid
|
|||
\`\`\`
|
||||
`,
|
||||
);
|
||||
expect(result.contents).toMatchSnapshot();
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"<h1>Heading 1</h1>
|
||||
<p>No Mermaid diagram :(</p>
|
||||
<pre><code class="language-js">this is not mermaid
|
||||
</code></pre>"
|
||||
`);
|
||||
});
|
||||
|
||||
it('works for basic mermaid code blocks', async () => {
|
||||
const mdxCompiler = createTestCompiler();
|
||||
const result = await mdxCompiler.process(`# Heading 1
|
||||
const result = await process(`# Heading 1
|
||||
|
||||
\`\`\`mermaid
|
||||
graph TD;
|
||||
|
@ -41,6 +55,13 @@ graph TD;
|
|||
B-->D;
|
||||
C-->D;
|
||||
\`\`\``);
|
||||
expect(result.contents).toMatchSnapshot();
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"<h1>Heading 1</h1>
|
||||
<mermaid value="graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
B-->D;
|
||||
C-->D;"></mermaid>"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import visit from 'unist-util-visit';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
import type {Code} from 'mdast';
|
||||
|
||||
|
@ -17,6 +18,7 @@ export default function plugin(): Transformer {
|
|||
return (root) => {
|
||||
visit(root, 'code', (node: Code, index, parent) => {
|
||||
if (node.lang === 'mermaid') {
|
||||
// TODO migrate to mdxJsxFlowElement? cf admonitions
|
||||
parent!.children.splice(index, 1, {
|
||||
type: 'mermaidCodeBlock',
|
||||
data: {
|
||||
|
|
|
@ -45,7 +45,7 @@ exports[`toc remark plugin escapes inline code 1`] = `
|
|||
id: 'divitestidiv-1',
|
||||
level: 2
|
||||
}
|
||||
];
|
||||
]
|
||||
|
||||
## \`<Head />\`
|
||||
|
||||
|
@ -78,7 +78,7 @@ exports[`toc remark plugin exports even with existing name 1`] = `
|
|||
id: 'avengers',
|
||||
level: 3
|
||||
}
|
||||
];
|
||||
]
|
||||
|
||||
## Thanos
|
||||
|
||||
|
@ -89,11 +89,11 @@ exports[`toc remark plugin exports even with existing name 1`] = `
|
|||
`;
|
||||
|
||||
exports[`toc remark plugin handles empty headings 1`] = `
|
||||
"export const toc = [];
|
||||
"export const toc = []
|
||||
|
||||
# Ignore this
|
||||
|
||||
##
|
||||
##
|
||||
|
||||
## 
|
||||
"
|
||||
|
@ -120,7 +120,7 @@ export const toc = [
|
|||
id: 'again',
|
||||
level: 3
|
||||
}
|
||||
];
|
||||
]
|
||||
|
||||
## Title
|
||||
|
||||
|
@ -133,7 +133,7 @@ Content.
|
|||
`;
|
||||
|
||||
exports[`toc remark plugin outputs empty array for no TOC 1`] = `
|
||||
"export const toc = [];
|
||||
"export const toc = []
|
||||
|
||||
foo
|
||||
|
||||
|
@ -172,9 +172,9 @@ exports[`toc remark plugin works on non text phrasing content 1`] = `
|
|||
id: 'inlinecode',
|
||||
level: 2
|
||||
}
|
||||
];
|
||||
]
|
||||
|
||||
## _Emphasis_
|
||||
## *Emphasis*
|
||||
|
||||
### **Importance**
|
||||
|
||||
|
@ -208,7 +208,7 @@ exports[`toc remark plugin works on text content 1`] = `
|
|||
id: 'i--unicode',
|
||||
level: 2
|
||||
}
|
||||
];
|
||||
]
|
||||
|
||||
### Endi
|
||||
|
||||
|
|
|
@ -6,22 +6,25 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import remark from 'remark';
|
||||
import mdx from 'remark-mdx';
|
||||
import vfile from 'to-vfile';
|
||||
import plugin from '../index';
|
||||
import headings from '../../headings/index';
|
||||
|
||||
const processFixture = async (name: string) => {
|
||||
const {remark} = await import('remark');
|
||||
const {default: gfm} = await import('remark-gfm');
|
||||
const {default: mdx} = await import('remark-mdx');
|
||||
|
||||
const filePath = path.join(__dirname, '__fixtures__', `${name}.md`);
|
||||
const file = await vfile.read(filePath);
|
||||
const result = await remark()
|
||||
.use(headings)
|
||||
.use(gfm)
|
||||
.use(mdx)
|
||||
.use(plugin)
|
||||
.process(file);
|
||||
|
||||
return result.toString();
|
||||
return result.value;
|
||||
};
|
||||
|
||||
describe('toc remark plugin', () => {
|
||||
|
|
|
@ -8,28 +8,42 @@
|
|||
import {parse, type ParserOptions} from '@babel/parser';
|
||||
import traverse from '@babel/traverse';
|
||||
import stringifyObject from 'stringify-object';
|
||||
import toString from 'mdast-util-to-string';
|
||||
import visit from 'unist-util-visit';
|
||||
import {toValue} from '../utils';
|
||||
|
||||
import type {Identifier} from '@babel/types';
|
||||
import type {TOCItem} from '../..';
|
||||
import type {Node, Parent} from 'unist';
|
||||
import type {Heading, Literal} from 'mdast';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
|
||||
// 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
|
||||
// import type {Plugin} from 'unified';
|
||||
type Plugin = any; // TODO fix this asap
|
||||
|
||||
export type TOCItem = {
|
||||
readonly value: string;
|
||||
readonly id: string;
|
||||
readonly level: number;
|
||||
};
|
||||
|
||||
const parseOptions: ParserOptions = {
|
||||
plugins: ['jsx'],
|
||||
sourceType: 'module',
|
||||
};
|
||||
|
||||
const name = 'toc';
|
||||
|
||||
const isImport = (child: Node): child is Literal => child.type === 'import';
|
||||
const isImport = (child: any): child is Literal =>
|
||||
child.type === 'mdxjsEsm' && child.value.startsWith('import');
|
||||
const hasImports = (index: number) => index > -1;
|
||||
const isExport = (child: Node): child is Literal => child.type === 'export';
|
||||
const isExport = (child: any): child is Literal =>
|
||||
child.type === 'mdxjsEsm' && child.value.startsWith('export');
|
||||
|
||||
const isTarget = (child: Literal) => {
|
||||
interface PluginOptions {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const isTarget = (child: Literal, name: string) => {
|
||||
let found = false;
|
||||
const ast = parse(child.value, parseOptions);
|
||||
traverse(ast, {
|
||||
|
@ -42,24 +56,23 @@ const isTarget = (child: Literal) => {
|
|||
return found;
|
||||
};
|
||||
|
||||
const getOrCreateExistingTargetIndex = (children: Node[]) => {
|
||||
const getOrCreateExistingTargetIndex = async (
|
||||
children: Node[],
|
||||
name: string,
|
||||
) => {
|
||||
let importsIndex = -1;
|
||||
let targetIndex = -1;
|
||||
|
||||
children.forEach((child, index) => {
|
||||
if (isImport(child)) {
|
||||
importsIndex = index;
|
||||
} else if (isExport(child) && isTarget(child)) {
|
||||
} else if (isExport(child) && isTarget(child, name)) {
|
||||
targetIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
if (targetIndex === -1) {
|
||||
const target = {
|
||||
default: false,
|
||||
type: 'export',
|
||||
value: `export const ${name} = [];`,
|
||||
};
|
||||
const target = await createExportNode(name, []);
|
||||
|
||||
targetIndex = hasImports(importsIndex) ? importsIndex + 1 : 0;
|
||||
children.splice(targetIndex, 0, target);
|
||||
|
@ -68,31 +81,72 @@ const getOrCreateExistingTargetIndex = (children: Node[]) => {
|
|||
return targetIndex;
|
||||
};
|
||||
|
||||
export default function plugin(): Transformer {
|
||||
return (root) => {
|
||||
const plugin: Plugin = function plugin(
|
||||
options: PluginOptions = {},
|
||||
): Transformer {
|
||||
const name = options.name || 'toc';
|
||||
|
||||
return async (root) => {
|
||||
const {toString} = await import('mdast-util-to-string');
|
||||
const headings: TOCItem[] = [];
|
||||
|
||||
visit(root, 'heading', (child: Heading, index, parent) => {
|
||||
visit(root, 'heading', (child: Heading) => {
|
||||
const value = toString(child);
|
||||
|
||||
// depth: 1 headings are titles and not included in the TOC
|
||||
if (parent !== root || !value || child.depth < 2) {
|
||||
// depth:1 headings are titles and not included in the TOC
|
||||
if (!value || child.depth < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
headings.push({
|
||||
value: toValue(child),
|
||||
value: toValue(child, toString),
|
||||
id: child.data!.id as string,
|
||||
level: child.depth,
|
||||
});
|
||||
});
|
||||
const {children} = root as Parent<Literal>;
|
||||
const targetIndex = getOrCreateExistingTargetIndex(children);
|
||||
const targetIndex = await getOrCreateExistingTargetIndex(children, name);
|
||||
|
||||
if (headings.length) {
|
||||
children[targetIndex]!.value = `export const ${name} = ${stringifyObject(
|
||||
headings,
|
||||
)};`;
|
||||
if (headings?.length) {
|
||||
children[targetIndex] = await createExportNode(name, headings);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
|
||||
async function createExportNode(name: string, object: any) {
|
||||
const {valueToEstree} = await import('estree-util-value-to-estree');
|
||||
|
||||
return {
|
||||
type: 'mdxjsEsm',
|
||||
value: `export const ${name} = ${stringifyObject(object)}`,
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
body: [
|
||||
{
|
||||
type: 'ExportNamedDeclaration',
|
||||
declaration: {
|
||||
type: 'VariableDeclaration',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id: {
|
||||
type: 'Identifier',
|
||||
name,
|
||||
},
|
||||
init: valueToEstree(object),
|
||||
},
|
||||
],
|
||||
kind: 'const',
|
||||
},
|
||||
specifiers: [],
|
||||
source: null,
|
||||
},
|
||||
],
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||

|
||||
|
||||
in paragraph 
|
||||
|
||||

|
||||
|
||||

|
||||
|
@ -16,11 +18,9 @@
|
|||
|
||||

|
||||
|
||||

|
||||

|
||||
 
|
||||
|
||||

|
||||

|
||||
 
|
||||
|
||||

|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`transformImage plugin does not choke on invalid image 1`] = `
|
||||
"<img alt={"invalid image"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/invalid.png").default} />
|
||||
"<img alt="invalid image" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/invalid.png").default} />
|
||||
"
|
||||
`;
|
||||
|
||||
|
@ -21,27 +21,27 @@ exports[`transformImage plugin transform md images to <img /> 1`] = `
|
|||
|
||||
<img src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} width="200" height="200" />
|
||||
|
||||
<img alt={"img"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} width="200" height="200" />
|
||||
<img alt="img" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} width="200" height="200" />
|
||||
|
||||
<img alt={"img from second static folder"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static2/img2.png").default} width="256" height="82" />
|
||||
in paragraph <img alt="img" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} width="200" height="200" />
|
||||
|
||||
<img alt={"img from second static folder"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static2/img2.png").default} width="256" height="82" />
|
||||
<img alt="img from second static folder" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static2/img2.png").default} width="256" height="82" />
|
||||
|
||||
<img alt={"img with URL encoded chars"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static2/img2 copy.png").default} width="256" height="82" />
|
||||
<img alt="img from second static folder" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static2/img2.png").default} width="256" height="82" />
|
||||
|
||||
<img alt={"img"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} title="Title" width="200" height="200" /> <img alt={"img"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} width="200" height="200" />
|
||||
<img alt="img with URL encoded chars" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static2/img2 copy.png").default} width="256" height="82" />
|
||||
|
||||
<img alt={"img with "quotes""} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} title="'Quoted' title" width="200" height="200" />
|
||||
<img alt="img" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} title="Title" width="200" height="200" /> <img alt="img" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} width="200" height="200" />
|
||||
|
||||
<img alt={"site alias"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} width="200" height="200" />
|
||||
!/[img with "quotes"]/(./static/img.png ''Quoted' title')
|
||||
|
||||
<img alt={"img with hash"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default + '#light'} width="200" height="200" />
|
||||
<img alt={"img with hash"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default + '#dark'} width="200" height="200" />
|
||||
<img alt="site alias" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default} width="200" height="200" />
|
||||
|
||||
<img alt={"img with query"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png?w=10").default} width="200" height="200" />
|
||||
<img alt={"img with query"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png?w=10&h=10").default} width="200" height="200" />
|
||||
<img alt="img with hash" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default + '#light'} width="200" height="200" /> <img alt="img with hash" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png").default + '#dark'} width="200" height="200" />
|
||||
|
||||
<img alt={"img with both"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png?w=10&h=10").default + '#light'} width="200" height="200" />
|
||||
<img alt="img with query" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png?w=10").default} width="200" height="200" /> <img alt="img with query" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png?w=10&h=10").default} width="200" height="200" />
|
||||
|
||||
<img alt="img with both" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/img.png?w=10&h=10").default + '#light'} width="200" height="200" />
|
||||
|
||||
## Heading
|
||||
|
||||
|
|
|
@ -7,25 +7,24 @@
|
|||
|
||||
import {jest} from '@jest/globals';
|
||||
import path from 'path';
|
||||
import remark from 'remark';
|
||||
import mdx from 'remark-mdx';
|
||||
import vfile from 'to-vfile';
|
||||
import plugin, {type PluginOptions} from '../index';
|
||||
import headings from '../../headings/index';
|
||||
|
||||
const processFixture = async (
|
||||
name: string,
|
||||
options: Partial<PluginOptions>,
|
||||
) => {
|
||||
const {remark} = await import('remark');
|
||||
const {default: mdx} = await import('remark-mdx');
|
||||
const filePath = path.join(__dirname, `__fixtures__/${name}.md`);
|
||||
const file = await vfile.read(filePath);
|
||||
|
||||
const result = await remark()
|
||||
.use(headings)
|
||||
.use(mdx)
|
||||
.use(plugin, {siteDir: __dirname, staticDirs: [], ...options})
|
||||
.process(file);
|
||||
|
||||
return result.toString();
|
||||
return result.value;
|
||||
};
|
||||
|
||||
const staticDirs = [
|
||||
|
|
|
@ -9,7 +9,6 @@ import path from 'path';
|
|||
import url from 'url';
|
||||
import fs from 'fs-extra';
|
||||
import {promisify} from 'util';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {
|
||||
toMessageRelativeFilePath,
|
||||
posixPath,
|
||||
|
@ -20,15 +19,20 @@ import {
|
|||
import visit from 'unist-util-visit';
|
||||
import escapeHtml from 'escape-html';
|
||||
import sizeOf from 'image-size';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {assetRequireAttributeValue} from '../utils';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {MdxJsxTextElement} from 'mdast-util-mdx';
|
||||
import type {Image} from 'mdast';
|
||||
import type {Parent} from 'unist';
|
||||
import type {Image, Literal} from 'mdast';
|
||||
|
||||
const {
|
||||
loaders: {inlineMarkdownImageFileLoader},
|
||||
} = getFileLoaderUtils();
|
||||
|
||||
export type PluginOptions = {
|
||||
type PluginOptions = {
|
||||
staticDirs: string[];
|
||||
siteDir: string;
|
||||
};
|
||||
|
@ -40,10 +44,14 @@ type Context = PluginOptions & {
|
|||
type Target = [node: Image, index: number, parent: Parent];
|
||||
|
||||
async function toImageRequireNode(
|
||||
[node, index, parent]: Target,
|
||||
[node]: Target,
|
||||
imagePath: string,
|
||||
filePath: string,
|
||||
) {
|
||||
// MdxJsxTextElement => see https://github.com/facebook/docusaurus/pull/8288#discussion_r1125871405
|
||||
const jsxNode = node as unknown as MdxJsxTextElement;
|
||||
const attributes: MdxJsxTextElement['attributes'] = [];
|
||||
|
||||
let relativeImagePath = posixPath(
|
||||
path.relative(path.dirname(filePath), imagePath),
|
||||
);
|
||||
|
@ -52,21 +60,46 @@ async function toImageRequireNode(
|
|||
const parsedUrl = url.parse(node.url);
|
||||
const hash = parsedUrl.hash ?? '';
|
||||
const search = parsedUrl.search ?? '';
|
||||
|
||||
const alt = node.alt ? `alt={"${escapeHtml(node.alt)}"} ` : '';
|
||||
const src = `require("${inlineMarkdownImageFileLoader}${
|
||||
const requireString = `${inlineMarkdownImageFileLoader}${
|
||||
escapePath(relativeImagePath) + search
|
||||
}").default${hash ? ` + '${hash}'` : ''}`;
|
||||
const title = node.title ? ` title="${escapeHtml(node.title)}"` : '';
|
||||
let width = '';
|
||||
let height = '';
|
||||
}`;
|
||||
if (node.alt) {
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'alt',
|
||||
value: escapeHtml(node.alt),
|
||||
});
|
||||
}
|
||||
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'src',
|
||||
value: assetRequireAttributeValue(requireString, hash),
|
||||
});
|
||||
|
||||
if (node.title) {
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'title',
|
||||
value: escapeHtml(node.title),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const size = (await promisify(sizeOf)(imagePath))!;
|
||||
if (size.width) {
|
||||
width = ` width="${size.width}"`;
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'width',
|
||||
value: String(size.width),
|
||||
});
|
||||
}
|
||||
if (size.height) {
|
||||
height = ` height="${size.height}"`;
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'height',
|
||||
value: String(size.height),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// Workaround for https://github.com/yarnpkg/berry/pull/3889#issuecomment-1034469784
|
||||
|
@ -77,12 +110,14 @@ ${(err as Error).message}`;
|
|||
}
|
||||
}
|
||||
|
||||
const jsxNode: Literal = {
|
||||
type: 'jsx',
|
||||
value: `<img ${alt}src={${src}}${title}${width}${height} />`,
|
||||
};
|
||||
Object.keys(jsxNode).forEach(
|
||||
(key) => delete jsxNode[key as keyof typeof jsxNode],
|
||||
);
|
||||
|
||||
parent.children.splice(index, 1, jsxNode);
|
||||
jsxNode.type = 'mdxJsxTextElement';
|
||||
jsxNode.name = 'img';
|
||||
jsxNode.attributes = attributes;
|
||||
jsxNode.children = [];
|
||||
}
|
||||
|
||||
async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
|
||||
|
@ -122,7 +157,7 @@ async function getImageAbsolutePath(
|
|||
}
|
||||
return imageFilePath;
|
||||
}
|
||||
// Relative paths are resolved against the source file's folder.
|
||||
// relative paths are resolved against the source file's folder
|
||||
const imageFilePath = path.join(
|
||||
path.dirname(filePath),
|
||||
decodeURIComponent(imagePath),
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
[asset](./asset.pdf)
|
||||
|
||||
in paragraph [asset](./asset.pdf)
|
||||
|
||||
[asset with URL encoded chars](./asset%20%282%29.pdf)
|
||||
|
||||
[asset with hash](./asset.pdf#page=2)
|
||||
|
|
|
@ -12,15 +12,17 @@ exports[`transformAsset plugin pathname protocol 1`] = `
|
|||
exports[`transformAsset plugin transform md links to <a /> 1`] = `
|
||||
"[asset](https://example.com/asset.pdf)
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf').default}></a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} />
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf').default}>asset</a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset</a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset (2).pdf').default}>asset with URL encoded chars</a>
|
||||
in paragraph <a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset</a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf').default + '#page=2'}>asset with hash</a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset (2).pdf").default}>asset with URL encoded chars</a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf').default} title="Title">asset</a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default + '#page=2'}>asset with hash</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} title="Title">asset</a>
|
||||
|
||||
[page](noUrl.md)
|
||||
|
||||
|
@ -34,24 +36,24 @@ exports[`transformAsset plugin transform md links to <a /> 1`] = `
|
|||
|
||||
[assets](/github/!file-loader!/assets.pdf)
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf').default}>asset</a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset</a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static2/asset2.pdf').default}>asset2</a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static2/asset2.pdf").default}>asset2</a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf').default}>staticAsset.pdf</a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>staticAsset.pdf</a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf').default}>@site/static/staticAsset.pdf</a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>@site/static/staticAsset.pdf</a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf').default + '#page=2'} title="Title">@site/static/staticAsset.pdf</a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default + '#page=2'} title="Title">@site/static/staticAsset.pdf</a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf').default}>Just staticAsset.pdf</a>, and <a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf').default}><strong>awesome</strong> staticAsset 2.pdf 'It is really "AWESOME"'</a>, but also <a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf').default}>coded <code>staticAsset 3.pdf</code></a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>Just staticAsset.pdf</a>, and <a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>**awesome** staticAsset 2.pdf 'It is really "AWESOME"'</a>, but also <a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>coded \`staticAsset 3.pdf\`</a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAssetImage.png').default}><img alt={"Clickable Docusaurus logo"} src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/staticAssetImage.png").default} width="200" height="200" /></a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAssetImage.png").default}><img alt="Clickable Docusaurus logo" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/staticAssetImage.png").default} width="200" height="200" /></a>
|
||||
|
||||
<a target="_blank" href={require('!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf').default}><span style={{color: "red"}}>Stylized link to asset file</span></a>
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}><span style={{color: "red"}}>Stylized link to asset file</span></a>
|
||||
|
||||
<a target="_blank" href={require('./data.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json').default}>JSON</a>
|
||||
<a target="_blank" href={require("./data.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON</a>
|
||||
|
||||
<a target="_blank" href={require('./static/static-json.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json').default}>static JSON</a>
|
||||
<a target="_blank" href={require("./static/static-json.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON</a>
|
||||
"
|
||||
`;
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import remark from 'remark';
|
||||
import mdx from 'remark-mdx';
|
||||
import vfile from 'to-vfile';
|
||||
import plugin from '..';
|
||||
import transformImage, {type PluginOptions} from '../../transformImage';
|
||||
|
||||
const processFixture = async (name: string, options?: PluginOptions) => {
|
||||
const {remark} = await import('remark');
|
||||
const {default: mdx} = await import('remark-mdx');
|
||||
const siteDir = path.join(__dirname, `__fixtures__`);
|
||||
const staticDirs = [
|
||||
path.join(siteDir, 'static'),
|
||||
|
@ -29,7 +29,7 @@ const processFixture = async (name: string, options?: PluginOptions) => {
|
|||
})
|
||||
.process(file);
|
||||
|
||||
return result.toString();
|
||||
return result.value;
|
||||
};
|
||||
|
||||
describe('transformAsset plugin', () => {
|
||||
|
|
|
@ -17,8 +17,11 @@ import {
|
|||
} from '@docusaurus/utils';
|
||||
import visit from 'unist-util-visit';
|
||||
import escapeHtml from 'escape-html';
|
||||
import {stringifyContent} from '../utils';
|
||||
import {assetRequireAttributeValue} from '../utils';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {MdxJsxTextElement} from 'mdast-util-mdx';
|
||||
import type {Parent} from 'unist';
|
||||
import type {Link, Literal} from 'mdast';
|
||||
|
||||
|
@ -26,7 +29,7 @@ const {
|
|||
loaders: {inlineMarkdownLinkFileLoader},
|
||||
} = getFileLoaderUtils();
|
||||
|
||||
export type PluginOptions = {
|
||||
type PluginOptions = {
|
||||
staticDirs: string[];
|
||||
siteDir: string;
|
||||
};
|
||||
|
@ -40,11 +43,15 @@ type Target = [node: Link, index: number, parent: Parent];
|
|||
/**
|
||||
* Transforms the link node to a JSX `<a>` element with a `require()` call.
|
||||
*/
|
||||
function toAssetRequireNode(
|
||||
[node, index, parent]: Target,
|
||||
async function toAssetRequireNode(
|
||||
[node]: Target,
|
||||
assetPath: string,
|
||||
filePath: string,
|
||||
) {
|
||||
// MdxJsxTextElement => see https://github.com/facebook/docusaurus/pull/8288#discussion_r1125871405
|
||||
const jsxNode = node as unknown as MdxJsxTextElement;
|
||||
const attributes: MdxJsxTextElement['attributes'] = [];
|
||||
|
||||
// require("assets/file.pdf") means requiring from a package called assets
|
||||
const relativeAssetPath = `./${posixPath(
|
||||
path.relative(path.dirname(filePath), assetPath),
|
||||
|
@ -54,23 +61,43 @@ function toAssetRequireNode(
|
|||
const hash = parsedUrl.hash ?? '';
|
||||
const search = parsedUrl.search ?? '';
|
||||
|
||||
const href = `require('${
|
||||
const requireString = `${
|
||||
// A hack to stop Webpack from using its built-in loader to parse JSON
|
||||
path.extname(relativeAssetPath) === '.json'
|
||||
? `${relativeAssetPath.replace('.json', '.raw')}!=`
|
||||
: ''
|
||||
}${inlineMarkdownLinkFileLoader}${
|
||||
escapePath(relativeAssetPath) + search
|
||||
}').default${hash ? ` + '${hash}'` : ''}`;
|
||||
const children = stringifyContent(node);
|
||||
const title = node.title ? ` title="${escapeHtml(node.title)}"` : '';
|
||||
}${inlineMarkdownLinkFileLoader}${escapePath(relativeAssetPath) + search}`;
|
||||
|
||||
const jsxNode: Literal = {
|
||||
type: 'jsx',
|
||||
value: `<a target="_blank" href={${href}}${title}>${children}</a>`,
|
||||
};
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'target',
|
||||
value: '_blank',
|
||||
});
|
||||
|
||||
parent.children.splice(index, 1, jsxNode);
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'href',
|
||||
value: assetRequireAttributeValue(requireString, hash),
|
||||
});
|
||||
|
||||
if (node.title) {
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'title',
|
||||
value: escapeHtml(node.title),
|
||||
});
|
||||
}
|
||||
|
||||
const {children} = node;
|
||||
|
||||
Object.keys(jsxNode).forEach(
|
||||
(key) => delete jsxNode[key as keyof typeof jsxNode],
|
||||
);
|
||||
|
||||
jsxNode.type = 'mdxJsxTextElement';
|
||||
jsxNode.name = 'a';
|
||||
jsxNode.attributes = attributes;
|
||||
jsxNode.children = children;
|
||||
}
|
||||
|
||||
async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) {
|
||||
|
@ -144,7 +171,7 @@ async function processLinkNode(target: Target, context: Context) {
|
|||
context,
|
||||
);
|
||||
if (assetPath) {
|
||||
toAssetRequireNode(target, assetPath, context.filePath);
|
||||
await toAssetRequireNode(target, assetPath, context.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
# MDX code blocks test document
|
||||
|
||||
## Some basic markdown
|
||||
|
||||
text
|
||||
|
||||
[link](https://facebook.com)
|
||||
|
||||
**bold**
|
||||
|
||||

|
||||
|
||||
## Some basic MDX
|
||||
|
||||
import XYZ from 'xyz';
|
||||
|
||||
<XYZ abc="1" def={[1, '42', {hello: 'world'}]} style={{color: 'red'}}>
|
||||
<span>Test</span>
|
||||
</XYZ>
|
||||
|
||||
## Some basic MDX code block
|
||||
|
||||
```mdx-code-block
|
||||
import Avatar from 'avatar';
|
||||
|
||||
<Avatar style={{color: 'red'}}>
|
||||
<div>Sebastien Lorber</div>
|
||||
</Avatar>
|
||||
```
|
||||
|
||||
## Some complex MDX with nested code blocks
|
||||
|
||||
<Tabs
|
||||
defaultValue="bash"
|
||||
values={[
|
||||
{ label: 'Bash', value: 'bash' },
|
||||
{ label: 'Windows', value: 'windows' },
|
||||
{ label: 'PowerShell', value: 'powershell' }
|
||||
]}>
|
||||
<TabItem value="bash">
|
||||
|
||||
```bash
|
||||
GIT_USER=<GITHUB_USERNAME> yarn deploy
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="windows">
|
||||
|
||||
```batch
|
||||
cmd /C "set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="powershell">
|
||||
|
||||
```powershell
|
||||
cmd /C 'set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Some complex MDX code block with nested code blocks
|
||||
|
||||
````mdx-code-block
|
||||
<Tabs
|
||||
defaultValue="bash"
|
||||
values={[
|
||||
{ label: 'Bash', value: 'bash' },
|
||||
{ label: 'Windows', value: 'windows' },
|
||||
{ label: 'PowerShell', value: 'powershell' }
|
||||
]}>
|
||||
<TabItem value="bash">
|
||||
|
||||
```bash
|
||||
GIT_USER=<GITHUB_USERNAME> yarn deploy
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="windows">
|
||||
|
||||
```batch
|
||||
cmd /C "set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="powershell">
|
||||
|
||||
```powershell
|
||||
cmd /C 'set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
````
|
|
@ -1,819 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`unwrapMdxCodeBlocks remark plugin unwraps the mdx code blocks 1`] = `
|
||||
"# MDX code blocks test document
|
||||
|
||||
## Some basic markdown
|
||||
|
||||
text
|
||||
|
||||
[link](https://facebook.com)
|
||||
|
||||
**bold**
|
||||
|
||||

|
||||
|
||||
## Some basic MDX
|
||||
|
||||
import XYZ from 'xyz';
|
||||
|
||||
<XYZ abc="1" def={[1, '42', {hello: 'world'}]} style={{color: 'red'}}>
|
||||
<span>Test</span>
|
||||
</XYZ>
|
||||
|
||||
## Some basic MDX code block
|
||||
|
||||
import Avatar from 'avatar';
|
||||
|
||||
<Avatar style={{color: 'red'}}>
|
||||
<div>Sebastien Lorber</div>
|
||||
</Avatar>
|
||||
|
||||
## Some complex MDX with nested code blocks
|
||||
|
||||
<Tabs
|
||||
defaultValue="bash"
|
||||
values={[
|
||||
{ label: 'Bash', value: 'bash' },
|
||||
{ label: 'Windows', value: 'windows' },
|
||||
{ label: 'PowerShell', value: 'powershell' }
|
||||
]}>
|
||||
<TabItem value="bash">
|
||||
|
||||
\`\`\`bash
|
||||
GIT_USER=<GITHUB_USERNAME> yarn deploy
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="windows">
|
||||
|
||||
\`\`\`batch
|
||||
cmd /C "set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy"
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="powershell">
|
||||
|
||||
\`\`\`powershell
|
||||
cmd /C 'set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy'
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Some complex MDX code block with nested code blocks
|
||||
|
||||
<Tabs
|
||||
defaultValue="bash"
|
||||
values={[
|
||||
{ label: 'Bash', value: 'bash' },
|
||||
{ label: 'Windows', value: 'windows' },
|
||||
{ label: 'PowerShell', value: 'powershell' }
|
||||
]}>
|
||||
<TabItem value="bash">
|
||||
|
||||
\`\`\`bash
|
||||
GIT_USER=<GITHUB_USERNAME> yarn deploy
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="windows">
|
||||
|
||||
\`\`\`batch
|
||||
cmd /C "set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy"
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="powershell">
|
||||
|
||||
\`\`\`powershell
|
||||
cmd /C 'set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy'
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`unwrapMdxCodeBlocks remark plugin unwraps the mdx code blocks AST 1`] = `
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 32,
|
||||
"line": 1,
|
||||
"offset": 31,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 3,
|
||||
"line": 1,
|
||||
"offset": 2,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "MDX code blocks test document",
|
||||
},
|
||||
],
|
||||
"depth": 1,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 32,
|
||||
"line": 1,
|
||||
"offset": 31,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 1,
|
||||
"offset": 0,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 23,
|
||||
"line": 3,
|
||||
"offset": 55,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 4,
|
||||
"line": 3,
|
||||
"offset": 36,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Some basic markdown",
|
||||
},
|
||||
],
|
||||
"depth": 2,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 23,
|
||||
"line": 3,
|
||||
"offset": 55,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 3,
|
||||
"offset": 33,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 5,
|
||||
"line": 5,
|
||||
"offset": 61,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 5,
|
||||
"offset": 57,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "text",
|
||||
},
|
||||
],
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 5,
|
||||
"line": 5,
|
||||
"offset": 61,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 5,
|
||||
"offset": 57,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 7,
|
||||
"offset": 68,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 2,
|
||||
"line": 7,
|
||||
"offset": 64,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "link",
|
||||
},
|
||||
],
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 29,
|
||||
"line": 7,
|
||||
"offset": 91,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 7,
|
||||
"offset": 63,
|
||||
},
|
||||
},
|
||||
"title": null,
|
||||
"type": "link",
|
||||
"url": "https://facebook.com",
|
||||
},
|
||||
],
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 29,
|
||||
"line": 7,
|
||||
"offset": 91,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 7,
|
||||
"offset": 63,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 7,
|
||||
"line": 9,
|
||||
"offset": 99,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 3,
|
||||
"line": 9,
|
||||
"offset": 95,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "bold",
|
||||
},
|
||||
],
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 9,
|
||||
"line": 9,
|
||||
"offset": 101,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 9,
|
||||
"offset": 93,
|
||||
},
|
||||
},
|
||||
"type": "strong",
|
||||
},
|
||||
],
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 9,
|
||||
"line": 9,
|
||||
"offset": 101,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 9,
|
||||
"offset": 93,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"alt": "image",
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 43,
|
||||
"line": 11,
|
||||
"offset": 145,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 11,
|
||||
"offset": 103,
|
||||
},
|
||||
},
|
||||
"title": null,
|
||||
"type": "image",
|
||||
"url": "https://facebook.com/favicon.ico",
|
||||
},
|
||||
],
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 43,
|
||||
"line": 11,
|
||||
"offset": 145,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 11,
|
||||
"offset": 103,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 18,
|
||||
"line": 13,
|
||||
"offset": 164,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 4,
|
||||
"line": 13,
|
||||
"offset": 150,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Some basic MDX",
|
||||
},
|
||||
],
|
||||
"depth": 2,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 18,
|
||||
"line": 13,
|
||||
"offset": 164,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 13,
|
||||
"offset": 147,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 23,
|
||||
"line": 15,
|
||||
"offset": 188,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 15,
|
||||
"offset": 166,
|
||||
},
|
||||
},
|
||||
"type": "import",
|
||||
"value": "import XYZ from 'xyz';",
|
||||
},
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 7,
|
||||
"line": 19,
|
||||
"offset": 287,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 17,
|
||||
"offset": 190,
|
||||
},
|
||||
},
|
||||
"type": "jsx",
|
||||
"value": "<XYZ abc="1" def={[1, '42', {hello: 'world'}]} style={{color: 'red'}}>
|
||||
<span>Test</span>
|
||||
</XYZ>",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 29,
|
||||
"line": 21,
|
||||
"offset": 317,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 4,
|
||||
"line": 21,
|
||||
"offset": 292,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Some basic MDX code block",
|
||||
},
|
||||
],
|
||||
"depth": 2,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 29,
|
||||
"line": 21,
|
||||
"offset": 317,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 21,
|
||||
"offset": 289,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
{
|
||||
"lang": "mdx-code-block",
|
||||
"meta": null,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 4,
|
||||
"line": 29,
|
||||
"offset": 442,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 23,
|
||||
"offset": 319,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "import Avatar from 'avatar';
|
||||
|
||||
<Avatar style={{color: 'red'}}>
|
||||
<div>Sebastien Lorber</div>
|
||||
</Avatar>",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 44,
|
||||
"line": 31,
|
||||
"offset": 487,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 4,
|
||||
"line": 31,
|
||||
"offset": 447,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Some complex MDX with nested code blocks",
|
||||
},
|
||||
],
|
||||
"depth": 2,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 44,
|
||||
"line": 31,
|
||||
"offset": 487,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 31,
|
||||
"offset": 444,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 25,
|
||||
"line": 40,
|
||||
"offset": 688,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 33,
|
||||
"offset": 489,
|
||||
},
|
||||
},
|
||||
"type": "jsx",
|
||||
"value": "<Tabs
|
||||
defaultValue="bash"
|
||||
values={[
|
||||
{ label: 'Bash', value: 'bash' },
|
||||
{ label: 'Windows', value: 'windows' },
|
||||
{ label: 'PowerShell', value: 'powershell' }
|
||||
]}>
|
||||
<TabItem value="bash">",
|
||||
},
|
||||
{
|
||||
"lang": "bash",
|
||||
"meta": null,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 4,
|
||||
"line": 44,
|
||||
"offset": 740,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 42,
|
||||
"offset": 690,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "GIT_USER=<GITHUB_USERNAME> yarn deploy",
|
||||
},
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 28,
|
||||
"line": 47,
|
||||
"offset": 782,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 46,
|
||||
"offset": 742,
|
||||
},
|
||||
},
|
||||
"type": "jsx",
|
||||
"value": " </TabItem>
|
||||
<TabItem value="windows">",
|
||||
},
|
||||
{
|
||||
"lang": null,
|
||||
"meta": null,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 8,
|
||||
"line": 51,
|
||||
"offset": 865,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 49,
|
||||
"offset": 784,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "\`\`\`batch
|
||||
cmd /C "set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy"
|
||||
\`\`\`",
|
||||
},
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 31,
|
||||
"line": 54,
|
||||
"offset": 910,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 53,
|
||||
"offset": 867,
|
||||
},
|
||||
},
|
||||
"type": "jsx",
|
||||
"value": " </TabItem>
|
||||
<TabItem value="powershell">",
|
||||
},
|
||||
{
|
||||
"lang": "powershell",
|
||||
"meta": null,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 4,
|
||||
"line": 58,
|
||||
"offset": 986,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 56,
|
||||
"offset": 912,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "cmd /C 'set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy'",
|
||||
},
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 8,
|
||||
"line": 61,
|
||||
"offset": 1008,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 60,
|
||||
"offset": 988,
|
||||
},
|
||||
},
|
||||
"type": "jsx",
|
||||
"value": " </TabItem>
|
||||
</Tabs>",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 55,
|
||||
"line": 63,
|
||||
"offset": 1064,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 4,
|
||||
"line": 63,
|
||||
"offset": 1013,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Some complex MDX code block with nested code blocks",
|
||||
},
|
||||
],
|
||||
"depth": 2,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 55,
|
||||
"line": 63,
|
||||
"offset": 1064,
|
||||
},
|
||||
"indent": [],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 63,
|
||||
"offset": 1010,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
{
|
||||
"lang": "mdx-code-block",
|
||||
"meta": null,
|
||||
"position": Position {
|
||||
"end": {
|
||||
"column": 5,
|
||||
"line": 95,
|
||||
"offset": 1585,
|
||||
},
|
||||
"indent": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
],
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 65,
|
||||
"offset": 1066,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "<Tabs
|
||||
defaultValue="bash"
|
||||
values={[
|
||||
{ label: 'Bash', value: 'bash' },
|
||||
{ label: 'Windows', value: 'windows' },
|
||||
{ label: 'PowerShell', value: 'powershell' }
|
||||
]}>
|
||||
<TabItem value="bash">
|
||||
|
||||
\`\`\`bash
|
||||
GIT_USER=<GITHUB_USERNAME> yarn deploy
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="windows">
|
||||
|
||||
\`\`\`batch
|
||||
cmd /C "set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy"
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="powershell">
|
||||
|
||||
\`\`\`powershell
|
||||
cmd /C 'set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy'
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
</Tabs>",
|
||||
},
|
||||
],
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 1,
|
||||
"line": 96,
|
||||
"offset": 1586,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 1,
|
||||
"offset": 0,
|
||||
},
|
||||
},
|
||||
"type": "root",
|
||||
}
|
||||
`;
|
|
@ -1,36 +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 path from 'path';
|
||||
import remark from 'remark';
|
||||
import mdx from 'remark-mdx';
|
||||
import vfile from 'to-vfile';
|
||||
import plugin from '..';
|
||||
|
||||
const processFixture = async (name: string) => {
|
||||
const file = await vfile.read(path.join(__dirname, '__fixtures__', name));
|
||||
const result = await remark().use(mdx).use(plugin).process(file);
|
||||
return result.toString();
|
||||
};
|
||||
|
||||
const processFixtureAST = async (name: string) => {
|
||||
const file = await vfile.read(path.join(__dirname, '__fixtures__', name));
|
||||
return remark().use(mdx).use(plugin).parse(file);
|
||||
};
|
||||
|
||||
describe('unwrapMdxCodeBlocks remark plugin', () => {
|
||||
it('unwraps the mdx code blocks', async () => {
|
||||
const result = await processFixture('has-mdx-code-blocks.mdx');
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// The AST output should be parsed correctly or the MDX loader won't work!
|
||||
it('unwraps the mdx code blocks AST', async () => {
|
||||
const result = await processFixtureAST('has-mdx-code-blocks.mdx');
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,33 +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 visit from 'unist-util-visit';
|
||||
import type {Transformer, Processor} from 'unified';
|
||||
import type {Code, Parent} from 'mdast';
|
||||
|
||||
// This plugin is mostly to help integrating Docusaurus with translation systems
|
||||
// that do not support well MDX embedded JSX syntax (like Crowdin).
|
||||
// We wrap the JSX syntax in code blocks so that translation tools don't mess up
|
||||
// with the markup, but the JSX inside such code blocks should still be
|
||||
// evaluated as JSX
|
||||
// See https://github.com/facebook/docusaurus/pull/4278
|
||||
export default function plugin(this: Processor): Transformer {
|
||||
return (root) => {
|
||||
visit(root, 'code', (node: Code, index, parent) => {
|
||||
if (node.lang === 'mdx-code-block') {
|
||||
const newChildren = (this.parse(node.value) as Parent).children;
|
||||
|
||||
// Replace the mdx code block by its content, parsed
|
||||
parent!.children.splice(
|
||||
parent!.children.indexOf(node),
|
||||
1,
|
||||
...newChildren,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
|
@ -6,31 +6,99 @@
|
|||
*/
|
||||
|
||||
import escapeHtml from 'escape-html';
|
||||
import toString from 'mdast-util-to-string';
|
||||
import type {Parent} from 'unist';
|
||||
import type {PhrasingContent, Heading} from 'mdast';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {MdxJsxAttributeValueExpression} from 'mdast-util-mdx';
|
||||
|
||||
export function stringifyContent(node: Parent): string {
|
||||
return (node.children as PhrasingContent[]).map(toValue).join('');
|
||||
export function stringifyContent(
|
||||
node: Parent,
|
||||
toString: (param: unknown) => string, // TODO weird but works): string {
|
||||
): string {
|
||||
return (node.children as PhrasingContent[])
|
||||
.map((item) => toValue(item, toString))
|
||||
.join('');
|
||||
}
|
||||
|
||||
export function toValue(node: PhrasingContent | Heading): string {
|
||||
export function toValue(
|
||||
node: PhrasingContent | Heading,
|
||||
toString: (param: unknown) => string, // TODO weird but works
|
||||
): string {
|
||||
switch (node.type) {
|
||||
case 'mdxJsxTextElement': {
|
||||
const tag = node.name;
|
||||
return `<${tag}>${stringifyContent(node, toString)}</${tag}>`;
|
||||
}
|
||||
case 'text':
|
||||
return escapeHtml(node.value);
|
||||
case 'heading':
|
||||
return stringifyContent(node);
|
||||
return stringifyContent(node, toString);
|
||||
case 'inlineCode':
|
||||
return `<code>${escapeHtml(node.value)}</code>`;
|
||||
case 'emphasis':
|
||||
return `<em>${stringifyContent(node)}</em>`;
|
||||
return `<em>${stringifyContent(node, toString)}</em>`;
|
||||
case 'strong':
|
||||
return `<strong>${stringifyContent(node)}</strong>`;
|
||||
return `<strong>${stringifyContent(node, toString)}</strong>`;
|
||||
case 'delete':
|
||||
return `<del>${stringifyContent(node)}</del>`;
|
||||
return `<del>${stringifyContent(node, toString)}</del>`;
|
||||
case 'link':
|
||||
return stringifyContent(node);
|
||||
return stringifyContent(node, toString);
|
||||
default:
|
||||
return toString(node);
|
||||
}
|
||||
}
|
||||
|
||||
export function assetRequireAttributeValue(
|
||||
requireString: string,
|
||||
hash: string,
|
||||
): MdxJsxAttributeValueExpression {
|
||||
return {
|
||||
type: 'mdxJsxAttributeValueExpression',
|
||||
value: `require("${requireString}").default${hash && ` + '${hash}'`}`,
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'BinaryExpression',
|
||||
left: {
|
||||
type: 'MemberExpression',
|
||||
object: {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: 'require',
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: 'Literal',
|
||||
value: requireString,
|
||||
raw: `"${requireString}"`,
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: 'default',
|
||||
},
|
||||
computed: false,
|
||||
optional: false,
|
||||
},
|
||||
operator: '+',
|
||||
right: {
|
||||
type: 'Literal',
|
||||
value: hash,
|
||||
raw: `"${hash}"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
sourceType: 'module',
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue