@graphty/graph-io 0.3.41 → 0.3.43

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 (107) 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-RVtXVetw.js} +36 -22
  17. package/dist/chunks/exporter-RVtXVetw.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 +5 -4
  49. package/dist/gexf.js.map +1 -1
  50. package/dist/gml.js +3 -3
  51. package/dist/graph-io.js +27 -27
  52. package/dist/graphml.js +2 -2
  53. package/dist/json.js +1 -1
  54. package/dist/neo4j.js +3 -3
  55. package/dist/obo.js +1 -1
  56. package/dist/pajek.js +1 -1
  57. package/dist/src/children.d.ts.map +1 -1
  58. package/dist/src/children.js +50 -27
  59. package/dist/src/children.js.map +1 -1
  60. package/dist/src/common/attributes.d.ts.map +1 -1
  61. package/dist/src/common/attributes.js +52 -33
  62. package/dist/src/common/attributes.js.map +1 -1
  63. package/dist/src/common/input.d.ts.map +1 -1
  64. package/dist/src/common/input.js +178 -82
  65. package/dist/src/common/input.js.map +1 -1
  66. package/dist/src/common/lists.js +30 -15
  67. package/dist/src/common/lists.js.map +1 -1
  68. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  69. package/dist/src/formats/csv/exporter.js +211 -121
  70. package/dist/src/formats/csv/exporter.js.map +1 -1
  71. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  72. package/dist/src/formats/csv/importer.js +113 -74
  73. package/dist/src/formats/csv/importer.js.map +1 -1
  74. package/dist/src/formats/csv/records.d.ts.map +1 -1
  75. package/dist/src/formats/csv/records.js +60 -26
  76. package/dist/src/formats/csv/records.js.map +1 -1
  77. package/dist/src/formats/dot/tokenizer.d.ts +39 -0
  78. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  79. package/dist/src/formats/dot/tokenizer.js +112 -66
  80. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  81. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  82. package/dist/src/formats/gexf/exporter.js +3 -2
  83. package/dist/src/formats/gexf/exporter.js.map +1 -1
  84. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  85. package/dist/src/formats/json/dialect.js +54 -20
  86. package/dist/src/formats/json/dialect.js.map +1 -1
  87. package/dist/src/formats/json/importer.d.ts.map +1 -1
  88. package/dist/src/formats/json/importer.js +2 -1
  89. package/dist/src/formats/json/importer.js.map +1 -1
  90. package/dist/xgmml.js +2 -2
  91. package/package.json +2 -2
  92. package/src/children.ts +54 -27
  93. package/src/common/attributes.ts +59 -30
  94. package/src/common/input.ts +223 -95
  95. package/src/common/lists.ts +29 -15
  96. package/src/formats/csv/exporter.ts +274 -136
  97. package/src/formats/csv/importer.ts +138 -78
  98. package/src/formats/csv/records.ts +71 -25
  99. package/src/formats/dot/tokenizer.ts +118 -68
  100. package/src/formats/gexf/exporter.ts +3 -2
  101. package/src/formats/json/dialect.ts +61 -22
  102. package/src/formats/json/importer.ts +2 -1
  103. package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
  104. package/dist/chunks/export-DD3Lvw-F.js.map +0 -1
  105. package/dist/chunks/exporter-BqtmlxNr.js.map +0 -1
  106. package/dist/chunks/importer-S-abxJHn.js.map +0 -1
  107. package/dist/chunks/records-Uc3LkABx.js.map +0 -1
