mirror of
https://github.com/actions/setup-java.git
synced 2026-08-25 10:03:13 +01:00
* Combine Dependabot npm updates Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Rebuild action bundles Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a5d254ad-e29c-4477-ae66-cba6e36d7e03 * Update Licensed cache for XML dependencies Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a5d254ad-e29c-4477-ae66-cba6e36d7e03 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a5d254ad-e29c-4477-ae66-cba6e36d7e03
7165 lines
222 KiB
JavaScript
7165 lines
222 KiB
JavaScript
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; <'); // '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 javascript:, 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. <script or <script)',
|
||
// Entities include optional trailing semicolon: < or < (both valid in HTML5)
|
||
pattern: /(?:�*3[Cc];?|�*60;?|<)\s*script/i,
|
||
},
|
||
{
|
||
id: 'html-entity-obfuscated-javascript',
|
||
description: 'HTML-entity-encoded javascript: (partial — catches common j or j for "j")',
|
||
pattern: /(?:�*6[Aa];?|�*106;?)\s*(?:�*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 '
'")
|
||
} 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;
|
||
};
|
||
|
||
/***/ })
|
||
|
||
};
|