@poe-platform/safe-fs 0.1.817 → 0.1.819

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,4 +1,5 @@
1
- import { XmlFrames } from "./frames.js";
1
+ import { XmlAttributes } from "./attributes.js";
2
+ import { XmlFrames, bindNamespace, namespaceValue, namespaceMetadata, hasNamespace } from "./frames.js";
2
3
  import { validCharacter } from "./characters.js";
3
4
  export { normalizeXmlChunks } from "./characters.js";
4
5
  import { XmlSource } from "./source.js";
@@ -67,6 +68,41 @@ function qualifiedNameSync(name, cache) {
67
68
  }
68
69
  return parts;
69
70
  }
71
+ /** Validate a name span without retaining its spelling. */
72
+ function* validateNameSpan(source, start, end) {
73
+ let first = true, colon = -1, work = 0;
74
+ for (let offset = start; offset < end;) {
75
+ let point = yield* source.charCodeAt(offset);
76
+ if (point >= 0xd800 && point <= 0xdbff && offset + 1 < end) {
77
+ const low = yield* source.charCodeAt(offset + 1);
78
+ if (low >= 0xdc00 && low <= 0xdfff)
79
+ point = 0x10000 + (point - 0xd800) * 1024 + low - 0xdc00;
80
+ }
81
+ if (point === 58) {
82
+ if (colon >= 0 || first)
83
+ invalid("invalid qualified name");
84
+ colon = offset;
85
+ first = true;
86
+ }
87
+ else {
88
+ if (!(first ? nameStart(point) : namePart(point)))
89
+ invalid("invalid qualified name");
90
+ first = false;
91
+ }
92
+ const size = point > 0xffff ? 2 : 1;
93
+ offset += size;
94
+ work += size;
95
+ if (work >= 512) {
96
+ yield 512;
97
+ work -= 512;
98
+ }
99
+ }
100
+ if (first)
101
+ invalid("invalid qualified name");
102
+ if (work)
103
+ yield work;
104
+ return colon;
105
+ }
70
106
  function* entity(source, start, end, recover) {
71
107
  // Inspect only a bounded prefix, even for arbitrarily long numeric references.
72
108
  const prefix = typeof source === "string" ? source.slice(start, Math.min(end, start + 4))
@@ -101,7 +137,7 @@ function* entity(source, start, end, recover) {
101
137
  return String.fromCodePoint(point);
102
138
  }
103
139
  /** Decoded text fragments; source read/work requests remain host-serviced. */
104
- function* textParts(source, start, end, decode, recover) {
140
+ function* textParts(source, start, end, decode, recover, attribute = false) {
105
141
  let cursor = start;
106
142
  while (cursor < end) {
107
143
  let finish = Math.min(end, cursor + 512);
@@ -110,7 +146,13 @@ function* textParts(source, start, end, decode, recover) {
110
146
  if (last >= 0xd800 && last <= 0xdbff)
111
147
  finish--;
112
148
  }
113
- const raw = yield* source.slice(cursor, finish);
149
+ let raw = yield* source.slice(cursor, finish);
150
+ if (attribute) {
151
+ let normalized = "";
152
+ for (const character of raw)
153
+ normalized += "\t\n\r".includes(character) ? " " : character;
154
+ raw = normalized;
155
+ }
114
156
  const found = decode ? raw.indexOf("&") : -1;
115
157
  if (found < 0) {
116
158
  yield raw;
@@ -139,9 +181,9 @@ function* textParts(source, start, end, decode, recover) {
139
181
  cursor = semicolon + 1;
140
182
  }
141
183
  }
142
- function* textFragments(source, start, end, decode, recover) {
184
+ function* textFragments(source, start, end, decode, recover, attribute = false) {
143
185
  let pending = "";
144
- for (const part of textParts(source, start, end, decode, recover)) {
186
+ for (const part of textParts(source, start, end, decode, recover, attribute)) {
145
187
  if (typeof part !== "string") {
146
188
  yield part;
147
189
  continue;
@@ -253,8 +295,22 @@ function validateLimits(limits) {
253
295
  }
254
296
  export function* parseXmlSourceSteps(length, limits = {}) {
255
297
  validateLimits(limits);
298
+ if (limits.deferAttributeNames && (!limits.storeAttributes || !limits.storeNamespaces))
299
+ throw new TypeError("Deferred attribute names require stored attributes and namespaces");
300
+ if (limits.deferElementNames && limits.retainTree !== false)
301
+ throw new TypeError("Deferred element names require retainTree: false");
302
+ if (limits.deferContentNames && !limits.fragmentContent)
303
+ throw new TypeError("Deferred XML content names require fragmentContent: true");
304
+ if (limits.fragmentAttributes && !limits.storeAttributes)
305
+ throw new TypeError("XML attribute fragments require storeAttributes: true");
256
306
  if (limits.storeFrames && limits.retainTree !== false)
257
307
  throw new TypeError("Stored XML frames require retainTree: false");
308
+ if (limits.storeAttributes && limits.retainTree !== false)
309
+ throw new TypeError("Stored XML attributes require retainTree: false");
310
+ if (limits.deferNamespaces && !limits.storeNamespaces)
311
+ throw new TypeError("Deferred XML namespaces require stored namespaces");
312
+ if (limits.storeNamespaces && limits.retainTree !== false)
313
+ throw new TypeError("Stored XML namespaces require retainTree: false");
258
314
  if (limits.fragmentContent && limits.retainTree !== false)
259
315
  throw new TypeError("XML content fragments require retainTree: false");
260
316
  const source = new XmlSource(length);
@@ -277,6 +333,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
277
333
  throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
278
334
  textLength += text.length;
279
335
  };
336
+ const initialNamespaces = limits.storeNamespaces ? yield* bindNamespace({ reference: 0, size: 0 }, "xml", xmlNamespace) : new Map([["xml", xmlNamespace]]);
280
337
  const stack = new XmlFrames(limits.storeFrames === true);
281
338
  let root;
282
339
  const prolog = [];
@@ -308,7 +365,24 @@ export function* parseXmlSourceSteps(length, limits = {}) {
308
365
  }
309
366
  return offset - start;
310
367
  };
311
- const scanName = function* () {
368
+ const sameName = function* (left, right, span) {
369
+ if (!left.nameSource && !span)
370
+ return left.name === right;
371
+ const leftLength = left.nameSource ? left.nameSource.end - left.nameSource.start : left.name.length;
372
+ const rightLength = span ? span.end - span.start : right.length;
373
+ if (leftLength !== rightLength)
374
+ return false;
375
+ for (let index = 0; index < leftLength; index += 512) {
376
+ const end = Math.min(leftLength, index + 512);
377
+ const a = left.nameSource ? yield* source.slice(left.nameSource.start + index, left.nameSource.start + end) : left.name.slice(index, end);
378
+ const b = span ? yield* source.slice(span.start + index, span.start + end) : right.slice(index, end);
379
+ yield end - index;
380
+ if (a !== b)
381
+ return false;
382
+ }
383
+ return true;
384
+ };
385
+ const scanName = function* (defer) {
312
386
  const start = offset;
313
387
  while (yield* source.has(offset)) {
314
388
  const c = (yield* source.charCodeAt(offset));
@@ -321,6 +395,21 @@ export function* parseXmlSourceSteps(length, limits = {}) {
321
395
  pendingWork -= 512;
322
396
  }
323
397
  }
398
+ if (defer && offset - start > 512) {
399
+ const colon = yield* validateNameSpan(source, start, offset);
400
+ if (defer === "content")
401
+ return ["", "", "", { start, end: offset }];
402
+ if (colon - start > 512 && !limits.storeNamespaces) {
403
+ const name = yield* source.slice(start, offset);
404
+ return [name, name.slice(0, colon - start), name.slice(colon - start + 1)];
405
+ }
406
+ const localStart = colon < 0 ? start : colon + 1;
407
+ const prefixSource = colon - start > 512 ? { start, end: colon } : undefined;
408
+ const prefix = colon < 0 || prefixSource ? "" : yield* source.slice(start, colon);
409
+ const localNameSource = offset - localStart > 512 ? { start: localStart, end: offset } : undefined;
410
+ const local = localNameSource ? "" : yield* source.slice(localStart, offset);
411
+ return ["", prefix, local, { start, end: offset }, localNameSource, prefixSource];
412
+ }
324
413
  const name = (yield* source.slice(start, offset));
325
414
  pendingWork += offset - start;
326
415
  while (pendingWork >= 512) {
@@ -330,7 +419,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
330
419
  const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
331
420
  return [name, prefix, localName];
332
421
  };
333
- const rawContent = function* (kind, start, end, parent, target = "") {
422
+ const rawContent = function* (kind, start, end, parent, target = "", targetSource) {
334
423
  if (end - start > maxTextLength - textLength)
335
424
  throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
336
425
  textLength += end - start;
@@ -344,7 +433,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
344
433
  finish--;
345
434
  }
346
435
  const text = yield* source.slice(cursor, finish);
347
- const content = kind === "processing-instruction" ? { kind, target, text } : { kind, text };
436
+ const content = kind === "processing-instruction" ? { kind, target, text, ...(targetSource ? { targetSource } : {}) } : { kind, text };
348
437
  if (kind === "cdata" && retainTree)
349
438
  parent.element.text += text;
350
439
  if (retain)
@@ -480,7 +569,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
480
569
  else if ((yield* source.startsWith("<?", offset))) {
481
570
  const start = offset;
482
571
  offset += 2;
483
- const [target] = yield* scanName();
572
+ const [target, , , targetSource] = yield* scanName(limits.deferContentNames ? "content" : undefined);
484
573
  while (pendingWork >= 512) {
485
574
  yield 512;
486
575
  pendingWork -= 512;
@@ -513,7 +602,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
513
602
  yield 512;
514
603
  pendingWork -= 512;
515
604
  }
516
- yield* rawContent("processing-instruction", body, end, yield* stack.peek(), target);
605
+ yield* rawContent("processing-instruction", body, end, yield* stack.peek(), target, targetSource);
517
606
  }
518
607
  offset = end + 2;
519
608
  }
@@ -522,14 +611,14 @@ export function* parseXmlSourceSteps(length, limits = {}) {
522
611
  }
523
612
  else if ((yield* source.charCodeAt(offset + 1)) === 47) {
524
613
  offset += 2;
525
- const [name] = yield* scanName();
614
+ const [name, , , nameSource] = yield* scanName(limits.deferElementNames ? "element" : undefined);
526
615
  yield* skipWhitespace();
527
616
  while (pendingWork >= 512) {
528
617
  yield 512;
529
618
  pendingWork -= 512;
530
619
  }
531
620
  const closed = (yield* source.slice(offset++, offset)) === ">" ? (yield* stack.pop()) : undefined;
532
- if (closed?.name !== name) {
621
+ if (!closed || !(yield* sameName(closed.element, name, nameSource))) {
533
622
  if (!limits.recover)
534
623
  invalid("mismatched closing tag");
535
624
  limits.recover("mismatched closing tag");
@@ -540,9 +629,9 @@ export function* parseXmlSourceSteps(length, limits = {}) {
540
629
  else {
541
630
  offset++;
542
631
  const repeated = previousEmpty && (yield* source.startsWith(previousEmpty.suffix, offset)) ? previousEmpty : undefined;
543
- const [name, prefix, localName] = repeated
632
+ const [name, prefix, localName, nameSource, localNameSource, prefixSource] = repeated
544
633
  ? [repeated.name, repeated.prefix, repeated.localName]
545
- : (yield* scanName());
634
+ : (yield* scanName(limits.deferElementNames ? "element" : undefined));
546
635
  if (repeated)
547
636
  offset += name.length;
548
637
  // Preserve scan/validation work while reusing the admitted spelling.
@@ -552,9 +641,9 @@ export function* parseXmlSourceSteps(length, limits = {}) {
552
641
  yield 512;
553
642
  pendingWork -= 512;
554
643
  }
555
- let attributes;
556
- const attrMeta = [];
557
- let namespaces = (yield* stack.peek())?.namespaces ?? new Map([["xml", xmlNamespace]]);
644
+ const attributes = new XmlAttributes(limits.storeAttributes === true);
645
+ const namespaceParent = yield* stack.peek();
646
+ let namespaces = namespaceParent?.namespaceScope ?? namespaceParent?.namespaces ?? (initialNamespaces instanceof Map ? new Map(initialNamespaces) : initialNamespaces);
558
647
  let ownsNamespaces = stack.length === 0;
559
648
  while (!repeated) {
560
649
  const ws = (yield* skipWhitespace());
@@ -567,16 +656,16 @@ export function* parseXmlSourceSteps(length, limits = {}) {
567
656
  break;
568
657
  if (ws === 0)
569
658
  invalid("attributes require whitespace");
570
- const [attribute, attrPrefix, attrLocal] = yield* scanName();
659
+ const [attribute, attrPrefix, attrLocal, nameSource, localNameSource, attrPrefixSource] = yield* scanName(limits.deferAttributeNames ? "attribute" : undefined);
571
660
  while (pendingWork >= 512) {
572
661
  yield 512;
573
662
  pendingWork -= 512;
574
663
  }
575
- if (attributes?.has(attribute))
664
+ if ((yield* attributes.has(attribute, nameSource)))
576
665
  invalid("duplicate attribute");
577
666
  if (++attributeCount > maxAttributes)
578
667
  throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
579
- if ((attributes?.size ?? 0) >= maxAttributesPerElement)
668
+ if (attributes.size >= maxAttributesPerElement)
580
669
  throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
581
670
  yield* skipWhitespace();
582
671
  if ((yield* source.slice(offset++, offset)) !== "=")
@@ -594,31 +683,59 @@ export function* parseXmlSourceSteps(length, limits = {}) {
594
683
  }
595
684
  if (end < 0)
596
685
  invalid("unterminated attribute");
597
- const raw = (yield* source.slice(offset, end));
598
- pendingWork += raw.length * 2;
599
- while (pendingWork >= 512) {
600
- yield 512;
601
- pendingWork -= 512;
686
+ const fragmented = limits.fragmentAttributes && (limits.storeNamespaces || attribute !== "xmlns" && attrPrefix !== "xmlns");
687
+ const valueStart = offset;
688
+ let value = "", size = 0, matchesXml = true, matchesXmlns = true;
689
+ if (fragmented) {
690
+ if ((yield* source.indexOf("<", offset, end)) >= 0)
691
+ invalid("less-than in attribute");
692
+ for (const part of textFragments(source, offset, end, true, limits.recover, true)) {
693
+ if (typeof part !== "string")
694
+ yield part;
695
+ else {
696
+ matchesXml = matchesXml && part === xmlNamespace.slice(size, size + part.length);
697
+ matchesXmlns = matchesXmlns && part === xmlnsNamespace.slice(size, size + part.length);
698
+ size += part.length;
699
+ }
700
+ }
701
+ matchesXml = matchesXml && size === xmlNamespace.length;
702
+ matchesXmlns = matchesXmlns && size === xmlnsNamespace.length;
703
+ if (size > maxTextLength - textLength)
704
+ throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
705
+ textLength += size;
602
706
  }
603
- if (raw.indexOf("<") >= 0)
604
- invalid("less-than in attribute");
605
- const normalized = /[\t\n\r]/.test(raw) ? raw.replace(/[\t\n\r]/g, " ") : raw;
606
- const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized, limits.recover);
607
- while (pendingWork >= 512) {
608
- yield 512;
609
- pendingWork -= 512;
707
+ else {
708
+ const raw = (yield* source.slice(offset, end));
709
+ pendingWork += raw.length * 2;
710
+ while (pendingWork >= 512) {
711
+ yield 512;
712
+ pendingWork -= 512;
713
+ }
714
+ if (raw.indexOf("<") >= 0)
715
+ invalid("less-than in attribute");
716
+ let normalized = raw;
717
+ if (raw.includes("\t") || raw.includes("\n") || raw.includes("\r")) {
718
+ normalized = "";
719
+ for (const character of raw)
720
+ normalized += "\t\n\r".includes(character) ? " " : character;
721
+ }
722
+ value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized, limits.recover);
723
+ while (pendingWork >= 512) {
724
+ yield 512;
725
+ pendingWork -= 512;
726
+ }
727
+ admitText(value);
610
728
  }
611
- admitText(value);
612
- (attributes ??= new Map()).set(attribute, value);
613
- attrMeta.push([attribute, attrPrefix, attrLocal]);
729
+ yield* attributes.append(attribute, attrPrefix, attrLocal, value, limits.fragmentAttributes ? { start: offset, end } : undefined, nameSource, localNameSource, attrPrefixSource);
614
730
  offset = end + 1;
615
- if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
616
- const nsPrefix = attribute === "xmlns" ? "" : attribute.slice(6);
617
- if (nsPrefix === "xmlns" || value === xmlnsNamespace
618
- || (nsPrefix === "xml") !== (value === xmlNamespace)
619
- || (nsPrefix !== "" && value === ""))
731
+ if (attribute === "xmlns" || attrPrefix === "xmlns") {
732
+ const nsPrefix = attribute === "xmlns" ? "" : attrLocal;
733
+ const nsPrefixSource = attrPrefix === "xmlns" ? localNameSource : undefined;
734
+ if (nsPrefix === "xmlns" || (fragmented ? matchesXmlns : value === xmlnsNamespace)
735
+ || (nsPrefix === "xml") !== (fragmented ? matchesXml : value === xmlNamespace)
736
+ || ((nsPrefix !== "" || nsPrefixSource) && (fragmented ? size === 0 : value === "")))
620
737
  invalid("invalid namespace binding");
621
- if (!ownsNamespaces) {
738
+ if (!ownsNamespaces && namespaces instanceof Map) {
622
739
  const copy = new Map();
623
740
  for (const [key, uri] of namespaces) {
624
741
  copy.set(key, uri);
@@ -627,25 +744,27 @@ export function* parseXmlSourceSteps(length, limits = {}) {
627
744
  namespaces = copy;
628
745
  ownsNamespaces = true;
629
746
  }
630
- namespaces.set(nsPrefix, value);
747
+ namespaces = yield* bindNamespace(namespaces, nsPrefix, fragmented
748
+ ? textFragments(source, valueStart, end, true, limits.recover ? () => { } : undefined, true) : value, nsPrefixSource);
631
749
  if (namespaces.size > maxNamespaces)
632
750
  throw new XmlLimitError("maxNamespaces", "XML namespace scope limit exceeded");
633
751
  }
634
752
  }
635
- if (attrMeta.length > 0) {
636
- const expanded = attrMeta.length > 1 ? new Set() : undefined;
637
- for (let i = 0; i < attrMeta.length; i++) {
638
- const [attribute, attrPrefix, attrLocal] = attrMeta[i];
639
- if (attribute === "xmlns" || attribute.startsWith("xmlns:"))
753
+ if (attributes.length > 0) {
754
+ for (let cursor = attributes.first; cursor;) {
755
+ const record = yield* attributes.read(cursor, false);
756
+ cursor = record.next;
757
+ const { name: attribute, prefix: attrPrefix, localName: attrLocal } = record;
758
+ if (attribute === "xmlns" || attrPrefix === "xmlns")
640
759
  continue;
641
760
  pendingWork += attribute.length;
642
- if (attrPrefix && !namespaces.has(attrPrefix))
761
+ if ((attrPrefix || record.prefixSource) && !(yield* hasNamespace(namespaces, attrPrefix, record.prefixSource)))
643
762
  invalid("unbound attribute prefix");
644
- if (expanded) {
645
- const key = `${attrPrefix ? namespaces.get(attrPrefix) : ""}\0${attrLocal}`;
646
- if (expanded.has(key))
763
+ if (attributes.length > 1) {
764
+ const backed = limits.storeAttributes && !(namespaces instanceof Map) ? namespaces : undefined;
765
+ const key = backed ? attrLocal : `${attrPrefix ? (yield* namespaceValue(namespaces, attrPrefix)) : ""}\0${attrLocal}`;
766
+ if (yield* attributes.expanded(key, backed ? { scope: backed, prefix: attrPrefix, ...(record.prefixSource ? { prefixSource: record.prefixSource } : {}) } : undefined, record.localNameSource))
647
767
  invalid("duplicate expanded attribute");
648
- expanded.add(key);
649
768
  }
650
769
  }
651
770
  }
@@ -654,23 +773,26 @@ export function* parseXmlSourceSteps(length, limits = {}) {
654
773
  yield 512;
655
774
  pendingWork -= 512;
656
775
  }
657
- if (prefix === "xmlns" || (prefix && !namespaces.has(prefix)))
776
+ if (prefix === "xmlns" || ((prefix || prefixSource) && !(yield* hasNamespace(namespaces, prefix, prefixSource))))
658
777
  invalid("unbound element prefix");
659
778
  if (++nodes > maxNodes)
660
779
  throw new XmlLimitError("maxNodes", "XML resource limit exceeded");
661
780
  if (stack.length + 1 > maxDepth)
662
781
  throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
663
- const namespace = namespaces.get(prefix) ?? "";
664
- limits.onElement?.({ name, namespace, localName }, (yield* stack.peek())?.element, stack.length + 1);
782
+ const namespace = yield* namespaceMetadata(namespaces, prefix, limits.deferNamespaces, prefixSource);
783
+ const elementName = { name, localName, ...(nameSource ? { nameSource, prefix } : {}), ...(localNameSource ? { localNameSource } : {}), ...(prefixSource ? { prefixSource } : {}) };
784
+ limits.onElement?.({ ...elementName, ...namespace }, (yield* stack.peek())?.element, stack.length + 1);
665
785
  admitContent();
666
786
  const retainedAttributes = [];
667
- if (retainContent && attributes) {
668
- for (let i = 0; i < attrMeta.length; i++) {
669
- const [attribute, attrPrefix, attrLocal] = attrMeta[i];
670
- const value = attributes.get(attribute);
787
+ if (retainContent) {
788
+ for (let cursor = attributes.first; cursor;) {
789
+ const record = yield* attributes.read(cursor, !limits.storeAttributes);
790
+ cursor = record.next;
791
+ const { name: attribute, prefix: attrPrefix, localName: attrLocal, value } = record;
671
792
  admitContent();
672
- retainedAttributes.push({ name: attribute, localName: attrLocal,
673
- namespace: attribute === "xmlns" || attrPrefix === "xmlns" ? xmlnsNamespace : attrPrefix ? namespaces.get(attrPrefix) : "", value });
793
+ if (!limits.storeAttributes)
794
+ retainedAttributes.push({ name: attribute, localName: attrLocal,
795
+ namespace: attribute === "xmlns" || attrPrefix === "xmlns" ? xmlnsNamespace : attrPrefix ? (yield* namespaceValue(namespaces, attrPrefix)) : "", value });
674
796
  pendingWork += attribute.length + 1;
675
797
  }
676
798
  }
@@ -679,7 +801,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
679
801
  pendingWork -= 512;
680
802
  }
681
803
  const content = retain ? [] : undefined;
682
- const element = { kind: "element", name, namespace, localName, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent ? namespaces : emptyNamespaces, ...(root === undefined && retainContent ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
804
+ const element = { kind: "element", ...elementName, ...namespace, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent && namespaces instanceof Map ? namespaces : emptyNamespaces, ...(root === undefined && retainContent ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
683
805
  const parent = (yield* stack.peek());
684
806
  if (parent) {
685
807
  if (retainTree) {
@@ -701,11 +823,37 @@ export function* parseXmlSourceSteps(length, limits = {}) {
701
823
  }
702
824
  // Only cache compact, attribute-free spellings. Namespace resolution and
703
825
  // every admission counter still run for each distinct physical element.
704
- if (empty && !attributes && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
826
+ if (empty && !nameSource && !attributes.length && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
705
827
  previousEmpty = { suffix: name + "/>", name, prefix, localName };
706
828
  limits.events?.({ type: "open", element, parent: parent?.element });
829
+ if (limits.storeAttributes && limits.onAttribute && retainContent) {
830
+ yield 1;
831
+ for (let cursor = attributes.first; cursor;) {
832
+ const attribute = yield* attributes.read(cursor, true);
833
+ cursor = attribute.next;
834
+ const namespace = attribute.name === "xmlns" || attribute.prefix === "xmlns" ? { namespace: xmlnsNamespace }
835
+ : attribute.prefix || attribute.prefixSource ? yield* namespaceMetadata(namespaces, attribute.prefix, limits.deferNamespaces, attribute.prefixSource) : { namespace: "" };
836
+ const nameMetadata = { name: attribute.name, localName: attribute.localName, ...(attribute.nameSource ? { nameSource: attribute.nameSource, prefix: attribute.prefix } : {}), ...(attribute.localNameSource ? { localNameSource: attribute.localNameSource } : {}), ...(attribute.prefixSource ? { prefixSource: attribute.prefixSource } : {}) };
837
+ let continuation = false;
838
+ if (attribute.source) {
839
+ for (const part of textFragments(source, attribute.source.start, attribute.source.end, true, limits.recover ? () => { } : undefined, true)) {
840
+ if (typeof part !== "string") {
841
+ yield part;
842
+ continue;
843
+ }
844
+ limits.onAttribute({ ...nameMetadata, ...namespace, value: part }, element, continuation);
845
+ continuation = true;
846
+ yield 1;
847
+ }
848
+ }
849
+ if (!continuation) {
850
+ limits.onAttribute({ ...nameMetadata, ...namespace, value: attribute.value }, element);
851
+ yield 1;
852
+ }
853
+ }
854
+ }
707
855
  if (!empty)
708
- yield* stack.push({ element, content, name, namespaces });
856
+ yield* stack.push({ element, content, name, namespaces: namespaces instanceof Map ? namespaces : new Map(), ...(namespaces instanceof Map ? {} : { namespaceScope: namespaces }) });
709
857
  else
710
858
  limits.events?.({ type: "close", element, parent: parent?.element });
711
859
  }
@@ -1,3 +1,7 @@
1
+ export interface XmlSourceSpan {
2
+ readonly start: number;
3
+ readonly end: number;
4
+ }
1
5
  /** Fill value before advancing. Streaming reads may be short; mark complete at EOF. */
2
6
  export interface XmlSourceRead {
3
7
  readonly offset: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.817",
3
+ "version": "0.1.819",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",