@graphty/graph-io 0.3.38 → 0.3.40

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 (152) hide show
  1. package/dist/chunks/{choose-CsO24U-5.js → choose-CS3fvGT1.js} +2 -2
  2. package/dist/chunks/{choose-CsO24U-5.js.map → choose-CS3fvGT1.js.map} +1 -1
  3. package/dist/chunks/{constants-BPRiVyBu.js → constants-DqKZT6cH.js} +2 -2
  4. package/dist/chunks/{constants-BPRiVyBu.js.map → constants-DqKZT6cH.js.map} +1 -1
  5. package/dist/chunks/{escape-BRcyzD77.js → escape-BJRLd3vJ.js} +4 -3
  6. package/dist/chunks/escape-BJRLd3vJ.js.map +1 -0
  7. package/dist/chunks/{export-CTUlibzr.js → export-B-aVCwyG.js} +103 -92
  8. package/dist/chunks/export-B-aVCwyG.js.map +1 -0
  9. package/dist/chunks/{exporter-Bf5tZA0X.js → exporter-6ocL_9x_.js} +6 -5
  10. package/dist/chunks/exporter-6ocL_9x_.js.map +1 -0
  11. package/dist/chunks/{exporter-Bn4jdYfd.js → exporter-BVXnV9-9.js} +43 -30
  12. package/dist/chunks/exporter-BVXnV9-9.js.map +1 -0
  13. package/dist/chunks/{exporter-D-qQuRcG.js → exporter-Bcp-l4Bo.js} +6 -6
  14. package/dist/chunks/exporter-Bcp-l4Bo.js.map +1 -0
  15. package/dist/chunks/{exporter-F0vT919P.js → exporter-SFYkDzD0.js} +5 -4
  16. package/dist/chunks/exporter-SFYkDzD0.js.map +1 -0
  17. package/dist/chunks/{importer-Bgb9sgZZ.js → importer-AjwOJrn2.js} +6 -8
  18. package/dist/chunks/importer-AjwOJrn2.js.map +1 -0
  19. package/dist/chunks/{importer-BuzbaFFY.js → importer-BJlGAOht.js} +28 -13
  20. package/dist/chunks/importer-BJlGAOht.js.map +1 -0
  21. package/dist/chunks/{importer-DqMtyYLX.js → importer-BpuS3J8C.js} +12 -14
  22. package/dist/chunks/importer-BpuS3J8C.js.map +1 -0
  23. package/dist/chunks/{importer-CEuUmyPe.js → importer-CPqnIY8m.js} +6 -6
  24. package/dist/chunks/{importer-CEuUmyPe.js.map → importer-CPqnIY8m.js.map} +1 -1
  25. package/dist/chunks/{importer-yWUPR_nU.js → importer-DEp_fYjf.js} +30 -12
  26. package/dist/chunks/importer-DEp_fYjf.js.map +1 -0
  27. package/dist/chunks/{importer-CNMQnHDd.js → importer-D_srHyj6.js} +112 -62
  28. package/dist/chunks/importer-D_srHyj6.js.map +1 -0
  29. package/dist/chunks/{importer-oIUx9u_g.js → importer-DcKB97kW.js} +40 -18
  30. package/dist/chunks/importer-DcKB97kW.js.map +1 -0
  31. package/dist/chunks/{json-elements-AkglPS3m.js → json-elements-DpO6AXIc.js} +7 -5
  32. package/dist/chunks/{json-elements-AkglPS3m.js.map → json-elements-DpO6AXIc.js.map} +1 -1
  33. package/dist/chunks/{ontology-export-DDoG3egY.js → ontology-export-50L9DOV4.js} +2 -2
  34. package/dist/chunks/{ontology-export-DDoG3egY.js.map → ontology-export-50L9DOV4.js.map} +1 -1
  35. package/dist/chunks/{records-ChmWZGuz.js → records-r3SFHz11.js} +137 -96
  36. package/dist/chunks/records-r3SFHz11.js.map +1 -0
  37. package/dist/chunks/{text-Btw3sBe_.js → text-Bjfa1xYL.js} +2 -2
  38. package/dist/chunks/{text-Btw3sBe_.js.map → text-Bjfa1xYL.js.map} +1 -1
  39. package/dist/csv.js +4 -4
  40. package/dist/cx.js +1 -1
  41. package/dist/cx2.js +2 -2
  42. package/dist/cys.js +1 -1
  43. package/dist/dot.js +1 -1
  44. package/dist/gexf.js +6 -4
  45. package/dist/gexf.js.map +1 -1
  46. package/dist/gml.js +8 -6
  47. package/dist/gml.js.map +1 -1
  48. package/dist/graph-io.js +26 -26
  49. package/dist/graphml.js +2 -2
  50. package/dist/json.js +1 -1
  51. package/dist/neo4j.js +3 -3
  52. package/dist/obo.js +1 -1
  53. package/dist/pajek.js +1 -1
  54. package/dist/src/common/direction.js +1 -1
  55. package/dist/src/common/direction.js.map +1 -1
  56. package/dist/src/common/export.d.ts.map +1 -1
  57. package/dist/src/common/export.js +136 -71
  58. package/dist/src/common/export.js.map +1 -1
  59. package/dist/src/common/input.js +1 -1
  60. package/dist/src/common/input.js.map +1 -1
  61. package/dist/src/common/json-elements.d.ts.map +1 -1
  62. package/dist/src/common/json-elements.js +4 -2
  63. package/dist/src/common/json-elements.js.map +1 -1
  64. package/dist/src/common/xml.d.ts.map +1 -1
  65. package/dist/src/common/xml.js +2 -1
  66. package/dist/src/common/xml.js.map +1 -1
  67. package/dist/src/formats/csv/records.d.ts.map +1 -1
  68. package/dist/src/formats/csv/records.js +138 -100
  69. package/dist/src/formats/csv/records.js.map +1 -1
  70. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  71. package/dist/src/formats/cx/importer.js +108 -53
  72. package/dist/src/formats/cx/importer.js.map +1 -1
  73. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  74. package/dist/src/formats/cx2/exporter.js +2 -1
  75. package/dist/src/formats/cx2/exporter.js.map +1 -1
  76. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  77. package/dist/src/formats/cx2/importer.js +28 -6
  78. package/dist/src/formats/cx2/importer.js.map +1 -1
  79. package/dist/src/formats/dot/importer.d.ts +6 -0
  80. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  81. package/dist/src/formats/dot/importer.js +11 -6
  82. package/dist/src/formats/dot/importer.js.map +1 -1
  83. package/dist/src/formats/dot/names.d.ts +5 -0
  84. package/dist/src/formats/dot/names.d.ts.map +1 -1
  85. package/dist/src/formats/dot/names.js +5 -0
  86. package/dist/src/formats/dot/names.js.map +1 -1
  87. package/dist/src/formats/dot/tokenizer.js +9 -9
  88. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  89. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  90. package/dist/src/formats/gexf/exporter.js +4 -2
  91. package/dist/src/formats/gexf/exporter.js.map +1 -1
  92. package/dist/src/formats/gml/importer.d.ts +6 -0
  93. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  94. package/dist/src/formats/gml/importer.js +6 -3
  95. package/dist/src/formats/gml/importer.js.map +1 -1
  96. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  97. package/dist/src/formats/graphml/exporter.js +2 -1
  98. package/dist/src/formats/graphml/exporter.js.map +1 -1
  99. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  100. package/dist/src/formats/graphml/importer.js +5 -6
  101. package/dist/src/formats/graphml/importer.js.map +1 -1
  102. package/dist/src/formats/obo/importer.d.ts.map +1 -1
  103. package/dist/src/formats/obo/importer.js +2 -1
  104. package/dist/src/formats/obo/importer.js.map +1 -1
  105. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  106. package/dist/src/formats/pajek/exporter.js +4 -5
  107. package/dist/src/formats/pajek/exporter.js.map +1 -1
  108. package/dist/src/formats/xgmml/emit.d.ts +6 -0
  109. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  110. package/dist/src/formats/xgmml/emit.js +33 -10
  111. package/dist/src/formats/xgmml/emit.js.map +1 -1
  112. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  113. package/dist/src/formats/xgmml/exporter.js +54 -22
  114. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  115. package/dist/src/sniff.js +1 -1
  116. package/dist/src/sniff.js.map +1 -1
  117. package/dist/xgmml.js +2 -2
  118. package/package.json +2 -2
  119. package/src/common/direction.ts +1 -1
  120. package/src/common/export.ts +190 -97
  121. package/src/common/input.ts +1 -1
  122. package/src/common/json-elements.ts +4 -2
  123. package/src/common/xml.ts +2 -1
  124. package/src/formats/csv/records.ts +143 -99
  125. package/src/formats/cx/importer.ts +140 -58
  126. package/src/formats/cx2/exporter.ts +2 -1
  127. package/src/formats/cx2/importer.ts +34 -10
  128. package/src/formats/dot/importer.ts +35 -4
  129. package/src/formats/dot/names.ts +6 -0
  130. package/src/formats/dot/tokenizer.ts +9 -9
  131. package/src/formats/gexf/exporter.ts +4 -2
  132. package/src/formats/gml/importer.ts +14 -3
  133. package/src/formats/graphml/exporter.ts +2 -3
  134. package/src/formats/graphml/importer.ts +5 -6
  135. package/src/formats/obo/importer.ts +2 -1
  136. package/src/formats/pajek/exporter.ts +6 -4
  137. package/src/formats/xgmml/emit.ts +34 -10
  138. package/src/formats/xgmml/exporter.ts +63 -23
  139. package/src/sniff.ts +1 -1
  140. package/dist/chunks/escape-BRcyzD77.js.map +0 -1
  141. package/dist/chunks/export-CTUlibzr.js.map +0 -1
  142. package/dist/chunks/exporter-Bf5tZA0X.js.map +0 -1
  143. package/dist/chunks/exporter-Bn4jdYfd.js.map +0 -1
  144. package/dist/chunks/exporter-D-qQuRcG.js.map +0 -1
  145. package/dist/chunks/exporter-F0vT919P.js.map +0 -1
  146. package/dist/chunks/importer-Bgb9sgZZ.js.map +0 -1
  147. package/dist/chunks/importer-BuzbaFFY.js.map +0 -1
  148. package/dist/chunks/importer-CNMQnHDd.js.map +0 -1
  149. package/dist/chunks/importer-DqMtyYLX.js.map +0 -1
  150. package/dist/chunks/importer-oIUx9u_g.js.map +0 -1
  151. package/dist/chunks/importer-yWUPR_nU.js.map +0 -1
  152. package/dist/chunks/records-ChmWZGuz.js.map +0 -1
