@graphty/graph-io 0.3.41 → 0.3.42

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 (98) hide show
  1. package/README.md +2 -0
  2. package/dist/chunks/{children-CL3Cy0ez.js → children-CA4coBi0.js} +36 -27
  3. package/dist/chunks/children-CA4coBi0.js.map +1 -0
  4. package/dist/chunks/{choose-CwFQamH9.js → choose-DvpbdRuz.js} +2 -2
  5. package/dist/chunks/{choose-CwFQamH9.js.map → choose-DvpbdRuz.js.map} +1 -1
  6. package/dist/chunks/{constants-Cf4Nn6tw.js → constants-fT3E8JYY.js} +2 -2
  7. package/dist/chunks/{constants-Cf4Nn6tw.js.map → constants-fT3E8JYY.js.map} +1 -1
  8. package/dist/chunks/{escape-DBb8zuNE.js → escape-B62ZGM_a.js} +2 -2
  9. package/dist/chunks/{escape-DBb8zuNE.js.map → escape-B62ZGM_a.js.map} +1 -1
  10. package/dist/chunks/{export-DD3Lvw-F.js → export-rKLeGQMo.js} +232 -128
  11. package/dist/chunks/export-rKLeGQMo.js.map +1 -0
  12. package/dist/chunks/{exporter-CS35R6Fg.js → exporter-8hLNemFk.js} +4 -4
  13. package/dist/chunks/{exporter-CS35R6Fg.js.map → exporter-8hLNemFk.js.map} +1 -1
  14. package/dist/chunks/{exporter-D8r1N2dU.js → exporter-BWwEuOPT.js} +5 -5
  15. package/dist/chunks/{exporter-D8r1N2dU.js.map → exporter-BWwEuOPT.js.map} +1 -1
  16. package/dist/chunks/{exporter-BqtmlxNr.js → exporter-CpRyHZ3H.js} +34 -21
  17. package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
  18. package/dist/chunks/{exporter-NMplSnYn.js → exporter-yLxKBvLE.js} +3 -3
  19. package/dist/chunks/{exporter-NMplSnYn.js.map → exporter-yLxKBvLE.js.map} +1 -1
  20. package/dist/chunks/{importer-S-abxJHn.js → importer-BbNb7eNg.js} +111 -71
  21. package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
  22. package/dist/chunks/{importer-BdWHmlmG.js → importer-C98dpSH4.js} +5 -5
  23. package/dist/chunks/{importer-BdWHmlmG.js.map → importer-C98dpSH4.js.map} +1 -1
  24. package/dist/chunks/{importer-r29cWnEf.js → importer-D794XUTe.js} +3 -3
  25. package/dist/chunks/{importer-r29cWnEf.js.map → importer-D794XUTe.js.map} +1 -1
  26. package/dist/chunks/{importer-BfbzWk6k.js → importer-Dz7tI8Sa.js} +5 -5
  27. package/dist/chunks/{importer-BfbzWk6k.js.map → importer-Dz7tI8Sa.js.map} +1 -1
  28. package/dist/chunks/{importer-DviEsHy4.js → importer-Ht7LNg8L.js} +4 -4
  29. package/dist/chunks/{importer-DviEsHy4.js.map → importer-Ht7LNg8L.js.map} +1 -1
  30. package/dist/chunks/{importer-BKC7AmCA.js → importer-WrGRZb0N.js} +6 -6
  31. package/dist/chunks/{importer-BKC7AmCA.js.map → importer-WrGRZb0N.js.map} +1 -1
  32. package/dist/chunks/{importer-BmaZcskq.js → importer-xZ-3suY1.js} +5 -5
  33. package/dist/chunks/{importer-BmaZcskq.js.map → importer-xZ-3suY1.js.map} +1 -1
  34. package/dist/chunks/{json-elements-DdF-etPT.js → json-elements-DKSqpNW1.js} +3 -3
  35. package/dist/chunks/{json-elements-DdF-etPT.js.map → json-elements-DKSqpNW1.js.map} +1 -1
  36. package/dist/chunks/{ontology-export-BC0zyPhj.js → ontology-export-CtRXjg_i.js} +2 -2
  37. package/dist/chunks/{ontology-export-BC0zyPhj.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
  38. package/dist/chunks/{records-Uc3LkABx.js → records-Cu4bxYOm.js} +56 -26
  39. package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
  40. package/dist/chunks/{text-Bh9SaAob.js → text-DHOJXnED.js} +2 -2
  41. package/dist/chunks/{text-Bh9SaAob.js.map → text-DHOJXnED.js.map} +1 -1
  42. package/dist/csv.js +260 -196
  43. package/dist/csv.js.map +1 -1
  44. package/dist/cx.js +1 -1
  45. package/dist/cx2.js +2 -2
  46. package/dist/cys.js +1 -1
  47. package/dist/dot.js +1 -1
  48. package/dist/gexf.js +2 -2
  49. package/dist/gml.js +3 -3
  50. package/dist/graph-io.js +27 -27
  51. package/dist/graphml.js +2 -2
  52. package/dist/json.js +1 -1
  53. package/dist/neo4j.js +3 -3
  54. package/dist/obo.js +1 -1
  55. package/dist/pajek.js +1 -1
  56. package/dist/src/children.d.ts.map +1 -1
  57. package/dist/src/children.js +50 -27
  58. package/dist/src/children.js.map +1 -1
  59. package/dist/src/common/attributes.d.ts.map +1 -1
  60. package/dist/src/common/attributes.js +52 -33
  61. package/dist/src/common/attributes.js.map +1 -1
  62. package/dist/src/common/input.d.ts.map +1 -1
  63. package/dist/src/common/input.js +178 -82
  64. package/dist/src/common/input.js.map +1 -1
  65. package/dist/src/common/lists.js +30 -15
  66. package/dist/src/common/lists.js.map +1 -1
  67. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  68. package/dist/src/formats/csv/exporter.js +211 -121
  69. package/dist/src/formats/csv/exporter.js.map +1 -1
  70. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  71. package/dist/src/formats/csv/importer.js +113 -74
  72. package/dist/src/formats/csv/importer.js.map +1 -1
  73. package/dist/src/formats/csv/records.d.ts.map +1 -1
  74. package/dist/src/formats/csv/records.js +60 -26
  75. package/dist/src/formats/csv/records.js.map +1 -1
  76. package/dist/src/formats/dot/tokenizer.d.ts +39 -0
  77. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  78. package/dist/src/formats/dot/tokenizer.js +112 -66
  79. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  80. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  81. package/dist/src/formats/json/dialect.js +54 -20
  82. package/dist/src/formats/json/dialect.js.map +1 -1
  83. package/dist/xgmml.js +2 -2
  84. package/package.json +2 -2
  85. package/src/children.ts +54 -27
  86. package/src/common/attributes.ts +59 -30
  87. package/src/common/input.ts +223 -95
  88. package/src/common/lists.ts +29 -15
  89. package/src/formats/csv/exporter.ts +274 -136
  90. package/src/formats/csv/importer.ts +138 -78
  91. package/src/formats/csv/records.ts +71 -25
  92. package/src/formats/dot/tokenizer.ts +118 -68
  93. package/src/formats/json/dialect.ts +61 -22
  94. package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
  95. package/dist/chunks/export-DD3Lvw-F.js.map +0 -1
  96. package/dist/chunks/exporter-BqtmlxNr.js.map +0 -1
  97. package/dist/chunks/importer-S-abxJHn.js.map +0 -1
  98. package/dist/chunks/records-Uc3LkABx.js.map +0 -1
@@ -22,7 +22,14 @@
22
22
  * colon is written `id:` when it has no weight so the importer does not read its tail as one.
23
23
  */
24
24
 
25
- import { type Column, GraphFormatError, type GraphSnapshot, type NodeId } from "@graphty/graph-format";
25
+ import {
26
+ type Column,
27
+ type F32Column,
28
+ type F64Column,
29
+ GraphFormatError,
30
+ type GraphSnapshot,
31
+ type NodeId,
32
+ } from "@graphty/graph-format";
26
33
 
27
34
  import { DICT_SAMPLE_ROWS, DictHeuristic } from "../../common/attributes.js";
28
35
  import { type PairFolding, pairFolding } from "../../common/direction.js";
@@ -371,16 +378,6 @@ function cellText(column: Column, row: number): string | null {
371
378
  }
372
379
  }
