import { MdNode } from '@toast-ui/toastmark'; import { sanitizeHTML } from '@/sanitizer/htmlSanitizer'; import { HTMLToWwConvertorMap, FlattenHTMLToWwConvertorMap, ToWwConvertorState, } from '@t/convertor'; import { includes } from '@/utils/common'; import { reHTMLTag } from '@/utils/constants'; export function getTextWithoutTrailingNewline(text: string) { return text[text.length - 1] === '\n' ? text.slice(0, text.length - 1) : text; } export function isCustomHTMLInlineNode({ schema }: ToWwConvertorState, node: MdNode) { const html = node.literal!; const matched = html.match(reHTMLTag); if (matched) { const [, openTagName, , closeTagName] = matched; const typeName = (openTagName || closeTagName).toLowerCase(); return node.type === 'htmlInline' && !!(schema.marks[typeName] || schema.nodes[typeName]); } return false; } export function isInlineNode({ type }: MdNode) { return includes(['text', 'strong', 'emph', 'strike', 'image', 'link', 'code'], type); } function isSoftbreak(mdNode: MdNode | null) { return mdNode?.type === 'softbreak'; } function isListNode({ type, literal }: MdNode) { const matched = type === 'htmlInline' && literal!.match(reHTMLTag); if (matched) { const [, openTagName, , closeTagName] = matched; const tagName = openTagName || closeTagName; if (tagName) { return includes(['ul', 'ol', 'li'], tagName.toLowerCase()); } } return false; } function getListItemAttrs({ literal }: MdNode) { const task = /data-task/.test(literal!); const checked = /data-task-checked/.test(literal!); return { task, checked }; } function getMatchedAttributeValue(rawHTML: string, ...attrNames: string[]) { const wrapper = document.createElement('div'); wrapper.innerHTML = sanitizeHTML(rawHTML); const el = wrapper.firstChild as HTMLElement; return attrNames.map((attrName) => el.getAttribute(attrName) || ''); } function createConvertors(convertors: HTMLToWwConvertorMap) { const convertorMap: FlattenHTMLToWwConvertorMap = {}; Object.keys(convertors).forEach((key) => { const tagNames = key.split(', '); tagNames.forEach((tagName) => { const name = tagName.toLowerCase(); convertorMap[name] = convertors[key]!; }); }); return convertorMap; } const convertors: HTMLToWwConvertorMap = { 'b, strong': (state, _, openTagName) => { const { strong } = state.schema.marks; if (openTagName) { state.openMark(strong.create({ rawHTML: openTagName })); } else { state.closeMark(strong); } }, 'i, em': (state, _, openTagName) => { const { emph } = state.schema.marks; if (openTagName) { state.openMark(emph.create({ rawHTML: openTagName })); } else { state.closeMark(emph); } }, 's, del': (state, _, openTagName) => { const { strike } = state.schema.marks; if (openTagName) { state.openMark(strike.create({ rawHTML: openTagName })); } else { state.closeMark(strike); } }, code: (state, _, openTagName) => { const { code } = state.schema.marks; if (openTagName) { state.openMark(code.create({ rawHTML: openTagName })); } else { state.closeMark(code); } }, a: (state, node, openTagName) => { const tag = node.literal!; const { link } = state.schema.marks; if (openTagName) { const [linkUrl] = getMatchedAttributeValue(tag, 'href'); state.openMark( link.create({ linkUrl, rawHTML: openTagName, }) ); } else { state.closeMark(link); } }, img: (state, node, openTagName) => { const tag = node.literal!; if (openTagName) { const [imageUrl, altText] = getMatchedAttributeValue(tag, 'src', 'alt'); const { image } = state.schema.nodes; state.addNode(image, { rawHTML: openTagName, imageUrl, ...(altText && { altText }), }); } }, hr: (state, _, openTagName) => { state.addNode(state.schema.nodes.thematicBreak, { rawHTML: openTagName }); }, br: (state, node) => { const { paragraph } = state.schema.nodes; const { parent, prev, next } = node; if (parent?.type === 'paragraph') { // should open a paragraph node when line text has only
tag // ex) first line\n\n
\nfourth line if (isSoftbreak(prev)) { state.openNode(paragraph); } // should close a paragraph node when line text has only
tag // ex) first line\n\n
\nfourth line if (isSoftbreak(next)) { state.closeNode(); // should close a paragraph node and open a paragraph node to separate between blocks // when
tag is in the middle of the paragraph // ex) first
line\nthird line } else if (next) { state.closeNode(); state.openNode(paragraph); } } else if (parent?.type === 'tableCell') { if (prev && (isInlineNode(prev) || isCustomHTMLInlineNode(state, prev))) { state.closeNode(); } if (next && (isInlineNode(next) || isCustomHTMLInlineNode(state, next))) { state.openNode(paragraph); } } }, pre: (state, node, openTagName) => { const container = document.createElement('div'); container.innerHTML = node.literal!; const literal = container.firstChild?.firstChild?.textContent; state.openNode(state.schema.nodes.codeBlock, { rawHTML: openTagName }); state.addText(getTextWithoutTrailingNewline(literal!)); state.closeNode(); }, 'ul, ol': (state, node, openTagName) => { // in the table cell, '