import { HTMLConvertorMap } from '@t/renderer'; import { Node, HeadingNode, CodeBlockNode, ListNode, LinkNode, CustomBlockNode, } from '../commonmark/node'; import { escapeXml } from '../commonmark/common'; import { filterDisallowedTags } from './tagFilter'; const CUSTOM_SYNTAX_LENGTH = 4; export const baseConvertors: HTMLConvertorMap = { heading(node, { entering }) { return { type: entering ? 'openTag' : 'closeTag', tagName: `h${(node as HeadingNode).level}`, outerNewLine: true, }; }, text(node) { return { type: 'text', content: node.literal!, }; }, softbreak(_, { options }) { return { type: 'html', content: options.softbreak, }; }, linebreak() { return { type: 'html', content: '
\n', }; }, emph(_, { entering }) { return { type: entering ? 'openTag' : 'closeTag', tagName: 'em', }; }, strong(_, { entering }) { return { type: entering ? 'openTag' : 'closeTag', tagName: 'strong', }; }, paragraph(node, { entering }) { const grandparent = node.parent?.parent; if (grandparent && grandparent.type === 'list') { if ((grandparent as ListNode).listData!.tight) { return null; } } return { type: entering ? 'openTag' : 'closeTag', tagName: 'p', outerNewLine: true, }; }, thematicBreak() { return { type: 'openTag', tagName: 'hr', outerNewLine: true, selfClose: true, }; }, blockQuote(_, { entering }) { return { type: entering ? 'openTag' : 'closeTag', tagName: 'blockquote', outerNewLine: true, innerNewLine: true, }; }, list(node, { entering }) { const { type, start } = (node as ListNode).listData!; const tagName = type === 'bullet' ? 'ul' : 'ol'; const attributes: Record = {}; if (tagName === 'ol' && start !== null && start !== 1) { attributes.start = start.toString(); } return { type: entering ? 'openTag' : 'closeTag', tagName, attributes, outerNewLine: true, }; }, item(_, { entering }) { return { type: entering ? 'openTag' : 'closeTag', tagName: 'li', outerNewLine: true, }; }, htmlInline(node, { options }) { const content = options.tagFilter ? filterDisallowedTags(node.literal!) : node.literal!; return { type: 'html', content }; }, htmlBlock(node, { options }) { const content = options.tagFilter ? filterDisallowedTags(node.literal!) : node.literal!; if (options.nodeId) { return [ { type: 'openTag', tagName: 'div', outerNewLine: true }, { type: 'html', content }, { type: 'closeTag', tagName: 'div', outerNewLine: true }, ]; } return { type: 'html', content, outerNewLine: true }; }, code(node) { return [ { type: 'openTag', tagName: 'code' }, { type: 'text', content: node.literal! }, { type: 'closeTag', tagName: 'code' }, ]; }, codeBlock(node) { const infoStr = (node as CodeBlockNode).info; const infoWords = infoStr ? infoStr.split(/\s+/) : []; const codeClassNames = []; if (infoWords.length > 0 && infoWords[0].length > 0) { codeClassNames.push(`language-${escapeXml(infoWords[0])}`); } return [ { type: 'openTag', tagName: 'pre', outerNewLine: true }, { type: 'openTag', tagName: 'code', classNames: codeClassNames }, { type: 'text', content: node.literal! }, { type: 'closeTag', tagName: 'code' }, { type: 'closeTag', tagName: 'pre', outerNewLine: true }, ]; }, link(node: Node, { entering }) { if (entering) { const { title, destination } = node as LinkNode; return { type: 'openTag', tagName: 'a', attributes: { href: escapeXml(destination!), ...(title && { title: escapeXml(title) }), }, }; } return { type: 'closeTag', tagName: 'a' }; }, image(node: Node, { getChildrenText, skipChildren }) { const { title, destination } = node as LinkNode; skipChildren(); return { type: 'openTag', tagName: 'img', selfClose: true, attributes: { src: escapeXml(destination!), alt: getChildrenText(node), ...(title && { title: escapeXml(title) }), }, }; }, customBlock(node, context, convertors) { const info = (node as CustomBlockNode).info!.trim().toLowerCase(); const customConvertor = convertors![info]; if (customConvertor) { try { return customConvertor!(node, context); } catch (e) { console.warn( `[@toast-ui/editor] - The error occurred when ${info} block node was parsed in markdown renderer: ${e}` ); } } return [ { type: 'openTag', tagName: 'div', outerNewLine: true }, { type: 'text', content: node.literal! }, { type: 'closeTag', tagName: 'div', outerNewLine: true }, ]; }, frontMatter(node) { return [ { type: 'openTag', tagName: 'div', outerNewLine: true, // Because front matter is metadata, it should not be render. attributes: { style: 'white-space: pre; display: none;' }, }, { type: 'text', content: node.literal! }, { type: 'closeTag', tagName: 'div', outerNewLine: true }, ]; }, customInline(node, context, convertors) { const { info, firstChild } = node as CustomBlockNode; const nomalizedInfo = info.trim().toLowerCase(); const customConvertor = convertors![nomalizedInfo]; const { entering } = context; if (customConvertor) { try { return customConvertor!(node, context); } catch (e) { console.warn( `[@toast-ui/editor] - The error occurred when ${nomalizedInfo} inline node was parsed in markdown renderer: ${e}` ); } } return entering ? [ { type: 'openTag', tagName: 'span' }, { type: 'text', content: `$$${info}${firstChild ? ' ' : ''}` }, ] : [ { type: 'text', content: '$$' }, { type: 'closeTag', tagName: 'span' }, ]; }, };