import { ElementType } from "domelementtype"; import { Element, Text, Comment, CDATA, Document, ProcessingInstruction, } from "./node.js"; export * from "./node.js"; // Default options const defaultOpts = { withStartIndices: false, withEndIndices: false, xmlMode: false, }; export class DomHandler { /** * @param callback Called once parsing has completed. * @param options Settings for the handler. * @param elementCB Callback whenever a tag is closed. */ constructor(callback, options, elementCB) { /** The elements of the DOM */ this.dom = []; /** The root element for the DOM */ this.root = new Document(this.dom); /** Indicated whether parsing has been completed. */ this.done = false; /** Stack of open tags. */ this.tagStack = [this.root]; /** A data node that is still being written to. */ this.lastNode = null; /** Reference to the parser instance. Used for location information. */ this.parser = null; // Make it possible to skip arguments, for backwards-compatibility if (typeof options === "function") { elementCB = options; options = defaultOpts; } if (typeof callback === "object") { options = callback; callback = undefined; } this.callback = callback !== null && callback !== void 0 ? callback : null; this.options = options !== null && options !== void 0 ? options : defaultOpts; this.elementCB = elementCB !== null && elementCB !== void 0 ? elementCB : null; } onparserinit(parser) { this.parser = parser; } // Resets the handler back to starting state onreset() { this.dom = []; this.root = new Document(this.dom); this.done = false; this.tagStack = [this.root]; this.lastNode = null; this.parser = null; } // Signals the handler that parsing is done onend() { if (this.done) return; this.done = true; this.parser = null; this.handleCallback(null); } onerror(error) { this.handleCallback(error); } onclosetag() { this.lastNode = null; const elem = this.tagStack.pop(); if (this.options.withEndIndices) { elem.endIndex = this.parser.endIndex; } if (this.elementCB) this.elementCB(elem); } onopentag(name, attribs) { const type = this.options.xmlMode ? ElementType.Tag : undefined; const element = new Element(name, attribs, undefined, type); this.addNode(element); this.tagStack.push(element); } ontext(data) { const { lastNode } = this; if (lastNode && lastNode.type === ElementType.Text) { lastNode.data += data; if (this.options.withEndIndices) { lastNode.endIndex = this.parser.endIndex; } } else { const node = new Text(data); this.addNode(node); this.lastNode = node; } } oncomment(data) { if (this.lastNode && this.lastNode.type === ElementType.Comment) { this.lastNode.data += data; return; } const node = new Comment(data); this.addNode(node); this.lastNode = node; } oncommentend() { this.lastNode = null; } oncdatastart() { const text = new Text(""); const node = new CDATA([text]); this.addNode(node); text.parent = node; this.lastNode = text; } oncdataend() { this.lastNode = null; } onprocessinginstruction(name, data) { const node = new ProcessingInstruction(name, data); this.addNode(node); } handleCallback(error) { if (typeof this.callback === "function") { this.callback(error, this.dom); } else if (error) { throw error; } } addNode(node) { const parent = this.tagStack[this.tagStack.length - 1]; const previousSibling = parent.children[parent.children.length - 1]; if (this.options.withStartIndices) { node.startIndex = this.parser.startIndex; } if (this.options.withEndIndices) { node.endIndex = this.parser.endIndex; } parent.children.push(node); if (previousSibling) { node.prev = previousSibling; previousSibling.next = node; } node.parent = parent; this.lastNode = null; } } export default DomHandler;