diff --git a/mod.ts b/mod.ts index 93eb18d..545f590 100644 --- a/mod.ts +++ b/mod.ts @@ -31,6 +31,7 @@ export class Renderer extends Marked.Renderer { allowMath: boolean; baseUrl: string | undefined; #slugger: GitHubSlugger; + mermaidImport: boolean = false; constructor(options: Marked.MarkedOptions & RenderOptions = {}) { super(options); @@ -63,23 +64,41 @@ export class Renderer extends Marked.Renderer { // a language of `ts, ignore` should really be `ts` // and it should be lowercase to ensure it has parity with regular github markdown language = language?.split(",")?.[0].toLocaleLowerCase(); + const isMermaid = language === "mermaid"; // transform math code blocks into HTML+MathML // https://github.blog/changelog/2022-06-28-fenced-block-syntax-for-mathematical-expressions/ if (language === "math" && this.allowMath) { return katex.renderToString(code, { displayMode: true }); } + if (isMermaid) { + this.mermaidImport = true; + } const grammar = language && Object.hasOwnProperty.call(Prism.languages, language) ? Prism.languages[language] : undefined; if (grammar === undefined) { + if (isMermaid) { + return minify(`
+
${code}
+
${code}
+
`); + } return `
${he.encode(code)}
`; } const html = Prism.highlight(code, grammar, language!); const titleHtml = title ? `
${title}
` : ``; + if (isMermaid) { + return minify(` +
+
${titleHtml}
${html}
+
${code}
+
+ `); + } return `
${titleHtml}
${html}
`; } @@ -99,6 +118,10 @@ export class Renderer extends Marked.Renderer { } } +function minify(str: string): string { + return str.replace(/^\s+|\s+$|\n/gm, ""); +} + const BLOCK_MATH_REGEXP = /\$\$\s(.+?)\s\$\$/g; const INLINE_MATH_REGEXP = /\s\$((?=\S).*?(?=\S))\$/g; @@ -169,8 +192,33 @@ export function render(markdown: string, opts: RenderOptions = {}): string { : Marked.marked.parse(markdown, marked_opts) ) as string; + let additionalCode = ""; + if (marked_opts.renderer.mermaidImport) { + additionalCode = minify(` + + + `); + } + if (opts.disableHtmlSanitization) { - return html; + return additionalCode + html; } let defaultAllowedTags = sanitizeHtml.defaults.allowedTags.concat([ @@ -243,6 +291,8 @@ export function render(markdown: string, opts: RenderOptions = {}): string { "markdown-alert", "markdown-alert-*", "markdown-code-title", + "mermaid-code", + "mermaid-container", ], span: [ "token", @@ -334,22 +384,25 @@ export function render(markdown: string, opts: RenderOptions = {}): string { ], }; - return sanitizeHtml(html, { - transformTags: { - img: transformMedia, - video: transformMedia, - }, - allowedTags: [...defaultAllowedTags, ...(opts.allowedTags ?? [])], - allowedAttributes: mergeAttributes( - defaultAllowedAttributes, - opts.allowedAttributes ?? {}, - ), - allowedClasses: { ...defaultAllowedClasses, ...opts.allowedClasses }, - allowProtocolRelative: false, - parser: { - lowerCaseAttributeNames: false, - }, - }); + return ( + additionalCode + + sanitizeHtml(html, { + transformTags: { + img: transformMedia, + video: transformMedia, + }, + allowedTags: [...defaultAllowedTags, ...(opts.allowedTags ?? [])], + allowedAttributes: mergeAttributes( + defaultAllowedAttributes, + opts.allowedAttributes ?? {}, + ), + allowedClasses: { ...defaultAllowedClasses, ...opts.allowedClasses }, + allowProtocolRelative: false, + parser: { + lowerCaseAttributeNames: false, + }, + }) + ); } function mergeAttributes( diff --git a/test/test.ts b/test/test.ts index 1310ac0..581222f 100644 --- a/test/test.ts +++ b/test/test.ts @@ -268,6 +268,40 @@ Deno.test("code fence with a title", () => { assertEquals(html, expected); }); +Deno.test("code containing mermaid", () => { + // test with two code blocks to see if the script and styles are not replicated + const markdown = + "```mermaid\ngraph TD;A-->B;A-->C;B-->D;C-->D;\n```\n\n```mermaid\ngraph TD;A-->B;A-->C;B-->D;C-->D;\n```"; + const expected = ` + +
graph TD;A-->B;A-->C;B-->D;C-->D;
graph TD;A-->B;A-->C;B-->D;C-->D;
+
graph TD;A-->B;A-->C;B-->D;C-->D;
graph TD;A-->B;A-->C;B-->D;C-->D;
`; + + const html = render(markdown); + assertEquals( + html, + expected + .split("\n") + .map((line) => line.trim()) + .join(""), + ); +}); + Deno.test("link with title", () => { const markdown = `[link](https://example.com "asdf")`; const expected =