373
380
 
374
- /**
375
- * Build the plan of an export: resolve options, choose the rows and columns, and collect the
376
- * CSV-specific loss notes (check() adds the generic ones). The value-level notes need a pass over
377
- * every written cell and are collected for check() only; export() needs the plan and the fatal
378
- * id-collision note alone.
379
- * @param snapshot - the snapshot
380
- * @param options - the caller's options
381
- * @param values - whether to scan the values for notes
382
- * @returns the plan
383
- */
384
381
  /**
385
382
  * W_CSV_NODE_TABLE: the node columns an edge-table or adjacency export leaves for a second, node-table export.
386
383
  * @param names - the node columns, label first
@@ -469,6 +466,16 @@ function idNotes(
469
466
  }
470
467
  }
471
468
 
469
+ /**
470
+ * Build the plan of an export: resolve options, choose the rows and columns, and collect the
471
+ * CSV-specific loss notes (check() adds the generic ones). The value-level notes need a pass over
472
+ * every written cell and are collected for check() only; export() needs the plan and the fatal
473
+ * id-collision note alone.
474
+ * @param snapshot - the snapshot
475
+ * @param options - the caller's options
476
+ * @param values - whether to scan the values for notes
477
+ * @returns the plan
478
+ */
472
479
  function planExport(
473
480
  snapshot: GraphSnapshot,
474
481
  options: (CsvExportOptions & CommonExportOptions) | undefined,
@@ -492,7 +499,66 @@ function planExport(
492
499
  edgeRows.push(e);
493
500
  }
494
501
  }