package/dist/csv.js CHANGED
@@ -1,8 +1,8 @@
1
- import { b as plural, bU as DICT_SAMPLE_ROWS, ac as DictHeuristic, R as ROLE_TAKEN_CODE$1, I as ID_MERGED_CODE$1, m as DUPLICATE_EDGE_ID_CODE$1, n as DUPLICATE_NODE_CODE$1, t as MISSING_ID_CODE$1, s as MISSING_ENDPOINT_CODE$1, E as EMPTY_INPUT_CODE$1, al as resolveImportOptions, a8 as ImportReportBuilder, ao as reportSinkOptions, an as reportUnusedOptions, aq as DirectionResolver, ar as IdCoercer, am as throwIfAborted, O as OPTION_IGNORED_CODE, aK as parseWeightText, a0 as uniqueColumnName, ay as RENAMED_CODE, a1 as declareResolved, c as capabilities, L as LOSS, a as checkCapabilities, r as resolveExportOptions, p as pairFolding, bD as countMixedEdges, $ as agree, e as explicitWeights, b_ as otherFormatDialect, bS as joinListText, aG as formatInteger, aF as formatDecimal, aH as canonicalId, M as MIXED_DIRECTION_CODE, D as DIRECTION_FORCED_CODE, d as DIRECTION_REFUSED_CODE, S as SINK_OPTION_CODE, C as COLUMN_RENAMED_CODE, w as UNKNOWN_ENCODING_CODE, x as ENCODING_FALLBACK_CODE, y as INVALID_ENCODING_CODE, z as INVALID_UTF8_CODE, P as PRECISION_CODE, F as ELEMENT_ISSUE, G as INPUT_ISSUE } from "./chunks/export-DD3Lvw-F.js";
2
- import { i as inferTextDtype, T as TextCellWriter, p as parseTextCell, W as WIDENING_UNSUPPORTED_CODE } from "./chunks/text-Bh9SaAob.js";
1
+ import { b as plural, bU as DICT_SAMPLE_ROWS, ac as DictHeuristic, R as ROLE_TAKEN_CODE$1, I as ID_MERGED_CODE$1, m as DUPLICATE_EDGE_ID_CODE$1, n as DUPLICATE_NODE_CODE$1, t as MISSING_ID_CODE$1, s as MISSING_ENDPOINT_CODE$1, E as EMPTY_INPUT_CODE$1, al as resolveImportOptions, a8 as ImportReportBuilder, ao as reportSinkOptions, an as reportUnusedOptions, aq as DirectionResolver, ar as IdCoercer, am as throwIfAborted, O as OPTION_IGNORED_CODE, aK as parseWeightText, a0 as uniqueColumnName, ay as RENAMED_CODE, a1 as declareResolved, c as capabilities, L as LOSS, a as checkCapabilities, r as resolveExportOptions, p as pairFolding, $ as agree, e as explicitWeights, b_ as otherFormatDialect, bD as countMixedEdges, bS as joinListText, aG as formatInteger, aF as formatDecimal, aH as canonicalId, M as MIXED_DIRECTION_CODE, D as DIRECTION_FORCED_CODE, d as DIRECTION_REFUSED_CODE, S as SINK_OPTION_CODE, C as COLUMN_RENAMED_CODE, w as UNKNOWN_ENCODING_CODE, x as ENCODING_FALLBACK_CODE, y as INVALID_ENCODING_CODE, z as INVALID_UTF8_CODE, P as PRECISION_CODE, F as ELEMENT_ISSUE, G as INPUT_ISSUE } from "./chunks/export-rKLeGQMo.js";
2
+ import { i as inferTextDtype, T as TextCellWriter, p as parseTextCell, W as WIDENING_UNSUPPORTED_CODE } from "./chunks/text-DHOJXnED.js";
3
3
  import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
4
- import { s as stripLeadingComments, a as sniffNewline, b as sniffDelimiter, D as DELIMITER_CANDIDATES, c as checkRecordSyntax, C as CsvRecordReader, d as splitRecords, B as BAD_QUOTE_CODE, U as UNCLOSED_QUOTE_CODE } from "./chunks/records-Uc3LkABx.js";
5
- import { o as quoteCsvCell } from "./chunks/escape-DBb8zuNE.js";
4
+ import { s as stripLeadingComments, a as sniffNewline, b as sniffDelimiter, D as DELIMITER_CANDIDATES, C as CsvRecordReader, d as splitRecords, c as checkRecordSyntax, B as BAD_QUOTE_CODE, U as UNCLOSED_QUOTE_CODE } from "./chunks/records-Cu4bxYOm.js";
5
+ import { o as quoteCsvCell } from "./chunks/escape-B62ZGM_a.js";
6
6
  import { j as joinText, e as encodeChunks } from "./chunks/writer-DxjgHwSe.js";
