mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 09:47:48 +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
|
@ -13,3 +13,5 @@ packages/docusaurus-*/lib/*
|
|||
packages/create-docusaurus/lib/*
|
||||
packages/create-docusaurus/templates/
|
||||
website/static/katex/katex.min.css
|
||||
|
||||
jest/vendor
|
||||
|
|
14
jest.config.mjs
vendored
14
jest.config.mjs
vendored
|
@ -80,6 +80,20 @@ export default {
|
|||
'@docusaurus/plugin-content-docs/src/client/index.ts',
|
||||
|
||||
'@testing-utils/(.*)': '<rootDir>/jest/utils/$1.ts',
|
||||
|
||||
// MDX packages are ESM-only and it is a pain to use in Jest
|
||||
// So we use them in Jest tests as CJS versions
|
||||
// see https://mdxjs.com/docs/troubleshooting-mdx/#problems-integrating-mdx
|
||||
'^unified$': '<rootDir>/jest/vendor/unified@10.1.2.js',
|
||||
'^@mdx-js/mdx$': '<rootDir>/jest/vendor/@mdx-js__mdx@2.1.5.js',
|
||||
'^remark$': '<rootDir>/jest/vendor/remark@14.0.2.js',
|
||||
'^remark-mdx$': '<rootDir>/jest/vendor/remark-mdx@2.1.5.js',
|
||||
'^remark-directive$': '<rootDir>/jest/vendor/remark-directive@2.0.1.js',
|
||||
'^remark-gfm$': '<rootDir>/jest/vendor/remark-gfm@3.0.1.js',
|
||||
'^estree-util-value-to-estree$':
|
||||
'<rootDir>/jest/vendor/estree-util-value-to-estree@2.1.0.js',
|
||||
'^mdast-util-to-string$':
|
||||
'<rootDir>/jest/vendor/mdast-util-to-string@3.1.0.js',
|
||||
},
|
||||
snapshotSerializers: [
|
||||
'<rootDir>/jest/snapshotPathNormalizer.ts',
|
||||
|
|
7
jest/deps.d.ts
vendored
7
jest/deps.d.ts
vendored
|
@ -13,13 +13,6 @@ declare module 'to-vfile' {
|
|||
export function read(path: string, encoding?: string): Promise<VFile>;
|
||||
}
|
||||
|
||||
declare module 'remark-mdx' {
|
||||
import type {Plugin} from 'unified';
|
||||
|
||||
const mdx: Plugin;
|
||||
export = mdx;
|
||||
}
|
||||
|
||||
declare module 'remark-rehype';
|
||||
|
||||
declare module 'rehype-stringify';
|
||||
|
|
|
@ -55,12 +55,12 @@
|
|||
"test": "jest",
|
||||
"test:build:website": "./admin/scripts/test-release.sh",
|
||||
"watch": "yarn lerna run --parallel watch",
|
||||
"clear": "(yarn workspace website clear || echo 'Failure while running docusaurus clear') && yarn lerna exec --ignore docusaurus yarn rimraf lib",
|
||||
"clear": "(yarn workspace website clear || echo 'Failure while running docusaurus clear') && yarn rimraf test-website && yarn rimraf test-website-in-workspace && yarn lerna exec --ignore docusaurus yarn rimraf lib",
|
||||
"test:baseUrl": "yarn build:website:baseUrl && yarn serve:website:baseUrl",
|
||||
"lock:update": "npx --yes yarn-deduplicate"
|
||||
},
|
||||
"dependencies": {
|
||||
"unified": "^9.2.2"
|
||||
"unified": "^10.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crowdin/cli": "^3.10.0",
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"dependencies": {
|
||||
"@docusaurus/core": "^3.0.0-alpha.0",
|
||||
"@docusaurus/preset-classic": "^3.0.0-alpha.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@mdx-js/react": "^2.1.5",
|
||||
"clsx": "^1.2.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"dependencies": {
|
||||
"@docusaurus/core": "^3.0.0-alpha.0",
|
||||
"@docusaurus/preset-classic": "^3.0.0-alpha.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@mdx-js/react": "^2.1.5",
|
||||
"clsx": "^1.2.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"dependencies": {
|
||||
"@docusaurus/core": "^3.0.0-alpha.0",
|
||||
"@docusaurus/preset-classic": "^3.0.0-alpha.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@mdx-js/react": "^2.1.5",
|
||||
"clsx": "^1.2.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
|
|
|
@ -22,16 +22,23 @@
|
|||
"@babel/traverse": "^7.21.2",
|
||||
"@docusaurus/logger": "^3.0.0-alpha.0",
|
||||
"@docusaurus/utils": "^3.0.0-alpha.0",
|
||||
"@mdx-js/mdx": "^1.6.22",
|
||||
"@docusaurus/utils-validation": "^3.0.0-alpha.0",
|
||||
"@mdx-js/mdx": "^2.1.5",
|
||||
"escape-html": "^1.0.3",
|
||||
"estree-util-value-to-estree": "^2.1.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"hastscript": "^7.1.0",
|
||||
"image-size": "^1.0.2",
|
||||
"mdast-util-to-string": "^2.0.0",
|
||||
"mdast-util-to-string": "^3.0.0",
|
||||
"mdast-util-mdx": "^2.0.0",
|
||||
"remark-comment": "^1.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-directive": "^2.0.1",
|
||||
"remark-emoji": "^2.2.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tslib": "^2.5.0",
|
||||
"unified": "^9.2.2",
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-visit": "^2.0.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.76.0"
|
||||
|
@ -43,8 +50,8 @@
|
|||
"@types/stringify-object": "^3.3.1",
|
||||
"@types/unist": "^2.0.6",
|
||||
"rehype-stringify": "^8.0.0",
|
||||
"remark": "^12.0.1",
|
||||
"remark-mdx": "^1.6.21",
|
||||
"remark": "^14.0.2",
|
||||
"remark-mdx": "^2.1.5",
|
||||
"remark-rehype": "^8.1.0",
|
||||
"to-vfile": "^6.1.0",
|
||||
"unist-builder": "^2.0.3",
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"remark-stringify": "^8.1.0",
|
||||
"semver": "^7.3.8",
|
||||
"tslib": "^2.5.0",
|
||||
"unified": "^9.2.2",
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-visit": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -37,10 +37,10 @@ async function walk(dir: string): Promise<string[]> {
|
|||
return results;
|
||||
}
|
||||
|
||||
function sanitizedFileContent(
|
||||
async function sanitizedFileContent(
|
||||
content: string,
|
||||
migrateMDFiles: boolean,
|
||||
): string {
|
||||
): Promise<string> {
|
||||
const extractedData = extractMetadata(content);
|
||||
const extractedMetaData = Object.entries(extractedData.metadata)
|
||||
.map(
|
||||
|
@ -55,7 +55,7 @@ ${extractedMetaData}
|
|||
---
|
||||
${
|
||||
migrateMDFiles
|
||||
? sanitizeMD(extractedData.rawContent)
|
||||
? await sanitizeMD(extractedData.rawContent)
|
||||
: extractedData.rawContent
|
||||
}`;
|
||||
return sanitizedData;
|
||||
|
@ -427,7 +427,7 @@ async function migrateBlogFiles(context: MigrationContext) {
|
|||
const content = await fs.readFile(file, 'utf-8');
|
||||
await fs.outputFile(
|
||||
file,
|
||||
sanitizedFileContent(content, shouldMigrateMdFiles),
|
||||
await sanitizedFileContent(content, shouldMigrateMdFiles),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
@ -515,7 +515,7 @@ async function migrateVersionedDocs(
|
|||
const content = await fs.readFile(pathToFile, 'utf-8');
|
||||
await fs.outputFile(
|
||||
pathToFile,
|
||||
sanitizedFileContent(
|
||||
await sanitizedFileContent(
|
||||
content.replace(versionRegex, ''),
|
||||
shouldMigrateMdFiles,
|
||||
),
|
||||
|
@ -695,7 +695,7 @@ async function migrateLatestDocs(context: MigrationContext) {
|
|||
const content = await fs.readFile(file, 'utf-8');
|
||||
await fs.outputFile(
|
||||
file,
|
||||
sanitizedFileContent(content, shouldMigrateMdFiles),
|
||||
await sanitizedFileContent(content, shouldMigrateMdFiles),
|
||||
);
|
||||
}
|
||||
}),
|
||||
|
@ -752,7 +752,10 @@ export async function migrateMDToMDX(
|
|||
files.map(async (filePath) => {
|
||||
if (path.extname(filePath) === '.md') {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
await fs.outputFile(filePath, sanitizedFileContent(content, true));
|
||||
await fs.outputFile(
|
||||
filePath,
|
||||
await sanitizedFileContent(content, true),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import markdown from 'remark-parse';
|
||||
import toJsx from '@mapbox/hast-util-to-jsx';
|
||||
import unified from 'unified';
|
||||
import parse from 'rehype-parse';
|
||||
import visit from 'unist-util-visit';
|
||||
import remarkStringify from 'remark-stringify';
|
||||
|
@ -21,7 +20,9 @@ const tags = htmlTags.reduce((acc: {[key: string]: boolean}, tag) => {
|
|||
return acc;
|
||||
}, {});
|
||||
|
||||
export default function sanitizeMD(code: string): string {
|
||||
export default async function sanitizeMD(code: string): Promise<string> {
|
||||
const {unified} = await import('unified');
|
||||
|
||||
const markdownTree = unified().use(markdown).parse(code);
|
||||
visit(markdownTree, 'code', (node: Code) => {
|
||||
node.value = `\n<!--${node.value}-->\n`;
|
||||
|
@ -30,7 +31,8 @@ export default function sanitizeMD(code: string): string {
|
|||
node.value = `<!--${node.value}-->`;
|
||||
});
|
||||
|
||||
const markdownString = unified()
|
||||
// @ts-expect-error: :/
|
||||
const markdownString: string = await unified()
|
||||
.use(remarkStringify, {fence: '`', fences: true})
|
||||
.stringify(markdownTree);
|
||||
|
||||
|
@ -45,6 +47,7 @@ export default function sanitizeMD(code: string): string {
|
|||
delete (node as Partial<Element>).tagName;
|
||||
}
|
||||
});
|
||||
|
||||
return toJsx(htmlTree)
|
||||
.replace(/\{\/\*|\*\/\}/g, '')
|
||||
.replace(/\{\/\*|\*\/\}/g, '')
|
||||
|
|
|
@ -26,7 +26,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
|
|||
beforeDefaultRehypePlugins: [],
|
||||
beforeDefaultRemarkPlugins: [],
|
||||
admonitions: true,
|
||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||
truncateMarker: /<!--\s*truncate\s*-->|\{\/\*\s*truncate\s*\*\/\}/,
|
||||
rehypePlugins: [],
|
||||
remarkPlugins: [],
|
||||
showReadingTime: true,
|
||||
|
|
|
@ -1,65 +1,101 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`footnoteIDFixer remark plugin appends a hash to each footnote def/ref 1`] = `
|
||||
"/* @jsxRuntime classic */
|
||||
/* @jsx mdx */
|
||||
|
||||
|
||||
|
||||
const layoutProps = {
|
||||
|
||||
};
|
||||
const MDXLayout = "wrapper"
|
||||
export default function MDXContent({
|
||||
components,
|
||||
...props
|
||||
}) {
|
||||
return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout">
|
||||
<p>{\`foo\`}<sup parentName="p" {...{
|
||||
"id": "fnref-1-[HASH]"
|
||||
}}><a parentName="sup" {...{
|
||||
"href": "#fn-1-[HASH]",
|
||||
"className": "footnote-ref"
|
||||
}}>{\`1\`}</a></sup></p>
|
||||
<p>{\`bar\`}<sup parentName="p" {...{
|
||||
"id": "fnref-2-[HASH]"
|
||||
}}><a parentName="sup" {...{
|
||||
"href": "#fn-2-[HASH]",
|
||||
"className": "footnote-ref"
|
||||
}}>{\`2\`}</a></sup></p>
|
||||
<p>{\`baz\`}<sup parentName="p" {...{
|
||||
"id": "fnref-3-[HASH]"
|
||||
}}><a parentName="sup" {...{
|
||||
"href": "#fn-3-[HASH]",
|
||||
"className": "footnote-ref"
|
||||
}}>{\`3\`}</a></sup></p>
|
||||
<div {...{
|
||||
"className": "footnotes"
|
||||
}}>
|
||||
<hr parentName="div"></hr>
|
||||
<ol parentName="div">
|
||||
<li parentName="ol" {...{
|
||||
"id": "fn-1-[HASH]"
|
||||
}}>{\`foo\`}<a parentName="li" {...{
|
||||
"href": "#fnref-1-[HASH]",
|
||||
"className": "footnote-backref"
|
||||
}}>{\`↩\`}</a></li>
|
||||
<li parentName="ol" {...{
|
||||
"id": "fn-2-[HASH]"
|
||||
}}>{\`foo\`}<a parentName="li" {...{
|
||||
"href": "#fnref-2-[HASH]",
|
||||
"className": "footnote-backref"
|
||||
}}>{\`↩\`}</a></li>
|
||||
<li parentName="ol" {...{
|
||||
"id": "fn-3-[HASH]"
|
||||
}}>{\`foo\`}<a parentName="li" {...{
|
||||
"href": "#fnref-3-[HASH]",
|
||||
"className": "footnote-backref"
|
||||
}}>{\`↩\`}</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
</MDXLayout>;
|
||||
"/*@jsxRuntime automatic @jsxImportSource react*/
|
||||
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
|
||||
function _createMdxContent(props) {
|
||||
const _components = Object.assign({
|
||||
p: "p",
|
||||
sup: "sup",
|
||||
a: "a",
|
||||
section: "section",
|
||||
h2: "h2",
|
||||
ol: "ol",
|
||||
li: "li"
|
||||
}, props.components);
|
||||
return _jsxs(_Fragment, {
|
||||
children: [_jsxs(_components.p, {
|
||||
children: ["foo", _jsx(_components.sup, {
|
||||
children: _jsx(_components.a, {
|
||||
href: "#user-content-fn-1-[HASH]",
|
||||
id: "user-content-fnref-1-[HASH]",
|
||||
"data-footnote-ref": true,
|
||||
"aria-describedby": "footnote-label",
|
||||
children: "1"
|
||||
})
|
||||
})]
|
||||
}), "/n", _jsxs(_components.p, {
|
||||
children: ["bar", _jsx(_components.sup, {
|
||||
children: _jsx(_components.a, {
|
||||
href: "#user-content-fn-2-[HASH]",
|
||||
id: "user-content-fnref-2-[HASH]",
|
||||
"data-footnote-ref": true,
|
||||
"aria-describedby": "footnote-label",
|
||||
children: "2"
|
||||
})
|
||||
})]
|
||||
}), "/n", _jsxs(_components.p, {
|
||||
children: ["baz", _jsx(_components.sup, {
|
||||
children: _jsx(_components.a, {
|
||||
href: "#user-content-fn-3-[HASH]",
|
||||
id: "user-content-fnref-3-[HASH]",
|
||||
"data-footnote-ref": true,
|
||||
"aria-describedby": "footnote-label",
|
||||
children: "3"
|
||||
})
|
||||
})]
|
||||
}), "/n", _jsxs(_components.section, {
|
||||
"data-footnotes": true,
|
||||
className: "footnotes",
|
||||
children: [_jsx(_components.h2, {
|
||||
className: "sr-only",
|
||||
id: "footnote-label",
|
||||
children: "Footnotes"
|
||||
}), "/n", _jsxs(_components.ol, {
|
||||
children: ["/n", _jsxs(_components.li, {
|
||||
id: "user-content-fn-1-[HASH]",
|
||||
children: ["/n", _jsxs(_components.p, {
|
||||
children: ["foo ", _jsx(_components.a, {
|
||||
href: "#user-content-fnref-1-[HASH]",
|
||||
"data-footnote-backref": true,
|
||||
className: "data-footnote-backref",
|
||||
"aria-label": "Back to content",
|
||||
children: "↩"
|
||||
})]
|
||||
}), "/n"]
|
||||
}), "/n", _jsxs(_components.li, {
|
||||
id: "user-content-fn-2-[HASH]",
|
||||
children: ["/n", _jsxs(_components.p, {
|
||||
children: ["foo ", _jsx(_components.a, {
|
||||
href: "#user-content-fnref-2-[HASH]",
|
||||
"data-footnote-backref": true,
|
||||
className: "data-footnote-backref",
|
||||
"aria-label": "Back to content",
|
||||
children: "↩"
|
||||
})]
|
||||
}), "/n"]
|
||||
}), "/n", _jsxs(_components.li, {
|
||||
id: "user-content-fn-3-[HASH]",
|
||||
children: ["/n", _jsxs(_components.p, {
|
||||
children: ["foo ", _jsx(_components.a, {
|
||||
href: "#user-content-fnref-3-[HASH]",
|
||||
"data-footnote-backref": true,
|
||||
className: "data-footnote-backref",
|
||||
"aria-label": "Back to content",
|
||||
children: "↩"
|
||||
})]
|
||||
}), "/n"]
|
||||
}), "/n"]
|
||||
}), "/n"]
|
||||
})]
|
||||
});
|
||||
}
|
||||
;
|
||||
MDXContent.isMDXComponent = true;"
|
||||
function MDXContent(props = {}) {
|
||||
const {wrapper: MDXLayout} = props.components || ({});
|
||||
return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, {
|
||||
children: _jsx(_createMdxContent, props)
|
||||
})) : _createMdxContent(props);
|
||||
}
|
||||
export default MDXContent;
|
||||
"
|
||||
`;
|
||||
|
|
|
@ -5,20 +5,24 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import vfile from 'to-vfile';
|
||||
|
||||
import {simpleHash} from '@docusaurus/utils';
|
||||
import mdx from '@mdx-js/mdx';
|
||||
import footnoteIDFixer from '../footnoteIDFixer';
|
||||
|
||||
const processFixture = async (name: string) => {
|
||||
const filepath = path.join(__dirname, `__fixtures__/${name}.md`);
|
||||
const result = await mdx(await fs.readFile(filepath, 'utf8'), {
|
||||
filepath,
|
||||
remarkPlugins: [footnoteIDFixer],
|
||||
const mdx = await import('@mdx-js/mdx');
|
||||
const {default: gfm} = await import('remark-gfm');
|
||||
|
||||
const filePath = path.join(__dirname, `__fixtures__/${name}.md`);
|
||||
const file = await vfile.read(filePath);
|
||||
|
||||
const result = await mdx.compile(file, {
|
||||
remarkPlugins: [gfm, footnoteIDFixer],
|
||||
});
|
||||
|
||||
return result.toString();
|
||||
return result.value;
|
||||
};
|
||||
|
||||
describe('footnoteIDFixer remark plugin', () => {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import visit from 'unist-util-visit';
|
||||
import {simpleHash} from '@docusaurus/utils';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
import type {FootnoteReference, FootnoteDefinition} from 'mdast';
|
||||
|
||||
|
|
|
@ -17,14 +17,16 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-mdx": "^2.0.0",
|
||||
"npm-to-yarn": "^2.0.0",
|
||||
"tslib": "^2.5.0",
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-visit": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdast": "^3.0.10",
|
||||
"remark": "^12.0.1",
|
||||
"remark-mdx": "^1.6.21",
|
||||
"remark": "^14.0.2",
|
||||
"remark-mdx": "^2.1.5",
|
||||
"to-vfile": "^6.1.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
21
packages/docusaurus-remark-plugin-npm2yarn/src/__tests__/__fixtures__/multiple.md
generated
Normal file
21
packages/docusaurus-remark-plugin-npm2yarn/src/__tests__/__fixtures__/multiple.md
generated
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Title
|
||||
|
||||
```bash npm2yarn
|
||||
$ npm install --global docusaurus
|
||||
```
|
||||
|
||||
<div className="nested-npm2yarn">
|
||||
```bash npm2yarn
|
||||
npm install
|
||||
```
|
||||
</div>
|
||||
|
||||
```bash
|
||||
echo "no npm2yarn here"
|
||||
```
|
||||
|
||||
## Subtitle
|
||||
|
||||
```bash npm2yarn
|
||||
yarn add @docusaurus/core
|
||||
```
|
|
@ -1,67 +1,127 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`npm2yarn plugin does not re-import tabs components real-world multiple npm2yarn usage 1`] = `
|
||||
"import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
# Title
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
$ npm install --global docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
$ yarn global add docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
$ pnpm add --global docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<div className="nested-npm2yarn">
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm install
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn install
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
pnpm install
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
\`\`\`bash
|
||||
echo "no npm2yarn here"
|
||||
\`\`\`
|
||||
|
||||
## Subtitle
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
yarn add @docusaurus/core
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn add @docusaurus/core
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
yarn add @docusaurus/core
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`npm2yarn plugin does not re-import tabs components when already imported above 1`] = `
|
||||
"import Tabs from '@theme/Tabs';
|
||||
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
$ npm install --global docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
$ npm install --global docusaurus
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
\`\`\`bash
|
||||
$ yarn global add docusaurus
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
$ pnpm add --global docusaurus
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
$ yarn global add docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
$ pnpm add --global docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`npm2yarn plugin does not re-import tabs components when already imported below 1`] = `
|
||||
"<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
$ npm install --global docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
$ npm install --global docusaurus
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
\`\`\`bash
|
||||
$ yarn global add docusaurus
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
$ pnpm add --global docusaurus
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
$ yarn global add docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
$ pnpm add --global docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
|
@ -86,322 +146,278 @@ npm install --save docusaurus-plugin-name
|
|||
`;
|
||||
|
||||
exports[`npm2yarn plugin work with custom converter 1`] = `
|
||||
"import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
"import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## Installing a plugin
|
||||
|
||||
A plugin is usually a npm package, so you install them like other npm packages using npm.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="Turbo">
|
||||
|
||||
\`\`\`bash
|
||||
turbo install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="Turbo">
|
||||
\`\`\`bash
|
||||
turbo install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`npm2yarn plugin work with pnpm converter 1`] = `
|
||||
"import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
"import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## Installing a plugin
|
||||
|
||||
A plugin is usually a npm package, so you install them like other npm packages using npm.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
pnpm add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
pnpm add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`npm2yarn plugin work with yarn converter 1`] = `
|
||||
"import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
"import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## Installing a plugin
|
||||
|
||||
A plugin is usually a npm package, so you install them like other npm packages using npm.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
\`\`\`bash
|
||||
yarn add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`npm2yarn plugin works on installation file 1`] = `
|
||||
"import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
"import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
$ npm install --global docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
$ npm install --global docusaurus
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
\`\`\`bash
|
||||
$ yarn global add docusaurus
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
$ pnpm add --global docusaurus
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
$ yarn global add docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
$ pnpm add --global docusaurus
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`npm2yarn plugin works on plugin file 1`] = `
|
||||
"import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
"import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## Installing a plugin
|
||||
|
||||
A plugin is usually a npm package, so you install them like other npm packages using npm.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
\`\`\`bash
|
||||
yarn add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
pnpm add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
pnpm add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`npm2yarn plugin works with common commands 1`] = `
|
||||
"import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
"import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
<Tabs groupId="npm2yarn">
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm run xxx -- --arg
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
npm run xxx -- --arg
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
\`\`\`bash
|
||||
yarn xxx --arg
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
pnpm run xxx -- --arg
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn xxx --arg
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
pnpm run xxx -- --arg
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs groupId="npm2yarn">
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm install package
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
npm install package
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
\`\`\`bash
|
||||
yarn add package
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
pnpm add package
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn add package
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
pnpm add package
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs groupId="npm2yarn">
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm remove package-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
npm remove package-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
\`\`\`bash
|
||||
yarn remove package-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
pnpm remove package-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn remove package-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
pnpm remove package-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs groupId="npm2yarn">
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm init docusaurus
|
||||
npm init docusaurus@latest my-website classic
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn create docusaurus
|
||||
yarn create docusaurus@latest my-website classic
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
\`\`\`bash
|
||||
npm init docusaurus
|
||||
npm init docusaurus@latest my-website classic
|
||||
\`\`\`
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
pnpm create docusaurus
|
||||
pnpm create docusaurus@latest my-website classic
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
</TabItem>
|
||||
exports[`npm2yarn plugin works with simplest md 1`] = `
|
||||
"import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
# Title
|
||||
|
||||
\`\`\`bash
|
||||
yarn create docusaurus
|
||||
yarn create docusaurus@latest my-website classic
|
||||
\`\`\`
|
||||
Hey
|
||||
|
||||
</TabItem>
|
||||
<Tabs>
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm install test
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
pnpm create docusaurus
|
||||
pnpm create docusaurus@latest my-website classic
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn add test
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
pnpm add test
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`npm2yarn plugin works with sync option 1`] = `
|
||||
"import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
"import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## Installing a plugin
|
||||
|
||||
A plugin is usually a npm package, so you install them like other npm packages using npm.
|
||||
|
||||
<Tabs groupId="npm2yarn">
|
||||
<TabItem value="npm">
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
\`\`\`bash
|
||||
npm install --save docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
\`\`\`bash
|
||||
yarn add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
\`\`\`bash
|
||||
pnpm add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
\`\`\`bash
|
||||
yarn add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
\`\`\`bash
|
||||
pnpm add docusaurus-plugin-name
|
||||
\`\`\`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
"
|
||||
`;
|
||||
|
|
|
@ -7,22 +7,49 @@
|
|||
|
||||
import path from 'path';
|
||||
import vfile from 'to-vfile';
|
||||
import mdx from 'remark-mdx';
|
||||
import remark from 'remark';
|
||||
import dedent from 'dedent';
|
||||
import npm2yarn from '../index';
|
||||
|
||||
const process = async (
|
||||
content: any,
|
||||
options?: Parameters<typeof npm2yarn>[0],
|
||||
) => {
|
||||
const {remark} = await import('remark');
|
||||
const {default: mdx} = await import('remark-mdx');
|
||||
|
||||
const result = await remark()
|
||||
.use(mdx)
|
||||
.use(npm2yarn, options)
|
||||
.process(content);
|
||||
|
||||
return result.value;
|
||||
};
|
||||
|
||||
const processFixture = async (
|
||||
name: string,
|
||||
options?: Parameters<typeof npm2yarn>[0],
|
||||
) => {
|
||||
const filePath = path.join(__dirname, '__fixtures__', `${name}.md`);
|
||||
const file = await vfile.read(filePath);
|
||||
const result = await remark().use(mdx).use(npm2yarn, options).process(file);
|
||||
|
||||
return result.toString();
|
||||
return process(file, options);
|
||||
};
|
||||
|
||||
describe('npm2yarn plugin', () => {
|
||||
it('works with simplest md', async () => {
|
||||
const result = await process(dedent`
|
||||
# Title
|
||||
|
||||
Hey
|
||||
|
||||
\`\`\`bash npm2yarn
|
||||
npm install test
|
||||
\`\`\`
|
||||
|
||||
`);
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('works on installation file', async () => {
|
||||
const result = await processFixture('installation');
|
||||
|
||||
|
@ -65,6 +92,11 @@ describe('npm2yarn plugin', () => {
|
|||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not re-import tabs components real-world multiple npm2yarn usage', async () => {
|
||||
const result = await processFixture('multiple');
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('work with yarn converter', async () => {
|
||||
const result = await processFixture('plugin', {converters: ['yarn']});
|
||||
|
||||
|
|
|
@ -7,95 +7,186 @@
|
|||
|
||||
import visit from 'unist-util-visit';
|
||||
import npmToYarn from 'npm-to-yarn';
|
||||
import type {Code, Content, Literal} from 'mdast';
|
||||
import type {Plugin} from 'unified';
|
||||
import type {Code, Literal} from 'mdast';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {MdxJsxFlowElement, MdxJsxAttribute} from 'mdast-util-mdx';
|
||||
import type {Node, Parent} from 'unist';
|
||||
// @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';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type Plugin<T> = any; // TODO fix this asap
|
||||
|
||||
type KnownConverter = 'yarn' | 'pnpm';
|
||||
|
||||
type CustomConverter = [name: string, cb: (npmCode: string) => string];
|
||||
|
||||
type Converter = CustomConverter | KnownConverter;
|
||||
|
||||
type PluginOptions = {
|
||||
sync?: boolean;
|
||||
converters?: (CustomConverter | 'yarn' | 'pnpm')[];
|
||||
converters?: Converter[];
|
||||
};
|
||||
|
||||
function createTabItem(
|
||||
code: string,
|
||||
node: Code,
|
||||
value: string,
|
||||
label?: string,
|
||||
) {
|
||||
return [
|
||||
{
|
||||
type: 'jsx',
|
||||
value: `<TabItem value="${value}"${label ? ` label="${label}"` : ''}>`,
|
||||
},
|
||||
{
|
||||
type: node.type,
|
||||
lang: node.lang,
|
||||
value: code,
|
||||
},
|
||||
{
|
||||
type: 'jsx',
|
||||
value: '</TabItem>',
|
||||
},
|
||||
] as Content[];
|
||||
function createAttribute(
|
||||
attributeName: string,
|
||||
attributeValue: MdxJsxAttribute['value'],
|
||||
): MdxJsxAttribute {
|
||||
return {
|
||||
type: 'mdxJsxAttribute',
|
||||
name: attributeName,
|
||||
value: attributeValue,
|
||||
};
|
||||
}
|
||||
|
||||
function createTabItem({
|
||||
code,
|
||||
node,
|
||||
value,
|
||||
label,
|
||||
}: {
|
||||
code: string;
|
||||
node: Code;
|
||||
value: string;
|
||||
label?: string;
|
||||
}): MdxJsxFlowElement {
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'TabItem',
|
||||
attributes: [
|
||||
createAttribute('value', value),
|
||||
label && createAttribute('label', label),
|
||||
].filter((attr): attr is MdxJsxAttribute => Boolean(attr)),
|
||||
children: [
|
||||
{
|
||||
type: node.type,
|
||||
lang: node.lang,
|
||||
value: code,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const transformNode = (
|
||||
node: Code,
|
||||
isSync: boolean,
|
||||
converters: (CustomConverter | 'yarn' | 'pnpm')[],
|
||||
converters: Converter[],
|
||||
) => {
|
||||
const groupIdProp = isSync ? ' groupId="npm2yarn"' : '';
|
||||
const groupIdProp = isSync
|
||||
? {
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'groupId',
|
||||
value: 'npm2yarn',
|
||||
}
|
||||
: undefined;
|
||||
const npmCode = node.value;
|
||||
|
||||
function createConvertedTabItem(converter: Converter) {
|
||||
if (typeof converter === 'string') {
|
||||
return createTabItem({
|
||||
code: npmToYarn(npmCode, converter),
|
||||
node,
|
||||
value: converter,
|
||||
label: converter === 'yarn' ? 'Yarn' : converter,
|
||||
});
|
||||
}
|
||||
const [converterName, converterFn] = converter;
|
||||
return createTabItem({
|
||||
code: converterFn(npmCode),
|
||||
node,
|
||||
value: converterName,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'jsx',
|
||||
value: `<Tabs${groupIdProp}>`,
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'Tabs',
|
||||
attributes: [groupIdProp].filter(Boolean),
|
||||
children: [
|
||||
createTabItem({code: npmCode, node, value: 'npm'}),
|
||||
...converters.flatMap(createConvertedTabItem),
|
||||
],
|
||||
},
|
||||
...createTabItem(npmCode, node, 'npm'),
|
||||
...converters.flatMap((converter) =>
|
||||
typeof converter === 'string'
|
||||
? createTabItem(
|
||||
npmToYarn(npmCode, converter),
|
||||
node,
|
||||
converter,
|
||||
converter === 'yarn' ? 'Yarn' : converter,
|
||||
)
|
||||
: createTabItem(converter[1](npmCode), node, converter[0]),
|
||||
),
|
||||
{
|
||||
type: 'jsx',
|
||||
value: '</Tabs>',
|
||||
},
|
||||
] as Content[];
|
||||
] as any[];
|
||||
};
|
||||
|
||||
const isImport = (node: Node): node is Literal => node.type === 'import';
|
||||
const isMdxEsmLiteral = (node: Node): node is Literal =>
|
||||
node.type === 'mdxjsEsm';
|
||||
// TODO legacy approximation, good-enough for now but not 100% accurate
|
||||
const isTabsImport = (node: Node): boolean =>
|
||||
isMdxEsmLiteral(node) && node.value.includes('@theme/Tabs');
|
||||
|
||||
const isParent = (node: Node): node is Parent =>
|
||||
Array.isArray((node as Parent).children);
|
||||
const matchNode = (node: Node): node is Code =>
|
||||
const isNpm2Yarn = (node: Node): node is Code =>
|
||||
node.type === 'code' && (node as Code).meta === 'npm2yarn';
|
||||
const nodeForImport: Literal = {
|
||||
type: 'import',
|
||||
value:
|
||||
"import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';",
|
||||
};
|
||||
|
||||
const plugin: Plugin<[PluginOptions?]> = (options = {}) => {
|
||||
function createImportNode() {
|
||||
return {
|
||||
type: 'mdxjsEsm',
|
||||
value:
|
||||
"import Tabs from '@theme/Tabs'\nimport TabItem from '@theme/TabItem'",
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
body: [
|
||||
{
|
||||
type: 'ImportDeclaration',
|
||||
specifiers: [
|
||||
{
|
||||
type: 'ImportDefaultSpecifier',
|
||||
local: {type: 'Identifier', name: 'Tabs'},
|
||||
},
|
||||
],
|
||||
source: {
|
||||
type: 'Literal',
|
||||
value: '@theme/Tabs',
|
||||
raw: "'@theme/Tabs'",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ImportDeclaration',
|
||||
specifiers: [
|
||||
{
|
||||
type: 'ImportDefaultSpecifier',
|
||||
local: {type: 'Identifier', name: 'TabItem'},
|
||||
},
|
||||
],
|
||||
source: {
|
||||
type: 'Literal',
|
||||
value: '@theme/TabItem',
|
||||
raw: "'@theme/TabItem'",
|
||||
},
|
||||
},
|
||||
],
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const plugin: Plugin<[PluginOptions?]> = (options = {}): Transformer => {
|
||||
// @ts-expect-error: todo temporary
|
||||
const {sync = false, converters = ['yarn', 'pnpm']} = options;
|
||||
return (root) => {
|
||||
let transformed = false as boolean;
|
||||
let alreadyImported = false as boolean;
|
||||
let transformed = false;
|
||||
let alreadyImported = false;
|
||||
|
||||
visit(root, (node: Node) => {
|
||||
if (isImport(node) && node.value.includes('@theme/Tabs')) {
|
||||
if (isTabsImport(node)) {
|
||||
alreadyImported = true;
|
||||
}
|
||||
|
||||
if (isParent(node)) {
|
||||
let index = 0;
|
||||
while (index < node.children.length) {
|
||||
const child = node.children[index]!;
|
||||
if (matchNode(child)) {
|
||||
if (isNpm2Yarn(child)) {
|
||||
const result = transformNode(child, sync, converters);
|
||||
node.children.splice(index, 1, ...result);
|
||||
index += result.length;
|
||||
|
@ -106,8 +197,9 @@ const plugin: Plugin<[PluginOptions?]> = (options = {}) => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (transformed && !alreadyImported) {
|
||||
(root as Parent).children.unshift(nodeForImport);
|
||||
(root as Parent).children.unshift(createImportNode());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
"@docusaurus/utils": "^3.0.0-alpha.0",
|
||||
"@docusaurus/utils-common": "^3.0.0-alpha.0",
|
||||
"@docusaurus/utils-validation": "^3.0.0-alpha.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@mdx-js/react": "^2.1.5",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"infima": "0.2.0-alpha.43",
|
||||
|
|
|
@ -312,14 +312,6 @@ export default function getSwizzleConfig(): SwizzleConfig {
|
|||
},
|
||||
description: 'The component used to render <details> tags in MDX',
|
||||
},
|
||||
'MDXComponents/Head': {
|
||||
actions: {
|
||||
eject: 'forbidden',
|
||||
wrap: 'forbidden',
|
||||
},
|
||||
description:
|
||||
'Technical component used to assign metadata (generally for SEO purpose) to the current MDX document',
|
||||
},
|
||||
'MDXComponents/Heading': {
|
||||
actions: {
|
||||
eject: 'safe',
|
||||
|
|
|
@ -861,14 +861,6 @@ declare module '@theme/MDXComponents/Img' {
|
|||
export default function MDXImg(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/MDXComponents/Head' {
|
||||
import type {ComponentProps} from 'react';
|
||||
|
||||
export interface Props extends ComponentProps<'head'> {}
|
||||
|
||||
export default function MDXHead(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/MDXComponents/Heading' {
|
||||
import type {ComponentProps} from 'react';
|
||||
import type Heading from '@theme/Heading';
|
||||
|
@ -889,7 +881,6 @@ declare module '@theme/MDXComponents/Pre' {
|
|||
declare module '@theme/MDXComponents' {
|
||||
import type {ComponentType, ComponentProps} from 'react';
|
||||
|
||||
import type MDXHead from '@theme/MDXComponents/Head';
|
||||
import type MDXCode from '@theme/MDXComponents/Code';
|
||||
import type MDXA from '@theme/MDXComponents/A';
|
||||
import type MDXPre from '@theme/MDXComponents/Pre';
|
||||
|
@ -898,13 +889,14 @@ declare module '@theme/MDXComponents' {
|
|||
import type MDXImg from '@theme/MDXComponents/Img';
|
||||
import type Admonition from '@theme/Admonition';
|
||||
import type Mermaid from '@theme/Mermaid';
|
||||
import type Head from '@docusaurus/Head';
|
||||
|
||||
export type MDXComponentsObject = {
|
||||
readonly head: typeof MDXHead;
|
||||
readonly Head: typeof Head;
|
||||
readonly Details: typeof MDXDetails;
|
||||
readonly code: typeof MDXCode;
|
||||
readonly a: typeof MDXA;
|
||||
readonly pre: typeof MDXPre;
|
||||
readonly details: typeof MDXDetails;
|
||||
readonly ul: typeof MDXUl;
|
||||
readonly img: typeof MDXImg;
|
||||
readonly h1: (props: ComponentProps<'h1'>) => JSX.Element;
|
||||
|
|
|
@ -6,50 +6,13 @@
|
|||
*/
|
||||
|
||||
import type {ComponentProps} from 'react';
|
||||
import React, {isValidElement} from 'react';
|
||||
import React from 'react';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import type {Props} from '@theme/MDXComponents/Code';
|
||||
|
||||
export default function MDXCode(props: Props): JSX.Element {
|
||||
const inlineElements: (string | undefined)[] = [
|
||||
'a',
|
||||
'abbr',
|
||||
'b',
|
||||
'br',
|
||||
'button',
|
||||
'cite',
|
||||
'code',
|
||||
'del',
|
||||
'dfn',
|
||||
'em',
|
||||
'i',
|
||||
'img',
|
||||
'input',
|
||||
'ins',
|
||||
'kbd',
|
||||
'label',
|
||||
'object',
|
||||
'output',
|
||||
'q',
|
||||
'ruby',
|
||||
's',
|
||||
'small',
|
||||
'span',
|
||||
'strong',
|
||||
'sub',
|
||||
'sup',
|
||||
'time',
|
||||
'u',
|
||||
'var',
|
||||
'wbr',
|
||||
];
|
||||
const shouldBeInline = React.Children.toArray(props.children).every(
|
||||
(el) =>
|
||||
(typeof el === 'string' && !el.includes('\n')) ||
|
||||
(isValidElement(el) &&
|
||||
inlineElements.includes(
|
||||
(el.props as {mdxType: string} | null)?.mdxType,
|
||||
)),
|
||||
(el) => typeof el === 'string' && !el.includes('\n'),
|
||||
);
|
||||
|
||||
return shouldBeInline ? (
|
||||
|
|
|
@ -15,8 +15,7 @@ export default function MDXDetails(props: Props): JSX.Element {
|
|||
// Details theme component
|
||||
const summary = items.find(
|
||||
(item): item is ReactElement<ComponentProps<'summary'>> =>
|
||||
React.isValidElement(item) &&
|
||||
(item.props as {mdxType: string} | null)?.mdxType === 'summary',
|
||||
React.isValidElement(item) && item.type === 'summary',
|
||||
);
|
||||
const children = <>{items.filter((item) => item !== summary)}</>;
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React, {type ReactElement} from 'react';
|
||||
import Head, {type Props as HeadProps} from '@docusaurus/Head';
|
||||
import type {Props} from '@theme/MDXComponents/Head';
|
||||
|
||||
type MDXElement = ReactElement<
|
||||
{mdxType?: string; originalType?: string} | undefined
|
||||
>;
|
||||
|
||||
// MDX elements are wrapped through the MDX pragma. In some cases (notably usage
|
||||
// with Head/Helmet) we need to unwrap those elements.
|
||||
function unwrapMDXElement(element: MDXElement) {
|
||||
if (element.props?.mdxType && element.props.originalType) {
|
||||
const {mdxType, originalType, ...newProps} = element.props;
|
||||
return React.createElement(element.props.originalType, newProps);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
export default function MDXHead(props: Props): JSX.Element {
|
||||
const unwrappedChildren = React.Children.map(props.children, (child) =>
|
||||
React.isValidElement(child) ? unwrapMDXElement(child as MDXElement) : child,
|
||||
);
|
||||
return <Head {...(props as HeadProps)}>{unwrappedChildren}</Head>;
|
||||
}
|
|
@ -5,19 +5,11 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {isValidElement} from 'react';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import React, {type ReactNode} from 'react';
|
||||
import type {Props} from '@theme/MDXComponents/Pre';
|
||||
|
||||
export default function MDXPre(props: Props): JSX.Element {
|
||||
return (
|
||||
<CodeBlock
|
||||
// If this pre is created by a ``` fenced codeblock, unwrap the children
|
||||
{...(isValidElement(props.children) &&
|
||||
(props.children.props as {originalType: string} | null)?.originalType ===
|
||||
'code'
|
||||
? props.children.props
|
||||
: {...props})}
|
||||
/>
|
||||
);
|
||||
export default function MDXPre(props: Props): ReactNode | undefined {
|
||||
// With MDX 2, this element is only used for fenced code blocks
|
||||
// It always receives a MDXComponents/Code as children
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import MDXHead from '@theme/MDXComponents/Head';
|
||||
import Head from '@docusaurus/Head';
|
||||
import MDXCode from '@theme/MDXComponents/Code';
|
||||
import MDXA from '@theme/MDXComponents/A';
|
||||
import MDXPre from '@theme/MDXComponents/Pre';
|
||||
|
@ -20,11 +20,11 @@ import Mermaid from '@theme/Mermaid';
|
|||
import type {MDXComponentsObject} from '@theme/MDXComponents';
|
||||
|
||||
const MDXComponents: MDXComponentsObject = {
|
||||
head: MDXHead,
|
||||
Head,
|
||||
Details: MDXDetails,
|
||||
code: MDXCode,
|
||||
a: MDXA,
|
||||
pre: MDXPre,
|
||||
details: MDXDetails,
|
||||
ul: MDXUl,
|
||||
img: MDXImg,
|
||||
h1: (props) => <MDXHeading as="h1" {...props} />,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import {MDXProvider} from '@mdx-js/react';
|
||||
import MDXComponents from '@theme/MDXComponents';
|
||||
import type {Props} from '@theme/MDXContent';
|
||||
|
|
|
@ -10,6 +10,7 @@ import clsx from 'clsx';
|
|||
import {
|
||||
useScrollPositionBlocker,
|
||||
useTabs,
|
||||
sanitizeTabsChildren,
|
||||
type TabItemProps,
|
||||
} from '@docusaurus/theme-common/internal';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
|
@ -152,7 +153,8 @@ export default function Tabs(props: Props): JSX.Element {
|
|||
// Remount tabs after hydration
|
||||
// Temporary fix for https://github.com/facebook/docusaurus/issues/5653
|
||||
key={String(isBrowser)}
|
||||
{...props}
|
||||
/>
|
||||
{...props}>
|
||||
{sanitizeTabsChildren(props.children)}
|
||||
</TabsComponent>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ CSS variables, meant to be overridden by final theme
|
|||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.collapsibleContent > *:last-child {
|
||||
.collapsibleContent p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.details > summary > p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export {
|
|||
useAnnouncementBar,
|
||||
} from './contexts/announcementBar';
|
||||
|
||||
export {useTabs} from './utils/tabsUtils';
|
||||
export {useTabs, sanitizeTabsChildren} from './utils/tabsUtils';
|
||||
export type {TabValue, TabsProps, TabItemProps} from './utils/tabsUtils';
|
||||
|
||||
export {useNavbarMobileSidebar} from './contexts/navbarMobileSidebar';
|
||||
|
|
|
@ -14,15 +14,16 @@ function extractMDXAdmonitionTitle(children: ReactNode): {
|
|||
rest: ReactNode;
|
||||
} {
|
||||
const items = React.Children.toArray(children);
|
||||
const mdxAdmonitionTitle = items.find(
|
||||
(item) =>
|
||||
React.isValidElement(item) &&
|
||||
(item.props as {mdxType: string} | null)?.mdxType ===
|
||||
'mdxAdmonitionTitle',
|
||||
const mdxAdmonitionTitleWrapper = items.find(
|
||||
(item) => React.isValidElement(item) && item.type === 'mdxAdmonitionTitle',
|
||||
) as JSX.Element | undefined;
|
||||
const rest = items.filter((item) => item !== mdxAdmonitionTitle);
|
||||
|
||||
const rest = items.filter((item) => item !== mdxAdmonitionTitleWrapper);
|
||||
|
||||
const mdxAdmonitionTitle = mdxAdmonitionTitleWrapper?.props.children;
|
||||
|
||||
return {
|
||||
mdxAdmonitionTitle: mdxAdmonitionTitle?.props.children,
|
||||
mdxAdmonitionTitle,
|
||||
rest: rest.length > 0 ? <>{rest}</> : null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -61,25 +61,27 @@ function isTabItem(
|
|||
return !!props && typeof props === 'object' && 'value' in props;
|
||||
}
|
||||
|
||||
function ensureValidChildren(children: TabsProps['children']) {
|
||||
return (React.Children.map(children, (child) => {
|
||||
// Pass falsy values through: allow conditionally not rendering a tab
|
||||
if (!child || (isValidElement(child) && isTabItem(child))) {
|
||||
return child;
|
||||
}
|
||||
// child.type.name will give non-sensical values in prod because of
|
||||
// minification, but we assume it won't throw in prod.
|
||||
throw new Error(
|
||||
`Docusaurus error: Bad <Tabs> child <${
|
||||
// @ts-expect-error: guarding against unexpected cases
|
||||
typeof child.type === 'string' ? child.type : child.type.name
|
||||
}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`,
|
||||
);
|
||||
})?.filter(Boolean) ?? []) as ReactElement<TabItemProps>[];
|
||||
export function sanitizeTabsChildren(children: TabsProps['children']) {
|
||||
return (React.Children.toArray(children)
|
||||
.filter((child) => child !== '\n')
|
||||
.map((child) => {
|
||||
if (!child || (isValidElement(child) && isTabItem(child))) {
|
||||
return child;
|
||||
}
|
||||
// child.type.name will give non-sensical values in prod because of
|
||||
// minification, but we assume it won't throw in prod.
|
||||
throw new Error(
|
||||
`Docusaurus error: Bad <Tabs> child <${
|
||||
// @ts-expect-error: guarding against unexpected cases
|
||||
typeof child.type === 'string' ? child.type : child.type.name
|
||||
}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`,
|
||||
);
|
||||
})
|
||||
?.filter(Boolean) ?? []) as ReactElement<TabItemProps>[];
|
||||
}
|
||||
|
||||
function extractChildrenTabValues(children: TabsProps['children']): TabValue[] {
|
||||
return ensureValidChildren(children).map(
|
||||
return sanitizeTabsChildren(children).map(
|
||||
({props: {value, label, attributes, default: isDefault}}) => ({
|
||||
value,
|
||||
label,
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
"@docusaurus/theme-common": "^3.0.0-alpha.0",
|
||||
"@docusaurus/types": "^3.0.0-alpha.0",
|
||||
"@docusaurus/utils-validation": "^3.0.0-alpha.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"mermaid": "^9.4.3",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
|
|
20
packages/docusaurus-types/src/config.d.ts
vendored
20
packages/docusaurus-types/src/config.d.ts
vendored
|
@ -27,7 +27,25 @@ export type MarkdownConfig = {
|
|||
* @see https://docusaurus.io/docs/markdown-features/diagrams/
|
||||
* @default false
|
||||
*/
|
||||
mermaid?: boolean;
|
||||
mermaid: boolean;
|
||||
|
||||
/**
|
||||
* Gives opportunity to preprocess the MDX string content before compiling.
|
||||
* A good escape hatch that can be used to handle edge cases.
|
||||
*
|
||||
* @param args
|
||||
*/
|
||||
preprocessor?: (args: {filePath: string; fileContent: string}) => string;
|
||||
|
||||
/**
|
||||
* Set of flags make it easier to upgrade from MDX 1 to MDX 2
|
||||
* See also https://github.com/facebook/docusaurus/issues/4029
|
||||
*/
|
||||
mdx1Compat: {
|
||||
comments: boolean;
|
||||
admonitions: boolean;
|
||||
headingIds: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,27 +2,19 @@
|
|||
|
||||
exports[`validation schemas admonitionsSchema: for value=[] 1`] = `""value" does not look like a valid admonitions config"`;
|
||||
|
||||
exports[`validation schemas admonitionsSchema: for value={"customTypes":{"myKeyword":{"keyword":"myKeyword","infima":true,"svg":"<svg width=\\"512px\\" height=\\"512px\\" viewBox=\\"0 0 512 512\\" xmlns=\\"http://www.w3.org/2000/svg\\"></svg>"}}} 1`] = `
|
||||
"The Docusaurus admonitions system has changed, and the option "customTypes" does not exist anymore.
|
||||
You now need to swizzle the admonitions component to provide UI customizations such as icons.
|
||||
Please refer to https://github.com/facebook/docusaurus/pull/7152 for detailed upgrade instructions."
|
||||
`;
|
||||
exports[`validation schemas admonitionsSchema: for value={"customTypes":{"myKeyword":{"keyword":"myKeyword","infima":true,"svg":"<svg width=\\"512px\\" height=\\"512px\\" viewBox=\\"0 0 512 512\\" xmlns=\\"http://www.w3.org/2000/svg\\"></svg>"}}} 1`] = `""customTypes" is not allowed"`;
|
||||
|
||||
exports[`validation schemas admonitionsSchema: for value={"icons":"emoji"} 1`] = `
|
||||
"The Docusaurus admonitions system has changed, and the option "icons" does not exist anymore.
|
||||
You now need to swizzle the admonitions component to provide UI customizations such as icons.
|
||||
Please refer to https://github.com/facebook/docusaurus/pull/7152 for detailed upgrade instructions."
|
||||
`;
|
||||
exports[`validation schemas admonitionsSchema: for value={"icons":"emoji"} 1`] = `""icons" is not allowed"`;
|
||||
|
||||
exports[`validation schemas admonitionsSchema: for value={"infima":true} 1`] = `
|
||||
"The Docusaurus admonitions system has changed, and the option "infima" does not exist anymore.
|
||||
You now need to swizzle the admonitions component to provide UI customizations such as icons.
|
||||
Please refer to https://github.com/facebook/docusaurus/pull/7152 for detailed upgrade instructions."
|
||||
`;
|
||||
exports[`validation schemas admonitionsSchema: for value={"infima":true} 1`] = `""infima" is not allowed"`;
|
||||
|
||||
exports[`validation schemas admonitionsSchema: for value={"keywords":["custom-keyword"],"extendDefaults":42} 1`] = `""extendDefaults" must be a boolean"`;
|
||||
|
||||
exports[`validation schemas admonitionsSchema: for value={"tag":""} 1`] = `""tag" is not allowed to be empty"`;
|
||||
exports[`validation schemas admonitionsSchema: for value={"tag":""} 1`] = `"It is not possible anymore to use a custom admonition tag. The only admonition tag supported is ':::' (Markdown Directive syntax)"`;
|
||||
|
||||
exports[`validation schemas admonitionsSchema: for value={"tag":"+++","keywords":["info","tip"]} 1`] = `"It is not possible anymore to use a custom admonition tag. The only admonition tag supported is ':::' (Markdown Directive syntax)"`;
|
||||
|
||||
exports[`validation schemas admonitionsSchema: for value={"tag":"+++"} 1`] = `"It is not possible anymore to use a custom admonition tag. The only admonition tag supported is ':::' (Markdown Directive syntax)"`;
|
||||
|
||||
exports[`validation schemas admonitionsSchema: for value={"unknownAttribute":"val"} 1`] = `""unknownAttribute" is not allowed"`;
|
||||
|
||||
|
|
|
@ -97,22 +97,22 @@ describe('validation schemas', () => {
|
|||
testOK(true);
|
||||
testOK(false);
|
||||
testOK({});
|
||||
testOK({tag: '+++'});
|
||||
testOK({keywords: ['info', 'tip']});
|
||||
testOK({keywords: ['info', 'tip'], extendDefaults: true});
|
||||
testOK({keywords: ['info', 'tip'], extendDefaults: false});
|
||||
testOK({keywords: []});
|
||||
testOK({keywords: [], extendDefaults: true}); // noop
|
||||
testOK({keywords: [], extendDefaults: false}); // disable admonitions
|
||||
testOK({tag: '+++', keywords: ['info', 'tip']});
|
||||
testOK({tag: '+++', keywords: ['custom-keyword'], extendDefaults: true});
|
||||
testOK({tag: '+++', keywords: ['custom-keyword'], extendDefaults: false});
|
||||
testOK({keywords: ['custom-keyword'], extendDefaults: true});
|
||||
testOK({keywords: ['custom-keyword'], extendDefaults: false});
|
||||
|
||||
testFail(3);
|
||||
testFail([]);
|
||||
testFail({unknownAttribute: 'val'});
|
||||
testFail({tag: ''});
|
||||
testFail({keywords: ['custom-keyword'], extendDefaults: 42});
|
||||
testFail({tag: '+++'});
|
||||
testFail({tag: '+++', keywords: ['info', 'tip']});
|
||||
|
||||
// Legacy types
|
||||
testFail({
|
||||
|
|
|
@ -37,17 +37,10 @@ const MarkdownPluginsSchema = Joi.array()
|
|||
export const RemarkPluginsSchema = MarkdownPluginsSchema;
|
||||
export const RehypePluginsSchema = MarkdownPluginsSchema;
|
||||
|
||||
const LegacyAdmonitionConfigSchema = Joi.forbidden().messages({
|
||||
'any.unknown': `The Docusaurus admonitions system has changed, and the option {#label} does not exist anymore.
|
||||
You now need to swizzle the admonitions component to provide UI customizations such as icons.
|
||||
Please refer to https://github.com/facebook/docusaurus/pull/7152 for detailed upgrade instructions.`,
|
||||
});
|
||||
|
||||
export const AdmonitionsSchema = JoiFrontMatter.alternatives()
|
||||
.try(
|
||||
JoiFrontMatter.boolean().required(),
|
||||
JoiFrontMatter.object({
|
||||
tag: JoiFrontMatter.string(),
|
||||
keywords: JoiFrontMatter.array().items(
|
||||
JoiFrontMatter.string(),
|
||||
// Apparently this is how we tell job to accept empty arrays...
|
||||
|
@ -55,10 +48,10 @@ export const AdmonitionsSchema = JoiFrontMatter.alternatives()
|
|||
),
|
||||
extendDefaults: JoiFrontMatter.boolean(),
|
||||
|
||||
// TODO Remove before 2023
|
||||
customTypes: LegacyAdmonitionConfigSchema,
|
||||
icons: LegacyAdmonitionConfigSchema,
|
||||
infima: LegacyAdmonitionConfigSchema,
|
||||
// TODO Remove before 2024
|
||||
tag: Joi.any().forbidden().messages({
|
||||
'any.unknown': `It is not possible anymore to use a custom admonition tag. The only admonition tag supported is ':::' (Markdown Directive syntax)`,
|
||||
}),
|
||||
}).required(),
|
||||
)
|
||||
.default(true)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import logger from '@docusaurus/logger';
|
||||
import Yaml from 'js-yaml';
|
||||
import {PluginIdSchema} from './validationSchemas';
|
||||
import type {ValidationOptions} from 'joi';
|
||||
import type Joi from './Joi';
|
||||
|
||||
/** Print warnings returned from Joi validation. */
|
||||
|
@ -77,13 +78,15 @@ export function normalizeThemeConfig<T>(
|
|||
* Validate front matter with better error message
|
||||
*/
|
||||
export function validateFrontMatter<T>(
|
||||
frontMatter: {[key: string]: unknown},
|
||||
frontMatter: unknown,
|
||||
schema: Joi.ObjectSchema<T>,
|
||||
options?: ValidationOptions,
|
||||
): T {
|
||||
const {value, error, warning} = schema.validate(frontMatter, {
|
||||
convert: true,
|
||||
allowUnknown: true,
|
||||
abortEarly: false,
|
||||
...options,
|
||||
});
|
||||
|
||||
printWarning(warning);
|
||||
|
|
|
@ -12,6 +12,9 @@ import {
|
|||
parseMarkdownString,
|
||||
parseMarkdownHeadingId,
|
||||
writeMarkdownHeadingId,
|
||||
escapeMarkdownHeadingIds,
|
||||
unwrapMdxCodeBlocks,
|
||||
admonitionTitleToDirectiveLabel,
|
||||
} from '../markdownUtils';
|
||||
|
||||
describe('createExcerpt', () => {
|
||||
|
@ -916,6 +919,556 @@ describe('parseMarkdownHeadingId', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('escapeMarkdownHeadingIds', () => {
|
||||
it('can escape simple heading id', () => {
|
||||
expect(escapeMarkdownHeadingIds('# title 1 {#id-1}')).toBe(
|
||||
'# title 1 \\{#id-1}',
|
||||
);
|
||||
expect(escapeMarkdownHeadingIds('# title 1 {#id-1}')).toBe(
|
||||
'# title 1 \\{#id-1}',
|
||||
);
|
||||
expect(escapeMarkdownHeadingIds('# title 1{#id-1}')).toBe(
|
||||
'# title 1\\{#id-1}',
|
||||
);
|
||||
expect(escapeMarkdownHeadingIds('# title 1 \\{#id-1}')).toBe(
|
||||
'# title 1 \\{#id-1}',
|
||||
);
|
||||
expect(escapeMarkdownHeadingIds('# title 1\\{#id-1}')).toBe(
|
||||
'# title 1\\{#id-1}',
|
||||
);
|
||||
});
|
||||
|
||||
it('can escape level 1-6 heading ids', () => {
|
||||
expect(
|
||||
escapeMarkdownHeadingIds(dedent`
|
||||
# title 1 {#id-1}
|
||||
|
||||
## title 2 {#id-2}
|
||||
|
||||
### title 3 {#id-3}
|
||||
|
||||
#### title 4 {#id-4}
|
||||
|
||||
##### title 5 {#id-5}
|
||||
|
||||
###### title 6 {#id-6}
|
||||
`),
|
||||
).toEqual(dedent`
|
||||
# title 1 \{#id-1}
|
||||
|
||||
## title 2 \{#id-2}
|
||||
|
||||
### title 3 \{#id-3}
|
||||
|
||||
#### title 4 \{#id-4}
|
||||
|
||||
##### title 5 \{#id-5}
|
||||
|
||||
###### title 6 \{#id-6}
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not escape level 7 heading id', () => {
|
||||
expect(
|
||||
escapeMarkdownHeadingIds(dedent`
|
||||
####### title 7 {#id-7}
|
||||
`),
|
||||
).toEqual(dedent`
|
||||
####### title 7 {#id-7}
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not escape non-heading', () => {
|
||||
expect(
|
||||
escapeMarkdownHeadingIds(dedent`
|
||||
some text {#non-id}
|
||||
`),
|
||||
).toEqual(dedent`
|
||||
some text {#non-id}
|
||||
`);
|
||||
});
|
||||
|
||||
it('works for realistic example', () => {
|
||||
expect(
|
||||
escapeMarkdownHeadingIds(dedent`
|
||||
# Support
|
||||
|
||||
Docusaurus has a community of thousands of developers.
|
||||
|
||||
On this page we've listed some Docusaurus-related communities that you can be a part of; see the other pages in this section for additional online and in-person learning materials.
|
||||
|
||||
Before participating in Docusaurus' communities, [please read our Code of Conduct](https://engineering.fb.com/codeofconduct/). We have adopted the [Contributor Covenant](https://www.contributor-covenant.org/) and we expect that all community members adhere to the guidelines within.
|
||||
|
||||
## Stack Overflow {#stack-overflow}
|
||||
|
||||
Stack Overflow is a popular forum to ask code-level questions or if you're stuck with a specific error. Read through the [existing questions](https://stackoverflow.com/questions/tagged/docusaurus) tagged with **docusaurus** or [ask your own](https://stackoverflow.com/questions/ask?tags=docusaurus)!
|
||||
|
||||
## Discussion forums \{#discussion-forums}
|
||||
|
||||
There are many online forums for discussion about best practices and application architecture as well as the future of Docusaurus. If you have an answerable code-level question, Stack Overflow is usually a better fit.
|
||||
|
||||
- [Docusaurus online chat](https://discord.gg/docusaurus)
|
||||
- [#help-and-questions](https://discord.gg/fwbcrQ3dHR) for user help
|
||||
- [#contributors](https://discord.gg/6g6ASPA) for contributing help
|
||||
- [Reddit's Docusaurus community](https://www.reddit.com/r/docusaurus/)
|
||||
|
||||
## Feature requests {#feature-requests}
|
||||
|
||||
For new feature requests, you can create a post on our [feature requests board (Canny)](/feature-requests), which is a handy tool for road-mapping and allows for sorting by upvotes, which gives the core team a better indicator of what features are in high demand, as compared to GitHub issues which are harder to triage. Refrain from making a Pull Request for new features (especially large ones) as someone might already be working on it or will be part of our roadmap. Talk to us first!
|
||||
|
||||
## News {#news}
|
||||
|
||||
For the latest news about Docusaurus, [follow **@docusaurus** on Twitter](https://twitter.com/docusaurus) and the [official Docusaurus blog](/blog) on this website.
|
||||
`),
|
||||
).toEqual(dedent`
|
||||
# Support
|
||||
|
||||
Docusaurus has a community of thousands of developers.
|
||||
|
||||
On this page we've listed some Docusaurus-related communities that you can be a part of; see the other pages in this section for additional online and in-person learning materials.
|
||||
|
||||
Before participating in Docusaurus' communities, [please read our Code of Conduct](https://engineering.fb.com/codeofconduct/). We have adopted the [Contributor Covenant](https://www.contributor-covenant.org/) and we expect that all community members adhere to the guidelines within.
|
||||
|
||||
## Stack Overflow \{#stack-overflow}
|
||||
|
||||
Stack Overflow is a popular forum to ask code-level questions or if you're stuck with a specific error. Read through the [existing questions](https://stackoverflow.com/questions/tagged/docusaurus) tagged with **docusaurus** or [ask your own](https://stackoverflow.com/questions/ask?tags=docusaurus)!
|
||||
|
||||
## Discussion forums \{#discussion-forums}
|
||||
|
||||
There are many online forums for discussion about best practices and application architecture as well as the future of Docusaurus. If you have an answerable code-level question, Stack Overflow is usually a better fit.
|
||||
|
||||
- [Docusaurus online chat](https://discord.gg/docusaurus)
|
||||
- [#help-and-questions](https://discord.gg/fwbcrQ3dHR) for user help
|
||||
- [#contributors](https://discord.gg/6g6ASPA) for contributing help
|
||||
- [Reddit's Docusaurus community](https://www.reddit.com/r/docusaurus/)
|
||||
|
||||
## Feature requests \{#feature-requests}
|
||||
|
||||
For new feature requests, you can create a post on our [feature requests board (Canny)](/feature-requests), which is a handy tool for road-mapping and allows for sorting by upvotes, which gives the core team a better indicator of what features are in high demand, as compared to GitHub issues which are harder to triage. Refrain from making a Pull Request for new features (especially large ones) as someone might already be working on it or will be part of our roadmap. Talk to us first!
|
||||
|
||||
## News \{#news}
|
||||
|
||||
For the latest news about Docusaurus, [follow **@docusaurus** on Twitter](https://twitter.com/docusaurus) and the [official Docusaurus blog](/blog) on this website.
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwrapMdxCodeBlocks', () => {
|
||||
it('can unwrap a simple mdx code block', () => {
|
||||
expect(
|
||||
unwrapMdxCodeBlocks(dedent`
|
||||
# Title
|
||||
|
||||
\`\`\`mdx-code-block
|
||||
import Comp, {User} from "@site/components/comp"
|
||||
|
||||
<Comp prop="test">
|
||||
<User user={{firstName: "Sébastien"}} />
|
||||
</Comp>
|
||||
|
||||
export const age = 36
|
||||
\`\`\`
|
||||
|
||||
text
|
||||
`),
|
||||
).toEqual(dedent`
|
||||
# Title
|
||||
|
||||
import Comp, {User} from "@site/components/comp"
|
||||
|
||||
<Comp prop="test">
|
||||
<User user={{firstName: "Sébastien"}} />
|
||||
</Comp>
|
||||
|
||||
export const age = 36
|
||||
|
||||
text
|
||||
`);
|
||||
});
|
||||
|
||||
it('can unwrap a nested mdx code block', () => {
|
||||
expect(
|
||||
unwrapMdxCodeBlocks(dedent`
|
||||
# Title
|
||||
|
||||
\`\`\`\`mdx-code-block
|
||||
|
||||
some content
|
||||
|
||||
\`\`\`js
|
||||
export const age = 36
|
||||
\`\`\`
|
||||
|
||||
\`\`\`\`
|
||||
|
||||
text
|
||||
`),
|
||||
).toEqual(dedent`
|
||||
# Title
|
||||
|
||||
|
||||
some content
|
||||
|
||||
\`\`\`js
|
||||
export const age = 36
|
||||
\`\`\`
|
||||
|
||||
|
||||
text
|
||||
`);
|
||||
});
|
||||
|
||||
it('works for realistic example', () => {
|
||||
expect(
|
||||
unwrapMdxCodeBlocks(dedent`
|
||||
# Canary releases
|
||||
|
||||
\`\`\`mdx-code-block
|
||||
import {
|
||||
VersionsProvider,
|
||||
} from "@site/src/components/Versions";
|
||||
|
||||
<VersionsProvider prop={{attr: 42}} test="yes">
|
||||
\`\`\`
|
||||
|
||||
Docusaurus has a canary releases system.
|
||||
|
||||
It permits you to **test new unreleased features** as soon as the pull requests are merged on the [next version](./5-release-process.md#next-version) of Docusaurus.
|
||||
|
||||
It is a good way to **give feedback to maintainers**, ensuring the newly implemented feature works as intended.
|
||||
|
||||
:::note
|
||||
|
||||
Using a canary release in production might seem risky, but in practice, it's not.
|
||||
|
||||
A canary release passes all automated tests and is used in production by the Docusaurus site itself.
|
||||
|
||||
\`\`\`mdx-code-block
|
||||
</VersionsProvider>
|
||||
\`\`\`
|
||||
`),
|
||||
).toEqual(dedent`
|
||||
# Canary releases
|
||||
|
||||
import {
|
||||
VersionsProvider,
|
||||
} from "@site/src/components/Versions";
|
||||
|
||||
<VersionsProvider prop={{attr: 42}} test="yes">
|
||||
|
||||
Docusaurus has a canary releases system.
|
||||
|
||||
It permits you to **test new unreleased features** as soon as the pull requests are merged on the [next version](./5-release-process.md#next-version) of Docusaurus.
|
||||
|
||||
It is a good way to **give feedback to maintainers**, ensuring the newly implemented feature works as intended.
|
||||
|
||||
:::note
|
||||
|
||||
Using a canary release in production might seem risky, but in practice, it's not.
|
||||
|
||||
A canary release passes all automated tests and is used in production by the Docusaurus site itself.
|
||||
|
||||
</VersionsProvider>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admonitionTitleToDirectiveLabel', () => {
|
||||
const directives = ['info', 'note', 'tip', 'caution'];
|
||||
|
||||
it('does not transform markdown without any admonition', () => {
|
||||
expect(
|
||||
admonitionTitleToDirectiveLabel(
|
||||
dedent`
|
||||
# Title
|
||||
|
||||
intro
|
||||
|
||||
## Sub Title
|
||||
|
||||
content
|
||||
`,
|
||||
directives,
|
||||
),
|
||||
).toEqual(dedent`
|
||||
# Title
|
||||
|
||||
intro
|
||||
|
||||
## Sub Title
|
||||
|
||||
content
|
||||
`);
|
||||
});
|
||||
|
||||
it('transform simple admonition', () => {
|
||||
expect(
|
||||
admonitionTitleToDirectiveLabel(
|
||||
dedent`
|
||||
before
|
||||
|
||||
:::note Title
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`,
|
||||
directives,
|
||||
),
|
||||
).toEqual(dedent`
|
||||
before
|
||||
|
||||
:::note[Title]
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not transform already transformed admonition', () => {
|
||||
expect(
|
||||
admonitionTitleToDirectiveLabel(
|
||||
dedent`
|
||||
before
|
||||
|
||||
:::note[Title]
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`,
|
||||
directives,
|
||||
),
|
||||
).toEqual(dedent`
|
||||
before
|
||||
|
||||
:::note[Title]
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not transform non-container directives', () => {
|
||||
expect(
|
||||
admonitionTitleToDirectiveLabel(
|
||||
dedent`
|
||||
before
|
||||
|
||||
::note Title
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`,
|
||||
directives,
|
||||
),
|
||||
).toEqual(dedent`
|
||||
before
|
||||
|
||||
::note Title
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not transform left-padded directives', () => {
|
||||
expect(
|
||||
admonitionTitleToDirectiveLabel(
|
||||
dedent`
|
||||
before
|
||||
|
||||
:::note Title
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`,
|
||||
directives,
|
||||
),
|
||||
).toEqual(dedent`
|
||||
before
|
||||
|
||||
:::note Title
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not transform admonition without title', () => {
|
||||
expect(
|
||||
admonitionTitleToDirectiveLabel(
|
||||
dedent`
|
||||
before
|
||||
|
||||
:::note
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`,
|
||||
directives,
|
||||
),
|
||||
).toEqual(dedent`
|
||||
before
|
||||
|
||||
:::note
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not transform non-admonition directive', () => {
|
||||
expect(
|
||||
admonitionTitleToDirectiveLabel(
|
||||
dedent`
|
||||
before
|
||||
|
||||
:::whatever Title
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`,
|
||||
directives,
|
||||
),
|
||||
).toEqual(dedent`
|
||||
before
|
||||
|
||||
:::whatever Title
|
||||
|
||||
content
|
||||
|
||||
:::
|
||||
|
||||
after
|
||||
`);
|
||||
});
|
||||
|
||||
it('transform real-world nested messy admonitions', () => {
|
||||
expect(
|
||||
admonitionTitleToDirectiveLabel(
|
||||
dedent`
|
||||
---
|
||||
title: "contains :::note"
|
||||
---
|
||||
|
||||
# Title
|
||||
|
||||
intro
|
||||
|
||||
::::note note **title**
|
||||
|
||||
note content
|
||||
|
||||
::::tip tip <span>title</span>
|
||||
|
||||
tip content
|
||||
|
||||
:::whatever whatever title
|
||||
|
||||
whatever content
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
:::::
|
||||
|
||||
## Heading {#my-heading}
|
||||
|
||||
::::info weird spaced title
|
||||
|
||||
into content
|
||||
|
||||
:::tip[tip directiveLabel]
|
||||
|
||||
tip content
|
||||
|
||||
::::
|
||||
|
||||
## Conclusion
|
||||
|
||||
end
|
||||
`,
|
||||
directives,
|
||||
),
|
||||
).toEqual(dedent`
|
||||
---
|
||||
title: "contains :::note"
|
||||
---
|
||||
|
||||
# Title
|
||||
|
||||
intro
|
||||
|
||||
::::note[note **title**]
|
||||
|
||||
note content
|
||||
|
||||
::::tip[tip <span>title</span>]
|
||||
|
||||
tip content
|
||||
|
||||
:::whatever whatever title
|
||||
|
||||
whatever content
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
:::::
|
||||
|
||||
## Heading {#my-heading}
|
||||
|
||||
::::info[weird spaced title]
|
||||
|
||||
into content
|
||||
|
||||
:::tip[tip directiveLabel]
|
||||
|
||||
tip content
|
||||
|
||||
::::
|
||||
|
||||
## Conclusion
|
||||
|
||||
end
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeMarkdownHeadingId', () => {
|
||||
it('works for simple level-2 heading', () => {
|
||||
expect(writeMarkdownHeadingId('## ABC')).toBe('## ABC {#abc}');
|
||||
|
|
|
@ -66,6 +66,9 @@ export {
|
|||
} from './tags';
|
||||
export {
|
||||
parseMarkdownHeadingId,
|
||||
escapeMarkdownHeadingIds,
|
||||
unwrapMdxCodeBlocks,
|
||||
admonitionTitleToDirectiveLabel,
|
||||
createExcerpt,
|
||||
parseFrontMatter,
|
||||
parseMarkdownContentTitle,
|
||||
|
|
|
@ -39,6 +39,75 @@ export function parseMarkdownHeadingId(heading: string): {
|
|||
return {text: heading, id: undefined};
|
||||
}
|
||||
|
||||
/**
|
||||
* MDX 2 requires escaping { with a \ so our anchor syntax need that now.
|
||||
* See https://mdxjs.com/docs/troubleshooting-mdx/#could-not-parse-expression-with-acorn-error
|
||||
*/
|
||||
export function escapeMarkdownHeadingIds(content: string): string {
|
||||
const markdownHeadingRegexp = /(?:^|\n)#{1,6}(?!#).*/g;
|
||||
return content.replaceAll(markdownHeadingRegexp, (substring) =>
|
||||
// TODO probably not the most efficient impl...
|
||||
substring
|
||||
.replace('{#', '\\{#')
|
||||
// prevent duplicate escaping
|
||||
.replace('\\\\{#', '\\{#'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hacky temporary escape hatch for Crowdin bad MDX support
|
||||
* See https://docusaurus.io/docs/i18n/crowdin#mdx
|
||||
*
|
||||
* TODO Titus suggested a clean solution based on ```mdx eval and Remark
|
||||
* See https://github.com/mdx-js/mdx/issues/701#issuecomment-947030041
|
||||
*
|
||||
* @param content
|
||||
*/
|
||||
export function unwrapMdxCodeBlocks(content: string): string {
|
||||
// We only support 3/4 backticks on purpose, should be good enough
|
||||
const regexp3 =
|
||||
/(?<begin>^|\n)```mdx-code-block\n(?<children>.*?)\n```(?<end>\n|$)/gs;
|
||||
const regexp4 =
|
||||
/(?<begin>^|\n)````mdx-code-block\n(?<children>.*?)\n````(?<end>\n|$)/gs;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const replacer = (substring: string, ...args: any[]) => {
|
||||
const groups = args.at(-1);
|
||||
return `${groups.begin}${groups.children}${groups.end}`;
|
||||
};
|
||||
|
||||
return content.replaceAll(regexp3, replacer).replaceAll(regexp4, replacer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add support for our legacy ":::note Title" admonition syntax
|
||||
* Not supported by https://github.com/remarkjs/remark-directive
|
||||
* Syntax is transformed to ":::note[Title]" (container directive label)
|
||||
* See https://talk.commonmark.org/t/generic-directives-plugins-syntax/444
|
||||
*
|
||||
* @param content
|
||||
* @param admonitionContainerDirectives
|
||||
*/
|
||||
export function admonitionTitleToDirectiveLabel(
|
||||
content: string,
|
||||
admonitionContainerDirectives: string[],
|
||||
): string {
|
||||
// this will also process ":::note Title" inside docs code blocks
|
||||
// good enough: we fixed older versions docs to not be affected
|
||||
|
||||
const directiveNameGroup = `(${admonitionContainerDirectives.join('|')})`;
|
||||
const regexp = new RegExp(
|
||||
`^(?<directive>:{3,}${directiveNameGroup}) +(?<title>.*)$`,
|
||||
'gm',
|
||||
);
|
||||
|
||||
return content.replaceAll(regexp, (substring, ...args: any[]) => {
|
||||
const groups = args.at(-1);
|
||||
|
||||
return `${groups.directive}[${groups.title}]`;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Find a better way to do so, possibly by compiling the Markdown content,
|
||||
// stripping out HTML tags and obtaining the first line.
|
||||
/**
|
||||
|
|
|
@ -17,7 +17,13 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
|||
"path": "i18n",
|
||||
},
|
||||
"markdown": {
|
||||
"mdx1Compat": {
|
||||
"admonitions": true,
|
||||
"comments": true,
|
||||
"headingIds": true,
|
||||
},
|
||||
"mermaid": false,
|
||||
"preprocessor": undefined,
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
@ -58,7 +64,13 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
|||
"path": "i18n",
|
||||
},
|
||||
"markdown": {
|
||||
"mdx1Compat": {
|
||||
"admonitions": true,
|
||||
"comments": true,
|
||||
"headingIds": true,
|
||||
},
|
||||
"mermaid": false,
|
||||
"preprocessor": undefined,
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
@ -101,7 +113,13 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
|||
"path": "i18n",
|
||||
},
|
||||
"markdown": {
|
||||
"mdx1Compat": {
|
||||
"admonitions": true,
|
||||
"comments": true,
|
||||
"headingIds": true,
|
||||
},
|
||||
"mermaid": false,
|
||||
"preprocessor": undefined,
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
@ -144,7 +162,13 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
|||
"path": "i18n",
|
||||
},
|
||||
"markdown": {
|
||||
"mdx1Compat": {
|
||||
"admonitions": true,
|
||||
"comments": true,
|
||||
"headingIds": true,
|
||||
},
|
||||
"mermaid": false,
|
||||
"preprocessor": undefined,
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
@ -190,7 +214,13 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
|||
"path": "i18n",
|
||||
},
|
||||
"markdown": {
|
||||
"mdx1Compat": {
|
||||
"admonitions": true,
|
||||
"comments": true,
|
||||
"headingIds": true,
|
||||
},
|
||||
"mermaid": false,
|
||||
"preprocessor": undefined,
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
|
|
@ -91,7 +91,13 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
|||
"path": "i18n",
|
||||
},
|
||||
"markdown": {
|
||||
"mdx1Compat": {
|
||||
"admonitions": true,
|
||||
"comments": true,
|
||||
"headingIds": true,
|
||||
},
|
||||
"mermaid": false,
|
||||
"preprocessor": undefined,
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('normalizeConfig', () => {
|
|||
});
|
||||
|
||||
it('accepts correctly defined config options', () => {
|
||||
const userConfig = {
|
||||
const userConfig: Config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...baseConfig,
|
||||
tagline: 'my awesome site',
|
||||
|
@ -60,6 +60,12 @@ describe('normalizeConfig', () => {
|
|||
],
|
||||
markdown: {
|
||||
mermaid: true,
|
||||
preprocessor: ({fileContent}) => fileContent,
|
||||
mdx1Compat: {
|
||||
comments: true,
|
||||
admonitions: false,
|
||||
headingIds: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const normalizedConfig = normalizeConfig(userConfig);
|
||||
|
@ -496,6 +502,12 @@ describe('markdown', () => {
|
|||
it('accepts valid markdown object', () => {
|
||||
const markdown: DocusaurusConfig['markdown'] = {
|
||||
mermaid: true,
|
||||
preprocessor: ({fileContent}) => fileContent,
|
||||
mdx1Compat: {
|
||||
comments: false,
|
||||
admonitions: true,
|
||||
headingIds: false,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
|
@ -504,6 +516,51 @@ describe('markdown', () => {
|
|||
).toEqual(expect.objectContaining({markdown}));
|
||||
});
|
||||
|
||||
it('accepts partial markdown object', () => {
|
||||
const markdown: DeepPartial<DocusaurusConfig['markdown']> = {
|
||||
mdx1Compat: {
|
||||
admonitions: true,
|
||||
headingIds: false,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
markdown,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
markdown: {
|
||||
...DEFAULT_CONFIG.markdown,
|
||||
...markdown,
|
||||
mdx1Compat: {
|
||||
...DEFAULT_CONFIG.markdown.mdx1Compat,
|
||||
...markdown.mdx1Compat,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('throw for preprocessor bad arity', () => {
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
markdown: {preprocessor: () => 'content'},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""markdown.preprocessor" must have an arity of 1
|
||||
"
|
||||
`);
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
// @ts-expect-error: types forbid this
|
||||
markdown: {preprocessor: (arg1, arg2) => String(arg1) + String(arg2)},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""markdown.preprocessor" must have an arity of 1
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('throw for null object', () => {
|
||||
expect(() => {
|
||||
normalizeConfig({
|
||||
|
|
|
@ -66,6 +66,12 @@ export const DEFAULT_CONFIG: Pick<
|
|||
staticDirectories: [DEFAULT_STATIC_DIR_NAME],
|
||||
markdown: {
|
||||
mermaid: false,
|
||||
preprocessor: undefined,
|
||||
mdx1Compat: {
|
||||
comments: true,
|
||||
admonitions: true,
|
||||
headingIds: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -271,6 +277,21 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
|
|||
}).optional(),
|
||||
markdown: Joi.object({
|
||||
mermaid: Joi.boolean().default(DEFAULT_CONFIG.markdown.mermaid),
|
||||
preprocessor: Joi.function()
|
||||
.arity(1)
|
||||
.optional()
|
||||
.default(() => DEFAULT_CONFIG.markdown.preprocessor),
|
||||
mdx1Compat: Joi.object({
|
||||
comments: Joi.boolean().default(
|
||||
DEFAULT_CONFIG.markdown.mdx1Compat.comments,
|
||||
),
|
||||
admonitions: Joi.boolean().default(
|
||||
DEFAULT_CONFIG.markdown.mdx1Compat.admonitions,
|
||||
),
|
||||
headingIds: Joi.boolean().default(
|
||||
DEFAULT_CONFIG.markdown.mdx1Compat.headingIds,
|
||||
),
|
||||
}).default(DEFAULT_CONFIG.markdown.mdx1Compat),
|
||||
}).default(DEFAULT_CONFIG.markdown),
|
||||
}).messages({
|
||||
'docusaurus.configValidationWarning':
|
||||
|
|
|
@ -40,7 +40,9 @@ export function formatStatsErrorMessage(
|
|||
// Also the error causal chain is lost here
|
||||
// We log the stacktrace inside serverEntry.tsx for now (not ideal)
|
||||
const {errors} = formatWebpackMessages(statsJson);
|
||||
return errors.join('\n---\n');
|
||||
return errors
|
||||
.map((str) => logger.red(str))
|
||||
.join(`\n\n${logger.yellow('--------------------------')}\n\n`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -137,6 +137,7 @@ immer
|
|||
infima
|
||||
inlines
|
||||
intelli
|
||||
intellij
|
||||
interactiveness
|
||||
interpolatable
|
||||
investec
|
||||
|
@ -179,6 +180,7 @@ mdast
|
|||
mdxa
|
||||
mdxast
|
||||
mdxhast
|
||||
mdxjs
|
||||
metadatum
|
||||
metastring
|
||||
metrica
|
||||
|
@ -190,6 +192,9 @@ mkcert
|
|||
mkdir
|
||||
mkdirs
|
||||
mkdocs
|
||||
mkdn
|
||||
mdwn
|
||||
mkdown
|
||||
moesif
|
||||
msapplication
|
||||
nabors
|
||||
|
@ -389,6 +394,7 @@ wcag
|
|||
webfactory
|
||||
webp
|
||||
webpackbar
|
||||
webstorm
|
||||
wolcott
|
||||
writeups
|
||||
xclip
|
||||
|
@ -400,3 +406,4 @@ yangshunz
|
|||
zhou
|
||||
zoomable
|
||||
zpao
|
||||
hastscript
|
||||
|
|
|
@ -7,7 +7,7 @@ toc_max_heading_level: 4
|
|||
tags: [paginated-tag]
|
||||
---
|
||||
|
||||
<!-- truncate -->
|
||||
{/* truncate */}
|
||||
|
||||
import Content, {
|
||||
toc as ContentToc,
|
||||
|
|
|
@ -16,7 +16,7 @@ hide_reading_time: true
|
|||
|
||||
Some MDX tests, mostly to test how the RSS feed render those
|
||||
|
||||
<!-- truncate -->
|
||||
{/* truncate */}
|
||||
|
||||
## Imports
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ tags:
|
|||
|
||||
Some MDX tests, mostly to test how the RSS feed render those
|
||||
|
||||
<!-- truncate -->
|
||||
{/* truncate */}
|
||||
|
||||
Test MDX with require calls
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ tags: [paginated-tag, blog, docusaurus]
|
|||
|
||||
Did you know you can use multiple instances of the same plugin?
|
||||
|
||||
<!--truncate-->
|
||||
{/* truncate */}
|
||||
|
||||
:::tip
|
||||
|
||||
|
|
|
@ -16,24 +16,22 @@
|
|||
|
||||
## Large font icon
|
||||
|
||||
import Admonition from '@theme/Admonition';
|
||||
|
||||
```mdx-code-block
|
||||
<admonition
|
||||
<Admonition
|
||||
type="tip"
|
||||
icon={<span style={{fontSize: 60}}>💡</span>}
|
||||
title="Did you know...">
|
||||
<p>
|
||||
content
|
||||
</p>
|
||||
</admonition>
|
||||
</Admonition>
|
||||
|
||||
<admonition
|
||||
<Admonition
|
||||
type="info"
|
||||
icon={<span style={{fontSize: 40}}>ℹ️</span>}
|
||||
title="Did you know...">
|
||||
<p>
|
||||
content
|
||||
</p>
|
||||
</admonition>
|
||||
</Admonition>
|
||||
```
|
||||
|
||||
## Large svg icon
|
||||
|
@ -41,21 +39,17 @@
|
|||
```mdx-code-block
|
||||
import InfoIcon from "@theme/Admonition/Icon/Info"
|
||||
|
||||
<admonition
|
||||
<Admonition
|
||||
type="tip"
|
||||
icon={<InfoIcon style={{width: 60, height: 60}}/>}
|
||||
title="Did you know...">
|
||||
<p>
|
||||
content
|
||||
</p>
|
||||
</admonition>
|
||||
</Admonition>
|
||||
|
||||
<admonition
|
||||
<Admonition
|
||||
type="info"
|
||||
icon={<InfoIcon style={{width: 40, height: 40}}/>}
|
||||
title="Did you know...">
|
||||
<p>
|
||||
content
|
||||
</p>
|
||||
</admonition>
|
||||
</Admonition>
|
||||
```
|
||||
|
|
|
@ -5,6 +5,14 @@ import TabItem from '@theme/TabItem';
|
|||
|
||||
# Code block tests
|
||||
|
||||
:::danger legacy test page - MDX v1
|
||||
|
||||
This test page is quite outdated: MDX v2 lowercase tags are not substituted anymore in the same way as they were in v1.
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
```java
|
||||
class HelloWorld {
|
||||
public static void main(String args[]) {
|
||||
|
@ -27,7 +35,7 @@ Multi-line text inside `pre` will turn into one-liner, but it's okay (https://gi
|
|||
|
||||
<pre>1 2 3</pre>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
{/* prettier-ignore */}
|
||||
<pre>
|
||||
1
|
||||
2
|
||||
|
|
|
@ -18,6 +18,11 @@ import Readme from "../README.mdx"
|
|||
<Readme />
|
||||
```
|
||||
|
||||
### Markdown tests
|
||||
|
||||
- [Markdown tests MDX](/tests/pages/markdown-tests-mdx)
|
||||
- [Markdown tests MD](/tests/pages/markdown-tests-md)
|
||||
|
||||
### Other tests
|
||||
|
||||
- [Crash test](/tests/pages/crashTest)
|
||||
|
@ -25,8 +30,6 @@ import Readme from "../README.mdx"
|
|||
- [Link tests](/tests/pages/link-tests)
|
||||
- [Error boundary tests](/tests/pages/error-boundary-tests)
|
||||
- [Hydration tests](/tests/pages/hydration-tests)
|
||||
- [Asset linking tests](/tests/pages/markdown-tests)
|
||||
- [General Markdown tests](/tests/pages/markdownPageTests)
|
||||
- [TOC tests](/tests/pages/page-toc-tests)
|
||||
- [Diagram tests](/tests/pages/diagrams)
|
||||
- [Tabs tests](/tests/pages/tabs-tests)
|
||||
|
|
44
website/_dogfooding/_pages tests/markdown-tests-md.md
Normal file
44
website/_dogfooding/_pages tests/markdown-tests-md.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
title: Markdown Page tests title
|
||||
description: Markdown Page tests description
|
||||
wrapperClassName: docusaurus-markdown-example
|
||||
---
|
||||
|
||||
# Markdown .md tests
|
||||
|
||||
This file should be interpreted in a more CommonMark compliant way
|
||||
|
||||
## Comment
|
||||
|
||||
Html comment: <!-- comment -->
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
MDX comment: {/* comment */}
|
||||
|
||||
## JSX syntax
|
||||
|
||||
import BrowserWindow from '@site/src/components/BrowserWindow';
|
||||
|
||||
<BrowserWindow>
|
||||
|
||||
BrowserWindow content
|
||||
|
||||
<BrowserWindow/>
|
||||
|
||||
export const answer = 42;
|
||||
|
||||
Test {xyz}
|
||||
|
||||
## Admonition
|
||||
|
||||
Admonitions still work
|
||||
|
||||
:::note[title]
|
||||
|
||||
note
|
||||
|
||||
:::
|
||||
|
||||
## Heading Id {#custom-heading-id}
|
||||
|
||||
Custom heading syntax `{#custom-heading-id}` still works
|
|
@ -4,7 +4,7 @@ description: Markdown Page tests description
|
|||
wrapperClassName: docusaurus-markdown-example
|
||||
---
|
||||
|
||||
# Markdown page tests
|
||||
# Markdown .mdx tests
|
||||
|
||||
This is a page generated from Markdown to illustrate the Markdown page feature and test some edge cases.
|
||||
|
||||
|
@ -51,20 +51,12 @@ import TabItem from '@theme/TabItem';
|
|||
MDX comments can be used with
|
||||
|
||||
```mdx
|
||||
<!--
|
||||
|
||||
My comment
|
||||
|
||||
-->
|
||||
{/* My comment */}
|
||||
```
|
||||
|
||||
See, nothing is displayed:
|
||||
|
||||
<!--
|
||||
|
||||
My comment
|
||||
|
||||
-->
|
||||
{/* My comment */}
|
||||
|
||||
## Import code block from source code file
|
||||
|
||||
|
@ -180,7 +172,7 @@ function Clock(props) {
|
|||
|
||||
### Weird heading {#a b}
|
||||
|
||||
### Weird heading {#a{b}
|
||||
### Weird heading {#a\{b}
|
||||
|
||||
## Pipe
|
||||
|
||||
|
@ -247,9 +239,19 @@ Can be arbitrarily nested:
|
|||
- [ ] Task
|
||||
- [ ] Task
|
||||
|
||||
## Emojis
|
||||
|
||||
Emojis in this text will be replaced with [remark-emoji](https://www.npmjs.com/package/remark-emoji): :dog: :+1:
|
||||
|
||||
## Admonitions
|
||||
|
||||
:::caution Interpolated `title` with a <button style={{color: "red"}} onClick={() => alert("it works")}>button</button>
|
||||
:::caution Interpolated `title` with a <button style={{color: "red"}} onClick={() => alert("it works")}>button</button> (old syntax)
|
||||
|
||||
Admonition body
|
||||
|
||||
:::
|
||||
|
||||
:::caution[Interpolated `title` with a <button style={{color: "red"}} onClick={() => alert("it works")}>button</button> (directive label syntax)]
|
||||
|
||||
Admonition body
|
||||
|
||||
|
@ -281,4 +283,60 @@ hey
|
|||
|
||||
:::::
|
||||
|
||||
after admonition
|
||||
::::tip[Use tabs in admonitions]
|
||||
|
||||
:::info[Admonition nested]
|
||||
|
||||
test
|
||||
|
||||
:::
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="apple" label="Apple">
|
||||
|
||||
:::note[Admonition in tab]
|
||||
|
||||
test
|
||||
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="orange" label="Orange">This is an orange 🍊</TabItem>
|
||||
<TabItem value="banana" label="Banana">This is a banana 🍌</TabItem>
|
||||
</Tabs>
|
||||
|
||||
::::
|
||||
|
||||
## Linking
|
||||
|
||||
This is a test page to see if Docusaurus Markdown features are working properly
|
||||
|
||||
### Linking to assets
|
||||
|
||||
See [#3337](https://github.com/facebook/docusaurus/issues/3337)
|
||||
|
||||
- [/someFile.pdf](/someFile.pdf)
|
||||
|
||||
- [/someFile.xyz](/someFile.xyz)
|
||||
|
||||
- [@site/\_dogfooding/\_asset-tests/someFile.pdf](@site/_dogfooding/_asset-tests/someFile.pdf)
|
||||
|
||||
- [@site/\_dogfooding/\_asset-tests/someFile.xyz](@site/_dogfooding/_asset-tests/someFile.xyz)
|
||||
|
||||
### Linking to non-SPA page hosted within website
|
||||
|
||||
See [#3309](https://github.com/facebook/docusaurus/issues/3309)
|
||||
|
||||
- [pathname:///dogfooding/javadoc](pathname:///dogfooding/javadoc)
|
||||
|
||||
- [pathname:///dogfooding/javadoc/index.html](pathname:///dogfooding/javadoc/index.html)
|
||||
|
||||
- [pathname://../dogfooding/javadoc](pathname://../dogfooding/javadoc)
|
||||
|
||||
- [pathname://../dogfooding/javadoc/index.html](pathname://../dogfooding/javadoc/index.html)
|
||||
|
||||
### Linking to JSON
|
||||
|
||||
- [./script.js](./_script.js)
|
||||
|
||||
- [./data.json](./data.json)
|
|
@ -1,33 +0,0 @@
|
|||
# Markdown tests
|
||||
|
||||
This is a test page to see if Docusaurus Markdown features are working properly
|
||||
|
||||
## Linking to assets
|
||||
|
||||
See [#3337](https://github.com/facebook/docusaurus/issues/3337)
|
||||
|
||||
- [/someFile.pdf](/someFile.pdf)
|
||||
|
||||
- [/someFile.xyz](/someFile.xyz)
|
||||
|
||||
- [@site/\_dogfooding/\_asset-tests/someFile.pdf](@site/_dogfooding/_asset-tests/someFile.pdf)
|
||||
|
||||
- [@site/\_dogfooding/\_asset-tests/someFile.xyz](@site/_dogfooding/_asset-tests/someFile.xyz)
|
||||
|
||||
## Linking to non-SPA page hosted within website
|
||||
|
||||
See [#3309](https://github.com/facebook/docusaurus/issues/3309)
|
||||
|
||||
- [pathname:///dogfooding/javadoc](pathname:///dogfooding/javadoc)
|
||||
|
||||
- [pathname:///dogfooding/javadoc/index.html](pathname:///dogfooding/javadoc/index.html)
|
||||
|
||||
- [pathname://../dogfooding/javadoc](pathname://../dogfooding/javadoc)
|
||||
|
||||
- [pathname://../dogfooding/javadoc/index.html](pathname://../dogfooding/javadoc/index.html)
|
||||
|
||||
## Linking to JSON
|
||||
|
||||
- [./script.js](./_script.js)
|
||||
|
||||
- [./data.json](./data.json)
|
|
@ -21,15 +21,12 @@ It all started with this [RFC issue](https://github.com/facebook/docusaurus/issu
|
|||
<blockquote>
|
||||
<h4>
|
||||
<a href="https://github.com/facebook/docusaurus/issues/789">
|
||||
[RFC] Docusaurus v2 · Issue #789 · facebook/docusaurus
|
||||
{'[RFC] Docusaurus v2 · Issue #789 · facebook/docusaurus'}
|
||||
</a>
|
||||
</h4>
|
||||
<p>
|
||||
These are some of the problems I'm seeing in Docusaurus now and also how we
|
||||
can address them in v2. A number of the ideas here were inspired by VuePress
|
||||
and other static site generators. In the current static site generators
|
||||
ecosystem, t...
|
||||
</p>
|
||||
These are some of the problems I'm seeing in Docusaurus now and also how we can
|
||||
address them in v2. A number of the ideas here were inspired by VuePress and other
|
||||
static site generators. In the current static site generators ecosystem, t...
|
||||
</blockquote>
|
||||
|
||||
Most of the suggested improvements are mentioned in the issue; I will provide details on some of issues in Docusaurus 1 and how we are going to address them in Docusaurus 2.
|
||||
|
|
|
@ -32,7 +32,7 @@ After **4 years of work, [75 alphas](https://github.com/facebook/docusaurus/rele
|
|||
|
||||

|
||||
|
||||
<!--truncate-->
|
||||
{/* truncate */}
|
||||
|
||||
:::info We are on [ProductHunt](https://www.producthunt.com/posts/docusaurus-2-0) and [Hacker News](https://news.ycombinator.com/item?id=32303052)!
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ The upgrade should be easy: as explained in our [release process documentation](
|
|||
|
||||

|
||||
|
||||
<!--truncate-->
|
||||
{/* truncate */}
|
||||
|
||||
## Highlights
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ The i18n configuration object to [localize your site](../i18n/i18n-introduction.
|
|||
|
||||
Example:
|
||||
|
||||
<!-- cSpell:ignore فارسی -->
|
||||
{/* cSpell:ignore فارسی */}
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
|
|
|
@ -61,7 +61,7 @@ Accepted fields:
|
|||
| `rehypePlugins` | `any[]` | `[]` | Rehype plugins passed to MDX. |
|
||||
| `beforeDefaultRemarkPlugins` | `any[]` | `[]` | Custom Remark plugins passed to MDX before the default Docusaurus Remark plugins. |
|
||||
| `beforeDefaultRehypePlugins` | `any[]` | `[]` | Custom Rehype plugins passed to MDX before the default Docusaurus Rehype plugins. |
|
||||
| `truncateMarker` | `RegExp` | `/<!--\s*(truncate)\s*-->/` | Truncate marker marking where the summary ends. |
|
||||
| `truncateMarker` | `RegExp` | `/<!--\s*truncate\s*-->/` \| `\{\/\*\s*truncate\s*\*\/\}/` | Truncate marker marking where the summary ends. |
|
||||
| `showReadingTime` | `boolean` | `true` | Show estimated reading time for the blog post. |
|
||||
| `readingTime` | `ReadingTimeFn` | The default reading time | A callback to customize the reading time number displayed. |
|
||||
| `authorsMapPath` | `string` | `'authors.yml'` | Path to the authors map file, relative to the blog content directory. |
|
||||
|
@ -69,7 +69,7 @@ Accepted fields:
|
|||
| `feedOptions.type` | <code><a href="#FeedType">FeedType</a> \| <a href="#FeedType">FeedType</a>[] \| 'all' \| null</code> | **Required** | Type of feed to be generated. Use `null` to disable generation. |
|
||||
| `feedOptions.createFeedItems` | <code><a href="#CreateFeedItemsFn">CreateFeedItemsFn</a> \| undefined</code> | `undefined` | An optional function which can be used to transform and / or filter the items in the feed. |
|
||||
| `feedOptions.title` | `string` | `siteConfig.title` | Title of the feed. |
|
||||
| `feedOptions.description` | `string` | <code>\`${siteConfig.title} Blog\`</code> | Description of the feed. |
|
||||
| `feedOptions.description` | `string` | <code>\`$\{siteConfig.title} Blog\`</code> | Description of the feed. |
|
||||
| `feedOptions.copyright` | `string` | `undefined` | Copyright message. |
|
||||
| `feedOptions.language` | `string` (See [documentation](http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes) for possible values) | `undefined` | Language metadata of the feed. |
|
||||
| `sortPosts` | <code>'descending' \| 'ascending' </code> | `'descending'` | Governs the direction of blog post sorting. |
|
||||
|
|
|
@ -63,7 +63,7 @@ hide_table_of_contents: false
|
|||
|
||||
Welcome to this blog. This blog is created with [**Docusaurus 2**](https://docusaurus.io/).
|
||||
|
||||
<!--truncate-->
|
||||
<!-- truncate -->
|
||||
|
||||
This is my first post on Docusaurus 2.
|
||||
|
||||
|
@ -78,22 +78,31 @@ The blog's index page (by default, it is at `/blog`) is the _blog list page_, wh
|
|||
|
||||
Use the `<!--truncate-->` marker in your blog post to represent what will be shown as the summary when viewing all published blog posts. Anything above `<!--truncate-->` will be part of the summary. For example:
|
||||
|
||||
```md
|
||||
```md title="website/blog/my-post.md" {7}
|
||||
---
|
||||
title: Truncation Example
|
||||
title: Markdown blog truncation example
|
||||
---
|
||||
|
||||
All these will be part of the blog post summary.
|
||||
|
||||
Even this.
|
||||
|
||||
<!--truncate-->
|
||||
<!-- truncate -->
|
||||
|
||||
But anything from here on down will not be.
|
||||
```
|
||||
|
||||
Not this.
|
||||
For files using the `.mdx` extension, use a [MDX](https://mdxjs.com/) comment `{/* truncate */}` instead:
|
||||
|
||||
Or this.
|
||||
{/* prettier-ignore */}
|
||||
```md title="website/blog/my-post.mdx" {7}
|
||||
---
|
||||
title: MDX blog truncation Example
|
||||
---
|
||||
|
||||
All these will be part of the blog post summary.
|
||||
|
||||
{/* truncate */}
|
||||
|
||||
But anything from here on down will not be.
|
||||
```
|
||||
|
||||
By default, 10 posts are shown on each blog list page, but you can control pagination with the `postsPerPage` option in the plugin configuration. If you set `postsPerPage: 'ALL'`, pagination will be disabled and all posts will be displayed on the first page. You can also add a meta description to the blog list page for better SEO:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue