import { Context, HTMLConvertorMap, HTMLToken, MdNode, MdNodeType, OpenTagToken, RawHTMLToken, Renderer, TextToken, } from '@toast-ui/toastmark'; import { ProsemirrorNode, Mark } from 'prosemirror-model'; import isArray from 'tui-code-snippet/type/isArray'; import { getHTMLRenderConvertors } from '@/markdown/htmlRenderConvertors'; import { ToDOMAdaptor } from '@t/convertor'; import { includes, last } from '@/utils/common'; import { CustomHTMLRenderer, LinkAttributes } from '@t/editor'; import { setAttributes } from '@/utils/dom'; import { createMdLikeNode, isContainer, isPmNode } from './mdLikeNode'; interface TokenToDOM { openTag: (token: HTMLToken, stack: T[]) => void; closeTag: (token: HTMLToken, stack: T[]) => void; html: (token: HTMLToken, stack: T[]) => void; text: (token: HTMLToken, stack: T[]) => void; } const tokenToDOMNode: TokenToDOM = { openTag(token, stack) { const { tagName, classNames, attributes } = token as OpenTagToken; const el = document.createElement(tagName); let attrs: Record = {}; if (classNames) { el.className = classNames.join(' '); } if (attributes) { attrs = { ...attrs, ...attributes }; } setAttributes(attrs, el); stack.push(el); }, closeTag(_, stack) { if (stack.length > 1) { const el = stack.pop(); last(stack).appendChild(el!); } }, html(token, stack) { last(stack).insertAdjacentHTML('beforeend', (token as RawHTMLToken).content); }, text(token, stack) { const textNode = document.createTextNode((token as TextToken).content); last(stack).appendChild(textNode); }, }; export class WwToDOMAdaptor implements ToDOMAdaptor { private customConvertorKeys: string[]; renderer: Renderer; convertors: HTMLConvertorMap; constructor(linkAttributes: LinkAttributes | null, customRenderer: CustomHTMLRenderer) { const convertors = getHTMLRenderConvertors(linkAttributes, customRenderer); const customHTMLConvertor = { ...customRenderer.htmlBlock, ...customRenderer.htmlInline }; // flatten the html block, inline convertor to other custom convertors this.customConvertorKeys = Object.keys(customRenderer).concat(Object.keys(customHTMLConvertor)); this.renderer = new Renderer({ gfm: true, convertors: { ...convertors, ...customHTMLConvertor }, }); this.convertors = this.renderer.getConvertors(); } private generateTokens(node: ProsemirrorNode | Mark) { const mdLikeNode = createMdLikeNode(node); const context: Context = { entering: true, leaf: isPmNode(node) ? node.isLeaf : false, options: this.renderer.getOptions(), getChildrenText: () => (isPmNode(node) ? node.textContent : ''), skipChildren: () => false, }; const convertor = this.convertors[node.type.name as MdNodeType]!; const converted = convertor(mdLikeNode as MdNode, context, this.convertors)!; let tokens: HTMLToken[] = isArray(converted) ? converted : [converted]; if (isContainer(node.type.name) || node.attrs.htmlInline) { context.entering = false; tokens.push({ type: 'text', content: isPmNode(node) ? node.textContent : '' }); tokens = tokens.concat(convertor(mdLikeNode as MdNode, context, this.convertors)!); } return tokens; } private toDOMNode(node: ProsemirrorNode | Mark) { const tokens = this.generateTokens(node); const stack: HTMLElement[] = []; tokens.forEach((token) => tokenToDOMNode[token.type](token, stack)); return stack[0]; } getToDOMNode(name: string) { if (includes(this.customConvertorKeys, name)) { return this.toDOMNode.bind(this); } return null; } }