Merge pull request #3962 from aloisklink/build/add-autogenerated-header-after-yaml-codeblocks
Add support for YAML frontmatter in Markdown docs (used for Vitepress config)
This commit is contained in:
commit
0aa7da261f
|
@ -90,6 +90,7 @@
|
|||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^2.7.1",
|
||||
"remark": "^14.0.2",
|
||||
"remark-frontmatter": "^4.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
|
|
|
@ -37,16 +37,14 @@ import { JSDOM } from 'jsdom';
|
|||
import type { Code, Root } from 'mdast';
|
||||
import { posix, dirname, relative, join } from 'path';
|
||||
import prettier from 'prettier';
|
||||
import { remark as remarkBuilder } from 'remark';
|
||||
import { remark } from 'remark';
|
||||
import remarkFrontmatter from 'remark-frontmatter';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import chokidar from 'chokidar';
|
||||
import mm from 'micromatch';
|
||||
// @ts-ignore No typescript declaration file
|
||||
import flatmap from 'unist-util-flatmap';
|
||||
|
||||
// support tables and other GitHub Flavored Markdown syntax in markdown
|
||||
const remark = remarkBuilder().use(remarkGfm);
|
||||
|
||||
const MERMAID_MAJOR_VERSION = (
|
||||
JSON.parse(readFileSync('../mermaid/package.json', 'utf8')).version as string
|
||||
).split('.')[0];
|
||||
|
@ -201,48 +199,86 @@ const transformIncludeStatements = (file: string, text: string): string => {
|
|||
});
|
||||
};
|
||||
|
||||
/** Options for {@link transformMarkdownAst} */
|
||||
interface TransformMarkdownAstOptions {
|
||||
/**
|
||||
* Used to indicate the original/source file.
|
||||
*/
|
||||
originalFilename: string;
|
||||
/** If `true`, add a warning that the file is autogenerated */
|
||||
addAutogeneratedWarning?: boolean;
|
||||
/**
|
||||
* If `true`, remove the YAML metadata from the Markdown input.
|
||||
* Generally, YAML metadata is only used for Vitepress.
|
||||
*/
|
||||
removeYAML?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform code blocks in a Markdown file.
|
||||
* Use remark.parse() to turn the given content (a String) into an AST.
|
||||
* Remark plugin that transforms mermaid repo markdown to Vitepress/GFM markdown.
|
||||
*
|
||||
* For any AST node that is a code block: transform it as needed:
|
||||
* - blocks marked as MERMAID_DIAGRAM_ONLY will be set to a 'mermaid' code block so it will be rendered as (only) a diagram
|
||||
* - blocks marked as MERMAID_EXAMPLE_KEYWORDS will be copied and the original node will be a code only block and the copy with be rendered as the diagram
|
||||
* - blocks marked as BLOCK_QUOTE_KEYWORDS will be transformed into block quotes
|
||||
*
|
||||
* Convert the AST back to a string and return it.
|
||||
* If `addAutogeneratedWarning` is `true`, generates a header stating that this file is autogenerated.
|
||||
*
|
||||
* @param content - the contents of a Markdown file
|
||||
* @returns the contents with transformed code blocks
|
||||
* @returns plugin function for Remark
|
||||
*/
|
||||
export const transformBlocks = (content: string): string => {
|
||||
const ast: Root = remark.parse(content);
|
||||
const astWithTransformedBlocks = flatmap(ast, (node: Code) => {
|
||||
if (node.type !== 'code' || !node.lang) {
|
||||
return [node]; // no transformation if this is not a code block
|
||||
export function transformMarkdownAst({
|
||||
originalFilename,
|
||||
addAutogeneratedWarning,
|
||||
removeYAML,
|
||||
}: TransformMarkdownAstOptions) {
|
||||
return (tree: Root, _file?: any): Root => {
|
||||
const astWithTransformedBlocks = flatmap(tree, (node: Code) => {
|
||||
if (node.type !== 'code' || !node.lang) {
|
||||
return [node]; // no transformation if this is not a code block
|
||||
}
|
||||
|
||||
if (node.lang === MERMAID_DIAGRAM_ONLY) {
|
||||
// Set the lang to 'mermaid' so it will be rendered as a diagram.
|
||||
node.lang = MERMAID_KEYWORD;
|
||||
return [node];
|
||||
} else if (MERMAID_EXAMPLE_KEYWORDS.includes(node.lang)) {
|
||||
// Return 2 nodes:
|
||||
// 1. the original node with the language now set to 'mermaid-example' (will be rendered as code), and
|
||||
// 2. a copy of the original node with the language set to 'mermaid' (will be rendered as a diagram)
|
||||
node.lang = MERMAID_CODE_ONLY_KEYWORD;
|
||||
return [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
|
||||
}
|
||||
|
||||
// Transform these blocks into block quotes.
|
||||
if (BLOCK_QUOTE_KEYWORDS.includes(node.lang)) {
|
||||
return [remark.parse(transformToBlockQuote(node.value, node.lang, node.meta))];
|
||||
}
|
||||
|
||||
return [node]; // default is to do nothing to the node
|
||||
}) as Root;
|
||||
|
||||
if (addAutogeneratedWarning) {
|
||||
// Add the header to the start of the file
|
||||
const headerNode = remark.parse(generateHeader(originalFilename)).children[0];
|
||||
if (astWithTransformedBlocks.children[0].type === 'yaml') {
|
||||
// insert header after the YAML frontmatter if it exists
|
||||
astWithTransformedBlocks.children.splice(1, 0, headerNode);
|
||||
} else {
|
||||
astWithTransformedBlocks.children.unshift(headerNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.lang === MERMAID_DIAGRAM_ONLY) {
|
||||
// Set the lang to 'mermaid' so it will be rendered as a diagram.
|
||||
node.lang = MERMAID_KEYWORD;
|
||||
return [node];
|
||||
} else if (MERMAID_EXAMPLE_KEYWORDS.includes(node.lang)) {
|
||||
// Return 2 nodes:
|
||||
// 1. the original node with the language now set to 'mermaid-example' (will be rendered as code), and
|
||||
// 2. a copy of the original node with the language set to 'mermaid' (will be rendered as a diagram)
|
||||
node.lang = MERMAID_CODE_ONLY_KEYWORD;
|
||||
return [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
|
||||
if (removeYAML) {
|
||||
const firstNode = astWithTransformedBlocks.children[0];
|
||||
if (firstNode.type == 'yaml') {
|
||||
// YAML is currently only used for Vitepress metadata, so we should remove it for GFM output
|
||||
astWithTransformedBlocks.children.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// Transform these blocks into block quotes.
|
||||
if (BLOCK_QUOTE_KEYWORDS.includes(node.lang)) {
|
||||
return [remark.parse(transformToBlockQuote(node.value, node.lang, node.meta))];
|
||||
}
|
||||
|
||||
return [node]; // default is to do nothing to the node
|
||||
});
|
||||
|
||||
return remark.stringify(astWithTransformedBlocks);
|
||||
};
|
||||
return astWithTransformedBlocks;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a markdown file and write the transformed file to the directory for published
|
||||
|
@ -260,11 +296,18 @@ export const transformBlocks = (content: string): string => {
|
|||
*/
|
||||
const transformMarkdown = (file: string) => {
|
||||
const doc = injectPlaceholders(transformIncludeStatements(file, readSyncedUTF8file(file)));
|
||||
let transformed = transformBlocks(doc);
|
||||
if (!noHeader) {
|
||||
// Add the header to the start of the file
|
||||
transformed = `${generateHeader(file)}\n${transformed}`;
|
||||
}
|
||||
|
||||
let transformed = remark()
|
||||
.use(remarkGfm)
|
||||
.use(remarkFrontmatter, ['yaml']) // support YAML front-matter in Markdown
|
||||
.use(transformMarkdownAst, {
|
||||
// mermaid project specific plugin
|
||||
originalFilename: file,
|
||||
addAutogeneratedWarning: !noHeader,
|
||||
removeYAML: !noHeader,
|
||||
})
|
||||
.processSync(doc)
|
||||
.toString();
|
||||
|
||||
if (vitepress && file === 'src/docs/index.md') {
|
||||
// Skip transforming index if vitepress is enabled
|
||||
|
|
|
@ -1,40 +1,24 @@
|
|||
import { transformBlocks, transformToBlockQuote } from './docs.mjs';
|
||||
import { remark as remarkBuilder } from 'remark'; // import it this way so we can mock it
|
||||
import { transformMarkdownAst, transformToBlockQuote } from './docs.mjs';
|
||||
|
||||
import { remark } from 'remark'; // import it this way so we can mock it
|
||||
import remarkFrontmatter from 'remark-frontmatter';
|
||||
import { vi, afterEach, describe, it, expect } from 'vitest';
|
||||
|
||||
const remark = remarkBuilder();
|
||||
|
||||
vi.mock('remark', async (importOriginal) => {
|
||||
const { remark: originalRemarkBuilder } = (await importOriginal()) as {
|
||||
remark: typeof remarkBuilder;
|
||||
};
|
||||
|
||||
// make sure that both `docs.mts` and this test file are using the same remark
|
||||
// object so that we can mock it
|
||||
const sharedRemark = originalRemarkBuilder();
|
||||
return {
|
||||
remark: () => sharedRemark,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const originalFilename = 'example-input-filename.md';
|
||||
const remarkBuilder = remark().use(remarkFrontmatter, ['yaml']); // support YAML front-matter in Markdown
|
||||
|
||||
describe('docs.mts', () => {
|
||||
describe('transformBlocks', () => {
|
||||
it('uses remark.parse to create the AST for the file ', () => {
|
||||
const remarkParseSpy = vi
|
||||
.spyOn(remark, 'parse')
|
||||
.mockReturnValue({ type: 'root', children: [] });
|
||||
const contents = 'Markdown file contents';
|
||||
transformBlocks(contents);
|
||||
expect(remarkParseSpy).toHaveBeenCalledWith(contents);
|
||||
});
|
||||
describe('transformMarkdownAst', () => {
|
||||
describe('checks each AST node', () => {
|
||||
it('does no transformation if there are no code blocks', async () => {
|
||||
const contents = 'Markdown file contents\n';
|
||||
const result = transformBlocks(contents);
|
||||
const result = (
|
||||
await remarkBuilder().use(transformMarkdownAst, { originalFilename }).process(contents)
|
||||
).toString();
|
||||
expect(result).toEqual(contents);
|
||||
});
|
||||
|
||||
|
@ -46,8 +30,12 @@ describe('docs.mts', () => {
|
|||
const lang_keyword = 'mermaid-nocode';
|
||||
const contents = beforeCodeLine + '```' + lang_keyword + '\n' + diagram_text + '\n```\n';
|
||||
|
||||
it('changes the language to "mermaid"', () => {
|
||||
const result = transformBlocks(contents);
|
||||
it('changes the language to "mermaid"', async () => {
|
||||
const result = (
|
||||
await remarkBuilder()
|
||||
.use(transformMarkdownAst, { originalFilename })
|
||||
.process(contents)
|
||||
).toString();
|
||||
expect(result).toEqual(
|
||||
beforeCodeLine + '\n' + '```' + 'mermaid' + '\n' + diagram_text + '\n```\n'
|
||||
);
|
||||
|
@ -61,8 +49,12 @@ describe('docs.mts', () => {
|
|||
const contents =
|
||||
beforeCodeLine + '```' + lang_keyword + '\n' + diagram_text + '\n```\n';
|
||||
|
||||
it('changes the language to "mermaid-example" and adds a copy of the code block with language = "mermaid"', () => {
|
||||
const result = transformBlocks(contents);
|
||||
it('changes the language to "mermaid-example" and adds a copy of the code block with language = "mermaid"', async () => {
|
||||
const result = (
|
||||
await remarkBuilder()
|
||||
.use(transformMarkdownAst, { originalFilename })
|
||||
.process(contents)
|
||||
).toString();
|
||||
expect(result).toEqual(
|
||||
beforeCodeLine +
|
||||
'\n' +
|
||||
|
@ -77,16 +69,40 @@ describe('docs.mts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('calls transformToBlockQuote with the node information', () => {
|
||||
it('calls transformToBlockQuote with the node information', async () => {
|
||||
const lang_keyword = 'note';
|
||||
const contents =
|
||||
beforeCodeLine + '```' + lang_keyword + '\n' + 'This is the text\n' + '```\n';
|
||||
|
||||
const result = transformBlocks(contents);
|
||||
const result = (
|
||||
await remarkBuilder().use(transformMarkdownAst, { originalFilename }).process(contents)
|
||||
).toString();
|
||||
expect(result).toEqual(beforeCodeLine + '\n> **Note**\n' + '> This is the text\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove YAML if `removeYAML` is true', async () => {
|
||||
const contents = `---
|
||||
title: Flowcharts Syntax
|
||||
---
|
||||
|
||||
This Markdown should be kept.
|
||||
`;
|
||||
const withYaml = (
|
||||
await remarkBuilder().use(transformMarkdownAst, { originalFilename }).process(contents)
|
||||
).toString();
|
||||
// no change
|
||||
expect(withYaml).toEqual(contents);
|
||||
|
||||
const withoutYaml = (
|
||||
await remarkBuilder()
|
||||
.use(transformMarkdownAst, { originalFilename, removeYAML: true })
|
||||
.process(contents)
|
||||
).toString();
|
||||
// no change
|
||||
expect(withoutYaml).toEqual('This Markdown should be kept.\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformToBlockQuote', () => {
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
---
|
||||
title: Flowcharts Syntax
|
||||
outline: 'deep' # shows all h3 headings in outline in Vitepress
|
||||
---
|
||||
|
||||
# Flowcharts - Basic Syntax
|
||||
|
||||
All Flowcharts are composed of **nodes**, the geometric shapes and **edges**, the arrows or lines. The mermaid code defines the way that these **nodes** and **edges** are made and interact.
|
||||
|
|
|
@ -275,6 +275,9 @@ importers:
|
|||
remark:
|
||||
specifier: ^14.0.2
|
||||
version: 14.0.2
|
||||
remark-frontmatter:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
remark-gfm:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
|
@ -4106,7 +4109,7 @@ packages:
|
|||
/axios/0.21.4_debug@4.3.2:
|
||||
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.2_debug@4.3.2
|
||||
follow-redirects: 1.15.2
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: true
|
||||
|
@ -6799,6 +6802,12 @@ packages:
|
|||
reusify: 1.0.4
|
||||
dev: true
|
||||
|
||||
/fault/2.0.1:
|
||||
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
|
||||
dependencies:
|
||||
format: 0.2.2
|
||||
dev: true
|
||||
|
||||
/faye-websocket/0.11.4:
|
||||
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
@ -6898,7 +6907,7 @@ packages:
|
|||
resolution: {integrity: sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA==}
|
||||
dev: true
|
||||
|
||||
/follow-redirects/1.15.2_debug@4.3.2:
|
||||
/follow-redirects/1.15.2:
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
|
@ -6906,8 +6915,6 @@ packages:
|
|||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dependencies:
|
||||
debug: 4.3.2
|
||||
dev: true
|
||||
|
||||
/foreground-child/2.0.0:
|
||||
|
@ -6949,6 +6956,11 @@ packages:
|
|||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
/format/0.2.2:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
dev: true
|
||||
|
||||
/forwarded/0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -7442,7 +7454,7 @@ packages:
|
|||
engines: {node: '>=8.0.0'}
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
follow-redirects: 1.15.2_debug@4.3.2
|
||||
follow-redirects: 1.15.2
|
||||
requires-port: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
@ -8883,6 +8895,12 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/mdast-util-frontmatter/1.0.0:
|
||||
resolution: {integrity: sha512-7itKvp0arEVNpCktOET/eLFAYaZ+0cNjVtFtIPxgQ5tV+3i+D4SDDTjTzPWl44LT59PC+xdx+glNTawBdF98Mw==}
|
||||
dependencies:
|
||||
micromark-extension-frontmatter: 1.0.0
|
||||
dev: true
|
||||
|
||||
/mdast-util-gfm-autolink-literal/1.0.2:
|
||||
resolution: {integrity: sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg==}
|
||||
dependencies:
|
||||
|
@ -9053,6 +9071,14 @@ packages:
|
|||
uvu: 0.5.6
|
||||
dev: true
|
||||
|
||||
/micromark-extension-frontmatter/1.0.0:
|
||||
resolution: {integrity: sha512-EXjmRnupoX6yYuUJSQhrQ9ggK0iQtQlpi6xeJzVD5xscyAI+giqco5fdymayZhJMbIFecjnE2yz85S9NzIgQpg==}
|
||||
dependencies:
|
||||
fault: 2.0.1
|
||||
micromark-util-character: 1.1.0
|
||||
micromark-util-symbol: 1.0.1
|
||||
dev: true
|
||||
|
||||
/micromark-extension-gfm-autolink-literal/1.0.3:
|
||||
resolution: {integrity: sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==}
|
||||
dependencies:
|
||||
|
@ -10284,6 +10310,15 @@ packages:
|
|||
jsesc: 0.5.0
|
||||
dev: true
|
||||
|
||||
/remark-frontmatter/4.0.1:
|
||||
resolution: {integrity: sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA==}
|
||||
dependencies:
|
||||
'@types/mdast': 3.0.10
|
||||
mdast-util-frontmatter: 1.0.0
|
||||
micromark-extension-frontmatter: 1.0.0
|
||||
unified: 10.1.2
|
||||
dev: true
|
||||
|
||||
/remark-gfm/3.0.1:
|
||||
resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==}
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in New Issue