xml-sax-ts 0.2.0 → 0.4.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.
package/dist/index.js CHANGED
@@ -21,49 +21,125 @@ var XmlSaxError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/entities.ts
24
- var NAMED_ENTITIES = {
25
- lt: "<",
26
- gt: ">",
27
- amp: "&",
28
- quot: '"',
29
- apos: "'"
30
- };
31
24
  function decodeEntities(input, onError) {
32
- let output = "";
25
+ const firstAmp = input.indexOf("&");
26
+ if (firstAmp === -1) {
27
+ return input;
28
+ }
29
+ let result = "";
33
30
  let i = 0;
34
31
  while (i < input.length) {
35
- const ch = input[i];
36
- if (ch !== "&") {
37
- output += ch;
38
- i += 1;
39
- continue;
32
+ const amp = input.indexOf("&", i);
33
+ if (amp === -1) {
34
+ if (i === 0) {
35
+ return input;
36
+ }
37
+ return i < input.length ? result + input.slice(i) : result;
40
38
  }
41
- const semi = input.indexOf(";", i + 1);
39
+ if (amp > i) {
40
+ result += input.slice(i, amp);
41
+ }
42
+ const semi = input.indexOf(";", amp + 1);
42
43
  if (semi === -1) {
43
- const err = new XmlSaxError("Unterminated entity", i, 0, 0);
44
+ const err = new XmlSaxError("Unterminated entity", amp, 0, 0);
44
45
  onError?.(err);
45
46
  throw err;
46
47
  }
47
- const entity = input.slice(i + 1, semi);
48
48
  let decoded;
49
- if (entity.startsWith("#x") || entity.startsWith("#X")) {
50
- const codePoint = Number.parseInt(entity.slice(2), 16);
51
- decoded = decodeCodePoint(codePoint);
52
- } else if (entity.startsWith("#")) {
53
- const codePoint = Number.parseInt(entity.slice(1), 10);
54
- decoded = decodeCodePoint(codePoint);
49
+ const marker = input[amp + 1];
50
+ if (marker === "#") {
51
+ const numeric = parseNumericEntity(input, amp + 2, semi);
52
+ decoded = numeric === void 0 ? void 0 : decodeCodePoint(numeric);
55
53
  } else {
56
- decoded = NAMED_ENTITIES[entity];
54
+ decoded = decodeNamedEntity(input, amp + 1, semi);
57
55
  }
58
56
  if (decoded === void 0) {
59
- const err = new XmlSaxError(`Unknown entity: &${entity};`, i, 0, 0);
57
+ const entity = input.slice(amp + 1, semi);
58
+ const err = new XmlSaxError(`Unknown entity: &${entity};`, amp, 0, 0);
60
59
  onError?.(err);
61
60
  throw err;
62
61
  }
63
- output += decoded;
62
+ result += decoded;
64
63
  i = semi + 1;
65
64
  }
66
- return output;
65
+ return result;
66
+ }
67
+ function decodeNamedEntity(input, start, end) {
68
+ const len = end - start;
69
+ if (len === 2) {
70
+ if (input[start] === "l" && input[start + 1] === "t") {
71
+ return "<";
72
+ }
73
+ if (input[start] === "g" && input[start + 1] === "t") {
74
+ return ">";
75
+ }
76
+ return void 0;
77
+ }
78
+ if (len === 3) {
79
+ if (input[start] === "a" && input[start + 1] === "m" && input[start + 2] === "p") {
80
+ return "&";
81
+ }
82
+ return void 0;
83
+ }
84
+ if (len === 4) {
85
+ const maybeQuot = input[start] === "q" && input[start + 1] === "u" && input[start + 2] === "o" && input[start + 3] === "t";
86
+ if (maybeQuot) {
87
+ return '"';
88
+ }
89
+ const maybeApos = input[start] === "a" && input[start + 1] === "p" && input[start + 2] === "o" && input[start + 3] === "s";
90
+ if (maybeApos) {
91
+ return "'";
92
+ }
93
+ }
94
+ return void 0;
95
+ }
96
+ function parseNumericEntity(input, start, end) {
97
+ if (start >= end) {
98
+ return void 0;
99
+ }
100
+ let i = start;
101
+ let radix = 10;
102
+ const marker = input[i];
103
+ if (marker === "x" || marker === "X") {
104
+ radix = 16;
105
+ i += 1;
106
+ }
107
+ if (i >= end) {
108
+ return void 0;
109
+ }
110
+ let value = 0;
111
+ for (; i < end; i += 1) {
112
+ const ch = input[i];
113
+ if (ch === void 0) {
114
+ return void 0;
115
+ }
116
+ const digit = radix === 16 ? hexDigit(ch) : decimalDigit(ch);
117
+ if (digit === -1) {
118
+ return void 0;
119
+ }
120
+ value = value * radix + digit;
121
+ }
122
+ return value;
123
+ }
124
+ function decimalDigit(ch) {
125
+ const code = ch.charCodeAt(0) - 48;
126
+ if (code < 0 || code > 9) {
127
+ return -1;
128
+ }
129
+ return code;
130
+ }
131
+ function hexDigit(ch) {
132
+ const code = ch.charCodeAt(0);
133
+ if (code >= 48 && code <= 57) {
134
+ return code - 48;
135
+ }
136
+ if (code >= 65 && code <= 70) {
137
+ return code - 55;
138
+ }
139
+ if (code >= 97 && code <= 102) {
140
+ return code - 87;
141
+ }
142
+ return -1;
67
143
  }
68
144
  function decodeCodePoint(codePoint) {
69
145
  if (!Number.isFinite(codePoint)) {
@@ -82,8 +158,7 @@ function splitTextForEntities(text) {
82
158
  if (lastAmp === -1) {
83
159
  return { emit: text, carry: "" };
84
160
  }
85
- const nextSemi = text.indexOf(";", lastAmp + 1);
86
- if (nextSemi === -1) {
161
+ if (!text.includes(";", lastAmp + 1)) {
87
162
  return {
88
163
  emit: text.slice(0, lastAmp),
89
164
  carry: text.slice(lastAmp)
@@ -96,10 +171,32 @@ function splitTextForEntities(text) {
96
171
  var DEFAULT_OPTIONS = {
97
172
  xmlns: true,
98
173
  includeNamespaceAttributes: false,
99
- allowDoctype: true
174
+ allowDoctype: true,
175
+ coalesceText: false,
176
+ trackPosition: true
100
177
  };
101
178
  var XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
102
179
  var XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
180
+ var WHITESPACE_RE = /\s/;
181
+ var CRLF_RE = /\r\n?/g;
182
+ var NAME_START_TABLE = new Uint8Array(128);
183
+ var NAME_CHAR_TABLE = new Uint8Array(128);
184
+ for (let code = 65; code <= 90; code += 1) {
185
+ NAME_START_TABLE[code] = 1;
186
+ NAME_CHAR_TABLE[code] = 1;
187
+ }
188
+ for (let code = 97; code <= 122; code += 1) {
189
+ NAME_START_TABLE[code] = 1;
190
+ NAME_CHAR_TABLE[code] = 1;
191
+ }
192
+ for (let code = 48; code <= 57; code += 1) {
193
+ NAME_CHAR_TABLE[code] = 1;
194
+ }
195
+ NAME_START_TABLE[95] = 1;
196
+ NAME_CHAR_TABLE[95] = 1;
197
+ NAME_CHAR_TABLE[58] = 1;
198
+ NAME_CHAR_TABLE[45] = 1;
199
+ NAME_CHAR_TABLE[46] = 1;
103
200
  var XmlSaxParser = class {
104
201
  constructor(options = {}) {
105
202
  this.buffer = "";
@@ -115,7 +212,22 @@ var XmlSaxParser = class {
115
212
  ];
116
213
  this.closed = false;
117
214
  this.pendingCR = false;
118
- this.options = { ...DEFAULT_OPTIONS, ...options };
215
+ this.pendingTextParts = [];
216
+ this._rawAttrs = [];
217
+ const resolved = { ...DEFAULT_OPTIONS, ...options };
218
+ this.xmlns = resolved.xmlns;
219
+ this.includeNamespaceAttributes = resolved.includeNamespaceAttributes;
220
+ this.allowDoctype = resolved.allowDoctype;
221
+ this.coalesceText = resolved.coalesceText;
222
+ this.trackPosition = resolved.trackPosition;
223
+ this.onOpenTag = resolved.onOpenTag;
224
+ this.onCloseTag = resolved.onCloseTag;
225
+ this.onText = resolved.onText;
226
+ this.onCdata = resolved.onCdata;
227
+ this.onComment = resolved.onComment;
228
+ this.onProcessingInstruction = resolved.onProcessingInstruction;
229
+ this.onDoctype = resolved.onDoctype;
230
+ this.onError = resolved.onError;
119
231
  }
120
232
  feed(chunk) {
121
233
  if (this.closed) {
@@ -133,6 +245,7 @@ var XmlSaxParser = class {
133
245
  }
134
246
  this._parseBuffer(true);
135
247
  this._flushPendingCR();
248
+ this._flushTextBuffer();
136
249
  if (this.buffer.length > 0) {
137
250
  this._error("Unexpected end of input");
138
251
  }
@@ -147,12 +260,21 @@ var XmlSaxParser = class {
147
260
  const lt = this.buffer.indexOf("<", i);
148
261
  if (lt === -1) {
149
262
  const tail = this.buffer.slice(i);
150
- const split = splitTextForEntities(tail);
151
- if (split.emit.length > 0) {
152
- this._emitText(split.emit, true);
153
- this._advance(split.emit);
263
+ if (!tail.includes("&")) {
264
+ if (tail.length > 0) {
265
+ this._emitText(tail, true);
266
+ this._advance(tail);
267
+ }
268
+ } else {
269
+ const split = splitTextForEntities(tail);
270
+ if (split.emit.length > 0) {
271
+ this._emitText(split.emit, true);
272
+ this._advance(split.emit);
273
+ }
274
+ this.buffer = split.carry;
275
+ return;
154
276
  }
155
- this.buffer = split.carry;
277
+ this.buffer = "";
156
278
  return;
157
279
  }
158
280
  if (lt > i) {
@@ -167,8 +289,7 @@ var XmlSaxParser = class {
167
289
  if (consumed === null) {
168
290
  break;
169
291
  }
170
- const markup = this.buffer.slice(lt, lt + consumed);
171
- this._advance(markup);
292
+ this._advanceSpan(lt, lt + consumed);
172
293
  i = lt + consumed;
173
294
  }
174
295
  this.buffer = this.buffer.slice(i);
@@ -178,35 +299,9 @@ var XmlSaxParser = class {
178
299
  }
179
300
  _parseMarkupFrom(start, final) {
180
301
  assert(this.buffer[start] === "<", "Markup must start with '<'");
181
- this._flushPendingCR();
182
- if (this.buffer.startsWith("<!--", start)) {
183
- const end = this.buffer.indexOf("-->", start + 4);
184
- if (end === -1) {
185
- if (final) {
186
- this._error("Unterminated comment");
187
- }
188
- return null;
189
- }
190
- const comment = this.buffer.slice(start + 4, end);
191
- this.options.onComment?.(comment);
192
- return end + 3 - start;
193
- }
194
- if (this.buffer.startsWith("<![CDATA[", start)) {
195
- const end = this.buffer.indexOf("]]>", start + 9);
196
- if (end === -1) {
197
- if (final) {
198
- this._error("Unterminated CDATA section");
199
- }
200
- return null;
201
- }
202
- const cdata = this.buffer.slice(start + 9, end);
203
- const normalized = this._normalizeText(cdata, false);
204
- if (normalized.length > 0) {
205
- this.options.onCdata?.(normalized);
206
- }
207
- return end + 3 - start;
208
- }
209
- if (this.buffer.startsWith("<?", start)) {
302
+ if (this.pendingCR) this._flushPendingCR();
303
+ const secondCode = this.buffer.charCodeAt(start + 1);
304
+ if (secondCode === 63) {
210
305
  const end = this.buffer.indexOf("?>", start + 2);
211
306
  if (end === -1) {
212
307
  if (final) {
@@ -215,30 +310,64 @@ var XmlSaxParser = class {
215
310
  return null;
216
311
  }
217
312
  const body = this.buffer.slice(start + 2, end).trim();
218
- const split = body.search(/\s/);
313
+ const split = body.search(WHITESPACE_RE);
219
314
  const target = split === -1 ? body : body.slice(0, split);
220
315
  const data = split === -1 ? "" : body.slice(split).trim();
221
316
  const pi = { target, body: data };
222
- this.options.onProcessingInstruction?.(pi);
317
+ this._flushTextBuffer();
318
+ this.onProcessingInstruction?.(pi);
223
319
  return end + 2 - start;
224
320
  }
225
- if (this.buffer.startsWith("<!DOCTYPE", start)) {
226
- const end = this._findDoctypeEnd(start + 9);
227
- if (end === -1) {
228
- if (final) {
229
- this._error("Unterminated doctype declaration");
321
+ if (secondCode === 33) {
322
+ const thirdCode = this.buffer.charCodeAt(start + 2);
323
+ if (thirdCode === 45 && this.buffer.charCodeAt(start + 3) === 45) {
324
+ const end = this.buffer.indexOf("-->", start + 4);
325
+ if (end === -1) {
326
+ if (final) {
327
+ this._error("Unterminated comment");
328
+ }
329
+ return null;
230
330
  }
231
- return null;
331
+ const comment = this.buffer.slice(start + 4, end);
332
+ this._flushTextBuffer();
333
+ this.onComment?.(comment);
334
+ return end + 3 - start;
232
335
  }
233
- if (!this.options.allowDoctype) {
234
- this._error("Doctype is not allowed");
336
+ if (thirdCode === 91 && this.buffer.startsWith("<![CDATA[", start)) {
337
+ const end = this.buffer.indexOf("]]>", start + 9);
338
+ if (end === -1) {
339
+ if (final) {
340
+ this._error("Unterminated CDATA section");
341
+ }
342
+ return null;
343
+ }
344
+ const cdata = this.buffer.slice(start + 9, end);
345
+ const normalized = this._normalizeText(cdata, false);
346
+ if (normalized.length > 0) {
347
+ this._flushTextBuffer();
348
+ this.onCdata?.(normalized);
349
+ }
350
+ return end + 3 - start;
351
+ }
352
+ if (thirdCode === 68 && this.buffer.startsWith("<!DOCTYPE", start)) {
353
+ const end = this._findDoctypeEnd(start + 9);
354
+ if (end === -1) {
355
+ if (final) {
356
+ this._error("Unterminated doctype declaration");
357
+ }
358
+ return null;
359
+ }
360
+ if (!this.allowDoctype) {
361
+ this._error("Doctype is not allowed");
362
+ }
363
+ const raw = this.buffer.slice(start + 9, end).trim();
364
+ const doctype = { raw };
365
+ this._flushTextBuffer();
366
+ this.onDoctype?.(doctype);
367
+ return end + 1 - start;
235
368
  }
236
- const raw = this.buffer.slice(start + 9, end).trim();
237
- const doctype = { raw };
238
- this.options.onDoctype?.(doctype);
239
- return end + 1 - start;
240
369
  }
241
- if (this.buffer.startsWith("</", start)) {
370
+ if (secondCode === 47) {
242
371
  const end = this.buffer.indexOf(">", start + 2);
243
372
  if (end === -1) {
244
373
  if (final) {
@@ -246,12 +375,13 @@ var XmlSaxParser = class {
246
375
  }
247
376
  return null;
248
377
  }
249
- const raw = this.buffer.slice(start + 2, end).trim();
250
- const parsed = this._parseName(raw, 0, raw.length);
251
- if (raw.slice(parsed.end).trim().length > 0) {
378
+ let i = this._skipWhitespace(this.buffer, start + 2, end);
379
+ const parsed = this._parseName(this.buffer, i, end);
380
+ i = this._skipWhitespace(this.buffer, parsed.end, end);
381
+ if (i !== end) {
252
382
  this._error("Invalid closing tag");
253
383
  }
254
- this._handleCloseTag(parsed.name);
384
+ this._handleCloseTag(parsed.name, parsed.end);
255
385
  return end + 1 - start;
256
386
  }
257
387
  const tagEnd = this._findTagEnd(start + 1);
@@ -261,30 +391,54 @@ var XmlSaxParser = class {
261
391
  }
262
392
  return null;
263
393
  }
264
- const content = this.buffer.slice(start + 1, tagEnd);
265
- this._handleStartTag(content);
394
+ this._handleStartTagRange(start + 1, tagEnd);
266
395
  return tagEnd + 1 - start;
267
396
  }
268
- _handleStartTag(content) {
269
- const trimmed = content.trim();
270
- const selfClosing = trimmed.endsWith("/");
271
- const body = selfClosing ? trimmed.slice(0, -1).trim() : trimmed;
272
- const parsed = this._parseTagBody(body);
273
- let ns = this._currentNs();
274
- if (this.options.xmlns) {
275
- ns = Object.create(ns);
397
+ _handleStartTagRange(start, end) {
398
+ this._flushTextBuffer();
399
+ const parsed = this._parseStartTagRange(start, end);
400
+ const selfClosing = parsed.selfClosing;
401
+ if (!this.xmlns) {
402
+ const plainName = parsed.name;
403
+ const attributes2 = /* @__PURE__ */ Object.create(null);
276
404
  for (const attr of parsed.attributes) {
277
- if (attr.name === "xmlns") {
278
- ns[""] = attr.value;
279
- } else if (attr.name.startsWith("xmlns:")) {
280
- ns[attr.name.slice(6)] = attr.value;
405
+ attributes2[attr.name] = attr.value;
406
+ }
407
+ const tag2 = {
408
+ name: plainName,
409
+ attributes: attributes2,
410
+ isSelfClosing: selfClosing
411
+ };
412
+ this.onOpenTag?.(tag2);
413
+ if (selfClosing) {
414
+ this.onCloseTag?.({ name: plainName });
415
+ return;
416
+ }
417
+ this.elementStack.push({
418
+ rawName: parsed.name,
419
+ closeTag: { name: plainName }
420
+ });
421
+ return;
422
+ }
423
+ const parentNs = this._currentNs();
424
+ let ns = parentNs;
425
+ for (const attr of parsed.attributes) {
426
+ if (attr.name === "xmlns") {
427
+ if (ns === parentNs) {
428
+ ns = Object.create(parentNs);
281
429
  }
430
+ ns[""] = attr.value;
431
+ } else if (attr.name.startsWith("xmlns:")) {
432
+ if (ns === parentNs) {
433
+ ns = Object.create(parentNs);
434
+ }
435
+ ns[attr.name.slice(6)] = attr.value;
282
436
  }
283
437
  }
284
438
  const resolvedName = this._resolveName(parsed.name, ns);
285
- const attributes = {};
439
+ const attributes = /* @__PURE__ */ Object.create(null);
286
440
  for (const attr of parsed.attributes) {
287
- if (this.options.xmlns && !this.options.includeNamespaceAttributes) {
441
+ if (!this.includeNamespaceAttributes) {
288
442
  if (attr.name === "xmlns" || attr.name.startsWith("xmlns:")) {
289
443
  continue;
290
444
  }
@@ -306,86 +460,118 @@ var XmlSaxParser = class {
306
460
  attributes,
307
461
  isSelfClosing: selfClosing
308
462
  };
309
- this.options.onOpenTag?.(tag);
463
+ this.onOpenTag?.(tag);
310
464
  if (selfClosing) {
311
- const closeTag = {
465
+ this.onCloseTag?.({
312
466
  name: resolvedName.name,
313
467
  prefix: resolvedName.prefix,
314
468
  local: resolvedName.local,
315
469
  uri: resolvedName.uri
316
- };
317
- this.options.onCloseTag?.(closeTag);
470
+ });
318
471
  return;
319
472
  }
320
- this.elementStack.push({ rawName: parsed.name, resolved: resolvedName, ns });
473
+ this.elementStack.push({
474
+ rawName: parsed.name,
475
+ closeTag: {
476
+ name: resolvedName.name,
477
+ prefix: resolvedName.prefix,
478
+ local: resolvedName.local,
479
+ uri: resolvedName.uri
480
+ }
481
+ });
321
482
  this.nsStack.push(ns);
322
483
  }
323
- _handleCloseTag(rawName) {
324
- const entry = this.elementStack.pop();
325
- const ns = this.nsStack.pop();
326
- if (!entry || !ns) {
327
- this._error("Closing tag without matching start tag");
328
- }
329
- if (entry.rawName !== rawName) {
330
- this._error(`Mismatched closing tag: expected </${entry.rawName}>`);
331
- }
332
- const closeTag = {
333
- name: entry.resolved.name,
334
- prefix: entry.resolved.prefix,
335
- local: entry.resolved.local,
336
- uri: entry.resolved.uri
337
- };
338
- this.options.onCloseTag?.(closeTag);
339
- }
340
- _parseTagBody(body) {
341
- let i = 0;
342
- const length = body.length;
343
- i = this._skipWhitespace(body, i, length);
344
- const parsedName = this._parseName(body, i, length);
484
+ _parseStartTagRange(start, end) {
485
+ let i = this._skipWhitespace(this.buffer, start, end);
486
+ const parsedName = this._parseName(this.buffer, i, end);
345
487
  i = parsedName.end;
346
- const attributes = [];
347
- while (i < length) {
348
- i = this._skipWhitespace(body, i, length);
349
- if (i >= length) {
488
+ const attributes = this._rawAttrs;
489
+ attributes.length = 0;
490
+ let selfClosing = false;
491
+ while (i < end) {
492
+ i = this._skipWhitespace(this.buffer, i, end);
493
+ if (i >= end) {
494
+ break;
495
+ }
496
+ if (this.buffer.charCodeAt(i) === 47) {
497
+ i += 1;
498
+ i = this._skipWhitespace(this.buffer, i, end);
499
+ if (i !== end) {
500
+ this._error("Invalid self-closing tag");
501
+ }
502
+ selfClosing = true;
350
503
  break;
351
504
  }
352
- const attrName = this._parseName(body, i, length);
505
+ const attrName = this._parseName(this.buffer, i, end);
353
506
  i = attrName.end;
354
- i = this._skipWhitespace(body, i, length);
355
- if (body[i] !== "=") {
507
+ i = this._skipWhitespace(this.buffer, i, end);
508
+ if (this.buffer.charCodeAt(i) !== 61) {
356
509
  this._error("Attribute without '='");
357
510
  }
358
511
  i += 1;
359
- i = this._skipWhitespace(body, i, length);
360
- const quote = body[i];
361
- if (quote !== '"' && quote !== "'") {
512
+ i = this._skipWhitespace(this.buffer, i, end);
513
+ const quoteCode = this.buffer.charCodeAt(i);
514
+ if (quoteCode !== 34 && quoteCode !== 39) {
362
515
  this._error("Attribute value must be quoted");
363
516
  }
517
+ const quote = String.fromCharCode(quoteCode);
364
518
  i += 1;
365
- const valueEnd = body.indexOf(quote, i);
366
- if (valueEnd === -1) {
519
+ const valueEnd = this.buffer.indexOf(quote, i);
520
+ if (valueEnd === -1 || valueEnd >= end) {
367
521
  this._error("Unterminated attribute value");
368
522
  }
369
- const rawValue = body.slice(i, valueEnd);
370
- const normalized = rawValue.replace(/\r\n?/g, "\n");
371
- const value = decodeEntities(normalized, this.options.onError);
523
+ const rawValue = this.buffer.slice(i, valueEnd);
524
+ const normalized = rawValue.includes("\r") ? rawValue.replace(CRLF_RE, "\n") : rawValue;
525
+ const value = !normalized.includes("&") ? normalized : decodeEntities(normalized, this.onError);
372
526
  attributes.push({ name: attrName.name, value });
373
527
  i = valueEnd + 1;
374
528
  }
375
- return { name: parsedName.name, attributes };
529
+ return { name: parsedName.name, attributes, selfClosing };
530
+ }
531
+ _handleCloseTag(rawName, _nameEnd) {
532
+ this._flushTextBuffer();
533
+ const entry = this.elementStack.pop();
534
+ const ns = this.xmlns ? this.nsStack.pop() : this._currentNs();
535
+ if (!entry || !ns) {
536
+ this._error("Closing tag without matching start tag");
537
+ }
538
+ if (entry.rawName !== rawName) {
539
+ this._error(`Mismatched closing tag: expected </${entry.rawName}>`);
540
+ }
541
+ this.onCloseTag?.(entry.closeTag);
376
542
  }
377
543
  _emitText(text, allowPendingCR) {
378
544
  const normalized = this._normalizeText(text, allowPendingCR);
379
545
  if (normalized.length === 0) {
380
546
  return;
381
547
  }
382
- const decoded = decodeEntities(normalized, this.options.onError);
548
+ if (!normalized.includes("&")) {
549
+ this._emitDecodedText(normalized);
550
+ return;
551
+ }
552
+ const decoded = decodeEntities(normalized, this.onError);
383
553
  if (decoded.length > 0) {
384
- this.options.onText?.(decoded);
554
+ this._emitDecodedText(decoded);
555
+ }
556
+ }
557
+ _emitDecodedText(text) {
558
+ if (!this.coalesceText) {
559
+ this.onText?.(text);
560
+ return;
561
+ }
562
+ this.pendingTextParts.push(text);
563
+ }
564
+ _flushTextBuffer() {
565
+ if (!this.coalesceText || this.pendingTextParts.length === 0) {
566
+ return;
385
567
  }
568
+ const first = this.pendingTextParts[0];
569
+ const text = this.pendingTextParts.length === 1 && first !== void 0 ? first : this.pendingTextParts.join("");
570
+ this.pendingTextParts.length = 0;
571
+ this.onText?.(text);
386
572
  }
387
573
  _resolveName(rawName, ns) {
388
- if (!this.options.xmlns) {
574
+ if (!this.xmlns) {
389
575
  const split2 = rawName.indexOf(":");
390
576
  if (split2 === -1) {
391
577
  return { name: rawName, prefix: "", local: rawName, uri: "" };
@@ -420,7 +606,7 @@ var XmlSaxParser = class {
420
606
  };
421
607
  }
422
608
  _resolveAttributeName(rawName, ns) {
423
- if (!this.options.xmlns) {
609
+ if (!this.xmlns) {
424
610
  return this._resolveName(rawName, ns);
425
611
  }
426
612
  if (rawName === "xmlns") {
@@ -454,49 +640,59 @@ var XmlSaxParser = class {
454
640
  };
455
641
  }
456
642
  _findTagEnd(start) {
457
- let quote = null;
643
+ const quickEnd = this.buffer.indexOf(">", start);
644
+ if (quickEnd === -1) {
645
+ return -1;
646
+ }
647
+ const firstDoubleQuote = this.buffer.indexOf('"', start);
648
+ const firstSingleQuote = this.buffer.indexOf("'", start);
649
+ const firstQuote = firstDoubleQuote === -1 ? firstSingleQuote : firstSingleQuote === -1 ? firstDoubleQuote : Math.min(firstDoubleQuote, firstSingleQuote);
650
+ if (firstQuote === -1 || firstQuote > quickEnd) {
651
+ return quickEnd;
652
+ }
653
+ let quoteCode = 0;
458
654
  for (let i = start; i < this.buffer.length; i += 1) {
459
- const ch = this.buffer[i];
460
- if (quote) {
461
- if (ch === quote) {
462
- quote = null;
655
+ const code = this.buffer.charCodeAt(i);
656
+ if (quoteCode) {
657
+ if (code === quoteCode) {
658
+ quoteCode = 0;
463
659
  }
464
660
  continue;
465
661
  }
466
- if (ch === '"' || ch === "'") {
467
- quote = ch;
662
+ if (code === 34 || code === 39) {
663
+ quoteCode = code;
468
664
  continue;
469
665
  }
470
- if (ch === ">") {
666
+ if (code === 62) {
471
667
  return i;
472
668
  }
473
669
  }
474
670
  return -1;
475
671
  }
476
672
  _findDoctypeEnd(start) {
477
- let quote = null;
673
+ let quoteCode = 0;
478
674
  let bracketDepth = 0;
479
675
  for (let i = start; i < this.buffer.length; i += 1) {
480
- const ch = this.buffer[i];
481
- if (quote) {
482
- if (ch === quote) {
483
- quote = null;
676
+ const code = this.buffer.charCodeAt(i);
677
+ if (quoteCode) {
678
+ if (code === quoteCode) {
679
+ quoteCode = 0;
484
680
  }
485
681
  continue;
486
682
  }
487
- if (ch === '"' || ch === "'") {
488
- quote = ch;
683
+ if (code === 34 || code === 39) {
684
+ quoteCode = code;
489
685
  continue;
490
686
  }
491
- if (ch === "[") {
687
+ if (code === 91) {
492
688
  bracketDepth += 1;
493
689
  continue;
494
690
  }
495
- if (ch === "]") {
691
+ if (code === 93) {
496
692
  bracketDepth = Math.max(0, bracketDepth - 1);
497
693
  continue;
498
694
  }
499
- if (ch === ">" && bracketDepth === 0) {
695
+ if (code === 62 && bracketDepth === 0) {
500
696
  return i;
501
697
  }
502
698
  }
@@ -506,34 +702,25 @@ var XmlSaxParser = class {
506
702
  if (start >= end) {
507
703
  this._error("Expected name");
508
704
  }
509
- const first = input[start];
510
- if (first === void 0) {
511
- this._error("Expected name");
512
- }
513
- if (!this._isNameStart(first)) {
514
- this._error(`Invalid name start: '${first}'`);
705
+ const firstCode = input.charCodeAt(start);
706
+ if (firstCode !== firstCode || firstCode >= 128 || NAME_START_TABLE[firstCode] === 0) {
707
+ this._error(`Invalid name start: '${input[start] ?? ""}'`);
515
708
  }
516
709
  let i = start + 1;
517
710
  while (i < end) {
518
- const ch = input[i];
519
- if (ch === void 0 || !this._isNameChar(ch)) {
711
+ const code = input.charCodeAt(i);
712
+ if (code >= 128 || NAME_CHAR_TABLE[code] === 0) {
520
713
  break;
521
714
  }
522
715
  i += 1;
523
716
  }
524
717
  return { name: input.slice(start, i), end: i };
525
718
  }
526
- _isNameStart(ch) {
527
- return /[A-Za-z_]/.test(ch);
528
- }
529
- _isNameChar(ch) {
530
- return /[A-Za-z0-9_:\-.]/.test(ch);
531
- }
532
719
  _skipWhitespace(input, start, end) {
533
720
  let i = start;
534
721
  while (i < end) {
535
- const ch = input[i];
536
- if (ch === void 0 || !/\s/.test(ch)) {
722
+ const code = input.charCodeAt(i);
723
+ if (code !== 32 && code !== 9 && code !== 10 && code !== 13) {
537
724
  break;
538
725
  }
539
726
  i += 1;
@@ -545,12 +732,21 @@ var XmlSaxParser = class {
545
732
  }
546
733
  _advance(text) {
547
734
  this.offset += text.length;
548
- const lastNewline = text.lastIndexOf("\n");
549
- if (lastNewline === -1) {
735
+ if (!this.trackPosition) {
736
+ return;
737
+ }
738
+ let pos = text.indexOf("\n");
739
+ if (pos === -1) {
550
740
  this.column += text.length;
551
741
  return;
552
742
  }
553
- const newlineCount = text.split("\n").length - 1;
743
+ let newlineCount = 0;
744
+ let lastNewline = -1;
745
+ while (pos !== -1) {
746
+ newlineCount += 1;
747
+ lastNewline = pos;
748
+ pos = text.indexOf("\n", pos + 1);
749
+ }
554
750
  this.line += newlineCount;
555
751
  this.column = text.length - lastNewline;
556
752
  }
@@ -558,32 +754,58 @@ var XmlSaxParser = class {
558
754
  if (!text) {
559
755
  return "";
560
756
  }
757
+ if (!this.pendingCR && !text.includes("\r")) {
758
+ return text;
759
+ }
561
760
  let value = text;
562
761
  let prefix = "";
563
762
  if (this.pendingCR) {
564
763
  prefix = "\n";
565
- if (value.startsWith("\n")) {
764
+ if (value.charCodeAt(0) === 10) {
566
765
  value = value.slice(1);
567
766
  }
568
767
  this.pendingCR = false;
569
768
  }
570
- if (allowPendingCR && value.endsWith("\r")) {
769
+ if (allowPendingCR && value.charCodeAt(value.length - 1) === 13) {
571
770
  this.pendingCR = true;
572
771
  value = value.slice(0, -1);
573
772
  }
574
- const normalized = value.replace(/\r\n?/g, "\n");
575
- return `${prefix}${normalized}`;
773
+ const normalized = !value.includes("\r") ? value : value.replace(CRLF_RE, "\n");
774
+ return prefix ? `${prefix}${normalized}` : normalized;
775
+ }
776
+ _advanceSpan(start, end) {
777
+ const length = end - start;
778
+ this.offset += length;
779
+ if (!this.trackPosition) {
780
+ return;
781
+ }
782
+ let pos = this.buffer.indexOf("\n", start);
783
+ if (pos === -1 || pos >= end) {
784
+ this.column += length;
785
+ return;
786
+ }
787
+ let newlineCount = 0;
788
+ let lastNewline = -1;
789
+ while (pos !== -1 && pos < end) {
790
+ newlineCount += 1;
791
+ lastNewline = pos;
792
+ pos = this.buffer.indexOf("\n", pos + 1);
793
+ }
794
+ this.line += newlineCount;
795
+ this.column = end - lastNewline;
576
796
  }
577
797
  _flushPendingCR() {
578
798
  if (!this.pendingCR) {
579
799
  return;
580
800
  }
581
801
  this.pendingCR = false;
582
- this.options.onText?.("\n");
802
+ this._emitDecodedText("\n");
583
803
  }
584
804
  _error(message) {
585
- const error = new XmlSaxError(message, this.offset, this.line, this.column);
586
- this.options.onError?.(error);
805
+ const line = this.trackPosition ? this.line : 0;
806
+ const column = this.trackPosition ? this.column : 0;
807
+ const error = new XmlSaxError(message, this.offset, line, column);
808
+ this.onError?.(error);
587
809
  throw error;
588
810
  }
589
811
  };
@@ -597,7 +819,7 @@ var TreeBuilder = class {
597
819
  const node = {
598
820
  name: tag.name,
599
821
  attributes: Object.fromEntries(
600
- Object.entries(tag.attributes).map(([key, attr]) => [key, attr.value])
822
+ Object.entries(tag.attributes).map(([key, attr]) => [key, typeof attr === "string" ? attr : attr.value])
601
823
  ),
602
824
  children: []
603
825
  };
@@ -737,11 +959,13 @@ function stripNamespace(name) {
737
959
  }
738
960
  function resolveName(value) {
739
961
  if (typeof value !== "string") {
962
+ const prefix = value.prefix ?? "";
963
+ const local = value.local ?? (prefix ? value.name.slice(prefix.length + 1) : value.name);
740
964
  return {
741
965
  name: value.name,
742
- localName: value.local,
743
- prefix: value.prefix,
744
- uri: value.uri
966
+ localName: local,
967
+ prefix,
968
+ uri: value.uri ?? ""
745
969
  };
746
970
  }
747
971
  const index = value.indexOf(":");
@@ -759,6 +983,16 @@ function buildObject(root, options = {}) {
759
983
  const settings = buildSettings(options);
760
984
  return buildNode(root, settings, []);
761
985
  }
986
+ function buildXmlNode(obj, options = {}) {
987
+ const settings = buildXmlSettings(options);
988
+ const root = resolveRoot(obj, settings);
989
+ const rootName = normalizeName(root.name, settings);
990
+ return buildElement(rootName, root.value, settings, []);
991
+ }
992
+ function objectToXml(obj, options = {}) {
993
+ const node = buildXmlNode(obj, options);
994
+ return serializeXml(node, options);
995
+ }
762
996
  var ObjectBuilder = class {
763
997
  constructor(options = {}) {
764
998
  this.stack = [];
@@ -821,6 +1055,9 @@ var ObjectBuilder = class {
821
1055
  function buildSettings(options) {
822
1056
  return { ...DEFAULT_OBJECT_OPTIONS, ...options };
823
1057
  }
1058
+ function buildXmlSettings(options) {
1059
+ return { ...DEFAULT_OBJECT_OPTIONS, ...options };
1060
+ }
824
1061
  function buildNode(node, options, path) {
825
1062
  const name = normalizeName(node.name, options);
826
1063
  const attributes = normalizeAttributeMap(node.attributes ?? {}, options);
@@ -849,11 +1086,17 @@ function normalizeName(name, options) {
849
1086
  }
850
1087
  return name;
851
1088
  }
1089
+ function normalizeXmlName(name, options) {
1090
+ if (options.stripNamespaces) {
1091
+ return stripNamespace(name);
1092
+ }
1093
+ return name;
1094
+ }
852
1095
  function normalizeAttributes(attributes, options) {
853
1096
  const result = /* @__PURE__ */ Object.create(null);
854
1097
  for (const [key, attr] of Object.entries(attributes)) {
855
1098
  const name = normalizeName(key, options);
856
- result[name] = attr.value;
1099
+ result[name] = typeof attr === "string" ? attr : attr.value;
857
1100
  }
858
1101
  return result;
859
1102
  }
@@ -888,6 +1131,151 @@ function shouldForceArray(name, path, options) {
888
1131
  }
889
1132
  return rule(name, path);
890
1133
  }
1134
+ function resolveRoot(obj, options) {
1135
+ if (isRecord(obj)) {
1136
+ const keys = Object.keys(obj);
1137
+ if (keys.length === 1) {
1138
+ const name = keys[0] ?? "";
1139
+ return { name, value: obj[name] };
1140
+ }
1141
+ }
1142
+ if (!options.rootName) {
1143
+ throw new Error("Root element name is required when object has multiple keys");
1144
+ }
1145
+ return { name: options.rootName, value: obj };
1146
+ }
1147
+ function buildElement(name, value, options, path) {
1148
+ const attributes = /* @__PURE__ */ Object.create(null);
1149
+ const children = [];
1150
+ const nextPath = [...path, name];
1151
+ if (Array.isArray(value)) {
1152
+ for (const item of value) {
1153
+ appendContent(children, item, options, nextPath);
1154
+ }
1155
+ return finalizeNode(name, attributes, children);
1156
+ }
1157
+ if (isPrimitive(value)) {
1158
+ const text = coerceText(value);
1159
+ if (text !== null) {
1160
+ children.push(text);
1161
+ }
1162
+ return finalizeNode(name, attributes, children);
1163
+ }
1164
+ if (isRecord(value)) {
1165
+ for (const [key, entryValue] of Object.entries(value)) {
1166
+ if (isAttributeKey(key, options)) {
1167
+ const attrName = normalizeXmlName(key.slice(options.attributePrefix.length), options);
1168
+ const attrValue = coerceText(entryValue);
1169
+ if (attrValue !== null) {
1170
+ attributes[attrName] = attrValue;
1171
+ }
1172
+ continue;
1173
+ }
1174
+ if (key === options.textKey) {
1175
+ appendText(children, entryValue, options);
1176
+ continue;
1177
+ }
1178
+ const childName = normalizeXmlName(key, options);
1179
+ addChildElements(children, childName, entryValue, options, nextPath);
1180
+ }
1181
+ }
1182
+ return finalizeNode(name, attributes, children);
1183
+ }
1184
+ function addChildElements(children, name, value, options, path) {
1185
+ const forcedArray = shouldForceArray(name, path, options);
1186
+ const items = Array.isArray(value) ? value : forcedArray ? [value] : [value];
1187
+ for (const item of items) {
1188
+ if (item === void 0 || item === null) {
1189
+ children.push({ name });
1190
+ continue;
1191
+ }
1192
+ children.push(buildElement(name, item, options, path));
1193
+ }
1194
+ }
1195
+ function appendContent(children, value, options, path) {
1196
+ if (value === void 0 || value === null) {
1197
+ return;
1198
+ }
1199
+ if (Array.isArray(value)) {
1200
+ for (const item of value) {
1201
+ appendContent(children, item, options, path);
1202
+ }
1203
+ return;
1204
+ }
1205
+ if (isPrimitive(value)) {
1206
+ const text = coerceText(value);
1207
+ if (text !== null) {
1208
+ children.push(text);
1209
+ }
1210
+ return;
1211
+ }
1212
+ if (isRecord(value)) {
1213
+ for (const [key, entryValue] of Object.entries(value)) {
1214
+ const childName = normalizeXmlName(key, options);
1215
+ addChildElements(children, childName, entryValue, options, path);
1216
+ }
1217
+ }
1218
+ }
1219
+ function appendText(children, value, options) {
1220
+ if (value === void 0 || value === null) {
1221
+ return;
1222
+ }
1223
+ if (Array.isArray(value)) {
1224
+ const parts = value.map((item) => coerceText(item)).filter((item) => item !== null);
1225
+ if (parts.length === 0) {
1226
+ return;
1227
+ }
1228
+ if (options.coalesceText) {
1229
+ children.push(parts.join(""));
1230
+ return;
1231
+ }
1232
+ for (const part of parts) {
1233
+ children.push(part);
1234
+ }
1235
+ return;
1236
+ }
1237
+ const text = coerceText(value);
1238
+ if (text !== null) {
1239
+ children.push(text);
1240
+ }
1241
+ }
1242
+ function finalizeNode(name, attributes, children) {
1243
+ const node = { name };
1244
+ if (Object.keys(attributes).length > 0) {
1245
+ node.attributes = attributes;
1246
+ }
1247
+ if (children.length > 0) {
1248
+ node.children = children;
1249
+ }
1250
+ return node;
1251
+ }
1252
+ function isAttributeKey(key, options) {
1253
+ if (!options.attributePrefix) {
1254
+ return false;
1255
+ }
1256
+ return key.startsWith(options.attributePrefix) && key.length > options.attributePrefix.length;
1257
+ }
1258
+ function isRecord(value) {
1259
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1260
+ }
1261
+ function isPrimitive(value) {
1262
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1263
+ }
1264
+ function coerceText(value) {
1265
+ if (value === void 0 || value === null) {
1266
+ return null;
1267
+ }
1268
+ if (typeof value === "string") {
1269
+ return value;
1270
+ }
1271
+ if (typeof value === "number" || typeof value === "boolean") {
1272
+ return String(value);
1273
+ }
1274
+ if (value instanceof Date) {
1275
+ return value.toISOString();
1276
+ }
1277
+ return null;
1278
+ }
891
1279
  function finalizeElement(state, options) {
892
1280
  const hasAttributes = Object.keys(state.attributes).length > 0;
893
1281
  const hasChildren = Object.keys(state.children).length > 0;
@@ -912,6 +1300,6 @@ function finalizeElement(state, options) {
912
1300
  return result;
913
1301
  }
914
1302
 
915
- export { ObjectBuilder, TreeBuilder, XmlSaxError, XmlSaxParser, buildObject, parseXmlString, resolveName, serializeXml, stripNamespace };
1303
+ export { ObjectBuilder, TreeBuilder, XmlSaxError, XmlSaxParser, buildObject, buildXmlNode, objectToXml, parseXmlString, resolveName, serializeXml, stripNamespace };
916
1304
  //# sourceMappingURL=index.js.map
917
1305
  //# sourceMappingURL=index.js.map