Skip to content

Commit

Permalink
Merge pull request #3962 from aloisklink/build/add-autogenerated-head…
Browse files Browse the repository at this point in the history
…er-after-yaml-codeblocks

Add support for YAML frontmatter in Markdown docs (used for Vitepress config)
  • Loading branch information
sidharthv96 committed Jan 25, 2023
2 parents c256a6b + 816f2f5 commit 0aa7da2
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 77 deletions.
1 change: 1 addition & 0 deletions packages/mermaid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
121 changes: 82 additions & 39 deletions packages/mermaid/src/docs.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
82 changes: 49 additions & 33 deletions packages/mermaid/src/docs.spec.ts
Original file line number Diff line number Diff line change
@@ -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 { vi, afterEach, describe, it, expect } from 'vitest';

const remark = remarkBuilder();

vi.mock('remark', async (importOriginal) => {
const { remark: originalRemarkBuilder } = (await importOriginal()) as {
remark: typeof remarkBuilder;
};
import { transformMarkdownAst, transformToBlockQuote } from './docs.mjs';

// 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,
};
});
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';

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);
});

Expand All @@ -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'
);
Expand All @@ -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' +
Expand All @@ -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', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/mermaid/src/docs/syntax/flowchart.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading

0 comments on commit 0aa7da2

Please sign in to comment.