7
7
  const SOURCE_NAMES = Object.freeze([
8
8
  "source",
@@ -336,6 +336,38 @@ function isPadded(text, quoted) {
336
336
  }
337
337
  function resolveCsvOptions(options) {
338
338
  const o = options ?? {};
339
+ checkDialectOptions(o);
340
+ for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"]) {
341
+ checkColumnRef(name, o[name]);
342
+ }
343
+ if (o.typeColumn !== null) {
344
+ checkColumnRef("typeColumn", o.typeColumn);
345
+ }
346
+ if (o.table === "adjacency") {
347
+ checkAdjacencyOptions(o);
348
+ }
349
+ if (o.rowNumberIds !== void 0 && typeof o.rowNumberIds !== "boolean") {
350
+ throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: expected a boolean", {
351
+ option: "rowNumberIds",
352
+ found: o.rowNumberIds
353
+ });
354
+ }
355
+ return {
356
+ delimiter: o.delimiter ?? null,
357
+ decimalComma: o.decimal === ",",
358
+ header: o.header ?? "auto",
359
+ table: o.table ?? "auto",
360
+ sourceColumn: o.sourceColumn ?? null,
361
+ targetColumn: o.targetColumn ?? null,
362
+ typeColumn: o.typeColumn,
363
+ idColumn: o.idColumn ?? null,
364
+ labelColumn: o.labelColumn ?? null,
365
+ nodes: nodesInput(o.nodes),
366
+ rowNumberIds: o.rowNumberIds ?? false,
367
+ explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null
368
+ };
369
+ }
370
+ function checkDialectOptions(o) {
339
371
  if (o.delimiter !== void 0 && (typeof o.delimiter !== "string" || o.delimiter.length === 0)) {
340
372
  throw new GraphFormatError("E_UNSUPPORTED", "option delimiter: expected a non-empty string", {
341
373
  option: "delimiter",
@@ -369,48 +401,22 @@ function resolveCsvOptions(options) {
369
401
  found: o.table
370
402
  });
371
403
  }
372
- for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"]) {
373
- checkColumnRef(name, o[name]);
374
- }
375
- if (o.typeColumn !== null) {
376
- checkColumnRef("typeColumn", o.typeColumn);
377
- }
378
- if (o.table === "adjacency") {
379
- for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"]) {
380
- if (o[name] !== void 0) {
381
- throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
382
- option: name,
383
- found: o[name]
384
- });
385
- }
386
- }
387
- if (o.rowNumberIds === true) {
388
- throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: an adjacency table names its nodes", {
389
- option: "rowNumberIds",
390
- found: o.rowNumberIds
404
+ }
405
+ function checkAdjacencyOptions(o) {
406
+ for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"]) {
407
+ if (o[name] !== void 0) {
408
+ throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
409
+ option: name,
410
+ found: o[name]
391
411
  });
392
412
  }
393
413
  }
394
- if (o.rowNumberIds !== void 0 && typeof o.rowNumberIds !== "boolean") {
395
- throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: expected a boolean", {
414
+ if (o.rowNumberIds === true) {
415
+ throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: an adjacency table names its nodes", {
396
416
  option: "rowNumberIds",
397
417
  found: o.rowNumberIds
398
418
  });
399
419
  }
400
- return {
401
- delimiter: o.delimiter ?? null,
402
- decimalComma: o.decimal === ",",
403
- header: o.header ?? "auto",
404
- table: o.table ?? "auto",
405
- sourceColumn: o.sourceColumn ?? null,
406
- targetColumn: o.targetColumn ?? null,
407
- typeColumn: o.typeColumn,
408
- idColumn: o.idColumn ?? null,
409
- labelColumn: o.labelColumn ?? null,
410
- nodes: nodesInput(o.nodes),
411
- rowNumberIds: o.rowNumberIds ?? false,
412
- explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null
413
- };
414
420
  }
