fast-xml-parser
Version:
Validate XML, Parse XML, Build XML without C/C++ based libraries
835 lines (724 loc) • 29.1 kB
JavaScript
'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;
}