495
- // A node table holds no edge: the direction and mutual notes are the edge table's.
502
+ directionNotes(snapshot, csv, edgeRows.length, note);
503
+ if (folding.mutualCount > 0 && csv.table !== "nodes") {
504
+ note(
505
+ CSV_LOSS.MUTUAL_EXPANDED,
506
+ `${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
507
+ null,
508
+ folding.mutualCount,
509
+ );
510
+ }
511
+ if (csv.table === "edges") {
512
+ noteNodeCoverage(snapshot, edgeRows, note);
513
+ }
514
+
515
+ // weights
516
+ const weights = explicitWeights(snapshot);
517
+
518
+ const { edgeId, edgeLabel, edgeColumns } = planEdgeColumns(snapshot, csv, edgeRows, values, note);
519
+ const { nodeLabel, nodeColumns } = planNodeColumns(snapshot, csv, values, note);
520
+ if (csv.table !== "nodes") {
521
+ nodeTableNote(
522
+ [...(nodeLabel === null ? [] : [nodeLabel.meta.name]), ...nodeColumns.map((c) => c.column.meta.name)],
523
+ csv.table,
524
+ notes,
525
+ );
526
+ }
527
+
528
+ return {
529
+ csv,
530
+ common,
531
+ notes,
532
+ edgeRows,
533
+ folding,
534
+ edgeId,
535
+ edgeLabel,
536
+ weights,
537
+ edgeColumns,
538
+ nodeLabel,
539
+ nodeColumns,
540
+ idText,
541
+ };
542
+ }
543
+
544
+ /** Records one loss note. */
545
+ type NoteFn = (code: string, message: string, column?: string | null, count?: number | null) => void;
546
+
547
+ /**
548
+ * W_CSV_DIRECTION_DROPPED: a table with no direction column (the generic dialect, an adjacency
549
+ * table) loses the direction of undirected edges, and the Gephi edge table of an undirected graph
550
+ * without edges has no Type cell to carry it. A node table holds no edge, so it gets no note.
551
+ * @param snapshot - the snapshot
552
+ * @param csv - the resolved CSV options
553
+ * @param edgeRowCount - how many edge rows are written
554
+ * @param note - the recorder
555
+ */
556
+ function directionNotes(
557
+ snapshot: GraphSnapshot,
558
+ csv: ResolvedCsvExportOptions,
559
+ edgeRowCount: number,
560
+ note: NoteFn,
561
+ ): void {
496
562
  if (csv.table !== "nodes" && (csv.dialect.type === null || csv.table === "adjacency")) {
497
563
  const where = csv.table === "adjacency" ? "an adjacency table" : "the generic dialect";
498
564
  const mixed = countMixedEdges(snapshot);
@@ -511,7 +577,7 @@ function planExport(
511
577
  mixed,
512
578
  );
513
579
  }
514
- } else if (csv.table === "edges" && !snapshot.directed && edgeRows.length === 0) {
580
+ } else if (csv.table === "edges" && !snapshot.directed && edgeRowCount === 0) {
515
581
  note(
516
582
  CSV_LOSS.DIRECTION_DROPPED,
517
583
  "the direction lives in the Type cell of each edge row; an undirected graph without edges reads back as directed",
@@ -519,22 +585,25 @@ function planExport(
519
585
  0,
520
586
  );
521
587
  }
522
- if (folding.mutualCount > 0 && csv.table !== "nodes") {
523
- note(
524
- CSV_LOSS.MUTUAL_EXPANDED,
525
- `${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
526
- null,
527
- folding.mutualCount,
528
- );
529
- }
530
- if (csv.table === "edges") {
531
- noteNodeCoverage(snapshot, edgeRows, note);
532
- }
533
-
534
- // weights
535
- const weights = explicitWeights(snapshot);
588
+ }
536
589
 
