import { CloseTagToken, Context, HTMLConvertorMap, HTMLRenderer, HTMLToken, OpenTagToken, RawHTMLToken, RendererOptions, TagToken, TextToken, } from '@t/renderer'; import { MdNodeType } from '@t/node'; import { Node, isContainer, isCustomBlock, isCustomInline } from '../commonmark/node'; import { escapeXml } from '../commonmark/common'; import { last } from '../helper'; import { baseConvertors } from './baseConvertors'; import { gfmConvertors } from './gfmConvertors'; const defaultOptions: RendererOptions = { softbreak: '\n', gfm: false, tagFilter: false, nodeId: false, }; function getChildrenText(node: Node) { const buffer: string[] = []; const walker = node.walker(); let event: ReturnType = null; while ((event = walker.next())) { const { node } = event; if (node.type === 'text') { buffer.push(node.literal!); } } return buffer.join(''); } export class Renderer implements HTMLRenderer { private convertors: HTMLConvertorMap; private options: RendererOptions; private buffer: string[] = []; constructor(customOptions?: Partial) { this.options = { ...defaultOptions, ...customOptions }; this.convertors = this.createConvertors(); delete this.options.convertors; } private createConvertors() { let convertors: HTMLConvertorMap = { ...baseConvertors }; if (this.options.gfm) { convertors = { ...convertors, ...gfmConvertors }; } if (this.options.convertors) { const customConvertors = this.options.convertors; const nodeTypes = Object.keys(customConvertors) as MdNodeType[]; const defaultConvertors = { ...baseConvertors, ...gfmConvertors }; nodeTypes.forEach((nodeType) => { const orgConvertor = convertors[nodeType]; const convertor = customConvertors[nodeType]!; const convertorType = Object.keys(defaultConvertors).indexOf(nodeType) === -1 ? nodeType.toLowerCase() : nodeType; if (orgConvertor) { convertors[convertorType] = (node, context, convertors) => { context.origin = () => orgConvertor(node, context, convertors); return convertor(node, context); }; } else { convertors[convertorType] = convertor; } }); } return convertors; } getConvertors() { return this.convertors; } getOptions() { return this.options; } render(rootNode: Node): string { this.buffer = []; const walker = rootNode.walker(); let event: ReturnType = null; while ((event = walker.next())) { const { node, entering } = event; const convertor = this.convertors[node.type]; if (!convertor) { continue; } let skipped = false; const context: Context = { entering, leaf: !isContainer(node), options: this.options, getChildrenText, skipChildren: () => { skipped = true; }, }; const converted = isCustomBlock(node) || isCustomInline(node) ? convertor(node, context, this.convertors) : convertor(node, context); if (converted) { const htmlNodes = Array.isArray(converted) ? converted : [converted]; htmlNodes.forEach((htmlNode, index) => { if (htmlNode.type === 'openTag' && this.options.nodeId && index === 0) { if (!htmlNode.attributes) { htmlNode.attributes = {}; } htmlNode.attributes['data-nodeid'] = String(node.id); } this.renderHTMLNode(htmlNode); }); if (skipped) { walker.resumeAt(node, false); walker.next(); } } } this.addNewLine(); return this.buffer.join(''); } renderHTMLNode(node: HTMLToken) { switch (node.type) { case 'openTag': case 'closeTag': this.renderElementNode(node); break; case 'text': this.renderTextNode(node); break; case 'html': this.renderRawHtmlNode(node); break; default: // no-default-case } } private generateOpenTagString(node: OpenTagToken) { const { tagName, classNames, attributes } = node; this.buffer.push(`<${tagName}`); if (classNames && classNames.length > 0) { this.buffer.push(` class="${classNames.join(' ')}"`); } if (attributes) { Object.keys(attributes).forEach((attrName) => { const attrValue = attributes[attrName]; this.buffer.push(` ${attrName}="${attrValue}"`); }); } if (node.selfClose) { this.buffer.push(' /'); } this.buffer.push('>'); } private generateCloseTagString({ tagName }: CloseTagToken) { this.buffer.push(``); } private addNewLine() { if (this.buffer.length && last(last(this.buffer)) !== '\n') { this.buffer.push('\n'); } } private addOuterNewLine(node: TagToken | RawHTMLToken) { if (node.outerNewLine) { this.addNewLine(); } } private addInnerNewLine(node: TagToken) { if (node.innerNewLine) { this.addNewLine(); } } private renderTextNode(node: TextToken) { this.buffer.push(escapeXml(node.content)); } private renderRawHtmlNode(node: RawHTMLToken) { this.addOuterNewLine(node); this.buffer.push(node.content); this.addOuterNewLine(node); } private renderElementNode(node: OpenTagToken | CloseTagToken) { if (node.type === 'openTag') { this.addOuterNewLine(node); this.generateOpenTagString(node); if (node.selfClose) { this.addOuterNewLine(node); } else { this.addInnerNewLine(node); } } else { this.addInnerNewLine(node); this.generateCloseTagString(node); this.addOuterNewLine(node); } } }