fast-xml-parser 4.5.2 → 4.5.4

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