537
- // the edge table's columns
590
+ /**
591
+ * The edge table's columns, with their notes: the attribute columns' names (edge table only), the
592
+ * columns an adjacency table drops, and the values of every column the edge table writes.
593
+ * @param snapshot - the snapshot
594
+ * @param csv - the resolved CSV options
595
+ * @param edgeRows - the edge rows written
596
+ * @param values - whether the values are inspected
597
+ * @param note - the recorder
598
+ * @returns the id and label role columns and the attribute columns
599
+ */
600
+ function planEdgeColumns(
601
+ snapshot: GraphSnapshot,
602
+ csv: ResolvedCsvExportOptions,
603
+ edgeRows: readonly number[],
604
+ values: boolean,
605
+ note: NoteFn,
606
+ ): { edgeId: Column | null; edgeLabel: Column | null; edgeColumns: ColumnOut[] } {
538
607
  const edgeId = snapshot.edges.byRole("id");
539
608
  const edgeLabel = snapshot.edges.byRole("label");
540
609
  const edgeReserved = new Set<string>([csv.dialect.source, csv.dialect.target, csv.dialect.weight].map(lower));
@@ -567,8 +636,23 @@ function planExport(
567
636
  checkValues(column, "edge", edgeRows, note);
568
637
  }
569
638
  }
639
+ return { edgeId, edgeLabel, edgeColumns };
640
+ }
570
641
 
