@graphty/graph-io 0.3.39 → 0.3.41
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/chunks/{choose-CS3fvGT1.js → choose-CwFQamH9.js} +2 -2
- package/dist/chunks/{choose-CS3fvGT1.js.map → choose-CwFQamH9.js.map} +1 -1
- package/dist/chunks/{constants-DqKZT6cH.js → constants-Cf4Nn6tw.js} +2 -2
- package/dist/chunks/{constants-DqKZT6cH.js.map → constants-Cf4Nn6tw.js.map} +1 -1
- package/dist/chunks/{escape-BpYMQZVa.js → escape-DBb8zuNE.js} +4 -3
- package/dist/chunks/escape-DBb8zuNE.js.map +1 -0
- package/dist/chunks/{export-B-aVCwyG.js → export-DD3Lvw-F.js} +173 -116
- package/dist/chunks/export-DD3Lvw-F.js.map +1 -0
- package/dist/chunks/{exporter-CNJZF_BN.js → exporter-BqtmlxNr.js} +545 -459
- package/dist/chunks/exporter-BqtmlxNr.js.map +1 -0
- package/dist/chunks/{exporter-BqKs1Q12.js → exporter-CS35R6Fg.js} +6 -5
- package/dist/chunks/exporter-CS35R6Fg.js.map +1 -0
- package/dist/chunks/{exporter-DXqCPNrI.js → exporter-D8r1N2dU.js} +67 -42
- package/dist/chunks/exporter-D8r1N2dU.js.map +1 -0
- package/dist/chunks/{exporter-SFYkDzD0.js → exporter-NMplSnYn.js} +23 -20
- package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-NMplSnYn.js.map} +1 -1
- package/dist/chunks/{importer-CSt-gaYE.js → importer-BKC7AmCA.js} +42 -29
- package/dist/chunks/importer-BKC7AmCA.js.map +1 -0
- package/dist/chunks/{importer-DvFznt5-.js → importer-BdWHmlmG.js} +112 -62
- package/dist/chunks/importer-BdWHmlmG.js.map +1 -0
- package/dist/chunks/{importer-CqgYq4Ld.js → importer-BfbzWk6k.js} +202 -181
- package/dist/chunks/importer-BfbzWk6k.js.map +1 -0
- package/dist/chunks/{importer-PVXkeoaS.js → importer-BmaZcskq.js} +79 -66
- package/dist/chunks/importer-BmaZcskq.js.map +1 -0
- package/dist/chunks/{importer-B7Ir269L.js → importer-DviEsHy4.js} +118 -72
- package/dist/chunks/importer-DviEsHy4.js.map +1 -0
- package/dist/chunks/{importer--tQQsY4W.js → importer-S-abxJHn.js} +5 -5
- package/dist/chunks/{importer--tQQsY4W.js.map → importer-S-abxJHn.js.map} +1 -1
- package/dist/chunks/{importer-BfJ1jnNr.js → importer-r29cWnEf.js} +28 -13
- package/dist/chunks/importer-r29cWnEf.js.map +1 -0
- package/dist/chunks/{json-elements-CXs4z1iL.js → json-elements-DdF-etPT.js} +7 -5
- package/dist/chunks/{json-elements-CXs4z1iL.js.map → json-elements-DdF-etPT.js.map} +1 -1
- package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-BC0zyPhj.js} +2 -2
- package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-BC0zyPhj.js.map} +1 -1
- package/dist/chunks/{records-C9jd7yLD.js → records-Uc3LkABx.js} +150 -106
- package/dist/chunks/records-Uc3LkABx.js.map +1 -0
- package/dist/chunks/{text-Bjfa1xYL.js → text-Bh9SaAob.js} +20 -11
- package/dist/chunks/text-Bh9SaAob.js.map +1 -0
- package/dist/csv.js +67 -49
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +199 -99
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +170 -149
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +26 -26
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +103 -65
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +34 -9
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +180 -102
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/json-elements.d.ts.map +1 -1
- package/dist/src/common/json-elements.js +4 -2
- package/dist/src/common/json-elements.js.map +1 -1
- package/dist/src/common/text.d.ts +7 -0
- package/dist/src/common/text.d.ts.map +1 -1
- package/dist/src/common/text.js +15 -7
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +2 -1
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/common/zip.d.ts.map +1 -1
- package/dist/src/common/zip.js +36 -19
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +77 -25
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +16 -6
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +156 -110
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +108 -53
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
- package/dist/src/formats/cx2/exporter.js +2 -1
- package/dist/src/formats/cx2/exporter.js.map +1 -1
- package/dist/src/formats/cx2/importer.d.ts.map +1 -1
- package/dist/src/formats/cx2/importer.js +28 -6
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +11 -8
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +69 -39
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +138 -50
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +51 -35
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +103 -58
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +17 -8
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +36 -25
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +20 -15
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/tree.js +23 -14
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +22 -10
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +38 -21
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +536 -343
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/obographs.js +29 -17
- package/dist/src/formats/json/obographs.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +73 -43
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/header.d.ts.map +1 -1
- package/dist/src/formats/neo4j/header.js +37 -16
- package/dist/src/formats/neo4j/header.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +21 -11
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/obo/exporter.d.ts.map +1 -1
- package/dist/src/formats/obo/exporter.js +18 -7
- package/dist/src/formats/obo/exporter.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +142 -73
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +26 -15
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +85 -58
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +13 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +112 -50
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +92 -34
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +38 -18
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/sniff.js +109 -57
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/common/attributes.ts +59 -19
- package/src/common/input.ts +216 -102
- package/src/common/json-elements.ts +4 -2
- package/src/common/text.ts +24 -15
- package/src/common/xml.ts +2 -1
- package/src/common/zip.ts +47 -18
- package/src/formats/csv/exporter.ts +103 -39
- package/src/formats/csv/importer.ts +17 -6
- package/src/formats/csv/records.ts +162 -109
- package/src/formats/cx/importer.ts +140 -58
- package/src/formats/cx2/exporter.ts +2 -1
- package/src/formats/cx2/importer.ts +34 -10
- package/src/formats/cys/tables.ts +11 -10
- package/src/formats/gexf/exporter.ts +71 -38
- package/src/formats/gexf/importer.ts +173 -63
- package/src/formats/gml/exporter.ts +78 -82
- package/src/formats/gml/importer.ts +119 -68
- package/src/formats/gml/syntax.ts +18 -8
- package/src/formats/graphml/exporter.ts +46 -27
- package/src/formats/graphml/importer.ts +21 -13
- package/src/formats/graphml/tree.ts +22 -12
- package/src/formats/graphml/yfiles.ts +23 -10
- package/src/formats/json/exporter.ts +40 -21
- package/src/formats/json/importer.ts +701 -385
- package/src/formats/json/obographs.ts +35 -15
- package/src/formats/neo4j/exporter.ts +79 -41
- package/src/formats/neo4j/header.ts +50 -16
- package/src/formats/neo4j/importer.ts +21 -10
- package/src/formats/obo/exporter.ts +34 -17
- package/src/formats/pajek/exporter.ts +180 -117
- package/src/formats/pajek/importer.ts +27 -15
- package/src/formats/pajek/syntax.ts +99 -58
- package/src/formats/xgmml/emit.ts +122 -50
- package/src/formats/xgmml/exporter.ts +103 -35
- package/src/formats/xgmml/importer.ts +45 -23
- package/src/sniff.ts +115 -59
- package/dist/chunks/escape-BpYMQZVa.js.map +0 -1
- package/dist/chunks/export-B-aVCwyG.js.map +0 -1
- package/dist/chunks/exporter-BqKs1Q12.js.map +0 -1
- package/dist/chunks/exporter-CNJZF_BN.js.map +0 -1
- package/dist/chunks/exporter-DXqCPNrI.js.map +0 -1
- package/dist/chunks/importer-B7Ir269L.js.map +0 -1
- package/dist/chunks/importer-BfJ1jnNr.js.map +0 -1
- package/dist/chunks/importer-CSt-gaYE.js.map +0 -1
- package/dist/chunks/importer-CqgYq4Ld.js.map +0 -1
- package/dist/chunks/importer-DvFznt5-.js.map +0 -1
- package/dist/chunks/importer-PVXkeoaS.js.map +0 -1
- package/dist/chunks/records-C9jd7yLD.js.map +0 -1
- package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
|
@@ -407,6 +407,25 @@ function nodeTableNote(names: readonly string[], table: string, notes: LossNote[
|
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* The number of node ids whose text an earlier id already has (a number and a string).
|
|
412
|
+
* @param ids - the snapshot's ids
|
|
413
|
+
* @returns the collisions
|
|
414
|
+
*/
|
|
415
|
+
function idTextCollisions(ids: GraphSnapshot["ids"]): number {
|
|
416
|
+
const seen = new Set<string>();
|
|
417
|
+
let collisions = 0;
|
|
418
|
+
for (let i = 0; i < ids.size; i++) {
|
|
419
|
+
const text = String(ids.idOf(i));
|
|
420
|
+
if (seen.has(text)) {
|
|
421
|
+
collisions++;
|
|
422
|
+
} else {
|
|
423
|
+
seen.add(text);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return collisions;
|
|
427
|
+
}
|
|
428
|
+
|
|
410
429
|
/**
|
|
411
430
|
* The id notes: text collisions are fatal (export() throws E_INVALID_ID); type changes under the
|
|
412
431
|
* canonical re-read are reported.
|
|
@@ -421,16 +440,7 @@ function idNotes(
|
|
|
421
440
|
): void {
|
|
422
441
|
const { ids } = snapshot;
|
|
423
442
|
if (ids.kind === "mixed") {
|
|
424
|
-
const
|
|
425
|
-
let collisions = 0;
|
|
426
|
-
for (let i = 0; i < ids.size; i++) {
|
|
427
|
-
const text = String(ids.idOf(i));
|
|
428
|
-
if (seen.has(text)) {
|
|
429
|
-
collisions++;
|
|
430
|
-
} else {
|
|
431
|
-
seen.add(text);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
443
|
+
const collisions = idTextCollisions(ids);
|
|
434
444
|
if (collisions > 0) {
|
|
435
445
|
note(
|
|
436
446
|
CSV_LOSS.ID_TEXT_COLLISION,
|
|
@@ -699,40 +709,81 @@ function attributeColumns(
|
|
|
699
709
|
if (role !== null && SKIPPED_ROLES.has(role)) {
|
|
700
710
|
continue;
|
|
701
711
|
}
|
|
702
|
-
const low = lower(name);
|
|
703
712
|
const set = column.length - column.nullCount;
|
|
704
|
-
|
|
705
|
-
if (reserved.has(low) || isIdName || (idColumn !== null && lower(idColumn.meta.name) === low)) {
|
|
706
|
-
note?.(
|
|
707
|
-
CSV_LOSS.RESERVED_NAME,
|
|
708
|
-
`${domain} column "${name}" is not written: the name is reserved for the ${isIdName ? "edge id" : low} column`,
|
|
709
|
-
name,
|
|
710
|
-
set,
|
|
711
|
-
);
|
|
713
|
+
if (isReservedName(name, domain, reserved, idColumn, set, note)) {
|
|
712
714
|
continue;
|
|
713
715
|
}
|
|
714
|
-
if (findColumn([name], LABEL_NAMES) >= 0) {
|
|
715
|
-
|
|
716
|
-
note?.(
|
|
717
|
-
CSV_LOSS.RESERVED_NAME,
|
|
718
|
-
`${domain} column "${name}" is not written: the name is reserved for the label column`,
|
|
719
|
-
name,
|
|
720
|
-
set,
|
|
721
|
-
);
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
note?.(
|
|
725
|
-
CSV_LOSS.ROLE_ASSUMED,
|
|
726
|
-
`${domain} column "${name}" reads back with the label role (string)`,
|
|
727
|
-
name,
|
|
728
|
-
set,
|
|
729
|
-
);
|
|
716
|
+
if (findColumn([name], LABEL_NAMES) >= 0 && !labelNameWritten(name, domain, labelColumn, set, note)) {
|
|
717
|
+
continue;
|
|
730
718
|
}
|
|
731
719
|
out.push({ column, header: name });
|
|
732
720
|
}
|
|
733
721
|
return out;
|
|
734
722
|
}
|
|
735
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Whether an attribute column's name is reserved for a structural column (a dialect header, an
|
|
726
|
+
* edge id name, the id column's name), with its note.
|
|
727
|
+
* @param name - the column name
|
|
728
|
+
* @param domain - node or edge, for messages
|
|
729
|
+
* @param reserved - lower-case header names the dialect writes for structure
|
|
730
|
+
* @param idColumn - the table's id role column, or null
|
|
731
|
+
* @param set - the column's set cells
|
|
732
|
+
* @param note - the recorder, or null when this table's notes are not wanted
|
|
733
|
+
* @returns true when the column is not written
|
|
734
|
+
*/
|
|
735
|
+
function isReservedName(
|
|
736
|
+
name: string,
|
|
737
|
+
domain: "node" | "edge",
|
|
738
|
+
reserved: ReadonlySet<string>,
|
|
739
|
+
idColumn: Column | null,
|
|
740
|
+
set: number,
|
|
741
|
+
note: ((code: string, message: string, column?: string | null, count?: number | null) => void) | null,
|
|
742
|
+
): boolean {
|
|
743
|
+
const low = lower(name);
|
|
744
|
+
const isIdName = domain === "edge" ? findColumn([name], EDGE_ID_NAMES) >= 0 : false;
|
|
745
|
+
if (reserved.has(low) || isIdName || (idColumn !== null && lower(idColumn.meta.name) === low)) {
|
|
746
|
+
note?.(
|
|
747
|
+
CSV_LOSS.RESERVED_NAME,
|
|
748
|
+
`${domain} column "${name}" is not written: the name is reserved for the ${isIdName ? "edge id" : low} column`,
|
|
749
|
+
name,
|
|
750
|
+
set,
|
|
751
|
+
);
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Whether an attribute column with a label header name is written, with its note: not when the
|
|
759
|
+
* table has a label column (the name is reserved for it), else it reads back with the label role.
|
|
760
|
+
* @param name - the column name
|
|
761
|
+
* @param domain - node or edge, for messages
|
|
762
|
+
* @param labelColumn - the table's label role column, or null
|
|
763
|
+
* @param set - the column's set cells
|
|
764
|
+
* @param note - the recorder, or null when this table's notes are not wanted
|
|
765
|
+
* @returns true when the column is written
|
|
766
|
+
*/
|
|
767
|
+
function labelNameWritten(
|
|
768
|
+
name: string,
|
|
769
|
+
domain: "node" | "edge",
|
|
770
|
+
labelColumn: Column | null,
|
|
771
|
+
set: number,
|
|
772
|
+
note: ((code: string, message: string, column?: string | null, count?: number | null) => void) | null,
|
|
773
|
+
): boolean {
|
|
774
|
+
if (labelColumn !== null) {
|
|
775
|
+
note?.(
|
|
776
|
+
CSV_LOSS.RESERVED_NAME,
|
|
777
|
+
`${domain} column "${name}" is not written: the name is reserved for the label column`,
|
|
778
|
+
name,
|
|
779
|
+
set,
|
|
780
|
+
);
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
note?.(CSV_LOSS.ROLE_ASSUMED, `${domain} column "${name}" reads back with the label role (string)`, name, set);
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
|
|
736
787
|
/**
|
|
737
788
|
* Notes about a role column (id or label): a name the importer does not map to the role, and a
|
|
738
789
|
* dtype other than string / dict (read back as string).
|
|
@@ -912,6 +963,22 @@ function neighbourCell(id: string, weight: string | null): string {
|
|
|
912
963
|
return id.includes(":") ? `${id}:` : id;
|
|
913
964
|
}
|
|
914
965
|
|
|
966
|
+
/**
|
|
967
|
+
* The end of the run of consecutive edge rows that share the source of row `r`.
|
|
968
|
+
* @param src - the edge sources by edge index
|
|
969
|
+
* @param edgeRows - the edges in write order
|
|
970
|
+
* @param r - the run's first row
|
|
971
|
+
* @returns the index after the run's last row
|
|
972
|
+
*/
|
|
973
|
+
function sourceRunEnd(src: ArrayLike<number>, edgeRows: ArrayLike<number>, r: number): number {
|
|
974
|
+
const source = src[edgeRows[r]];
|
|
975
|
+
let end = r;
|
|
976
|
+
while (end < edgeRows.length && src[edgeRows[end]] === source) {
|
|
977
|
+
end++;
|
|
978
|
+
}
|
|
979
|
+
return end;
|
|
980
|
+
}
|
|
981
|
+
|
|
915
982
|
/**
|
|
916
983
|
* The rows of the adjacency table (design section 8.5: the order survives the importer's first-
|
|
917
984
|
* appearance numbering). Consecutive edges with one source share a row, so the edge order is kept;
|
|
@@ -953,10 +1020,7 @@ function* adjacencyLines(snapshot: GraphSnapshot, plan: Plan): Generator<string,
|
|
|
953
1020
|
let r = 0;
|
|
954
1021
|
while (r < edgeRows.length) {
|
|
955
1022
|
const source = list.src[edgeRows[r]];
|
|
956
|
-
|
|
957
|
-
while (end < edgeRows.length && list.src[edgeRows[end]] === source) {
|
|
958
|
-
end++;
|
|
959
|
-
}
|
|
1023
|
+
const end = sourceRunEnd(list.src, edgeRows, r);
|
|
960
1024
|
// the nodes this row introduces, in the order the importer meets them; in index order from
|
|
961
1025
|
// `next` they need nothing more
|
|
962
1026
|
const fresh: number[] = [];
|
|
@@ -575,6 +575,22 @@ function isUnset(text: string, quoted: boolean | undefined): boolean {
|
|
|
575
575
|
return quoted !== true && isBlank(text);
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
/**
|
|
579
|
+
* The number of set neighbor cells of an adjacency row (every cell after the first).
|
|
580
|
+
* @param row - the cells
|
|
581
|
+
* @param quoted - whether each cell was quoted
|
|
582
|
+
* @returns the set neighbor cells
|
|
583
|
+
*/
|
|
584
|
+
function setNeighborCells(row: readonly string[], quoted: readonly boolean[]): number {
|
|
585
|
+
let neighbors = 0;
|
|
586
|
+
for (let k = 1; k < row.length; k++) {
|
|
587
|
+
if (!isUnset(row[k], quoted[k])) {
|
|
588
|
+
neighbors++;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return neighbors;
|
|
592
|
+
}
|
|
593
|
+
|
|
578
594
|
/** Rows between two checks of the cancellation signal on an in-memory input. */
|
|
579
595
|
const ABORT_CHECK_INTERVAL = 64;
|
|
580
596
|
|
|
@@ -1520,12 +1536,7 @@ class TableReader {
|
|
|
1520
1536
|
const { where } = this;
|
|
1521
1537
|
where.line = line;
|
|
1522
1538
|
where.element = null;
|
|
1523
|
-
|
|
1524
|
-
for (let k = 1; k < row.length; k++) {
|
|
1525
|
-
if (!isUnset(row[k], quoted[k])) {
|
|
1526
|
-
neighbors++;
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1539
|
+
const neighbors = setNeighborCells(row, quoted);
|
|
1529
1540
|
if (isUnset(row[0], quoted[0])) {
|
|
1530
1541
|
report.error("missing-value", MISSING_ID_CODE, `blank node cell`, { line });
|
|
1531
1542
|
counts.skippedNodes++;
|
|
@@ -410,16 +410,7 @@ export function sniffDelimiter(
|
|
|
410
410
|
if (rows.length === 0) {
|
|
411
411
|
continue;
|
|
412
412
|
}
|
|
413
|
-
|
|
414
|
-
let delta = 0;
|
|
415
|
-
for (let i = 0; i < rows.length; i++) {
|
|
416
|
-
const count = rows[i].length;
|
|
417
|
-
total += count;
|
|
418
|
-
if (i > 0) {
|
|
419
|
-
delta += Math.abs(count - rows[i - 1].length);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
const average = total / rows.length;
|
|
413
|
+
const { average, delta } = rowWidths(rows);
|
|
423
414
|
if (average > 1.99 && delta < bestDelta) {
|
|
424
415
|
best = delimiter;
|
|
425
416
|
bestDelta = delta;
|
|
@@ -428,6 +419,24 @@ export function sniffDelimiter(
|
|
|
428
419
|
return best;
|
|
429
420
|
}
|
|
430
421
|
|
|
422
|
+
/**
|
|
423
|
+
* The average width of the rows and the sum of the width changes from each row to the next.
|
|
424
|
+
* @param rows - the rows (at least one)
|
|
425
|
+
* @returns the average and the delta
|
|
426
|
+
*/
|
|
427
|
+
function rowWidths(rows: readonly (readonly string[])[]): { average: number; delta: number } {
|
|
428
|
+
let total = 0;
|
|
429
|
+
let delta = 0;
|
|
430
|
+
for (let i = 0; i < rows.length; i++) {
|
|
431
|
+
const count = rows[i].length;
|
|
432
|
+
total += count;
|
|
433
|
+
if (i > 0) {
|
|
434
|
+
delta += Math.abs(count - rows[i - 1].length);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return { average: total / rows.length, delta };
|
|
438
|
+
}
|
|
439
|
+
|
|
431
440
|
/**
|
|
432
441
|
* Whether a parsed row is a blank line: one cell holding only whitespace.
|
|
433
442
|
* @param row - the row
|
|
@@ -739,125 +748,169 @@ class RecordScanner {
|
|
|
739
748
|
* @returns the position after the last character consumed
|
|
740
749
|
*/
|
|
741
750
|
scan(chunk: string, from: number): number {
|
|
742
|
-
const {
|
|
751
|
+
const { delimiterCode } = this;
|
|
743
752
|
const n = chunk.length;
|
|
744
753
|
let i = from;
|
|
745
754
|
this.segment = from;
|
|
746
755
|
while (i < n) {
|
|
747
756
|
const c = chunk.charCodeAt(i);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
}
|
|
757
|
+
const { state } = this;
|
|
758
|
+
if (state === COMMENT) {
|
|
759
|
+
this.commentChar(chunk, i, c);
|
|
758
760
|
i++;
|
|
759
761
|
continue;
|
|
760
762
|
}
|
|
761
|
-
if (
|
|
762
|
-
|
|
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;
|
|
763
|
+
if (state === QUOTED) {
|
|
764
|
+
i = this.quotedRun(chunk, i);
|
|
776
765
|
continue;
|
|
777
766
|
}
|
|
778
767
|
const isDelimiter = c === delimiterCode || (this.collapse && c === TAB);
|
|
779
|
-
if (
|
|
780
|
-
|
|
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++;
|
|
768
|
+
if (state === CLOSING || isDelimiter || c === LF || c === CR) {
|
|
769
|
+
i = this.boundary(chunk, i, c, isDelimiter);
|
|
833
770
|
if (this.ready) {
|
|
834
771
|
return i;
|
|
835
772
|
}
|
|
836
773
|
continue;
|
|
837
774
|
}
|
|
775
|
+
this.fieldChar(c, i);
|
|
776
|
+
i++;
|
|
777
|
+
}
|
|
778
|
+
return i;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* One character of a leading comment line: a line break ends the comment.
|
|
783
|
+
* @param chunk - the chunk
|
|
784
|
+
* @param i - the character's index
|
|
785
|
+
* @param c - the character's code
|
|
786
|
+
*/
|
|
787
|
+
private commentChar(chunk: string, i: number, c: number): void {
|
|
788
|
+
if (c !== LF && c !== CR) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
this.comment = this.grow(this.comment, chunk.slice(this.segment, i));
|
|
792
|
+
this.reader.leadingComments.push(this.comment);
|
|
793
|
+
this.reader.leadingCommentLines.push(this.line);
|
|
794
|
+
this.comment = "";
|
|
795
|
+
this.state = START;
|
|
796
|
+
this.endLine(c);
|
|
797
|
+
this.segment = i + 1;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Inside a quoted field: jump to the next quote; the run in between is field text whose line
|
|
802
|
+
* breaks are counted.
|
|
803
|
+
* @param chunk - the chunk
|
|
804
|
+
* @param i - where the run starts
|
|
805
|
+
* @returns the position after the quote, or the chunk end when there is none
|
|
806
|
+
*/
|
|
807
|
+
private quotedRun(chunk: string, i: number): number {
|
|
808
|
+
const q = chunk.indexOf(this.quoteText, i);
|
|
809
|
+
const end = q < 0 ? chunk.length : q;
|
|
810
|
+
if (end > i) {
|
|
811
|
+
this.countLineBreaks(chunk, i, end);
|
|
812
|
+
}
|
|
813
|
+
if (q < 0) {
|
|
814
|
+
return chunk.length;
|
|
815
|
+
}
|
|
816
|
+
this.field = this.grow(this.field, chunk.slice(this.segment, q));
|
|
817
|
+
this.state = CLOSING;
|
|
818
|
+
this.lastWasCr = false;
|
|
819
|
+
return q + 1;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* The character after a quote inside a quoted field, a delimiter or a line break: a doubled
|
|
824
|
+
* quote, the end of a field, or the end of a record.
|
|
825
|
+
* @param chunk - the chunk
|
|
826
|
+
* @param i - the character's index
|
|
827
|
+
* @param c - the character's code
|
|
828
|
+
* @param isDelimiter - whether it separates fields
|
|
829
|
+
* @returns the position after the character
|
|
830
|
+
*/
|
|
831
|
+
private boundary(chunk: string, i: number, c: number, isDelimiter: boolean): number {
|
|
832
|
+
if (this.state === CLOSING) {
|
|
833
|
+
if (c === this.quoteCode) {
|
|
834
|
+
this.field = this.grow(this.field, this.quoteText);
|
|
835
|
+
this.segment = i + 1;
|
|
836
|
+
this.state = QUOTED;
|
|
837
|
+
this.lastWasCr = false;
|
|
838
|
+
return i + 1;
|
|
839
|
+
}
|
|
840
|
+
if (!isDelimiter && c !== LF && c !== CR) {
|
|
841
|
+
this.reader.badQuote(this.startLine, this.count + 1);
|
|
842
|
+
}
|
|
843
|
+
this.state = AFTER_QUOTED;
|
|
844
|
+
this.segment = i;
|
|
845
|
+
}
|
|
846
|
+
if (c === LF && this.lastWasCr) {
|
|
847
|
+
// the second half of a CRLF already ended the record
|
|
838
848
|
this.lastWasCr = false;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
849
|
+
this.segment = i + 1;
|
|
850
|
+
return i + 1;
|
|
851
|
+
}
|
|
852
|
+
if (this.state === UNQUOTED) {
|
|
853
|
+
this.field = this.grow(this.field, chunk.slice(this.segment, i));
|
|
854
|
+
}
|
|
855
|
+
if (isDelimiter) {
|
|
856
|
+
// in the whitespace dialect a run of whitespace, or indentation, is one separator
|
|
857
|
+
if (!(this.collapse && this.state === START)) {
|
|
858
|
+
this.push();
|
|
859
|
+
this.state = START;
|
|
860
|
+
}
|
|
861
|
+
this.lastWasCr = false;
|
|
862
|
+
this.segment = i + 1;
|
|
863
|
+
return i + 1;
|
|
864
|
+
}
|
|
865
|
+
this.lineEnd(c);
|
|
866
|
+
this.segment = i + 1;
|
|
867
|
+
return i + 1;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* A line break that ends a record (or a blank line).
|
|
872
|
+
* @param c - the LF or CR
|
|
873
|
+
*/
|
|
874
|
+
private lineEnd(c: number): void {
|
|
875
|
+
// a line ending right after a whitespace run in the whitespace dialect ends no field
|
|
876
|
+
if (!(this.state === START && (this.count === 0 || this.collapse))) {
|
|
877
|
+
this.push();
|
|
878
|
+
}
|
|
879
|
+
if (this.count > 0) {
|
|
880
|
+
this.ready = true;
|
|
881
|
+
this.recordLine = this.startLine;
|
|
882
|
+
}
|
|
883
|
+
this.state = START;
|
|
884
|
+
this.endLine(c);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Any other character outside a quoted field: the start of a field or a comment, or field text.
|
|
889
|
+
* @param c - the character's code
|
|
890
|
+
* @param i - its index
|
|
891
|
+
*/
|
|
892
|
+
private fieldChar(c: number, i: number): void {
|
|
893
|
+
this.lastWasCr = false;
|
|
894
|
+
if (this.state !== START) {
|
|
895
|
+
if (c === this.quoteCode && this.state === UNQUOTED && this.reader.strayQuoteLine === 0) {
|
|
856
896
|
this.reader.strayQuoteLine = this.startLine;
|
|
857
897
|
}
|
|
858
|
-
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (this.leading && this.count === 0 && this.commentCodes.includes(c)) {
|
|
901
|
+
this.state = COMMENT;
|
|
902
|
+
this.segment = i;
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
this.leading = false;
|
|
906
|
+
if (c === this.quoteCode) {
|
|
907
|
+
this.state = QUOTED;
|
|
908
|
+
this.fieldQuoted = true;
|
|
909
|
+
this.segment = i + 1;
|
|
910
|
+
} else {
|
|
911
|
+
this.state = UNQUOTED;
|
|
912
|
+
this.segment = i;
|
|
859
913
|
}
|
|
860
|
-
return i;
|
|
861
914
|
}
|
|
862
915
|
|
|
863
916
|
/**
|