@btc-embedded/cdk-extensions 0.23.4 → 0.23.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.jsii +5 -5
  2. package/CHANGELOG.md +7 -0
  3. package/assets/cli/catnip.js +154 -166
  4. package/lib/constructs/EventPipe.js +1 -1
  5. package/lib/constructs/ExportedService.js +1 -1
  6. package/lib/constructs/S3Bucket.js +1 -1
  7. package/lib/constructs/SecureRestApi.js +1 -1
  8. package/lib/constructs/SecureRestApiV2.js +1 -1
  9. package/lib/constructs/api-keys/ApiKeyClientAuthorization.js +1 -1
  10. package/lib/constructs/api-keys/ApiKeyManagement.js +1 -1
  11. package/lib/constructs/api-keys/ApiKeyPreTokenHandler.js +1 -1
  12. package/lib/constructs/api-keys/ApiKeyStore.js +1 -1
  13. package/lib/extensions/ApiGatewayExtension.js +1 -1
  14. package/lib/extensions/ApplicationContainer.js +1 -1
  15. package/lib/extensions/ApplicationLoadBalancerExtension.js +1 -1
  16. package/lib/extensions/ApplicationLoadBalancerExtensionV2.js +1 -1
  17. package/lib/extensions/CloudMapExtension.js +1 -1
  18. package/lib/extensions/DeactivatableServiceExtension.js +1 -1
  19. package/lib/extensions/DeploymentConfigExtension.js +1 -1
  20. package/lib/extensions/DocumentDbAccessExtension.js +1 -1
  21. package/lib/extensions/DomainEventMessagingExtension.js +1 -1
  22. package/lib/extensions/EfsMountExtension.js +1 -1
  23. package/lib/extensions/ExtraContainerExtension.js +1 -1
  24. package/lib/extensions/HTTPApiExtension.js +1 -1
  25. package/lib/extensions/LogExtension.js +1 -1
  26. package/lib/extensions/ModifyContainerDefinitionExtension.js +1 -1
  27. package/lib/extensions/ModifyTaskDefinitionExtension.js +1 -1
  28. package/lib/extensions/OpenIdExtension.js +1 -1
  29. package/lib/extensions/OpenTelemetryExtension.js +1 -1
  30. package/lib/extensions/PostgresDbAccessExtension.js +1 -1
  31. package/lib/extensions/SharedVolumeExtension.js +1 -1
  32. package/lib/extensions/TcpKeepAliveExtension.js +1 -1
  33. package/lib/platform/ApiGateway.js +1 -1
  34. package/lib/platform/ApiGatewayVpcLink.js +2 -2
  35. package/lib/platform/ApplicationLoadBalancer.js +1 -1
  36. package/lib/platform/ApplicationLoadBalancerV2.d.ts +1 -0
  37. package/lib/platform/ApplicationLoadBalancerV2.js +10 -3
  38. package/lib/platform/AuroraPostgresDB.js +2 -2
  39. package/lib/platform/BTCLogGroup.js +1 -1
  40. package/lib/platform/CognitoUserPool.js +2 -2
  41. package/lib/platform/DefaultUserPoolClients.js +1 -1
  42. package/lib/platform/DocumentDB.js +2 -2
  43. package/lib/platform/EcsCluster.js +1 -1
  44. package/lib/platform/EfsFileSystem.js +1 -1
  45. package/lib/platform/HostedZone.js +1 -1
  46. package/lib/platform/PrivateDnsNamespace.js +1 -1
  47. package/lib/platform/ResourceServer.js +1 -1
  48. package/lib/platform/Vpc.js +1 -1
  49. package/lib/platform/VpcV2.js +1 -1
  50. package/lib/stacks/ApplicationStack.js +1 -1
  51. package/lib/utils/BasePlatformStackResolver.js +1 -1
  52. package/lib/utils/StackParameter.js +1 -1
  53. package/node_modules/@nodable/entities/README.md +41 -0
  54. package/node_modules/@nodable/entities/package.json +54 -0
  55. package/node_modules/@nodable/entities/src/EntityDecoder.js +543 -0
  56. package/node_modules/@nodable/entities/src/EntityEncoder.js +194 -0
  57. package/node_modules/@nodable/entities/src/entities.js +1177 -0
  58. package/node_modules/@nodable/entities/src/entityTries.js +49 -0
  59. package/node_modules/@nodable/entities/src/index.d.ts +264 -0
  60. package/node_modules/@nodable/entities/src/index.js +29 -0
  61. package/node_modules/fast-xml-builder/CHANGELOG.md +40 -0
  62. package/node_modules/fast-xml-builder/LICENSE +21 -0
  63. package/node_modules/fast-xml-builder/README.md +74 -0
  64. package/node_modules/fast-xml-builder/lib/fxb.cjs +1 -0
  65. package/node_modules/fast-xml-builder/lib/fxb.d.cts +270 -0
  66. package/node_modules/fast-xml-builder/lib/fxb.min.js +2 -0
  67. package/node_modules/fast-xml-builder/lib/fxb.min.js.map +1 -0
  68. package/node_modules/fast-xml-builder/package.json +81 -0
  69. package/node_modules/fast-xml-builder/src/fxb.d.ts +270 -0
  70. package/node_modules/fast-xml-builder/src/fxb.js +599 -0
  71. package/node_modules/fast-xml-builder/src/ignoreAttributes.js +18 -0
  72. package/node_modules/fast-xml-builder/src/orderedJs2Xml.js +359 -0
  73. package/node_modules/fast-xml-builder/src/util.js +16 -0
  74. package/node_modules/fast-xml-parser/CHANGELOG.md +165 -0
  75. package/node_modules/fast-xml-parser/README.md +21 -44
  76. package/node_modules/fast-xml-parser/lib/fxbuilder.min.js +1 -1
  77. package/node_modules/fast-xml-parser/lib/fxbuilder.min.js.map +1 -1
  78. package/node_modules/fast-xml-parser/lib/fxp.cjs +1 -1
  79. package/node_modules/fast-xml-parser/lib/fxp.d.cts +343 -31
  80. package/node_modules/fast-xml-parser/lib/fxp.min.js +1 -1
  81. package/node_modules/fast-xml-parser/lib/fxp.min.js.map +1 -1
  82. package/node_modules/fast-xml-parser/lib/fxparser.min.js +1 -1
  83. package/node_modules/fast-xml-parser/lib/fxparser.min.js.map +1 -1
  84. package/node_modules/fast-xml-parser/lib/fxvalidator.min.js +1 -1
  85. package/node_modules/fast-xml-parser/lib/fxvalidator.min.js.map +1 -1
  86. package/node_modules/fast-xml-parser/package.json +13 -8
  87. package/node_modules/fast-xml-parser/src/fxp.d.ts +335 -30
  88. package/node_modules/fast-xml-parser/src/fxp.js +1 -1
  89. package/node_modules/fast-xml-parser/src/util.js +18 -25
  90. package/node_modules/fast-xml-parser/src/v6/EntitiesParser.js +89 -87
  91. package/node_modules/fast-xml-parser/src/v6/OptionsBuilder.js +10 -10
  92. package/node_modules/fast-xml-parser/src/v6/OutputBuilders/BaseOutputBuilder.js +23 -23
  93. package/node_modules/fast-xml-parser/src/v6/OutputBuilders/JsArrBuilder.js +29 -29
  94. package/node_modules/fast-xml-parser/src/v6/OutputBuilders/JsMinArrBuilder.js +1 -1
  95. package/node_modules/fast-xml-parser/src/v6/OutputBuilders/JsObjBuilder.js +39 -39
  96. package/node_modules/fast-xml-parser/src/v6/OutputBuilders/ParserOptionsBuilder.js +21 -21
  97. package/node_modules/fast-xml-parser/src/v6/XMLParser.js +22 -22
  98. package/node_modules/fast-xml-parser/src/v6/valueParsers/EntitiesParser.js +85 -85
  99. package/node_modules/fast-xml-parser/src/validator.js +34 -34
  100. package/node_modules/fast-xml-parser/src/xmlbuilder/json2xml.js +5 -284
  101. package/node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js +335 -293
  102. package/node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js +160 -43
  103. package/node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js +540 -308
  104. package/node_modules/fast-xml-parser/src/xmlparser/XMLParser.js +26 -26
  105. package/node_modules/fast-xml-parser/src/xmlparser/node2json.js +99 -41
  106. package/node_modules/fast-xml-parser/src/xmlparser/xmlNode.js +10 -10
  107. package/node_modules/path-expression-matcher/LICENSE +21 -0
  108. package/node_modules/path-expression-matcher/README.md +872 -0
  109. package/node_modules/path-expression-matcher/lib/pem.cjs +1 -0
  110. package/node_modules/path-expression-matcher/lib/pem.d.cts +634 -0
  111. package/node_modules/path-expression-matcher/lib/pem.min.js +2 -0
  112. package/node_modules/path-expression-matcher/lib/pem.min.js.map +1 -0
  113. package/node_modules/path-expression-matcher/package.json +78 -0
  114. package/node_modules/path-expression-matcher/src/Expression.js +232 -0
  115. package/node_modules/path-expression-matcher/src/ExpressionSet.js +209 -0
  116. package/node_modules/path-expression-matcher/src/Matcher.js +570 -0
  117. package/node_modules/path-expression-matcher/src/index.d.ts +523 -0
  118. package/node_modules/path-expression-matcher/src/index.js +29 -0
  119. package/node_modules/strnum/CHANGELOG.md +12 -2
  120. package/node_modules/strnum/README.md +1 -0
  121. package/node_modules/strnum/package.json +5 -4
  122. package/node_modules/strnum/strnum.js +99 -65
  123. package/node_modules/xml-naming/README.md +189 -0
  124. package/node_modules/xml-naming/package.json +54 -0
  125. package/node_modules/xml-naming/src/index.d.ts +74 -0
  126. package/node_modules/xml-naming/src/index.js +270 -0
  127. package/package.json +3 -2
  128. package/renovate.json5 +1 -0
  129. package/node_modules/fast-xml-parser/src/xmlbuilder/orderedJs2Xml.js +0 -134
  130. package/node_modules/strnum/.github/SECURITY.md +0 -5
  131. package/node_modules/strnum/.vscode/launch.json +0 -25
  132. package/node_modules/strnum/algo.stflow +0 -84
  133. package/node_modules/strnum/strnum.test.js +0 -173
  134. package/node_modules/strnum/test.js +0 -9
  135. /package/node_modules/{fast-xml-parser/src/xmlbuilder → fast-xml-builder/src}/prettifyJs2Xml.js +0 -0
