UNPKG

html-dom-parser

Version:
569 lines (568 loc) • 16.7 kB
const require_Tokenizer = require("./Tokenizer.js"); //#region node_modules/htmlparser2/dist/Parser.js const { fromCodePoint } = String; const formTags = /* @__PURE__ */ new Set([ "input", "option", "optgroup", "select", "button", "datalist", "textarea" ]); const pTag = /* @__PURE__ */ new Set(["p"]); const headingTags = /* @__PURE__ */ new Set([ "h1", "h2", "h3", "h4", "h5", "h6", "p" ]); const tableSectionTags = /* @__PURE__ */ new Set(["thead", "tbody"]); const ddtTags = /* @__PURE__ */ new Set(["dd", "dt"]); const rtpTags = /* @__PURE__ */ new Set(["rt", "rp"]); const openImpliesClose = /* @__PURE__ */ new Map([ ["tr", /* @__PURE__ */ new Set([ "tr", "th", "td" ])], ["th", /* @__PURE__ */ new Set(["th"])], ["td", /* @__PURE__ */ new Set([ "thead", "th", "td" ])], ["body", /* @__PURE__ */ new Set([ "head", "link", "script" ])], ["a", /* @__PURE__ */ new Set(["a"])], ["li", /* @__PURE__ */ new Set(["li"])], ["p", pTag], ["h1", headingTags], ["h2", headingTags], ["h3", headingTags], ["h4", headingTags], ["h5", headingTags], ["h6", headingTags], ["select", formTags], ["input", formTags], ["output", formTags], ["button", formTags], ["datalist", formTags], ["textarea", formTags], ["option", /* @__PURE__ */ new Set(["option"])], ["optgroup", /* @__PURE__ */ new Set(["optgroup", "option"])], ["dd", ddtTags], ["dt", ddtTags], ["address", pTag], ["article", pTag], ["aside", pTag], ["blockquote", pTag], ["details", pTag], ["div", pTag], ["dl", pTag], ["fieldset", pTag], ["figcaption", pTag], ["figure", pTag], ["footer", pTag], ["form", pTag], ["header", pTag], ["hr", pTag], ["main", pTag], ["nav", pTag], ["ol", pTag], ["pre", pTag], ["section", pTag], ["table", pTag], ["ul", pTag], ["rt", rtpTags], ["rp", rtpTags], ["tbody", tableSectionTags], ["tfoot", tableSectionTags] ]); const DOCUMENT_TYPE = "doctype"; const voidElements = /* @__PURE__ */ new Set([ "area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source", "track", "wbr" ]); const foreignContextElements = /* @__PURE__ */ new Set(["math", "svg"]); /** * Elements that can be used to integrate HTML content within foreign namespaces (e.g., SVG or MathML). * * Entries must use the SVG-adjusted casing (e.g. "foreignObject" not * "foreignobject") since they are compared against adjusted tag names. */ const htmlIntegrationElements = /* @__PURE__ */ new Set([ "mi", "mo", "mn", "ms", "mtext", "annotation-xml", "foreignObject", "desc", "title" ]); const svgTagNameAdjustments = /* @__PURE__ */ new Map([ ["altglyph", "altGlyph"], ["altglyphdef", "altGlyphDef"], ["altglyphitem", "altGlyphItem"], ["animatecolor", "animateColor"], ["animatemotion", "animateMotion"], ["animatetransform", "animateTransform"], ["clippath", "clipPath"], ["feblend", "feBlend"], ["fecolormatrix", "feColorMatrix"], ["fecomponenttransfer", "feComponentTransfer"], ["fecomposite", "feComposite"], ["feconvolvematrix", "feConvolveMatrix"], ["fediffuselighting", "feDiffuseLighting"], ["fedisplacementmap", "feDisplacementMap"], ["fedistantlight", "feDistantLight"], ["fedropshadow", "feDropShadow"], ["feflood", "feFlood"], ["fefunca", "feFuncA"], ["fefuncb", "feFuncB"], ["fefuncg", "feFuncG"], ["fefuncr", "feFuncR"], ["fegaussianblur", "feGaussianBlur"], ["feimage", "feImage"], ["femerge", "feMerge"], ["femergenode", "feMergeNode"], ["femorphology", "feMorphology"], ["feoffset", "feOffset"], ["fepointlight", "fePointLight"], ["fespecularlighting", "feSpecularLighting"], ["fespotlight", "feSpotLight"], ["fetile", "feTile"], ["feturbulence", "feTurbulence"], ["foreignobject", "foreignObject"], ["glyphref", "glyphRef"], ["lineargradient", "linearGradient"], ["radialgradient", "radialGradient"], ["textpath", "textPath"] ]); var ForeignContext; (function(ForeignContext) { ForeignContext[ForeignContext["None"] = 0] = "None"; ForeignContext[ForeignContext["Svg"] = 1] = "Svg"; ForeignContext[ForeignContext["MathML"] = 2] = "MathML"; })(ForeignContext || (ForeignContext = {})); const reNameEnd = /\s|\//; /** * Incremental parser implementation. */ var Parser = class { options; /** The start index of the last event. */ startIndex = 0; /** The end index of the last event. */ endIndex = 0; /** * Store the start index of the current open tag, * so we can update the start index for attributes. */ openTagStart = 0; tagname = ""; attribname = ""; attribvalue = ""; attribs = null; stack = []; foreignContext; cbs; lowerCaseTagNames; lowerCaseAttributeNames; recognizeSelfClosing; /** We are parsing HTML. Inverse of the `xmlMode` option. */ htmlMode; tokenizer; buffers = []; bufferOffset = 0; /** The index of the last written buffer. Used when resuming after a `pause()`. */ writeIndex = 0; /** Indicates whether the parser has finished running / `.end` has been called. */ ended = false; constructor(cbs, options = {}) { this.options = options; this.cbs = cbs ?? {}; this.htmlMode = !this.options.xmlMode; this.lowerCaseTagNames = options.lowerCaseTags ?? this.htmlMode; this.lowerCaseAttributeNames = options.lowerCaseAttributeNames ?? this.htmlMode; this.recognizeSelfClosing = options.recognizeSelfClosing ?? !this.htmlMode; this.tokenizer = new (options.Tokenizer ?? require_Tokenizer.default)(this.options, this); this.foreignContext = [ForeignContext.None]; this.cbs.onparserinit?.(this); } /** * @param start Start index for the current parser event. * @param endIndex End index for the current parser event. * @internal */ ontext(start, endIndex) { const data = this.getSlice(start, endIndex); this.endIndex = endIndex - 1; this.cbs.ontext?.(data); this.startIndex = endIndex; } /** * @param cp Current Unicode code point. * @param endIndex End index for the current parser event. * @internal */ ontextentity(cp, endIndex) { this.endIndex = endIndex - 1; this.cbs.ontext?.(fromCodePoint(cp)); this.startIndex = endIndex; } /** @internal */ isInForeignContext() { return this.foreignContext[0] !== ForeignContext.None; } /** * Checks if the current tag is a void element. Override this if you want * to specify your own additional void elements. * @param name Name of the pseudo selector. */ isVoidElement(name) { return this.htmlMode && voidElements.has(name); } /** * Read a tag name from the buffer. * * When `lowerCaseTagNames` is enabled (the default in HTML mode), the name * is lowercased and may be adjusted for SVG casing or the `image` → `img` * alias. * @param start Start index of the tag name in the buffer. * @param endIndex End index of the tag name in the buffer. */ readTagName(start, endIndex) { const name = this.lowerCaseTagNames ? this.getSlice(start, endIndex).toLowerCase() : this.getSlice(start, endIndex); if (!(this.lowerCaseTagNames && this.htmlMode)) return name; if (this.foreignContext[0] === ForeignContext.Svg) return svgTagNameAdjustments.get(name) ?? name; if (this.foreignContext.length > 1) { const adjusted = svgTagNameAdjustments.get(name); if (adjusted !== void 0 && this.stack.includes(adjusted)) return adjusted; } if (!this.isInForeignContext()) return name === "image" ? "img" : name; return name; } /** * @param start Start index for the current parser event. * @param endIndex End index for the current parser event. * @internal */ onopentagname(start, endIndex) { this.endIndex = endIndex; this.emitOpenTag(this.readTagName(start, endIndex)); } emitOpenTag(name) { this.openTagStart = this.startIndex; this.tagname = name; if (this.htmlMode && name === "form" && this.stack.includes("form")) { this.tagname = ""; return; } const impliesClose = this.htmlMode && openImpliesClose.get(name); if (impliesClose) while (this.stack.length > 0 && impliesClose.has(this.stack[0])) this.popElement(true); if (!this.isVoidElement(name)) { this.stack.unshift(name); if (this.htmlMode) { if (name === "svg") this.foreignContext.unshift(ForeignContext.Svg); else if (name === "math") this.foreignContext.unshift(ForeignContext.MathML); else if (htmlIntegrationElements.has(name)) this.foreignContext.unshift(ForeignContext.None); } } this.cbs.onopentagname?.(name); if (this.cbs.onopentag) this.attribs = {}; } endOpenTag(isImplied) { this.startIndex = this.openTagStart; if (this.attribs) { this.cbs.onopentag?.(this.tagname, this.attribs, isImplied); this.attribs = null; } if (this.cbs.onclosetag && this.isVoidElement(this.tagname)) this.cbs.onclosetag(this.tagname, true); this.tagname = ""; } /** * @param endIndex End index for the current parser event. * @internal */ onopentagend(endIndex) { this.endIndex = endIndex; this.endOpenTag(false); this.startIndex = endIndex + 1; } /** * @param start Start index for the current parser event. * @param endIndex End index for the current parser event. * @internal */ onclosetag(start, endIndex) { this.endIndex = endIndex; const name = this.readTagName(start, endIndex); if (!this.isVoidElement(name)) { const pos = this.stack.indexOf(name); if (pos !== -1) { for (let index = 0; index < pos; index++) this.popElement(true); this.popElement(false); } else if (this.htmlMode && name === "p") { this.emitOpenTag("p"); this.closeCurrentTag(true); } } else if (this.htmlMode && name === "br") { this.cbs.onopentagname?.("br"); this.cbs.onopentag?.("br", {}, true); this.cbs.onclosetag?.("br", false); } this.startIndex = endIndex + 1; } /** * @param endIndex End index for the current parser event. * @internal */ onselfclosingtag(endIndex) { this.endIndex = endIndex; if (this.recognizeSelfClosing || this.isInForeignContext()) { this.closeCurrentTag(false); this.startIndex = endIndex + 1; } else this.onopentagend(endIndex); } /** * Pop the top element off the stack, emit a close event, and maintain * the foreign context stack. * @param implied Whether this close is implied (not from an explicit end tag). */ popElement(implied) { const element = this.stack.shift(); if (this.htmlMode && (foreignContextElements.has(element) || htmlIntegrationElements.has(element))) this.foreignContext.shift(); this.cbs.onclosetag?.(element, implied); } closeCurrentTag(isOpenImplied) { const name = this.tagname; this.endOpenTag(isOpenImplied); if (this.stack[0] === name) this.popElement(!isOpenImplied); } /** * @param start Start index for the current parser event. * @param endIndex End index for the current parser event. * @internal */ onattribname(start, endIndex) { this.startIndex = start; const name = this.getSlice(start, endIndex); this.attribname = this.lowerCaseAttributeNames ? name.toLowerCase() : name; } /** * @param start Start index for the current parser event. * @param endIndex End index for the current parser event. * @internal */ onattribdata(start, endIndex) { this.attribvalue += this.getSlice(start, endIndex); } /** * @param cp Current Unicode code point. * @internal */ onattribentity(cp) { this.attribvalue += fromCodePoint(cp); } /** * @param quote Quote type used for the current attribute. * @param endIndex End index for the current parser event. * @internal */ onattribend(quote, endIndex) { this.endIndex = endIndex; this.cbs.onattribute?.(this.attribname, this.attribvalue, quote === require_Tokenizer.QuoteType.Double ? "\"" : quote === require_Tokenizer.QuoteType.Single ? "'" : quote === require_Tokenizer.QuoteType.NoValue ? void 0 : null); if (this.attribs && !Object.hasOwn(this.attribs, this.attribname)) this.attribs[this.attribname] = this.attribvalue; this.attribvalue = ""; } getInstructionName(value) { const index = value.search(reNameEnd); let name = index < 0 ? value : value.substr(0, index); if (this.lowerCaseTagNames) name = name.toLowerCase(); return name; } /** * @param start Start index for the current parser event. * @param endIndex End index for the current parser event. * @internal */ ondeclaration(start, endIndex) { this.endIndex = endIndex; const value = this.getSlice(start, endIndex); if (this.cbs.onprocessinginstruction) { const name = this.htmlMode ? this.lowerCaseTagNames ? DOCUMENT_TYPE : value.slice(0, 7) : this.getInstructionName(value); this.cbs.onprocessinginstruction(`!${name}`, `!${value}`); } this.startIndex = endIndex + 1; } /** * @param start Start index for the current parser event. * @param endIndex End index for the current parser event. * @internal */ onprocessinginstruction(start, endIndex) { this.endIndex = endIndex; const value = this.getSlice(start, endIndex); if (this.cbs.onprocessinginstruction) { const name = this.getInstructionName(value); this.cbs.onprocessinginstruction(`?${name}`, `?${value}`); } this.startIndex = endIndex + 1; } /** * @param start Start index for the current parser event. * @param endIndex End index for the current parser event. * @param offset Offset applied when computing parser indices. * @internal */ oncomment(start, endIndex, offset) { this.endIndex = endIndex; this.cbs.oncomment?.(this.getSlice(start, endIndex - offset)); this.cbs.oncommentend?.(); this.startIndex = endIndex + 1; } /** * @param start Start index for the current parser event. * @param endIndex End index for the current parser event. * @param offset Offset applied when computing parser indices. * @internal */ oncdata(start, endIndex, offset) { this.endIndex = endIndex; const value = this.getSlice(start, endIndex - offset); if (!this.htmlMode || this.options.recognizeCDATA) { this.cbs.oncdatastart?.(); this.cbs.ontext?.(value); this.cbs.oncdataend?.(); } else if (this.isInForeignContext()) this.cbs.ontext?.(value); else { this.cbs.oncomment?.(`[CDATA[${value}]]`); this.cbs.oncommentend?.(); } this.startIndex = endIndex + 1; } /** @internal */ onend() { if (this.cbs.onclosetag) { this.endIndex = this.startIndex; for (let index = 0; index < this.stack.length; index++) this.cbs.onclosetag(this.stack[index], true); } this.cbs.onend?.(); } /** * Resets the parser to a blank state, ready to parse a new HTML document */ reset() { this.cbs.onreset?.(); this.tokenizer.reset(); this.tagname = ""; this.attribname = ""; this.attribvalue = ""; this.attribs = null; this.stack.length = 0; this.startIndex = 0; this.endIndex = 0; this.cbs.onparserinit?.(this); this.buffers.length = 0; this.foreignContext.length = 0; this.foreignContext.unshift(ForeignContext.None); this.bufferOffset = 0; this.writeIndex = 0; this.ended = false; } /** * Resets the parser, then parses a complete document and * pushes it to the handler. * @param data Document to parse. */ parseComplete(data) { this.reset(); this.end(data); } getSlice(start, end) { if (start === end) return ""; while (start - this.bufferOffset >= this.buffers[0].length) this.shiftBuffer(); let slice = this.buffers[0].slice(start - this.bufferOffset, end - this.bufferOffset); while (end - this.bufferOffset > this.buffers[0].length) { this.shiftBuffer(); slice += this.buffers[0].slice(0, end - this.bufferOffset); } return slice; } shiftBuffer() { this.bufferOffset += this.buffers[0].length; this.writeIndex--; this.buffers.shift(); } /** * Parses a chunk of data and calls the corresponding callbacks. * @param chunk Chunk to parse. */ write(chunk) { if (this.ended) { this.cbs.onerror?.(/* @__PURE__ */ new Error(".write() after done!")); return; } this.buffers.push(chunk); if (this.tokenizer.running) { this.tokenizer.write(chunk); this.writeIndex++; } } /** * Parses the end of the buffer and clears the stack, calls onend. * @param chunk Optional final chunk to parse. */ end(chunk) { if (this.ended) { this.cbs.onerror?.(/* @__PURE__ */ new Error(".end() after done!")); return; } if (chunk) this.write(chunk); this.ended = true; this.tokenizer.end(); } /** * Pauses parsing. The parser won't emit events until `resume` is called. */ pause() { this.tokenizer.pause(); } /** * Resumes parsing after `pause` was called. */ resume() { this.tokenizer.resume(); while (this.tokenizer.running && this.writeIndex < this.buffers.length) this.tokenizer.write(this.buffers[this.writeIndex++]); if (this.ended) this.tokenizer.end(); } }; //#endregion exports.Parser = Parser; //# sourceMappingURL=Parser.js.map