import marked from '../parser/marked' import Prism from 'prismjs' import katex from 'katex' import loadRenderer from '../renderers' import githubMarkdownCss from 'github-markdown-css/github-markdown.css' import footnoteCss from '../assets/styles/exportStyle.css' import highlightCss from 'prismjs/themes/prism.css' import katexCss from 'katex/dist/katex.css' import footerHeaderCss from '../assets/styles/headerFooterStyle.css' import { EXPORT_DOMPURIFY_CONFIG } from '../config' import { sanitize, unescapeHtml } from '../utils' import { validEmoji } from '../ui/emojis' export const getSanitizeHtml = (markdown, options) => { const html = marked(markdown, options) return sanitize(html, EXPORT_DOMPURIFY_CONFIG) } const DIAGRAM_TYPE = [ 'mermaid', 'flowchart', 'sequence', 'vega-lite' ] class ExportHtml { constructor (markdown, muya) { this.markdown = markdown this.muya = muya this.exportContainer = null this.mathRendererCalled = false } async renderMermaid () { const codes = this.exportContainer.querySelectorAll('code.language-mermaid') for (const code of codes) { const preEle = code.parentNode const mermaidContainer = document.createElement('div') mermaidContainer.innerHTML = code.innerHTML mermaidContainer.classList.add('mermaid') preEle.replaceWith(mermaidContainer) } const mermaid = await loadRenderer('mermaid') // We only export light theme, so set mermaid theme to `default`, in the future, we can choose whick theme to export. mermaid.initialize({ theme: 'default' }) mermaid.init(undefined, this.exportContainer.querySelectorAll('div.mermaid')) if (this.muya) { mermaid.initialize({ theme: this.muya.options.mermaidTheme }) } } async renderDiagram () { const selector = 'code.language-vega-lite, code.language-flowchart, code.language-sequence' const RENDER_MAP = { flowchart: await loadRenderer('flowchart'), sequence: await loadRenderer('sequence'), 'vega-lite': await loadRenderer('vega-lite') } const codes = this.exportContainer.querySelectorAll(selector) for (const code of codes) { const rawCode = unescapeHtml(code.innerHTML) const functionType = /sequence/.test(code.className) ? 'sequence' : (/flowchart/.test(code.className) ? 'flowchart' : 'vega-lite') const render = RENDER_MAP[functionType] const preParent = code.parentNode const diagramContainer = document.createElement('div') diagramContainer.classList.add(functionType) preParent.replaceWith(diagramContainer) const options = {} if (functionType === 'sequence') { Object.assign(options, { theme: 'hand' }) } else if (functionType === 'vega-lite') { Object.assign(options, { actions: false, tooltip: false, renderer: 'svg', theme: 'latimes' // only render light theme }) } try { if (functionType === 'flowchart' || functionType === 'sequence') { const diagram = render.parse(rawCode) diagramContainer.innerHTML = '' diagram.drawSVG(diagramContainer, options) } if (functionType === 'vega-lite') { await render(diagramContainer, JSON.parse(rawCode), options) } } catch (err) { console.log(err) diagramContainer.innerHTML = '< Invalid Diagram >' } } } mathRenderer = (math, displayMode) => { this.mathRendererCalled = true return katex.renderToString(math, { displayMode }) } // render pure html by marked async renderHtml () { this.mathRendererCalled = false let html = marked(this.markdown, { superSubScript: this.muya ? this.muya.options.superSubScript : false, footnote: this.muya ? this.muya.options.footnote : false, highlight (code, lang) { // Language may be undefined (GH#591) if (!lang) { return code } if (DIAGRAM_TYPE.includes(lang)) { return code } const grammar = Prism.languages[lang] if (!grammar) { console.warn(`Unable to find grammar for "${lang}".`) return code } return Prism.highlight(code, grammar, lang) }, emojiRenderer (emoji) { const validate = validEmoji(emoji) if (validate) { return validate.emoji } else { return `:${emoji}:` } }, mathRenderer: this.mathRenderer }) html = sanitize(html, EXPORT_DOMPURIFY_CONFIG) const exportContainer = this.exportContainer = document.createElement('div') exportContainer.classList.add('ag-render-container') exportContainer.innerHTML = html document.body.appendChild(exportContainer) // render only render the light theme of mermaid and diragram... await this.renderMermaid() await this.renderDiagram() let result = exportContainer.innerHTML exportContainer.remove() // hack to add arrow marker to output html const pathes = document.querySelectorAll('path[id^=raphael-marker-]') const def = '' result = result.replace(def, () => { let str = '' for (const path of pathes) { str += path.outerHTML } return `${def}${str}` }) this.exportContainer = null return result } /** * Get HTML with style * * @param {*} options Document options */ async generate (options) { const { printOptimization } = options // WORKAROUND: Hide Prism.js style when exporting or printing. Otherwise the background color is white in the dark theme. const highlightCssStyle = printOptimization ? `@media print { ${highlightCss} }` : highlightCss const html = this._prepareHtml(await this.renderHtml(), options) const katexCssStyle = this.mathRendererCalled ? katexCss : '' this.mathRendererCalled = false // `extraCss` may changed in the mean time. const { title, extraCss } = options return ` ${sanitize(title, EXPORT_DOMPURIFY_CONFIG)} ${html} ` } /** * @private * * @param {string} html The converted HTML text. * @param {*} options The export options. */ _prepareHtml (html, options) { const { header, footer } = options const appendHeaderFooter = !!header || !!footer if (!appendHeaderFooter) { return createMarkdownArticle(html) } if (!options.extraCss) { options.extraCss = footerHeaderCss } else { options.extraCss = footerHeaderCss + options.extraCss } let output = HF_TABLE_START if (header) { output += createTableHeader(options) } if (footer) { output += HF_TABLE_FOOTER() output = createRealFooter(options) + output } output = output + createTableBody(html) + HF_TABLE_END return sanitize(output, EXPORT_DOMPURIFY_CONFIG) } } // Variables and function to generate the header and footer. const HF_TABLE_START = '' const createTableBody = html => { return `` } const HF_TABLE_END = '
${createMarkdownArticle(html)}
' /// The header at is shown at the top. const createTableHeader = options => { const { header, headerFooterStyled } = options const { type, left, center, right } = header let headerClass = type === 1 ? 'single' : '' headerClass += getHeaderFooterStyledClass(headerFooterStyled) return `
${left}
${center}
${right}
` } /// Fake footer to reserve space. const HF_TABLE_FOOTER = `
 
` /// The real footer at is shown at the bottom. const createRealFooter = options => { const { footer, headerFooterStyled } = options const { type, left, center, right } = footer let footerClass = type === 1 ? 'single' : '' footerClass += getHeaderFooterStyledClass(headerFooterStyled) return `` } /// Generate the mardown article HTML. const createMarkdownArticle = html => { return `
${html}
` } /// Return the class whether a header/footer should be styled. const getHeaderFooterStyledClass = value => { if (value === undefined) { // Prefer theme settings. return '' } return !value ? ' simple' : ' styled' } export default ExportHtml