UNPKG

fast-xml-parser

Version:

Validate XML, Parse XML, Build XML without C/C++ based libraries

835 lines (724 loc) 29.1 kB
'use strict'; ///@ts-check import { getAllMatches, isExist, DANGEROUS_PROPERTY_NAMES, criticalProperties } from '../util.js'; import xmlNode from './xmlNode.js'; import DocTypeReader from './DocTypeReader.js'; import toNumber from "strnum"; import getIgnoreAttributesFn from "../ignoreAttributes.js"; import { Expression, Matcher } from 'path-expression-matcher'; import { ExpressionSet } from 'path-expression-matcher'; import { EntityDecoder, XML, CURRENCY, COMMON_HTML } from '@nodable/entities'; // const regx = // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)' // .replace(/NAME/g, util.nameRegexp); //const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g"); //const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g"); // Helper functions for attribute and namespace handling /** * Extract raw attributes (without prefix) from prefixed attribute map * @param {object} prefixedAttrs - Attributes with prefix from buildAttributesMap * @param {object} options - Parser options containing attributeNamePrefix * @returns {object} Raw attributes for matcher */ function extractRawAttributes(prefixedAttrs, options) { if (!prefixedAttrs) return {}; // Handle attributesGroupName option const attrs = options.attributesGroupName ? prefixedAttrs[options.attributesGroupName] : prefixedAttrs; if (!attrs) return {}; const rawAttrs = {}; for (const key in attrs) { // Remove the attribute prefix to get raw name if (key.startsWith(options.attributeNamePrefix)) { const rawName = key.substring(options.attributeNamePrefix.length); rawAttrs[rawName] = attrs[key]; } else { // Attribute without prefix (shouldn't normally happen, but be safe) rawAttrs[key] = attrs[key]; } } return rawAttrs; } /** * Extract namespace from raw tag name * @param {string} rawTagName - Tag name possibly with namespace (e.g., "soap:Envelope") * @returns {string|undefined} Namespace or undefined */ function extractNamespace(rawTagName) { if (!rawTagName || typeof rawTagName !== 'string') return undefined; const colonIndex = rawTagName.indexOf(':'); if (colonIndex !== -1 && colonIndex > 0) { const ns = rawTagName.substring(0, colonIndex); // Don't treat xmlns as a namespace if (ns !== 'xmlns') { return ns; } } return undefined; } export default class OrderedObjParser { constructor(options) { this.options = options; this.currentNode = null; this.tagsNodeStack = []; this.parseXml = parseXml; this.parseTextData = parseTextData; this.resolveNameSpace = resolveNameSpace; this.buildAttributesMap = buildAttributesMap; this.isItStopNode = isItStopNode; this.replaceEntitiesValue = replaceEntitiesValue; this.readStopNodeData = readStopNodeData; this.saveTextToParentTag = saveTextToParentTag; this.addChild = addChild; this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes) this.entityExpansionCount = 0; this.currentExpandedLength = 0; let namedEntities = { ...XML }; if (this.options.entityDecoder) { this.entityDecoder = this.options.entityDecoder } else { if (typeof this.options.htmlEntities === "object") namedEntities = this.options.htmlEntities; else if (this.options.htmlEntities === true) namedEntities = { ...COMMON_HTML, ...CURRENCY }; this.entityDecoder = new EntityDecoder({ namedEntities: namedEntities, numericAllowed: this.options.htmlEntities, limit: { maxTotalExpansions: this.options.processEntities.maxTotalExpansions, maxExpandedLength: this.options.processEntities.maxExpandedLength, applyLimitsTo: this.options.processEntities.appliesTo, } //postCheck: resolved => resolved }); } // Initialize path matcher for path-expression-matcher this.matcher = new Matcher(); // Live read-only proxy of matcher — PEM creates and caches this internally. // All user callbacks receive this instead of the mutable matcher. this.readonlyMatcher = this.matcher.readOnly(); // Flag to track if current node is a stop node (optimization) this.isCurrentNodeStopNode = false; // Pre-compile stopNodes expressions this.stopNodeExpressionsSet = new ExpressionSet(); const stopNodesOpts = this.options.stopNodes; if (stopNodesOpts && stopNodesOpts.length > 0) { for (let i = 0; i < stopNodesOpts.length; i++) { const stopNodeExp = stopNodesOpts[i]; if (typeof stopNodeExp === 'string') { // Convert string to Expression object this.stopNodeExpressionsSet.add(new Expression(stopNodeExp)); } else if (stopNodeExp instanceof Expression) { // Already an Expression object this.stopNodeExpressionsSet.add(stopNodeExp); } } this.stopNodeExpressionsSet.seal(); } } } /** * @param {string} val * @param {string} tagName * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath * @param {boolean} dontTrim * @param {boolean} hasAttributes * @param {boolean} isLeafNode * @param {boolean} escapeEntities */ function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) { const options = this.options; if (val !== undefined) { if (options.trimValues && !dontTrim) { val = val.trim(); } if (val.length > 0) { if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath); // Pass jPath string or matcher based on options.jPath setting const jPathOrMatcher = options.jPath ? jPath.toString() : jPath; const newval = options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode); if (newval === null || newval === undefined) { //don't parse return val; } else if (typeof newval !== typeof val || newval !== val) { //overwrite return newval; } else if (options.trimValues) { return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { const trimmedVal = val.trim(); if (trimmedVal === val) { return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { return val; } } } } } function resolveNameSpace(tagname) { if (this.options.removeNSPrefix) { const tags = tagname.split(':'); const prefix = tagname.charAt(0) === '/' ? '/' : ''; if (tags[0] === 'xmlns') { return ''; } if (tags.length === 2) { tagname = prefix + tags[1]; } } return tagname; } //TODO: change regex to capture NS //const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm"); const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm'); function buildAttributesMap(attrStr, jPath, tagName, force = false) { const options = this.options; if (force === true || (options.ignoreAttributes !== true && typeof attrStr === 'string')) { // attrStr = attrStr.replace(/\r?\n/g, ' '); //attrStr = attrStr || attrStr.trim(); const matches = getAllMatches(attrStr, attrsRegx); const len = matches.length; //don't make it inline const attrs = {}; // Pre-process values once: trim + entity replacement // Reused in both matcher update and second pass const processedVals = new Array(len); let hasRawAttrs = false; const rawAttrsForMatcher = {}; for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); const oldVal = matches[i][4]; if (attrName.length && oldVal !== undefined) { let val = oldVal; if (options.trimValues) val = val.trim(); val = this.replaceEntitiesValue(val, tagName, this.readonlyMatcher); processedVals[i] = val; rawAttrsForMatcher[attrName] = val; hasRawAttrs = true; } } // Update matcher ONCE before second pass, if applicable if (hasRawAttrs && typeof jPath === 'object' && jPath.updateCurrent) { jPath.updateCurrent(rawAttrsForMatcher); } // Hoist toString() once — path doesn't change during attribute processing const jPathStr = options.jPath ? jPath.toString() : this.readonlyMatcher; // Second pass: apply processors, build final attrs let hasAttrs = false; for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); if (this.ignoreAttributesFn(attrName, jPathStr)) continue; let aName = options.attributeNamePrefix + attrName; if (attrName.length) { if (options.transformAttributeName) { aName = options.transformAttributeName(aName); } aName = sanitizeName(aName, options); if (matches[i][4] !== undefined) { // Reuse already-processed value — no double entity replacement const oldVal = processedVals[i]; const newVal = options.attributeValueProcessor(attrName, oldVal, jPathStr); if (newVal === null || newVal === undefined) { attrs[aName] = oldVal; } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) { attrs[aName] = newVal; } else { attrs[aName] = parseValue(oldVal, options.parseAttributeValue, options.numberParseOptions); } hasAttrs = true; } else if (options.allowBooleanAttributes) { attrs[aName] = true; hasAttrs = true; } } } if (!hasAttrs) return; if (options.attributesGroupName) { const attrCollection = {}; attrCollection[options.attributesGroupName] = attrs; return attrCollection; } return attrs; } } const parseXml = function (xmlData) { xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line const xmlObj = new xmlNode('!xml'); let currentNode = xmlObj; let textData = ""; // Reset matcher for new document this.matcher.reset(); this.entityDecoder.reset(); // Reset entity expansion counters for this document this.entityExpansionCount = 0; this.currentExpandedLength = 0; const options = this.options; const docTypeReader = new DocTypeReader(options.processEntities); const xmlLen = xmlData.length; for (let i = 0; i < xmlLen; i++) {//for each char in XML data const ch = xmlData[i]; if (ch === '<') { // const nextIndex = i+1; // const _2ndChar = xmlData[nextIndex]; const c1 = xmlData.charCodeAt(i + 1); if (c1 === 47) {//Closing Tag '/' const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.") let tagName = xmlData.substring(i + 2, closeIndex).trim(); if (options.removeNSPrefix) { const colonIndex = tagName.indexOf(":"); if (colonIndex !== -1) { tagName = tagName.substr(colonIndex + 1); } } tagName = transformTagName(options.transformTagName, tagName, "", options).tagName; if (currentNode) { textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); } //check if last tag of nested tag was unpaired tag const lastTagName = this.matcher.getCurrentTag(); if (tagName && options.unpairedTagsSet.has(tagName)) { throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`); } if (lastTagName && options.unpairedTagsSet.has(lastTagName)) { // Pop the unpaired tag this.matcher.pop(); this.tagsNodeStack.pop(); } // Pop the closing tag this.matcher.pop(); this.isCurrentNodeStopNode = false; // Reset flag when closing tag currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope textData = ""; i = closeIndex; } else if (c1 === 63) { //'?' let tagData = readTagExp(xmlData, i, false, "?>"); if (!tagData) throw new Error("Pi Tag is not closed."); textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); const attsMap = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName, true); if (attsMap) { const ver = attsMap[this.options.attributeNamePrefix + "version"]; this.entityDecoder.setXmlVersion(Number(ver) || 1.0); } if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) { //do nothing } else { const childNode = new xmlNode(tagData.tagName); childNode.add(options.textNodeName, ""); if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent && options.ignoreAttributes !== true) { childNode[":@"] = attsMap } this.addChild(currentNode, childNode, this.readonlyMatcher, i); } i = tagData.closeIndex + 1; } else if (c1 === 33 && xmlData.charCodeAt(i + 2) === 45 && xmlData.charCodeAt(i + 3) === 45) { //'!--' const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.") if (options.commentPropName) { const comment = xmlData.substring(i + 4, endIndex - 2); textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); currentNode.add(options.commentPropName, [{ [options.textNodeName]: comment }]); } i = endIndex; } else if (c1 === 33 && xmlData.charCodeAt(i + 2) === 68) { //'!D' const result = docTypeReader.readDocType(xmlData, i); this.entityDecoder.addInputEntities(result.entities); i = result.i; } else if (c1 === 33 && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2; const tagExp = xmlData.substring(i + 9, closeIndex); textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true); if (val == undefined) val = ""; //cdata should be set even if it is 0 length string if (options.cdataPropName) { currentNode.add(options.cdataPropName, [{ [options.textNodeName]: tagExp }]); } else { currentNode.add(options.textNodeName, val); } i = closeIndex + 2; } else {//Opening tag let result = readTagExp(xmlData, i, options.removeNSPrefix); // Safety check: readTagExp can return undefined if (!result) { // Log context for debugging const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlLen, i + 50)); throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`); } let tagName = result.tagName; const rawTagName = result.rawTagName; let tagExp = result.tagExp; let attrExpPresent = result.attrExpPresent; let closeIndex = result.closeIndex; ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); if (options.strictReservedNames && (tagName === options.commentPropName || tagName === options.cdataPropName || tagName === options.textNodeName || tagName === options.attributesGroupName )) { throw new Error(`Invalid tag name: ${tagName}`); } //save text as child node if (currentNode && textData) { if (currentNode.tagname !== '!xml') { //when nested tag is found textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false); } } //check if last tag was unpaired tag const lastTag = currentNode; if (lastTag && options.unpairedTagsSet.has(lastTag.tagname)) { currentNode = this.tagsNodeStack.pop(); this.matcher.pop(); } // Clean up self-closing syntax BEFORE processing attributes // This is where tagExp gets the trailing / removed let isSelfClosing = false; if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) { isSelfClosing = true; if (tagName[tagName.length - 1] === "/") { tagName = tagName.substr(0, tagName.length - 1); tagExp = tagName; } else { tagExp = tagExp.substr(0, tagExp.length - 1); } // Re-check attrExpPresent after cleaning attrExpPresent = (tagName !== tagExp); } // Now process attributes with CLEAN tagExp (no trailing /) let prefixedAttrs = null; let rawAttrs = {}; let namespace = undefined; // Extract namespace from rawTagName namespace = extractNamespace(rawTagName); // Push tag to matcher FIRST (with empty attrs for now) so callbacks see correct path if (tagName !== xmlObj.tagname) { this.matcher.push(tagName, {}, namespace); } // Now build attributes - callbacks will see correct matcher state if (tagName !== tagExp && attrExpPresent) { // Build attributes (returns prefixed attributes for the tree) // Note: buildAttributesMap now internally updates the matcher with raw attributes prefixedAttrs = this.buildAttributesMap(tagExp, this.matcher, tagName); if (prefixedAttrs) { // Extract raw attributes (without prefix) for our use //TODO: seems a performance overhead rawAttrs = extractRawAttributes(prefixedAttrs, options); } } // Now check if this is a stop node (after attributes are set) if (tagName !== xmlObj.tagname) { this.isCurrentNodeStopNode = this.isItStopNode(); } const startIndex = i; if (this.isCurrentNodeStopNode) { let tagContent = ""; // For self-closing tags, content is empty if (isSelfClosing) { i = result.closeIndex; } //unpaired tag else if (options.unpairedTagsSet.has(tagName)) { i = result.closeIndex; } //normal tag else { //read until closing tag is found const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1); if (!result) throw new Error(`Unexpected end of ${rawTagName}`); i = result.i; tagContent = result.tagContent; } const childNode = new xmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } // For stop nodes, store raw content as-is without any processing childNode.add(options.textNodeName, tagContent); this.matcher.pop(); // Pop the stop node tag this.isCurrentNodeStopNode = false; // Reset flag this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); } else { //selfClosing tag if (isSelfClosing) { ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); const childNode = new xmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop self-closing tag this.isCurrentNodeStopNode = false; // Reset flag } else if (options.unpairedTagsSet.has(tagName)) {//unpaired tag const childNode = new xmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop unpaired tag this.isCurrentNodeStopNode = false; // Reset flag i = result.closeIndex; // Continue to next iteration without changing currentNode continue; } //opening tag else { const childNode = new xmlNode(tagName); if (this.tagsNodeStack.length > options.maxNestedTags) { throw new Error("Maximum nested tags exceeded"); } this.tagsNodeStack.push(currentNode); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); currentNode = childNode; } textData = ""; i = closeIndex; } } } else { textData += xmlData[i]; } } return xmlObj.child; } function addChild(currentNode, childNode, matcher, startIndex) { // unset startIndex if not requested if (!this.options.captureMetaData) startIndex = undefined; // Pass jPath string or matcher based on options.jPath setting const jPathOrMatcher = this.options.jPath ? matcher.toString() : matcher; const result = this.options.updateTag(childNode.tagname, jPathOrMatcher, childNode[":@"]) if (result === false) { //do nothing } else if (typeof result === "string") { childNode.tagname = result currentNode.addChild(childNode, startIndex); } else { currentNode.addChild(childNode, startIndex); } } /** * @param {object} val - Entity object with regex and val properties * @param {string} tagName - Tag name * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath */ function replaceEntitiesValue(val, tagName, jPath) { const entityConfig = this.options.processEntities; if (!entityConfig || !entityConfig.enabled) { return val; } // Check if tag is allowed to contain entities if (entityConfig.allowedTags) { const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; const allowed = Array.isArray(entityConfig.allowedTags) ? entityConfig.allowedTags.includes(tagName) : entityConfig.allowedTags(tagName, jPathOrMatcher); if (!allowed) { return val; } } // Apply custom tag filter if provided if (entityConfig.tagFilter) { const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; if (!entityConfig.tagFilter(tagName, jPathOrMatcher)) { return val; // Skip based on custom filter } } return this.entityDecoder.decode(val); } function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) { if (textData) { //store previously collected data as textNode if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0 textData = this.parseTextData(textData, parentNode.tagname, matcher, false, parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false, isLeafNode); if (textData !== undefined && textData !== "") parentNode.add(this.options.textNodeName, textData); textData = ""; } return textData; } /** * @param {Array<Expression>} stopNodeExpressions - Array of compiled Expression objects * @param {Matcher} matcher - Current path matcher */ function isItStopNode() { if (this.stopNodeExpressionsSet.size === 0) return false; return this.matcher.matchesAny(this.stopNodeExpressionsSet); } /** * Returns the tag Expression and where it is ending handling single-double quotes situation * @param {string} xmlData * @param {number} i starting index * @returns */ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") { let attrBoundary = 0; const chars = []; const len = xmlData.length; const closeCode0 = closingChar.charCodeAt(0); const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1; for (let index = i; index < len; index++) { const code = xmlData.charCodeAt(index); if (attrBoundary) { if (code === attrBoundary) attrBoundary = 0; } else if (code === 34 || code === 39) { // " or ' attrBoundary = code; } else if (code === closeCode0) { if (closeCode1 !== -1) { if (xmlData.charCodeAt(index + 1) === closeCode1) { return { data: String.fromCharCode(...chars), index }; } } else { return { data: String.fromCharCode(...chars), index }; } } else if (code === 9) { // \t chars.push(32); // space continue; } chars.push(code); } } function findClosingIndex(xmlData, str, i, errMsg) { const closingIndex = xmlData.indexOf(str, i); if (closingIndex === -1) { throw new Error(errMsg) } else { return closingIndex + str.length - 1; } } function findClosingChar(xmlData, char, i, errMsg) { const closingIndex = xmlData.indexOf(char, i); if (closingIndex === -1) throw new Error(errMsg); return closingIndex; // no offset needed } function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") { const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar); if (!result) return; let tagExp = result.data; const closeIndex = result.index; const separatorIndex = tagExp.search(/\s/); let tagName = tagExp; let attrExpPresent = true; if (separatorIndex !== -1) {//separate tag name and attributes expression tagName = tagExp.substring(0, separatorIndex); tagExp = tagExp.substring(separatorIndex + 1).trimStart(); } const rawTagName = tagName; if (removeNSPrefix) { const colonIndex = tagName.indexOf(":"); if (colonIndex !== -1) { tagName = tagName.substr(colonIndex + 1); attrExpPresent = tagName !== result.data.substr(colonIndex + 1); } } return { tagName: tagName, tagExp: tagExp, closeIndex: closeIndex, attrExpPresent: attrExpPresent, rawTagName: rawTagName, } } /** * find paired tag for a stop node * @param {string} xmlData * @param {string} tagName * @param {number} i */ function readStopNodeData(xmlData, tagName, i) { const startIndex = i; // Starting at 1 since we already have an open tag let openTagCount = 1; const xmllen = xmlData.length; for (; i < xmllen; i++) { if (xmlData[i] === "<") { const c1 = xmlData.charCodeAt(i + 1); if (c1 === 47) {//close tag '/' const closeIndex = findClosingChar(xmlData, ">", i, `${tagName} is not closed`); let closeTagName = xmlData.substring(i + 2, closeIndex).trim(); if (closeTagName === tagName) { openTagCount--; if (openTagCount === 0) { return { tagContent: xmlData.substring(startIndex, i), i: closeIndex } } } i = closeIndex; } else if (c1 === 63) { //? const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.") i = closeIndex; } else if (c1 === 33 && xmlData.charCodeAt(i + 2) === 45 && xmlData.charCodeAt(i + 3) === 45) { // '!--' const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.") i = closeIndex; } else if (c1 === 33 && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2; i = closeIndex; } else { const tagData = readTagExp(xmlData, i, '>') if (tagData) { const openTagName = tagData && tagData.tagName; if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length - 1] !== "/") { openTagCount++; } i = tagData.closeIndex; } } } }//end for loop } function parseValue(val, shouldParse, options) { if (shouldParse && typeof val === 'string') { //console.log(options) const newval = val.trim(); if (newval === 'true') return true; else if (newval === 'false') return false; else return toNumber(val, options); } else { if (isExist(val)) { return val; } else { return ''; } } } function fromCodePoint(str, base, prefix) { const codePoint = Number.parseInt(str, base); if (codePoint >= 0 && codePoint <= 0x10FFFF) { return String.fromCodePoint(codePoint); } else { return prefix + str + ";"; } } function transformTagName(fn, tagName, tagExp, options) { if (fn) { const newTagName = fn(tagName); if (tagExp === tagName) { tagExp = newTagName } tagName = newTagName; } tagName = sanitizeName(tagName, options); return { tagName, tagExp }; } function sanitizeName(name, options) { if (criticalProperties.includes(name)) { throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`); } else if (DANGEROUS_PROPERTY_NAMES.includes(name)) { return options.onDangerousProperty(name); } return name; }