import HTMLRenderer from './html_renderer.js'; /** @typedef {{scope?: string, language?: string, children: Node[]} | string} Node */ /** @typedef {{scope?: string, language?: string, children: Node[]} } DataNode */ /** @typedef {import('highlight.js').Emitter} Emitter */ /** */ /** @returns {DataNode} */ const newNode = (opts = {}) => { /** @type DataNode */ const result = { children: [] }; Object.assign(result, opts); return result; }; class TokenTree { constructor() { /** @type DataNode */ this.rootNode = newNode(); this.stack = [this.rootNode]; } get top() { return this.stack[this.stack.length - 1]; } get root() { return this.rootNode; } /** @param {Node} node */ add(node) { this.top.children.push(node); } /** @param {string} scope */ openNode(scope) { /** @type Node */ const node = newNode({ scope }); this.add(node); this.stack.push(node); } closeNode() { if (this.stack.length > 1) { return this.stack.pop(); } // eslint-disable-next-line no-undefined return undefined; } closeAllNodes() { while (this.closeNode()); } toJSON() { return JSON.stringify(this.rootNode, null, 4); } /** * @typedef { import("./html_renderer").Renderer } Renderer * @param {Renderer} builder */ walk(builder) { // this does not return this.constructor._walk(builder, this.rootNode); // this works // return TokenTree._walk(builder, this.rootNode); } /** * @param {Renderer} builder * @param {Node} node */ static _walk(builder, node) { if (typeof node === "string") { builder.addText(node); } else if (node.children) { builder.openNode(node); node.children.forEach((child) => this._walk(builder, child)); builder.closeNode(node); } return builder; } /** * @param {Node} node */ static _collapse(node) { if (typeof node === "string") return; if (!node.children) return; if (node.children.every(el => typeof el === "string")) { // node.text = node.children.join(""); // delete node.children; node.children = [node.children.join("")]; } else { node.children.forEach((child) => { TokenTree._collapse(child); }); } } } /** Currently this is all private API, but this is the minimal API necessary that an Emitter must implement to fully support the parser. Minimal interface: - addText(text) - __addSublanguage(emitter, subLanguageName) - startScope(scope) - endScope() - finalize() - toHTML() */ /** * @implements {Emitter} */ export default class TokenTreeEmitter extends TokenTree { /** * @param {*} options */ constructor(options) { super(); this.options = options; } /** * @param {string} text */ addText(text) { if (text === "") { return; } this.add(text); } /** @param {string} scope */ startScope(scope) { this.openNode(scope); } endScope() { this.closeNode(); } /** * @param {Emitter & {root: DataNode}} emitter * @param {string} name */ __addSublanguage(emitter, name) { /** @type DataNode */ const node = emitter.root; if (name) node.scope = `language:${name}`; this.add(node); } toHTML() { const renderer = new HTMLRenderer(this, this.options); return renderer.value(); } finalize() { this.closeAllNodes(); return true; } }