From 34aef1a6f50fec9f8c271994f1ccc266c07517df Mon Sep 17 00:00:00 2001 From: "Ashley Engelund (weedySeaDragon @ github)" Date: Mon, 26 Dec 2022 13:13:02 -0800 Subject: [PATCH 1/2] refactor: transformBlocks(); + MERMAID_DIAGRAM_ONLY behavior; comments, constants --- packages/mermaid/src/docs.mts | 94 +++++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/packages/mermaid/src/docs.mts b/packages/mermaid/src/docs.mts index 1833d8be70..58261b32ec 100644 --- a/packages/mermaid/src/docs.mts +++ b/packages/mermaid/src/docs.mts @@ -48,14 +48,26 @@ const MERMAID_MAJOR_VERSION = ( ).split('.')[0]; const CDN_URL = 'https://cdn.jsdelivr.net/npm'; // 'https://unpkg.com'; +const MERMAID_KEYWORD = 'mermaid'; +const MERMAID_CODE_ONLY_KEYWORD = 'mermaid-example'; + +// These keywords will produce both a mermaid diagram and a code block with the diagram source +const MERMAID_EXAMPLE_KEYWORDS = [MERMAID_KEYWORD, 'mmd', MERMAID_CODE_ONLY_KEYWORD]; // 'mmd' is an old keyword that used to be used + +// This keyword will only produce a mermaid diagram +const MERMAID_DIAGRAM_ONLY = 'mermaid-nocode'; + +// These will be transformed into block quotes +const BLOCK_QUOTE_KEYWORDS = ['note', 'tip', 'warning', 'danger']; + +// options for running the main command const verifyOnly: boolean = process.argv.includes('--verify'); const git: boolean = process.argv.includes('--git'); const watch: boolean = process.argv.includes('--watch'); const vitepress: boolean = process.argv.includes('--vitepress'); const noHeader: boolean = process.argv.includes('--noHeader') || vitepress; -// These paths are from the root of the mono-repo, not from the -// mermaid sub-directory +// These paths are from the root of the mono-repo, not from the mermaid subdirectory const SOURCE_DOCS_DIR = 'src/docs'; const FINAL_DOCS_DIR = vitepress ? 'src/vitepress' : '../../docs'; @@ -153,7 +165,11 @@ const blockIcons: Record = { const capitalize = (word: string) => word[0].toUpperCase() + word.slice(1); -const transformToBlockQuote = (content: string, type: string, customTitle?: string | null) => { +export const transformToBlockQuote = ( + content: string, + type: string, + customTitle?: string | null +) => { if (vitepress) { const vitepressType = type === 'note' ? 'info' : type; return `::: ${vitepressType} ${customTitle || ''}\n${content}\n:::`; @@ -180,41 +196,67 @@ const transformIncludeStatements = (file: string, text: string): string => { } }); }; + /** - * Transform a markdown file and write the transformed file to the directory for published - * documentation + * Transform code blocks in a Markdown file. + * Use remark.parse() to turn the given content (a String) into an AST. + * 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 * - * 1. Add a `mermaid-example` block before every `mermaid` or `mmd` block On the docsify site (one - * place where the documentation is published), this will show the code used for the mermaid - * diagram - * 2. Add the text that says the file is automatically generated - * 3. Use prettier to format the file Verify that the file has been changed and write out the changes + * Convert the AST back to a string and return it. * - * @param file {string} name of the file that will be verified + * @param content - the contents of a Markdown file + * @returns the contents with transformed code blocks */ -const transformMarkdown = (file: string) => { - const doc = injectPlaceholders(transformIncludeStatements(file, readSyncedUTF8file(file))); - const ast: Root = remark.parse(doc); - const out = flatmap(ast, (c: Code) => { - if (c.type !== 'code' || !c.lang) { - return [c]; +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 } - // Convert mermaid code blocks to mermaid-example blocks - if (['mermaid', 'mmd', 'mermaid-example'].includes(c.lang)) { - c.lang = 'mermaid-example'; - return [c, Object.assign({}, c, { lang: 'mermaid' })]; + 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 codeblocks into block quotes. - if (['note', 'tip', 'warning', 'danger'].includes(c.lang)) { - return [remark.parse(transformToBlockQuote(c.value, c.lang, c.meta))]; + // Transform these blocks into block quotes. + if (BLOCK_QUOTE_KEYWORDS.includes(node.lang)) { + return [remark.parse(transformToBlockQuote(node.value, node.lang, node.meta))]; } - return [c]; + return [node]; // default is to do nothing to the node }); - let transformed = remark.stringify(out); + return remark.stringify(astWithTransformedBlocks); +}; + +/** + * Transform a markdown file and write the transformed file to the directory for published + * documentation + * + * 1. include any included files (copy and insert the source) + * 2. Add a `mermaid-example` block before every `mermaid` or `mmd` block On the main documentation site (one + * place where the documentation is published), this will show the code for the mermaid diagram + * 3. Transform blocks to block quotes as needed + * 4. Add the text that says the file is automatically generated + * 5. Use prettier to format the file. + * 6. Verify that the file has been changed and write out the changes + * + * @param file {string} name of the file that will be verified + */ +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}`; From 78443861a610160734404982570cbe7e94cfdb77 Mon Sep 17 00:00:00 2001 From: "Ashley Engelund (weedySeaDragon @ github)" Date: Mon, 26 Dec 2022 13:13:47 -0800 Subject: [PATCH 2/2] test/spec for some of docs.mts --- packages/mermaid/src/docs.spec.ts | 115 ++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 packages/mermaid/src/docs.spec.ts diff --git a/packages/mermaid/src/docs.spec.ts b/packages/mermaid/src/docs.spec.ts new file mode 100644 index 0000000000..3f7465ee49 --- /dev/null +++ b/packages/mermaid/src/docs.spec.ts @@ -0,0 +1,115 @@ +import { transformBlocks, transformToBlockQuote } from './docs.mjs'; + +import { remark } from 'remark'; // import it this way so we can mock it +vi.mock('remark'); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +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('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); + expect(result).toEqual(contents); + }); + + describe('is a code block', () => { + const beforeCodeLine = 'test\n'; + const diagram_text = 'graph\n A --> B\n'; + + describe('language = "mermaid-nocode"', () => { + 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); + expect(result).toEqual( + beforeCodeLine + '\n' + '```' + 'mermaid' + '\n' + diagram_text + '\n```\n' + ); + }); + }); + + describe('language = "mermaid" | "mmd" | "mermaid-example"', () => { + const mermaid_keywords = ['mermaid', 'mmd', 'mermaid-example']; + + mermaid_keywords.forEach((lang_keyword) => { + 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); + expect(result).toEqual( + beforeCodeLine + + '\n' + + '```mermaid-example\n' + + diagram_text + + '\n```\n' + + '\n```mermaid\n' + + diagram_text + + '\n```\n' + ); + }); + }); + }); + + it('calls transformToBlockQuote with the node information', () => { + const lang_keyword = 'note'; + const contents = + beforeCodeLine + '```' + lang_keyword + '\n' + 'This is the text\n' + '```\n'; + + const result = transformBlocks(contents); + expect(result).toEqual(beforeCodeLine + '\n> **Note**\n' + '> This is the text\n'); + }); + }); + }); + }); + + describe('transformToBlockQuote', () => { + // TODO Is there a way to test this with --vitepress given as a process argument? + + describe('vitepress is not given as an argument', () => { + it('everything starts with "> " (= block quote)', () => { + const result = transformToBlockQuote('first line\n\n\nfourth line', 'blorfType'); + expect(result).toMatch(/> (.)*\n> first line(?:\n> ){3}fourth line/); + }); + + it('includes an icon if there is one for the type', () => { + const result = transformToBlockQuote( + 'first line\n\n\nfourth line', + 'danger', + 'Custom Title' + ); + expect(result).toMatch(/> \*\*‼️ Custom Title\*\* /); + }); + + describe('a custom title is given', () => { + it('custom title is surrounded in spaces, in bold', () => { + const result = transformToBlockQuote( + 'first line\n\n\nfourth line', + 'blorfType', + 'Custom Title' + ); + expect(result).toMatch(/> \*\*Custom Title\*\* /); + }); + }); + + describe.skip('no custom title is given', () => { + it('title is the icon and the capitalized type, in bold', () => { + const result = transformToBlockQuote('first line\n\n\nfourth line', 'blorf type'); + expect(result).toMatch(/> \*\*Blorf Type\*\* /); + }); + }); + }); + }); +});