@depup/fast-xml-parser 5.5.6-depup.0

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 (54) hide show
  1. package/CHANGELOG.md +752 -0
  2. package/LICENSE +21 -0
  3. package/README.md +31 -0
  4. package/changes.json +10 -0
  5. package/lib/fxbuilder.min.js +2 -0
  6. package/lib/fxbuilder.min.js.map +1 -0
  7. package/lib/fxp.cjs +1 -0
  8. package/lib/fxp.d.cts +595 -0
  9. package/lib/fxp.min.js +2 -0
  10. package/lib/fxp.min.js.map +1 -0
  11. package/lib/fxparser.min.js +2 -0
  12. package/lib/fxparser.min.js.map +1 -0
  13. package/lib/fxvalidator.min.js +2 -0
  14. package/lib/fxvalidator.min.js.map +1 -0
  15. package/package.json +112 -0
  16. package/src/cli/cli.js +97 -0
  17. package/src/cli/man.js +17 -0
  18. package/src/cli/read.js +43 -0
  19. package/src/fxp.d.ts +577 -0
  20. package/src/fxp.js +14 -0
  21. package/src/ignoreAttributes.js +18 -0
  22. package/src/util.js +61 -0
  23. package/src/v6/CharsSymbol.js +16 -0
  24. package/src/v6/EntitiesParser.js +106 -0
  25. package/src/v6/OptionsBuilder.js +61 -0
  26. package/src/v6/OutputBuilders/BaseOutputBuilder.js +69 -0
  27. package/src/v6/OutputBuilders/JsArrBuilder.js +103 -0
  28. package/src/v6/OutputBuilders/JsMinArrBuilder.js +100 -0
  29. package/src/v6/OutputBuilders/JsObjBuilder.js +154 -0
  30. package/src/v6/OutputBuilders/ParserOptionsBuilder.js +94 -0
  31. package/src/v6/Report.js +0 -0
  32. package/src/v6/TagPath.js +81 -0
  33. package/src/v6/TagPathMatcher.js +13 -0
  34. package/src/v6/XMLParser.js +83 -0
  35. package/src/v6/Xml2JsParser.js +235 -0
  36. package/src/v6/XmlPartReader.js +210 -0
  37. package/src/v6/XmlSpecialTagsReader.js +111 -0
  38. package/src/v6/inputSource/BufferSource.js +116 -0
  39. package/src/v6/inputSource/StringSource.js +121 -0
  40. package/src/v6/valueParsers/EntitiesParser.js +105 -0
  41. package/src/v6/valueParsers/booleanParser.js +22 -0
  42. package/src/v6/valueParsers/booleanParserExt.js +19 -0
  43. package/src/v6/valueParsers/currency.js +38 -0
  44. package/src/v6/valueParsers/join.js +13 -0
  45. package/src/v6/valueParsers/number.js +14 -0
  46. package/src/v6/valueParsers/trim.js +6 -0
  47. package/src/validator.js +425 -0
  48. package/src/xmlbuilder/json2xml.js +6 -0
  49. package/src/xmlparser/DocTypeReader.js +401 -0
  50. package/src/xmlparser/OptionsBuilder.js +159 -0
  51. package/src/xmlparser/OrderedObjParser.js +905 -0
  52. package/src/xmlparser/XMLParser.js +71 -0
  53. package/src/xmlparser/node2json.js +174 -0
  54. package/src/xmlparser/xmlNode.js +40 -0
