import { escapeHTML } from './utils.js'; /** * @typedef {object} Renderer * @property {(text: string) => void} addText * @property {(node: Node) => void} openNode * @property {(node: Node) => void} closeNode * @property {() => string} value */ /** @typedef {{scope?: string, language?: string, sublanguage?: boolean}} Node */ /** @typedef {{walk: (r: Renderer) => void}} Tree */ /** */ const SPAN_CLOSE = ''; /** * Determines if a node needs to be wrapped in * * @param {Node} node */ const emitsWrappingTags = (node) => { // rarely we can have a sublanguage where language is undefined // TODO: track down why return !!node.scope; }; /** * * @param {string} name * @param {{prefix:string}} options */ const scopeToCSSClass = (name, { prefix }) => { // sub-language if (name.startsWith("language:")) { return name.replace("language:", "language-"); } // tiered scope: comment.line if (name.includes(".")) { const pieces = name.split("."); return [ `${prefix}${pieces.shift()}`, ...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`)) ].join(" "); } // simple scope return `${prefix}${name}`; }; /** @type {Renderer} */ export default class HTMLRenderer { /** * Creates a new HTMLRenderer * * @param {Tree} parseTree - the parse tree (must support `walk` API) * @param {{classPrefix: string}} options */ constructor(parseTree, options) { this.buffer = ""; this.classPrefix = options.classPrefix; parseTree.walk(this); } /** * Adds texts to the output stream * * @param {string} text */ addText(text) { this.buffer += escapeHTML(text); } /** * Adds a node open to the output stream (if needed) * * @param {Node} node */ openNode(node) { if (!emitsWrappingTags(node)) return; const className = scopeToCSSClass(node.scope, { prefix: this.classPrefix }); this.span(className); } /** * Adds a node close to the output stream (if needed) * * @param {Node} node */ closeNode(node) { if (!emitsWrappingTags(node)) return; this.buffer += SPAN_CLOSE; } /** * returns the accumulated buffer */ value() { return this.buffer; } // helpers /** * Builds a span element * * @param {string} className */ span(className) { this.buffer += ``; } }