415
421
  function checkColumnRef(name, value) {
416
422
  if (value === void 0) {
@@ -573,39 +579,11 @@ class TableReader {
573
579
  const firstLine = this.reader.line;
574
580
  const firstQuoted = this.reader.quoted.slice(0, firstRow.length);
575
581
  const pending = [];
576
- let header;
577
- let mode = this.state.csv.header;
578
- if (mode === "auto" && this.kind === "adjacency") {
579
- mode = false;
580
- } else if (mode === "auto" && this.rowNumbers) {
581
- mode = true;
582
- }
583
- if (mode === "auto") {
584
- const second = await iterator.next();
585
- const secondRow = second.done ? null : second.value;
586
- const secondLine = this.reader.line;
587
- const secondQuoted = this.reader.quoted.slice(0, secondRow?.length ?? 0);
588
- header = looksLikeHeader(firstRow, secondRow) || this.namesExplicitColumn(firstRow);
589
- if (!header) {
590
- pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
591
- }
592
- if (secondRow !== null) {
593
- pending.push({ row: secondRow, quoted: secondQuoted, line: secondLine });
594
- }
595
- } else {
596
- header = mode;
597
- if (!header) {
598
- pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
599
- }
600
- }
601
- if (this.rowNumbers && !header) {
602
- report.warning(
603
- "unsupported",
604
- OPTION_IGNORED_CODE,
605
- "option rowNumberIds ignored: a headerless node table takes its ids from column 0",
606
- { line: firstLine, element: "rowNumberIds" }
607
- );
608
- }
582
+ const header = await this.decideHeader(
583
+ iterator,
584
+ { row: firstRow, quoted: firstQuoted, line: firstLine },
585
+ pending
586
+ );
609
587
  const names = header ? uniqueNames(headerNames(firstRow), report, firstLine) : positionalNames(firstRow.length);
610
588
  const last = firstRow.length - 1;
611
589
  const optionalLast = header && last > 0 && isUnset(firstRow[last], firstQuoted[last]);
@@ -635,6 +613,70 @@ class TableReader {
635
613
  for (const { row, quoted, line } of pending) {
636
614
  this.processRow(row, quoted, line);
637
615
  }
616
+ await this.readRemainingRows(iterator);
617
+ if (this.dataRows === 0 && header) {
618
+ report.warning("missing-value", NO_DATA_ROWS_CODE, "the table has a header and no data rows", {
619
+ line: firstLine
620
+ });
621
+ }
622
+ this.reportReaderFindings();
623
+ }
624
+ /**
625
+ * Decide whether the first row is the header, reading the second row when the header option is
626
+ * "auto", and queue the rows read so far that are data.
627
+ * @param iterator - the record iterator, past the first row
628
+ * @param first - the first row
629
+ * @param pending - receives the data rows read so far, in order
630
+ * @returns whether the first row is the header
631
+ */
632
+ async decideHeader(iterator, first, pending) {
633
+ const mode = this.headerMode();
634
+ let second = null;
635
+ let header;
636
+ if (mode === "auto") {
637
+ const next = await iterator.next();
638
+ second = next.done ? null : { row: next.value, quoted: this.reader.quoted.slice(0, next.value.length), line: this.reader.line };
639
+ header = looksLikeHeader(first.row, second?.row ?? null) || this.namesExplicitColumn(first.row);
640
+ } else {
641
+ header = mode;
642
+ }
643
+ if (!header) {
644
+ pending.push(first);
645
+ }
646
+ if (second !== null) {
647
+ pending.push(second);
648
+ }
649
+ if (this.rowNumbers && !header) {
650
+ this.state.report.warning(
651
+ "unsupported",
652
+ OPTION_IGNORED_CODE,
653
+ "option rowNumberIds ignored: a headerless node table takes its ids from column 0",
654
+ { line: first.line, element: "rowNumberIds" }
655
+ );
656
+ }
657
+ return header;
658
+ }
659
+ /**
660
+ * The header option as this table reads it: an adjacency table has no header unless the caller
661
+ * says so (its rows vary in width); a table numbered by rowNumberIds has one, since a headerless
662
+ * table takes its ids from column 0.
663
+ * @returns true, false or "auto"
664
+ */
665
+ headerMode() {
666
+ const mode = this.state.csv.header;
667
+ if (mode === "auto" && this.kind === "adjacency") {
668
+ return false;
669
+ }
670
+ if (mode === "auto" && this.rowNumbers) {
671
+ return true;
672
+ }
673
+ return mode;
674
+ }
675
+ /**
676
+ * Push every row after the header and the rows read to decide it, then finish the column writers.
677
+ * @param iterator - the record iterator
678
+ */
679
+ async readRemainingRows(iterator) {
638
680
  const { signal } = this.state.common;
639
681
  let sinceCheck = 0;
640
682
  for (; ; ) {
@@ -651,12 +693,6 @@ class TableReader {
651
693
  for (const writer of this.writers) {
652
694
  writer?.finish();
653
695
  }
654
- if (this.dataRows === 0 && header) {
655
- report.warning("missing-value", NO_DATA_ROWS_CODE, "the table has a header and no data rows", {
656
- line: firstLine
657
- });
658
- }
659
- this.reportReaderFindings();
660
696
  }
661
697
  /**
662
698
  * Whether a row is Excel's delimiter directive (`sep=;`) under a delimiter the caller gave (a
@@ -1991,6 +2027,44 @@ function planExport(snapshot, options, values) {
1991
2027
  edgeRows.push(e);
1992
2028
  }
1993
2029
  }
2030
+ directionNotes(snapshot, csv, edgeRows.length, note);
2031
+ if (folding.mutualCount > 0 && csv.table !== "nodes") {
2032
+ note(
2033
+ CSV_LOSS.MUTUAL_EXPANDED,
2034
+ `${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
2035
+ null,
2036
+ folding.mutualCount
2037
+ );
2038
+ }
2039
+ if (csv.table === "edges") {
2040
+ noteNodeCoverage(snapshot, edgeRows, note);
2041
+ }
2042
+ const weights = explicitWeights(snapshot);
2043
+ const { edgeId, edgeLabel, edgeColumns } = planEdgeColumns(snapshot, csv, edgeRows, values, note);
2044
+ const { nodeLabel, nodeColumns } = planNodeColumns(snapshot, csv, values, note);
2045
+ if (csv.table !== "nodes") {
2046
+ nodeTableNote(
2047
+ [...nodeLabel === null ? [] : [nodeLabel.meta.name], ...nodeColumns.map((c) => c.column.meta.name)],
2048
+ csv.table,
2049
+ notes
2050
+ );
2051
+ }
2052
+ return {
2053
+ csv,
2054
+ common,
2055
+ notes,
2056
+ edgeRows,
2057
+ folding,
2058
+ edgeId,
2059
+ edgeLabel,
2060
+ weights,
2061
+ edgeColumns,
2062
+ nodeLabel,
2063
+ nodeColumns,
2064
+ idText
2065
+ };
2066
+ }
2067
+ function directionNotes(snapshot, csv, edgeRowCount, note) {
1994
2068
  if (csv.table !== "nodes" && (csv.dialect.type === null || csv.table === "adjacency")) {
1995
2069
  const where = csv.table === "adjacency" ? "an adjacency table" : "the generic dialect";
1996
2070
  const mixed = countMixedEdges(snapshot);
@@ -2009,7 +2083,7 @@ function planExport(snapshot, options, values) {
2009
2083
  mixed
2010
2084
  );
2011
2085
  }
2012
- } else if (csv.table === "edges" && !snapshot.directed && edgeRows.length === 0) {
2086
+ } else if (csv.table === "edges" && !snapshot.directed && edgeRowCount === 0) {
2013
2087
  note(
2014
2088
  CSV_LOSS.DIRECTION_DROPPED,
2015
2089
  "the direction lives in the Type cell of each edge row; an undirected graph without edges reads back as directed",
@@ -2017,18 +2091,8 @@ function planExport(snapshot, options, values) {
2017
2091
  0
2018
2092
  );
2019
2093
  }
2020
- if (folding.mutualCount > 0 && csv.table !== "nodes") {
2021
- note(
2022
- CSV_LOSS.MUTUAL_EXPANDED,
2023
- `${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
2024
- null,
2025
- folding.mutualCount
2026
- );
2027
- }
2028
- if (csv.table === "edges") {
2029
- noteNodeCoverage(snapshot, edgeRows, note);
2030
- }
2031
- const weights = explicitWeights(snapshot);
2094
+ }
2095
+ function planEdgeColumns(snapshot, csv, edgeRows, values, note) {
2032
2096
  const edgeId = snapshot.edges.byRole("id");
2033
2097
  const edgeLabel = snapshot.edges.byRole("label");
2034
2098
  const edgeReserved = new Set([csv.dialect.source, csv.dialect.target, csv.dialect.weight].map(lower));
@@ -2061,6 +2125,9 @@ function planExport(snapshot, options, values) {
2061
2125
  checkValues(column, "edge", edgeRows, note);
2062
2126
  }
2063
2127
  }
2128
+ return { edgeId, edgeLabel, edgeColumns };
2129
+ }
2130
+ function planNodeColumns(snapshot, csv, values, note) {
2064
2131
  const nodeLabel = snapshot.nodes.byRole("label");
2065
2132
  const nodeReserved = /* @__PURE__ */ new Set([lower(csv.dialect.id)]);
2066
2133
  const nodeRows = [];
@@ -2077,34 +2144,13 @@ function planExport(snapshot, options, values) {
2077
2144
  nodeLabel,
2078
2145
  csv.table === "nodes" ? note : null
2079
2146
  );
2080
- if (csv.table === "nodes") {
2081
- if (values) {
2082
- checkRoleColumn(nodeLabel, "label", LABEL_NAMES, "node", nodeRows, note);
2083
- for (const { column } of nodeColumns) {
2084
- checkValues(column, "node", nodeRows, note);
2085
- }
2147
+ if (csv.table === "nodes" && values) {
2148
+ checkRoleColumn(nodeLabel, "label", LABEL_NAMES, "node", nodeRows, note);
2149
+ for (const { column } of nodeColumns) {
2150
+ checkValues(column, "node", nodeRows, note);
2086
2151
  }
2087
- } else {
2088
- nodeTableNote(
2089
- [...nodeLabel === null ? [] : [nodeLabel.meta.name], ...nodeColumns.map((c) => c.column.meta.name)],
2090
- csv.table,
2091
- notes
2092
- );
2093
2152
  }
2094
- return {
2095
- csv,
2096
- common,
2097
- notes,
2098
- edgeRows,
2099
- folding,
2100
- edgeId,
2101
- edgeLabel,
2102
- weights,
2103
- edgeColumns,
2104
- nodeLabel,
2105
- nodeColumns,
2106
- idText
2107
- };
2153
+ return { nodeLabel, nodeColumns };
2108
2154
  }
2109
2155
  function noteNodeCoverage(snapshot, edgeRows, note) {
2110
2156
  const { nodeCount } = snapshot;
@@ -2236,20 +2282,7 @@ function checkValues(column, domain, rows, note, roleColumn = false) {
2236
2282
  const { name, dtype } = column.meta;
2237
2283
  const label = `${domain} column "${name}"`;
2238
2284
  if (column.dtype === "f32" || column.dtype === "f64") {
2239
- let nonFinite = 0;
2240
- const { components } = column.meta;
2241
- const { data } = column;
2242
- for (const row of rows) {
2243
- if (!column.isSet(row)) {
2244
- continue;
2245
- }
2246
- for (let k = 0; k < components; k++) {
2247
- if (!Number.isFinite(data[row * components + k])) {
2248
- nonFinite++;
2249
- break;
2250
- }
2251
- }
2252
- }
2285
+ const nonFinite = countNonFinite(column, rows);
2253
2286
  if (nonFinite > 0) {
2254
2287
  note(
2255
2288
  CSV_LOSS.NONFINITE,
@@ -2263,15 +2296,50 @@ function checkValues(column, domain, rows, note, roleColumn = false) {
2263
2296
  if (dtype !== "string" && dtype !== "dict") {
2264
2297
  return;
2265
2298
  }
2299
+ const scan = scanText(column, rows);
2300
+ if (roleColumn || scan.widest < 0) {
2301
+ return;
2302
+ }
2303
+ const readsAsDict = scan.candidate && scan.heuristic.decide() === "dict";
2304
+ if (scan.widest < WIDENING.string && !readsAsDict) {
2305
+ note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[scan.widest]}`, name, scan.set);
2306
+ return;
2307
+ }
2308
+ if (dtype === "dict" && !readsAsDict) {
2309
+ note(
2310
+ CSV_LOSS.STORAGE_CLASS_CHANGED,
2311
+ `${label}: reads back as string (cardinality too high for a dict)`,
2312
+ name,
2313
+ null
2314
+ );
2315
+ } else if (dtype === "string" && readsAsDict) {
2316
+ note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
2317
+ }
2318
+ }
2319
+ function countNonFinite(column, rows) {
2320
+ let nonFinite = 0;
2321
+ const { components } = column.meta;
2322
+ const { data } = column;
2323
+ for (const row of rows) {
2324
+ if (!column.isSet(row)) {
2325
+ continue;
2326
+ }
2327
+ for (let k = 0; k < components; k++) {
2328
+ if (!Number.isFinite(data[row * components + k])) {
2329
+ nonFinite++;
2330
+ break;
2331
+ }
2332
+ }
2333
+ }
2334
+ return nonFinite;
2335
+ }
2336
+ function scanText(column, rows) {
2266
2337
  let widest = -1;
2267
2338
  let candidate = true;
2268
2339
  let set = 0;
2269
2340
  const heuristic = new DictHeuristic(DICT_SAMPLE_ROWS);
2270
2341
  for (const row of rows) {
2271
- if (!column.isSet(row)) {
2272
- continue;
2273
- }
2274
- const text = column.value(row);
2342
+ const text = column.isSet(row) ? column.value(row) : null;
2275
2343
  if (typeof text !== "string") {
2276
2344
  continue;
2277
2345
  }
@@ -2279,30 +2347,11 @@ function checkValues(column, domain, rows, note, roleColumn = false) {
2279
2347
  const kind = inferTextDtype(text);
2280
2348
  widest = Math.max(widest, WIDENING[kind]);
2281
2349
  if (!heuristic.decided) {
2282
- if (kind !== "string") {
2283
- candidate = false;
2284
- }
2350
+ candidate &&= kind === "string";
2285
2351
  heuristic.observe(text);
2286
2352
  }
2287
2353
  }
2288
- if (roleColumn || widest < 0) {
2289
- return;
2290
- }
2291
- const readsAsDict = candidate && heuristic.decide() === "dict";
2292
- if (widest < WIDENING.string && !readsAsDict) {
2293
- note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[widest]}`, name, set);
2294
- return;
2295
- }
2296
- if (dtype === "dict" && !readsAsDict) {
2297
- note(
2298
- CSV_LOSS.STORAGE_CLASS_CHANGED,
2299
- `${label}: reads back as string (cardinality too high for a dict)`,
2300
- name,
2301
- null
2302
- );
2303
- } else if (dtype === "string" && readsAsDict) {
2304
- note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
2305
- }
2354
+ return { widest, candidate, set, heuristic };
2306
2355
  }
2307
2356
  function typeText(snapshot, plan, e) {
2308
2357
  if (!snapshot.directed || !plan.folding.sourceDirected(e)) {
@@ -2384,59 +2433,54 @@ function* adjacencyLines(snapshot, plan) {
2384
2433
  yield* singles(snapshot.nodeCount - 1);
2385
2434
  }
2386
2435
  function* lines(snapshot, plan) {
2387
- const { csv } = plan;
2388
- if (csv.table === "adjacency") {
2436
+ if (plan.csv.table === "adjacency") {
2389
2437
  yield* adjacencyLines(snapshot, plan);
2390
- return;
2438
+ } else if (plan.csv.table === "nodes") {
2439
+ yield* nodeLines(snapshot, plan);
2440
+ } else {
2441
+ yield* edgeLines(snapshot, plan);
2391
2442
  }
2392
- const { delimiter, newline } = csv;
2443
+ }
2444
+ function cellWriters(delimiter) {
2393
2445
  const quote = (text) => quoteCsvCell(text, delimiter);
2394
2446
  const cell = (column, row) => {
2395
2447
  const text = cellText(column, row);
2396
2448
  return text === null ? "" : quote(text);
2397
2449
  };
2398
- if (csv.table === "nodes") {
2399
- const headers2 = [csv.dialect.id];
2400
- if (plan.nodeLabel !== null) {
2401
- headers2.push(plan.nodeLabel.meta.name);
2402
- }
2403
- for (const { header } of plan.nodeColumns) {
2404
- headers2.push(header);
2405
- }
2406
- if (csv.header) {
2407
- yield headers2.map(quote).join(delimiter) + newline;
2408
- }
2409
- for (let i = 0; i < snapshot.nodeCount; i++) {
2410
- const cells = [quote(plan.idText(i))];
2411
- if (plan.nodeLabel !== null) {
2412
- cells.push(cell(plan.nodeLabel, i));
2413
- }
2414
- for (const { column } of plan.nodeColumns) {
2415
- cells.push(cell(column, i));
2416
- }
2417
- yield cells.join(delimiter) + newline;
2418
- }
2419
- return;
2420
- }
2421
- const headers = [csv.dialect.source, csv.dialect.target];
2422
- if (csv.dialect.type !== null) {
2423
- headers.push(csv.dialect.type);
2424
- }
2425
- if (plan.edgeId !== null) {
2426
- headers.push(plan.edgeId.meta.name);
2427
- }
2428
- if (plan.edgeLabel !== null) {
2429
- headers.push(plan.edgeLabel.meta.name);
2430
- }
2431
- if (plan.weights.weighted) {
2432
- headers.push(csv.dialect.weight);
2450
+ return { quote, cell };
2451
+ }
2452
+ function* nodeLines(snapshot, plan) {
2453
+ const { csv } = plan;
2454
+ const { delimiter, newline } = csv;
2455
+ const { quote, cell } = cellWriters(delimiter);
2456
+ const headers = [csv.dialect.id];
2457
+ if (plan.nodeLabel !== null) {
2458
+ headers.push(plan.nodeLabel.meta.name);
2433
2459
  }
2434
- for (const { header } of plan.edgeColumns) {
2460
+ for (const { header } of plan.nodeColumns) {
2435
2461
  headers.push(header);
2436
2462
  }
2437
2463
  if (csv.header) {
2438
2464
  yield headers.map(quote).join(delimiter) + newline;
2439
2465
  }
2466
+ for (let i = 0; i < snapshot.nodeCount; i++) {
2467
+ const cells = [quote(plan.idText(i))];
2468
+ if (plan.nodeLabel !== null) {
2469
+ cells.push(cell(plan.nodeLabel, i));
2470
+ }
2471
+ for (const { column } of plan.nodeColumns) {
2472
+ cells.push(cell(column, i));
2473
+ }
2474
+ yield cells.join(delimiter) + newline;
2475
+ }
2476
+ }
2477
+ function* edgeLines(snapshot, plan) {
2478
+ const { csv } = plan;
2479
+ const { delimiter, newline } = csv;
2480
+ const { quote, cell } = cellWriters(delimiter);
2481
+ if (csv.header) {
2482
+ yield edgeHeaders(plan).map(quote).join(delimiter) + newline;
2483
+ }
2440
2484
  const list = snapshot.edgeList();
2441
2485
  for (const e of plan.edgeRows) {
2442
2486
  const cells = [quote(plan.idText(list.src[e])), quote(plan.idText(list.dst[e]))];
@@ -2458,6 +2502,26 @@ function* lines(snapshot, plan) {
2458
2502
  yield cells.join(delimiter) + newline;
2459
2503
  }
2460
2504
  }
2505
+ function edgeHeaders(plan) {
2506
+ const { dialect } = plan.csv;
2507
+ const headers = [dialect.source, dialect.target];
2508
+ if (dialect.type !== null) {
2509
+ headers.push(dialect.type);
2510
+ }
2511
+ if (plan.edgeId !== null) {
2512
+ headers.push(plan.edgeId.meta.name);
2513
+ }
2514
+ if (plan.edgeLabel !== null) {
2515
+ headers.push(plan.edgeLabel.meta.name);
2516
+ }
2517
+ if (plan.weights.weighted) {
2518
+ headers.push(dialect.weight);
2519
+ }
2520
+ for (const { header } of plan.edgeColumns) {
2521
+ headers.push(header);
2522
+ }
2523
+ return headers;
2524
+ }
2461
2525
  function prepare(snapshot, options) {
2462
2526
  const plan = planExport(snapshot, options, false);
2463
2527
  const collision = plan.notes.find((n) => n.code === CSV_LOSS.ID_TEXT_COLLISION);