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;
}
}