571
- // the node table's columns
642
+ /**
643
+ * The node table's columns, with their notes when the node table is the one written.
644
+ * @param snapshot - the snapshot
645
+ * @param csv - the resolved CSV options
646
+ * @param values - whether the values are inspected
647
+ * @param note - the recorder
648
+ * @returns the label role column and the attribute columns
649
+ */
650
+ function planNodeColumns(
651
+ snapshot: GraphSnapshot,
652
+ csv: ResolvedCsvExportOptions,
653
+ values: boolean,
654
+ note: NoteFn,
655
+ ): { nodeLabel: Column | null; nodeColumns: ColumnOut[] } {
572
656
  const nodeLabel = snapshot.nodes.byRole("label");
573
657
  const nodeReserved = new Set<string>([lower(csv.dialect.id)]);
574
658
  const nodeRows: number[] = [];
@@ -585,35 +669,13 @@ function planExport(
585
669
  nodeLabel,
586
670
  csv.table === "nodes" ? note : null,
587
671
  );
588
- if (csv.table === "nodes") {
589
- if (values) {
590
- checkRoleColumn(nodeLabel, "label", LABEL_NAMES, "node", nodeRows, note);
591
- for (const { column } of nodeColumns) {
592
- checkValues(column, "node", nodeRows, note);
593
- }
672
+ if (csv.table === "nodes" && values) {
673
+ checkRoleColumn(nodeLabel, "label", LABEL_NAMES, "node", nodeRows, note);
674
+ for (const { column } of nodeColumns) {
675
+ checkValues(column, "node", nodeRows, note);
594
676
  }
595
- } else {
596
- nodeTableNote(
597
- [...(nodeLabel === null ? [] : [nodeLabel.meta.name]), ...nodeColumns.map((c) => c.column.meta.name)],
598
- csv.table,
599
- notes,
600
- );
601
677
  }
602
-
603
- return {
604
- csv,
605
- common,
606
- notes,
607
- edgeRows,
608
- folding,
609
- edgeId,
610
- edgeLabel,
611
- weights,
612
- edgeColumns,
613
- nodeLabel,
614
- nodeColumns,
615
- idText,
616
- };
678
+ return { nodeLabel, nodeColumns };
617
679
  }
618
680
 
619
681
  /**
@@ -859,26 +921,13 @@ function checkValues(
859
921
  column: Column,
860
922
  domain: "node" | "edge",
861
923
  rows: readonly number[],
862
- note: (code: string, message: string, column?: string | null, count?: number | null) => void,
924
+ note: NoteFn,
863
925
  roleColumn = false,
864
926
  ): void {
865
927
  const { name, dtype } = column.meta;
866
928
  const label = `${domain} column "${name}"`;
867
929
  if (column.dtype === "f32" || column.dtype === "f64") {
868
- let nonFinite = 0;
869
- const { components } = column.meta;
870
- const { data } = column;
871
- for (const row of rows) {
872
- if (!column.isSet(row)) {
873
- continue;
874
- }
875
- for (let k = 0; k < components; k++) {
876
- if (!Number.isFinite(data[row * components + k])) {
877
- nonFinite++;
878
- break;
879
- }
880
- }
881
- }
930
+ const nonFinite = countNonFinite(column, rows);
882
931
  if (nonFinite > 0) {
883
932
  note(
884
933
  CSV_LOSS.NONFINITE,
@@ -892,15 +941,77 @@ function checkValues(
892
941
  if (dtype !== "string" && dtype !== "dict") {
893
942
  return;
894
943
  }
944
+ const scan = scanText(column, rows);
945
+ if (roleColumn || scan.widest < 0) {
946
+ return;
947
+ }
948
+ const readsAsDict = scan.candidate && scan.heuristic.decide() === "dict";
949
+ if (scan.widest < WIDENING.string && !readsAsDict) {
950
+ note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[scan.widest]}`, name, scan.set);
951
+ return;
952
+ }
953
+ if (dtype === "dict" && !readsAsDict) {
954
+ note(
955
+ CSV_LOSS.STORAGE_CLASS_CHANGED,
956
+ `${label}: reads back as string (cardinality too high for a dict)`,
957
+ name,
958
+ null,
959
+ );
960
+ } else if (dtype === "string" && readsAsDict) {
961
+ note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
962
+ }
963
+ }
964
+
965
+ /**
966
+ * The rows of a float column holding at least one non-finite component.
967
+ * @param column - the f32 or f64 column
968
+ * @param rows - the rows written
969
+ * @returns how many rows hold one
970
+ */
971
+ function countNonFinite(column: F32Column | F64Column, rows: readonly number[]): number {
972
+ let nonFinite = 0;
973
+ const { components } = column.meta;
974
+ const { data } = column;
975
+ for (const row of rows) {
976
+ if (!column.isSet(row)) {
977
+ continue;
978
+ }
979
+ for (let k = 0; k < components; k++) {
980
+ if (!Number.isFinite(data[row * components + k])) {
981
+ nonFinite++;
982
+ break;
983
+ }
984
+ }
985
+ }
986
+ return nonFinite;
987
+ }
988
+
989
+ /** What a text column's written values read back as. */
990
+ interface TextScan {
991
+ /** The widest dtype the values infer to (WIDENING), -1 when no value is set. */
992
+ readonly widest: number;
993
+ /** Whether every value the dict heuristic sampled infers to string. */
994
+ readonly candidate: boolean;
995
+ /** How many rows hold a string. */
996
+ readonly set: number;
997
+ /** The dict heuristic over the sampled values. */
998
+ readonly heuristic: DictHeuristic;
999
+ }
1000
+
1001
+ /**
1002
+ * Infer the dtype every written value of a text column reads back as, and sample the values for
1003
+ * the dict heuristic.
1004
+ * @param column - the string or dict column
1005
+ * @param rows - the rows written
1006
+ * @returns the scan
1007
+ */
1008
+ function scanText(column: Column, rows: readonly number[]): TextScan {
895
1009
  let widest = -1;
896
1010
  let candidate = true;
897
1011
  let set = 0;
898
1012
  const heuristic = new DictHeuristic(DICT_SAMPLE_ROWS);
899
1013
  for (const row of rows) {
900
- if (!column.isSet(row)) {
901
- continue;
902
- }
903
- const text = column.value(row);
1014
+ const text = column.isSet(row) ? column.value(row) : null;
904
1015
  if (typeof text !== "string") {
905
1016
  continue;
906
1017
  }
@@ -908,30 +1019,11 @@ function checkValues(
908
1019
  const kind = inferTextDtype(text);
909
1020
  widest = Math.max(widest, WIDENING[kind]);
910
1021
  if (!heuristic.decided) {
911
- if (kind !== "string") {
912
- candidate = false;
913
- }
1022
+ candidate &&= kind === "string";
914
1023
  heuristic.observe(text);
915
1024
  }
916
1025
  }
917
- if (roleColumn || widest < 0) {
918
- return;
919
- }
920
- const readsAsDict = candidate && heuristic.decide() === "dict";
921
- if (widest < WIDENING.string && !readsAsDict) {
922
- note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[widest]}`, name, set);
923
- return;
924
- }
925
- if (dtype === "dict" && !readsAsDict) {
926
- note(
927
- CSV_LOSS.STORAGE_CLASS_CHANGED,
928
- `${label}: reads back as string (cardinality too high for a dict)`,
929
- name,
930
- null,
931
- );
932
- } else if (dtype === "string" && readsAsDict) {
933
- note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
934
- }
1026
+ return { widest, candidate, set, heuristic };
935
1027
  }
936
1028
 
937
1029
  /**
@@ -1064,59 +1156,79 @@ function* adjacencyLines(snapshot: GraphSnapshot, plan: Plan): Generator<string,
1064
1156
  * @returns nothing
1065
1157
  */
1066
1158
  function* lines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, undefined> {
1067
- const { csv } = plan;
1068
- if (csv.table === "adjacency") {
1159
+ if (plan.csv.table === "adjacency") {
1069
1160
  yield* adjacencyLines(snapshot, plan);
1070
- return;
1161
+ } else if (plan.csv.table === "nodes") {
1162
+ yield* nodeLines(snapshot, plan);
1163
+ } else {
1164
+ yield* edgeLines(snapshot, plan);
1071
1165
  }
1072
- const { delimiter, newline } = csv;
1166
+ }
1167
+
1168
+ /**
1169
+ * The cell writers of a delimiter.
1170
+ * @param delimiter - the field delimiter
1171
+ * @returns `quote` for a text and `cell` for a column value (an unset value is the empty cell)
1172
+ */
1173
+ function cellWriters(delimiter: string): {
1174
+ quote: (text: string) => string;
1175
+ cell: (column: Column, row: number) => string;
1176
+ } {
1073
1177
  const quote = (text: string): string => quoteCsvCell(text, delimiter);
1074
1178
  const cell = (column: Column, row: number): string => {
1075
1179
  const text = cellText(column, row);
1076
1180
  return text === null ? "" : quote(text);
1077
1181
  };
1078
- if (csv.table === "nodes") {
1079
- const headers = [csv.dialect.id];
1080
- if (plan.nodeLabel !== null) {
1081
- headers.push(plan.nodeLabel.meta.name);
1082
- }
1083
- for (const { header } of plan.nodeColumns) {
1084
- headers.push(header);
1085
- }
1086
- if (csv.header) {
1087
- yield headers.map(quote).join(delimiter) + newline;
1088
- }
1089
- for (let i = 0; i < snapshot.nodeCount; i++) {
1090
- const cells = [quote(plan.idText(i))];
1091
- if (plan.nodeLabel !== null) {
1092
- cells.push(cell(plan.nodeLabel, i));
1093
- }
1094
- for (const { column } of plan.nodeColumns) {
1095
- cells.push(cell(column, i));
1096
- }
1097
- yield cells.join(delimiter) + newline;
1098
- }
1099
- return;
1100
- }
1101
- const headers = [csv.dialect.source, csv.dialect.target];
1102
- if (csv.dialect.type !== null) {
1103
- headers.push(csv.dialect.type);
1104
- }
1105
- if (plan.edgeId !== null) {
1106
- headers.push(plan.edgeId.meta.name);
1107
- }
1108
- if (plan.edgeLabel !== null) {
1109
- headers.push(plan.edgeLabel.meta.name);
1110
- }
1111
- if (plan.weights.weighted) {
1112
- headers.push(csv.dialect.weight);
1182
+ return { quote, cell };
1183
+ }
1184
+
1185
+ /**
1186
+ * The lines of the node table: the id, the label and the attribute columns of every node.
1187
+ * @param snapshot - the snapshot
1188
+ * @param plan - the plan
1189
+ * @yields one line at a time
1190
+ * @returns nothing
1191
+ */
1192
+ function* nodeLines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, undefined> {
1193
+ const { csv } = plan;
1194
+ const { delimiter, newline } = csv;
1195
+ const { quote, cell } = cellWriters(delimiter);
1196
+ const headers = [csv.dialect.id];
1197
+ if (plan.nodeLabel !== null) {
1198
+ headers.push(plan.nodeLabel.meta.name);
1113
1199
  }
1114
- for (const { header } of plan.edgeColumns) {
1200
+ for (const { header } of plan.nodeColumns) {
1115
1201
  headers.push(header);
1116
1202
  }
1117
1203
  if (csv.header) {
1118
1204
  yield headers.map(quote).join(delimiter) + newline;
1119
1205
  }
1206
+ for (let i = 0; i < snapshot.nodeCount; i++) {
1207
+ const cells = [quote(plan.idText(i))];
1208
+ if (plan.nodeLabel !== null) {
1209
+ cells.push(cell(plan.nodeLabel, i));
1210
+ }
1211
+ for (const { column } of plan.nodeColumns) {
1212
+ cells.push(cell(column, i));
1213
+ }
1214
+ yield cells.join(delimiter) + newline;
1215
+ }
1216
+ }
1217
+
1218
+ /**
1219
+ * The lines of the edge table: one row per written edge.
1220
+ * @param snapshot - the snapshot
1221
+ * @param plan - the plan
1222
+ * @yields one line at a time
1223
+ * @returns nothing
1224
+ */
1225
+ function* edgeLines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, undefined> {
1226
+ const { csv } = plan;
1227
+ const { delimiter, newline } = csv;
1228
+ const { quote, cell } = cellWriters(delimiter);
1229
+ if (csv.header) {
1230
+ yield edgeHeaders(plan).map(quote).join(delimiter) + newline;
1231
+ }
1120
1232
  const list = snapshot.edgeList();
1121
1233
  for (const e of plan.edgeRows) {
1122
1234
  const cells = [quote(plan.idText(list.src[e])), quote(plan.idText(list.dst[e]))];
@@ -1139,6 +1251,32 @@ function* lines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, un
1139
1251
  }
1140
1252
  }
1141
1253
 
1254
+ /**
1255
+ * The header of the edge table.
1256
+ * @param plan - the plan
1257
+ * @returns the header names, unquoted
1258
+ */
1259
+ function edgeHeaders(plan: Plan): string[] {
1260
+ const { dialect } = plan.csv;
1261
+ const headers = [dialect.source, dialect.target];
1262
+ if (dialect.type !== null) {
1263
+ headers.push(dialect.type);
1264
+ }
1265
+ if (plan.edgeId !== null) {
1266
+ headers.push(plan.edgeId.meta.name);
1267
+ }
1268
+ if (plan.edgeLabel !== null) {
1269
+ headers.push(plan.edgeLabel.meta.name);
1270
+ }
1271
+ if (plan.weights.weighted) {
1272
+ headers.push(dialect.weight);
1273
+ }
1274
+ for (const { header } of plan.edgeColumns) {
1275
+ headers.push(header);
1276
+ }
1277
+ return headers;
1278
+ }
1279
+
1142
1280
  /**
1143
1281
  * Plan an export and refuse it when a fatal note is present (E_INVALID_ID for id text collisions),
1144
1282
  * before anything is written.