@@ -637,115 +637,208 @@ function checkColumns(
637
637
  note: NoteFn,
638
638
  ): void {
639
639
  for (const column of table) {
640
- const { meta } = column;
641
- const { name, role } = meta;
640
+ const { name, role } = column.meta;
642
641
  if (role !== null && STRUCTURAL_ROLES.has(role)) {
643
642
  continue;
644
643
  }
645
644
  const label = `${domain} column "${name}"`;
646
- const roleNames = roleNamesOf(domain, extras);
647
- if (role !== null && roleNames !== undefined) {
648
- const fixed = roleNames[role];
649
- if (fixed !== undefined && fixed !== name) {
650
- note(
651
- LOSS.COLUMN_NAME_CHANGED,
652
- `${label} (the ${ROLE_WORDS[role] ?? role}) is written as the format's own ${role} and reads back as "${fixed}"`,
653
- name,
654
- column.length - column.nullCount,
655
- );
656
- }
657
- }
645
+ noteRenamedRole(column, label, roleNamesOf(domain, extras), note);
658
646
  if (domain === "edge" && role === "id") {
659
647
  continue;
660
648
  }
661
- const named = extras.writtenColumns?.[domain as "node" | "edge"]?.includes(name) ?? false;
662
- if (
663
- writesAttributes(domain, extras) === false &&
664
- !named &&
665
- !(role !== null && (extras.roles?.has(role) ?? false))
666
- ) {
649
+ if (isDroppedColumn(column, domain, extras)) {
667
650
  note(LOSS.COLUMN_DROPPED, `${label} is not written`, name, column.length - column.nullCount);
668
651
  continue;
669
652
  }
670
- if (role === "timeText") {
671
- if (extras.temporalText !== true) {
672
- note(
673
- LOSS.TEMPORAL_TEXT,
674
- `${label} (the original text of a date or time column) cannot be written`,
675
- name,
676
- column.length - column.nullCount,
677
- );
678
- }
679
- continue;
680
- }
681
- if (role === "position") {
682
- if (!caps.positions) {
683
- note(LOSS.POSITIONS, `${label} (positions) cannot be written`, name, column.length - column.nullCount);
684
- } else if (extras.positionDtype !== undefined && meta.dtype !== extras.positionDtype) {
685
- note(
686
- LOSS.DTYPE,
687
- `${label} (positions) is ${meta.dtype}; the format reads positions back as ${extras.positionDtype}`,
688
- name,
689
- column.length - column.nullCount,
690
- );
691
- }
692
- continue;
693
- }
694
- if (role !== null && VIZ_ROLES.has(role)) {
695
- if (!caps.viz) {
696
- note(LOSS.VIZ, `${label} (${role}) cannot be written`, name, column.length - column.nullCount);
697
- }
698
- continue;
699
- }
700
- if (role !== null && HIERARCHY_ROLES.has(role)) {
701
- if (!caps.hierarchy) {
702
- note(LOSS.HIERARCHY, `${label} (${role}) cannot be written`, name, column.length - column.nullCount);
703
- }
704
- continue;
705
- }
706
- if (role !== null && TEMPORAL_ROLES.has(role)) {
707
- if (caps.temporal === "none") {
708
- note(LOSS.TEMPORAL, `${label} (${role}) cannot be written`, name, column.length - column.nullCount);
709
- }
710
- continue;
711
- }
712
- if (role === "spells") {
713
- if (caps.temporal !== "spells" && caps.temporal !== "dynamic-values") {
714
- note(LOSS.SPELLS, `${label} (spells) cannot be written`, name, column.length - column.nullCount);
715
- }
716
- continue;
717
- }
718
- if (role === "open" || role === "spellsOpen") {
719
- // The open bits are written by the format's own temporal syntax when it has one; the
720
- // column's u8 storage is never an attribute dtype, so no dtype note either way.
721
- if (extras.openIntervals !== true) {
722
- note(
723
- LOSS.OPEN_INTERVAL,
724
- `${label} (open intervals) cannot be written`,
725
- name,
726
- column.length - column.nullCount,
727
- );
728
- }
729
- continue;
730
- }
731
- if (role !== null && !(extras.roles?.has(role) ?? false)) {
732
- note(
733
- LOSS.ROLE,
734
- `${label} (${role}) is written as a plain attribute; the format has no ${role} slot and the role is lost`,
735
- name,
736
- column.length - column.nullCount,
737
- );
738
- }
739
- if (meta.dynamic && caps.temporal !== "dynamic-values") {
740
- note(LOSS.DYNAMIC_VALUES, `${label} is dynamic; only its static value can be written`, name, null);
741
- }
742
- checkDtype(column, label, caps, note);
743
- if (meta.default !== undefined && !caps.defaults) {
744
- note(LOSS.DEFAULT, `${label} declares a default; the format has no defaults`, name, null);
653
+ if (!checkSlotRole(column, label, caps, extras, note)) {
654
+ checkAttributeColumn(column, label, caps, extras, note);
745
655
  }
746
- if (meta.options !== null && !caps.options) {
747
- note(LOSS.OPTIONS, `${label} declares options; the format has no enumerations`, name, null);
656
+ }
657
+ }
658
+
659
+ /**
660
+ * The note of a role column the format writes under its own fixed name.
661
+ * @param column - the column
662
+ * @param label - the column's label for messages
663
+ * @param roleNames - the format's fixed names per role, if any
664
+ * @param note - the recorder
665
+ */
666
+ function noteRenamedRole(
667
+ column: Column,
668
+ label: string,
669
+ roleNames: Readonly<Record<string, string>> | undefined,
670
+ note: NoteFn,
671
+ ): void {
672
+ const { name, role } = column.meta;
673
+ if (role === null || roleNames === undefined) {
674
+ return;
675
+ }
676
+ const fixed = roleNames[role];
677
+ if (fixed !== undefined && fixed !== name) {
678
+ note(
679
+ LOSS.COLUMN_NAME_CHANGED,
680
+ `${label} (the ${ROLE_WORDS[role] ?? role}) is written as the format's own ${role} and reads back as "${fixed}"`,
681
+ name,
682
+ column.length - column.nullCount,
683
+ );
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Whether a format that writes no attributes of this kind drops the column (it is neither named
689
+ * in CheckExtras.writtenColumns nor of a role the format has a slot for).
690
+ * @param column - the column
691
+ * @param domain - "node", "edge" or "graph"
692
+ * @param extras - format extras
693
+ * @returns true when the column is not written
694
+ */
695
+ function isDroppedColumn(column: Column, domain: string, extras: CheckExtras): boolean {
696
+ const { name, role } = column.meta;
697
+ const named = extras.writtenColumns?.[domain as "node" | "edge"]?.includes(name) ?? false;
698
+ return (
699
+ writesAttributes(domain, extras) === false && !named && !(role !== null && (extras.roles?.has(role) ?? false))
700
+ );
701
+ }
702
+
703
+ /** How a format writes one slot role: what it loses when it cannot, and when it can. */
704
+ interface SlotRule {
705
+ readonly code: string;
706
+ /** What the message calls the column; the role itself when absent. */
707
+ readonly what?: string;
708
+ readonly written: (caps: ExportCapabilities, extras: CheckExtras) => boolean;
709
+ }
710
+
711
+ /**
712
+ * The roles (other than position) a format writes in its own syntax rather than as an attribute.
713
+ * @returns the rules by role
714
+ */
715
+ function slotRules(): ReadonlyMap<string, SlotRule> {
716
+ const rules = new Map<string, SlotRule>();
717
+ const add = (roles: Iterable<string>, rule: SlotRule): void => {
718
+ for (const role of roles) {
719
+ rules.set(role, rule);
748
720
  }
721
+ };
722
+ add(["timeText"], {
723
+ code: LOSS.TEMPORAL_TEXT,
724
+ what: "the original text of a date or time column",
725
+ written: (_caps, extras) => extras.temporalText === true,
726
+ });
727
+ add(VIZ_ROLES, { code: LOSS.VIZ, written: (caps) => caps.viz });
728
+ add(HIERARCHY_ROLES, { code: LOSS.HIERARCHY, written: (caps) => caps.hierarchy });
729
+ add(TEMPORAL_ROLES, { code: LOSS.TEMPORAL, written: (caps) => caps.temporal !== "none" });
730
+ add(["spells"], {
731
+ code: LOSS.SPELLS,
732
+ what: "spells",
733
+ written: (caps) => caps.temporal === "spells" || caps.temporal === "dynamic-values",
734
+ });
735
+ // The open bits are written by the format's own temporal syntax when it has one; the
736
+ // column's u8 storage is never an attribute dtype, so no dtype note either way.
737
+ add(["open", "spellsOpen"], {
738
+ code: LOSS.OPEN_INTERVAL,
739
+ what: "open intervals",
740
+ written: (_caps, extras) => extras.openIntervals === true,
741
+ });
742
+ return rules;
743
+ }
744
+
745
+ const SLOT_RULES = slotRules();
746
+
747
+ /**
748
+ * The checks of a column whose role the format writes in its own syntax (positions, viz,
749
+ * hierarchy, temporal roles, spells, open intervals, original date text), not as an attribute.
750
+ * @param column - the column
751
+ * @param label - the column's label for messages
752
+ * @param caps - the capabilities
753
+ * @param extras - format extras
754
+ * @param note - the recorder
755
+ * @returns true when the role is one of those, so no attribute checks follow
756
+ */
757
+ function checkSlotRole(
758
+ column: Column,
759
+ label: string,
760
+ caps: ExportCapabilities,
761
+ extras: CheckExtras,
762
+ note: NoteFn,
763
+ ): boolean {
764
+ const { name, role } = column.meta;
765
+ if (role === "position") {
766
+ checkPositionColumn(column, label, caps, extras, note);
767
+ return true;
768
+ }
769
+ const rule = role === null ? undefined : SLOT_RULES.get(role);
770
+ if (role === null || rule === undefined) {
771
+ return false;
772
+ }
773
+ if (!rule.written(caps, extras)) {
774
+ note(rule.code, `${label} (${rule.what ?? role}) cannot be written`, name, column.length - column.nullCount);
775
+ }
776
+ return true;
777
+ }
778
+
779
+ /**
780
+ * The checks of a position column: whether the format has positions, and their dtype.
781
+ * @param column - the column
782
+ * @param label - the column's label for messages
783
+ * @param caps - the capabilities
784
+ * @param extras - format extras
785
+ * @param note - the recorder
786
+ */
787
+ function checkPositionColumn(
788
+ column: Column,
789
+ label: string,
790
+ caps: ExportCapabilities,
791
+ extras: CheckExtras,
792
+ note: NoteFn,
793
+ ): void {
794
+ const { meta } = column;
795
+ if (!caps.positions) {
796
+ note(LOSS.POSITIONS, `${label} (positions) cannot be written`, meta.name, column.length - column.nullCount);
797
+ } else if (extras.positionDtype !== undefined && meta.dtype !== extras.positionDtype) {
798
+ note(
799
+ LOSS.DTYPE,
800
+ `${label} (positions) is ${meta.dtype}; the format reads positions back as ${extras.positionDtype}`,
801
+ meta.name,
802
+ column.length - column.nullCount,
803
+ );
804
+ }
805
+ }
806
+
807
+ /**
808
+ * The checks of a column written as a plain attribute: a role the format has no slot for,
809
+ * dynamic values, the dtype, a declared default and declared options.
810
+ * @param column - the column
811
+ * @param label - the column's label for messages
812
+ * @param caps - the capabilities
813
+ * @param extras - format extras
814
+ * @param note - the recorder
815
+ */
816
+ function checkAttributeColumn(
817
+ column: Column,
818
+ label: string,
819
+ caps: ExportCapabilities,
820
+ extras: CheckExtras,
821
+ note: NoteFn,
822
+ ): void {
823
+ const { meta } = column;
824
+ const { name, role } = meta;
825
+ if (role !== null && !(extras.roles?.has(role) ?? false)) {
826
+ note(
827
+ LOSS.ROLE,
828
+ `${label} (${role}) is written as a plain attribute; the format has no ${role} slot and the role is lost`,
829
+ name,
830
+ column.length - column.nullCount,
831
+ );
832
+ }
833
+ if (meta.dynamic && caps.temporal !== "dynamic-values") {
834
+ note(LOSS.DYNAMIC_VALUES, `${label} is dynamic; only its static value can be written`, name, null);
835
+ }
836
+ checkDtype(column, label, caps, note);
837
+ if (meta.default !== undefined && !caps.defaults) {
838
+ note(LOSS.DEFAULT, `${label} declares a default; the format has no defaults`, name, null);
839
+ }
840
+ if (meta.options !== null && !caps.options) {
841
+ note(LOSS.OPTIONS, `${label} declares options; the format has no enumerations`, name, null);
749
842
  }
750
843
  }
751
844
 
@@ -1007,7 +1007,7 @@ export function throwIfAborted(signal: AbortSignal | null | undefined): void {
1007
1007
  throw abortError();
1008
1008
  }
1009
1009
  // the platform contract: the reason itself, whatever its type
1010
- throw reason as Error;
1010
+ throw reason as Error; // NOSONAR(S3696): rethrows the abort signal's reason, as the platform does
1011
1011
  }
1012
1012
 
1013
1013
  /**
@@ -1493,10 +1493,11 @@ async function* blockElements(
1493
1493
  * @category Plugin helpers
1494
1494
  */
1495
1495
  export function reportTooDeep(report: ImportReportBuilder, event: Extract<AspectEvent, { kind: "deep" }>): void {
1496
+ const what = event.aspect === null ? "a member of the document" : `an element of "${event.aspect}"`;
1496
1497
  report.error(
1497
1498
  "parse-error",
1498
1499
  BAD_ASPECT_BLOCK_CODE,
1499
- `${event.aspect === null ? "a member of the document" : `an element of "${event.aspect}"`} is nested ${event.depth} levels deep, more than the ${MAX_ELEMENT_DEPTH} a graph keeps; skipped`,
1500
+ `${what} is nested ${event.depth} levels deep, more than the ${MAX_ELEMENT_DEPTH} a graph keeps; skipped`,
1500
1501
  { line: event.line, element: event.aspect },
1501
1502
  );
1502
1503
  }
@@ -1893,7 +1894,8 @@ export class CxStructure {
1893
1894
  }
1894
1895
  const error = typeof value.error === "string" ? value.error : "";
1895
1896
  if (value.success === false) {
1896
- const message = `the producer marked the document as failed${error.length > 0 ? `: ${error}` : ""}`;
1897
+ const detail = error.length > 0 ? `: ${error}` : "";
1898
+ const message = `the producer marked the document as failed${detail}`;
1897
1899
  this.report.error("validation-error", STATUS_FAILED_CODE, message, { line, element: "status" });
1898
1900
  throw this.report.abort(message, { code: STATUS_FAILED_CODE });
1899
1901
  }
package/src/common/xml.ts CHANGED
@@ -416,7 +416,8 @@ export function decodeEntities(raw: string, line: number, repairs?: XmlRepairs):
416
416
  const digits = name.slice(hex ? 2 : 1);
417
417
  // XML 1.0 puts no limit on leading zeros (&#x0000000041; is "A")
418
418
  const ok = hex ? /^0*[\da-fA-F]{1,6}$/.test(digits) : /^0*\d{1,7}$/.test(digits);
419
- const cp = ok ? Number.parseInt(digits, hex ? 16 : 10) : -1;
419
+ const radix = hex ? 16 : 10;
420
+ const cp = ok ? Number.parseInt(digits, radix) : -1;
420
421
  const low = cp >= 0xd800 && cp <= 0xdbff ? lowSurrogateAfter(raw, semi + 1, repairs) : null;
421
422
  if (low !== null) {
422
423
  repairs?.surrogatePair?.(lineAt(raw, amp, line));
@@ -739,125 +739,169 @@ class RecordScanner {
739
739
  * @returns the position after the last character consumed
740
740
  */
741
741
  scan(chunk: string, from: number): number {
742
- const { quoteCode, delimiterCode } = this;
742
+ const { delimiterCode } = this;
743
743
  const n = chunk.length;
744
744
  let i = from;
745
745
  this.segment = from;
746
746
  while (i < n) {
747
747
  const c = chunk.charCodeAt(i);
748
- if (this.state === COMMENT) {
749
- if (c === LF || c === CR) {
750
- this.comment = this.grow(this.comment, chunk.slice(this.segment, i));
751
- this.reader.leadingComments.push(this.comment);
752
- this.reader.leadingCommentLines.push(this.line);
753
- this.comment = "";
754
- this.state = START;
755
- this.endLine(c);
756
- this.segment = i + 1;
757
- }
748
+ const { state } = this;
749
+ if (state === COMMENT) {
750
+ this.commentChar(chunk, i, c);
758
751
  i++;
759
752
  continue;
760
753
  }
761
- if (this.state === QUOTED) {
762
- // jump to the next quote; the run in between is field text whose line breaks are counted
763
- const q = chunk.indexOf(this.quoteText, i);
764
- const end = q < 0 ? n : q;
765
- if (end > i) {
766
- this.countLineBreaks(chunk, i, end);
767
- }
768
- if (q < 0) {
769
- i = n;
770
- break;
771
- }
772
- this.field = this.grow(this.field, chunk.slice(this.segment, q));
773
- this.state = CLOSING;
774
- this.lastWasCr = false;
775
- i = q + 1;
754
+ if (state === QUOTED) {
755
+ i = this.quotedRun(chunk, i);
776
756
  continue;
777
757
  }
778
758
  const isDelimiter = c === delimiterCode || (this.collapse && c === TAB);
779
- if (this.state === CLOSING) {
780
- if (c === quoteCode) {
781
- this.field = this.grow(this.field, this.quoteText);
782
- this.segment = i + 1;
783
- this.state = QUOTED;
784
- this.lastWasCr = false;
785
- i++;
786
- continue;
787
- }
788
- if (!isDelimiter && c !== LF && c !== CR) {
789
- this.reader.badQuote(this.startLine, this.count + 1);
790
- }
791
- this.state = AFTER_QUOTED;
792
- this.segment = i;
793
- // fall through to the delimiter / line-end handling below without consuming c
794
- }
795
- if (isDelimiter || c === LF || c === CR) {
796
- if (c === LF && this.lastWasCr) {
797
- // the second half of a CRLF already ended the record
798
- this.lastWasCr = false;
799
- this.segment = i + 1;
800
- i++;
801
- continue;
802
- }
803
- if (this.state === UNQUOTED) {
804
- this.field = this.grow(this.field, chunk.slice(this.segment, i));
805
- }
806
- if (isDelimiter && this.collapse && this.state === START) {
807
- // a run of whitespace, or indentation: one separator
808
- this.lastWasCr = false;
809
- this.segment = i + 1;
810
- i++;
811
- continue;
812
- }
813
- if (isDelimiter) {
814
- this.push();
815
- this.state = START;
816
- this.lastWasCr = false;
817
- this.segment = i + 1;
818
- i++;
819
- continue;
820
- }
821
- // a line ending right after a whitespace run in the whitespace dialect ends no field
822
- if (!(this.state === START && (this.count === 0 || this.collapse))) {
823
- this.push();
824
- }
825
- if (this.count > 0) {
826
- this.ready = true;
827
- this.recordLine = this.startLine;
828
- }
829
- this.state = START;
830
- this.endLine(c);
831
- this.segment = i + 1;
832
- i++;
759
+ if (state === CLOSING || isDelimiter || c === LF || c === CR) {
760
+ i = this.boundary(chunk, i, c, isDelimiter);
833
761
  if (this.ready) {
834
762
  return i;
835
763
  }
836
764
  continue;
837
765
  }
766
+ this.fieldChar(c, i);
767
+ i++;
768
+ }
769
+ return i;
770
+ }
771
+
772
+ /**
773
+ * One character of a leading comment line: a line break ends the comment.
774
+ * @param chunk - the chunk
775
+ * @param i - the character's index
776
+ * @param c - the character's code
777
+ */
778
+ private commentChar(chunk: string, i: number, c: number): void {
779
+ if (c !== LF && c !== CR) {
780
+ return;
781
+ }
782
+ this.comment = this.grow(this.comment, chunk.slice(this.segment, i));
783
+ this.reader.leadingComments.push(this.comment);
784
+ this.reader.leadingCommentLines.push(this.line);
785
+ this.comment = "";
786
+ this.state = START;
787
+ this.endLine(c);
788
+ this.segment = i + 1;
789
+ }
790
+
791
+ /**
792
+ * Inside a quoted field: jump to the next quote; the run in between is field text whose line
793
+ * breaks are counted.
794
+ * @param chunk - the chunk
795
+ * @param i - where the run starts
796
+ * @returns the position after the quote, or the chunk end when there is none
797
+ */
798
+ private quotedRun(chunk: string, i: number): number {
799
+ const q = chunk.indexOf(this.quoteText, i);
800
+ const end = q < 0 ? chunk.length : q;
801
+ if (end > i) {
802
+ this.countLineBreaks(chunk, i, end);
803
+ }
804
+ if (q < 0) {
805
+ return chunk.length;
806
+ }
807
+ this.field = this.grow(this.field, chunk.slice(this.segment, q));
808
+ this.state = CLOSING;
809
+ this.lastWasCr = false;
810
+ return q + 1;
811
+ }
812
+
813
+ /**
814
+ * The character after a quote inside a quoted field, a delimiter or a line break: a doubled
815
+ * quote, the end of a field, or the end of a record.
816
+ * @param chunk - the chunk
817
+ * @param i - the character's index
818
+ * @param c - the character's code
819
+ * @param isDelimiter - whether it separates fields
820
+ * @returns the position after the character
821
+ */
822
+ private boundary(chunk: string, i: number, c: number, isDelimiter: boolean): number {
823
+ if (this.state === CLOSING) {
824
+ if (c === this.quoteCode) {
825
+ this.field = this.grow(this.field, this.quoteText);
826
+ this.segment = i + 1;
827
+ this.state = QUOTED;
828
+ this.lastWasCr = false;
829
+ return i + 1;
830
+ }
831
+ if (!isDelimiter && c !== LF && c !== CR) {
832
+ this.reader.badQuote(this.startLine, this.count + 1);
833
+ }
834
+ this.state = AFTER_QUOTED;
835
+ this.segment = i;
836
+ }
837
+ if (c === LF && this.lastWasCr) {
838
+ // the second half of a CRLF already ended the record
838
839
  this.lastWasCr = false;
839
- if (this.state === START) {
840
- if (this.leading && this.count === 0 && this.commentCodes.includes(c)) {
841
- this.state = COMMENT;
842
- this.segment = i;
843
- i++;
844
- continue;
845
- }
846
- this.leading = false;
847
- if (c === quoteCode) {
848
- this.state = QUOTED;
849
- this.fieldQuoted = true;
850
- this.segment = i + 1;
851
- } else {
852
- this.state = UNQUOTED;
853
- this.segment = i;
854
- }
855
- } else if (c === quoteCode && this.state === UNQUOTED && this.reader.strayQuoteLine === 0) {
840
+ this.segment = i + 1;
841
+ return i + 1;
842
+ }
843
+ if (this.state === UNQUOTED) {
844
+ this.field = this.grow(this.field, chunk.slice(this.segment, i));
845
+ }
846
+ if (isDelimiter) {
847
+ // in the whitespace dialect a run of whitespace, or indentation, is one separator
848
+ if (!(this.collapse && this.state === START)) {
849
+ this.push();
850
+ this.state = START;
851
+ }
852
+ this.lastWasCr = false;
853
+ this.segment = i + 1;
854
+ return i + 1;
855
+ }
856
+ this.lineEnd(c);
857
+ this.segment = i + 1;
858
+ return i + 1;
859
+ }
860
+
861
+ /**
862
+ * A line break that ends a record (or a blank line).
863
+ * @param c - the LF or CR
864
+ */
865
+ private lineEnd(c: number): void {
866
+ // a line ending right after a whitespace run in the whitespace dialect ends no field
867
+ if (!(this.state === START && (this.count === 0 || this.collapse))) {
868
+ this.push();
869
+ }
870
+ if (this.count > 0) {
871
+ this.ready = true;
872
+ this.recordLine = this.startLine;
873
+ }
874
+ this.state = START;
875
+ this.endLine(c);
876
+ }
877
+
878
+ /**
879
+ * Any other character outside a quoted field: the start of a field or a comment, or field text.
880
+ * @param c - the character's code
881
+ * @param i - its index
882
+ */
883
+ private fieldChar(c: number, i: number): void {
884
+ this.lastWasCr = false;
885
+ if (this.state !== START) {
886
+ if (c === this.quoteCode && this.state === UNQUOTED && this.reader.strayQuoteLine === 0) {
856
887
  this.reader.strayQuoteLine = this.startLine;
857
888
  }
858
- i++;
889
+ return;
890
+ }
891
+ if (this.leading && this.count === 0 && this.commentCodes.includes(c)) {
892
+ this.state = COMMENT;
893
+ this.segment = i;
894
+ return;
895
+ }
896
+ this.leading = false;
897
+ if (c === this.quoteCode) {
898
+ this.state = QUOTED;
899
+ this.fieldQuoted = true;
900
+ this.segment = i + 1;
901
+ } else {
902
+ this.state = UNQUOTED;
903
+ this.segment = i;
859
904
  }
860
- return i;
861
905
  }
862
906
 
863
907
  /**