@@ -1,11 +1,14 @@
1
1
  'use strict';
2
2
  ///@ts-check
3
3
 
4
- import {getAllMatches, isExist} from '../util.js';
4
+ import { getAllMatches, isExist, DANGEROUS_PROPERTY_NAMES, criticalProperties } from '../util.js';
5
5
  import xmlNode from './xmlNode.js';
6
- import readDocType from './DocTypeReader.js';
6
+ import DocTypeReader from './DocTypeReader.js';
7
7
  import toNumber from "strnum";
8
8
  import getIgnoreAttributesFn from "../ignoreAttributes.js";
9
+ import { Expression, Matcher } from 'path-expression-matcher';
10
+ import { ExpressionSet } from 'path-expression-matcher';
11
+ import { EntityDecoder, XML, CURRENCY, COMMON_HTML } from '@nodable/entities';
9
12
 
10
13
  // const regx =
11
14
  // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
@@ -14,37 +17,62 @@ import getIgnoreAttributesFn from "../ignoreAttributes.js";
14
17
  //const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
15
18
  //const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");
16
19
 
17
- export default class OrderedObjParser{
18
- constructor(options){
20
+ // Helper functions for attribute and namespace handling
21
+
22
+ /**
23
+ * Extract raw attributes (without prefix) from prefixed attribute map
24
+ * @param {object} prefixedAttrs - Attributes with prefix from buildAttributesMap
25
+ * @param {object} options - Parser options containing attributeNamePrefix
26
+ * @returns {object} Raw attributes for matcher
27
+ */
28
+ function extractRawAttributes(prefixedAttrs, options) {
29
+ if (!prefixedAttrs) return {};
30
+
31
+ // Handle attributesGroupName option
32
+ const attrs = options.attributesGroupName
33
+ ? prefixedAttrs[options.attributesGroupName]
34
+ : prefixedAttrs;
35
+
36
+ if (!attrs) return {};
37
+
38
+ const rawAttrs = {};
39
+ for (const key in attrs) {
40
+ // Remove the attribute prefix to get raw name
41
+ if (key.startsWith(options.attributeNamePrefix)) {
42
+ const rawName = key.substring(options.attributeNamePrefix.length);
43
+ rawAttrs[rawName] = attrs[key];
44
+ } else {
45
+ // Attribute without prefix (shouldn't normally happen, but be safe)
46
+ rawAttrs[key] = attrs[key];
47
+ }
48
+ }
49
+ return rawAttrs;
50
+ }
51
+
52
+ /**
53
+ * Extract namespace from raw tag name
54
+ * @param {string} rawTagName - Tag name possibly with namespace (e.g., "soap:Envelope")
55
+ * @returns {string|undefined} Namespace or undefined
56
+ */
57
+ function extractNamespace(rawTagName) {
58
+ if (!rawTagName || typeof rawTagName !== 'string') return undefined;
59
+
60
+ const colonIndex = rawTagName.indexOf(':');
61
+ if (colonIndex !== -1 && colonIndex > 0) {
62
+ const ns = rawTagName.substring(0, colonIndex);
63
+ // Don't treat xmlns as a namespace
64
+ if (ns !== 'xmlns') {
65
+ return ns;
66
+ }
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ export default class OrderedObjParser {
72
+ constructor(options, externalEntities) {
19
73
  this.options = options;
20
74
  this.currentNode = null;
21
75
  this.tagsNodeStack = [];
22
- this.docTypeEntities = {};
23
- this.lastEntities = {
24
- "apos" : { regex: /&(apos|#39|#x27);/g, val : "'"},
25
- "gt" : { regex: /&(gt|#62|#x3E);/g, val : ">"},
26
- "lt" : { regex: /&(lt|#60|#x3C);/g, val : "<"},
27
- "quot" : { regex: /&(quot|#34|#x22);/g, val : "\""},
28
- };
29
- this.ampEntity = { regex: /&(amp|#38|#x26);/g, val : "&"};
30
- this.htmlEntities = {
31
- "space": { regex: /&(nbsp|#160);/g, val: " " },
32
- // "lt" : { regex: /&(lt|#60);/g, val: "<" },
33
- // "gt" : { regex: /&(gt|#62);/g, val: ">" },
34
- // "amp" : { regex: /&(amp|#38);/g, val: "&" },
35
- // "quot" : { regex: /&(quot|#34);/g, val: "\"" },
36
- // "apos" : { regex: /&(apos|#39);/g, val: "'" },
37
- "cent" : { regex: /&(cent|#162);/g, val: "¢" },
38
- "pound" : { regex: /&(pound|#163);/g, val: "£" },
39
- "yen" : { regex: /&(yen|#165);/g, val: "¥" },
40
- "euro" : { regex: /&(euro|#8364);/g, val: "€" },
41
- "copyright" : { regex: /&(copy|#169);/g, val: "©" },
42
- "reg" : { regex: /&(reg|#174);/g, val: "®" },
43
- "inr" : { regex: /&(inr|#8377);/g, val: "₹" },
44
- "num_dec": { regex: /&#([0-9]{1,7});/g, val : (_, str) => String.fromCodePoint(Number.parseInt(str, 10)) },
45
- "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val : (_, str) => String.fromCodePoint(Number.parseInt(str, 16)) },
46
- };
47
- this.addExternalEntities = addExternalEntities;
48
76
  this.parseXml = parseXml;
49
77
  this.parseTextData = parseTextData;
50
78
  this.resolveNameSpace = resolveNameSpace;
@@ -55,52 +83,88 @@ export default class OrderedObjParser{
55
83
  this.saveTextToParentTag = saveTextToParentTag;
56
84
  this.addChild = addChild;
57
85
  this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)
58
- }
59
-
60
- }
86
+ this.entityExpansionCount = 0;
87
+ this.currentExpandedLength = 0;
88
+ let namedEntities = { ...XML };
89
+ if (this.options.entityDecoder) {
90
+ this.entityDecoder = this.options.entityDecoder
91
+ } else {
92
+ if (typeof this.options.htmlEntities === "object") namedEntities = this.options.htmlEntities;
93
+ else if (this.options.htmlEntities === true) namedEntities = { ...COMMON_HTML, ...CURRENCY };
94
+ this.entityDecoder = new EntityDecoder({
95
+ namedEntities: { ...namedEntities, ...externalEntities },
96
+ numericAllowed: this.options.htmlEntities,
97
+ limit: {
98
+ maxTotalExpansions: this.options.processEntities.maxTotalExpansions,
99
+ maxExpandedLength: this.options.processEntities.maxExpandedLength,
100
+ applyLimitsTo: this.options.processEntities.appliesTo,
101
+ }
102
+ //postCheck: resolved => resolved
103
+ });
104
+ }
61
105
 
62
- function addExternalEntities(externalEntities){
63
- const entKeys = Object.keys(externalEntities);
64
- for (let i = 0; i < entKeys.length; i++) {
65
- const ent = entKeys[i];
66
- this.lastEntities[ent] = {
67
- regex: new RegExp("&"+ent+";","g"),
68
- val : externalEntities[ent]
106
+ // Initialize path matcher for path-expression-matcher
107
+ this.matcher = new Matcher();
108
+ this.readonlyMatcher = this.matcher.readOnly();
109
+
110
+ // Flag to track if current node is a stop node (optimization)
111
+ this.isCurrentNodeStopNode = false;
112
+
113
+ // Pre-compile stopNodes expressions
114
+ this.stopNodeExpressionsSet = new ExpressionSet();
115
+ const stopNodesOpts = this.options.stopNodes;
116
+ if (stopNodesOpts && stopNodesOpts.length > 0) {
117
+ for (let i = 0; i < stopNodesOpts.length; i++) {
118
+ const stopNodeExp = stopNodesOpts[i];
119
+ if (typeof stopNodeExp === 'string') {
120
+ // Convert string to Expression object
121
+ this.stopNodeExpressionsSet.add(new Expression(stopNodeExp));
122
+ } else if (stopNodeExp instanceof Expression) {
123
+ // Already an Expression object
124
+ this.stopNodeExpressionsSet.add(stopNodeExp);
125
+ }
126
+ }
127
+ this.stopNodeExpressionsSet.seal();
69
128
  }
70
129
  }
130
+
71
131
  }
72
132
 
133
+
73
134
  /**
74
135
  * @param {string} val
75
136
  * @param {string} tagName
76
- * @param {string} jPath
137
+ * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
77
138
  * @param {boolean} dontTrim
78
139
  * @param {boolean} hasAttributes
79
140
  * @param {boolean} isLeafNode
80
141
  * @param {boolean} escapeEntities
81
142
  */
82
143
  function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {
144
+ const options = this.options;
83
145
  if (val !== undefined) {
84
- if (this.options.trimValues && !dontTrim) {
146
+ if (options.trimValues && !dontTrim) {
85
147
  val = val.trim();
86
148
  }
87
- if(val.length > 0){
88
- if(!escapeEntities) val = this.replaceEntitiesValue(val);
89
-
90
- const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);
91
- if(newval === null || newval === undefined){
149
+ if (val.length > 0) {
150
+ if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
151
+
152
+ // Pass jPath string or matcher based on options.jPath setting
153
+ const jPathOrMatcher = options.jPath ? jPath.toString() : jPath;
154
+ const newval = options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode);
155
+ if (newval === null || newval === undefined) {
92
156
  //don't parse
93
157
  return val;
94
- }else if(typeof newval !== typeof val || newval !== val){
158
+ } else if (typeof newval !== typeof val || newval !== val) {
95
159
  //overwrite
96
160
  return newval;
97
- }else if(this.options.trimValues){
98
- return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
99
- }else{
161
+ } else if (options.trimValues) {
162
+ return parseValue(val, options.parseTagValue, options.numberParseOptions);
163
+ } else {
100
164
  const trimmedVal = val.trim();
101
- if(trimmedVal === val){
102
- return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
103
- }else{
165
+ if (trimmedVal === val) {
166
+ return parseValue(val, options.parseTagValue, options.numberParseOptions);
167
+ } else {
104
168
  return val;
105
169
  }
106
170
  }
@@ -126,342 +190,460 @@ function resolveNameSpace(tagname) {
126
190
  //const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm");
127
191
  const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
128
192
 
129
- function buildAttributesMap(attrStr, jPath, tagName) {
130
- if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') {
193
+ function buildAttributesMap(attrStr, jPath, tagName, force = false) {
194
+ const options = this.options;
195
+ if (force === true || (options.ignoreAttributes !== true && typeof attrStr === 'string')) {
131
196
  // attrStr = attrStr.replace(/\r?\n/g, ' ');
132
197
  //attrStr = attrStr || attrStr.trim();
133
198
 
134
199
  const matches = getAllMatches(attrStr, attrsRegx);
135
200
  const len = matches.length; //don't make it inline
136
201
  const attrs = {};
202
+
203
+ // Pre-process values once: trim + entity replacement
204
+ // Reused in both matcher update and second pass
205
+ const processedVals = new Array(len);
206
+ let hasRawAttrs = false;
207
+ const rawAttrsForMatcher = {};
208
+
137
209
  for (let i = 0; i < len; i++) {
138
210
  const attrName = this.resolveNameSpace(matches[i][1]);
139
- if (this.ignoreAttributesFn(attrName, jPath)) {
140
- continue
211
+ const oldVal = matches[i][4];
212
+
213
+ if (attrName.length && oldVal !== undefined) {
214
+ let val = oldVal;
215
+ if (options.trimValues) val = val.trim();
216
+ val = this.replaceEntitiesValue(val, tagName, this.readonlyMatcher);
217
+ processedVals[i] = val;
218
+
219
+ rawAttrsForMatcher[attrName] = val;
220
+ hasRawAttrs = true;
141
221
  }
142
- let oldVal = matches[i][4];
143
- let aName = this.options.attributeNamePrefix + attrName;
222
+ }
223
+
224
+ // Update matcher ONCE before second pass, if applicable
225
+ if (hasRawAttrs && typeof jPath === 'object' && jPath.updateCurrent) {
226
+ jPath.updateCurrent(rawAttrsForMatcher);
227
+ }
228
+
229
+ // Hoist toString() once — path doesn't change during attribute processing
230
+ const jPathStr = options.jPath ? jPath.toString() : this.readonlyMatcher;
231
+
232
+ // Second pass: apply processors, build final attrs
233
+ let hasAttrs = false;
234
+ for (let i = 0; i < len; i++) {
235
+ const attrName = this.resolveNameSpace(matches[i][1]);
236
+
237
+ if (this.ignoreAttributesFn(attrName, jPathStr)) continue;
238
+
239
+ let aName = options.attributeNamePrefix + attrName;
240
+
144
241
  if (attrName.length) {
145
- if (this.options.transformAttributeName) {
146
- aName = this.options.transformAttributeName(aName);
242
+ if (options.transformAttributeName) {
243
+ aName = options.transformAttributeName(aName);
147
244
  }
148
- if(aName === "__proto__") aName = "#__proto__";
149
- if (oldVal !== undefined) {
150
- if (this.options.trimValues) {
151
- oldVal = oldVal.trim();
152
- }
153
- oldVal = this.replaceEntitiesValue(oldVal);
154
- const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath);
155
- if(newVal === null || newVal === undefined){
156
- //don't parse
245
+ aName = sanitizeName(aName, options);
246
+
247
+ if (matches[i][4] !== undefined) {
248
+ // Reuse already-processed value — no double entity replacement
249
+ const oldVal = processedVals[i];
250
+
251
+ const newVal = options.attributeValueProcessor(attrName, oldVal, jPathStr);
252
+ if (newVal === null || newVal === undefined) {
157
253
  attrs[aName] = oldVal;
158
- }else if(typeof newVal !== typeof oldVal || newVal !== oldVal){
159
- //overwrite
254
+ } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
160
255
  attrs[aName] = newVal;
161
- }else{
162
- //parse
163
- attrs[aName] = parseValue(
164
- oldVal,
165
- this.options.parseAttributeValue,
166
- this.options.numberParseOptions
167
- );
256
+ } else {
257
+ attrs[aName] = parseValue(oldVal, options.parseAttributeValue, options.numberParseOptions);
168
258
  }
169
- } else if (this.options.allowBooleanAttributes) {
259
+ hasAttrs = true;
260
+ } else if (options.allowBooleanAttributes) {
170
261
  attrs[aName] = true;
262
+ hasAttrs = true;
171
263
  }
172
264
  }
173
265
  }
174
- if (!Object.keys(attrs).length) {
175
- return;
176
- }
177
- if (this.options.attributesGroupName) {
266
+
267
+ if (!hasAttrs) return;
268
+
269
+ if (options.attributesGroupName && !options.preserveOrder) {
178
270
  const attrCollection = {};
179
- attrCollection[this.options.attributesGroupName] = attrs;
271
+ attrCollection[options.attributesGroupName] = attrs;
180
272
  return attrCollection;
181
273
  }
182
- return attrs
274
+ return attrs;
183
275
  }
184
276
  }
185
-
186
- const parseXml = function(xmlData) {
277
+ const parseXml = function (xmlData) {
187
278
  xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line
188
279
  const xmlObj = new xmlNode('!xml');
189
280
  let currentNode = xmlObj;
190
281
  let textData = "";
191
- let jPath = "";
192
- for(let i=0; i< xmlData.length; i++){//for each char in XML data
282
+
283
+ // Reset matcher for new document
284
+ this.matcher.reset();
285
+ this.entityDecoder.reset();
286
+
287
+ // Reset entity expansion counters for this document
288
+ this.entityExpansionCount = 0;
289
+ this.currentExpandedLength = 0;
290
+ const options = this.options;
291
+ const docTypeReader = new DocTypeReader(options.processEntities);
292
+ const xmlLen = xmlData.length;
293
+ for (let i = 0; i < xmlLen; i++) {//for each char in XML data
193
294
  const ch = xmlData[i];
194
- if(ch === '<'){
295
+ if (ch === '<') {
195
296
  // const nextIndex = i+1;
196
297
  // const _2ndChar = xmlData[nextIndex];
197
- if( xmlData[i+1] === '/') {//Closing Tag
298
+ const c1 = xmlData.charCodeAt(i + 1);
299
+ if (c1 === 47) {//Closing Tag '/'
198
300
  const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
199
- let tagName = xmlData.substring(i+2,closeIndex).trim();
301
+ let tagName = xmlData.substring(i + 2, closeIndex).trim();
200
302
 
201
- if(this.options.removeNSPrefix){
303
+ if (options.removeNSPrefix) {
202
304
  const colonIndex = tagName.indexOf(":");
203
- if(colonIndex !== -1){
204
- tagName = tagName.substr(colonIndex+1);
305
+ if (colonIndex !== -1) {
306
+ tagName = tagName.substr(colonIndex + 1);
205
307
  }
206
308
  }
207
309
 
208
- if(this.options.transformTagName) {
209
- tagName = this.options.transformTagName(tagName);
210
- }
310
+ tagName = transformTagName(options.transformTagName, tagName, "", options).tagName;
211
311
 
212
- if(currentNode){
213
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
312
+ if (currentNode) {
313
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
214
314
  }
215
315
 
216
316
  //check if last tag of nested tag was unpaired tag
217
- const lastTagName = jPath.substring(jPath.lastIndexOf(".")+1);
218
- if(tagName && this.options.unpairedTags.indexOf(tagName) !== -1 ){
317
+ const lastTagName = this.matcher.getCurrentTag();
318
+ if (tagName && options.unpairedTagsSet.has(tagName)) {
219
319
  throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
220
320
  }
221
- let propIndex = 0
222
- if(lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1 ){
223
- propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.')-1)
321
+ if (lastTagName && options.unpairedTagsSet.has(lastTagName)) {
322
+ // Pop the unpaired tag
323
+ this.matcher.pop();
224
324
  this.tagsNodeStack.pop();
225
- }else{
226
- propIndex = jPath.lastIndexOf(".");
227
325
  }
228
- jPath = jPath.substring(0, propIndex);
326
+ // Pop the closing tag
327
+ this.matcher.pop();
328
+ this.isCurrentNodeStopNode = false; // Reset flag when closing tag
229
329
 
230
330
  currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
231
331
  textData = "";
232
332
  i = closeIndex;
233
- } else if( xmlData[i+1] === '?') {
333
+ } else if (c1 === 63) { //'?'
234
334
 
235
- let tagData = readTagExp(xmlData,i, false, "?>");
236
- if(!tagData) throw new Error("Pi Tag is not closed.");
335
+ let tagData = readTagExp(xmlData, i, false, "?>");
336
+ if (!tagData) throw new Error("Pi Tag is not closed.");
237
337
 
238
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
239
- if( (this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags){
338
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
339
+ const attsMap = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName, true);
340
+ if (attsMap) {
341
+ const ver = attsMap[this.options.attributeNamePrefix + "version"];
342
+ this.entityDecoder.setXmlVersion(Number(ver) || 1.0);
343
+ docTypeReader.setXmlVersion(Number(ver) || 1.0);
344
+ }
345
+ if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) {
346
+ //do nothing
347
+ } else {
240
348
 
241
- }else{
242
-
243
349
  const childNode = new xmlNode(tagData.tagName);
244
- childNode.add(this.options.textNodeName, "");
245
-
246
- if(tagData.tagName !== tagData.tagExp && tagData.attrExpPresent){
247
- childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName);
350
+ childNode.add(options.textNodeName, "");
351
+
352
+ if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent && options.ignoreAttributes !== true) {
353
+ childNode[":@"] = attsMap
248
354
  }
249
- this.addChild(currentNode, childNode, jPath, i);
355
+ this.addChild(currentNode, childNode, this.readonlyMatcher, i);
250
356
  }
251
357
 
252
358
 
253
359
  i = tagData.closeIndex + 1;
254
- } else if(xmlData.substr(i + 1, 3) === '!--') {
255
- const endIndex = findClosingIndex(xmlData, "-->", i+4, "Comment is not closed.")
256
- if(this.options.commentPropName){
360
+ } else if (c1 === 33
361
+ && xmlData.charCodeAt(i + 2) === 45
362
+ && xmlData.charCodeAt(i + 3) === 45) { //'!--'
363
+ const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.")
364
+ if (options.commentPropName) {
257
365
  const comment = xmlData.substring(i + 4, endIndex - 2);
258
366
 
259
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
367
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
260
368
 
261
- currentNode.add(this.options.commentPropName, [ { [this.options.textNodeName] : comment } ]);
369
+ currentNode.add(options.commentPropName, [{ [options.textNodeName]: comment }]);
262
370
  }
263
371
  i = endIndex;
264
- } else if( xmlData.substr(i + 1, 2) === '!D') {
265
- const result = readDocType(xmlData, i);
266
- this.docTypeEntities = result.entities;
372
+ } else if (c1 === 33
373
+ && xmlData.charCodeAt(i + 2) === 68) { //'!D'
374
+ const result = docTypeReader.readDocType(xmlData, i);
375
+ this.entityDecoder.addInputEntities(result.entities);
267
376
  i = result.i;
268
- }else if(xmlData.substr(i + 1, 2) === '![') {
377
+ } else if (c1 === 33
378
+ && xmlData.charCodeAt(i + 2) === 91) { // '!['
269
379
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
270
- const tagExp = xmlData.substring(i + 9,closeIndex);
380
+ const tagExp = xmlData.substring(i + 9, closeIndex);
271
381
 
272
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
382
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
273
383
 
274
- let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true);
275
- if(val == undefined) val = "";
384
+ let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true);
385
+ if (val == undefined) val = "";
276
386
 
277
387
  //cdata should be set even if it is 0 length string
278
- if(this.options.cdataPropName){
279
- currentNode.add(this.options.cdataPropName, [ { [this.options.textNodeName] : tagExp } ]);
280
- }else{
281
- currentNode.add(this.options.textNodeName, val);
388
+ if (options.cdataPropName) {
389
+ currentNode.add(options.cdataPropName, [{ [options.textNodeName]: tagExp }]);
390
+ } else {
391
+ currentNode.add(options.textNodeName, val);
282
392
  }
283
-
393
+
284
394
  i = closeIndex + 2;
285
- }else {//Opening tag
286
- let result = readTagExp(xmlData,i, this.options.removeNSPrefix);
287
- let tagName= result.tagName;
395
+ } else {//Opening tag
396
+ let result = readTagExp(xmlData, i, options.removeNSPrefix);
397
+
398
+ // Safety check: readTagExp can return undefined
399
+ if (!result) {
400
+ // Log context for debugging
401
+ const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlLen, i + 50));
402
+ throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`);
403
+ }
404
+
405
+ let tagName = result.tagName;
288
406
  const rawTagName = result.rawTagName;
289
407
  let tagExp = result.tagExp;
290
408
  let attrExpPresent = result.attrExpPresent;
291
409
  let closeIndex = result.closeIndex;
292
410
 
293
- if (this.options.transformTagName) {
294
- tagName = this.options.transformTagName(tagName);
411
+ ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options));
412
+
413
+ if (options.strictReservedNames &&
414
+ (tagName === options.commentPropName
415
+ || tagName === options.cdataPropName
416
+ || tagName === options.textNodeName
417
+ || tagName === options.attributesGroupName
418
+ )) {
419
+ throw new Error(`Invalid tag name: ${tagName}`);
295
420
  }
296
-
421
+
297
422
  //save text as child node
298
423
  if (currentNode && textData) {
299
- if(currentNode.tagname !== '!xml'){
424
+ if (currentNode.tagname !== '!xml') {
300
425
  //when nested tag is found
301
- textData = this.saveTextToParentTag(textData, currentNode, jPath, false);
426
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false);
302
427
  }
303
428
  }
304
429
 
305
430
  //check if last tag was unpaired tag
306
431
  const lastTag = currentNode;
307
- if(lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1 ){
432
+ if (lastTag && options.unpairedTagsSet.has(lastTag.tagname)) {
308
433
  currentNode = this.tagsNodeStack.pop();
309
- jPath = jPath.substring(0, jPath.lastIndexOf("."));
434
+ this.matcher.pop();
435
+ }
436
+
437
+ // Clean up self-closing syntax BEFORE processing attributes
438
+ // This is where tagExp gets the trailing / removed
439
+ let isSelfClosing = false;
440
+ if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
441
+ isSelfClosing = true;
442
+ if (tagName[tagName.length - 1] === "/") {
443
+ tagName = tagName.substr(0, tagName.length - 1);
444
+ tagExp = tagName;
445
+ } else {
446
+ tagExp = tagExp.substr(0, tagExp.length - 1);
447
+ }
448
+
449
+ // Re-check attrExpPresent after cleaning
450
+ attrExpPresent = (tagName !== tagExp);
451
+ }
452
+
453
+ // Now process attributes with CLEAN tagExp (no trailing /)
454
+ let prefixedAttrs = null;
455
+ let rawAttrs = {};
456
+ let namespace = undefined;
457
+
458
+ // Extract namespace from rawTagName
459
+ namespace = extractNamespace(rawTagName);
460
+
461
+ // Push tag to matcher FIRST (with empty attrs for now) so callbacks see correct path
462
+ if (tagName !== xmlObj.tagname) {
463
+ this.matcher.push(tagName, {}, namespace);
464
+ }
465
+
466
+ // Now build attributes - callbacks will see correct matcher state
467
+ if (tagName !== tagExp && attrExpPresent) {
468
+ // Build attributes (returns prefixed attributes for the tree)
469
+ // Note: buildAttributesMap now internally updates the matcher with raw attributes
470
+ prefixedAttrs = this.buildAttributesMap(tagExp, this.matcher, tagName);
471
+
472
+ if (prefixedAttrs) {
473
+ // Extract raw attributes (without prefix) for our use
474
+ //TODO: seems a performance overhead
475
+ rawAttrs = extractRawAttributes(prefixedAttrs, options);
476
+ }
310
477
  }
311
- if(tagName !== xmlObj.tagname){
312
- jPath += jPath ? "." + tagName : tagName;
478
+
479
+ // Now check if this is a stop node (after attributes are set)
480
+ if (tagName !== xmlObj.tagname) {
481
+ this.isCurrentNodeStopNode = this.isItStopNode();
313
482
  }
483
+
314
484
  const startIndex = i;
315
- if (this.isItStopNode(this.options.stopNodes, jPath, tagName)) {
485
+ if (this.isCurrentNodeStopNode) {
316
486
  let tagContent = "";
317
- //self-closing tag
318
- if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
319
- if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
320
- tagName = tagName.substr(0, tagName.length - 1);
321
- jPath = jPath.substr(0, jPath.length - 1);
322
- tagExp = tagName;
323
- }else{
324
- tagExp = tagExp.substr(0, tagExp.length - 1);
325
- }
487
+
488
+ // For self-closing tags, content is empty
489
+ if (isSelfClosing) {
326
490
  i = result.closeIndex;
327
491
  }
328
492
  //unpaired tag
329
- else if(this.options.unpairedTags.indexOf(tagName) !== -1){
330
-
493
+ else if (options.unpairedTagsSet.has(tagName)) {
331
494
  i = result.closeIndex;
332
495
  }
333
496
  //normal tag
334
- else{
497
+ else {
335
498
  //read until closing tag is found
336
499
  const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1);
337
- if(!result) throw new Error(`Unexpected end of ${rawTagName}`);
500
+ if (!result) throw new Error(`Unexpected end of ${rawTagName}`);
338
501
  i = result.i;
339
502
  tagContent = result.tagContent;
340
503
  }
341
504
 
342
505
  const childNode = new xmlNode(tagName);
343
506
 
344
- if(tagName !== tagExp && attrExpPresent){
345
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
346
- }
347
- if(tagContent) {
348
- tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
507
+ if (prefixedAttrs) {
508
+ childNode[":@"] = prefixedAttrs;
349
509
  }
350
-
351
- jPath = jPath.substr(0, jPath.lastIndexOf("."));
352
- childNode.add(this.options.textNodeName, tagContent);
353
-
354
- this.addChild(currentNode, childNode, jPath, startIndex);
355
- }else{
356
- //selfClosing tag
357
- if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
358
- if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
359
- tagName = tagName.substr(0, tagName.length - 1);
360
- jPath = jPath.substr(0, jPath.length - 1);
361
- tagExp = tagName;
362
- }else{
363
- tagExp = tagExp.substr(0, tagExp.length - 1);
364
- }
365
-
366
- if(this.options.transformTagName) {
367
- tagName = this.options.transformTagName(tagName);
368
- }
510
+
511
+ // For stop nodes, store raw content as-is without any processing
512
+ childNode.add(options.textNodeName, tagContent);
513
+
514
+ this.matcher.pop(); // Pop the stop node tag
515
+ this.isCurrentNodeStopNode = false; // Reset flag
516
+
517
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
518
+ } else {
519
+ //selfClosing tag
520
+ if (isSelfClosing) {
521
+ ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options));
369
522
 
370
523
  const childNode = new xmlNode(tagName);
371
- if(tagName !== tagExp && attrExpPresent){
372
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
524
+ if (prefixedAttrs) {
525
+ childNode[":@"] = prefixedAttrs;
373
526
  }
374
- this.addChild(currentNode, childNode, jPath, startIndex);
375
- jPath = jPath.substr(0, jPath.lastIndexOf("."));
527
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
528
+ this.matcher.pop(); // Pop self-closing tag
529
+ this.isCurrentNodeStopNode = false; // Reset flag
530
+ }
531
+ else if (options.unpairedTagsSet.has(tagName)) {//unpaired tag
532
+ const childNode = new xmlNode(tagName);
533
+ if (prefixedAttrs) {
534
+ childNode[":@"] = prefixedAttrs;
535
+ }
536
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
537
+ this.matcher.pop(); // Pop unpaired tag
538
+ this.isCurrentNodeStopNode = false; // Reset flag
539
+ i = result.closeIndex;
540
+ // Continue to next iteration without changing currentNode
541
+ continue;
376
542
  }
377
- //opening tag
378
- else{
379
- const childNode = new xmlNode( tagName);
543
+ //opening tag
544
+ else {
545
+ const childNode = new xmlNode(tagName);
546
+ if (this.tagsNodeStack.length > options.maxNestedTags) {
547
+ throw new Error("Maximum nested tags exceeded");
548
+ }
380
549
  this.tagsNodeStack.push(currentNode);
381
-
382
- if(tagName !== tagExp && attrExpPresent){
383
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
550
+
551
+ if (prefixedAttrs) {
552
+ childNode[":@"] = prefixedAttrs;
384
553
  }
385
- this.addChild(currentNode, childNode, jPath, startIndex);
554
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
386
555
  currentNode = childNode;
387
556
  }
388
557
  textData = "";
389
558
  i = closeIndex;
390
559
  }
391
560
  }
392
- }else{
561
+ } else {
393
562
  textData += xmlData[i];
394
563
  }
395
564
  }
396
565
  return xmlObj.child;
397
566
  }
398
567
 
399
- function addChild(currentNode, childNode, jPath, startIndex){
568
+ function addChild(currentNode, childNode, matcher, startIndex) {
400
569
  // unset startIndex if not requested
401
570
  if (!this.options.captureMetaData) startIndex = undefined;
402
- const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"])
403
- if(result === false){
404
- } else if(typeof result === "string"){
571
+
572
+ // Pass jPath string or matcher based on options.jPath setting
573
+ const jPathOrMatcher = this.options.jPath ? matcher.toString() : matcher;
574
+ const result = this.options.updateTag(childNode.tagname, jPathOrMatcher, childNode[":@"])
575
+ if (result === false) {
576
+ //do nothing
577
+ } else if (typeof result === "string") {
405
578
  childNode.tagname = result
406
579
  currentNode.addChild(childNode, startIndex);
407
- }else{
580
+ } else {
408
581
  currentNode.addChild(childNode, startIndex);
409
582
  }
410
583
  }
411
584
 
412
- const replaceEntitiesValue = function(val){
585
+ /**
586
+ * @param {object} val - Entity object with regex and val properties
587
+ * @param {string} tagName - Tag name
588
+ * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
589
+ */
590
+ function replaceEntitiesValue(val, tagName, jPath) {
591
+ const entityConfig = this.options.processEntities;
413
592
 
414
- if(this.options.processEntities){
415
- for(let entityName in this.docTypeEntities){
416
- const entity = this.docTypeEntities[entityName];
417
- val = val.replace( entity.regx, entity.val);
418
- }
419
- for(let entityName in this.lastEntities){
420
- const entity = this.lastEntities[entityName];
421
- val = val.replace( entity.regex, entity.val);
593
+ if (!entityConfig || !entityConfig.enabled) {
594
+ return val;
595
+ }
596
+
597
+ // Check if tag is allowed to contain entities
598
+ if (entityConfig.allowedTags) {
599
+ const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
600
+ const allowed = Array.isArray(entityConfig.allowedTags)
601
+ ? entityConfig.allowedTags.includes(tagName)
602
+ : entityConfig.allowedTags(tagName, jPathOrMatcher);
603
+
604
+ if (!allowed) {
605
+ return val;
422
606
  }
423
- if(this.options.htmlEntities){
424
- for(let entityName in this.htmlEntities){
425
- const entity = this.htmlEntities[entityName];
426
- val = val.replace( entity.regex, entity.val);
427
- }
607
+ }
608
+
609
+ // Apply custom tag filter if provided
610
+ if (entityConfig.tagFilter) {
611
+ const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
612
+ if (!entityConfig.tagFilter(tagName, jPathOrMatcher)) {
613
+ return val; // Skip based on custom filter
428
614
  }
429
- val = val.replace( this.ampEntity.regex, this.ampEntity.val);
430
615
  }
431
- return val;
616
+
617
+ return this.entityDecoder.decode(val);
432
618
  }
433
- function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) {
619
+
620
+
621
+ function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) {
434
622
  if (textData) { //store previously collected data as textNode
435
- if(isLeafNode === undefined) isLeafNode = currentNode.child.length === 0
436
-
623
+ if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0
624
+
437
625
  textData = this.parseTextData(textData,
438
- currentNode.tagname,
439
- jPath,
626
+ parentNode.tagname,
627
+ matcher,
440
628
  false,
441
- currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false,
629
+ parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false,
442
630
  isLeafNode);
443
631
 
444
632
  if (textData !== undefined && textData !== "")
445
- currentNode.add(this.options.textNodeName, textData);
633
+ parentNode.add(this.options.textNodeName, textData);
446
634
  textData = "";
447
635
  }
448
636
  return textData;
449
637
  }
450
638
 
451
- //TODO: use jPath to simplify the logic
452
639
  /**
453
- *
454
- * @param {string[]} stopNodes
455
- * @param {string} jPath
456
- * @param {string} currentTagName
640
+ * @param {Array<Expression>} stopNodeExpressions - Array of compiled Expression objects
641
+ * @param {Matcher} matcher - Current path matcher
457
642
  */
458
- function isItStopNode(stopNodes, jPath, currentTagName){
459
- const allNodesExp = "*." + currentTagName;
460
- for (const stopNodePath in stopNodes) {
461
- const stopNodeExp = stopNodes[stopNodePath];
462
- if( allNodesExp === stopNodeExp || jPath === stopNodeExp ) return true;
463
- }
464
- return false;
643
+ function isItStopNode() {
644
+ if (this.stopNodeExpressionsSet.size === 0) return false;
645
+
646
+ return this.matcher.matchesAny(this.stopNodeExpressionsSet);
465
647
  }
466
648
 
467
649
  /**
@@ -470,63 +652,75 @@ function isItStopNode(stopNodes, jPath, currentTagName){
470
652
  * @param {number} i starting index
471
653
  * @returns
472
654
  */
473
- function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){
474
- let attrBoundary;
475
- let tagExp = "";
476
- for (let index = i; index < xmlData.length; index++) {
477
- let ch = xmlData[index];
655
+ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
656
+ //TODO: ignore boolean attributes in tag expression
657
+ //TODO: if ignore attributes, dont read full attribute expression but the end. But read for xml declaration
658
+ let attrBoundary = 0;
659
+ const len = xmlData.length;
660
+ const closeCode0 = closingChar.charCodeAt(0);
661
+ const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1;
662
+
663
+ let result = '';
664
+ let segmentStart = i;
665
+
666
+ for (let index = i; index < len; index++) {
667
+ const code = xmlData.charCodeAt(index);
668
+
478
669
  if (attrBoundary) {
479
- if (ch === attrBoundary) attrBoundary = "";//reset
480
- } else if (ch === '"' || ch === "'") {
481
- attrBoundary = ch;
482
- } else if (ch === closingChar[0]) {
483
- if(closingChar[1]){
484
- if(xmlData[index + 1] === closingChar[1]){
485
- return {
486
- data: tagExp,
487
- index: index
488
- }
489
- }
490
- }else{
491
- return {
492
- data: tagExp,
493
- index: index
670
+ if (code === attrBoundary) attrBoundary = 0;
671
+ } else if (code === 34 || code === 39) { // " or '
672
+ attrBoundary = code;
673
+ } else if (code === closeCode0) {
674
+ if (closeCode1 !== -1) {
675
+ if (xmlData.charCodeAt(index + 1) === closeCode1) {
676
+ result += xmlData.substring(segmentStart, index);
677
+ return { data: result, index };
494
678
  }
679
+ } else {
680
+ result += xmlData.substring(segmentStart, index);
681
+ return { data: result, index };
495
682
  }
496
- } else if (ch === '\t') {
497
- ch = " "
683
+ } else if (code === 9 && !attrBoundary) { // \t - only replace with space outside attribute values
684
+ // Flush accumulated segment, add space, start new segment
685
+ result += xmlData.substring(segmentStart, index) + ' ';
686
+ segmentStart = index + 1;
498
687
  }
499
- tagExp += ch;
500
688
  }
501
689
  }
502
690
 
503
- function findClosingIndex(xmlData, str, i, errMsg){
691
+ function findClosingIndex(xmlData, str, i, errMsg) {
504
692
  const closingIndex = xmlData.indexOf(str, i);
505
- if(closingIndex === -1){
693
+ if (closingIndex === -1) {
506
694
  throw new Error(errMsg)
507
- }else{
695
+ } else {
508
696
  return closingIndex + str.length - 1;
509
697
  }
510
698
  }
511
699
 
512
- function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
513
- const result = tagExpWithClosingIndex(xmlData, i+1, closingChar);
514
- if(!result) return;
700
+ function findClosingChar(xmlData, char, i, errMsg) {
701
+ const closingIndex = xmlData.indexOf(char, i);
702
+ if (closingIndex === -1) throw new Error(errMsg);
703
+ return closingIndex; // no offset needed
704
+ }
705
+
706
+ function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
707
+ const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
708
+ if (!result) return;
515
709
  let tagExp = result.data;
516
710
  const closeIndex = result.index;
517
711
  const separatorIndex = tagExp.search(/\s/);
518
712
  let tagName = tagExp;
519
713
  let attrExpPresent = true;
520
- if(separatorIndex !== -1){//separate tag name and attributes expression
714
+ if (separatorIndex !== -1) {//separate tag name and attributes expression
521
715
  tagName = tagExp.substring(0, separatorIndex);
522
716
  tagExp = tagExp.substring(separatorIndex + 1).trimStart();
523
717
  }
524
718
 
525
719
  const rawTagName = tagName;
526
- if(removeNSPrefix){
720
+ if (removeNSPrefix) {
527
721
  const colonIndex = tagName.indexOf(":");
528
- if(colonIndex !== -1){
529
- tagName = tagName.substr(colonIndex+1);
722
+ if (colonIndex !== -1) {
723
+ tagName = tagName.substr(colonIndex + 1);
530
724
  attrExpPresent = tagName !== result.data.substr(colonIndex + 1);
531
725
  }
532
726
  }
@@ -545,47 +739,52 @@ function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
545
739
  * @param {string} tagName
546
740
  * @param {number} i
547
741
  */
548
- function readStopNodeData(xmlData, tagName, i){
742
+ function readStopNodeData(xmlData, tagName, i) {
549
743
  const startIndex = i;
550
744
  // Starting at 1 since we already have an open tag
551
745
  let openTagCount = 1;
552
746
 
553
- for (; i < xmlData.length; i++) {
554
- if( xmlData[i] === "<"){
555
- if (xmlData[i+1] === "/") {//close tag
556
- const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
557
- let closeTagName = xmlData.substring(i+2,closeIndex).trim();
558
- if(closeTagName === tagName){
559
- openTagCount--;
560
- if (openTagCount === 0) {
561
- return {
562
- tagContent: xmlData.substring(startIndex, i),
563
- i : closeIndex
564
- }
747
+ const xmllen = xmlData.length;
748
+ for (; i < xmllen; i++) {
749
+ if (xmlData[i] === "<") {
750
+ const c1 = xmlData.charCodeAt(i + 1);
751
+ if (c1 === 47) {//close tag '/'
752
+ const closeIndex = findClosingChar(xmlData, ">", i, `${tagName} is not closed`);
753
+ let closeTagName = xmlData.substring(i + 2, closeIndex).trim();
754
+ if (closeTagName === tagName) {
755
+ openTagCount--;
756
+ if (openTagCount === 0) {
757
+ return {
758
+ tagContent: xmlData.substring(startIndex, i),
759
+ i: closeIndex
565
760
  }
566
761
  }
567
- i=closeIndex;
568
- } else if(xmlData[i+1] === '?') {
569
- const closeIndex = findClosingIndex(xmlData, "?>", i+1, "StopNode is not closed.")
570
- i=closeIndex;
571
- } else if(xmlData.substr(i + 1, 3) === '!--') {
572
- const closeIndex = findClosingIndex(xmlData, "-->", i+3, "StopNode is not closed.")
573
- i=closeIndex;
574
- } else if(xmlData.substr(i + 1, 2) === '![') {
575
- const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
576
- i=closeIndex;
577
- } else {
578
- const tagData = readTagExp(xmlData, i, '>')
762
+ }
763
+ i = closeIndex;
764
+ } else if (c1 === 63) { //?
765
+ const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.")
766
+ i = closeIndex;
767
+ } else if (c1 === 33
768
+ && xmlData.charCodeAt(i + 2) === 45
769
+ && xmlData.charCodeAt(i + 3) === 45) { // '!--'
770
+ const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.")
771
+ i = closeIndex;
772
+ } else if (c1 === 33
773
+ && xmlData.charCodeAt(i + 2) === 91) { // '!['
774
+ const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
775
+ i = closeIndex;
776
+ } else {
777
+ const tagData = readTagExp(xmlData, i, false)
579
778
 
580
- if (tagData) {
581
- const openTagName = tagData && tagData.tagName;
582
- if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length-1] !== "/") {
583
- openTagCount++;
584
- }
585
- i=tagData.closeIndex;
779
+ if (tagData) {
780
+ const openTagName = tagData && tagData.tagName;
781
+ if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length - 1] !== "/") {
782
+ openTagCount++;
586
783
  }
784
+ i = tagData.closeIndex;
587
785
  }
588
786
  }
787
+ }
589
788
  }//end for loop
590
789
  }
591
790
 
@@ -593,8 +792,8 @@ function parseValue(val, shouldParse, options) {
593
792
  if (shouldParse && typeof val === 'string') {
594
793
  //console.log(options)
595
794
  const newval = val.trim();
596
- if(newval === 'true' ) return true;
597
- else if(newval === 'false' ) return false;
795
+ if (newval === 'true') return true;
796
+ else if (newval === 'false') return false;
598
797
  else return toNumber(val, options);
599
798
  } else {
600
799
  if (isExist(val)) {
@@ -604,3 +803,36 @@ function parseValue(val, shouldParse, options) {
604
803
  }
605
804
  }
606
805
  }
806
+
807
+ function fromCodePoint(str, base, prefix) {
808
+ const codePoint = Number.parseInt(str, base);
809
+
810
+ if (codePoint >= 0 && codePoint <= 0x10FFFF) {
811
+ return String.fromCodePoint(codePoint);
812
+ } else {
813
+ return prefix + str + ";";
814
+ }
815
+ }
816
+
817
+ function transformTagName(fn, tagName, tagExp, options) {
818
+ if (fn) {
819
+ const newTagName = fn(tagName);
820
+ if (tagExp === tagName) {
821
+ tagExp = newTagName
822
+ }
823
+ tagName = newTagName;
824
+ }
825
+ tagName = sanitizeName(tagName, options);
826
+ return { tagName, tagExp };
827
+ }
828
+
829
+
830
+
831
+ function sanitizeName(name, options) {
832
+ if (criticalProperties.includes(name)) {
833
+ throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`);
834
+ } else if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
835
+ return options.onDangerousProperty(name);
836
+ }
837
+ return name;
838
+ }