"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ed5 = require("xmlchars/xml/1.0/ed5"); const ed2 = require("xmlchars/xml/1.1/ed2"); const NSed3 = require("xmlchars/xmlns/1.0/ed3"); var isS = ed5.isS; var isChar10 = ed5.isChar; var isNameStartChar = ed5.isNameStartChar; var isNameChar = ed5.isNameChar; var S_LIST = ed5.S_LIST; var NAME_RE = ed5.NAME_RE; var isChar11 = ed2.isChar; var isNCNameStartChar = NSed3.isNCNameStartChar; var isNCNameChar = NSed3.isNCNameChar; var NC_NAME_RE = NSed3.NC_NAME_RE; const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"; const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/"; const rootNS = { // eslint-disable-next-line @typescript-eslint/no-explicit-any __proto__: null, xml: XML_NAMESPACE, xmlns: XMLNS_NAMESPACE, }; const XML_ENTITIES = { // eslint-disable-next-line @typescript-eslint/no-explicit-any __proto__: null, amp: "&", gt: ">", lt: "<", quot: "\"", apos: "'", }; // EOC: end-of-chunk const EOC = -1; const NL_LIKE = -2; const S_BEGIN = 0; // Initial state. const S_BEGIN_WHITESPACE = 1; // leading whitespace const S_DOCTYPE = 2; // const TAB = 9; const NL = 0xA; const CR = 0xD; const SPACE = 0x20; const BANG = 0x21; const DQUOTE = 0x22; const AMP = 0x26; const SQUOTE = 0x27; const MINUS = 0x2D; const FORWARD_SLASH = 0x2F; const SEMICOLON = 0x3B; const LESS = 0x3C; const EQUAL = 0x3D; const GREATER = 0x3E; const QUESTION = 0x3F; const OPEN_BRACKET = 0x5B; const CLOSE_BRACKET = 0x5D; const NEL = 0x85; const LS = 0x2028; // Line Separator const isQuote = (c) => c === DQUOTE || c === SQUOTE; const QUOTES = [DQUOTE, SQUOTE]; const DOCTYPE_TERMINATOR = [...QUOTES, OPEN_BRACKET, GREATER]; const DTD_TERMINATOR = [...QUOTES, LESS, CLOSE_BRACKET]; const XML_DECL_NAME_TERMINATOR = [EQUAL, QUESTION, ...S_LIST]; const ATTRIB_VALUE_UNQUOTED_TERMINATOR = [...S_LIST, GREATER, AMP, LESS]; function nsPairCheck(parser, prefix, uri) { switch (prefix) { case "xml": if (uri !== XML_NAMESPACE) { parser.fail(`xml prefix must be bound to ${XML_NAMESPACE}.`); } break; case "xmlns": if (uri !== XMLNS_NAMESPACE) { parser.fail(`xmlns prefix must be bound to ${XMLNS_NAMESPACE}.`); } break; default: } switch (uri) { case XMLNS_NAMESPACE: parser.fail(prefix === "" ? `the default namespace may not be set to ${uri}.` : `may not assign a prefix (even "xmlns") to the URI \ ${XMLNS_NAMESPACE}.`); break; case XML_NAMESPACE: switch (prefix) { case "xml": // Assinging the XML namespace to "xml" is fine. break; case "": parser.fail(`the default namespace may not be set to ${uri}.`); break; default: parser.fail("may not assign the xml namespace to another prefix."); } break; default: } } function nsMappingCheck(parser, mapping) { for (const local of Object.keys(mapping)) { nsPairCheck(parser, local, mapping[local]); } } const isNCName = (name) => NC_NAME_RE.test(name); const isName = (name) => NAME_RE.test(name); const FORBIDDEN_START = 0; const FORBIDDEN_BRACKET = 1; const FORBIDDEN_BRACKET_BRACKET = 2; /** * The list of supported events. */ exports.EVENTS = [ "xmldecl", "text", "processinginstruction", "doctype", "comment", "opentagstart", "attribute", "opentag", "closetag", "cdata", "error", "end", "ready", ]; const EVENT_NAME_TO_HANDLER_NAME = { xmldecl: "xmldeclHandler", text: "textHandler", processinginstruction: "piHandler", doctype: "doctypeHandler", comment: "commentHandler", opentagstart: "openTagStartHandler", attribute: "attributeHandler", opentag: "openTagHandler", closetag: "closeTagHandler", cdata: "cdataHandler", error: "errorHandler", end: "endHandler", ready: "readyHandler", }; class SaxesParser { /** * @param opt The parser options. */ constructor(opt) { this.opt = opt !== null && opt !== void 0 ? opt : {}; this.fragmentOpt = !!this.opt.fragment; const xmlnsOpt = this.xmlnsOpt = !!this.opt.xmlns; this.trackPosition = this.opt.position !== false; this.fileName = this.opt.fileName; if (xmlnsOpt) { // This is the function we use to perform name checks on PIs and entities. // When namespaces are used, colons are not allowed in PI target names or // entity names. So the check depends on whether namespaces are used. See: // // https://www.w3.org/XML/xml-names-19990114-errata.html // NE08 // this.nameStartCheck = isNCNameStartChar; this.nameCheck = isNCNameChar; this.isName = isNCName; // eslint-disable-next-line @typescript-eslint/unbound-method this.processAttribs = this.processAttribsNS; // eslint-disable-next-line @typescript-eslint/unbound-method this.pushAttrib = this.pushAttribNS; // eslint-disable-next-line @typescript-eslint/no-explicit-any this.ns = Object.assign({ __proto__: null }, rootNS); const additional = this.opt.additionalNamespaces; if (additional != null) { nsMappingCheck(this, additional); Object.assign(this.ns, additional); } } else { this.nameStartCheck = isNameStartChar; this.nameCheck = isNameChar; this.isName = isName; // eslint-disable-next-line @typescript-eslint/unbound-method this.processAttribs = this.processAttribsPlain; // eslint-disable-next-line @typescript-eslint/unbound-method this.pushAttrib = this.pushAttribPlain; } // // The order of the members in this table needs to correspond to the state // numbers given to the states that correspond to the methods being recorded // here. // this.stateTable = [ /* eslint-disable @typescript-eslint/unbound-method */ this.sBegin, this.sBeginWhitespace, this.sDoctype, this.sDoctypeQuote, this.sDTD, this.sDTDQuoted, this.sDTDOpenWaka, this.sDTDOpenWakaBang, this.sDTDComment, this.sDTDCommentEnding, this.sDTDCommentEnded, this.sDTDPI, this.sDTDPIEnding, this.sText, this.sEntity, this.sOpenWaka, this.sOpenWakaBang, this.sComment, this.sCommentEnding, this.sCommentEnded, this.sCData, this.sCDataEnding, this.sCDataEnding2, this.sPIFirstChar, this.sPIRest, this.sPIBody, this.sPIEnding, this.sXMLDeclNameStart, this.sXMLDeclName, this.sXMLDeclEq, this.sXMLDeclValueStart, this.sXMLDeclValue, this.sXMLDeclSeparator, this.sXMLDeclEnding, this.sOpenTag, this.sOpenTagSlash, this.sAttrib, this.sAttribName, this.sAttribNameSawWhite, this.sAttribValue, this.sAttribValueQuoted, this.sAttribValueClosed, this.sAttribValueUnquoted, this.sCloseTag, this.sCloseTagSawWhite, ]; this._init(); } /** * Indicates whether or not the parser is closed. If ``true``, wait for * the ``ready`` event to write again. */ get closed() { return this._closed; } _init() { var _a; this.openWakaBang = ""; this.text = ""; this.name = ""; this.piTarget = ""; this.entity = ""; this.q = null; this.tags = []; this.tag = null; this.topNS = null; this.chunk = ""; this.chunkPosition = 0; this.i = 0; this.prevI = 0; this.carriedFromPrevious = undefined; this.forbiddenState = FORBIDDEN_START; this.attribList = []; // The logic is organized so as to minimize the need to check // this.opt.fragment while parsing. const { fragmentOpt } = this; this.state = fragmentOpt ? S_TEXT : S_BEGIN; // We want these to be all true if we are dealing with a fragment. this.reportedTextBeforeRoot = this.reportedTextAfterRoot = this.closedRoot = this.sawRoot = fragmentOpt; // An XML declaration is intially possible only when parsing whole // documents. this.xmlDeclPossible = !fragmentOpt; this.xmlDeclExpects = ["version"]; this.entityReturnState = undefined; let { defaultXMLVersion } = this.opt; if (defaultXMLVersion === undefined) { if (this.opt.forceXMLVersion === true) { throw new Error("forceXMLVersion set but defaultXMLVersion is not set"); } defaultXMLVersion = "1.0"; } this.setXMLVersion(defaultXMLVersion); this.positionAtNewLine = 0; this.doctype = false; this._closed = false; this.xmlDecl = { version: undefined, encoding: undefined, standalone: undefined, }; this.line = 1; this.column = 0; this.ENTITIES = Object.create(XML_ENTITIES); // eslint-disable-next-line no-unused-expressions (_a = this.readyHandler) === null || _a === void 0 ? void 0 : _a.call(this); } /** * The stream position the parser is currently looking at. This field is * zero-based. * * This field is not based on counting Unicode characters but is to be * interpreted as a plain index into a JavaScript string. */ get position() { return this.chunkPosition + this.i; } /** * The column number of the next character to be read by the parser. * * This field is zero-based. (The first column in a line is 0.) * * This field reports the index at which the next character would be in the * line if the line were represented as a JavaScript string. Note that this * *can* be different to a count based on the number of *Unicode characters* * due to how JavaScript handles astral plane characters. * * See [[column]] for a number that corresponds to a count of Unicode * characters. */ get columnIndex() { return this.position - this.positionAtNewLine; } /** * Set an event listener on an event. The parser supports one handler per * event type. If you try to set an event handler over an existing handler, * the old handler is silently overwritten. * * @param name The event to listen to. * * @param handler The handler to set. */ on(name, handler) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this[EVENT_NAME_TO_HANDLER_NAME[name]] = handler; } /** * Unset an event handler. * * @parma name The event to stop listening to. */ off(name) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this[EVENT_NAME_TO_HANDLER_NAME[name]] = undefined; } /** * Make an error object. The error object will have a message that contains * the ``fileName`` option passed at the creation of the parser. If position * tracking was turned on, it will also have line and column number * information. * * @param message The message describing the error to report. * * @returns An error object with a properly formatted message. */ makeError(message) { var _a; let msg = (_a = this.fileName) !== null && _a !== void 0 ? _a : ""; if (this.trackPosition) { if (msg.length > 0) { msg += ":"; } msg += `${this.line}:${this.column}`; } if (msg.length > 0) { msg += ": "; } return new Error(msg + message); } /** * Report a parsing error. This method is made public so that client code may * check for issues that are outside the scope of this project and can report * errors. * * @param message The error to report. * * @returns this */ fail(message) { const err = this.makeError(message); const handler = this.errorHandler; if (handler === undefined) { throw err; } else { handler(err); } return this; } /** * Write a XML data to the parser. * * @param chunk The XML data to write. * * @returns this */ write(chunk) { if (this.closed) { return this.fail("cannot write after close; assign an onready handler."); } let end = false; if (chunk === null) { // We cannot return immediately because carriedFromPrevious may need // processing. end = true; chunk = ""; } else if (typeof chunk === "object") { chunk = chunk.toString(); } // We checked if performing a pre-decomposition of the string into an array // of single complete characters (``Array.from(chunk)``) would be faster // than the current repeated calls to ``charCodeAt``. As of August 2018, it // isn't. (There may be Node-specific code that would perform faster than // ``Array.from`` but don't want to be dependent on Node.) if (this.carriedFromPrevious !== undefined) { // The previous chunk had char we must carry over. chunk = `${this.carriedFromPrevious}${chunk}`; this.carriedFromPrevious = undefined; } let limit = chunk.length; const lastCode = chunk.charCodeAt(limit - 1); if (!end && // A trailing CR or surrogate must be carried over to the next // chunk. (lastCode === CR || (lastCode >= 0xD800 && lastCode <= 0xDBFF))) { // The chunk ends with a character that must be carried over. We cannot // know how to handle it until we get the next chunk or the end of the // stream. So save it for later. this.carriedFromPrevious = chunk[limit - 1]; limit--; chunk = chunk.slice(0, limit); } const { stateTable } = this; this.chunk = chunk; this.i = 0; while (this.i < limit) { // eslint-disable-next-line @typescript-eslint/no-explicit-any stateTable[this.state].call(this); } this.chunkPosition += limit; return end ? this.end() : this; } /** * Close the current stream. Perform final well-formedness checks and reset * the parser tstate. * * @returns this */ close() { return this.write(null); } /** * Get a single code point out of the current chunk. This updates the current * position if we do position tracking. * * This is the algorithm to use for XML 1.0. * * @returns The character read. */ getCode10() { const { chunk, i } = this; this.prevI = i; // Yes, we do this instead of doing this.i++. Doing it this way, we do not // read this.i again, which is a bit faster. this.i = i + 1; if (i >= chunk.length) { return EOC; } // Using charCodeAt and handling the surrogates ourselves is faster // than using codePointAt. const code = chunk.charCodeAt(i); this.column++; if (code < 0xD800) { if (code >= SPACE || code === TAB) { return code; } switch (code) { case NL: this.line++; this.column = 0; this.positionAtNewLine = this.position; return NL; case CR: // We may get NaN if we read past the end of the chunk, which is fine. if (chunk.charCodeAt(i + 1) === NL) { // A \r\n sequence is converted to \n so we have to skip over the // next character. We already know it has a size of 1 so ++ is fine // here. this.i = i + 2; } // Otherwise, a \r is just converted to \n, so we don't have to skip // ahead. // In either case, \r becomes \n. this.line++; this.column = 0; this.positionAtNewLine = this.position; return NL_LIKE; default: // If we get here, then code < SPACE and it is not NL CR or TAB. this.fail("disallowed character."); return code; } } if (code > 0xDBFF) { // This is a specialized version of isChar10 that takes into account // that in this context code > 0xDBFF and code <= 0xFFFF. So it does not // test cases that don't need testing. if (!(code >= 0xE000 && code <= 0xFFFD)) { this.fail("disallowed character."); } return code; } const final = 0x10000 + ((code - 0xD800) * 0x400) + (chunk.charCodeAt(i + 1) - 0xDC00); this.i = i + 2; // This is a specialized version of isChar10 that takes into account that in // this context necessarily final >= 0x10000. if (final > 0x10FFFF) { this.fail("disallowed character."); } return final; } /** * Get a single code point out of the current chunk. This updates the current * position if we do position tracking. * * This is the algorithm to use for XML 1.1. * * @returns {number} The character read. */ getCode11() { const { chunk, i } = this; this.prevI = i; // Yes, we do this instead of doing this.i++. Doing it this way, we do not // read this.i again, which is a bit faster. this.i = i + 1; if (i >= chunk.length) { return EOC; } // Using charCodeAt and handling the surrogates ourselves is faster // than using codePointAt. const code = chunk.charCodeAt(i); this.column++; if (code < 0xD800) { if ((code > 0x1F && code < 0x7F) || (code > 0x9F && code !== LS) || code === TAB) { return code; } switch (code) { case NL: // 0xA this.line++; this.column = 0; this.positionAtNewLine = this.position; return NL; case CR: { // 0xD // We may get NaN if we read past the end of the chunk, which is // fine. const next = chunk.charCodeAt(i + 1); if (next === NL || next === NEL) { // A CR NL or CR NEL sequence is converted to NL so we have to skip // over the next character. We already know it has a size of 1. this.i = i + 2; } // Otherwise, a CR is just converted to NL, no skip. } /* yes, fall through */ case NEL: // 0x85 case LS: // Ox2028 this.line++; this.column = 0; this.positionAtNewLine = this.position; return NL_LIKE; default: this.fail("disallowed character."); return code; } } if (code > 0xDBFF) { // This is a specialized version of isCharAndNotRestricted that takes into // account that in this context code > 0xDBFF and code <= 0xFFFF. So it // does not test cases that don't need testing. if (!(code >= 0xE000 && code <= 0xFFFD)) { this.fail("disallowed character."); } return code; } const final = 0x10000 + ((code - 0xD800) * 0x400) + (chunk.charCodeAt(i + 1) - 0xDC00); this.i = i + 2; // This is a specialized version of isCharAndNotRestricted that takes into // account that in this context necessarily final >= 0x10000. if (final > 0x10FFFF) { this.fail("disallowed character."); } return final; } /** * Like ``getCode`` but with the return value normalized so that ``NL`` is * returned for ``NL_LIKE``. */ getCodeNorm() { const c = this.getCode(); return c === NL_LIKE ? NL : c; } unget() { this.i = this.prevI; this.column--; } /** * Capture characters into a buffer until encountering one of a set of * characters. * * @param chars An array of codepoints. Encountering a character in the array * ends the capture. (``chars`` may safely contain ``NL``.) * * @return The character code that made the capture end, or ``EOC`` if we hit * the end of the chunk. The return value cannot be NL_LIKE: NL is returned * instead. */ captureTo(chars) { let { i: start } = this; const { chunk } = this; // eslint-disable-next-line no-constant-condition while (true) { const c = this.getCode(); const isNLLike = c === NL_LIKE; const final = isNLLike ? NL : c; if (final === EOC || chars.includes(final)) { this.text += chunk.slice(start, this.prevI); return final; } if (isNLLike) { this.text += `${chunk.slice(start, this.prevI)}\n`; start = this.i; } } } /** * Capture characters into a buffer until encountering a character. * * @param char The codepoint that ends the capture. **NOTE ``char`` MAY NOT * CONTAIN ``NL``.** Passing ``NL`` will result in buggy behavior. * * @return ``true`` if we ran into the character. Otherwise, we ran into the * end of the current chunk. */ captureToChar(char) { let { i: start } = this; const { chunk } = this; // eslint-disable-next-line no-constant-condition while (true) { let c = this.getCode(); switch (c) { case NL_LIKE: this.text += `${chunk.slice(start, this.prevI)}\n`; start = this.i; c = NL; break; case EOC: this.text += chunk.slice(start); return false; default: } if (c === char) { this.text += chunk.slice(start, this.prevI); return true; } } } /** * Capture characters that satisfy ``isNameChar`` into the ``name`` field of * this parser. * * @return The character code that made the test fail, or ``EOC`` if we hit * the end of the chunk. The return value cannot be NL_LIKE: NL is returned * instead. */ captureNameChars() { const { chunk, i: start } = this; // eslint-disable-next-line no-constant-condition while (true) { const c = this.getCode(); if (c === EOC) { this.name += chunk.slice(start); return EOC; } // NL is not a name char so we don't have to test specifically for it. if (!isNameChar(c)) { this.name += chunk.slice(start, this.prevI); return c === NL_LIKE ? NL : c; } } } /** * Skip white spaces. * * @return The character that ended the skip, or ``EOC`` if we hit * the end of the chunk. The return value cannot be NL_LIKE: NL is returned * instead. */ skipSpaces() { // eslint-disable-next-line no-constant-condition while (true) { const c = this.getCodeNorm(); if (c === EOC || !isS(c)) { return c; } } } setXMLVersion(version) { this.currentXMLVersion = version; /* eslint-disable @typescript-eslint/unbound-method */ if (version === "1.0") { this.isChar = isChar10; this.getCode = this.getCode10; } else { this.isChar = isChar11; this.getCode = this.getCode11; } /* eslint-enable @typescript-eslint/unbound-method */ } // STATE ENGINE METHODS // This needs to be a state separate from S_BEGIN_WHITESPACE because we want // to be sure never to come back to this state later. sBegin() { // We are essentially peeking at the first character of the chunk. Since // S_BEGIN can be in effect only when we start working on the first chunk, // the index at which we must look is necessarily 0. Note also that the // following test does not depend on decoding surrogates. // If the initial character is 0xFEFF, ignore it. if (this.chunk.charCodeAt(0) === 0xFEFF) { this.i++; this.column++; } this.state = S_BEGIN_WHITESPACE; } sBeginWhitespace() { // We need to know whether we've encountered spaces or not because as soon // as we run into a space, an XML declaration is no longer possible. Rather // than slow down skipSpaces even in places where we don't care whether it // skipped anything or not, we check whether prevI is equal to the value of // i from before we skip spaces. const iBefore = this.i; const c = this.skipSpaces(); if (this.prevI !== iBefore) { this.xmlDeclPossible = false; } switch (c) { case LESS: this.state = S_OPEN_WAKA; // We could naively call closeText but in this state, it is not normal // to have text be filled with any data. if (this.text.length !== 0) { throw new Error("no-empty text at start"); } break; case EOC: break; default: this.unget(); this.state = S_TEXT; this.xmlDeclPossible = false; } } sDoctype() { var _a; const c = this.captureTo(DOCTYPE_TERMINATOR); switch (c) { case GREATER: { // eslint-disable-next-line no-unused-expressions (_a = this.doctypeHandler) === null || _a === void 0 ? void 0 : _a.call(this, this.text); this.text = ""; this.state = S_TEXT; this.doctype = true; // just remember that we saw it. break; } case EOC: break; default: this.text += String.fromCodePoint(c); if (c === OPEN_BRACKET) { this.state = S_DTD; } else if (isQuote(c)) { this.state = S_DOCTYPE_QUOTE; this.q = c; } } } sDoctypeQuote() { const q = this.q; if (this.captureToChar(q)) { this.text += String.fromCodePoint(q); this.q = null; this.state = S_DOCTYPE; } } sDTD() { const c = this.captureTo(DTD_TERMINATOR); if (c === EOC) { return; } this.text += String.fromCodePoint(c); if (c === CLOSE_BRACKET) { this.state = S_DOCTYPE; } else if (c === LESS) { this.state = S_DTD_OPEN_WAKA; } else if (isQuote(c)) { this.state = S_DTD_QUOTED; this.q = c; } } sDTDQuoted() { const q = this.q; if (this.captureToChar(q)) { this.text += String.fromCodePoint(q); this.state = S_DTD; this.q = null; } } sDTDOpenWaka() { const c = this.getCodeNorm(); this.text += String.fromCodePoint(c); switch (c) { case BANG: this.state = S_DTD_OPEN_WAKA_BANG; this.openWakaBang = ""; break; case QUESTION: this.state = S_DTD_PI; break; default: this.state = S_DTD; } } sDTDOpenWakaBang() { const char = String.fromCodePoint(this.getCodeNorm()); const owb = this.openWakaBang += char; this.text += char; if (owb !== "-") { this.state = owb === "--" ? S_DTD_COMMENT : S_DTD; this.openWakaBang = ""; } } sDTDComment() { if (this.captureToChar(MINUS)) { this.text += "-"; this.state = S_DTD_COMMENT_ENDING; } } sDTDCommentEnding() { const c = this.getCodeNorm(); this.text += String.fromCodePoint(c); this.state = c === MINUS ? S_DTD_COMMENT_ENDED : S_DTD_COMMENT; } sDTDCommentEnded() { const c = this.getCodeNorm(); this.text += String.fromCodePoint(c); if (c === GREATER) { this.state = S_DTD; } else { this.fail("malformed comment."); // will be recorded as // a comment of " blah -- bloo " this.state = S_DTD_COMMENT; } } sDTDPI() { if (this.captureToChar(QUESTION)) { this.text += "?"; this.state = S_DTD_PI_ENDING; } } sDTDPIEnding() { const c = this.getCodeNorm(); this.text += String.fromCodePoint(c); if (c === GREATER) { this.state = S_DTD; } } sText() { // // We did try a version of saxes where the S_TEXT state was split in two // states: one for text inside the root element, and one for text // outside. This was avoiding having to test this.tags.length to decide // what implementation to actually use. // // Peformance testing on gigabyte-size files did not show any advantage to // using the two states solution instead of the current one. Conversely, it // made the code a bit more complicated elsewhere. For instance, a comment // can appear before the root element so when a comment ended it was // necessary to determine whether to return to the S_TEXT state or to the // new text-outside-root state. // if (this.tags.length !== 0) { this.handleTextInRoot(); } else { this.handleTextOutsideRoot(); } } sEntity() { // This is essentially a specialized version of captureToChar(SEMICOLON...) let { i: start } = this; const { chunk } = this; // eslint-disable-next-line no-labels, no-restricted-syntax loop: // eslint-disable-next-line no-constant-condition while (true) { switch (this.getCode()) { case NL_LIKE: this.entity += `${chunk.slice(start, this.prevI)}\n`; start = this.i; break; case SEMICOLON: { const { entityReturnState } = this; const entity = this.entity + chunk.slice(start, this.prevI); this.state = entityReturnState; let parsed; if (entity === "") { this.fail("empty entity name."); parsed = "&;"; } else { parsed = this.parseEntity(entity); this.entity = ""; } if (entityReturnState !== S_TEXT || this.textHandler !== undefined) { this.text += parsed; } // eslint-disable-next-line no-labels break loop; } case EOC: this.entity += chunk.slice(start); // eslint-disable-next-line no-labels break loop; default: } } } sOpenWaka() { // Reminder: a state handler is called with at least one character // available in the current chunk. So the first call to get code inside of // a state handler cannot return ``EOC``. That's why we don't test // for it. const c = this.getCode(); // either a /, ?, !, or text is coming next. if (isNameStartChar(c)) { this.state = S_OPEN_TAG; this.unget(); this.xmlDeclPossible = false; } else { switch (c) { case FORWARD_SLASH: this.state = S_CLOSE_TAG; this.xmlDeclPossible = false; break; case BANG: this.state = S_OPEN_WAKA_BANG; this.openWakaBang = ""; this.xmlDeclPossible = false; break; case QUESTION: this.state = S_PI_FIRST_CHAR; break; default: this.fail("disallowed character in tag name"); this.state = S_TEXT; this.xmlDeclPossible = false; } } } sOpenWakaBang() { this.openWakaBang += String.fromCodePoint(this.getCodeNorm()); switch (this.openWakaBang) { case "[CDATA[": if (!this.sawRoot && !this.reportedTextBeforeRoot) { this.fail("text data outside of root node."); this.reportedTextBeforeRoot = true; } if (this.closedRoot && !this.reportedTextAfterRoot) { this.fail("text data outside of root node."); this.reportedTextAfterRoot = true; } this.state = S_CDATA; this.openWakaBang = ""; break; case "--": this.state = S_COMMENT; this.openWakaBang = ""; break; case "DOCTYPE": this.state = S_DOCTYPE; if (this.doctype || this.sawRoot) { this.fail("inappropriately located doctype declaration."); } this.openWakaBang = ""; break; default: // 7 happens to be the maximum length of the string that can possibly // match one of the cases above. if (this.openWakaBang.length >= 7) { this.fail("incorrect syntax."); } } } sComment() { if (this.captureToChar(MINUS)) { this.state = S_COMMENT_ENDING; } } sCommentEnding() { var _a; const c = this.getCodeNorm(); if (c === MINUS) { this.state = S_COMMENT_ENDED; // eslint-disable-next-line no-unused-expressions (_a = this.commentHandler) === null || _a === void 0 ? void 0 : _a.call(this, this.text); this.text = ""; } else { this.text += `-${String.fromCodePoint(c)}`; this.state = S_COMMENT; } } sCommentEnded() { const c = this.getCodeNorm(); if (c !== GREATER) { this.fail("malformed comment."); // will be recorded as // a comment of " blah -- bloo " this.text += `--${String.fromCodePoint(c)}`; this.state = S_COMMENT; } else { this.state = S_TEXT; } } sCData() { if (this.captureToChar(CLOSE_BRACKET)) { this.state = S_CDATA_ENDING; } } sCDataEnding() { const c = this.getCodeNorm(); if (c === CLOSE_BRACKET) { this.state = S_CDATA_ENDING_2; } else { this.text += `]${String.fromCodePoint(c)}`; this.state = S_CDATA; } } sCDataEnding2() { var _a; const c = this.getCodeNorm(); switch (c) { case GREATER: { // eslint-disable-next-line no-unused-expressions (_a = this.cdataHandler) === null || _a === void 0 ? void 0 : _a.call(this, this.text); this.text = ""; this.state = S_TEXT; break; } case CLOSE_BRACKET: this.text += "]"; break; default: this.text += `]]${String.fromCodePoint(c)}`; this.state = S_CDATA; } } // We need this separate state to check the first character fo the pi target // with this.nameStartCheck which allows less characters than this.nameCheck. sPIFirstChar() { const c = this.getCodeNorm(); // This is first because in the case where the file is well-formed this is // the branch taken. We optimize for well-formedness. if (this.nameStartCheck(c)) { this.piTarget += String.fromCodePoint(c); this.state = S_PI_REST; } else if (c === QUESTION || isS(c)) { this.fail("processing instruction without a target."); this.state = c === QUESTION ? S_PI_ENDING : S_PI_BODY; } else { this.fail("disallowed character in processing instruction name."); this.piTarget += String.fromCodePoint(c); this.state = S_PI_REST; } } sPIRest() { // Capture characters into a piTarget while ``this.nameCheck`` run on the // character read returns true. const { chunk, i: start } = this; // eslint-disable-next-line no-constant-condition while (true) { const c = this.getCodeNorm(); if (c === EOC) { this.piTarget += chunk.slice(start); return; } // NL cannot satisfy this.nameCheck so we don't have to test specifically // for it. if (!this.nameCheck(c)) { this.piTarget += chunk.slice(start, this.prevI); const isQuestion = c === QUESTION; if (isQuestion || isS(c)) { if (this.piTarget === "xml") { if (!this.xmlDeclPossible) { this.fail("an XML declaration must be at the start of the document."); } this.state = isQuestion ? S_XML_DECL_ENDING : S_XML_DECL_NAME_START; } else { this.state = isQuestion ? S_PI_ENDING : S_PI_BODY; } } else { this.fail("disallowed character in processing instruction name."); this.piTarget += String.fromCodePoint(c); } break; } } } sPIBody() { if (this.text.length === 0) { const c = this.getCodeNorm(); if (c === QUESTION) { this.state = S_PI_ENDING; } else if (!isS(c)) { this.text = String.fromCodePoint(c); } } // The question mark character is not valid inside any of the XML // declaration name/value pairs. else if (this.captureToChar(QUESTION)) { this.state = S_PI_ENDING; } } sPIEnding() { var _a; const c = this.getCodeNorm(); if (c === GREATER) { const { piTarget } = this; if (piTarget.toLowerCase() === "xml") { this.fail("the XML declaration must appear at the start of the document."); } // eslint-disable-next-line no-unused-expressions (_a = this.piHandler) === null || _a === void 0 ? void 0 : _a.call(this, { target: piTarget, body: this.text, }); this.piTarget = this.text = ""; this.state = S_TEXT; } else if (c === QUESTION) { // We ran into ?? as part of a processing instruction. We initially took // the first ? as a sign that the PI was ending, but it is not. So we have // to add it to the body but we take the new ? as a sign that the PI is // ending. this.text += "?"; } else { this.text += `?${String.fromCodePoint(c)}`; this.state = S_PI_BODY; } this.xmlDeclPossible = false; } sXMLDeclNameStart() { const c = this.skipSpaces(); // The question mark character is not valid inside any of the XML // declaration name/value pairs. if (c === QUESTION) { // It is valid to go to S_XML_DECL_ENDING from this state. this.state = S_XML_DECL_ENDING; return; } if (c !== EOC) { this.state = S_XML_DECL_NAME; this.name = String.fromCodePoint(c); } } sXMLDeclName() { const c = this.captureTo(XML_DECL_NAME_TERMINATOR); // The question mark character is not valid inside any of the XML // declaration name/value pairs. if (c === QUESTION) { this.state = S_XML_DECL_ENDING; this.name += this.text; this.text = ""; this.fail("XML declaration is incomplete."); return; } if (!(isS(c) || c === EQUAL)) { return; } this.name += this.text; this.text = ""; if (!this.xmlDeclExpects.includes(this.name)) { switch (this.name.length) { case 0: this.fail("did not expect any more name/value pairs."); break; case 1: this.fail(`expected the name ${this.xmlDeclExpects[0]}.`); break; default: this.fail(`expected one of ${this.xmlDeclExpects.join(", ")}`); } } this.state = c === EQUAL ? S_XML_DECL_VALUE_START : S_XML_DECL_EQ; } sXMLDeclEq() { const c = this.getCodeNorm(); // The question mark character is not valid inside any of the XML // declaration name/value pairs. if (c === QUESTION) { this.state = S_XML_DECL_ENDING; this.fail("XML declaration is incomplete."); return; } if (isS(c)) { return; } if (c !== EQUAL) { this.fail("value required."); } this.state = S_XML_DECL_VALUE_START; } sXMLDeclValueStart() { const c = this.getCodeNorm(); // The question mark character is not valid inside any of the XML // declaration name/value pairs. if (c === QUESTION) { this.state = S_XML_DECL_ENDING; this.fail("XML declaration is incomplete."); return; } if (isS(c)) { return; } if (!isQuote(c)) { this.fail("value must be quoted."); this.q = SPACE; } else { this.q = c; } this.state = S_XML_DECL_VALUE; } sXMLDeclValue() { const c = this.captureTo([this.q, QUESTION]); // The question mark character is not valid inside any of the XML // declaration name/value pairs. if (c === QUESTION) { this.state = S_XML_DECL_ENDING; this.text = ""; this.fail("XML declaration is incomplete."); return; } if (c === EOC) { return; } const value = this.text; this.text = ""; switch (this.name) { case "version": { this.xmlDeclExpects = ["encoding", "standalone"]; const version = value; this.xmlDecl.version = version; // This is the test specified by XML 1.0 but it is fine for XML 1.1. if (!/^1\.[0-9]+$/.test(version)) { this.fail("version number must match /^1\\.[0-9]+$/."); } // When forceXMLVersion is set, the XML declaration is ignored. else if (!this.opt.forceXMLVersion) { this.setXMLVersion(version); } break; } case "encoding": if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(value)) { this.fail("encoding value must match \ /^[A-Za-z0-9][A-Za-z0-9._-]*$/."); } this.xmlDeclExpects = ["standalone"]; this.xmlDecl.encoding = value; break; case "standalone": if (value !== "yes" && value !== "no") { this.fail("standalone value must match \"yes\" or \"no\"."); } this.xmlDeclExpects = []; this.xmlDecl.standalone = value; break; default: // We don't need to raise an error here since we've already raised one // when checking what name was expected. } this.name = ""; this.state = S_XML_DECL_SEPARATOR; } sXMLDeclSeparator() { const c = this.getCodeNorm(); // The question mark character is not valid inside any of the XML // declaration name/value pairs. if (c === QUESTION) { // It is valid to go to S_XML_DECL_ENDING from this state. this.state = S_XML_DECL_ENDING; return; } if (!isS(c)) { this.fail("whitespace required."); this.unget(); } this.state = S_XML_DECL_NAME_START; } sXMLDeclEnding() { var _a; const c = this.getCodeNorm(); if (c === GREATER) { if (this.piTarget !== "xml") { this.fail("processing instructions are not allowed before root."); } else if (this.name !== "version" && this.xmlDeclExpects.includes("version")) { this.fail("XML declaration must contain a version."); } // eslint-disable-next-line no-unused-expressions (_a = this.xmldeclHandler) === null || _a === void 0 ? void 0 : _a.call(this, this.xmlDecl); this.name = ""; this.piTarget = this.text = ""; this.state = S_TEXT; } else { // We got here because the previous character was a ?, but the question // mark character is not valid inside any of the XML declaration // name/value pairs. this.fail("The character ? is disallowed anywhere in XML declarations."); } this.xmlDeclPossible = false; } sOpenTag() { var _a; const c = this.captureNameChars(); if (c === EOC) { return; } const tag = this.tag = { name: this.name, attributes: Object.create(null), }; this.name = ""; if (this.xmlnsOpt) { this.topNS = tag.ns = Object.create(null); } // eslint-disable-next-line no-unused-expressions (_a = this.openTagStartHandler) === null || _a === void 0 ? void 0 : _a.call(this, tag); this.sawRoot = true; if (!this.fragmentOpt && this.closedRoot) { this.fail("documents may contain only one root."); } switch (c) { case GREATER: this.openTag(); break; case FORWARD_SLASH: this.state = S_OPEN_TAG_SLASH; break; default: if (!isS(c)) { this.fail("disallowed character in tag name."); } this.state = S_ATTRIB; } } sOpenTagSlash() { if (this.getCode() === GREATER) { this.openSelfClosingTag(); } else { this.fail("forward-slash in opening tag not followed by >."); this.state = S_ATTRIB; } } sAttrib() { const c = this.skipSpaces(); if (c === EOC) { return; } if (isNameStartChar(c)) { this.unget(); this.state = S_ATTRIB_NAME; } else if (c === GREATER) { this.openTag(); } else if (c === FORWARD_SLASH) { this.state = S_OPEN_TAG_SLASH; } else { this.fail("disallowed character in attribute name."); } } sAttribName() { const c = this.captureNameChars(); if (c === EQUAL) { this.state = S_ATTRIB_VALUE; } else if (isS(c)) { this.state = S_ATTRIB_NAME_SAW_WHITE; } else if (c === GREATER) { this.fail("attribute without value."); this.pushAttrib(this.name, this.name); this.name = this.text = ""; this.openTag(); } else if (c !== EOC) { this.fail("disallowed character in attribute name."); } } sAttribNameSawWhite() { const c = this.skipSpaces(); switch (c) { case EOC: return; case EQUAL: this.state = S_ATTRIB_VALUE; break; default: this.fail("attribute without value."); // Should we do this??? // this.tag.attributes[this.name] = ""; this.text = ""; this.name = ""; if (c === GREATER) { this.openTag(); } else if (isNameStartChar(c)) { this.unget(); this.state = S_ATTRIB_NAME; } else { this.fail("disallowed character in attribute name."); this.state = S_ATTRIB; } } } sAttribValue() { const c = this.getCodeNorm(); if (isQuote(c)) { this.q = c; this.state = S_ATTRIB_VALUE_QUOTED; } else if (!isS(c)) { this.fail("unquoted attribute value."); this.state = S_ATTRIB_VALUE_UNQUOTED; this.unget(); } } sAttribValueQuoted() { // We deliberately do not use captureTo here. The specialized code we use // here is faster than using captureTo. const { q, chunk } = this; let { i: start } = this; // eslint-disable-next-line no-constant-condition while (true) { switch (this.getCode()) { case q: this.pushAttrib(this.name, this.text + chunk.slice(start, this.prevI)); this.name = this.text = ""; this.q = null; this.state = S_ATTRIB_VALUE_CLOSED; return; case AMP: this.text += chunk.slice(start, this.prevI); this.state = S_ENTITY; this.entityReturnState = S_ATTRIB_VALUE_QUOTED; return; case NL: case NL_LIKE: case TAB: this.text += `${chunk.slice(start, this.prevI)} `; start = this.i; break; case LESS: this.text += chunk.slice(start, this.prevI); this.fail("disallowed character."); return; case EOC: this.text += chunk.slice(start); return; default: } } } sAttribValueClosed() { const c = this.getCodeNorm(); if (isS(c)) { this.state = S_ATTRIB; } else if (c === GREATER) { this.openTag(); } else if (c === FORWARD_SLASH) { this.state = S_OPEN_TAG_SLASH; } else if (isNameStartChar(c)) { this.fail("no whitespace between attributes."); this.unget(); this.state = S_ATTRIB_NAME; } else { this.fail("disallowed character in attribute name."); } } sAttribValueUnquoted() { // We don't do anything regarding EOL or space handling for unquoted // attributes. We already have failed by the time we get here, and the // contract that saxes upholds states that upon failure, it is not safe to // rely on the data passed to event handlers (other than // ``onerror``). Passing "bad" data is not a problem. const c = this.captureTo(ATTRIB_VALUE_UNQUOTED_TERMINATOR); switch (c) { case AMP: this.state = S_ENTITY; this.entityReturnState = S_ATTRIB_VALUE_UNQUOTED; break; case LESS: this.fail("disallowed character."); break; case EOC: break; default: if (this.text.includes("]]>")) { this.fail("the string \"]]>\" is disallowed in char data."); } this.pushAttrib(this.name, this.text); this.name = this.text = ""; if (c === GREATER) { this.openTag(); } else { this.state = S_ATTRIB; } } } sCloseTag() { const c = this.captureNameChars(); if (c === GREATER) { this.closeTag(); } else if (isS(c)) { this.state = S_CLOSE_TAG_SAW_WHITE; } else if (c !== EOC) { this.fail("disallowed character in closing tag."); } } sCloseTagSawWhite() { switch (this.skipSpaces()) { case GREATER: this.closeTag(); break; case EOC: break; default: this.fail("disallowed character in closing tag."); } } // END OF STATE ENGINE METHODS handleTextInRoot() { // This is essentially a specialized version of captureTo which is optimized // for performing the ]]> check. A previous version of this code, checked // ``this.text`` for the presence of ]]>. It simplified the code but was // very costly when character data contained a lot of entities to be parsed. // // Since we are using a specialized loop, we also keep track of the presence // of ]]> in text data. The sequence ]]> is forbidden to appear as-is. // let { i: start, forbiddenState } = this; const { chunk, textHandler: handler } = this; // eslint-disable-next-line no-labels, no-restricted-syntax scanLoop: // eslint-disable-next-line no-constant-condition while (true) { switch (this.getCode()) { case LESS: { this.state = S_OPEN_WAKA; if (handler !== undefined) { const { text } = this; const slice = chunk.slice(start, this.prevI); if (text.length !== 0) { handler(text + slice); this.text = ""; } else if (slice.length !== 0) { handler(slice); } } forbiddenState = FORBIDDEN_START; // eslint-disable-next-line no-labels break scanLoop; } case AMP: this.state = S_ENTITY; this.entityReturnState = S_TEXT; if (handler !== undefined) { this.text += chunk.slice(start, this.prevI); } forbiddenState = FORBIDDEN_START; // eslint-disable-next-line no-labels break scanLoop; case CLOSE_BRACKET: switch (forbiddenState) { case FORBIDDEN_START: forbiddenState = FORBIDDEN_BRACKET; break; case FORBIDDEN_BRACKET: forbiddenState = FORBIDDEN_BRACKET_BRACKET; break; case FORBIDDEN_BRACKET_BRACKET: break; default: throw new Error("impossible state"); } break; case GREATER: if (forbiddenState === FORBIDDEN_BRACKET_BRACKET) { this.fail("the string \"]]>\" is disallowed in char data."); } forbiddenState = FORBIDDEN_START; break; case NL_LIKE: if (handler !== undefined) { this.text += `${chunk.slice(start, this.prevI)}\n`; } start = this.i; forbiddenState = FORBIDDEN_START; break; case EOC: if (handler !== undefined) { this.text += chunk.slice(start); } // eslint-disable-next-line no-labels break scanLoop; default: forbiddenState = FORBIDDEN_START; } } this.forbiddenState = forbiddenState; } handleTextOutsideRoot() { // This is essentially a specialized version of captureTo which is optimized // for a specialized task. We keep track of the presence of non-space // characters in the text since these are errors when appearing outside the // document root element. let { i: start } = this; const { chunk, textHandler: handler } = this; let nonSpace = false; // eslint-disable-next-line no-labels, no-restricted-syntax outRootLoop: // eslint-disable-next-line no-constant-condition while (true) { const code = this.getCode(); switch (code) { case LESS: { this.state = S_OPEN_WAKA; if (handler !== undefined) { const { text } = this; const slice = chunk.slice(start, this.prevI); if (text.length !== 0) { handler(text + slice); this.text = ""; } else if (slice.length !== 0) { handler(slice); } } // eslint-disable-next-line no-labels break outRootLoop; } case AMP: this.state = S_ENTITY; this.entityReturnState = S_TEXT; if (handler !== undefined) { this.text += chunk.slice(start, this.prevI); } nonSpace = true; // eslint-disable-next-line no-labels break outRootLoop; case NL_LIKE: if (handler !== undefined) { this.text += `${chunk.slice(start, this.prevI)}\n`; } start = this.i; break; case EOC: if (handler !== undefined) { this.text += chunk.slice(start); } // eslint-disable-next-line no-labels break outRootLoop; default: if (!isS(code)) { nonSpace = true; } } } if (!nonSpace) { return; } // We use the reportedTextBeforeRoot and reportedTextAfterRoot flags // to avoid reporting errors for every single character that is out of // place. if (!this.sawRoot && !this.reportedTextBeforeRoot) { this.fail("text data outside of root node."); this.reportedTextBeforeRoot = true; } if (this.closedRoot && !this.reportedTextAfterRoot) { this.fail("text data outside of root node."); this.reportedTextAfterRoot = true; } } pushAttribNS(name, value) { var _a; const { prefix, local } = this.qname(name); const attr = { name, prefix, local, value }; this.attribList.push(attr); // eslint-disable-next-line no-unused-expressions (_a = this.attributeHandler) === null || _a === void 0 ? void 0 : _a.call(this, attr); if (prefix === "xmlns") { const trimmed = value.trim(); if (this.currentXMLVersion === "1.0" && trimmed === "") { this.fail("invalid attempt to undefine prefix in XML 1.0"); } this.topNS[local] = trimmed; nsPairCheck(this, local, trimmed); } else if (name === "xmlns") { const trimmed = value.trim(); this.topNS[""] = trimmed; nsPairCheck(this, "", trimmed); } } pushAttribPlain(name, value) { var _a; const attr = { name, value }; this.attribList.push(attr); // eslint-disable-next-line no-unused-expressions (_a = this.attributeHandler) === null || _a === void 0 ? void 0 : _a.call(this, attr); } /** * End parsing. This performs final well-formedness checks and resets the * parser to a clean state. * * @returns this */ end() { var _a, _b; if (!this.sawRoot) { this.fail("document must contain a root element."); } const { tags } = this; while (tags.length > 0) { const tag = tags.pop(); this.fail(`unclosed tag: ${tag.name}`); } if ((this.state !== S_BEGIN) && (this.state !== S_TEXT)) { this.fail("unexpected end."); } const { text } = this; if (text.length !== 0) { // eslint-disable-next-line no-unused-expressions (_a = this.textHandler) === null || _a === void 0 ? void 0 : _a.call(this, text); this.text = ""; } this._closed = true; // eslint-disable-next-line no-unused-expressions (_b = this.endHandler) === null || _b === void 0 ? void 0 : _b.call(this); this._init(); return this; } /** * Resolve a namespace prefix. * * @param prefix The prefix to resolve. * * @returns The namespace URI or ``undefined`` if the prefix is not defined. */ resolve(prefix) { var _a, _b; let uri = this.topNS[prefix]; if (uri !== undefined) { return uri; } const { tags } = this; for (let index = tags.length - 1; index >= 0; index--) { uri = tags[index].ns[prefix]; if (uri !== undefined) { return uri; } } uri = this.ns[prefix]; if (uri !== undefined) { return uri; } return (_b = (_a = this.opt).resolvePrefix) === null || _b === void 0 ? void 0 : _b.call(_a, prefix); } /** * Parse a qname into its prefix and local name parts. * * @param name The name to parse * * @returns */ qname(name) { // This is faster than using name.split(":"). const colon = name.indexOf(":"); if (colon === -1) { return { prefix: "", local: name }; } const local = name.slice(colon + 1); const prefix = name.slice(0, colon); if (prefix === "" || local === "" || local.includes(":")) { this.fail(`malformed name: ${name}.`); } return { prefix, local }; } processAttribsNS() { var _a; const { attribList } = this; const tag = this.tag; { // add namespace info to tag const { prefix, local } = this.qname(tag.name); tag.prefix = prefix; tag.local = local; const uri = tag.uri = (_a = this.resolve(prefix)) !== null && _a !== void 0 ? _a : ""; if (prefix !== "") { if (prefix === "xmlns") { this.fail("tags may not have \"xmlns\" as prefix."); } if (uri === "") { this.fail(`unbound namespace prefix: ${JSON.stringify(prefix)}.`); tag.uri = prefix; } } } if (attribList.length === 0) { return; } const { attributes } = tag; const seen = new Set(); // Note: do not apply default ns to attributes: // http://www.w3.org/TR/REC-xml-names/#defaulting for (const attr of attribList) { const { name, prefix, local } = attr; let uri; let eqname; if (prefix === "") { uri = name === "xmlns" ? XMLNS_NAMESPACE : ""; eqname = name; } else { uri = this.resolve(prefix); // if there's any attributes with an undefined namespace, // then fail on them now. if (uri === undefined) { this.fail(`unbound namespace prefix: ${JSON.stringify(prefix)}.`); uri = prefix; } eqname = `{${uri}}${local}`; } if (seen.has(eqname)) { this.fail(`duplicate attribute: ${eqname}.`); } seen.add(eqname); attr.uri = uri; attributes[name] = attr; } this.attribList = []; } processAttribsPlain() { const { attribList } = this; // eslint-disable-next-line prefer-destructuring const attributes = this.tag.attributes; for (const { name, value } of attribList) { if (attributes[name] !== undefined) { this.fail(`duplicate attribute: ${name}.`); } attributes[name] = value; } this.attribList = []; } /** * Handle a complete open tag. This parser code calls this once it has seen * the whole tag. This method checks for well-formeness and then emits * ``onopentag``. */ openTag() { var _a; this.processAttribs(); const { tags } = this; const tag = this.tag; tag.isSelfClosing = false; // There cannot be any pending text here due to the onopentagstart that was // necessarily emitted before we get here. So we do not check text. // eslint-disable-next-line no-unused-expressions (_a = this.openTagHandler) === null || _a === void 0 ? void 0 : _a.call(this, tag); tags.push(tag); this.state = S_TEXT; this.name = ""; } /** * Handle a complete self-closing tag. This parser code calls this once it has * seen the whole tag. This method checks for well-formeness and then emits * ``onopentag`` and ``onclosetag``. */ openSelfClosingTag() { var _a, _b, _c; this.processAttribs(); const { tags } = this; const tag = this.tag; tag.isSelfClosing = true; // There cannot be any pending text here due to the onopentagstart that was // necessarily emitted before we get here. So we do not check text. // eslint-disable-next-line no-unused-expressions (_a = this.openTagHandler) === null || _a === void 0 ? void 0 : _a.call(this, tag); // eslint-disable-next-line no-unused-expressions (_b = this.closeTagHandler) === null || _b === void 0 ? void 0 : _b.call(this, tag); const top = this.tag = (_c = tags[tags.length - 1]) !== null && _c !== void 0 ? _c : null; if (top === null) { this.closedRoot = true; } this.state = S_TEXT; this.name = ""; } /** * Handle a complete close tag. This parser code calls this once it has seen * the whole tag. This method checks for well-formeness and then emits * ``onclosetag``. */ closeTag() { const { tags, name } = this; // Our state after this will be S_TEXT, no matter what, and we can clear // tagName now. this.state = S_TEXT; this.name = ""; if (name === "") { this.fail("weird empty close tag."); this.text += ""; return; } const handler = this.closeTagHandler; let l = tags.length; while (l-- > 0) { const tag = this.tag = tags.pop(); this.topNS = tag.ns; // eslint-disable-next-line no-unused-expressions handler === null || handler === void 0 ? void 0 : handler(tag); if (tag.name === name) { break; } this.fail("unexpected close tag."); } if (l === 0) { this.closedRoot = true; } else if (l < 0) { this.fail(`unmatched closing tag: ${name}.`); this.text += ``; } } /** * Resolves an entity. Makes any necessary well-formedness checks. * * @param entity The entity to resolve. * * @returns The parsed entity. */ parseEntity(entity) { // startsWith would be significantly slower for this test. // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with if (entity[0] !== "#") { const defined = this.ENTITIES[entity]; if (defined !== undefined) { return defined; } this.fail(this.isName(entity) ? "undefined entity." : "disallowed character in entity name."); return `&${entity};`; } let num = NaN; if (entity[1] === "x" && /^#x[0-9a-f]+$/i.test(entity)) { num = parseInt(entity.slice(2), 16); } else if (/^#[0-9]+$/.test(entity)) { num = parseInt(entity.slice(1), 10); } // The character reference is required to match the CHAR production. if (!this.isChar(num)) { this.fail("malformed character entity."); return `&${entity};`; } return String.fromCodePoint(num); } } exports.SaxesParser = SaxesParser; //# sourceMappingURL=saxes.js.map