Files
setup-java/dist/setup/824.index.js
T

7165 lines
222 KiB
JavaScript
Raw Permalink Normal View History

export const id = 824;
export const ids = [824];
export const modules = {
/***/ 5824:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ XMLParser: () => (/* reexport safe */ _xmlparser_XMLParser_js__WEBPACK_IMPORTED_MODULE_1__.A),
/* harmony export */ i: () => (/* binding */ XMLValidator)
/* harmony export */ });
/* harmony import */ var _validator_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1176);
/* harmony import */ var _xmlparser_XMLParser_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6009);
const XMLValidator = {
validate: _validator_js__WEBPACK_IMPORTED_MODULE_0__/* .validate */ .t
}
/***/ }),
/***/ 984:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ Eo: () => (/* binding */ isName),
/* harmony export */ Xe: () => (/* binding */ getAllMatches),
/* harmony export */ q9: () => (/* binding */ DANGEROUS_PROPERTY_NAMES),
/* harmony export */ vl: () => (/* binding */ criticalProperties),
/* harmony export */ yQ: () => (/* binding */ isExist)
/* harmony export */ });
/* unused harmony exports nameRegexp, isEmptyObject, getValue */
const nameStartChar = ':A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
const nameChar = nameStartChar + '\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040';
const nameRegexp = '[' + nameStartChar + '][' + nameChar + ']*';
const regexName = new RegExp('^' + nameRegexp + '$');
function getAllMatches(string, regex) {
const matches = [];
let match = regex.exec(string);
while (match) {
const allmatches = [];
allmatches.startIndex = regex.lastIndex - match[0].length;
const len = match.length;
for (let index = 0; index < len; index++) {
allmatches.push(match[index]);
}
matches.push(allmatches);
match = regex.exec(string);
}
return matches;
}
const isName = function (string) {
const match = regexName.exec(string);
return !(match === null || typeof match === 'undefined');
}
function isExist(v) {
return typeof v !== 'undefined';
}
function isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}
function getValue(v) {
if (exports.isExist(v)) {
return v;
} else {
return '';
}
}
/**
* Dangerous property names that could lead to prototype pollution or security issues
*/
const DANGEROUS_PROPERTY_NAMES = [
// '__proto__',
// 'constructor',
// 'prototype',
'hasOwnProperty',
'toString',
'valueOf',
'__defineGetter__',
'__defineSetter__',
'__lookupGetter__',
'__lookupSetter__'
];
const criticalProperties = ["__proto__", "constructor", "prototype"];
/***/ }),
/***/ 1176:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ t: () => (/* binding */ validate)
/* harmony export */ });
/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(984);
const defaultOptions = {
allowBooleanAttributes: false, //A tag can have attributes without any value
unpairedTags: []
};
//const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g");
function validate(xmlData, options) {
options = Object.assign({}, defaultOptions, options);
//xmlData = xmlData.replace(/(\r\n|\n|\r)/gm,"");//make it single line
//xmlData = xmlData.replace(/(^\s*<\?xml.*?\?>)/g,"");//Remove XML starting tag
//xmlData = xmlData.replace(/(<!DOCTYPE[\s\w\"\.\/\-\:]+(\[.*\])*\s*>)/g,"");//Remove DOCTYPE
const tags = [];
let tagFound = false;
//indicates that the root tag has been closed (aka. depth 0 has been reached)
let reachedRoot = false;
if (xmlData[0] === '\ufeff') {
// check for byte order mark (BOM)
xmlData = xmlData.substr(1);
}
for (let i = 0; i < xmlData.length; i++) {
if (xmlData[i] === '<' && xmlData[i + 1] === '?') {
i += 2;
i = readPI(xmlData, i);
if (i.err) return i;
} else if (xmlData[i] === '<') {
//starting of tag
//read until you reach to '>' avoiding any '>' in attribute value
let tagStartPos = i;
i++;
if (xmlData[i] === '!') {
i = readCommentAndCDATA(xmlData, i);
continue;
} else {
let closingTag = false;
if (xmlData[i] === '/') {
//closing tag
closingTag = true;
i++;
}
//read tagname
let tagName = '';
for (; i < xmlData.length &&
xmlData[i] !== '>' &&
xmlData[i] !== ' ' &&
xmlData[i] !== '\t' &&
xmlData[i] !== '\n' &&
xmlData[i] !== '\r'; i++
) {
tagName += xmlData[i];
}
tagName = tagName.trim();
//console.log(tagName);
if (tagName[tagName.length - 1] === '/') {
//self closing tag without attributes
tagName = tagName.substring(0, tagName.length - 1);
//continue;
i--;
}
if (!validateTagName(tagName)) {
let msg;
if (tagName.trim().length === 0) {
msg = "Invalid space after '<'.";
} else {
msg = "Tag '" + tagName + "' is an invalid name.";
}
return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i));
}
const result = readAttributeStr(xmlData, i);
if (result === false) {
return getErrorObject('InvalidAttr', "Attributes for '" + tagName + "' have open quote.", getLineNumberForPosition(xmlData, i));
}
let attrStr = result.value;
i = result.index;
if (attrStr[attrStr.length - 1] === '/') {
//self closing tag
const attrStrStart = i - attrStr.length;
attrStr = attrStr.substring(0, attrStr.length - 1);
const isValid = validateAttributeString(attrStr, options);
if (isValid === true) {
tagFound = true;
//continue; //text may presents after self closing tag
} else {
//the result from the nested function returns the position of the error within the attribute
//in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute
//this gives us the absolute index in the entire xml, which we can use to find the line at last
return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line));
}
} else if (closingTag) {
if (!result.tagClosed) {
return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
} else if (attrStr.trim().length > 0) {
return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
} else if (tags.length === 0) {
return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
} else {
const otg = tags.pop();
if (tagName !== otg.tagName) {
let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
return getErrorObject('InvalidTag',
"Expected closing tag '" + otg.tagName + "' (opened in line " + openPos.line + ", col " + openPos.col + ") instead of closing tag '" + tagName + "'.",
getLineNumberForPosition(xmlData, tagStartPos));
}
//when there are no more tags, we reached the root level.
if (tags.length == 0) {
reachedRoot = true;
}
}
} else {
const isValid = validateAttributeString(attrStr, options);
if (isValid !== true) {
//the result from the nested function returns the position of the error within the attribute
//in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute
//this gives us the absolute index in the entire xml, which we can use to find the line at last
return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
}
//if the root level has been reached before ...
if (reachedRoot === true) {
return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
} else if (options.unpairedTags.indexOf(tagName) !== -1) {
//don't push into stack
} else {
tags.push({ tagName, tagStartPos });
}
tagFound = true;
}
//skip tag text value
//It may include comments and CDATA value
for (i++; i < xmlData.length; i++) {
if (xmlData[i] === '<') {
if (xmlData[i + 1] === '!') {
//comment or CADATA
i++;
i = readCommentAndCDATA(xmlData, i);
continue;
} else if (xmlData[i + 1] === '?') {
i = readPI(xmlData, ++i);
if (i.err) return i;
} else {
break;
}
} else if (xmlData[i] === '&') {
const afterAmp = validateAmpersand(xmlData, i);
if (afterAmp == -1)
return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
i = afterAmp;
} else {
if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
return getErrorObject('InvalidXml', "Extra text at the end", getLineNumberForPosition(xmlData, i));
}
}
} //end of reading tag text value
if (xmlData[i] === '<') {
i--;
}
}
} else {
if (isWhiteSpace(xmlData[i])) {
continue;
}
return getErrorObject('InvalidChar', "char '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
}
}
if (!tagFound) {
return getErrorObject('InvalidXml', 'Start tag expected.', 1);
} else if (tags.length == 1) {
return getErrorObject('InvalidTag', "Unclosed tag '" + tags[0].tagName + "'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
} else if (tags.length > 0) {
return getErrorObject('InvalidXml', "Invalid '" +
JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '') +
"' found.", { line: 1, col: 1 });
}
return true;
};
function isWhiteSpace(char) {
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
}
/**
* Read Processing insstructions and skip
* @param {*} xmlData
* @param {*} i
*/
function readPI(xmlData, i) {
const start = i;
for (; i < xmlData.length; i++) {
if (xmlData[i] == '?' || xmlData[i] == ' ') {
//tagname
const tagname = xmlData.substr(start, i - start);
if (i > 5 && tagname === 'xml') {
return getErrorObject('InvalidXml', 'XML declaration allowed only at the start of the document.', getLineNumberForPosition(xmlData, i));
} else if (xmlData[i] == '?' && xmlData[i + 1] == '>') {
//check if valid attribut string
i++;
break;
} else {
continue;
}
}
}
return i;
}
function readCommentAndCDATA(xmlData, i) {
if (xmlData.length > i + 5 && xmlData[i + 1] === '-' && xmlData[i + 2] === '-') {
//comment
for (i += 3; i < xmlData.length; i++) {
if (xmlData[i] === '-' && xmlData[i + 1] === '-' && xmlData[i + 2] === '>') {
i += 2;
break;
}
}
} else if (
xmlData.length > i + 8 &&
xmlData[i + 1] === 'D' &&
xmlData[i + 2] === 'O' &&
xmlData[i + 3] === 'C' &&
xmlData[i + 4] === 'T' &&
xmlData[i + 5] === 'Y' &&
xmlData[i + 6] === 'P' &&
xmlData[i + 7] === 'E'
) {
let angleBracketsCount = 1;
for (i += 8; i < xmlData.length; i++) {
if (xmlData[i] === '<') {
angleBracketsCount++;
} else if (xmlData[i] === '>') {
angleBracketsCount--;
if (angleBracketsCount === 0) {
break;
}
}
}
} else if (
xmlData.length > i + 9 &&
xmlData[i + 1] === '[' &&
xmlData[i + 2] === 'C' &&
xmlData[i + 3] === 'D' &&
xmlData[i + 4] === 'A' &&
xmlData[i + 5] === 'T' &&
xmlData[i + 6] === 'A' &&
xmlData[i + 7] === '['
) {
for (i += 8; i < xmlData.length; i++) {
if (xmlData[i] === ']' && xmlData[i + 1] === ']' && xmlData[i + 2] === '>') {
i += 2;
break;
}
}
}
return i;
}
const doubleQuote = '"';
const singleQuote = "'";
/**
* Keep reading xmlData until '<' is found outside the attribute value.
* @param {string} xmlData
* @param {number} i
*/
function readAttributeStr(xmlData, i) {
let attrStr = '';
let startChar = '';
let tagClosed = false;
for (; i < xmlData.length; i++) {
if (xmlData[i] === doubleQuote || xmlData[i] === singleQuote) {
if (startChar === '') {
startChar = xmlData[i];
} else if (startChar !== xmlData[i]) {
//if vaue is enclosed with double quote then single quotes are allowed inside the value and vice versa
} else {
startChar = '';
}
} else if (xmlData[i] === '>') {
if (startChar === '') {
tagClosed = true;
break;
}
}
attrStr += xmlData[i];
}
if (startChar !== '') {
return false;
}
return {
value: attrStr,
index: i,
tagClosed: tagClosed
};
}
/**
* Select all the attributes whether valid or invalid.
*/
const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s\\S])*?)\\5)?', 'g');
//attr, ="sd", a="amit's", a="sd"b="saf", ab cd=""
function validateAttributeString(attrStr, options) {
//console.log("start:"+attrStr+":end");
//if(attrStr.trim().length === 0) return true; //empty string
const matches = (0,_util_js__WEBPACK_IMPORTED_MODULE_0__/* .getAllMatches */ .Xe)(attrStr, validAttrStrRegxp);
const attrNames = {};
for (let i = 0; i < matches.length; i++) {
if (matches[i][1].length === 0) {
//nospace before attribute name: a="sd"b="saf"
return getErrorObject('InvalidAttr', "Attribute '" + matches[i][2] + "' has no space in starting.", getPositionFromMatch(matches[i]))
} else if (matches[i][3] !== undefined && matches[i][4] === undefined) {
return getErrorObject('InvalidAttr', "Attribute '" + matches[i][2] + "' is without value.", getPositionFromMatch(matches[i]));
} else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
//independent attribute: ab
return getErrorObject('InvalidAttr', "boolean attribute '" + matches[i][2] + "' is not allowed.", getPositionFromMatch(matches[i]));
}
/* else if(matches[i][6] === undefined){//attribute without value: ab=
return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}};
} */
const attrName = matches[i][2];
if (!validateAttrName(attrName)) {
return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' is an invalid name.", getPositionFromMatch(matches[i]));
}
if (!Object.prototype.hasOwnProperty.call(attrNames, attrName)) {
//check for duplicate attribute.
attrNames[attrName] = 1;
} else {
return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' is repeated.", getPositionFromMatch(matches[i]));
}
}
return true;
}
function validateNumberAmpersand(xmlData, i) {
let re = /\d/;
if (xmlData[i] === 'x') {
i++;
re = /[\da-fA-F]/;
}
for (; i < xmlData.length; i++) {
if (xmlData[i] === ';')
return i;
if (!xmlData[i].match(re))
break;
}
return -1;
}
function validateAmpersand(xmlData, i) {
// https://www.w3.org/TR/xml/#dt-charref
i++;
if (xmlData[i] === ';')
return -1;
if (xmlData[i] === '#') {
i++;
return validateNumberAmpersand(xmlData, i);
}
let count = 0;
for (; i < xmlData.length; i++, count++) {
if (xmlData[i].match(/\w/) && count < 20)
continue;
if (xmlData[i] === ';')
break;
return -1;
}
return i;
}
function getErrorObject(code, message, lineNumber) {
return {
err: {
code: code,
msg: message,
line: lineNumber.line || lineNumber,
col: lineNumber.col,
},
};
}
function validateAttrName(attrName) {
return (0,_util_js__WEBPACK_IMPORTED_MODULE_0__/* .isName */ .Eo)(attrName);
}
// const startsWithXML = /^xml/i;
function validateTagName(tagname) {
return (0,_util_js__WEBPACK_IMPORTED_MODULE_0__/* .isName */ .Eo)(tagname) /* && !tagname.match(startsWithXML) */;
}
//this function returns the line number for the character at the given index
function getLineNumberForPosition(xmlData, index) {
const lines = xmlData.substring(0, index).split(/\r?\n/);
return {
line: lines.length,
// column number is last line's length + 1, because column numbering starts at 1:
col: lines[lines.length - 1].length + 1
};
}
//this function returns the position of the first character of match within attrStr
function getPositionFromMatch(match) {
return match.startIndex + match[1].length;
}
/***/ }),
/***/ 6009:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
A: () => (/* binding */ XMLParser)
});
// EXTERNAL MODULE: ./node_modules/fast-xml-parser/src/util.js
var util = __webpack_require__(984);
;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js
const defaultOnDangerousProperty = (name) => {
if (util/* DANGEROUS_PROPERTY_NAMES */.q9.includes(name)) {
return "__" + name;
}
return name;
};
const defaultOptions = {
preserveOrder: false,
attributeNamePrefix: '@_',
attributesGroupName: false,
textNodeName: '#text',
ignoreAttributes: true,
removeNSPrefix: false, // remove NS from tag name or attribute name if true
allowBooleanAttributes: false, //a tag can have attributes without any value
//ignoreRootElement : false,
parseTagValue: true,
parseAttributeValue: false,
trimValues: true, //Trim string values of tag and attributes
cdataPropName: false,
numberParseOptions: {
hex: true,
leadingZeros: true,
eNotation: true,
unicode: false
},
tagValueProcessor: function (tagName, val) {
return val;
},
attributeValueProcessor: function (attrName, val) {
return val;
},
stopNodes: [], //nested tags will not be parsed even for errors
alwaysCreateTextNode: false,
isArray: () => false,
commentPropName: false,
unpairedTags: [],
processEntities: true,
htmlEntities: false,
entityDecoder: null,
ignoreDeclaration: false,
ignorePiTags: false,
transformTagName: false,
transformAttributeName: false,
updateTag: function (tagName, jPath, attrs) {
return tagName
},
// skipEmptyListItem: false
captureMetaData: false,
maxNestedTags: 100,
strictReservedNames: true,
jPath: true, // if true, pass jPath string to callbacks; if false, pass matcher instance
onDangerousProperty: defaultOnDangerousProperty
};
/**
* Validates that a property name is safe to use
* @param {string} propertyName - The property name to validate
* @param {string} optionName - The option field name (for error message)
* @throws {Error} If property name is dangerous
*/
function validatePropertyName(propertyName, optionName) {
if (typeof propertyName !== 'string') {
return; // Only validate string property names
}
const normalized = propertyName.toLowerCase();
if (util/* DANGEROUS_PROPERTY_NAMES */.q9.some(dangerous => normalized === dangerous.toLowerCase())) {
throw new Error(
`[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
);
}
if (util/* criticalProperties */.vl.some(dangerous => normalized === dangerous.toLowerCase())) {
throw new Error(
`[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
);
}
}
/**
* Normalizes processEntities option for backward compatibility
* @param {boolean|object} value
* @returns {object} Always returns normalized object
*/
function normalizeProcessEntities(value, htmlEntities) {
// Boolean backward compatibility
if (typeof value === 'boolean') {
return {
enabled: value, // true or false
maxEntitySize: 10000,
maxExpansionDepth: 10000,
maxTotalExpansions: Infinity,
maxExpandedLength: 100000,
maxEntityCount: 1000,
allowedTags: null,
tagFilter: null,
appliesTo: "all",
};
}
// Object config - merge with defaults
if (typeof value === 'object' && value !== null) {
return {
enabled: value.enabled !== false,
maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000),
maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10000),
maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity),
maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000),
maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000),
allowedTags: value.allowedTags ?? null,
tagFilter: value.tagFilter ?? null,
appliesTo: value.appliesTo ?? "all",
};
}
// Default to enabled with limits
return normalizeProcessEntities(true);
}
const buildOptions = function (options) {
const built = Object.assign({}, defaultOptions, options);
// Validate property names to prevent prototype pollution
const propertyNameOptions = [
{ value: built.attributeNamePrefix, name: 'attributeNamePrefix' },
{ value: built.attributesGroupName, name: 'attributesGroupName' },
{ value: built.textNodeName, name: 'textNodeName' },
{ value: built.cdataPropName, name: 'cdataPropName' },
{ value: built.commentPropName, name: 'commentPropName' }
];
for (const { value, name } of propertyNameOptions) {
if (value) {
validatePropertyName(value, name);
}
}
if (built.onDangerousProperty === null) {
built.onDangerousProperty = defaultOnDangerousProperty;
}
// Always normalize processEntities for backward compatibility and validation
built.processEntities = normalizeProcessEntities(built.processEntities, built.htmlEntities);
built.unpairedTagsSet = new Set(built.unpairedTags);
// Convert old-style stopNodes for backward compatibility
if (built.stopNodes && Array.isArray(built.stopNodes)) {
built.stopNodes = built.stopNodes.map(node => {
if (typeof node === 'string' && node.startsWith('*.')) {
// Old syntax: *.tagname meant "tagname anywhere"
// Convert to new syntax: ..tagname
return '..' + node.substring(2);
}
return node;
});
}
//console.debug(built.processEntities)
return built;
};
;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/xmlNode.js
let METADATA_SYMBOL;
if (typeof Symbol !== "function") {
METADATA_SYMBOL = "@@xmlMetadata";
} else {
METADATA_SYMBOL = Symbol("XML Node Metadata");
}
class XmlNode {
constructor(tagname) {
this.tagname = tagname;
this.child = []; //nested tags, text, cdata, comments in order
this[":@"] = Object.create(null); //attributes map
}
add(key, val) {
// this.child.push( {name : key, val: val, isCdata: isCdata });
if (key === "__proto__") key = "#__proto__";
this.child.push({ [key]: val });
}
addChild(node, startIndex) {
if (node.tagname === "__proto__") node.tagname = "#__proto__";
if (node[":@"] && Object.keys(node[":@"]).length > 0) {
this.child.push({ [node.tagname]: node.child, [":@"]: node[":@"] });
} else {
this.child.push({ [node.tagname]: node.child });
}
// if requested, add the startIndex
this.addStartIndex(startIndex);
}
addStartIndex(startIndex) {
if (startIndex !== undefined) {
// Note: for now we just overwrite the metadata. If we had more complex metadata,
// we might need to do an object append here: metadata = { ...metadata, startIndex }
this.child[this.child.length - 1][METADATA_SYMBOL] = { startIndex };
}
}
addEndIndex(endIndex) {
const lastChild = this.child[this.child.length - 1];
// endIndex is write-once: when updateTag drops a node, the last child is a
// previously completed sibling whose endIndex must not be overwritten
if (lastChild !== undefined && lastChild[METADATA_SYMBOL] !== undefined
&& lastChild[METADATA_SYMBOL].endIndex === undefined) {
lastChild[METADATA_SYMBOL].endIndex = endIndex;
}
}
/** symbol used for metadata */
static getMetaDataSymbol() {
return METADATA_SYMBOL;
}
}
// EXTERNAL MODULE: ./node_modules/xml-naming/src/index.js
var src = __webpack_require__(4658);
;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js
class DocTypeReader {
constructor(options, xmlVersion) {
this.suppressValidationErr = !options;
this.options = options;
this.xmlVersion = xmlVersion || 1.0;
}
setXmlVersion(xmlVersion = 1.0) {
this.xmlVersion = xmlVersion;
}
readDocType(xmlData, i) {
const entities = Object.create(null);
let entityCount = 0;
if (xmlData[i + 3] === 'O' &&
xmlData[i + 4] === 'C' &&
xmlData[i + 5] === 'T' &&
xmlData[i + 6] === 'Y' &&
xmlData[i + 7] === 'P' &&
xmlData[i + 8] === 'E') {
i = i + 9;
let angleBracketsCount = 1;
let hasBody = false, comment = false;
let quoteChar = null; // tracks an open SYSTEM/PUBLIC literal before the '[' body
let exp = "";
for (; i < xmlData.length; i++) {
// Inside a quoted external-identifier literal — XML allows '<'
// and '>' as plain data here, so they must not be interpreted
// as DOCTYPE structure until the matching quote closes.
if (quoteChar !== null) {
if (xmlData[i] === quoteChar) quoteChar = null;
exp += xmlData[i];
continue;
}
if (!hasBody && !comment && (xmlData[i] === '"' || xmlData[i] === "'")) {
quoteChar = xmlData[i];
exp += xmlData[i];
continue;
}
if (xmlData[i] === '<' && !comment) { //Determine the tag type
if (hasBody && hasSeq(xmlData, "!ENTITY", i)) {
i += 7;
let entityName, val;
[entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);
if (val.indexOf("&") === -1) { //Parameter entities are not supported
if (this.options.enabled !== false &&
this.options.maxEntityCount != null &&
entityCount >= this.options.maxEntityCount) {
throw new Error(
`Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})`
);
}
//const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
//const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
entities[entityName] = val;
entityCount++;
}
}
else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) {
i += 8;//Not supported
const { index } = this.readElementExp(xmlData, i + 1);
i = index;
} else if (hasBody && hasSeq(xmlData, "!ATTLIST", i)) {
i += 8;//Not supported
// const {index} = this.readAttlistExp(xmlData,i+1);
// i = index;
} else if (hasBody && hasSeq(xmlData, "!NOTATION", i)) {
i += 9;//Not supported
const { index } = this.readNotationExp(xmlData, i + 1, this.suppressValidationErr);
i = index;
} else if (hasSeq(xmlData, "!--", i)) comment = true;
else throw new Error(`Invalid DOCTYPE`);
angleBracketsCount++;
exp = "";
} else if (xmlData[i] === '>') { //Read tag content
if (comment) {
if (xmlData[i - 1] === "-" && xmlData[i - 2] === "-") {
comment = false;
angleBracketsCount--;
}
} else {
angleBracketsCount--;
}
if (angleBracketsCount === 0) {
break;
}
} else if (xmlData[i] === '[') {
hasBody = true;
} else {
exp += xmlData[i];
}
}
if (quoteChar !== null || angleBracketsCount !== 0) {
throw new Error(`Unclosed DOCTYPE`);
}
} else {
throw new Error(`Invalid Tag instead of DOCTYPE`);
}
return { entities, i };
}
readEntityExp(xmlData, i) {
//External entities are not supported
// <!ENTITY ext SYSTEM "http://normal-website.com" >
//Parameter entities are not supported
// <!ENTITY entityname "&anotherElement;">
//Internal entities are supported
// <!ENTITY entityname "replacement text">
// Skip leading whitespace after <!ENTITY
i = skipWhitespace(xmlData, i);
// Read entity name
const startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
i++;
}
let entityName = xmlData.substring(startIndex, i);
validateEntityName(entityName, { xmlVersion: this.xmlVersion });
// Skip whitespace after entity name
i = skipWhitespace(xmlData, i);
// Check for unsupported constructs (external entities or parameter entities)
if (!this.suppressValidationErr) {
if (xmlData.substring(i, i + 6).toUpperCase() === "SYSTEM") {
throw new Error("External entities are not supported");
} else if (xmlData[i] === "%") {
throw new Error("Parameter entities are not supported");
}
}
// Read entity value (internal entity)
let entityValue = "";
[i, entityValue] = this.readIdentifierVal(xmlData, i, "entity");
// Validate entity size
if (this.options.enabled !== false &&
this.options.maxEntitySize != null &&
entityValue.length > this.options.maxEntitySize) {
throw new Error(
`Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})`
);
}
i--;
return [entityName, entityValue, i];
}
readNotationExp(xmlData, i) {
// Skip leading whitespace after <!NOTATION
i = skipWhitespace(xmlData, i);
// Read notation name
const startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
let notationName = xmlData.substring(startIndex, i);
!this.suppressValidationErr && validateEntityName(notationName, { xmlVersion: this.xmlVersion });
// Skip whitespace after notation name
i = skipWhitespace(xmlData, i);
// Check identifier type (SYSTEM or PUBLIC)
const identifierType = xmlData.substring(i, i + 6).toUpperCase();
if (!this.suppressValidationErr && identifierType !== "SYSTEM" && identifierType !== "PUBLIC") {
throw new Error(`Expected SYSTEM or PUBLIC, found "${identifierType}"`);
}
i += identifierType.length;
// Skip whitespace after identifier type
i = skipWhitespace(xmlData, i);
// Read public identifier (if PUBLIC)
let publicIdentifier = null;
let systemIdentifier = null;
if (identifierType === "PUBLIC") {
[i, publicIdentifier] = this.readIdentifierVal(xmlData, i, "publicIdentifier");
// Skip whitespace after public identifier
i = skipWhitespace(xmlData, i);
// Optionally read system identifier
if (xmlData[i] === '"' || xmlData[i] === "'") {
[i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
}
} else if (identifierType === "SYSTEM") {
// Read system identifier (mandatory for SYSTEM)
[i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
if (!this.suppressValidationErr && !systemIdentifier) {
throw new Error("Missing mandatory system identifier for SYSTEM notation");
}
}
return { notationName, publicIdentifier, systemIdentifier, index: --i };
}
readIdentifierVal(xmlData, i, type) {
let identifierVal = "";
const startChar = xmlData[i];
if (startChar !== '"' && startChar !== "'") {
throw new Error(`Expected quoted string, found "${startChar}"`);
}
i++;
const startIndex = i;
while (i < xmlData.length && xmlData[i] !== startChar) {
i++;
}
identifierVal = xmlData.substring(startIndex, i);
if (xmlData[i] !== startChar) {
throw new Error(`Unterminated ${type} value`);
}
i++;
return [i, identifierVal];
}
readElementExp(xmlData, i) {
// <!ELEMENT br EMPTY>
// <!ELEMENT div ANY>
// <!ELEMENT title (#PCDATA)>
// <!ELEMENT book (title, author+)>
// <!ELEMENT name (content-model)>
// Skip leading whitespace after <!ELEMENT
i = skipWhitespace(xmlData, i);
// Read element name
const startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
let elementName = xmlData.substring(startIndex, i);
// Validate element name
if (!this.suppressValidationErr && !(0,src/* qName */.fG)(elementName, { xmlVersion: this.xmlVersion })) {
throw new Error(`Invalid element name: "${elementName}"`);
}
// Skip whitespace after element name
i = skipWhitespace(xmlData, i);
let contentModel = "";
// Expect '(' to start content model
if (xmlData[i] === "E" && hasSeq(xmlData, "MPTY", i)) i += 4;
else if (xmlData[i] === "A" && hasSeq(xmlData, "NY", i)) i += 2;
else if (xmlData[i] === "(") {
i++; // Move past '('
// Read content model
const startIndex = i;
while (i < xmlData.length && xmlData[i] !== ")") {
i++;
}
contentModel = xmlData.substring(startIndex, i);
if (xmlData[i] !== ")") {
throw new Error("Unterminated content model");
}
} else if (!this.suppressValidationErr) {
throw new Error(`Invalid Element Expression, found "${xmlData[i]}"`);
}
return {
elementName,
contentModel: contentModel.trim(),
index: i
};
}
readAttlistExp(xmlData, i) {
// Skip leading whitespace after <!ATTLIST
i = skipWhitespace(xmlData, i);
// Read element name
let startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
let elementName = xmlData.substring(startIndex, i);
// Validate element name
validateEntityName(elementName, { xmlVersion: this.xmlVersion })
// Skip whitespace after element name
i = skipWhitespace(xmlData, i);
// Read attribute name
startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
let attributeName = xmlData.substring(startIndex, i);
// Validate attribute name
if (!validateEntityName(attributeName, { xmlVersion: this.xmlVersion })) {
throw new Error(`Invalid attribute name: "${attributeName}"`);
}
// Skip whitespace after attribute name
i = skipWhitespace(xmlData, i);
// Read attribute type
let attributeType = "";
if (xmlData.substring(i, i + 8).toUpperCase() === "NOTATION") {
attributeType = "NOTATION";
i += 8; // Move past "NOTATION"
// Skip whitespace after "NOTATION"
i = skipWhitespace(xmlData, i);
// Expect '(' to start the list of notations
if (xmlData[i] !== "(") {
throw new Error(`Expected '(', found "${xmlData[i]}"`);
}
i++; // Move past '('
// Read the list of allowed notations
let allowedNotations = [];
while (i < xmlData.length && xmlData[i] !== ")") {
const startIndex = i;
while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") {
i++;
}
let notation = xmlData.substring(startIndex, i);
// Validate notation name
notation = notation.trim();
if (!validateEntityName(notation, { xmlVersion: this.xmlVersion })) {
throw new Error(`Invalid notation name: "${notation}"`);
}
allowedNotations.push(notation);
// Skip '|' separator or exit loop
if (xmlData[i] === "|") {
i++; // Move past '|'
i = skipWhitespace(xmlData, i); // Skip optional whitespace after '|'
}
}
if (xmlData[i] !== ")") {
throw new Error("Unterminated list of notations");
}
i++; // Move past ')'
// Store the allowed notations as part of the attribute type
attributeType += " (" + allowedNotations.join("|") + ")";
} else {
// Handle simple types (e.g., CDATA, ID, IDREF, etc.)
const startIndex = i;
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
i++;
}
attributeType += xmlData.substring(startIndex, i);
// Validate simple attribute type
const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"];
if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) {
throw new Error(`Invalid attribute type: "${attributeType}"`);
}
}
// Skip whitespace after attribute type
i = skipWhitespace(xmlData, i);
// Read default value
let defaultValue = "";
if (xmlData.substring(i, i + 8).toUpperCase() === "#REQUIRED") {
defaultValue = "#REQUIRED";
i += 8;
} else if (xmlData.substring(i, i + 7).toUpperCase() === "#IMPLIED") {
defaultValue = "#IMPLIED";
i += 7;
} else {
[i, defaultValue] = this.readIdentifierVal(xmlData, i, "ATTLIST");
}
return {
elementName,
attributeName,
attributeType,
defaultValue,
index: i
}
}
}
const skipWhitespace = (data, index) => {
while (index < data.length && /\s/.test(data[index])) {
index++;
}
return index;
};
function hasSeq(data, seq, i) {
for (let j = 0; j < seq.length; j++) {
if (seq[j] !== data[i + j + 1]) return false;
}
return true;
}
function validateEntityName(name, xmlVersion) {
if ((0,src/* qName */.fG)(name, { xmlVersion: xmlVersion }))
return name;
else
throw new Error(`Invalid entity name ${name}`);
}
;// CONCATENATED MODULE: ./node_modules/anynum/digitTable.js
/**
* Flat lookup table: maps Unicode code point → ASCII digit (0-9).
* Only decimal digit characters (Unicode category Nd) are included.
*
* Strategy: Int32Array of size (maxCodePoint - minCodePoint + 1).
* Value 0xFF means "not a digit". Value 0-9 is the ASCII digit value.
* This gives O(1) lookup with no branching, no bisect, no loop.
*
* Memory: range is 0x0660 to 0x1FBF0 → ~129,936 entries × 1 byte = ~127 KB.
* Acceptable for a one-time init; lookup is a single array index.
*/
// All known Unicode Nd (decimal digit) script zero code points.
// Each script has exactly 10 consecutive digits: zero+0 .. zero+9.
const SCRIPT_ZEROS = [
// Basic Latin (ASCII) — included for completeness / pass-through
0x0030, // 0-9
// Arabic scripts
0x0660, // Arabic-Indic ٠١٢٣٤٥٦٧٨٩
0x06F0, // Extended Arabic-Indic (Urdu/Persian/Sindhi) ۰۱۲۳
// Indic scripts
0x0966, // Devanagari ०१२३४५६७८९
0x09E6, // Bengali ০১২৩৪৫৬৭৮৯
0x0A66, // Gurmukhi ੦੧੨੩੪੫੬੭੮੯
0x0AE6, // Gujarati ૦૧૨૩૪૫૬૭૮૯
0x0B66, // Odia ୦୧୨୩୪୫୬୭୮୯
0x0BE6, // Tamil ௦௧௨௩௪௫௬௭௮௯
0x0C66, // Telugu ౦౧౨౩౪౫౬౭౮౯
0x0CE6, // Kannada ೦೧೨೩೪೫೬೭೮೯
0x0D66, // Malayalam ൦൧൨൩൪൫൬൭൮൯
0x0DE6, // Sinhala Archaic ෦෧෨෩෪෫෬෭෮෯
// Southeast Asian scripts
0x0E50, // Thai ๐๑๒๓๔๕๖๗๘๙
0x0ED0, // Lao ໐໑໒໓໔໕໖໗໘໙
0x0F20, // Tibetan ༠༡༢༣༤༥༦༧༨༩
0x1040, // Myanmar ၀၁၂၃၄၅၆၇၈၉
0x1090, // Myanmar Shan ႐႑႒႓႔႕႖႗႘႙
0x17E0, // Khmer ០១២៣៤៥៦៧៨៩
0x1810, // Mongolian ᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙
0x1946, // Limbu ᥆᥇᥈᥉᥊᥋᥌᥍᥎᥏
0x19D0, // New Tai Lue ᧐᧑᧒᧓᧔᧕᧖᧗᧘᧙
0x1A80, // Tai Tham Hora ᪀᪁᪂᪃᪄᪅᪆᪇᪈᪉
0x1A90, // Tai Tham Tham ᪐᪑᪒᪓᪔᪕᪖᪗᪘᪙
0x1B50, // Balinese ᭐᭑᭒᭓᭔᭕᭖᭗᭘᭙
0x1BB0, // Sundanese ᮰᮱᮲᮳᮴᮵᮶᮷᮸᮹
0x1C40, // Lepcha ᱀᱁᱂᱃᱄᱅᱆᱇᱈᱉
0x1C50, // Ol Chiki ᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙
// Fullwidth (CJK context)
0xFF10, // Fullwidth 0123456789
// Mathematical digit variants (Unicode math block)
0x1D7CE, // Mathematical Bold
0x1D7D8, // Mathematical Double-Struck
0x1D7E2, // Mathematical Sans-Serif
0x1D7EC, // Mathematical Sans-Serif Bold
0x1D7F6, // Mathematical Monospace
// Other scripts
0x104A0, // Osmanya 𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩
0x10D30, // Hanifi Rohingya 𐴰𐴱𐴲𐴳𐴴𐴵𐴶𐴷𐴸𐴹
0x11066, // Brahmi 𑁦𑁧𑁨𑁩𑁪𑁫𑁬𑁭𑁮𑁯
0x110F0, // Sora Sompeng 𑃰𑃱𑃲𑃳𑃴𑃵𑃶𑃷𑃸𑃹
0x11136, // Chakma 𑄶𑄷𑄸𑄹𑄺𑄻𑄼𑄽𑄾𑄿
0x111D0, // Sharada 𑇐𑇑𑇒𑇓𑇔𑇕𑇖𑇗𑇘𑇙
0x112F0, // Khudawadi 𑋰𑋱𑋲𑋳𑋴𑋵𑋶𑋷𑋸𑋹
0x11450, // Newa 𑑐𑑑𑑒𑑓𑑔𑑕𑑖𑑗𑑘𑑙
0x114D0, // Tirhuta 𑓐𑓑𑓒𑓓𑓔𑓕𑓖𑓗𑓘𑓙
0x11650, // Modi 𑙐𑙑𑙒𑙓𑙔𑙕𑙖𑙗𑙘𑙙
0x116C0, // Takri 𑛀𑛁𑛂𑛃𑛄𑛅𑛆𑛇𑛈𑛉
0x11730, // Ahom 𑜰𑜱𑜲𑜳𑜴𑜵𑜶𑜷𑜸𑜹
0x118E0, // Warang Citi 𑣠𑣡𑣢𑣣𑣤𑣥𑣦𑣧𑣨𑣩
0x11950, // Dives Akuru 𑥐𑥑𑥒𑥓𑥔𑥕𑥖𑥗𑥘𑥙
0x11BF0, // Khitan Small Script 𑯰𑯱𑯲𑯳𑯴𑯵𑯶𑯷𑯸𑯹
0x11C50, // Bhaiksuki 𑱐𑱑𑱒𑱓𑱔𑱕𑱖𑱗𑱘𑱙
0x11D50, // Masaram Gondi 𑵐𑵑𑵒𑵓𑵔𑵕𑵖𑵗𑵘𑵙
0x11DA0, // Gunjala Gondi 𑶠𑶡𑶢𑶣𑶤𑶥𑶦𑶧𑶨𑶩
0x11F50, // Kawi 𑽐𑽑𑽒𑽓𑽔𑽕𑽖𑽗𑽘𑽙
0x16A60, // Mro 𖩠𖩡𖩢𖩣𖩤𖩥𖩦𖩧𖩨𖩩
0x16AC0, // Tangsa 𖫀𖫁𖫂𖫃𖫄𖫅𖫆𖫇𖫈𖫉
0x16B50, // Pahawh Hmong 𖭐𖭑𖭒𖭓𖭔𖭕𖭖𖭗𖭘𖭙
0x1E140, // Nyiakeng Puachue Hmong 𞅀𞅁𞅂𞅃𞅄𞅅𞅆𞅇𞅈𞅉
0x1E2F0, // Wancho 𞋰𞋱𞋲𞋳𞋴𞋵𞋶𞋷𞋸𞋹
0x1E4F0, // Nag Mundari 𞓰𞓱𞓲𞓳𞓴𞓵𞓶𞓷𞓸𞓹
0x1E950, // Adlam 𞥐𞥑𞥒𞥓𞥔𞥕𞥖𞥗𞥘𞥙
0x1FBF0, // Segmented digit symbols 🯰🯱🯲🯳🯴🯵🯶🯷🯸🯹
];
// Build a sparse Map for scripts above 0xFFFF (surrogate-pair range).
// These can't go into a flat Uint8Array indexed by code point efficiently.
const NOT_DIGIT = 0xFF;
const HIGH_MAP = new Map(); // codePoint → digit value (0-9)
const LOW_MAX = 0xFFFF;
const LOW_MIN = 0x0660; // first non-ASCII digit script
// Flat Uint8Array covering 0x0660 .. 0xFFFF
const TABLE_OFFSET = LOW_MIN;
const TABLE_SIZE = LOW_MAX - LOW_MIN + 1;
const TABLE = new Uint8Array(TABLE_SIZE).fill(NOT_DIGIT);
for (const zero of SCRIPT_ZEROS) {
for (let d = 0; d < 10; d++) {
const cp = zero + d;
if (cp <= LOW_MAX) {
TABLE[cp - TABLE_OFFSET] = d;
} else {
HIGH_MAP.set(cp, d);
}
}
}
;// CONCATENATED MODULE: ./node_modules/anynum/anynum.js
const CHAR_0 = 48; // '0'.charCodeAt(0)
const CHAR_9 = 57; // '9'.charCodeAt(0)
const CHAR_MINUS = 45; // '-'.charCodeAt(0)
// Unicode minus/hyphen variants worth normalizing to ASCII '-' in numeric context:
// U+2212 MINUS SIGN − (mathematically correct minus)
// U+FF0D FULLWIDTH HYPHEN-MINUS - (Japanese fullwidth context)
// U+FE63 SMALL HYPHEN-MINUS ﹣ (small form variant)
//
// NOT normalized (deliberate):
// U+2013 EN DASH – (punctuation, not a numeric sign)
// U+2014 EM DASH — (punctuation)
// U+2010 HYPHEN ‐ (typographic hyphen)
//
// Rationale: only characters a human or locale formatter would plausibly use
// as a numeric minus sign are normalized. Dashes used for punctuation are left
// alone to avoid mangling non-numeric strings.
const MINUS_SET = new Set([0x2212, 0xFF0D, 0xFE63]);
/**
* Normalize all Unicode decimal digit characters in a string to ASCII (0-9),
* and normalize Unicode minus variants to ASCII '-' (U+002D).
*
* Non-digit, non-minus characters are passed through unchanged.
*
* Performance design:
* - Fast path: if the string has no convertible characters, return it unchanged
* (zero allocation).
* - BMP digits (0x0660..0xFFFF excl. surrogates): flat Uint8Array lookup (O(1)).
* - Supplementary plane digits (> 0xFFFF, encoded as surrogate pairs): Map lookup.
* - Minus variants: checked inline with a small fixed Set.
*
* @param {string} str
* @returns {string}
*/
function anynum(str) {
if (typeof str !== 'string') return str;
const len = str.length;
if (len === 0) return str;
// Scan for first character needing conversion.
// If none found, return original string (zero allocation).
let firstHit = -1;
for (let i = 0; i < len; i++) {
const cc = str.charCodeAt(i);
// ASCII digit or ASCII minus — already normalized, skip fast
if ((cc >= CHAR_0 && cc <= CHAR_9) || cc === CHAR_MINUS) continue;
// Below first unicode digit script — check minus variants only
if (cc < TABLE_OFFSET) {
if (MINUS_SET.has(cc)) { firstHit = i; break; }
continue;
}
// Surrogate pairs live in BMP range 0xD800-0xDFFF — check before TABLE
if (cc >= 0xD800 && cc <= 0xDBFF) {
if (i + 1 < len) {
const low = str.charCodeAt(i + 1);
if (low >= 0xDC00 && low <= 0xDFFF) {
const cp = 0x10000 + ((cc - 0xD800) << 10) + (low - 0xDC00);
if (HIGH_MAP.has(cp)) { firstHit = i; break; }
}
}
continue;
}
// BMP non-surrogate: flat table lookup; also check minus variants in this range
if (TABLE[cc - TABLE_OFFSET] !== NOT_DIGIT || MINUS_SET.has(cc)) {
firstHit = i;
break;
}
}
// Nothing to replace — return original, zero allocation
if (firstHit === -1) return str;
// Build result: copy unchanged prefix, then convert from firstHit onward
const chars = [];
if (firstHit > 0) chars.push(str.slice(0, firstHit));
for (let i = firstHit; i < len; i++) {
const cc = str.charCodeAt(i);
// ASCII digit or ASCII minus — pass through
if ((cc >= CHAR_0 && cc <= CHAR_9) || cc === CHAR_MINUS) {
chars.push(str[i]);
continue;
}
// Below TABLE_OFFSET — check minus variants, else pass through
if (cc < TABLE_OFFSET) {
chars.push(MINUS_SET.has(cc) ? '-' : str[i]);
continue;
}
// Surrogate pairs
if (cc >= 0xD800 && cc <= 0xDBFF) {
if (i + 1 < len) {
const low = str.charCodeAt(i + 1);
if (low >= 0xDC00 && low <= 0xDFFF) {
const cp = 0x10000 + ((cc - 0xD800) << 10) + (low - 0xDC00);
const d = HIGH_MAP.get(cp);
if (d !== undefined) {
chars.push(String.fromCharCode(d + 48));
i++; // consume low surrogate
continue;
}
}
}
chars.push(str[i]);
continue;
}
// BMP non-surrogate: flat table lookup + minus variants
if (MINUS_SET.has(cc)) {
chars.push('-');
continue;
}
const d = TABLE[cc - TABLE_OFFSET];
chars.push(d !== NOT_DIGIT ? String.fromCharCode(d + 48) : str[i]);
}
return chars.join('');
}
/* harmony default export */ const anynum_anynum = (anynum);
;// CONCATENATED MODULE: ./node_modules/strnum/strnum.js
const hexRegex = /^[-+]?0x[a-fA-F0-9]+$/;
const binRegex = /^0b[01]+$/;
const octRegex = /^0o[0-7]+$/;
const numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/;
const consider = {
hex: true,
binary: false,
octal: false,
leadingZeros: true,
decimalPoint: "\.",
eNotation: true,
//skipLike: /regex/,
infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal))
unicode: false,
};
function toNumber(str, options = {}) {
options = Object.assign({}, consider, options);
if (!str || typeof str !== "string") return str;
let trimmedStr = str.trim();
if (trimmedStr.length === 0) return str;
else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str;
else if (trimmedStr === "0") return 0;
if (options.unicode) {
trimmedStr = anynum_anynum(trimmedStr);
if (trimmedStr === "0") return 0; // re-check after normalization
}
if (options.hex && hexRegex.test(trimmedStr)) {
return parse_int(trimmedStr, 16);
} else if (options.binary && binRegex.test(trimmedStr)) {
return parse_int(trimmedStr, 2);
} else if (options.octal && octRegex.test(trimmedStr)) {
return parse_int(trimmedStr, 8);
} else if (!isFinite(trimmedStr)) { //Infinity
return handleInfinity(str, Number(trimmedStr), options);
} else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation
return resolveEnotation(str, trimmedStr, options);
} else {
//separate negative sign, leading zeros, and rest number
const match = numRegex.exec(trimmedStr);
// +00.123 => [ , '+', '00', '.123', ..
if (match) {
const sign = match[1] || "";
const leadingZeros = match[2];
let numTrimmedByZeros = trimZeros(match[3]); //complete num without leading zeros
const decimalAdjacentToLeadingZeros = sign ? // 0., -00., 000.
str[leadingZeros.length + 1] === "."
: str[leadingZeros.length] === ".";
//trim ending zeros for floating number
if (!options.leadingZeros //leading zeros are not allowed
&& (leadingZeros.length > 1
|| (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))) {
// 00, 00.3, +03.24, 03, 03.24
return str;
}
else {//no leading zeros or leading zeros are allowed
const num = Number(trimmedStr);
const parsedStr = String(num);
if (num === 0) return num;
if (parsedStr.search(/[eE]/) !== -1) { //given number is long and parsed to eNotation
if (options.eNotation) return num;
else return str;
} else if (trimmedStr.indexOf(".") !== -1) { //floating number
if (parsedStr === "0") return num; //0.0
else if (parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000
else if (parsedStr === `${sign}${numTrimmedByZeros}`) return num;
else return str;
}
let n = leadingZeros ? numTrimmedByZeros : trimmedStr;
if (leadingZeros) {
// -009 => -9
return (n === parsedStr) || (sign + n === parsedStr) ? num : str
} else {
// +9
return (n === parsedStr) || (n === sign + parsedStr) ? num : str
}
}
} else { //non-numeric string
return str;
}
}
}
const eNotationRegx = /^([-+])?(0*)(\d*(\.\d*)?[eE][-\+]?\d+)$/;
function resolveEnotation(str, trimmedStr, options) {
if (!options.eNotation) return str;
const notation = trimmedStr.match(eNotationRegx);
if (notation) {
let sign = notation[1] || "";
const eChar = notation[3].indexOf("e") === -1 ? "E" : "e";
const leadingZeros = notation[2];
const eAdjacentToLeadingZeros = sign ? // 0E.
str[leadingZeros.length + 1] === eChar
: str[leadingZeros.length] === eChar;
if (leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str;
else if (leadingZeros.length === 1
&& (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) {
return Number(trimmedStr);
} else if (leadingZeros.length > 0) {
// Has leading zeros — only accept if leadingZeros option allows it
if (options.leadingZeros && !eAdjacentToLeadingZeros) {
trimmedStr = (notation[1] || "") + notation[3];
return Number(trimmedStr);
} else return str;
} else {
// No leading zeros — always valid e-notation, parse it
return Number(trimmedStr);
}
} else {
return str;
}
}
/**
*
* @param {string} numStr without leading zeros
* @returns
*/
function trimZeros(numStr) {
if (numStr && numStr.indexOf(".") !== -1) {//float
//remove ending zeros without the O(n^2) backtracking that /0+$/ hits
//when the string doesn't end in 0 but has a long internal zero-run
let end = numStr.length;
while (end > 0 && numStr.charCodeAt(end - 1) === 48 /* '0' */) end--;
numStr = numStr.slice(0, end);
if (numStr === ".") numStr = "0";
else if (numStr[0] === ".") numStr = "0" + numStr;
else if (numStr[numStr.length - 1] === ".") numStr = numStr.substring(0, numStr.length - 1);
return numStr;
}
return numStr;
}
function parse_int(numStr, base) {
const str = numStr.trim();
if (base === 2 || base === 8) numStr = str.substring(2);
if (parseInt) return parseInt(numStr, base);
else if (Number.parseInt) return Number.parseInt(numStr, base);
else if (window && window.parseInt) return window.parseInt(numStr, base);
else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported");
}
/**
* Handle infinite values based on user option
* @param {string} str - original input string
* @param {number} num - parsed number (Infinity or -Infinity)
* @param {object} options - user options
* @returns {string|number|null} based on infinity option
*/
function handleInfinity(str, num, options) {
const isPositive = num === Infinity;
switch (options.infinity.toLowerCase()) {
case "null":
return null;
case "infinity":
return num; // Return Infinity or -Infinity
case "string":
return isPositive ? "Infinity" : "-Infinity";
case "original":
default:
return str; // Return original string like "1e1000"
}
}
;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/ignoreAttributes.js
function getIgnoreAttributesFn(ignoreAttributes) {
if (typeof ignoreAttributes === 'function') {
return ignoreAttributes
}
if (Array.isArray(ignoreAttributes)) {
return (attrName) => {
for (const pattern of ignoreAttributes) {
if (typeof pattern === 'string' && attrName === pattern) {
return true
}
if (pattern instanceof RegExp && pattern.test(attrName)) {
return true
}
}
}
}
return () => false
}
// EXTERNAL MODULE: ./node_modules/path-expression-matcher/src/Matcher.js
var Matcher = __webpack_require__(8257);
// EXTERNAL MODULE: ./node_modules/path-expression-matcher/src/Expression.js
var Expression = __webpack_require__(3945);
;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/ExpressionSet.js
/**
* ExpressionSet - An indexed collection of Expressions for efficient bulk matching
*
* Instead of iterating all expressions on every tag, ExpressionSet pre-indexes
* them at insertion time by depth and terminal tag name. At match time, only
* the relevant bucket is evaluated — typically reducing checks from O(E) to O(1)
* lookup plus O(small bucket) matches.
*
* Three buckets are maintained:
* - `_byDepthAndTag` — exact depth + exact tag name (tightest, used first)
* - `_wildcardByDepth` — exact depth + wildcard tag `*` (depth-matched only)
* - `_deepWildcards` — expressions containing `..` (cannot be depth-indexed)
*
* @example
* import { Expression, ExpressionSet } from 'fast-xml-tagger';
*
* // Build once at config time
* const stopNodes = new ExpressionSet();
* stopNodes.add(new Expression('root.users.user'));
* stopNodes.add(new Expression('root.config.setting'));
* stopNodes.add(new Expression('..script'));
*
* // Query on every tag — hot path
* if (stopNodes.matchesAny(matcher)) { ... }
*/
class ExpressionSet {
constructor() {
/** @type {Map<string, import('./Expression.js').default[]>} depth:tag → expressions */
this._byDepthAndTag = new Map();
/** @type {Map<number, import('./Expression.js').default[]>} depth → wildcard-tag expressions */
this._wildcardByDepth = new Map();
/** @type {import('./Expression.js').default[]} expressions containing deep wildcard (..) */
this._deepWildcards = [];
/** @type {Map<string, import('./Expression.js').default[]>} terminalTag → deep wildcard expressions */
this._deepByTerminalTag = new Map();
/** @type {Set<string>} pattern strings already added — used for deduplication */
this._patterns = new Set();
/** @type {boolean} whether the set is sealed against further additions */
this._sealed = false;
}
/**
* Add an Expression to the set.
* Duplicate patterns (same pattern string) are silently ignored.
*
* @param {import('./Expression.js').default} expression - A pre-constructed Expression instance
* @returns {this} for chaining
* @throws {TypeError} if called after seal()
*
* @example
* set.add(new Expression('root.users.user'));
* set.add(new Expression('..script'));
*/
add(expression) {
if (this._sealed) {
throw new TypeError(
'ExpressionSet is sealed. Create a new ExpressionSet to add more expressions.'
);
}
// Deduplicate by pattern string
if (this._patterns.has(expression.pattern)) return this;
this._patterns.add(expression.pattern);
if (expression.hasDeepWildcard()) {
const lastSeg = expression.segments[expression.segments.length - 1];
if (lastSeg && lastSeg.type !== 'deep-wildcard' && lastSeg.tag !== '*') {
const tag = lastSeg.tag;
if (!this._deepByTerminalTag.has(tag)) this._deepByTerminalTag.set(tag, []);
this._deepByTerminalTag.get(tag).push(expression);
} else {
this._deepWildcards.push(expression);
}
return this;
}
const depth = expression.length;
const lastSeg = expression.segments[expression.segments.length - 1];
const tag = lastSeg?.tag;
if (!tag || tag === '*') {
// Can index by depth but not by tag
if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []);
this._wildcardByDepth.get(depth).push(expression);
} else {
// Tightest bucket: depth + tag
const key = `${depth}:${tag}`;
if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []);
this._byDepthAndTag.get(key).push(expression);
}
return this;
}
/**
* Add multiple expressions at once.
*
* @param {import('./Expression.js').default[]} expressions - Array of Expression instances
* @returns {this} for chaining
*
* @example
* set.addAll([
* new Expression('root.users.user'),
* new Expression('root.config.setting'),
* ]);
*/
addAll(expressions) {
for (const expr of expressions) this.add(expr);
return this;
}
/**
* Check whether a pattern string is already present in the set.
*
* @param {import('./Expression.js').default} expression
* @returns {boolean}
*/
has(expression) {
return this._patterns.has(expression.pattern);
}
/**
* Number of expressions in the set.
* @type {number}
*/
get size() {
return this._patterns.size;
}
/**
* Seal the set against further modifications.
* Useful to prevent accidental mutations after config is built.
* Calling add() or addAll() on a sealed set throws a TypeError.
*
* @returns {this}
*/
seal() {
this._sealed = true;
return this;
}
/**
* Whether the set has been sealed.
* @type {boolean}
*/
get isSealed() {
return this._sealed;
}
/**
* Test whether the matcher's current path matches any expression in the set.
*
* Evaluation order (cheapest → most expensive):
* 1. Exact depth + tag bucket — O(1) lookup, typically 0–2 expressions
* 2. Depth-only wildcard bucket — O(1) lookup, rare
* 3. Deep-wildcard list — always checked, but usually small
*
* @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
* @returns {boolean} true if any expression matches the current path
*
* @example
* if (stopNodes.matchesAny(matcher)) {
* // handle stop node
* }
*/
matchesAny(matcher) {
return this.findMatch(matcher) !== null;
}
/**
* Find and return the first Expression that matches the matcher's current path.
*
* Uses the same evaluation order as matchesAny (cheapest → most expensive):
* 1. Exact depth + tag bucket
* 2. Depth-only wildcard bucket
* 3. Deep-wildcard list
*
* @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
* @returns {import('./Expression.js').default | null} the first matching Expression, or null
*
* @example
* const expr = stopNodes.findMatch(matcher);
* if (expr) {
* // access expr.config, expr.pattern, etc.
* }
*/
findMatch(matcher) {
const depth = matcher.getDepth();
const tag = matcher.getCurrentTag();
// 1. Tightest bucket — most expressions live here
const exactKey = `${depth}:${tag}`;
const exactBucket = this._byDepthAndTag.get(exactKey);
if (exactBucket) {
for (let i = 0; i < exactBucket.length; i++) {
if (matcher.matches(exactBucket[i])) return exactBucket[i];
}
}
// 2. Depth-matched wildcard-tag expressions
const wildcardBucket = this._wildcardByDepth.get(depth);
if (wildcardBucket) {
for (let i = 0; i < wildcardBucket.length; i++) {
if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i];
}
}
// 3. Deep wildcards — indexed by terminal tag, then unindexed fallback
const deepBucket = this._deepByTerminalTag.get(tag);
if (deepBucket) {
for (let i = 0; i < deepBucket.length; i++) {
if (matcher.matches(deepBucket[i])) return deepBucket[i];
}
}
for (let i = 0; i < this._deepWildcards.length; i++) {
if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i];
}
return null;
}
}
;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/entities.js
// ---------------------------------------------------------------------------
// Complete HTML5 named entity reference
// Organized by logical categories for easy maintenance and selective importing
// ---------------------------------------------------------------------------
/**
* Basic Latin & Special Characters
* @type {Record<string, string>}
*/
const BASIC_LATIN = {
amp: '&',
AMP: '&',
lt: '<',
LT: '<',
gt: '>',
GT: '>',
quot: '"',
QUOT: '"',
apos: "'",
lsquo: '‘',
rsquo: '’',
ldquo: '“',
rdquo: '”',
lsquor: '‚',
rsquor: '’',
ldquor: '„',
bdquo: '„',
comma: ',',
period: '.',
colon: ':',
semi: ';',
excl: '!',
quest: '?',
num: '#',
dollar: '$',
percent: '%',
ast: '*',
commat: '@',
lowbar: '_',
verbar: '|',
vert: '|',
sol: '/',
bsol: '\\',
lbrace: '{',
rbrace: '}',
lbrack: '[',
rbrack: ']',
lpar: '(',
rpar: ')',
nbsp: '\u00a0',
iexcl: '¡',
cent: '¢',
pound: '£',
curren: '¤',
yen: '¥',
brvbar: '¦',
sect: '§',
uml: '¨',
copy: '©',
COPY: '©',
ordf: 'ª',
laquo: '«',
not: '¬',
shy: '\u00ad',
reg: '®',
REG: '®',
macr: '¯',
deg: '°',
plusmn: '±',
sup2: '²',
sup3: '³',
acute: '´',
micro: 'µ',
para: '¶',
middot: '·',
cedil: '¸',
sup1: '¹',
ordm: 'º',
raquo: '»',
frac14: '¼',
frac12: '½',
half: '½',
frac34: '¾',
iquest: '¿',
times: '×',
div: '÷',
divide: '÷',
};
/**
* Latin Extended & Accented Letters (A-Z)
* @type {Record<string, string>}
*/
const LATIN_ACCENTS = {
Agrave: 'À',
agrave: 'à',
Aacute: 'Á',
aacute: 'á',
Acirc: 'Â',
acirc: 'â',
Atilde: 'Ã',
atilde: 'ã',
Auml: 'Ä',
auml: 'ä',
Aring: 'Å',
aring: 'å',
AElig: 'Æ',
aelig: 'æ',
Ccedil: 'Ç',
ccedil: 'ç',
Egrave: 'È',
egrave: 'è',
Eacute: 'É',
eacute: 'é',
Ecirc: 'Ê',
ecirc: 'ê',
Euml: 'Ë',
euml: 'ë',
Igrave: 'Ì',
igrave: 'ì',
Iacute: 'Í',
iacute: 'í',
Icirc: 'Î',
icirc: 'î',
Iuml: 'Ï',
iuml: 'ï',
ETH: 'Ð',
eth: 'ð',
Ntilde: 'Ñ',
ntilde: 'ñ',
Ograve: 'Ò',
ograve: 'ò',
Oacute: 'Ó',
oacute: 'ó',
Ocirc: 'Ô',
ocirc: 'ô',
Otilde: 'Õ',
otilde: 'õ',
Ouml: 'Ö',
ouml: 'ö',
Oslash: 'Ø',
oslash: 'ø',
Ugrave: 'Ù',
ugrave: 'ù',
Uacute: 'Ú',
uacute: 'ú',
Ucirc: 'Û',
ucirc: 'û',
Uuml: 'Ü',
uuml: 'ü',
Yacute: 'Ý',
yacute: 'ý',
THORN: 'Þ',
thorn: 'þ',
szlig: 'ß',
yuml: 'ÿ',
Yuml: 'Ÿ',
};
/**
* Latin Extended (Letters with diacritics)
* @type {Record<string, string>}
*/
const LATIN_EXTENDED = {
Amacr: 'Ā',
amacr: 'ā',
Abreve: 'Ă',
abreve: 'ă',
Aogon: 'Ą',
aogon: 'ą',
Cacute: 'Ć',
cacute: 'ć',
Ccirc: 'Ĉ',
ccirc: 'ĉ',
Cdot: 'Ċ',
cdot: 'ċ',
Ccaron: 'Č',
ccaron: 'č',
Dcaron: 'Ď',
dcaron: 'ď',
Dstrok: 'Đ',
dstrok: 'đ',
Emacr: 'Ē',
emacr: 'ē',
Ecaron: 'Ě',
ecaron: 'ě',
Edot: 'Ė',
edot: 'ė',
Eogon: 'Ę',
eogon: 'ę',
Gcirc: 'Ĝ',
gcirc: 'ĝ',
Gbreve: 'Ğ',
gbreve: 'ğ',
Gdot: 'Ġ',
gdot: 'ġ',
Gcedil: 'Ģ',
Hcirc: 'Ĥ',
hcirc: 'ĥ',
Hstrok: 'Ħ',
hstrok: 'ħ',
Itilde: 'Ĩ',
itilde: 'ĩ',
Imacr: 'Ī',
imacr: 'ī',
Iogon: 'Į',
iogon: 'į',
Idot: 'İ',
IJlig: 'IJ',
ijlig: 'ij',
Jcirc: 'Ĵ',
jcirc: 'ĵ',
Kcedil: 'Ķ',
kcedil: 'ķ',
kgreen: 'ĸ',
Lacute: 'Ĺ',
lacute: 'ĺ',
Lcedil: 'Ļ',
lcedil: 'ļ',
Lcaron: 'Ľ',
lcaron: 'ľ',
Lmidot: 'Ŀ',
lmidot: 'ŀ',
Lstrok: 'Ł',
lstrok: 'ł',
Nacute: 'Ń',
nacute: 'ń',
Ncaron: 'Ň',
ncaron: 'ň',
Ncedil: 'Ņ',
ncedil: 'ņ',
ENG: 'Ŋ',
eng: 'ŋ',
Omacr: 'Ō',
omacr: 'ō',
Odblac: 'Ő',
odblac: 'ő',
OElig: 'Œ',
oelig: 'œ',
Racute: 'Ŕ',
racute: 'ŕ',
Rcaron: 'Ř',
rcaron: 'ř',
Rcedil: 'Ŗ',
rcedil: 'ŗ',
Sacute: 'Ś',
sacute: 'ś',
Scirc: 'Ŝ',
scirc: 'ŝ',
Scedil: 'Ş',
scedil: 'ş',
Scaron: 'Š',
scaron: 'š',
Tcedil: 'Ţ',
tcedil: 'ţ',
Tcaron: 'Ť',
tcaron: 'ť',
Tstrok: 'Ŧ',
tstrok: 'ŧ',
Utilde: 'Ũ',
utilde: 'ũ',
Umacr: 'Ū',
umacr: 'ū',
Ubreve: 'Ŭ',
ubreve: 'ŭ',
Uring: 'Ů',
uring: 'ů',
Udblac: 'Ű',
udblac: 'ű',
Uogon: 'Ų',
uogon: 'ų',
Wcirc: 'Ŵ',
wcirc: 'ŵ',
Ycirc: 'Ŷ',
ycirc: 'ŷ',
Zacute: 'Ź',
zacute: 'ź',
Zdot: 'Ż',
zdot: 'ż',
Zcaron: 'Ž',
zcaron: 'ž',
};
/**
* Greek Letters
* @type {Record<string, string>}
*/
const GREEK = {
Alpha: 'Α',
alpha: 'α',
Beta: 'Β',
beta: 'β',
Gamma: 'Γ',
gamma: 'γ',
Delta: 'Δ',
delta: 'δ',
Epsilon: 'Ε',
epsilon: 'ε',
epsiv: 'ϵ',
varepsilon: 'ϵ',
Zeta: 'Ζ',
zeta: 'ζ',
Eta: 'Η',
eta: 'η',
Theta: 'Θ',
theta: 'θ',
thetasym: 'ϑ',
vartheta: 'ϑ',
Iota: 'Ι',
iota: 'ι',
Kappa: 'Κ',
kappa: 'κ',
kappav: 'ϰ',
varkappa: 'ϰ',
Lambda: 'Λ',
lambda: 'λ',
Mu: 'Μ',
mu: 'μ',
Nu: 'Ν',
nu: 'ν',
Xi: 'Ξ',
xi: 'ξ',
Omicron: 'Ο',
omicron: 'ο',
Pi: 'Π',
pi: 'π',
piv: 'ϖ',
varpi: 'ϖ',
Rho: 'Ρ',
rho: 'ρ',
rhov: 'ϱ',
varrho: 'ϱ',
Sigma: 'Σ',
sigma: 'σ',
sigmaf: 'ς',
sigmav: 'ς',
varsigma: 'ς',
Tau: 'Τ',
tau: 'τ',
Upsilon: 'Υ',
upsilon: 'υ',
upsi: 'υ',
Upsi: 'ϒ',
upsih: 'ϒ',
Phi: 'Φ',
phi: 'φ',
phiv: 'ϕ',
varphi: 'ϕ',
Chi: 'Χ',
chi: 'χ',
Psi: 'Ψ',
psi: 'ψ',
Omega: 'Ω',
omega: 'ω',
ohm: 'Ω',
Gammad: 'Ϝ',
gammad: 'ϝ',
digamma: 'ϝ',
};
/**
* Cyrillic Letters
* @type {Record<string, string>}
*/
const CYRILLIC = {
Afr: '𝔄',
afr: '𝔞',
Acy: 'А',
acy: 'а',
Bcy: 'Б',
bcy: 'б',
Vcy: 'В',
vcy: 'в',
Gcy: 'Г',
gcy: 'г',
Dcy: 'Д',
dcy: 'д',
IEcy: 'Е',
iecy: 'е',
IOcy: 'Ё',
iocy: 'ё',
ZHcy: 'Ж',
zhcy: 'ж',
Zcy: 'З',
zcy: 'з',
Icy: 'И',
icy: 'и',
Jcy: 'Й',
jcy: 'й',
Kcy: 'К',
kcy: 'к',
Lcy: 'Л',
lcy: 'л',
Mcy: 'М',
mcy: 'м',
Ncy: 'Н',
ncy: 'н',
Ocy: 'О',
ocy: 'о',
Pcy: 'П',
pcy: 'п',
Rcy: 'Р',
rcy: 'р',
Scy: 'С',
scy: 'с',
Tcy: 'Т',
tcy: 'т',
Ucy: 'У',
ucy: 'у',
Fcy: 'Ф',
fcy: 'ф',
KHcy: 'Х',
khcy: 'х',
TScy: 'Ц',
tscy: 'ц',
CHcy: 'Ч',
chcy: 'ч',
SHcy: 'Ш',
shcy: 'ш',
SHCHcy: 'Щ',
shchcy: 'щ',
HARDcy: 'Ъ',
hardcy: 'ъ',
Ycy: 'Ы',
ycy: 'ы',
SOFTcy: 'Ь',
softcy: 'ь',
Ecy: 'Э',
ecy: 'э',
YUcy: 'Ю',
yucy: 'ю',
YAcy: 'Я',
yacy: 'я',
DJcy: 'Ђ',
djcy: 'ђ',
GJcy: 'Ѓ',
gjcy: 'ѓ',
Jukcy: 'Є',
jukcy: 'є',
DScy: 'Ѕ',
dscy: 'ѕ',
Iukcy: 'І',
iukcy: 'і',
YIcy: 'Ї',
yicy: 'ї',
Jsercy: 'Ј',
jsercy: 'ј',
LJcy: 'Љ',
ljcy: 'љ',
NJcy: 'Њ',
njcy: 'њ',
TSHcy: 'Ћ',
tshcy: 'ћ',
KJcy: 'Ќ',
kjcy: 'ќ',
Ubrcy: 'Ў',
ubrcy: 'ў',
DZcy: 'Џ',
dzcy: 'џ',
};
/**
* Mathematical Operators & Relations
* @type {Record<string, string>}
*/
const MATH = {
plus: '+',
pm: '±',
times: '×',
div: '÷',
divide: '÷',
sdot: '⋅',
star: '☆',
starf: '★',
bigstar: '★',
lowast: '∗',
ast: '*',
midast: '*',
compfn: '∘',
smallcircle: '∘',
bullet: '•',
bull: '•',
nbsp: '\u00a0',
hellip: '…',
mldr: '…',
prime: '′',
Prime: '″',
tprime: '‴',
bprime: '‵',
backprime: '‵',
minus: '−',
minusd: '∸',
dotminus: '∸',
plusdo: '∔',
dotplus: '∔',
plusmn: '±',
minusplus: '∓',
mnplus: '∓',
mp: '∓',
setminus: '∖',
smallsetminus: '∖',
Backslash: '∖',
setmn: '∖',
ssetmn: '∖',
lowbar: '_',
verbar: '|',
vert: '|',
VerticalLine: '|',
colon: ':',
Colon: '∷',
Proportion: '∷',
ratio: '∶',
equals: '=',
ne: '≠',
nequiv: '≢',
equiv: '≡',
Congruent: '≡',
sim: '∼',
thicksim: '∼',
thksim: '∼',
sime: '≃',
simeq: '≃',
TildeEqual: '≃',
asymp: '≈',
approx: '≈',
thickapprox: '≈',
thkap: '≈',
TildeTilde: '≈',
ncong: '≇',
cong: '≅',
TildeFullEqual: '≅',
asympeq: '≍',
CupCap: '≍',
bump: '≎',
Bumpeq: '≎',
HumpDownHump: '≎',
bumpe: '≏',
bumpeq: '≏',
HumpEqual: '≏',
le: '≤',
LessEqual: '≤',
ge: '≥',
GreaterEqual: '≥',
lesseqgtr: '⋚',
lesseqqgtr: '⪋',
greater: '>',
less: '<',
};
/**
* Mathematical Operators (Advanced)
* @type {Record<string, string>}
*/
const MATH_ADVANCED = {
alefsym: 'ℵ',
aleph: 'ℵ',
beth: 'ℶ',
gimel: 'ℷ',
daleth: 'ℸ',
forall: '∀',
ForAll: '∀',
part: '∂',
PartialD: '∂',
exist: '∃',
Exists: '∃',
nexist: '∄',
nexists: '∄',
empty: '∅',
emptyset: '∅',
emptyv: '∅',
varnothing: '∅',
nabla: '∇',
Del: '∇',
isin: '∈',
isinv: '∈',
in: '∈',
Element: '∈',
notin: '∉',
notinva: '∉',
ni: '∋',
niv: '∋',
SuchThat: '∋',
ReverseElement: '∋',
notni: '∌',
notniva: '∌',
prod: '∏',
Product: '∏',
coprod: '∐',
Coproduct: '∐',
sum: '∑',
Sum: '∑',
minus: '−',
mp: '∓',
plusdo: '∔',
dotplus: '∔',
setminus: '∖',
lowast: '∗',
radic: '√',
Sqrt: '√',
prop: '∝',
propto: '∝',
Proportional: '∝',
varpropto: '∝',
infin: '∞',
infintie: '⧝',
ang: '∠',
angle: '∠',
angmsd: '∡',
measuredangle: '∡',
angsph: '∢',
mid: '∣',
VerticalBar: '∣',
nmid: '∤',
nsmid: '∤',
npar: '∦',
parallel: '∥',
spar: '∥',
nparallel: '∦',
nspar: '∦',
and: '∧',
wedge: '∧',
or: '∨',
vee: '∨',
cap: '∩',
cup: '∪',
int: '∫',
Integral: '∫',
conint: '∮',
ContourIntegral: '∮',
Conint: '∯',
DoubleContourIntegral: '∯',
Cconint: '∰',
there4: '∴',
therefore: '∴',
Therefore: '∴',
becaus: '∵',
because: '∵',
Because: '∵',
ratio: '∶',
Proportion: '∷',
minusd: '∸',
dotminus: '∸',
mDDot: '∺',
homtht: '∻',
sim: '∼',
bsimg: '∽',
backsim: '∽',
ac: '∾',
mstpos: '∾',
acd: '∿',
VerticalTilde: '≀',
wr: '≀',
wreath: '≀',
nsime: '≄',
nsimeq: '≄',
ncong: '≇',
simne: '≆',
ncongdot: '⩭̸',
ngsim: '≵',
nsim: '≁',
napprox: '≉',
nap: '≉',
ngeq: '≱',
nge: '≱',
nleq: '≰',
nle: '≰',
ngtr: '≯',
ngt: '≯',
nless: '≮',
nlt: '≮',
nprec: '⊀',
npr: '⊀',
nsucc: '⊁',
nsc: '⊁',
};
/**
* Arrows
* @type {Record<string, string>}
*/
const ARROWS = {
larr: '←',
leftarrow: '←',
LeftArrow: '←',
uarr: '↑',
uparrow: '↑',
UpArrow: '↑',
rarr: '→',
rightarrow: '→',
RightArrow: '→',
darr: '↓',
downarrow: '↓',
DownArrow: '↓',
harr: '↔',
leftrightarrow: '↔',
LeftRightArrow: '↔',
varr: '↕',
updownarrow: '↕',
UpDownArrow: '↕',
nwarr: '↖',
nwarrow: '↖',
UpperLeftArrow: '↖',
nearr: '↗',
nearrow: '↗',
UpperRightArrow: '↗',
searr: '↘',
searrow: '↘',
LowerRightArrow: '↘',
swarr: '↙',
swarrow: '↙',
LowerLeftArrow: '↙',
lArr: '⇐',
Leftarrow: '⇐',
uArr: '⇑',
Uparrow: '⇑',
rArr: '⇒',
Rightarrow: '⇒',
dArr: '⇓',
Downarrow: '⇓',
hArr: '⇔',
Leftrightarrow: '⇔',
iff: '⇔',
vArr: '⇕',
Updownarrow: '⇕',
lAarr: '⇚',
Lleftarrow: '⇚',
rAarr: '⇛',
Rrightarrow: '⇛',
lrarr: '⇆',
leftrightarrows: '⇆',
rlarr: '⇄',
rightleftarrows: '⇄',
lrhar: '⇋',
leftrightharpoons: '⇋',
ReverseEquilibrium: '⇋',
rlhar: '⇌',
rightleftharpoons: '⇌',
Equilibrium: '⇌',
udarr: '⇅',
UpArrowDownArrow: '⇅',
duarr: '⇵',
DownArrowUpArrow: '⇵',
llarr: '⇇',
leftleftarrows: '⇇',
rrarr: '⇉',
rightrightarrows: '⇉',
ddarr: '⇊',
downdownarrows: '⇊',
har: '↽',
lhard: '↽',
leftharpoondown: '↽',
lharu: '↼',
leftharpoonup: '↼',
rhard: '⇁',
rightharpoondown: '⇁',
rharu: '⇀',
rightharpoonup: '⇀',
lsh: '↰',
Lsh: '↰',
rsh: '↱',
Rsh: '↱',
ldsh: '↲',
rdsh: '↳',
hookleftarrow: '↩',
hookrightarrow: '↪',
mapstoleft: '↤',
mapstoup: '↥',
map: '↦',
mapsto: '↦',
mapstodown: '↧',
crarr: '↵',
nleftarrow: '↚',
nleftrightarrow: '↮',
nrightarrow: '↛',
nrarr: '↛',
larrtl: '↢',
rarrtl: '↣',
leftarrowtail: '↢',
rightarrowtail: '↣',
twoheadleftarrow: '↞',
twoheadrightarrow: '↠',
Larr: '↞',
Rarr: '↠',
larrhk: '↩',
rarrhk: '↪',
larrlp: '↫',
looparrowleft: '↫',
rarrlp: '↬',
looparrowright: '↬',
harrw: '↭',
leftrightsquigarrow: '↭',
nrarrw: '↝̸',
rarrw: '↝',
rightsquigarrow: '↝',
larrbfs: '⤟',
rarrbfs: '⤠',
nvHarr: '⤄',
nvlArr: '⤂',
nvrArr: '⤃',
larrfs: '⤝',
rarrfs: '⤞',
Map: '⤅',
larrsim: '⥳',
rarrsim: '⥴',
harrcir: '⥈',
Uarrocir: '⥉',
lurdshar: '⥊',
ldrdhar: '⥧',
ldrushar: '⥋',
rdldhar: '⥩',
lrhard: '⥭',
uharr: '↾',
uharl: '↿',
dharr: '⇂',
dharl: '⇃',
Uarr: '↟',
Darr: '↡',
zigrarr: '⇝',
nwArr: '⇖',
neArr: '⇗',
seArr: '⇘',
swArr: '⇙',
nharr: '↮',
nhArr: '⇎',
nlarr: '↚',
nlArr: '⇍',
nrArr: '⇏',
larrb: '⇤',
LeftArrowBar: '⇤',
rarrb: '⇥',
RightArrowBar: '⇥',
};
/**
* Geometric Shapes
* @type {Record<string, string>}
*/
const SHAPES = {
square: '□',
Square: '□',
squ: '□',
squf: '▪',
squarf: '▪',
blacksquar: '▪',
blacksquare: '▪',
FilledVerySmallSquare: '▪',
blk34: '▓',
blk12: '▒',
blk14: '░',
block: '█',
srect: '▭',
rect: '▭',
sdot: '⋅',
sdotb: '⊡',
dotsquare: '⊡',
triangle: '▵',
tri: '▵',
trine: '▵',
utri: '▵',
triangledown: '▿',
dtri: '▿',
tridown: '▿',
triangleleft: '◃',
ltri: '◃',
triangleright: '▹',
rtri: '▹',
blacktriangle: '▴',
utrif: '▴',
blacktriangledown: '▾',
dtrif: '▾',
blacktriangleleft: '◂',
ltrif: '◂',
blacktriangleright: '▸',
rtrif: '▸',
loz: '◊',
lozenge: '◊',
blacklozenge: '⧫',
lozf: '⧫',
bigcirc: '◯',
xcirc: '◯',
circ: 'ˆ',
Circle: '○',
cir: '○',
o: '○',
bullet: '•',
bull: '•',
hellip: '…',
mldr: '…',
nldr: '‥',
boxh: '─',
HorizontalLine: '─',
boxv: '│',
boxdr: '┌',
boxdl: '┐',
boxur: '└',
boxul: '┘',
boxvr: '├',
boxvl: '┤',
boxhd: '┬',
boxhu: '┴',
boxvh: '┼',
boxH: '═',
boxV: '║',
boxdR: '╒',
boxDr: '╓',
boxDR: '╔',
boxDl: '╕',
boxdL: '╖',
boxDL: '╗',
boxuR: '╘',
boxUr: '╙',
boxUR: '╚',
boxUl: '╜',
boxuL: '╛',
boxUL: '╝',
boxvR: '╞',
boxVr: '╟',
boxVR: '╠',
boxVl: '╢',
boxvL: '╡',
boxVL: '╣',
boxHd: '╤',
boxhD: '╥',
boxHD: '╦',
boxHu: '╧',
boxhU: '╨',
boxHU: '╩',
boxvH: '╪',
boxVh: '╫',
boxVH: '╬',
};
/**
* Punctuation & Diacritics
* @type {Record<string, string>}
*/
const PUNCTUATION = {
excl: '!',
iexcl: '¡',
brvbar: '¦',
sect: '§',
uml: '¨',
copy: '©',
ordf: 'ª',
laquo: '«',
not: '¬',
shy: '\u00ad',
reg: '®',
macr: '¯',
deg: '°',
plusmn: '±',
sup2: '²',
sup3: '³',
acute: '´',
micro: 'µ',
para: '¶',
middot: '·',
cedil: '¸',
sup1: '¹',
ordm: 'º',
raquo: '»',
frac14: '¼',
frac12: '½',
frac34: '¾',
iquest: '¿',
nbsp: '\u00a0',
comma: ',',
period: '.',
colon: ':',
semi: ';',
vert: '|',
Verbar: '‖',
verbar: '|',
dblac: '˝',
circ: 'ˆ',
caron: 'ˇ',
breve: '˘',
dot: '˙',
ring: '˚',
ogon: '˛',
tilde: '˜',
DiacriticalGrave: '`',
DiacriticalAcute: '´',
DiacriticalTilde: '˜',
DiacriticalDot: '˙',
DiacriticalDoubleAcute: '˝',
grave: '`',
};
/**
* Currency Symbols
* @type {Record<string, string>}
*/
const CURRENCY = {
cent: '¢',
pound: '£',
curren: '¤',
yen: '¥',
euro: '€',
dollar: '$',
fnof: 'ƒ',
inr: '₹',
af: '؋',
birr: 'ብር',
peso: '₱',
rub: '₽',
won: '₩',
yuan: '¥',
cedil: '¸',
};
/**
* Fractions
* @type {Record<string, string>}
*/
const FRACTIONS = {
frac12: '½',
half: '½',
frac13: '⅓',
frac14: '¼',
frac15: '⅕',
frac16: '⅙',
frac18: '⅛',
frac23: '⅔',
frac25: '⅖',
frac34: '¾',
frac35: '⅗',
frac38: '⅜',
frac45: '⅘',
frac56: '⅚',
frac58: '⅝',
frac78: '⅞',
frasl: '⁄',
};
/**
* Miscellaneous Symbols
* @type {Record<string, string>}
*/
const MISC_SYMBOLS = {
trade: '™',
TRADE: '™',
telrec: '⌕',
target: '⌖',
ulcorn: '⌜',
ulcorner: '⌜',
urcorn: '⌝',
urcorner: '⌝',
dlcorn: '⌞',
llcorner: '⌞',
drcorn: '⌟',
lrcorner: '⌟',
intercal: '⊺',
intcal: '⊺',
oplus: '⊕',
CirclePlus: '⊕',
ominus: '⊖',
CircleMinus: '⊖',
otimes: '⊗',
CircleTimes: '⊗',
osol: '⊘',
odot: '⊙',
CircleDot: '⊙',
oast: '⊛',
circledast: '⊛',
odash: '⊝',
circleddash: '⊝',
ocirc: '⊚',
circledcirc: '⊚',
boxplus: '⊞',
plusb: '⊞',
boxminus: '⊟',
minusb: '⊟',
boxtimes: '⊠',
timesb: '⊠',
boxdot: '⊡',
sdotb: '⊡',
veebar: '⊻',
vee: '∨',
barvee: '⊽',
and: '∧',
wedge: '∧',
Cap: '⋒',
Cup: '⋓',
Fork: '⋔',
pitchfork: '⋔',
epar: '⋕',
ltlarr: '⥶',
nvap: '≍⃒',
nvsim: '∼⃒',
nvge: '≥⃒',
nvle: '≤⃒',
nvlt: '<⃒',
nvgt: '>⃒',
nvltrie: '⊴⃒',
nvrtrie: '⊵⃒',
Vdash: '⊩',
dashv: '⊣',
vDash: '⊨',
Vvdash: '⊪',
nvdash: '⊬',
nvDash: '⊭',
nVdash: '⊮',
nVDash: '⊯',
};
const XML = {
amp: "&",
apos: "'",
gt: ">",
lt: "<",
quot: "\""
}
const COMMON_HTML = {
nbsp: '\u00a0',
copy: '\u00a9',
reg: '\u00ae',
trade: '\u2122',
mdash: '\u2014',
ndash: '\u2013',
hellip: '\u2026',
laquo: '\u00ab',
raquo: '\u00bb',
lsquo: '\u2018',
rsquo: '\u2019',
ldquo: '\u201c',
rdquo: '\u201d',
bull: '\u2022',
para: '\u00b6',
sect: '\u00a7',
deg: '\u00b0',
frac12: '\u00bd',
frac14: '\u00bc',
frac34: '\u00be',
}
// ---------------------------------------------------------------------------
// Note: NUMERIC_ENTITIES (&#NNN; / &#xHH;) are handled by the scanner directly
// via String.fromCodePoint() without any map lookup.
// ---------------------------------------------------------------------------
;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/EntityDecoder.js
// ---------------------------------------------------------------------------
// Built-in named entity map (name → replacement string)
// No regex, no {regex,val} objects — just flat key/value pairs.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Entity hook action constants
// ---------------------------------------------------------------------------
/**
* Action constants for `onExternalEntity` and `onInputEntity` hooks.
*
* Use these instead of raw strings to avoid typos:
*
* @example
* import EntityDecoder, { ENTITY_ACTION } from './EntityDecoder.js';
* const dec = new EntityDecoder({
* onInputEntity: (name, value) => ENTITY_ACTION.BLOCK,
* });
*/
const ENTITY_ACTION = Object.freeze({
/** Resolve and expand the entity normally. */
ALLOW: 'allow',
/** Silently skip this entity — it will not be registered. */
BLOCK: 'block',
/** Throw an error, aborting entity registration entirely. */
THROW: 'throw',
});
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const SPECIAL_CHARS = new Set('!?\\\\/[]$%{}^&*()<>|+');
/**
* Validate that an entity name contains no dangerous characters.
* @param {string} name
* @returns {string} the name, unchanged
* @throws {Error} on invalid characters
*/
function EntityDecoder_validateEntityName(name) {
if (name[0] === '#') {
throw new Error(`[EntityReplacer] Invalid character '#' in entity name: "${name}"`);
}
for (const ch of name) {
if (SPECIAL_CHARS.has(ch)) {
throw new Error(`[EntityReplacer] Invalid character '${ch}' in entity name: "${name}"`);
}
}
return name;
}
/**
* Merge one or more entity maps into a flat name→string map.
* Accepts either:
* - plain string values: { amp: '&' }
* - legacy {regex,val} / {regx,val}: { lt: { regex: /.../, val: '<' } }
*
* Values containing '&' are skipped (recursive expansion risk).
*
* @param {...object} maps
* @returns {Record<string, string>}
*/
function mergeEntityMaps(...maps) {
const out = Object.create(null);
for (const map of maps) {
if (!map) continue;
for (const key of Object.keys(map)) {
const raw = map[key];
if (typeof raw === 'string') {
out[key] = raw;
} else if (raw && typeof raw === 'object' && raw.val !== undefined) {
// Legacy {regex,val} or {regx,val} — extract the string val only
const val = raw.val;
if (typeof val === 'string') {
out[key] = val;
}
// function vals are not supported in the scanner — skip
}
}
}
return out;
}
// ---------------------------------------------------------------------------
// applyLimitsTo helpers
// ---------------------------------------------------------------------------
const LIMIT_TIER_EXTERNAL = 'external'; // input/runtime + persistent external maps
const LIMIT_TIER_BASE = 'base'; // DEFAULT_XML_ENTITIES + namedEntities (system) maps
const LIMIT_TIER_ALL = 'all'; // every entity regardless of tier
/**
* Resolve `applyLimitsTo` option into a normalised Set of tier strings.
* Accepted values: 'external' | 'base' | 'all' | string[]
* Default: 'external' (only untrusted injected entities are counted).
* @param {string|string[]|undefined} raw
* @returns {Set<string>}
*/
function parseLimitTiers(raw) {
if (!raw || raw === LIMIT_TIER_EXTERNAL) return new Set([LIMIT_TIER_EXTERNAL]);
if (raw === LIMIT_TIER_ALL) return new Set([LIMIT_TIER_ALL]);
if (raw === LIMIT_TIER_BASE) return new Set([LIMIT_TIER_BASE]);
if (Array.isArray(raw)) return new Set(raw);
return new Set([LIMIT_TIER_EXTERNAL]); // safe default for unrecognised values
}
// ---------------------------------------------------------------------------
// NCR (Numeric Character Reference) classification
// ---------------------------------------------------------------------------
// Severity order — higher number = stricter action.
// Used to enforce minimum action levels for specific codepoint ranges.
const NCR_LEVEL = Object.freeze({ allow: 0, leave: 1, remove: 2, throw: 3 });
// XML 1.0 §2.2: allowed chars are #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
// Restricted C0: U+0001–U+001F excluding U+0009, U+000A, U+000D
const XML10_ALLOWED_C0 = new Set([0x09, 0x0A, 0x0D]);
/**
* Parse the `ncr` constructor option into flat, hot-path-friendly fields.
* @param {object|undefined} ncr
* @returns {{ xmlVersion: number, onLevel: number, nullLevel: number }}
*/
function parseNCRConfig(ncr) {
if (!ncr) {
return { xmlVersion: 1.0, onLevel: NCR_LEVEL.allow, nullLevel: NCR_LEVEL.remove };
}
const xmlVersion = ncr.xmlVersion === 1.1 ? 1.1 : 1.0;
const onLevel = NCR_LEVEL[ncr.onNCR] ?? NCR_LEVEL.allow;
const nullLevel = NCR_LEVEL[ncr.nullNCR] ?? NCR_LEVEL.remove;
// 'allow' is not meaningful for null — clamp to at least 'remove'
const clampedNull = Math.max(nullLevel, NCR_LEVEL.remove);
return { xmlVersion, onLevel, nullLevel: clampedNull };
}
// ---------------------------------------------------------------------------
// EntityReplacer
// ---------------------------------------------------------------------------
/**
* Single-pass, zero-regex entity replacer for XML/HTML content.
*
* Algorithm: scan the string once for '&', read to ';', resolve via map
* or direct codepoint conversion, build output chunks, join once at the end.
*
* Entity lookup priority (highest → lowest):
* 1. input / runtime (DOCTYPE entities for current document)
* 2. persistent external (survive across documents)
* 3. base named map (DEFAULT_XML_ENTITIES + user-supplied namedEntities)
*
* Both input and external resolve as the 'external' tier for limit purposes.
* Base map entities resolve as the 'base' tier.
*
* Numeric / hex references (&#NNN; / &#xHH;) are resolved directly via
* String.fromCodePoint() — no map needed. They count as 'base' tier.
*
* @example
* const replacer = new EntityReplacer({ namedEntities: COMMON_HTML });
* replacer.setExternalEntities({ brand: 'Acme' });
*
* const instance = replacer.reset();
* instance.addInputEntities({ version: '1.0' });
* instance.encode('&brand; v&version; &lt;'); // 'Acme v1.0 <'
*/
class EntityDecoder {
/**
* @param {object} [options]
* @param {object|null} [options.namedEntities] — extra named entities merged into base map
* @param {object} [options.limit] — security limits
* @param {number} [options.limit.maxTotalExpansions=0] — 0 = unlimited
* @param {number} [options.limit.maxExpandedLength=0] — 0 = unlimited
* @param {'external'|'base'|'all'|string[]} [options.limit.applyLimitsTo='external']
* Which entity tiers count against the security limits:
* - 'external' (default) — only input/runtime + persistent external entities
* - 'base' — only DEFAULT_XML_ENTITIES + namedEntities
* - 'all' — every entity regardless of tier
* - string[] — explicit combination, e.g. ['external', 'base']
* @param {((resolved: string, original: string) => string)|null} [options.postCheck=null]
* @param {string[]} [options.remove=[]] — entity names (e.g. ['nbsp', '#13']) to delete (replace with empty string)
* @param {string[]} [options.leave=[]] — entity names to keep as literal (unchanged in output)
* @param {object} [options.ncr] — Numeric Character Reference controls
* @param {1.0|1.1} [options.ncr.xmlVersion=1.0]
* XML version governing which codepoint ranges are restricted:
* - 1.0 — C0 controls U+0001–U+001F (except U+0009/000A/000D) are prohibited
* - 1.1 — C0 controls are allowed when written as NCRs; C1 (U+007F–U+009F) decoded as-is
* @param {'allow'|'leave'|'remove'|'throw'} [options.ncr.onNCR='allow']
* Base action for numeric references. Severity order: allow < leave < remove < throw.
* For codepoint ranges that carry a minimum level (surrogates → remove, XML 1.0 C0 → remove),
* the effective action is max(onNCR, rangeMinimum).
* @param {'remove'|'throw'} [options.ncr.nullNCR='remove']
* Action for U+0000 (null). 'allow' and 'leave' are clamped to 'remove' since null is never safe.
* @param {((name: string, value: string) => 'allow'|'block'|'throw')|null} [options.onExternalEntity=null]
* Hook called when an external entity is registered via `setExternalEntities()` or
* `addExternalEntity()`. Return `ENTITY_ACTION.ALLOW` to accept the entity,
* `ENTITY_ACTION.BLOCK` to silently skip it, or `ENTITY_ACTION.THROW` to abort with an error.
* @param {((name: string, value: string) => 'allow'|'block'|'throw')|null} [options.onInputEntity=null]
* Hook called when an input entity is registered via `addInputEntities()`. Return
* `ENTITY_ACTION.ALLOW` to accept, `ENTITY_ACTION.BLOCK` to silently skip, or
* `ENTITY_ACTION.THROW` to abort with an error.
*/
constructor(options = {}) {
this._limit = options.limit || {};
this._maxTotalExpansions = this._limit.maxTotalExpansions || 0;
this._maxExpandedLength = this._limit.maxExpandedLength || 0;
this._postCheck = typeof options.postCheck === 'function' ? options.postCheck : r => r;
this._limitTiers = parseLimitTiers(this._limit.applyLimitsTo ?? LIMIT_TIER_EXTERNAL);
this._numericAllowed = options.numericAllowed ?? true;
// Base map: DEFAULT_XML_ENTITIES + user-supplied extras. Immutable after construction.
this._baseMap = mergeEntityMaps(XML, options.namedEntities || null);
// Persistent external entities — survive across documents.
// Stored as a separate map so reset() never touches them.
/** @type {Record<string, string>} */
this._externalMap = Object.create(null);
// Input / runtime entities — current document only, wiped on reset().
/** @type {Record<string, string>} */
this._inputMap = Object.create(null);
// Per-document counters
this._totalExpansions = 0;
this._expandedLength = 0;
// --- New: remove / leave sets ---
/** @type {Set<string>} */
this._removeSet = new Set(options.remove && Array.isArray(options.remove) ? options.remove : []);
/** @type {Set<string>} */
this._leaveSet = new Set(options.leave && Array.isArray(options.leave) ? options.leave : []);
// --- NCR config (parsed into flat fields for hot-path speed) ---
const ncrCfg = parseNCRConfig(options.ncr);
this._ncrXmlVersion = ncrCfg.xmlVersion;
this._ncrOnLevel = ncrCfg.onLevel;
this._ncrNullLevel = ncrCfg.nullLevel;
// --- Registration hooks ---
/** @type {((name: string, value: string) => 'allow'|'block'|'throw')|null} */
this._onExternalEntity = typeof options.onExternalEntity === 'function'
? options.onExternalEntity
: null;
/** @type {((name: string, value: string) => 'allow'|'block'|'throw')|null} */
this._onInputEntity = typeof options.onInputEntity === 'function'
? options.onInputEntity
: null;
}
// -------------------------------------------------------------------------
// Private: registration hook dispatch
// -------------------------------------------------------------------------
/**
* Invoke a registration hook for a single entity name/value pair.
* Returns true when the entity should be accepted, false when it should be
* silently skipped (BLOCK), and throws when the hook returns THROW.
*
* @param {((name: string, value: string) => 'allow'|'block'|'throw')|null} hook
* @param {string} name
* @param {string} value
* @param {string} context — used in error messages ('external' | 'input')
* @returns {boolean} true = accept, false = skip
*/
_applyRegistrationHook(hook, name, value, context) {
if (!hook) return true; // no hook → always accept
const action = hook(name, value);
if (action === ENTITY_ACTION.BLOCK) return false;
if (action === ENTITY_ACTION.THROW) {
throw new Error(
`[EntityDecoder] Registration of ${context} entity "&${name};" was rejected by hook`
);
}
return true; // ALLOW or any unknown return value → accept
}
// -------------------------------------------------------------------------
// Persistent external entity registration
// -------------------------------------------------------------------------
/**
* Replace the full set of persistent external entities.
* All keys are validated — throws on invalid characters.
* If `onExternalEntity` is set, it is called once per entry; entries that
* return `ENTITY_ACTION.BLOCK` are silently omitted, `ENTITY_ACTION.THROW`
* aborts the whole call.
* @param {Record<string, string | { regex?: RegExp, val: string }>} map
*/
setExternalEntities(map) {
if (map) {
for (const key of Object.keys(map)) {
EntityDecoder_validateEntityName(key);
}
}
if (!this._onExternalEntity) {
this._externalMap = mergeEntityMaps(map);
return;
}
// Hook present — resolve values first, then filter
const flat = mergeEntityMaps(map);
const filtered = Object.create(null);
for (const [name, value] of Object.entries(flat)) {
if (this._applyRegistrationHook(this._onExternalEntity, name, value, 'external')) {
filtered[name] = value;
}
}
this._externalMap = filtered;
}
/**
* Add a single persistent external entity.
* If `onExternalEntity` is set it is called before the entity is stored;
* `ENTITY_ACTION.BLOCK` silently skips storage, `ENTITY_ACTION.THROW` raises.
* @param {string} key
* @param {string} value
*/
addExternalEntity(key, value) {
EntityDecoder_validateEntityName(key);
if (typeof value === 'string' && value.indexOf('&') === -1) {
if (this._applyRegistrationHook(this._onExternalEntity, key, value, 'external')) {
this._externalMap[key] = value;
}
}
}
// -------------------------------------------------------------------------
// Input / runtime entity registration (per document)
// -------------------------------------------------------------------------
/**
* Inject DOCTYPE entities for the current document.
* Also resets per-document expansion counters.
* If `onInputEntity` is set it is called once per entry; entries returning
* `ENTITY_ACTION.BLOCK` are silently omitted, `ENTITY_ACTION.THROW` aborts.
* @param {Record<string, string | { regx?: RegExp, regex?: RegExp, val: string }>} map
*/
addInputEntities(map) {
this._totalExpansions = 0;
this._expandedLength = 0;
if (!this._onInputEntity) {
this._inputMap = mergeEntityMaps(map);
return;
}
const flat = mergeEntityMaps(map);
const filtered = Object.create(null);
for (const [name, value] of Object.entries(flat)) {
if (this._applyRegistrationHook(this._onInputEntity, name, value, 'input')) {
filtered[name] = value;
}
}
this._inputMap = filtered;
}
// -------------------------------------------------------------------------
// Per-document reset
// -------------------------------------------------------------------------
/**
* Wipe input/runtime entities and reset counters.
* Call this before processing each new document.
* @returns {this}
*/
reset() {
this._inputMap = Object.create(null);
this._totalExpansions = 0;
this._expandedLength = 0;
return this;
}
// -------------------------------------------------------------------------
// XML version (can be set after construction, e.g. once parser reads <?xml?>)
// -------------------------------------------------------------------------
/**
* Update the XML version used for NCR classification.
* Call this as soon as the document's `<?xml version="...">` declaration is parsed.
* @param {1.0|1.1|number} version
*/
setXmlVersion(version) {
this._ncrXmlVersion = version === 1.1 ? 1.1 : 1.0;
}
// -------------------------------------------------------------------------
// Primary API
// -------------------------------------------------------------------------
/**
* Replace all entity references in `str` in a single pass.
*
* @param {string} str
* @returns {string}
*/
decode(str) {
if (typeof str !== 'string' || str.length === 0) return str;
//TODO: check if needed
if (str.indexOf('&') === -1) return str; // fast path — no entities at all
const original = str;
const chunks = [];
const len = str.length;
let last = 0; // start of next unprocessed literal chunk
let i = 0;
const limitExpansions = this._maxTotalExpansions > 0;
const limitLength = this._maxExpandedLength > 0;
const checkLimits = limitExpansions || limitLength;
while (i < len) {
// Scan forward to next '&'
if (str.charCodeAt(i) !== 38 /* '&' */) { i++; continue; }
// --- Found '&' at position i ---
// Scan forward to ';'
let j = i + 1;
while (j < len && str.charCodeAt(j) !== 59 /* ';' */ && (j - i) <= 32) j++;
if (j >= len || str.charCodeAt(j) !== 59) {
// No closing ';' within window — treat '&' as literal
i++;
continue;
}
// Raw token between '&' and ';' (exclusive)
const token = str.slice(i + 1, j);
if (token.length === 0) { i++; continue; }
let replacement;
let tier; // which limit tier this entity belongs to
if (this._removeSet.has(token)) {
// Remove entity: replace with empty string
replacement = '';
// If entity was unknown (replacement undefined), we still need a tier for limits.
// Treat as external tier because it's user-directed removal of an unknown reference.
if (tier === undefined) {
tier = LIMIT_TIER_EXTERNAL;
}
} else if (this._leaveSet.has(token)) {
// Do not replace — keep original &token; as literal
i++;
continue;
} else if (token.charCodeAt(0) === 35 /* '#' */) {
// ---- Numeric / NCR reference ----
// NCR classification always runs first — prohibited codepoints must be
// caught regardless of numericAllowed.
const ncrResult = this._resolveNCR(token);
if (ncrResult === undefined) {
// 'leave' action — keep original &token; as-is
i++;
continue;
}
replacement = ncrResult; // '' for remove, char string for allow
tier = LIMIT_TIER_BASE;
} else {
// ---- Named reference ----
const resolved = this._resolveName(token);
replacement = resolved?.value;
tier = resolved?.tier;
}
if (replacement === undefined) {
// Unknown entity — leave as-is, advance past '&' only
i++;
continue;
}
// Flush literal chunk before this entity
if (i > last) chunks.push(str.slice(last, i));
chunks.push(replacement);
last = j + 1; // skip past ';'
i = last;
// Apply expansion limits only if this tier is being tracked
if (checkLimits && this._tierCounts(tier)) {
if (limitExpansions) {
this._totalExpansions++;
if (this._totalExpansions > this._maxTotalExpansions) {
throw new Error(
`[EntityReplacer] Entity expansion count limit exceeded: ` +
`${this._totalExpansions} > ${this._maxTotalExpansions}`
);
}
}
if (limitLength) {
// delta: replacement.length minus the raw &token; length (token.length + 2 for '&' and ';')
const delta = replacement.length - (token.length + 2);
if (delta > 0) {
this._expandedLength += delta;
if (this._expandedLength > this._maxExpandedLength) {
throw new Error(
`[EntityReplacer] Expanded content length limit exceeded: ` +
`${this._expandedLength} > ${this._maxExpandedLength}`
);
}
}
}
}
}
// Flush trailing literal
if (last < len) chunks.push(str.slice(last));
// If nothing was replaced, chunks is empty — return original
const result = chunks.length === 0 ? str : chunks.join('');
return this._postCheck(result, original);
}
// -------------------------------------------------------------------------
// Private: limit tier check
// -------------------------------------------------------------------------
/**
* Returns true if a resolved entity of the given tier should count
* against the expansion/length limits.
* @param {string} tier — LIMIT_TIER_EXTERNAL | LIMIT_TIER_BASE
* @returns {boolean}
*/
_tierCounts(tier) {
if (this._limitTiers.has(LIMIT_TIER_ALL)) return true;
return this._limitTiers.has(tier);
}
// -------------------------------------------------------------------------
// Private: entity resolution
// -------------------------------------------------------------------------
/**
* Resolve a named entity token (without & and ;).
* Priority: inputMap > externalMap > baseMap
* Returns the resolved value tagged with its limit tier.
*
* @param {string} name
* @returns {{ value: string, tier: string }|undefined}
*/
_resolveName(name) {
// input and external both count as 'external' tier for limit purposes —
// they are injected at runtime and are the untrusted surface.
if (name in this._inputMap) return { value: this._inputMap[name], tier: LIMIT_TIER_EXTERNAL };
if (name in this._externalMap) return { value: this._externalMap[name], tier: LIMIT_TIER_EXTERNAL };
if (name in this._baseMap) return { value: this._baseMap[name], tier: LIMIT_TIER_BASE };
return undefined;
}
/**
* Classify a codepoint and return the minimum action level that must be applied.
* Returns -1 when no minimum is imposed (normal allow path).
*
* Ranges checked (in priority order):
* 1. U+0000 — null, governed by nullNCR (always ≥ remove)
* 2. U+D800–U+DFFF — surrogates, always prohibited (min: remove)
* 3. U+0001–U+001F \ {0x09,0x0A,0x0D} — XML 1.0 restricted C0 (min: remove)
* (skipped in XML 1.1 — C0 controls are allowed when written as NCRs)
*
* @param {number} cp — codepoint
* @returns {number} — minimum NCR_LEVEL value, or -1 for no restriction
*/
_classifyNCR(cp) {
// 1. Null
if (cp === 0) return this._ncrNullLevel;
// 2. Surrogates — always prohibited, minimum 'remove'
if (cp >= 0xD800 && cp <= 0xDFFF) return NCR_LEVEL.remove;
// 3. XML 1.0 restricted C0 controls
if (this._ncrXmlVersion === 1.0) {
if (cp >= 0x01 && cp <= 0x1F && !XML10_ALLOWED_C0.has(cp)) return NCR_LEVEL.remove;
}
return -1; // no restriction
}
/**
* Execute a resolved NCR action.
*
* @param {number} action — NCR_LEVEL value
* @param {string} token — raw token (e.g. '#38') for error messages
* @param {number} cp — codepoint, used only for error messages
* @returns {string|undefined}
* - decoded character string → 'allow'
* - '' → 'remove'
* - undefined → 'leave' (caller must skip past '&' only)
* - throws Error → 'throw'
*/
_applyNCRAction(action, token, cp) {
switch (action) {
case NCR_LEVEL.allow: return String.fromCodePoint(cp);
case NCR_LEVEL.remove: return '';
case NCR_LEVEL.leave: return undefined; // signal: keep literal
case NCR_LEVEL.throw:
throw new Error(
`[EntityDecoder] Prohibited numeric character reference ` +
`&${token}; (U+${cp.toString(16).toUpperCase().padStart(4, '0')})`
);
default: return String.fromCodePoint(cp);
}
}
/**
* Full NCR resolution pipeline for a numeric token.
*
* Steps:
* 1. Parse the codepoint (decimal or hex).
* 2. Validate the raw codepoint range (NaN, <0, >0x10FFFF).
* 3. If numericAllowed is false and no minimum restriction applies → leave as-is.
* 4. Classify the codepoint to find the minimum required action level.
* 5. Resolve effective action = max(onNCR, minimum).
* 6. Apply and return.
*
* @param {string} token — e.g. '#38', '#x26', '#X26'
* @returns {string|undefined}
* - string (incl. '') — replacement ('' = remove)
* - undefined — leave original &token; as-is
*/
_resolveNCR(token) {
// Step 1: parse codepoint
const second = token.charCodeAt(1);
let cp;
if (second === 120 /* x */ || second === 88 /* X */) {
cp = parseInt(token.slice(2), 16);
} else {
cp = parseInt(token.slice(1), 10);
}
// Step 2: out-of-range → leave as-is unconditionally
if (Number.isNaN(cp) || cp < 0 || cp > 0x10FFFF) return undefined;
// Step 3: classify to get minimum action level
const minimum = this._classifyNCR(cp);
// Step 4: if numericAllowed is false and no hard minimum → leave
if (!this._numericAllowed && minimum < NCR_LEVEL.remove) return undefined;
// Step 5: effective action = max(configured onNCR, range minimum)
const effective = minimum === -1
? this._ncrOnLevel
: Math.max(this._ncrOnLevel, minimum);
// Step 6: apply
return this._applyNCRAction(effective, token, cp);
}
}
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql.js
/**
* SQL context patterns — high-precision rules only.
*
* These rules have very low false-positive risk and are safe to apply to
* general user text (names, descriptions, search queries, etc.).
* All patterns are ReDoS-safe — unlike the `sql-injection` npm package
* which has an active CVE on its own detection regexes.
*
* For exhaustive coverage including noisier heuristics (comment sequences,
* hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead.
* Apply 'SQL-STRICT' only to strings that are specifically SQL fragments,
* not to general free-text fields.
*/
const SQL_PATTERNS = [
{
id: 'sql-block-comment-open',
description: 'SQL block comment open: /* ... */ — unusual in legitimate user text',
pattern: /\/\*/,
},
{
id: 'sql-union-select',
description: 'UNION SELECT — most common SQL injection aggregation attack',
pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i,
},
{
id: 'sql-drop-table',
description: 'DROP TABLE — destructive DDL injection',
pattern: /\bDROP\s{1,20}TABLE\b/i,
},
{
id: 'sql-drop-database',
description: 'DROP DATABASE — destructive DDL injection',
pattern: /\bDROP\s{1,20}DATABASE\b/i,
},
{
id: 'sql-insert-into',
description: 'INSERT INTO — data injection',
pattern: /\bINSERT\s{1,20}INTO\b/i,
},
{
id: 'sql-delete-from',
description: 'DELETE FROM — data deletion injection',
pattern: /\bDELETE\s{1,20}FROM\b/i,
},
{
id: 'sql-update-set',
description: 'UPDATE ... SET — data modification injection',
// Allows arbitrary content between UPDATE and SET (table name, alias, etc.)
pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i,
},
{
id: 'sql-exec-xp',
description: 'EXEC xp_ — MSSQL extended stored procedure execution',
pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i,
},
{
id: 'sql-tautology-string',
description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"",
// Last quote is optional — injection may truncate it: ' OR '1'='1--
pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i,
},
{
id: 'sql-tautology-numeric',
description: 'Numeric tautology: OR 1=1',
pattern: /\bOR\s{1,10}1\s*=\s*1\b/i,
},
{
id: 'sql-always-true-zero',
description: 'Numeric tautology: OR 0=0',
pattern: /\bOR\s{1,10}0\s*=\s*0\b/i,
},
{
id: 'sql-sleep-benchmark',
description: 'Time-based blind injection: SLEEP() or BENCHMARK()',
pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i,
},
{
id: 'sql-waitfor-delay',
description: 'MSSQL time-based blind injection: WAITFOR DELAY',
pattern: /\bWAITFOR\s{1,20}DELAY\b/i,
},
{
id: 'sql-char-function',
description: 'CHAR() function — used to obfuscate injected strings',
pattern: /\bCHAR\s*\(\s*\d{1,3}/i,
},
{
id: 'sql-information-schema',
description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration',
pattern: /\bINFORMATION_SCHEMA\b/i,
},
];
/* harmony default export */ const sql = (SQL_PATTERNS);
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql-strict.js
/**
* SQL-STRICT context patterns.
*
* Extends the base 'SQL' context with three additional rules that are
* effective at detecting real injections but carry a higher false-positive
* risk on general free-text input.
*
* Use 'SQL-STRICT' when:
* - The string is specifically a SQL fragment or database identifier
* - You control the input domain (e.g. a dedicated SQL search field)
* - You can tolerate occasional false positives in exchange for broader coverage
*
* Use 'SQL' (not STRICT) when:
* - The field is general user text (names, descriptions, comments)
* - False positives would block legitimate content (e.g. "see note -- above")
*
* Rules moved here from 'SQL' due to false-positive risk:
*
* sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary)
* sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words
* sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output
*/
const SQL_STRICT_EXTRA = [
{
id: 'sql-line-comment',
description: 'SQL line comment: -- followed by whitespace or end of string',
pattern: /--(?:\s|$)/,
},
{
id: 'sql-stacked-query',
description: 'Stacked queries: semicolon immediately followed by a SQL keyword',
pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i,
},
{
id: 'sql-hex-encoding',
description: 'Hex-encoded string injection: 0x41414141 style (MySQL)',
pattern: /\b0x[0-9a-f]{4,}/i,
},
];
// SQL-STRICT = all base SQL rules + the three noisy extras
const SQL_STRICT_PATTERNS = [...sql, ...SQL_STRICT_EXTRA];
/* harmony default export */ const sql_strict = (SQL_STRICT_PATTERNS);
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/html.js
/**
* HTML context patterns.
*
* Detects XSS vectors that are dangerous when a string ends up rendered as HTML.
* All patterns use bounded quantifiers to ensure linear-time matching (ReDoS-safe).
*
* Each entry is { pattern: RegExp, id: string, description: string }
* so callers can inspect which rule fired if they need to.
*/
const HTML_PATTERNS = [
{
id: 'html-script-open',
description: '<script opening tag',
pattern: /<script[\s>/]/i,
},
{
id: 'html-script-close',
description: '</script closing tag',
pattern: /<\/script[\s>]/i,
},
{
id: 'html-javascript-protocol',
description: 'javascript: URI scheme (with optional whitespace/encoding)',
// Handles j&#x61;vascript:, j\u0061vascript:, and whitespace variants
pattern: /j[\t\n\r ]*a[\t\n\r ]*v[\t\n\r ]*a[\t\n\r ]*s[\t\n\r ]*c[\t\n\r ]*r[\t\n\r ]*i[\t\n\r ]*p[\t\n\r ]*t[\t\n\r ]*:/i,
},
{
id: 'html-vbscript-protocol',
description: 'vbscript: URI scheme',
pattern: /vbscript[\t\n\r ]*:/i,
},
{
id: 'html-data-html',
description: 'data:text/html URI — can execute scripts in browsers',
pattern: /data[\t\n\r ]*:[\t\n\r ]*text\/html/i,
},
{
id: 'html-data-xhtml',
description: 'data:application/xhtml+xml URI',
pattern: /data[\t\n\r ]*:[\t\n\r ]*application\/xhtml/i,
},
{
id: 'html-data-svg',
description: 'data:image/svg+xml URI — can execute scripts',
pattern: /data[\t\n\r ]*:[\t\n\r ]*image\/svg\+xml/i,
},
{
id: 'html-inline-event-handler',
description: 'Inline event handler attributes: onclick=, onerror=, onload=, etc.',
// \bon ensures we match a word boundary so "phonetic=" is not caught
pattern: /\bon\w{1,30}\s*=/i,
},
{
id: 'html-entity-obfuscated-script',
description: 'HTML-entity-encoded <script (e.g. &#x3C;script or &lt;script)',
// Entities include optional trailing semicolon: &#x3C; or &#x3C (both valid in HTML5)
pattern: /(?:&#x0*3[Cc];?|&#0*60;?|&lt;)\s*script/i,
},
{
id: 'html-entity-obfuscated-javascript',
description: 'HTML-entity-encoded javascript: (partial — catches common &#106; or &#x6a; for "j")',
pattern: /(?:&#x0*6[Aa];?|&#0*106;?)\s*(?:&#x0*61;?|a)[\s\S]{0,80}script\s*:/i,
},
{
id: 'html-style-expression',
description: 'CSS expression() — IE-era code execution in style attributes',
pattern: /style[\s\S]{0,20}expression\s*\(/i,
},
{
id: 'html-object-embed',
description: '<object or <embed tags that can load active content',
pattern: /<(?:object|embed)[\s>/]/i,
},
{
id: 'html-base-tag',
description: '<base href= — can hijack all relative URLs on a page',
pattern: /<base[\s>]/i,
},
{
id: 'html-meta-refresh',
description: '<meta http-equiv="refresh" — can redirect users',
pattern: /<meta[\s\S]{0,40}http-equiv[\s\S]{0,20}refresh/i,
},
{
id: 'html-srcdoc',
description: 'srcdoc= attribute on iframes — embeds HTML that can run scripts',
pattern: /srcdoc\s*=/i,
},
{
id: 'html-iframe',
description: '<iframe tag',
pattern: /<iframe[\s>/]/i,
},
{
id: 'html-form',
description: '<form tag — can be used for phishing / credential harvesting injection',
pattern: /<form[\s>/]/i,
},
];
/* harmony default export */ const html = (HTML_PATTERNS);
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/xml.js
/**
* XML context patterns.
*
* Detects injection vectors that are specifically dangerous when a string
* is inserted into an XML document (not HTML rendering context).
*
* Key distinction from HTML: these patterns target parser-level attacks —
* things that can confuse or subvert an XML parser, trigger external entity
* resolution, or inject DTD content. HTML rendering concerns (XSS) belong
* in the HTML context.
*/
const XML_PATTERNS = [
{
id: 'xml-cdata-injection',
description: 'CDATA section injection: <![CDATA[ breaks out of text node context',
pattern: /<!\[CDATA\[/i,
},
{
id: 'xml-cdata-close',
description: 'CDATA close sequence: ]]> can terminate an enclosing CDATA section',
pattern: /\]\]>/,
},
{
id: 'xml-processing-instruction',
description: 'XML processing instruction: <?xml-stylesheet or <?php etc.',
pattern: /<\?(?:xml[\- ]|php|asp)/i,
},
{
id: 'xml-doctype-injection',
description: 'DOCTYPE declaration embedded in content — can define entities',
// Match <!DOCTYPE followed by end-of-string, whitespace, or [ (internal subset)
pattern: /<!DOCTYPE(?:[\s[]|$)/i,
},
{
id: 'xml-entity-system',
description: 'SYSTEM keyword — used in external entity declarations (XXE)',
pattern: /\bSYSTEM\s+["']/i,
},
{
id: 'xml-entity-public',
description: 'PUBLIC keyword — used in external entity declarations (XXE)',
pattern: /\bPUBLIC\s+["']/i,
},
{
id: 'xml-entity-declaration',
description: '<!ENTITY declaration — defines entities, potential XXE or entity expansion',
pattern: /<!ENTITY[\s%]/i,
},
{
id: 'xml-billion-laughs',
description: 'Entity reference chaining / billion laughs: repeated &eX; style references',
// Heuristic: 3+ consecutive entity refs suggests expansion attack
pattern: /(?:&\w{1,20};){3,}/,
},
{
id: 'xml-namespace-confusion',
description: 'xmlns: attribute injection — can redefine namespaces to confuse parsers',
pattern: /\bxmlns\s*(?::\w{1,40})?\s*=/i,
},
{
id: 'xml-comment-injection',
description: '<!-- comment injection — can hide content from some parsers',
pattern: /<!--/,
},
{
id: 'xml-comment-close',
description: '--> closes an enclosing XML comment',
pattern: /--!?>/,
},
{
id: 'xml-pi-close',
description: '?> closes an enclosing processing instruction',
pattern: /\?>/,
},
];
/* harmony default export */ const xml = (XML_PATTERNS);
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/svg.js
/**
* SVG context patterns.
*
* SVG is XML-based but renders in browsers, giving it a unique attack surface
* that combines XML parser behaviour with browser rendering and JavaScript execution.
*
* Many of these vectors bypass HTML sanitizers that don't understand SVG semantics
* (DOMPurify has documented bypass vulnerabilities specifically in SVG/XML context).
*/
const SVG_PATTERNS = [
{
id: 'svg-script-element',
description: '<script element inside SVG executes JavaScript',
pattern: /<script[\s>/]/i,
},
{
id: 'svg-xlink-href-javascript',
description: 'xlink:href with javascript: — classic SVG XSS via <a> or <use>',
pattern: /xlink\s*:\s*href\s*=\s*["']?\s*javascript\s*:/i,
},
{
id: 'svg-href-javascript',
description: 'href= with javascript: in SVG context (<a>, <animate>, etc.)',
pattern: /href\s*=\s*["']?\s*javascript\s*:/i,
},
{
id: 'svg-foreignobject',
description: '<foreignObject embeds HTML inside SVG — can execute scripts',
pattern: /<foreignObject[\s>/]/i,
},
{
id: 'svg-use-external',
description: '<use xlink:href or href pointing to external resource (non-fragment URL)',
// Match <use with href= where the value starts with a non-# character (external URL)
// [\"'][^#] catches quoted values not starting with #; [^\"'#\s>] catches unquoted
pattern: /<use[\s\S]{0,60}(?:xlink\s*:\s*)?href\s*=\s*(?:["'][^#]|[^"'#\s>])/i,
},
{
id: 'svg-animate-href',
description: '<animate attributeName="href" — can dynamically change href to javascript:',
pattern: /<animate[\s\S]{0,80}attributeName\s*=\s*["'][\s]*href["']/i,
},
{
id: 'svg-animate-xlinkhref',
description: '<animate attributeName="xlink:href"',
pattern: /<animate[\s\S]{0,80}attributeName\s*=\s*["'][\s]*xlink\s*:\s*href["']/i,
},
{
id: 'svg-set-javascript',
description: '<set to="javascript:..." — sets an attribute to a javascript: URI',
pattern: /<set[\s\S]{0,80}to\s*=\s*["']?\s*javascript\s*:/i,
},
{
id: 'svg-event-handler',
description: 'SVG-specific event handler attributes: onload=, onerror=, onactivate=, etc.',
pattern: /\bon(?:load|error|activate|begin|end|repeat|focus|blur|click|mouse\w{1,20}|key\w{1,20})\s*=/i,
},
{
id: 'svg-handler-generic',
description: 'Generic on* handler catch-all for SVG attributes',
pattern: /\bon\w{1,30}\s*=/i,
},
{
id: 'svg-filter-feimage',
description: '<feImage href= — filter primitive that can load external resources',
pattern: /<feImage[\s\S]{0,80}(?:xlink\s*:\s*)?href\s*=/i,
},
{
id: 'svg-image-external',
description: '<image xlink:href with http/https or javascript protocol',
pattern: /<image[\s\S]{0,80}(?:xlink\s*:\s*)?href\s*=\s*["']?\s*(?:https?|javascript)\s*:/i,
},
{
id: 'svg-style-javascript',
description: 'style= attribute containing javascript: (e.g. background:url(javascript:...))',
pattern: /style\s*=[\s\S]{0,60}javascript\s*:/i,
},
];
/* harmony default export */ const svg = (SVG_PATTERNS);
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/shell.js
/**
* SHELL context patterns.
*
* Detects shell injection vectors and path traversal patterns.
* Designed for use when a string will be passed to a shell command,
* used as a file path, or interpolated into OS-level operations.
*/
const SHELL_PATTERNS = [
{
id: 'shell-path-traversal-unix',
description: 'Unix path traversal: ../ — climbing the directory tree',
pattern: /\.\.\//,
},
{
id: 'shell-path-traversal-windows',
description: 'Windows path traversal: ..\\ — climbing the directory tree',
pattern: /\.\.\\/,
},
{
id: 'shell-path-traversal-encoded',
description: 'URL-encoded path traversal: %2e%2e or %2f variants',
pattern: /%2e%2e|%2f\.\.|\.\.%2f/i,
},
{
id: 'shell-null-byte',
description: 'Null byte injection: \\x00 or %00 — truncates strings in C-backed functions',
pattern: /\x00|%00/,
},
{
id: 'shell-semicolon',
description: 'Semicolon command separator: cmd1; cmd2',
pattern: /;/,
},
{
id: 'shell-pipe',
description: 'Pipe operator: cmd1 | cmd2',
pattern: /\|/,
},
{
id: 'shell-and-operator',
description: 'AND operator: cmd1 && cmd2',
pattern: /&&/,
},
{
id: 'shell-or-operator',
description: 'OR operator: cmd1 || cmd2',
pattern: /\|\|/,
},
{
id: 'shell-backtick',
description: 'Backtick command substitution: `cmd`',
pattern: /`/,
},
{
id: 'shell-dollar-paren',
description: 'Dollar-paren command substitution: $(cmd)',
pattern: /\$\(/,
},
{
id: 'shell-dollar-brace',
description: 'Dollar-brace variable expansion: ${var} — can be abused for injection',
pattern: /\$\{/,
},
{
id: 'shell-redirect-out',
description: 'Output redirection: cmd > file or cmd >> file',
pattern: />{1,2}/,
},
{
id: 'shell-redirect-in',
description: 'Input redirection: cmd < file',
pattern: /</,
},
{
id: 'shell-newline-injection',
description: 'Newline injection: \\n or \\r — can inject new shell commands',
pattern: /[\n\r]/,
},
{
id: 'shell-glob-star',
description: 'Glob expansion: * or ? — can expand to unintended files',
// Only flag when combined with path separators to reduce false positives
pattern: /[/\\][*?]/,
},
{
id: 'shell-absolute-root',
description: 'Absolute root path injection: string starting with / or \\ (Windows UNC)',
pattern: /^(?:\/|\\\\)/,
},
{
id: 'shell-windows-drive',
description: 'Windows drive letter path injection: C:\\ or D:/',
pattern: /^[a-zA-Z]:[/\\]/,
},
{
id: 'shell-curl-wget',
description: 'curl/wget with URL or flags — can exfiltrate data or download payloads',
// Require a URL scheme (http/https/ftp) or a flag (-) to reduce false positives
// "curl is a tool" won't match; "curl http://..." or "curl -s ..." will
pattern: /\b(?:curl|wget)\s+(?:https?:\/\/|ftp:\/\/|-)/i,
},
];
/* harmony default export */ const shell = (SHELL_PATTERNS);
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/redos.js
/**
* REDOS context patterns.
*
* Detects strings that, if used as regular expressions, could cause
* catastrophic backtracking (ReDoS — Regular Expression Denial of Service).
*
* These patterns detect the structural forms that lead to exponential or
* polynomial backtracking in NFA-based regex engines (V8, PCRE, Java, etc.).
*
* Use this context when user-supplied strings will be compiled into RegExp objects.
*/
const REDOS_PATTERNS = [
{
id: 'redos-nested-quantifier-plus',
description: 'Nested + quantifier inside a group with outer quantifier: (a+)+, (.+b)*, etc.',
// Matches any group containing a + quantifier, with an outer * or + — catches (a+)+, (.+b)*, etc.
pattern: /\([^)]*\+[^)]*\)[+*]/,
},
{
id: 'redos-nested-quantifier-star',
description: 'Nested * quantifier: (a*)* or (a*)+ — catastrophic backtracking',
pattern: /\([^)]*\*[^)]*\)[*+]/,
},
{
id: 'redos-nested-groups',
description: 'Doubly nested quantified groups: ((a+)+) — guaranteed catastrophic',
pattern: /\(\([^)]{0,40}\)[+*]\)[+*]/,
},
{
id: 'redos-alternation-overlap',
description: 'Overlapping alternation under quantifier: (a|a)+ — ambiguous NFA paths',
// Detect repeated identical alternatives under a quantifier
pattern: /\(([^|()]{1,20})\|(?:\1)(?:\|[^|()]{1,20}){0,5}\)[+*?]{1,2}/,
},
{
id: 'redos-star-plus-concat',
description: '(x*x)+ pattern — triggers super-linear backtracking',
pattern: /\([^)]{0,10}\*[^)]{0,10}\)[+*]/,
},
{
id: 'redos-dot-star-greedy',
description: '(.*){n,} or (.+){n,} — repeated greedy dot quantifiers',
pattern: /\(\.[*+]\)\{?\d/,
},
{
id: 'redos-large-repetition',
description: 'Very large fixed or range repetition count {1000,} or {1000,n} — denial of service via backtracking',
// Matches { followed by 4+ digits (≥1000), then optional ,digits }
pattern: /\{\d{4,}(?:,\d*)?\}/,
},
{
id: 'redos-catastrophic-alternation',
description: 'Long alternation with many similar branches — polynomial backtracking risk',
// Heuristic: 10+ pipe-separated alternatives in a single group
pattern: /\([^)]{0,200}(?:\|[^|)]{0,50}){9,}\)/,
},
];
/* harmony default export */ const redos = (REDOS_PATTERNS);
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/nosql.js
/**
* NOSQL context patterns.
*
* Detects injection vectors specific to NoSQL databases (primarily MongoDB)
* and JavaScript-evaluated queries.
*
* Attack categories:
* 1. MongoDB query operator injection: $where, $ne, $gt, $regex, $or, $and, etc.
* These operators, when injected into a JSON query object, can bypass
* authentication or exfiltrate data without knowing passwords.
*
* 2. JavaScript execution: $where clauses execute arbitrary JS server-side.
*
* 3. Prototype pollution: __proto__, constructor.prototype — can corrupt
* the prototype chain of all objects in the Node.js process.
*
* Pattern note: MongoDB operators appear as JSON keys. In JSON, keys are
* quoted: {"$where": ...} so the pattern must allow an optional closing
* quote between the operator name and the colon: /\$where["'\s]*:/
*/
// Shared suffix: optional closing quote/whitespace before the colon
// Handles: $op: (bare), "$op": (JSON), '$op': (single-quoted)
const SEP = /["'\s]*:/;
const sep = '["\'\\s]*:';
const NOSQL_PATTERNS = [
// ─── MongoDB $ operator injection ────────────────────────────────────────
{
id: 'nosql-where-operator',
description: '$where — executes arbitrary JavaScript server-side in MongoDB',
pattern: new RegExp(`\\$where${sep}`, 'i'),
},
{
id: 'nosql-ne-operator',
description: '$ne — "not equal" operator used to bypass equality checks',
pattern: new RegExp(`\\$ne${sep}`, 'i'),
},
{
id: 'nosql-gt-operator',
description: '$gt — "greater than" used to bypass password/value checks',
pattern: new RegExp(`\\$gte?${sep}`, 'i'),
},
{
id: 'nosql-lt-operator',
description: '$lt / $lte — "less than" bypass variants',
pattern: new RegExp(`\\$lte?${sep}`, 'i'),
},
{
id: 'nosql-regex-operator',
description: '$regex — can be used to extract data character by character (blind injection)',
pattern: new RegExp(`\\$regex${sep}`, 'i'),
},
{
id: 'nosql-or-operator',
description: '$or — logical OR; used to create always-true conditions',
pattern: new RegExp(`\\$or${sep}\\s*\\[`, 'i'),
},
{
id: 'nosql-and-operator',
description: '$and — logical AND operator injection',
pattern: new RegExp(`\\$and${sep}\\s*\\[`, 'i'),
},
{
id: 'nosql-nor-operator',
description: '$nor — logical NOR operator injection',
pattern: new RegExp(`\\$nor${sep}\\s*\\[`, 'i'),
},
{
id: 'nosql-exists-operator',
description: '$exists — can enumerate fields to determine schema',
pattern: new RegExp(`\\$exists${sep}`, 'i'),
},
{
id: 'nosql-in-operator',
description: '$in — matches any value in a list; can enumerate values',
pattern: new RegExp(`\\$in${sep}\\s*\\[`, 'i'),
},
{
id: 'nosql-expr-operator',
description: '$expr — allows aggregation expressions in queries (MongoDB 3.6+)',
pattern: new RegExp(`\\$expr${sep}`, 'i'),
},
{
id: 'nosql-function-operator',
description: '$function — executes arbitrary JavaScript in MongoDB 4.4+',
pattern: new RegExp(`\\$function${sep}`, 'i'),
},
{
id: 'nosql-accumulator-operator',
description: '$accumulator — custom aggregation with arbitrary JS execution',
pattern: new RegExp(`\\$accumulator${sep}`, 'i'),
},
// ─── Prototype pollution ─────────────────────────────────────────────────
{
id: 'nosql-proto-pollution',
description: '__proto__ — prototype pollution via object key injection',
pattern: /__proto__/,
},
{
id: 'nosql-constructor-prototype',
description: 'constructor.prototype — alternative prototype pollution vector (dot notation or JSON key)',
// Matches dot-notation (obj.constructor.prototype) and JSON key adjacency
// ("constructor": {"prototype": ...})
pattern: /constructor[\s"':.,{\[]*prototype/i,
},
{
id: 'nosql-proto-bracket',
description: '["__proto__"] — bracket-notation prototype pollution',
pattern: /\[["']__proto__["']\]/,
},
];
/* harmony default export */ const nosql = (NOSQL_PATTERNS);
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/log.js
/**
* LOG context patterns.
*
* Detects injection vectors that are dangerous when a string is written
* to a log file, passed to a logging framework, or interpolated into
* a log message that will be parsed or displayed.
*
* Attack categories:
* 1. CRLF injection — injects fake log lines by embedding newlines
* 2. Log4Shell (CVE-2021-44228) — ${jndi:...} triggers JNDI lookup in Log4j
* 3. SSTI in log templates — {{...}}, #{...} trigger template evaluation
* if the log message is passed through a template engine
* 4. Null byte injection — truncates log entries in some implementations
* 5. ANSI escape injection — manipulates terminal output when logs are
* tailed in a terminal (colour codes, cursor movement, etc.)
*
* Note: Newline characters (\n, \r) will produce false positives for
* multi-line legitimate values. Use this context only for single-line
* log field values (usernames, IDs, request parameters, etc.).
*/
const LOG_PATTERNS = [
// ─── CRLF / newline injection ─────────────────────────────────────────────
{
id: 'log-crlf-injection',
description: 'CRLF injection: literal \\r or \\n embeds fake log lines',
pattern: /[\r\n]/,
},
{
id: 'log-url-encoded-crlf',
description: 'URL-encoded CRLF: %0d, %0a, %0D, %0A — decoded by some log parsers',
pattern: /%0[dDaA]/,
},
{
id: 'log-unicode-newline',
description: 'Unicode newline variants: U+2028 (line separator), U+2029 (paragraph separator)',
pattern: /[\u2028\u2029]/,
},
// ─── Log4Shell / JNDI injection (CVE-2021-44228) ─────────────────────────
{
id: 'log-log4shell-jndi',
description: 'Log4Shell: ${jndi:...} triggers remote code execution in Apache Log4j',
pattern: /\$\{jndi\s*:/i,
},
{
id: 'log-log4shell-obfuscated',
description: 'Obfuscated Log4Shell: ${::-j}... lookup-bypass prefix used to evade WAF detection',
// ${::- is the Log4j lookup-bypass escape sequence; presence alone is suspicious
pattern: /\$\{::-/,
},
{
id: 'log-log4j-lookup',
description: 'Log4j lookup syntax: ${env:...}, ${sys:...}, ${ctx:...} — data exfiltration',
pattern: /\$\{(?:env|sys|ctx|main|map|sd|web|docker|k8s|spring)\s*:/i,
},
// ─── Server-Side Template Injection (SSTI) in log messages ───────────────
{
id: 'log-ssti-double-brace',
description: 'SSTI double-brace: {{expression}} — Jinja2, Twig, Handlebars, etc.',
pattern: /\{\{[\s\S]{0,80}\}\}/,
},
{
id: 'log-ssti-hash-brace',
description: 'SSTI hash-brace: #{expression} — Thymeleaf, Velocity, Ruby ERB',
pattern: /#\{[\s\S]{0,80}\}/,
},
{
id: 'log-ssti-dollar-brace',
description: 'SSTI/EL injection: ${expression with operators or method calls} — JSP EL, Freemarker, SpEL',
// Require that the ${...} content looks like an expression, not a plain variable name.
// Flags if the content contains: . ( * + operators, or known SSTI keywords.
// This avoids flagging ${PATH}, ${HOME} etc. (plain shell variables).
pattern: /\$\{[^}]*(?:\.|\(|\*|\+|\bclass\b|\bruntime\b|\bprocess\b|\bexec\b)[^}]{0,80}\}/i,
},
{
id: 'log-ssti-percent-tag',
description: 'SSTI ERB/ASP tag: <%= expression %> — Ruby ERB, ASP',
pattern: /<%=[\s\S]{0,80}%>/,
},
// ─── Null byte ────────────────────────────────────────────────────────────
{
id: 'log-null-byte',
description: 'Null byte: \\x00 or %00 — can truncate log entries in C-backed loggers',
pattern: /\x00|%00/,
},
// ─── ANSI escape injection ────────────────────────────────────────────────
{
id: 'log-ansi-escape',
description: 'ANSI escape sequence: ESC[ — can manipulate terminal output when logs are tailed',
pattern: /\x1b\[/,
},
];
/* harmony default export */ const log = (LOG_PATTERNS);
;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/index.js
/**
* is-unsafe v2
*
* Zero-dependency, DOM-free, pure predicate for detecting unsafe strings
* across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts.
*
* v2 change: contexts are imported as named pattern arrays rather than resolved
* via a string-keyed registry. This makes each context independently
* tree-shakeable — bundlers can drop any context you never import.
*
* @module is-unsafe
*/
// ─── Context pattern arrays (named exports) ────────────────────────────────
// Import only the ones you need. Each is independently tree-shakeable.
// SQL-STRICT needs a quoted identifier because of the hyphen
// ─── VALID_CONTEXTS convenience re-export ─────────────────────────────────
// Importing this pulls in ALL contexts. Use it only when you need all of them
// (e.g. for validation UI, tooling, or exhaustive audits).
// If you only need a subset, import the named contexts directly instead.
// ─── Attach labels to named contexts ──────────────────────────────────────
// Each built-in PatternList carries its canonical name so matchList can read
// list.label directly — no registry lookup needed at match time.
// Custom PatternLists default to 'CUSTOM' unless the caller sets list.label.
html.label = 'HTML';
xml.label = 'XML';
svg.label = 'SVG';
sql.label = 'SQL';
sql_strict.label = 'SQL-STRICT';
shell.label = 'SHELL';
redos.label = 'REDOS';
nosql.label = 'NOSQL';
log.label = 'LOG';
const VALID_CONTEXTS = Object.freeze({
HTML: html,
XML: xml,
SVG: svg,
SQL: sql,
'SQL-STRICT': sql_strict,
SHELL: shell,
REDOS: redos,
NOSQL: nosql,
LOG: log,
});
// ─── Types ────────────────────────────────────────────────────────────────
/**
* @typedef {{ id: string, description: string, pattern: RegExp }} Rule
*/
/**
* @typedef {Rule[]} PatternList
*/
/**
* @typedef {Object} MatchResult
* @property {string} context - Label identifying which context matched ('HTML', 'CUSTOM', etc.)
* @property {string} id - Rule identifier
* @property {string} description - Human-readable description of what was matched
* @property {RegExp} pattern - The pattern that matched
*/
// ─── Internal helpers ──────────────────────────────────────────────────────
/**
* @param {unknown} value
*/
function assertString(value) {
if (typeof value !== 'string') {
throw new TypeError(
`is-unsafe: first argument must be a string, got ${typeof value}`
);
}
}
/**
* @param {unknown} context
*/
function assertContext(context) {
if (context instanceof RegExp) return;
if (Array.isArray(context)) {
if (context.length === 0) {
throw new TypeError('is-unsafe: context must not be an empty array');
}
// Detect array-of-arrays vs flat pattern list
if (Array.isArray(context[0])) {
// Array of PatternLists
for (const list of context) {
if (!Array.isArray(list) || list.length === 0) {
throw new TypeError(
'is-unsafe: each context in the array must be a non-empty pattern array (PatternList)'
);
}
}
}
// else: flat PatternList — trust it, no deep validation needed
return;
}
throw new TypeError(
`is-unsafe: second argument must be a PatternList (e.g. HTML), ` +
`an array of PatternLists (e.g. [HTML, XML]), or a RegExp. Got: ${typeof context}`
);
}
/**
* Normalise any valid context arg into an array of PatternLists.
*
* @param {Rule[]|Rule[][]|RegExp} context
* @returns {{ lists: Rule[][]|null, regex: RegExp|null }}
*/
function normalise(context) {
if (context instanceof RegExp) return { lists: null, regex: context };
// Distinguish PatternList (array of rule objects) from array of PatternLists
if (Array.isArray(context[0])) return { lists: context, regex: null };
return { lists: [context], regex: null };
}
/**
* Test value against a single PatternList. Returns the first MatchResult or null.
*
* @param {string} value
* @param {Rule[]} list
* @returns {MatchResult|null}
*/
function matchList(value, list) {
const label = list.label ?? 'CUSTOM';
for (const rule of list) {
if (rule.pattern.test(value)) {
return { context: label, id: rule.id, description: rule.description, pattern: rule.pattern };
}
}
return null;
}
// ─── Public API ───────────────────────────────────────────────────────────
/**
* Returns `true` if `value` is unsafe in the given context(s), `false` otherwise.
*
* @param {string} value - The string to test
* @param {PatternList | PatternList[] | RegExp} context
* - A PatternList imported from is-unsafe (e.g. `HTML`, `XML`)
* - An array of PatternLists — returns true if unsafe in **any** of them
* - A custom RegExp — returns true if the pattern matches
* @returns {boolean}
*
* @example
* import { isUnsafe, HTML, SQL } from 'is-unsafe';
*
* isUnsafe('<script>alert(1)</script>', HTML) // true
* isUnsafe('hello world', HTML) // false
* isUnsafe('value', [HTML, SQL]) // false
* isUnsafe('value', /my-pattern/i) // false
*/
function isUnsafe(value, context) {
assertString(value);
assertContext(context);
const { lists, regex } = normalise(context);
if (regex) return regex.test(value);
for (const list of lists) {
if (matchList(value, list) !== null) return true;
}
return false;
}
/**
* Like `isUnsafe`, but returns the first `MatchResult` describing **why**
* the value was flagged, or `null` if it is safe.
*
* @param {string} value
* @param {PatternList | PatternList[] | RegExp} context
* @returns {MatchResult|null}
*
* @example
* import { whyUnsafe, HTML } from 'is-unsafe';
*
* whyUnsafe('<script>alert(1)</script>', HTML)
* // { context: 'HTML', id: 'html-script-open', description: '...', pattern: /.../ }
*/
function whyUnsafe(value, context) {
assertString(value);
assertContext(context);
const { lists, regex } = normalise(context);
if (regex) {
return regex.test(value)
? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex }
: null;
}
for (const list of lists) {
const result = matchList(value, list);
if (result !== null) return result;
}
return null;
}
/**
* Returns **all** matching rules across the given context(s), or an empty
* array if the value is safe. Useful for comprehensive auditing.
*
* @param {string} value
* @param {PatternList | PatternList[] | RegExp} context
* @returns {MatchResult[]}
*/
function allUnsafe(value, context) {
assertString(value);
assertContext(context);
const { lists, regex } = normalise(context);
const results = [];
if (regex) {
if (regex.test(value)) {
results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex });
}
return results;
}
for (const list of lists) {
const label = list.label ?? 'CUSTOM';
for (const rule of list) {
if (rule.pattern.test(value)) {
results.push({ context: label, id: rule.id, description: rule.description, pattern: rule.pattern });
}
}
}
return results;
}
/* harmony default export */ const is_unsafe_src = ((/* unused pure expression or super */ null && (isUnsafe)));
;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js
///@ts-check
// 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;
}
class OrderedObjParser {
constructor(options, externalEntities) {
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;
this.doctypefound = false;
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, ...externalEntities },
numericAllowed: this.options.htmlEntities,
limit: {
maxTotalExpansions: this.options.processEntities.maxTotalExpansions,
maxExpandedLength: this.options.processEntities.maxExpandedLength,
applyLimitsTo: this.options.processEntities.appliesTo,
},
// onExternalEntity: (name, value) => isUnsafe(value) ? 'block' : 'allow',
onInputEntity: (name, value) =>
//TODO: VALID_CONTEXTS.HTML should be set only if this.options.htmlEntities
isUnsafe(value, [html, xml]) ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW,
//postCheck: resolved => resolved
});
}
// Initialize path matcher for path-expression-matcher
this.matcher = new Matcher/* default */.A();
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/* default */.A(stopNodeExp));
} else if (stopNodeExp instanceof Expression/* default */.A) {
// 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 = (0,util/* getAllMatches */.Xe)(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 && !options.preserveOrder) {
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;
this.doctypefound = false;
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
//a closing tag with no matching opening tag leaves the stack empty
currentNode = this.tagsNodeStack.pop() || xmlObj;//avoid recursion, set the parent tag scope
if (options.captureMetaData && currentNode) {
currentNode.addEndIndex(closeIndex + 1);
}
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);
docTypeReader.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);
if (options.captureMetaData) {
// closeIndex points at '?' of the closing '?>'
currentNode.addEndIndex(tagData.closeIndex + 2);
}
}
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'
if (this.doctypefound) throw new Error("Multiple DOCTYPE declarations found.");
this.doctypefound = true;
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);
if (options.captureMetaData) {
currentNode.addEndIndex(i + 1);
}
} 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);
if (options.captureMetaData) {
currentNode.addEndIndex(closeIndex + 1);
}
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);
if (options.captureMetaData) {
currentNode.addEndIndex(result.closeIndex + 1);
}
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 = ">") {
//TODO: ignore boolean attributes in tag expression
//TODO: if ignore attributes, dont read full attribute expression but the end. But read for xml declaration
let attrBoundary = 0;
const len = xmlData.length;
const closeCode0 = closingChar.charCodeAt(0);
const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1;
let result = '';
let segmentStart = i;
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) {
result += xmlData.substring(segmentStart, index);
return { data: result, index };
}
} else {
result += xmlData.substring(segmentStart, index);
return { data: result, index };
}
} else if (code === 9 && !attrBoundary) { // \t - only replace with space outside attribute values
// Flush accumulated segment, add space, start new segment
result += xmlData.substring(segmentStart, index) + ' ';
segmentStart = index + 1;
}
}
}
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, false)
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 ((0,util/* isExist */.yQ)(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 (util/* criticalProperties */.vl.includes(name)) {
throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`);
} else if (util/* DANGEROUS_PROPERTY_NAMES */.q9.includes(name)) {
return options.onDangerousProperty(name);
}
return name;
}
;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/node2json.js
const node2json_METADATA_SYMBOL = XmlNode.getMetaDataSymbol();
/**
* Helper function to strip attribute prefix from attribute map
* @param {object} attrs - Attributes with prefix (e.g., {"@_class": "code"})
* @param {string} prefix - Attribute prefix to remove (e.g., "@_")
* @returns {object} Attributes without prefix (e.g., {"class": "code"})
*/
function stripAttributePrefix(attrs, prefix) {
if (!attrs || typeof attrs !== 'object') return {};
if (!prefix) return attrs;
const rawAttrs = {};
for (const key in attrs) {
if (key.startsWith(prefix)) {
const rawName = key.substring(prefix.length);
rawAttrs[rawName] = attrs[key];
} else {
// Attribute without prefix (shouldn't normally happen, but be safe)
rawAttrs[key] = attrs[key];
}
}
return rawAttrs;
}
/**
*
* @param {array} node
* @param {any} options
* @param {Matcher} matcher - Path matcher instance
* @returns
*/
function prettify(node, options, matcher, readonlyMatcher) {
return compress(node, options, matcher, readonlyMatcher);
}
/**
* @param {array} arr
* @param {object} options
* @param {Matcher} matcher - Path matcher instance
* @returns object
*/
function compress(arr, options, matcher, readonlyMatcher) {
let text;
const compressedObj = {}; //This is intended to be a plain object
for (let i = 0; i < arr.length; i++) {
const tagObj = arr[i];
const property = propName(tagObj);
// Push current property to matcher WITH RAW ATTRIBUTES (no prefix)
if (property !== undefined && property !== options.textNodeName) {
const rawAttrs = stripAttributePrefix(
tagObj[":@"] || {},
options.attributeNamePrefix
);
matcher.push(property, rawAttrs);
}
if (property === options.textNodeName) {
if (text === undefined) text = tagObj[property];
else text += "" + tagObj[property];
} else if (property === undefined) {
continue;
} else if (tagObj[property]) {
let val = compress(tagObj[property], options, matcher, readonlyMatcher);
const isLeaf = isLeafTag(val, options);
if (Object.keys(val).length === 0 && options.alwaysCreateTextNode) {
val[options.textNodeName] = "";
}
if (tagObj[":@"]) {
assignAttributes(val, tagObj[":@"], readonlyMatcher, options);
} else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) {
val = val[options.textNodeName];
} else if (Object.keys(val).length === 0) {
if (options.alwaysCreateTextNode) val[options.textNodeName] = "";
else val = "";
}
if (tagObj[node2json_METADATA_SYMBOL] !== undefined && typeof val === "object" && val !== null) {
val[node2json_METADATA_SYMBOL] = tagObj[node2json_METADATA_SYMBOL]; // copy over metadata
}
if (compressedObj[property] !== undefined && Object.prototype.hasOwnProperty.call(compressedObj, property)) {
if (!Array.isArray(compressedObj[property])) {
compressedObj[property] = [compressedObj[property]];
}
compressedObj[property].push(val);
} else {
//TODO: if a node is not an array, then check if it should be an array
//also determine if it is a leaf node
// Pass jPath string or readonlyMatcher based on options.jPath setting
const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher;
if (options.isArray(property, jPathOrMatcher, isLeaf)) {
compressedObj[property] = [val];
} else {
compressedObj[property] = val;
}
}
// Pop property from matcher after processing
if (property !== undefined && property !== options.textNodeName) {
matcher.pop();
}
}
}
// if(text && text.length > 0) compressedObj[options.textNodeName] = text;
if (typeof text === "string") {
if (text.length > 0) compressedObj[options.textNodeName] = text;
} else if (text !== undefined) compressedObj[options.textNodeName] = text;
return compressedObj;
}
function propName(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key !== ":@") return key;
}
}
function assignAttributes(obj, attrMap, readonlyMatcher, options) {
if (attrMap) {
const keys = Object.keys(attrMap);
const len = keys.length; //don't make it inline
for (let i = 0; i < len; i++) {
const atrrName = keys[i]; // This is the PREFIXED name (e.g., "@_class")
// Strip prefix for matcher path (for isArray callback)
const rawAttrName = atrrName.startsWith(options.attributeNamePrefix)
? atrrName.substring(options.attributeNamePrefix.length)
: atrrName;
// For attributes, we need to create a temporary path
// Pass jPath string or matcher based on options.jPath setting
const jPathOrMatcher = options.jPath
? readonlyMatcher.toString() + "." + rawAttrName
: readonlyMatcher;
if (options.isArray(atrrName, jPathOrMatcher, true, true)) {
obj[atrrName] = [attrMap[atrrName]];
} else {
obj[atrrName] = attrMap[atrrName];
}
}
}
}
function isLeafTag(obj, options) {
const { textNodeName } = options;
const propCount = Object.keys(obj).length;
if (propCount === 0) {
return true;
}
if (
propCount === 1 &&
(obj[textNodeName] || typeof obj[textNodeName] === "boolean" || obj[textNodeName] === 0)
) {
return true;
}
return false;
}
// EXTERNAL MODULE: ./node_modules/fast-xml-parser/src/validator.js
var validator = __webpack_require__(1176);
;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/XMLParser.js
class XMLParser {
constructor(options) {
this.externalEntities = {};
this.options = buildOptions(options);
}
/**
* Parse XML dats to JS object
* @param {string|Uint8Array} xmlData
* @param {boolean|Object} validationOption
*/
parse(xmlData, validationOption) {
if (typeof xmlData !== "string" && xmlData.toString) {
xmlData = xmlData.toString();
} else if (typeof xmlData !== "string") {
throw new Error("XML data is accepted in String or Bytes[] form.")
}
if (validationOption) {
if (validationOption === true) validationOption = {}; //validate with default options
const result = (0,validator/* validate */.t)(xmlData, validationOption);
if (result !== true) {
throw Error(`${result.err.msg}:${result.err.line}:${result.err.col}`)
}
}
const orderedObjParser = new OrderedObjParser(this.options, this.externalEntities);
// orderedObjParser.entityDecoder.setExternalEntities(this.externalEntities);
const orderedResult = orderedObjParser.parseXml(xmlData);
if (this.options.preserveOrder || orderedResult === undefined) return orderedResult;
else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher);
}
/**
* Add Entity which is not by default supported by this library
* @param {string} key
* @param {string} value
*/
addEntity(key, value) {
if (value.indexOf("&") !== -1) {
throw new Error("Entity value can't have '&'")
} else if (key.indexOf("&") !== -1 || key.indexOf(";") !== -1) {
throw new Error("An entity must be set without '&' and ';'. Eg. use '#xD' for '&#xD;'")
} else if (value === "&") {
throw new Error("An entity with value '&' is not permitted");
} else {
this.externalEntities[key] = value;
}
}
/**
* Returns a Symbol that can be used to access the metadata
* property on a node.
*
* If Symbol is not available in the environment, an ordinary property is used
* and the name of the property is here returned.
*
* The XMLMetaData property is only present when `captureMetaData`
* is true in the options.
*/
static getMetaDataSymbol() {
return XmlNode.getMetaDataSymbol();
}
}
/***/ }),
/***/ 3945:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ A: () => (/* binding */ Expression)
/* harmony export */ });
/**
* Expression - Parses and stores a tag pattern expression
*
* Patterns are parsed once and stored in an optimized structure for fast matching.
*
* @example
* const expr = new Expression("root.users.user");
* const expr2 = new Expression("..user[id]:first");
* const expr3 = new Expression("root/users/user", { separator: '/' });
*/
class Expression {
/**
* Create a new Expression
* @param {string} pattern - Pattern string (e.g., "root.users.user", "..user[id]")
* @param {Object} options - Configuration options
* @param {string} options.separator - Path separator (default: '.')
*/
constructor(pattern, options = {}, data) {
this.pattern = pattern;
this.separator = options.separator || '.';
this.segments = this._parse(pattern);
this.data = data;
// Cache expensive checks for performance (O(1) instead of O(n))
this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard');
this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined);
this._hasPositionSelector = this.segments.some(seg => seg.position !== undefined);
}
/**
* Parse pattern string into segments
* @private
* @param {string} pattern - Pattern to parse
* @returns {Array} Array of segment objects
*/
_parse(pattern) {
const segments = [];
// Split by separator but handle ".." specially
let i = 0;
let currentPart = '';
while (i < pattern.length) {
if (pattern[i] === this.separator) {
// Check if next char is also separator (deep wildcard)
if (i + 1 < pattern.length && pattern[i + 1] === this.separator) {
// Flush current part if any
if (currentPart.trim()) {
segments.push(this._parseSegment(currentPart.trim()));
currentPart = '';
}
// Add deep wildcard
segments.push({ type: 'deep-wildcard' });
i += 2; // Skip both separators
} else {
// Regular separator
if (currentPart.trim()) {
segments.push(this._parseSegment(currentPart.trim()));
}
currentPart = '';
i++;
}
} else {
currentPart += pattern[i];
i++;
}
}
// Flush remaining part
if (currentPart.trim()) {
segments.push(this._parseSegment(currentPart.trim()));
}
return segments;
}
/**
* Parse a single segment
* @private
* @param {string} part - Segment string (e.g., "user", "ns::user", "user[id]", "ns::user:first")
* @returns {Object} Segment object
*/
_parseSegment(part) {
const segment = { type: 'tag' };
// NEW NAMESPACE SYNTAX (v2.0):
// ============================
// Namespace uses DOUBLE colon (::)
// Position uses SINGLE colon (:)
//
// Examples:
// "user" → tag
// "user:first" → tag + position
// "user[id]" → tag + attribute
// "user[id]:first" → tag + attribute + position
// "ns::user" → namespace + tag
// "ns::user:first" → namespace + tag + position
// "ns::user[id]" → namespace + tag + attribute
// "ns::user[id]:first" → namespace + tag + attribute + position
// "ns::first" → namespace + tag named "first" (NO ambiguity!)
//
// This eliminates all ambiguity:
// :: = namespace separator
// : = position selector
// [] = attributes
// Step 1: Extract brackets [attr] or [attr=value]
let bracketContent = null;
let withoutBrackets = part;
const bracketMatch = part.match(/^([^\[]+)(\[[^\]]*\])(.*)$/);
if (bracketMatch) {
withoutBrackets = bracketMatch[1] + bracketMatch[3];
if (bracketMatch[2]) {
const content = bracketMatch[2].slice(1, -1);
if (content) {
bracketContent = content;
}
}
}
// Step 2: Check for namespace (double colon ::)
let namespace = undefined;
let tagAndPosition = withoutBrackets;
if (withoutBrackets.includes('::')) {
const nsIndex = withoutBrackets.indexOf('::');
namespace = withoutBrackets.substring(0, nsIndex).trim();
tagAndPosition = withoutBrackets.substring(nsIndex + 2).trim(); // Skip ::
if (!namespace) {
throw new Error(`Invalid namespace in pattern: ${part}`);
}
}
// Step 3: Parse tag and position (single colon :)
let tag = undefined;
let positionMatch = null;
if (tagAndPosition.includes(':')) {
const colonIndex = tagAndPosition.lastIndexOf(':'); // Use last colon for position
const tagPart = tagAndPosition.substring(0, colonIndex).trim();
const posPart = tagAndPosition.substring(colonIndex + 1).trim();
// Verify position is a valid keyword
const isPositionKeyword = ['first', 'last', 'odd', 'even'].includes(posPart) ||
/^nth\(\d+\)$/.test(posPart);
if (isPositionKeyword) {
tag = tagPart;
positionMatch = posPart;
} else {
// Not a valid position keyword, treat whole thing as tag
tag = tagAndPosition;
}
} else {
tag = tagAndPosition;
}
if (!tag) {
throw new Error(`Invalid segment pattern: ${part}`);
}
segment.tag = tag;
if (namespace) {
segment.namespace = namespace;
}
// Step 4: Parse attributes
if (bracketContent) {
if (bracketContent.includes('=')) {
const eqIndex = bracketContent.indexOf('=');
segment.attrName = bracketContent.substring(0, eqIndex).trim();
segment.attrValue = bracketContent.substring(eqIndex + 1).trim();
} else {
segment.attrName = bracketContent.trim();
}
}
// Step 5: Parse position selector
if (positionMatch) {
const nthMatch = positionMatch.match(/^nth\((\d+)\)$/);
if (nthMatch) {
segment.position = 'nth';
segment.positionValue = parseInt(nthMatch[1], 10);
} else {
segment.position = positionMatch;
}
}
return segment;
}
/**
* Get the number of segments
* @returns {number}
*/
get length() {
return this.segments.length;
}
/**
* Check if expression contains deep wildcard
* @returns {boolean}
*/
hasDeepWildcard() {
return this._hasDeepWildcard;
}
/**
* Check if expression has attribute conditions
* @returns {boolean}
*/
hasAttributeCondition() {
return this._hasAttributeCondition;
}
/**
* Check if expression has position selectors
* @returns {boolean}
*/
hasPositionSelector() {
return this._hasPositionSelector;
}
/**
* Get string representation
* @returns {string}
*/
toString() {
return this.pattern;
}
}
/***/ }),
/***/ 8257:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ A: () => (/* binding */ Matcher)
/* harmony export */ });
/* unused harmony export MatcherView */
/**
* MatcherView - A lightweight read-only view over a Matcher's internal state.
*
* Created once by Matcher and reused across all callbacks. Holds a direct
* reference to the parent Matcher so it always reflects current parser state
* with zero copying or freezing overhead.
*
* Users receive this via {@link Matcher#readOnly} or directly from parser
* callbacks. It exposes all query and matching methods but has no mutation
* methods — misuse is caught at the TypeScript level rather than at runtime.
*
* @example
* const matcher = new Matcher();
* const view = matcher.readOnly();
*
* matcher.push("root", {});
* view.getCurrentTag(); // "root"
* view.getDepth(); // 1
*/
class MatcherView {
/**
* @param {Matcher} matcher - The parent Matcher instance to read from.
*/
constructor(matcher) {
this._matcher = matcher;
}
/**
* Get the path separator used by the parent matcher.
* @returns {string}
*/
get separator() {
return this._matcher.separator;
}
/**
* Get current tag name.
* @returns {string|undefined}
*/
getCurrentTag() {
const path = this._matcher.path;
return path.length > 0 ? path[path.length - 1].tag : undefined;
}
/**
* Get current namespace.
* @returns {string|undefined}
*/
getCurrentNamespace() {
const path = this._matcher.path;
return path.length > 0 ? path[path.length - 1].namespace : undefined;
}
/**
* Get current node's attribute value.
* @param {string} attrName
* @returns {*}
*/
getAttrValue(attrName) {
const path = this._matcher.path;
if (path.length === 0) return undefined;
return path[path.length - 1].values?.[attrName];
}
/**
* Check if current node has an attribute.
* @param {string} attrName
* @returns {boolean}
*/
hasAttr(attrName) {
const path = this._matcher.path;
if (path.length === 0) return false;
const current = path[path.length - 1];
return current.values !== undefined && attrName in current.values;
}
/**
* Get the value of a "kept" attribute from the nearest ancestor (or
* current node) that declared it via `push(tag, attrs, ns, { keep: [...] })`.
* @param {string} attrName
* @returns {*}
*/
getAnyParentAttr(attrName) {
return this._matcher.getAnyParentAttr(attrName);
}
/**
* Check whether any ancestor (or the current node) kept the given
* attribute via `push(tag, attrs, ns, { keep: [...] })`.
* @param {string} attrName
* @returns {boolean}
*/
hasAnyParentAttr(attrName) {
return this._matcher.hasAnyParentAttr(attrName);
}
/**
* Get current node's sibling position (child index in parent).
* @returns {number}
*/
getPosition() {
const path = this._matcher.path;
if (path.length === 0) return -1;
return path[path.length - 1].position ?? 0;
}
/**
* Get current node's repeat counter (occurrence count of this tag name).
* @returns {number}
*/
getCounter() {
const path = this._matcher.path;
if (path.length === 0) return -1;
return path[path.length - 1].counter ?? 0;
}
/**
* Get current node's sibling index (alias for getPosition).
* @returns {number}
* @deprecated Use getPosition() or getCounter() instead
*/
getIndex() {
return this.getPosition();
}
/**
* Get current path depth.
* @returns {number}
*/
getDepth() {
return this._matcher.path.length;
}
/**
* Get path as string.
* @param {string} [separator] - Optional separator (uses default if not provided)
* @param {boolean} [includeNamespace=true]
* @returns {string}
*/
toString(separator, includeNamespace = true) {
return this._matcher.toString(separator, includeNamespace);
}
/**
* Get path as array of tag names.
* @returns {string[]}
*/
toArray() {
return this._matcher.path.map(n => n.tag);
}
/**
* Match current path against an Expression.
* @param {Expression} expression
* @returns {boolean}
*/
matches(expression) {
return this._matcher.matches(expression);
}
/**
* Match any expression in the given set against the current path.
* @param {ExpressionSet} exprSet
* @returns {boolean}
*/
matchesAny(exprSet) {
return exprSet.matchesAny(this._matcher);
}
}
/**
* Matcher - Tracks current path in XML/JSON tree and matches against Expressions.
*
* The matcher maintains a stack of nodes representing the current path from root to
* current tag. It only stores attribute values for the current (top) node to minimize
* memory usage. Sibling tracking is used to auto-calculate position and counter.
*
* Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to
* user callbacks — it always reflects current state with no Proxy overhead.
*
* @example
* const matcher = new Matcher();
* matcher.push("root", {});
* matcher.push("users", {});
* matcher.push("user", { id: "123", type: "admin" });
*
* const expr = new Expression("root.users.user");
* matcher.matches(expr); // true
*/
class Matcher {
/**
* Create a new Matcher.
* @param {Object} [options={}]
* @param {string} [options.separator='.'] - Default path separator
*/
constructor(options = {}) {
this.separator = options.separator || '.';
this.path = [];
this.siblingStacks = [];
// Each path node: { tag, values, position, counter, namespace? }
// values only present for current (last) node
// Each siblingStacks entry: Map<tagName, count> tracking occurrences at each level
this._pathStringCache = null;
this._view = new MatcherView(this);
// Kept-attribute stack: only populated when push() is called with options.keep.
this._keptAttrs = [];
}
/**
* Push a new tag onto the path.
* @param {string} tagName
* @param {Object|null} [attrValues=null]
* @param {string|null} [namespace=null]
* @param {Object|null} [options=null]
* @param {string[]} [options.keep] - Names of attributes (from attrValues)
*/
push(tagName, attrValues = null, namespace = null, options = null) {
this._pathStringCache = null;
// Remove values from previous current node (now becoming ancestor)
if (this.path.length > 0) {
this.path[this.path.length - 1].values = undefined;
}
// Get or create sibling tracking for current level
const currentLevel = this.path.length;
let level = this.siblingStacks[currentLevel];
if (!level) {
// `counts` tells same-name siblings apart (the "counter" — nth <item>
// among other <item>s). `total` is every child seen at this level so
// far, kept as a running number instead of re-added from `counts` on
// every push — a parent with many differently-named children would
// otherwise cost more per child the more distinct names it has.
level = { counts: new Map(), total: 0 };
this.siblingStacks[currentLevel] = level;
}
// Create a unique key for sibling tracking that includes namespace
const siblingKey = namespace ? `${namespace}:${tagName}` : tagName;
// Calculate counter (how many times this tag appeared at this level)
const counter = level.counts.get(siblingKey) || 0;
// Position = total children at this level seen before this one.
const position = level.total;
// Update sibling count for this tag, and the level's running total.
level.counts.set(siblingKey, counter + 1);
level.total++;
// Create new node
const node = {
tag: tagName,
position: position,
counter: counter
};
if (namespace !== null && namespace !== undefined) {
node.namespace = namespace;
}
if (attrValues !== null && attrValues !== undefined) {
node.values = attrValues;
}
this.path.push(node);
// Depth of the node we just pushed (1-based, matches this.path.length)
const depth = this.path.length;
// Copy only the requested attributes into the kept-attrs stack. This is
// the one part of push() whose cost scales with input (O(keep.length))
// rather than being O(1) — by design, since the caller is explicitly
// opting in for specific attribute names. No options/keep => zero added
// cost beyond the two property reads below.
const keep = options !== null ? options.keep : null;
if (keep !== null && keep !== undefined && keep.length > 0 && attrValues) {
for (let i = 0; i < keep.length; i++) {
const name = keep[i];
if (attrValues[name] !== undefined) {
this._keptAttrs.push({ depth, name, value: attrValues[name] });
}
}
}
}
/**
* Pop the last tag from the path.
* @returns {Object|undefined} The popped node
*/
pop() {
if (this.path.length === 0) return undefined;
this._pathStringCache = null;
const node = this.path.pop();
if (this.siblingStacks.length > this.path.length + 1) {
this.siblingStacks.length = this.path.length + 1;
}
// Drop any kept attributes that belonged to the popped node (or deeper).
// _keptAttrs is depth-ordered (push only ever appends increasing depths),
// so this is a backward scan that stops at the first surviving entry —
// typically O(1) since kept attrs are rare by design.
const poppedDepth = this.path.length + 1;
while (
this._keptAttrs.length > 0 &&
this._keptAttrs[this._keptAttrs.length - 1].depth >= poppedDepth
) {
this._keptAttrs.pop();
}
return node;
}
/**
* Update current node's attribute values.
* Useful when attributes are parsed after push.
* @param {Object} attrValues
*/
updateCurrent(attrValues) {
if (this.path.length > 0) {
const current = this.path[this.path.length - 1];
if (attrValues !== null && attrValues !== undefined) {
current.values = attrValues;
}
}
}
/**
* Get current tag name.
* @returns {string|undefined}
*/
getCurrentTag() {
return this.path.length > 0 ? this.path[this.path.length - 1].tag : undefined;
}
/**
* Get current namespace.
* @returns {string|undefined}
*/
getCurrentNamespace() {
return this.path.length > 0 ? this.path[this.path.length - 1].namespace : undefined;
}
/**
* Get current node's attribute value.
* @param {string} attrName
* @returns {*}
*/
getAttrValue(attrName) {
if (this.path.length === 0) return undefined;
return this.path[this.path.length - 1].values?.[attrName];
}
/**
* Check if current node has an attribute.
* @param {string} attrName
* @returns {boolean}
*/
hasAttr(attrName) {
if (this.path.length === 0) return false;
const current = this.path[this.path.length - 1];
return current.values !== undefined && attrName in current.values;
}
/**
* Get the value of a "kept" attribute from the nearest ancestor (or
* current node) that declared it via `push(tag, attrs, ns, { keep: [...] })`.
* Unlike getAttrValue(), this works regardless of how deep the path has
* gone since the attribute was pushed — but only for attribute names that
* were explicitly marked with `keep` at push time. Cost is proportional to
* the number of currently-kept attributes (typically 0-3), not path depth.
* @param {string} attrName
* @returns {*} the value, or undefined if no ancestor kept this attribute
*/
getAnyParentAttr(attrName) {
const kept = this._keptAttrs;
for (let i = kept.length - 1; i >= 0; i--) {
if (kept[i].name === attrName) return kept[i].value;
}
return undefined;
}
/**
* Check whether any ancestor (or the current node) kept the given
* attribute via `push(tag, attrs, ns, { keep: [...] })`.
* @param {string} attrName
* @returns {boolean}
*/
hasAnyParentAttr(attrName) {
const kept = this._keptAttrs;
for (let i = kept.length - 1; i >= 0; i--) {
if (kept[i].name === attrName) return true;
}
return false;
}
/**
* Get current node's sibling position (child index in parent).
* @returns {number}
*/
getPosition() {
if (this.path.length === 0) return -1;
return this.path[this.path.length - 1].position ?? 0;
}
/**
* Get current node's repeat counter (occurrence count of this tag name).
* @returns {number}
*/
getCounter() {
if (this.path.length === 0) return -1;
return this.path[this.path.length - 1].counter ?? 0;
}
/**
* Get current node's sibling index (alias for getPosition).
* @returns {number}
* @deprecated Use getPosition() or getCounter() instead
*/
getIndex() {
return this.getPosition();
}
/**
* Get current path depth.
* @returns {number}
*/
getDepth() {
return this.path.length;
}
/**
* Get path as string.
* @param {string} [separator] - Optional separator (uses default if not provided)
* @param {boolean} [includeNamespace=true]
* @returns {string}
*/
toString(separator, includeNamespace = true) {
const sep = separator || this.separator;
const isDefault = (sep === this.separator && includeNamespace === true);
if (isDefault) {
if (this._pathStringCache !== null) {
return this._pathStringCache;
}
const result = this.path.map(n =>
(n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
).join(sep);
this._pathStringCache = result;
return result;
}
return this.path.map(n =>
(includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
).join(sep);
}
/**
* Get path as array of tag names.
* @returns {string[]}
*/
toArray() {
return this.path.map(n => n.tag);
}
/**
* Reset the path to empty.
*/
reset() {
this._pathStringCache = null;
this.path = [];
this.siblingStacks = [];
this._keptAttrs = [];
}
/**
* Match current path against an Expression.
* @param {Expression} expression
* @returns {boolean}
*/
matches(expression) {
const segments = expression.segments;
if (segments.length === 0) {
return false;
}
if (expression.hasDeepWildcard()) {
return this._matchWithDeepWildcard(segments);
}
return this._matchSimple(segments);
}
/**
* @private
*/
_matchSimple(segments) {
if (this.path.length !== segments.length) {
return false;
}
for (let i = 0; i < segments.length; i++) {
if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) {
return false;
}
}
return true;
}
/**
* @private
*/
_matchWithDeepWildcard(segments) {
let pathIdx = this.path.length - 1;
let segIdx = segments.length - 1;
while (segIdx >= 0 && pathIdx >= 0) {
const segment = segments[segIdx];
if (segment.type === 'deep-wildcard') {
segIdx--;
if (segIdx < 0) {
return true;
}
const nextSeg = segments[segIdx];
let found = false;
for (let i = pathIdx; i >= 0; i--) {
if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) {
pathIdx = i - 1;
segIdx--;
found = true;
break;
}
}
if (!found) {
return false;
}
} else {
if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) {
return false;
}
pathIdx--;
segIdx--;
}
}
return segIdx < 0;
}
/**
* @private
*/
_matchSegment(segment, node, isCurrentNode) {
if (segment.tag !== '*' && segment.tag !== node.tag) {
return false;
}
if (segment.namespace !== undefined) {
if (segment.namespace !== '*' && segment.namespace !== node.namespace) {
return false;
}
}
if (segment.attrName !== undefined) {
if (!isCurrentNode) {
return false;
}
if (!node.values || !(segment.attrName in node.values)) {
return false;
}
if (segment.attrValue !== undefined) {
if (String(node.values[segment.attrName]) !== String(segment.attrValue)) {
return false;
}
}
}
if (segment.position !== undefined) {
if (!isCurrentNode) {
return false;
}
const counter = node.counter ?? 0;
if (segment.position === 'first' && counter !== 0) {
return false;
} else if (segment.position === 'odd' && counter % 2 !== 1) {
return false;
} else if (segment.position === 'even' && counter % 2 !== 0) {
return false;
} else if (segment.position === 'nth' && counter !== segment.positionValue) {
return false;
}
}
return true;
}
/**
* Match any expression in the given set against the current path.
* @param {ExpressionSet} exprSet
* @returns {boolean}
*/
matchesAny(exprSet) {
return exprSet.matchesAny(this);
}
/**
* Create a snapshot of current state.
* @returns {Object}
*/
snapshot() {
return {
path: this.path.map(node => ({ ...node })),
siblingStacks: this.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level),
keptAttrs: this._keptAttrs.map(entry => ({ ...entry }))
};
}
/**
* Restore state from snapshot.
* @param {Object} snapshot
*/
restore(snapshot) {
this._pathStringCache = null;
this.path = snapshot.path.map(node => ({ ...node }));
this.siblingStacks = snapshot.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level);
this._keptAttrs = (snapshot.keptAttrs || []).map(entry => ({ ...entry }));
}
/**
* Return the read-only {@link MatcherView} for this matcher.
*
* The same instance is returned on every call — no allocation occurs.
* It always reflects the current parser state and is safe to pass to
* user callbacks without risk of accidental mutation.
*
* @returns {MatcherView}
*
* @example
* const view = matcher.readOnly();
* // pass view to callbacks — it stays in sync automatically
* view.matches(expr); // ✓
* view.getCurrentTag(); // ✓
* // view.push(...) // ✗ method does not exist — caught by TypeScript
*/
readOnly() {
return this._view;
}
}
/***/ }),
/***/ 4658:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ fG: () => (/* binding */ qName),
/* harmony export */ fH: () => (/* binding */ createValidator)
/* harmony export */ });
/* unused harmony exports name, ncName, nmToken, nmTokens, validate, validateAll, sanitize */
/**
* xml-naming
* Validates XML Name productions as defined in the XML 1.0 and 1.1 specifications.
* Covers: Name, NCName, QName, NMToken, NMTokens
*
* XML 1.0 spec: https://www.w3.org/TR/xml/#NT-Name
* XML 1.1 spec: https://www.w3.org/TR/xml11/#NT-NameStartChar
* XML NS spec: https://www.w3.org/TR/xml-names/#NT-NCName
*/
// ---------------------------------------------------------------------------
// Character class strings — XML 1.0
//
// NameStartChar ::= ":" | [A-Z] | "_" | [a-z]
// | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF]
// | [#x370-#x37D] | [#x37F-#x1FFF] <- split to exclude #x0487
// | [#x200C-#x200D]
// | [#x2070-#x218F] | [#x2C00-#x2FEF]
// | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD]
//
// NameChar ::= NameStartChar | "-" | "." | [0-9]
// | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
//
// Note: \u0487 (Combining Cyrillic Millions Sign) was added in Unicode 4.0,
// after XML 1.0 was defined against Unicode 2.0. It falls inside the range
// \u037F-\u1FFF but must be excluded. We split that range into
// \u037F-\u0486 and \u0488-\u1FFF to exclude it explicitly.
// ---------------------------------------------------------------------------
const nameStartChar10 =
':A-Za-z_' +
'\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF' +
'\u0370-\u037D' +
'\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487
'\u200C-\u200D' +
'\u2070-\u218F' +
'\u2C00-\u2FEF' +
'\u3001-\uD7FF' +
'\uF900-\uFDCF' +
'\uFDF0-\uFFFD';
const nameChar10 =
nameStartChar10 +
'\\-\\.\\d' +
'\u00B7' +
'\u0300-\u036F' +
'\u203F-\u2040';
// ---------------------------------------------------------------------------
// Character class strings — XML 1.1
//
// Differences from XML 1.0:
//
// NameStartChar:
// 1.0 has split ranges: \u00C0-\u00D6, \u00D8-\u00F6, \u00F8-\u02FF
// 1.1 merges them into: \u00C0-\u02FF
// (\u00D7 x and \u00F7 / are division symbols, excluded in both versions)
//
// 1.0 tops out at \uFFFD (BMP only)
// 1.1 adds \u{10000}-\u{EFFFF} (supplementary planes)
// These require the /u flag on the RegExp — see buildRegexes below.
//
// NameChar:
// 1.1 adds \u0487 (Combining Cyrillic Millions Sign, added in Unicode 4.0)
// ---------------------------------------------------------------------------
const nameStartChar11 =
':A-Za-z_' +
'\u00C0-\u02FF' + // merged — 1.0 had three split ranges here
'\u0370-\u037D' +
'\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 (combining mark, never a NameStartChar)
'\u200C-\u200D' +
'\u2070-\u218F' +
'\u2C00-\u2FEF' +
'\u3001-\uD7FF' +
'\uF900-\uFDCF' +
'\uFDF0-\uFFFD' +
'\u{10000}-\u{EFFFF}'; // supplementary planes — REQUIRES /u flag on RegExp
const nameChar11 =
nameStartChar11 +
'\\-\\.\\d' +
'\u00B7' +
'\u0300-\u036F' +
'\u0487' + // Combining Cyrillic Millions Sign — valid in 1.1, not 1.0
'\u203F-\u2040';
// ---------------------------------------------------------------------------
// Regex builders
//
// XML 1.0 regexes: no flags — BMP only, standard JS regex behaviour.
// XML 1.1 regexes: /u flag — required for \u{10000}-\u{EFFFF} to match actual
// supplementary code points rather than lone surrogates (which are illegal XML).
// ---------------------------------------------------------------------------
const buildRegexes = (startChar, char, flags = '') => {
const ncStart = startChar.replace(':', '');
const ncChar = char.replace(':', '');
const ncNamePat = `[${ncStart}][${ncChar}]*`;
return {
name: new RegExp(`^[${startChar}][${char}]*$`, flags),
ncName: new RegExp(`^${ncNamePat}$`, flags),
qName: new RegExp(`^${ncNamePat}(?::${ncNamePat})?$`, flags),
nmToken: new RegExp(`^[${char}]+$`, flags),
nmTokens: new RegExp(`^[${char}]+(?:\\s+[${char}]+)*$`, flags),
};
};
const regexes10 = buildRegexes(nameStartChar10, nameChar10); // no /u — BMP only
const regexes11 = buildRegexes(nameStartChar11, nameChar11, 'u'); // /u — enables \u{10000}-\u{EFFFF}
// ---------------------------------------------------------------------------
// ASCII-only fast path (opt-in, off by default)
//
// The XML 1.0 vs 1.1 NameStartChar/NameChar productions differ *only* in
// their non-ASCII ranges (merged vs split Latin-1 ranges, \u0487, and
// supplementary planes). Restricted to ASCII, both versions collapse to the
// same character classes, so a single regex pair covers both xmlVersion
// values — no /u flag needed.
//
// Rationale: unicode-aware regexes (the /u flag, required for XML 1.1's
// supplementary-plane range) are measurably slower in V8 than plain
// non-unicode regexes on the same input, even when the input is pure ASCII.
// For the common case — HTML/SVG ids, XML tags — names are ASCII, so callers
// who know this can opt in to skip the unicode-aware matching path entirely.
// This is a real but *conditional* win: mainly for XML 1.1 input (avoids /u),
// or at scale where the larger unicode character classes add engine
// overhead. It also changes behaviour (rejects legitimate non-ASCII XML
// 1.0/1.1 names), so it must never be silently enabled — hence off by
// default.
// ---------------------------------------------------------------------------
const nameStartCharAscii = ':A-Za-z_';
const nameCharAscii = nameStartCharAscii + '\\-\\.\\d';
const regexesAscii = buildRegexes(nameStartCharAscii, nameCharAscii); // no /u — ASCII only
const getRegexes = (xmlVersion = '1.0', asciiOnly = false) => {
if (asciiOnly) return regexesAscii;
return xmlVersion === '1.1' ? regexes11 : regexes10;
};
// ---------------------------------------------------------------------------
// Boolean validators
// ---------------------------------------------------------------------------
/**
* Returns true if the string is a valid XML Name.
* Colons are allowed anywhere (Name production).
* Used for: DOCTYPE entity names, notation names, DTD element declarations.
*
* @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts]
* asciiOnly: skip unicode-aware matching, ASCII names only (default false).
*/
const name = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) =>
getRegexes(xmlVersion, asciiOnly).name.test(str);
/**
* Returns true if the string is a valid NCName (Non-Colonized Name).
* Colons are not permitted.
* Used for: namespace prefixes, local names, SVG id attributes.
*
* @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts]
* asciiOnly: skip unicode-aware matching, ASCII names only (default false).
*/
const ncName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) =>
getRegexes(xmlVersion, asciiOnly).ncName.test(str);
/**
* Returns true if the string is a valid QName (Qualified Name).
* Allows exactly one colon as a prefix separator: prefix:localName.
* Used for: element and attribute names in namespace-aware XML/SVG.
*
* @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts]
* asciiOnly: skip unicode-aware matching, ASCII names only (default false).
*/
const qName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) =>
getRegexes(xmlVersion, asciiOnly).qName.test(str);
/**
* Returns true if the string is a valid NMToken.
* Like Name but no restriction on the first character.
* Used for: DTD NMTOKEN attribute values.
*
* @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts]
* asciiOnly: skip unicode-aware matching, ASCII names only (default false).
*/
const nmToken = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) =>
getRegexes(xmlVersion, asciiOnly).nmToken.test(str);
/**
* Returns true if the string is a valid NMTokens value.
* A whitespace-separated list of NMToken values.
* Used for: DTD NMTOKENS attribute values.
*
* @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts]
* asciiOnly: skip unicode-aware matching, ASCII names only (default false).
*/
const nmTokens = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) =>
getRegexes(xmlVersion, asciiOnly).nmTokens.test(str);
// ---------------------------------------------------------------------------
// Memoized validator factory
//
// Real documents reuse a small vocabulary of tag/attribute names across many
// siblings (e.g. `id`, `class`, `href` repeated across hundreds of elements).
// The plain boolean validators above re-run the regex on every call
// regardless of repeats. `createValidator` returns a closure with a private
// string -> boolean cache, so repeated names after the first become O(1)
// lookups instead of regex tests.
//
// - opts (xmlVersion, asciiOnly) are fixed at creation time, so the regex is
// resolved once, not on every call.
// - The cache is private to the returned closure — no shared/global state,
// no cross-caller pollution.
// - `maxCacheSize` bounds memory: once the cache reaches this many entries,
// it stops accepting new ones (existing entries keep serving hits; new
// misses just fall through to the regex, uncached). This avoids unbounded
// growth against adversarial/high-cardinality input (e.g. validating
// attacker-supplied names with no repeats) without the cost/complexity of
// a full LRU, and without the perf cliff of reset-and-refill thrashing.
// - Call `.reset()` on the returned function to clear the cache manually
// (e.g. between unrelated parse calls).
// ---------------------------------------------------------------------------
const PRODUCTIONS = ['name', 'ncName', 'qName', 'nmToken', 'nmTokens'];
/**
* Returns a memoized boolean validator function for a single production,
* with opts fixed at creation time.
*
* @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production
* @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean, maxCacheSize?: number }} [opts]
* maxCacheSize: max number of distinct strings to cache (default 2048).
* Once reached, new strings are validated but not cached; existing cached
* entries keep being served.
* @returns {((str: string) => boolean) & { reset: () => void }}
*/
const createValidator = (production, { xmlVersion = '1.0', asciiOnly = false, maxCacheSize = 2048 } = {}) => {
if (!PRODUCTIONS.includes(production)) {
throw new TypeError(
`Unknown production "${production}". Must be one of: ${PRODUCTIONS.join(', ')}`
);
}
const regex = getRegexes(xmlVersion, asciiOnly)[production];
let cache = new Map();
const validator = (str) => {
const cached = cache.get(str);
if (cached !== undefined) return cached;
const result = regex.test(str);
if (cache.size < maxCacheSize) cache.set(str, result);
return result;
};
validator.reset = () => { cache = new Map(); };
return validator;
};
// ---------------------------------------------------------------------------
// Diagnostic validator
// ---------------------------------------------------------------------------
/**
* Validates a string against a named production and returns a detailed result.
*
* @param {string} str
* @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production
* @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts]
* @returns {{ valid: boolean, production: string, input: string, reason?: string, position?: number }}
*/
const validate = (str, production, { xmlVersion = '1.0', asciiOnly = false } = {}) => {
if (!PRODUCTIONS.includes(production)) {
throw new TypeError(
`Unknown production "${production}". Must be one of: ${PRODUCTIONS.join(', ')}`
);
}
const validators = { name, ncName, qName, nmToken, nmTokens };
const isValid = validators[production](str, { xmlVersion, asciiOnly });
if (isValid) return { valid: true, production, input: str };
let reason = 'Does not match the production rules';
let position;
// Diagnostic fallback char checks must mirror the same character set the
// boolean validator above used, or the reported reason/position could
// contradict the `valid: false` result (e.g. flagging a char as illegal
// that the unicode-aware check would have accepted).
const startCharPattern = asciiOnly ? /^[:A-Za-z_]/ : /^[:A-Za-z_\u00C0-\uFFFD]/;
const namePattern = asciiOnly ? /[\w\-\\.:]/ : /[\w\-\\.:\u00B7\u00C0-\uFFFD]/;
if (str.length === 0) {
reason = 'Input is empty';
} else if (production === 'ncName' && str.includes(':')) {
position = str.indexOf(':');
reason = 'Colon is not allowed in NCName';
} else if (production === 'qName' && str.startsWith(':')) {
reason = 'QName cannot start with a colon';
position = 0;
} else if (production === 'qName' && str.endsWith(':')) {
reason = 'QName cannot end with a colon';
position = str.length - 1;
} else if (production === 'qName' && (str.match(/:/g) || []).length > 1) {
reason = 'QName can have at most one colon';
position = str.lastIndexOf(':');
} else if (
['name', 'ncName', 'qName'].includes(production) &&
!startCharPattern.test(str[0])
) {
reason = `First character "${str[0]}" is not a valid NameStartChar`;
position = 0;
} else {
for (let i = 0; i < str.length; i++) {
if (!namePattern.test(str[i])) {
reason = `Character "${str[i]}" at position ${i} is not a valid NameChar`;
position = i;
break;
}
}
}
return { valid: false, production, input: str, reason, position };
};
// ---------------------------------------------------------------------------
// Batch validator
// ---------------------------------------------------------------------------
/**
* Validates an array of strings against a named production.
*
* @param {string[]} strings
* @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production
* @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts]
* @returns {Array<{ valid: boolean, production: string, input: string, reason?: string, position?: number }>}
*/
const validateAll = (strings, production, opts = {}) =>
strings.map(str => validate(str, production, opts));
// ---------------------------------------------------------------------------
// Sanitizer
// ---------------------------------------------------------------------------
/**
* Transforms an invalid string into the nearest valid XML name for the given production.
*
* @param {string} str
* @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production
* @param {{ replacement?: string, asciiOnly?: boolean }} [opts]
* asciiOnly: also replace any non-ASCII character, not just XML-illegal
* ones (default false).
* @returns {string}
*/
const sanitize = (str, production = 'name', { replacement = '_', asciiOnly = false } = {}) => {
if (!str) return replacement;
let result = str;
// Strip colons for NCName
if (production === 'ncName') {
result = result.replace(/:/g, '');
}
// Replace illegal characters
const allowedCharPattern = asciiOnly ? /[^\w\-\.:]/g : /[^\w\-\.:\u00B7\u00C0-\uFFFD]/g;
result = result.replace(allowedCharPattern, replacement);
// Fix invalid start character for Name / NCName / QName
if (production !== 'nmToken' && production !== 'nmTokens') {
if (/^[\-\.\d]/.test(result)) {
result = replacement + result;
}
}
return result || replacement;
};
/***/ })
};