@@ -0,0 +1,401 @@
1
+ import { isName } from '../util.js';
2
+
3
+ export default class DocTypeReader {
4
+ constructor(options) {
5
+ this.suppressValidationErr = !options;
6
+ this.options = options;
7
+ }
8
+
9
+ readDocType(xmlData, i) {
10
+ const entities = Object.create(null);
11
+ let entityCount = 0;
12
+
13
+ if (xmlData[i + 3] === 'O' &&
14
+ xmlData[i + 4] === 'C' &&
15
+ xmlData[i + 5] === 'T' &&
16
+ xmlData[i + 6] === 'Y' &&
17
+ xmlData[i + 7] === 'P' &&
18
+ xmlData[i + 8] === 'E') {
19
+ i = i + 9;
20
+ let angleBracketsCount = 1;
21
+ let hasBody = false, comment = false;
22
+ let exp = "";
23
+ for (; i < xmlData.length; i++) {
24
+ if (xmlData[i] === '<' && !comment) { //Determine the tag type
25
+ if (hasBody && hasSeq(xmlData, "!ENTITY", i)) {
26
+ i += 7;
27
+ let entityName, val;
28
+ [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);
29
+ if (val.indexOf("&") === -1) { //Parameter entities are not supported
30
+ if (this.options.enabled !== false &&
31
+ this.options.maxEntityCount &&
32
+ entityCount >= this.options.maxEntityCount) {
33
+ throw new Error(
34
+ `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})`
35
+ );
36
+ }
37
+ //const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
38
+ const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ entities[entityName] = {
40
+ regx: RegExp(`&${escaped};`, "g"),
41
+ val: val
42
+ };
43
+ entityCount++;
44
+ }
45
+ }
46
+ else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) {
47
+ i += 8;//Not supported
48
+ const { index } = this.readElementExp(xmlData, i + 1);
49
+ i = index;
50
+ } else if (hasBody && hasSeq(xmlData, "!ATTLIST", i)) {
51
+ i += 8;//Not supported
52
+ // const {index} = this.readAttlistExp(xmlData,i+1);
53
+ // i = index;
54
+ } else if (hasBody && hasSeq(xmlData, "!NOTATION", i)) {
55
+ i += 9;//Not supported
56
+ const { index } = this.readNotationExp(xmlData, i + 1, this.suppressValidationErr);
57
+ i = index;
58
+ } else if (hasSeq(xmlData, "!--", i)) comment = true;
59
+ else throw new Error(`Invalid DOCTYPE`);
60
+
61
+ angleBracketsCount++;
62
+ exp = "";
63
+ } else if (xmlData[i] === '>') { //Read tag content
64
+ if (comment) {
65
+ if (xmlData[i - 1] === "-" && xmlData[i - 2] === "-") {
66
+ comment = false;
67
+ angleBracketsCount--;
68
+ }
69
+ } else {
70
+ angleBracketsCount--;
71
+ }
72
+ if (angleBracketsCount === 0) {
73
+ break;
74
+ }
75
+ } else if (xmlData[i] === '[') {
76
+ hasBody = true;
77
+ } else {
78
+ exp += xmlData[i];
79
+ }
80
+ }
81
+ if (angleBracketsCount !== 0) {
82
+ throw new Error(`Unclosed DOCTYPE`);
83
+ }
84
+ } else {
85
+ throw new Error(`Invalid Tag instead of DOCTYPE`);
86
+ }
87
+ return { entities, i };
88
+ }
89
+ readEntityExp(xmlData, i) {
90
+ //External entities are not supported
91
+ // <!ENTITY ext SYSTEM "http://normal-website.com" >
92
+
93
+ //Parameter entities are not supported
94
+ // <!ENTITY entityname "&anotherElement;">
95
+
96
+ //Internal entities are supported
97
+ // <!ENTITY entityname "replacement text">
98
+
99
+ // Skip leading whitespace after <!ENTITY
100
+ i = skipWhitespace(xmlData, i);
101
+
102
+ // Read entity name
103
+ let entityName = "";
104
+ while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
105
+ entityName += xmlData[i];
106
+ i++;
107
+ }
108
+ validateEntityName(entityName);
109
+
110
+ // Skip whitespace after entity name
111
+ i = skipWhitespace(xmlData, i);
112
+
113
+ // Check for unsupported constructs (external entities or parameter entities)
114
+ if (!this.suppressValidationErr) {
115
+ if (xmlData.substring(i, i + 6).toUpperCase() === "SYSTEM") {
116
+ throw new Error("External entities are not supported");
117
+ } else if (xmlData[i] === "%") {
118
+ throw new Error("Parameter entities are not supported");
119
+ }
120
+ }
121
+
122
+ // Read entity value (internal entity)
123
+ let entityValue = "";
124
+ [i, entityValue] = this.readIdentifierVal(xmlData, i, "entity");
125
+
126
+ // Validate entity size
127
+ if (this.options.enabled !== false &&
128
+ this.options.maxEntitySize &&
129
+ entityValue.length > this.options.maxEntitySize) {
130
+ throw new Error(
131
+ `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})`
132
+ );
133
+ }
134
+
135
+ i--;
136
+ return [entityName, entityValue, i];
137
+ }
138
+
139
+ readNotationExp(xmlData, i) {
140
+ // Skip leading whitespace after <!NOTATION
141
+ i = skipWhitespace(xmlData, i);
142
+
143
+ // Read notation name
144
+ let notationName = "";
145
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
146
+ notationName += xmlData[i];
147
+ i++;
148
+ }
149
+ !this.suppressValidationErr && validateEntityName(notationName);
150
+
151
+ // Skip whitespace after notation name
152
+ i = skipWhitespace(xmlData, i);
153
+
154
+ // Check identifier type (SYSTEM or PUBLIC)
155
+ const identifierType = xmlData.substring(i, i + 6).toUpperCase();
156
+ if (!this.suppressValidationErr && identifierType !== "SYSTEM" && identifierType !== "PUBLIC") {
157
+ throw new Error(`Expected SYSTEM or PUBLIC, found "${identifierType}"`);
158
+ }
159
+ i += identifierType.length;
160
+
161
+ // Skip whitespace after identifier type
162
+ i = skipWhitespace(xmlData, i);
163
+
164
+ // Read public identifier (if PUBLIC)
165
+ let publicIdentifier = null;
166
+ let systemIdentifier = null;
167
+
168
+ if (identifierType === "PUBLIC") {
169
+ [i, publicIdentifier] = this.readIdentifierVal(xmlData, i, "publicIdentifier");
170
+
171
+ // Skip whitespace after public identifier
172
+ i = skipWhitespace(xmlData, i);
173
+
174
+ // Optionally read system identifier
175
+ if (xmlData[i] === '"' || xmlData[i] === "'") {
176
+ [i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
177
+ }
178
+ } else if (identifierType === "SYSTEM") {
179
+ // Read system identifier (mandatory for SYSTEM)
180
+ [i, systemIdentifier] = this.readIdentifierVal(xmlData, i, "systemIdentifier");
181
+
182
+ if (!this.suppressValidationErr && !systemIdentifier) {
183
+ throw new Error("Missing mandatory system identifier for SYSTEM notation");
184
+ }
185
+ }
186
+
187
+ return { notationName, publicIdentifier, systemIdentifier, index: --i };
188
+ }
189
+
190
+ readIdentifierVal(xmlData, i, type) {
191
+ let identifierVal = "";
192
+ const startChar = xmlData[i];
193
+ if (startChar !== '"' && startChar !== "'") {
194
+ throw new Error(`Expected quoted string, found "${startChar}"`);
195
+ }
196
+ i++;
197
+
198
+ while (i < xmlData.length && xmlData[i] !== startChar) {
199
+ identifierVal += xmlData[i];
200
+ i++;
201
+ }
202
+
203
+ if (xmlData[i] !== startChar) {
204
+ throw new Error(`Unterminated ${type} value`);
205
+ }
206
+ i++;
207
+ return [i, identifierVal];
208
+ }
209
+
210
+ readElementExp(xmlData, i) {
211
+ // <!ELEMENT br EMPTY>
212
+ // <!ELEMENT div ANY>
213
+ // <!ELEMENT title (#PCDATA)>
214
+ // <!ELEMENT book (title, author+)>
215
+ // <!ELEMENT name (content-model)>
216
+
217
+ // Skip leading whitespace after <!ELEMENT
218
+ i = skipWhitespace(xmlData, i);
219
+
220
+ // Read element name
221
+ let elementName = "";
222
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
223
+ elementName += xmlData[i];
224
+ i++;
225
+ }
226
+
227
+ // Validate element name
228
+ if (!this.suppressValidationErr && !isName(elementName)) {
229
+ throw new Error(`Invalid element name: "${elementName}"`);
230
+ }
231
+
232
+ // Skip whitespace after element name
233
+ i = skipWhitespace(xmlData, i);
234
+ let contentModel = "";
235
+ // Expect '(' to start content model
236
+ if (xmlData[i] === "E" && hasSeq(xmlData, "MPTY", i)) i += 4;
237
+ else if (xmlData[i] === "A" && hasSeq(xmlData, "NY", i)) i += 2;
238
+ else if (xmlData[i] === "(") {
239
+ i++; // Move past '('
240
+
241
+ // Read content model
242
+ while (i < xmlData.length && xmlData[i] !== ")") {
243
+ contentModel += xmlData[i];
244
+ i++;
245
+ }
246
+ if (xmlData[i] !== ")") {
247
+ throw new Error("Unterminated content model");
248
+ }
249
+
250
+ } else if (!this.suppressValidationErr) {
251
+ throw new Error(`Invalid Element Expression, found "${xmlData[i]}"`);
252
+ }
253
+
254
+ return {
255
+ elementName,
256
+ contentModel: contentModel.trim(),
257
+ index: i
258
+ };
259
+ }
260
+
261
+ readAttlistExp(xmlData, i) {
262
+ // Skip leading whitespace after <!ATTLIST
263
+ i = skipWhitespace(xmlData, i);
264
+
265
+ // Read element name
266
+ let elementName = "";
267
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
268
+ elementName += xmlData[i];
269
+ i++;
270
+ }
271
+
272
+ // Validate element name
273
+ validateEntityName(elementName)
274
+
275
+ // Skip whitespace after element name
276
+ i = skipWhitespace(xmlData, i);
277
+
278
+ // Read attribute name
279
+ let attributeName = "";
280
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
281
+ attributeName += xmlData[i];
282
+ i++;
283
+ }
284
+
285
+ // Validate attribute name
286
+ if (!validateEntityName(attributeName)) {
287
+ throw new Error(`Invalid attribute name: "${attributeName}"`);
288
+ }
289
+
290
+ // Skip whitespace after attribute name
291
+ i = skipWhitespace(xmlData, i);
292
+
293
+ // Read attribute type
294
+ let attributeType = "";
295
+ if (xmlData.substring(i, i + 8).toUpperCase() === "NOTATION") {
296
+ attributeType = "NOTATION";
297
+ i += 8; // Move past "NOTATION"
298
+
299
+ // Skip whitespace after "NOTATION"
300
+ i = skipWhitespace(xmlData, i);
301
+
302
+ // Expect '(' to start the list of notations
303
+ if (xmlData[i] !== "(") {
304
+ throw new Error(`Expected '(', found "${xmlData[i]}"`);
305
+ }
306
+ i++; // Move past '('
307
+
308
+ // Read the list of allowed notations
309
+ let allowedNotations = [];
310
+ while (i < xmlData.length && xmlData[i] !== ")") {
311
+ let notation = "";
312
+ while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") {
313
+ notation += xmlData[i];
314
+ i++;
315
+ }
316
+
317
+ // Validate notation name
318
+ notation = notation.trim();
319
+ if (!validateEntityName(notation)) {
320
+ throw new Error(`Invalid notation name: "${notation}"`);
321
+ }
322
+
323
+ allowedNotations.push(notation);
324
+
325
+ // Skip '|' separator or exit loop
326
+ if (xmlData[i] === "|") {
327
+ i++; // Move past '|'
328
+ i = skipWhitespace(xmlData, i); // Skip optional whitespace after '|'
329
+ }
330
+ }
331
+
332
+ if (xmlData[i] !== ")") {
333
+ throw new Error("Unterminated list of notations");
334
+ }
335
+ i++; // Move past ')'
336
+
337
+ // Store the allowed notations as part of the attribute type
338
+ attributeType += " (" + allowedNotations.join("|") + ")";
339
+ } else {
340
+ // Handle simple types (e.g., CDATA, ID, IDREF, etc.)
341
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) {
342
+ attributeType += xmlData[i];
343
+ i++;
344
+ }
345
+
346
+ // Validate simple attribute type
347
+ const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"];
348
+ if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) {
349
+ throw new Error(`Invalid attribute type: "${attributeType}"`);
350
+ }
351
+ }
352
+
353
+ // Skip whitespace after attribute type
354
+ i = skipWhitespace(xmlData, i);
355
+
356
+ // Read default value
357
+ let defaultValue = "";
358
+ if (xmlData.substring(i, i + 8).toUpperCase() === "#REQUIRED") {
359
+ defaultValue = "#REQUIRED";
360
+ i += 8;
361
+ } else if (xmlData.substring(i, i + 7).toUpperCase() === "#IMPLIED") {
362
+ defaultValue = "#IMPLIED";
363
+ i += 7;
364
+ } else {
365
+ [i, defaultValue] = this.readIdentifierVal(xmlData, i, "ATTLIST");
366
+ }
367
+
368
+ return {
369
+ elementName,
370
+ attributeName,
371
+ attributeType,
372
+ defaultValue,
373
+ index: i
374
+ }
375
+ }
376
+ }
377
+
378
+
379
+
380
+ const skipWhitespace = (data, index) => {
381
+ while (index < data.length && /\s/.test(data[index])) {
382
+ index++;
383
+ }
384
+ return index;
385
+ };
386
+
387
+
388
+
389
+ function hasSeq(data, seq, i) {
390
+ for (let j = 0; j < seq.length; j++) {
391
+ if (seq[j] !== data[i + j + 1]) return false;
392
+ }
393
+ return true;
394
+ }
395
+
396
+ function validateEntityName(name) {
397
+ if (isName(name))
398
+ return name;
399
+ else
400
+ throw new Error(`Invalid entity name ${name}`);
401
+ }
@@ -0,0 +1,159 @@
1
+ import { DANGEROUS_PROPERTY_NAMES, criticalProperties } from "../util.js";
2
+
3
+ const defaultOnDangerousProperty = (name) => {
4
+ if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
5
+ return "__" + name;
6
+ }
7
+ return name;
8
+ };
9
+
10
+
11
+ export const defaultOptions = {
12
+ preserveOrder: false,
13
+ attributeNamePrefix: '@_',
14
+ attributesGroupName: false,
15
+ textNodeName: '#text',
16
+ ignoreAttributes: true,
17
+ removeNSPrefix: false, // remove NS from tag name or attribute name if true
18
+ allowBooleanAttributes: false, //a tag can have attributes without any value
19
+ //ignoreRootElement : false,
20
+ parseTagValue: true,
21
+ parseAttributeValue: false,
22
+ trimValues: true, //Trim string values of tag and attributes
23
+ cdataPropName: false,
24
+ numberParseOptions: {
25
+ hex: true,
26
+ leadingZeros: true,
27
+ eNotation: true
28
+ },
29
+ tagValueProcessor: function (tagName, val) {
30
+ return val;
31
+ },
32
+ attributeValueProcessor: function (attrName, val) {
33
+ return val;
34
+ },
35
+ stopNodes: [], //nested tags will not be parsed even for errors
36
+ alwaysCreateTextNode: false,
37
+ isArray: () => false,
38
+ commentPropName: false,
39
+ unpairedTags: [],
40
+ processEntities: true,
41
+ htmlEntities: false,
42
+ ignoreDeclaration: false,
43
+ ignorePiTags: false,
44
+ transformTagName: false,
45
+ transformAttributeName: false,
46
+ updateTag: function (tagName, jPath, attrs) {
47
+ return tagName
48
+ },
49
+ // skipEmptyListItem: false
50
+ captureMetaData: false,
51
+ maxNestedTags: 100,
52
+ strictReservedNames: true,
53
+ jPath: true, // if true, pass jPath string to callbacks; if false, pass matcher instance
54
+ onDangerousProperty: defaultOnDangerousProperty
55
+ };
56
+
57
+
58
+ /**
59
+ * Validates that a property name is safe to use
60
+ * @param {string} propertyName - The property name to validate
61
+ * @param {string} optionName - The option field name (for error message)
62
+ * @throws {Error} If property name is dangerous
63
+ */
64
+ function validatePropertyName(propertyName, optionName) {
65
+ if (typeof propertyName !== 'string') {
66
+ return; // Only validate string property names
67
+ }
68
+
69
+ const normalized = propertyName.toLowerCase();
70
+ if (DANGEROUS_PROPERTY_NAMES.some(dangerous => normalized === dangerous.toLowerCase())) {
71
+ throw new Error(
72
+ `[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
73
+ );
74
+ }
75
+
76
+ if (criticalProperties.some(dangerous => normalized === dangerous.toLowerCase())) {
77
+ throw new Error(
78
+ `[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
79
+ );
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Normalizes processEntities option for backward compatibility
85
+ * @param {boolean|object} value
86
+ * @returns {object} Always returns normalized object
87
+ */
88
+ function normalizeProcessEntities(value) {
89
+ // Boolean backward compatibility
90
+ if (typeof value === 'boolean') {
91
+ return {
92
+ enabled: value, // true or false
93
+ maxEntitySize: 10000,
94
+ maxExpansionDepth: 10,
95
+ maxTotalExpansions: 1000,
96
+ maxExpandedLength: 100000,
97
+ maxEntityCount: 100,
98
+ allowedTags: null,
99
+ tagFilter: null
100
+ };
101
+ }
102
+
103
+ // Object config - merge with defaults
104
+ if (typeof value === 'object' && value !== null) {
105
+ return {
106
+ enabled: value.enabled !== false, // default true if not specified
107
+ maxEntitySize: value.maxEntitySize ?? 10000,
108
+ maxExpansionDepth: value.maxExpansionDepth ?? 10,
109
+ maxTotalExpansions: value.maxTotalExpansions ?? 1000,
110
+ maxExpandedLength: value.maxExpandedLength ?? 100000,
111
+ maxEntityCount: value.maxEntityCount ?? 100,
112
+ allowedTags: value.allowedTags ?? null,
113
+ tagFilter: value.tagFilter ?? null
114
+ };
115
+ }
116
+
117
+ // Default to enabled with limits
118
+ return normalizeProcessEntities(true);
119
+ }
120
+
121
+ export const buildOptions = function (options) {
122
+ const built = Object.assign({}, defaultOptions, options);
123
+
124
+ // Validate property names to prevent prototype pollution
125
+ const propertyNameOptions = [
126
+ { value: built.attributeNamePrefix, name: 'attributeNamePrefix' },
127
+ { value: built.attributesGroupName, name: 'attributesGroupName' },
128
+ { value: built.textNodeName, name: 'textNodeName' },
129
+ { value: built.cdataPropName, name: 'cdataPropName' },
130
+ { value: built.commentPropName, name: 'commentPropName' }
131
+ ];
132
+
133
+ for (const { value, name } of propertyNameOptions) {
134
+ if (value) {
135
+ validatePropertyName(value, name);
136
+ }
137
+ }
138
+
139
+ if (built.onDangerousProperty === null) {
140
+ built.onDangerousProperty = defaultOnDangerousProperty;
141
+ }
142
+
143
+ // Always normalize processEntities for backward compatibility and validation
144
+ built.processEntities = normalizeProcessEntities(built.processEntities);
145
+
146
+ // Convert old-style stopNodes for backward compatibility
147
+ if (built.stopNodes && Array.isArray(built.stopNodes)) {
148
+ built.stopNodes = built.stopNodes.map(node => {
149
+ if (typeof node === 'string' && node.startsWith('*.')) {
150
+ // Old syntax: *.tagname meant "tagname anywhere"
151
+ // Convert to new syntax: ..tagname
152
+ return '..' + node.substring(2);
153
+ }
154
+ return node;
155
+ });
156
+ }
157
+ //console.debug(built.processEntities)
158
+ return built;
159
+ };