@graphty/graph-io 0.3.40 → 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 (210) 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-CS3fvGT1.js → choose-DvpbdRuz.js} +2 -2
  5. package/dist/chunks/{choose-CS3fvGT1.js.map → choose-DvpbdRuz.js.map} +1 -1
  6. package/dist/chunks/{constants-DqKZT6cH.js → constants-fT3E8JYY.js} +2 -2
  7. package/dist/chunks/{constants-DqKZT6cH.js.map → constants-fT3E8JYY.js.map} +1 -1
  8. package/dist/chunks/{escape-BJRLd3vJ.js → escape-B62ZGM_a.js} +2 -2
  9. package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-B62ZGM_a.js.map} +1 -1
  10. package/dist/chunks/{export-B-aVCwyG.js → export-rKLeGQMo.js} +404 -243
  11. package/dist/chunks/export-rKLeGQMo.js.map +1 -0
  12. package/dist/chunks/{exporter-6ocL_9x_.js → exporter-8hLNemFk.js} +4 -4
  13. package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-8hLNemFk.js.map} +1 -1
  14. package/dist/chunks/{exporter-BVXnV9-9.js → exporter-BWwEuOPT.js} +29 -17
  15. package/dist/chunks/exporter-BWwEuOPT.js.map +1 -0
  16. package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-CpRyHZ3H.js} +574 -475
  17. package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
  18. package/dist/chunks/{exporter-SFYkDzD0.js → exporter-yLxKBvLE.js} +23 -20
  19. package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-yLxKBvLE.js.map} +1 -1
  20. package/dist/chunks/{importer-DcKB97kW.js → importer-BbNb7eNg.js} +111 -71
  21. package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
  22. package/dist/chunks/{importer-D_srHyj6.js → importer-C98dpSH4.js} +5 -5
  23. package/dist/chunks/{importer-D_srHyj6.js.map → importer-C98dpSH4.js.map} +1 -1
  24. package/dist/chunks/{importer-BJlGAOht.js → importer-D794XUTe.js} +3 -3
  25. package/dist/chunks/{importer-BJlGAOht.js.map → importer-D794XUTe.js.map} +1 -1
  26. package/dist/chunks/{importer-AjwOJrn2.js → importer-Dz7tI8Sa.js} +202 -181
  27. package/dist/chunks/importer-Dz7tI8Sa.js.map +1 -0
  28. package/dist/chunks/{importer-DEp_fYjf.js → importer-Ht7LNg8L.js} +92 -64
  29. package/dist/chunks/importer-Ht7LNg8L.js.map +1 -0
  30. package/dist/chunks/{importer-CPqnIY8m.js → importer-WrGRZb0N.js} +42 -29
  31. package/dist/chunks/importer-WrGRZb0N.js.map +1 -0
  32. package/dist/chunks/{importer-BpuS3J8C.js → importer-xZ-3suY1.js} +74 -61
  33. package/dist/chunks/importer-xZ-3suY1.js.map +1 -0
  34. package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DKSqpNW1.js} +3 -3
  35. package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DKSqpNW1.js.map} +1 -1
  36. package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-CtRXjg_i.js} +2 -2
  37. package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
  38. package/dist/chunks/{records-r3SFHz11.js → records-Cu4bxYOm.js} +69 -36
  39. package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
  40. package/dist/chunks/{text-Bjfa1xYL.js → text-DHOJXnED.js} +20 -11
  41. package/dist/chunks/text-DHOJXnED.js.map +1 -0
  42. package/dist/csv.js +323 -241
  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 +195 -97
  49. package/dist/gexf.js.map +1 -1
  50. package/dist/gml.js +170 -149
  51. package/dist/gml.js.map +1 -1
  52. package/dist/graph-io.js +27 -27
  53. package/dist/graphml.js +2 -2
  54. package/dist/json.js +1 -1
  55. package/dist/neo4j.js +103 -65
  56. package/dist/neo4j.js.map +1 -1
  57. package/dist/obo.js +1 -1
  58. package/dist/pajek.js +1 -1
  59. package/dist/src/children.d.ts.map +1 -1
  60. package/dist/src/children.js +50 -27
  61. package/dist/src/children.js.map +1 -1
  62. package/dist/src/common/attributes.d.ts.map +1 -1
  63. package/dist/src/common/attributes.js +86 -42
  64. package/dist/src/common/attributes.js.map +1 -1
  65. package/dist/src/common/input.d.ts.map +1 -1
  66. package/dist/src/common/input.js +358 -184
  67. package/dist/src/common/input.js.map +1 -1
  68. package/dist/src/common/lists.js +30 -15
  69. package/dist/src/common/lists.js.map +1 -1
  70. package/dist/src/common/text.d.ts +7 -0
  71. package/dist/src/common/text.d.ts.map +1 -1
  72. package/dist/src/common/text.js +15 -7
  73. package/dist/src/common/text.js.map +1 -1
  74. package/dist/src/common/zip.d.ts.map +1 -1
  75. package/dist/src/common/zip.js +36 -19
  76. package/dist/src/common/zip.js.map +1 -1
  77. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  78. package/dist/src/formats/csv/exporter.js +288 -146
  79. package/dist/src/formats/csv/exporter.js.map +1 -1
  80. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  81. package/dist/src/formats/csv/importer.js +129 -80
  82. package/dist/src/formats/csv/importer.js.map +1 -1
  83. package/dist/src/formats/csv/records.d.ts.map +1 -1
  84. package/dist/src/formats/csv/records.js +78 -36
  85. package/dist/src/formats/csv/records.js.map +1 -1
  86. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  87. package/dist/src/formats/cys/tables.js +11 -8
  88. package/dist/src/formats/cys/tables.js.map +1 -1
  89. package/dist/src/formats/dot/tokenizer.d.ts +39 -0
  90. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  91. package/dist/src/formats/dot/tokenizer.js +112 -66
  92. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  93. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  94. package/dist/src/formats/gexf/exporter.js +65 -37
  95. package/dist/src/formats/gexf/exporter.js.map +1 -1
  96. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  97. package/dist/src/formats/gexf/importer.js +138 -50
  98. package/dist/src/formats/gexf/importer.js.map +1 -1
  99. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  100. package/dist/src/formats/gml/exporter.js +51 -35
  101. package/dist/src/formats/gml/exporter.js.map +1 -1
  102. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  103. package/dist/src/formats/gml/importer.js +103 -58
  104. package/dist/src/formats/gml/importer.js.map +1 -1
  105. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  106. package/dist/src/formats/gml/syntax.js +17 -8
  107. package/dist/src/formats/gml/syntax.js.map +1 -1
  108. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  109. package/dist/src/formats/graphml/exporter.js +34 -24
  110. package/dist/src/formats/graphml/exporter.js.map +1 -1
  111. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  112. package/dist/src/formats/graphml/importer.js +17 -13
  113. package/dist/src/formats/graphml/importer.js.map +1 -1
  114. package/dist/src/formats/graphml/tree.js +23 -14
  115. package/dist/src/formats/graphml/tree.js.map +1 -1
  116. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  117. package/dist/src/formats/graphml/yfiles.js +22 -10
  118. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  119. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  120. package/dist/src/formats/json/dialect.js +54 -20
  121. package/dist/src/formats/json/dialect.js.map +1 -1
  122. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  123. package/dist/src/formats/json/exporter.js +38 -21
  124. package/dist/src/formats/json/exporter.js.map +1 -1
  125. package/dist/src/formats/json/importer.d.ts.map +1 -1
  126. package/dist/src/formats/json/importer.js +536 -343
  127. package/dist/src/formats/json/importer.js.map +1 -1
  128. package/dist/src/formats/json/obographs.js +29 -17
  129. package/dist/src/formats/json/obographs.js.map +1 -1
  130. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  131. package/dist/src/formats/neo4j/exporter.js +73 -43
  132. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  133. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  134. package/dist/src/formats/neo4j/header.js +37 -16
  135. package/dist/src/formats/neo4j/header.js.map +1 -1
  136. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  137. package/dist/src/formats/neo4j/importer.js +21 -11
  138. package/dist/src/formats/neo4j/importer.js.map +1 -1
  139. package/dist/src/formats/obo/exporter.d.ts.map +1 -1
  140. package/dist/src/formats/obo/exporter.js +18 -7
  141. package/dist/src/formats/obo/exporter.js.map +1 -1
  142. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  143. package/dist/src/formats/pajek/exporter.js +142 -73
  144. package/dist/src/formats/pajek/exporter.js.map +1 -1
  145. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  146. package/dist/src/formats/pajek/importer.js +26 -15
  147. package/dist/src/formats/pajek/importer.js.map +1 -1
  148. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  149. package/dist/src/formats/pajek/syntax.js +85 -58
  150. package/dist/src/formats/pajek/syntax.js.map +1 -1
  151. package/dist/src/formats/xgmml/emit.d.ts +7 -0
  152. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  153. package/dist/src/formats/xgmml/emit.js +79 -40
  154. package/dist/src/formats/xgmml/emit.js.map +1 -1
  155. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  156. package/dist/src/formats/xgmml/exporter.js +38 -12
  157. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  158. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  159. package/dist/src/formats/xgmml/importer.js +38 -18
  160. package/dist/src/formats/xgmml/importer.js.map +1 -1
  161. package/dist/src/sniff.js +109 -57
  162. package/dist/src/sniff.js.map +1 -1
  163. package/dist/xgmml.js +2 -2
  164. package/package.json +2 -2
  165. package/src/children.ts +54 -27
  166. package/src/common/attributes.ts +118 -49
  167. package/src/common/input.ts +439 -197
  168. package/src/common/lists.ts +29 -15
  169. package/src/common/text.ts +24 -15
  170. package/src/common/zip.ts +47 -18
  171. package/src/formats/csv/exporter.ts +377 -175
  172. package/src/formats/csv/importer.ts +155 -84
  173. package/src/formats/csv/records.ts +90 -35
  174. package/src/formats/cys/tables.ts +11 -10
  175. package/src/formats/dot/tokenizer.ts +118 -68
  176. package/src/formats/gexf/exporter.ts +67 -36
  177. package/src/formats/gexf/importer.ts +173 -63
  178. package/src/formats/gml/exporter.ts +78 -82
  179. package/src/formats/gml/importer.ts +119 -68
  180. package/src/formats/gml/syntax.ts +18 -8
  181. package/src/formats/graphml/exporter.ts +44 -24
  182. package/src/formats/graphml/importer.ts +18 -11
  183. package/src/formats/graphml/tree.ts +22 -12
  184. package/src/formats/graphml/yfiles.ts +23 -10
  185. package/src/formats/json/dialect.ts +61 -22
  186. package/src/formats/json/exporter.ts +40 -21
  187. package/src/formats/json/importer.ts +701 -385
  188. package/src/formats/json/obographs.ts +35 -15
  189. package/src/formats/neo4j/exporter.ts +79 -41
  190. package/src/formats/neo4j/header.ts +50 -16
  191. package/src/formats/neo4j/importer.ts +21 -10
  192. package/src/formats/obo/exporter.ts +34 -17
  193. package/src/formats/pajek/exporter.ts +180 -117
  194. package/src/formats/pajek/importer.ts +27 -15
  195. package/src/formats/pajek/syntax.ts +99 -58
  196. package/src/formats/xgmml/emit.ts +88 -40
  197. package/src/formats/xgmml/exporter.ts +40 -12
  198. package/src/formats/xgmml/importer.ts +45 -23
  199. package/src/sniff.ts +115 -59
  200. package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
  201. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  202. package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
  203. package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
  204. package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
  205. package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
  206. package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
  207. package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
  208. package/dist/chunks/importer-DcKB97kW.js.map +0 -1
  209. package/dist/chunks/records-r3SFHz11.js.map +0 -1
  210. package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
@@ -385,6 +385,13 @@ interface AdjacencyPlan {
385
385
  }
386
386
 
387
387
  /** Everything one import call shares between its tables. */
388
+ /** A row read before the plan exists, kept to be pushed once it does. */
389
+ interface PendingRow {
390
+ readonly row: string[];
391
+ readonly quoted: readonly boolean[];
392
+ readonly line: number;
393
+ }
394
+
388
395
  interface ImportState {
389
396
  readonly sink: GraphSink;
390
397
  readonly report: ImportReportBuilder;
@@ -455,6 +462,43 @@ function isPadded(text: string, quoted: boolean | undefined): boolean {
455
462
  */
456
463
  function resolveCsvOptions(options: (CsvImportOptions & CommonImportOptions) | undefined): ResolvedCsvOptions {
457
464
  const o: CsvImportOptions = options ?? {};
465
+ checkDialectOptions(o);
466
+ for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"] as const) {
467
+ checkColumnRef(name, o[name]);
468
+ }
469
+ if (o.typeColumn !== null) {
470
+ checkColumnRef("typeColumn", o.typeColumn);
471
+ }
472
+ if (o.table === "adjacency") {
473
+ checkAdjacencyOptions(o);
474
+ }
475
+ if (o.rowNumberIds !== undefined && typeof o.rowNumberIds !== "boolean") {
476
+ throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: expected a boolean", {
477
+ option: "rowNumberIds",
478
+ found: o.rowNumberIds,
479
+ });
480
+ }
481
+ return {
482
+ delimiter: o.delimiter ?? null,
483
+ decimalComma: o.decimal === ",",
484
+ header: o.header ?? "auto",
485
+ table: o.table ?? "auto",
486
+ sourceColumn: o.sourceColumn ?? null,
487
+ targetColumn: o.targetColumn ?? null,
488
+ typeColumn: o.typeColumn,
489
+ idColumn: o.idColumn ?? null,
490
+ labelColumn: o.labelColumn ?? null,
491
+ nodes: nodesInput(o.nodes),
492
+ rowNumberIds: o.rowNumberIds ?? false,
493
+ explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null,
494
+ };
495
+ }
496
+
497
+ /**
498
+ * Check the dialect and table options: delimiter, decimal, header and table.
499
+ * @param o - the caller's CSV options
500
+ */
501
+ function checkDialectOptions(o: CsvImportOptions): void {
458
502
  if (o.delimiter !== undefined && (typeof o.delimiter !== "string" || o.delimiter.length === 0)) {
459
503
  throw new GraphFormatError("E_UNSUPPORTED", "option delimiter: expected a non-empty string", {
460
504
  option: "delimiter",
@@ -489,48 +533,27 @@ function resolveCsvOptions(options: (CsvImportOptions & CommonImportOptions) | u
489
533
  found: o.table,
490
534
  });
491
535
  }
492
- for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"] as const) {
493
- checkColumnRef(name, o[name]);
494
- }
495
- if (o.typeColumn !== null) {
496
- checkColumnRef("typeColumn", o.typeColumn);
497
- }
498
- if (o.table === "adjacency") {
499
- for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"] as const) {
500
- if (o[name] !== undefined) {
501
- throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
502
- option: name,
503
- found: o[name],
504
- });
505
- }
506
- }
507
- if (o.rowNumberIds === true) {
508
- throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: an adjacency table names its nodes", {
509
- option: "rowNumberIds",
510
- found: o.rowNumberIds,
536
+ }
537
+
538
+ /**
539
+ * Check the options an adjacency table has no use for: column options and rowNumberIds.
540
+ * @param o - the caller's CSV options
541
+ */
542
+ function checkAdjacencyOptions(o: CsvImportOptions): void {
543
+ for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"] as const) {
544
+ if (o[name] !== undefined) {
545
+ throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
546
+ option: name,
547
+ found: o[name],
511
548
  });
512
549
  }
513
550
  }
514
- if (o.rowNumberIds !== undefined && typeof o.rowNumberIds !== "boolean") {
515
- throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: expected a boolean", {
551
+ if (o.rowNumberIds === true) {
552
+ throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: an adjacency table names its nodes", {
516
553
  option: "rowNumberIds",
517
554
  found: o.rowNumberIds,
518
555
  });
519
556
  }
520
- return {
521
- delimiter: o.delimiter ?? null,
522
- decimalComma: o.decimal === ",",
523
- header: o.header ?? "auto",
524
- table: o.table ?? "auto",
525
- sourceColumn: o.sourceColumn ?? null,
526
- targetColumn: o.targetColumn ?? null,
527
- typeColumn: o.typeColumn,
528
- idColumn: o.idColumn ?? null,
529
- labelColumn: o.labelColumn ?? null,
530
- nodes: nodesInput(o.nodes),
531
- rowNumberIds: o.rowNumberIds ?? false,
532
- explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null,
533
- };
534
557
  }
535
558
 
536
559
  /**
@@ -575,6 +598,22 @@ function isUnset(text: string, quoted: boolean | undefined): boolean {
575
598
  return quoted !== true && isBlank(text);
576
599
  }
577
600
 
601
+ /**
602
+ * The number of set neighbor cells of an adjacency row (every cell after the first).
603
+ * @param row - the cells
604
+ * @param quoted - whether each cell was quoted
605
+ * @returns the set neighbor cells
606
+ */
607
+ function setNeighborCells(row: readonly string[], quoted: readonly boolean[]): number {
608
+ let neighbors = 0;
609
+ for (let k = 1; k < row.length; k++) {
610
+ if (!isUnset(row[k], quoted[k])) {
611
+ neighbors++;
612
+ }
613
+ }
614
+ return neighbors;
615
+ }
616
+
578
617
  /** Rows between two checks of the cancellation signal on an in-memory input. */
579
618
  const ABORT_CHECK_INTERVAL = 64;
580
619
 
@@ -807,43 +846,12 @@ class TableReader {
807
846
  : first.value;
808
847
  const firstLine = this.reader.line;
809
848
  const firstQuoted = this.reader.quoted.slice(0, firstRow.length);
810
- const pending: { row: string[]; quoted: readonly boolean[]; line: number }[] = [];
811
- let header: boolean;
812
- // an adjacency table has no header unless the caller says so: its rows vary in width; a
813
- // table numbered by rowNumberIds has one, since a headerless table takes its ids from column 0
814
- let mode = this.state.csv.header;
815
- if (mode === "auto" && this.kind === "adjacency") {
816
- mode = false;
817
- } else if (mode === "auto" && this.rowNumbers) {
818
- mode = true;
819
- }
820
- if (mode === "auto") {
821
- const second = await iterator.next();
822
- const secondRow: string[] | null = second.done ? null : second.value;
823
- const secondLine = this.reader.line;
824
- const secondQuoted = this.reader.quoted.slice(0, secondRow?.length ?? 0);
825
- // a column option naming a cell of the first row makes that row the header
826
- header = looksLikeHeader(firstRow, secondRow) || this.namesExplicitColumn(firstRow);
827
- if (!header) {
828
- pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
829
- }
830
- if (secondRow !== null) {
831
- pending.push({ row: secondRow, quoted: secondQuoted, line: secondLine });
832
- }
833
- } else {
834
- header = mode;
835
- if (!header) {
836
- pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
837
- }
838
- }
839
- if (this.rowNumbers && !header) {
840
- report.warning(
841
- "unsupported",
842
- OPTION_IGNORED_CODE,
843
- "option rowNumberIds ignored: a headerless node table takes its ids from column 0",
844
- { line: firstLine, element: "rowNumberIds" },
845
- );
846
- }
849
+ const pending: PendingRow[] = [];
850
+ const header = await this.decideHeader(
851
+ iterator,
852
+ { row: firstRow, quoted: firstQuoted, line: firstLine },
853
+ pending,
854
+ );
847
855
  const names = header ? uniqueNames(headerNames(firstRow), report, firstLine) : positionalNames(firstRow.length);
848
856
  const last = firstRow.length - 1;
849
857
  const optionalLast = header && last > 0 && isUnset(firstRow[last], firstQuoted[last]);
@@ -873,6 +881,80 @@ class TableReader {
873
881
  for (const { row, quoted, line } of pending) {
874
882
  this.processRow(row, quoted, line);
875
883
  }
884
+ await this.readRemainingRows(iterator);
885
+ if (this.dataRows === 0 && header) {
886
+ report.warning("missing-value", NO_DATA_ROWS_CODE, "the table has a header and no data rows", {
887
+ line: firstLine,
888
+ });
889
+ }
890
+ this.reportReaderFindings();
891
+ }
892
+
893
+ /**
894
+ * Decide whether the first row is the header, reading the second row when the header option is
895
+ * "auto", and queue the rows read so far that are data.
896
+ * @param iterator - the record iterator, past the first row
897
+ * @param first - the first row
898
+ * @param pending - receives the data rows read so far, in order
899
+ * @returns whether the first row is the header
900
+ */
901
+ private async decideHeader(
902
+ iterator: AsyncGenerator<string[], void, undefined>,
903
+ first: PendingRow,
904
+ pending: PendingRow[],
905
+ ): Promise<boolean> {
906
+ const mode = this.headerMode();
907
+ let second: PendingRow | null = null;
908
+ let header: boolean;
909
+ if (mode === "auto") {
910
+ const next = await iterator.next();
911
+ second = next.done
912
+ ? null
913
+ : { row: next.value, quoted: this.reader.quoted.slice(0, next.value.length), line: this.reader.line };
914
+ // a column option naming a cell of the first row makes that row the header
915
+ header = looksLikeHeader(first.row, second?.row ?? null) || this.namesExplicitColumn(first.row);
916
+ } else {
917
+ header = mode;
918
+ }
919
+ if (!header) {
920
+ pending.push(first);
921
+ }
922
+ if (second !== null) {
923
+ pending.push(second);
924
+ }
925
+ if (this.rowNumbers && !header) {
926
+ this.state.report.warning(
927
+ "unsupported",
928
+ OPTION_IGNORED_CODE,
929
+ "option rowNumberIds ignored: a headerless node table takes its ids from column 0",
930
+ { line: first.line, element: "rowNumberIds" },
931
+ );
932
+ }
933
+ return header;
934
+ }
935
+
936
+ /**
937
+ * The header option as this table reads it: an adjacency table has no header unless the caller
938
+ * says so (its rows vary in width); a table numbered by rowNumberIds has one, since a headerless
939
+ * table takes its ids from column 0.
940
+ * @returns true, false or "auto"
941
+ */
942
+ private headerMode(): boolean | "auto" {
943
+ const mode = this.state.csv.header;
944
+ if (mode === "auto" && this.kind === "adjacency") {
945
+ return false;
946
+ }
947
+ if (mode === "auto" && this.rowNumbers) {
948
+ return true;
949
+ }
950
+ return mode;
951
+ }
952
+
953
+ /**
954
+ * Push every row after the header and the rows read to decide it, then finish the column writers.
955
+ * @param iterator - the record iterator
956
+ */
957
+ private async readRemainingRows(iterator: AsyncGenerator<string[], void, undefined>): Promise<void> {
876
958
  const { signal } = this.state.common;
877
959
  let sinceCheck = 0;
878
960
  for (;;) {
@@ -889,12 +971,6 @@ class TableReader {
889
971
  for (const writer of this.writers) {
890
972
  writer?.finish();
891
973
  }
892
- if (this.dataRows === 0 && header) {
893
- report.warning("missing-value", NO_DATA_ROWS_CODE, "the table has a header and no data rows", {
894
- line: firstLine,
895
- });
896
- }
897
- this.reportReaderFindings();
898
974
  }
899
975
 
900
976
  /**
@@ -1520,12 +1596,7 @@ class TableReader {
1520
1596
  const { where } = this;
1521
1597
  where.line = line;
1522
1598
  where.element = null;
1523
- let neighbors = 0;
1524
- for (let k = 1; k < row.length; k++) {
1525
- if (!isUnset(row[k], quoted[k])) {
1526
- neighbors++;
1527
- }
1528
- }
1599
+ const neighbors = setNeighborCells(row, quoted);
1529
1600
  if (isUnset(row[0], quoted[0])) {
1530
1601
  report.error("missing-value", MISSING_ID_CODE, `blank node cell`, { line });
1531
1602
  counts.skippedNodes++;
@@ -410,16 +410,7 @@ export function sniffDelimiter(
410
410
  if (rows.length === 0) {
411
411
  continue;
412
412
  }
413
- let total = 0;
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
@@ -577,14 +586,8 @@ export class RecordReader implements AsyncIterable<number> {
577
586
  }
578
587
  const pieces: string[] = [];
579
588
  let length = 0;
580
- let breaks = 0;
581
- let lastWasCr = false;
582
589
  let sniffed = false;
583
- // the leading comment lines are left out of the sniff, so they do not count toward its rows
584
- const commentCodes = (this.syntax.comments ?? []).map((c) => c.codePointAt(0));
585
- let leading = commentCodes.length > 0;
586
- let lineStart = true;
587
- let inComment = false;
590
+ const preview = new PreviewBreaks((this.syntax.comments ?? []).map((c) => c.codePointAt(0)));
588
591
  for await (const chunk of source) {
589
592
  if (sniffed) {
590
593
  yield chunk;
@@ -592,24 +595,8 @@ export class RecordReader implements AsyncIterable<number> {
592
595
  }
593
596
  pieces.push(chunk);
594
597
  length += chunk.length;
595
- for (let i = 0; i < chunk.length && breaks < PREVIEW_ROWS; i++) {
596
- const c = chunk.charCodeAt(i);
597
- if (c === LF || c === CR) {
598
- if (!(c === LF && lastWasCr) && !inComment) {
599
- breaks++;
600
- }
601
- inComment = false;
602
- lineStart = true;
603
- } else {
604
- if (lineStart && leading) {
605
- inComment = commentCodes.includes(c);
606
- leading = inComment;
607
- }
608
- lineStart = false;
609
- }
610
- lastWasCr = c === CR;
611
- }
612
- if (breaks < PREVIEW_ROWS && length < PREVIEW_CHARS) {
598
+ preview.scan(chunk);
599
+ if (preview.breaks < PREVIEW_ROWS && length < PREVIEW_CHARS) {
613
600
  continue;
614
601
  }
615
602
  const text = pieces.join("");
@@ -659,6 +646,74 @@ export class RecordReader implements AsyncIterable<number> {
659
646
  }
660
647
  }
661
648
 
649
+ /**
650
+ * Counts the line breaks of the text buffered for the delimiter sniff, up to PREVIEW_ROWS. The
651
+ * leading comment lines are left out of the sniff, so they do not count toward its rows.
652
+ */
653
+ class PreviewBreaks {
654
+ /** The line breaks counted so far (at most PREVIEW_ROWS). */
655
+ breaks = 0;
656
+
657
+ private readonly commentCodes: readonly (number | undefined)[];
658
+
659
+ private lastWasCr = false;
660
+
661
+ private leading: boolean;
662
+
663
+ private lineStart = true;
664
+
665
+ private inComment = false;
666
+
667
+ /**
668
+ * Start counting at the start of the input.
669
+ * @param commentCodes - the code points of the comment characters
670
+ */
671
+ constructor(commentCodes: readonly (number | undefined)[]) {
672
+ this.commentCodes = commentCodes;
673
+ this.leading = commentCodes.length > 0;
674
+ }
675
+
676
+ /**
677
+ * Count the line breaks of the next chunk, stopping at PREVIEW_ROWS.
678
+ * @param chunk - the chunk
679
+ */
680
+ scan(chunk: string): void {
681
+ for (let i = 0; i < chunk.length && this.breaks < PREVIEW_ROWS; i++) {
682
+ const c = chunk.charCodeAt(i);
683
+ if (c === LF || c === CR) {
684
+ this.lineBreak(c);
685
+ } else {
686
+ this.character(c);
687
+ }
688
+ this.lastWasCr = c === CR;
689
+ }
690
+ }
691
+
692
+ /**
693
+ * A line break: counted unless it is the LF of a CR LF or ends a leading comment line.
694
+ * @param c - LF or CR
695
+ */
696
+ private lineBreak(c: number): void {
697
+ if (!(c === LF && this.lastWasCr) && !this.inComment) {
698
+ this.breaks++;
699
+ }
700
+ this.inComment = false;
701
+ this.lineStart = true;
702
+ }
703
+
704
+ /**
705
+ * Any other character: the first of a line decides whether a leading comment line starts.
706
+ * @param c - the character code
707
+ */
708
+ private character(c: number): void {
709
+ if (this.lineStart && this.leading) {
710
+ this.inComment = this.commentCodes.includes(c);
711
+ this.leading = this.inComment;
712
+ }
713
+ this.lineStart = false;
714
+ }
715
+ }
716
+
662
717
  /**
663
718
  * The record state machine of RecordReader, resumable at every record end so the reader can
664
719
  * yield a record before the next one overwrites the shared cell arrays. Every state lives on the
@@ -217,24 +217,25 @@ export function cellAtt(
217
217
  namespace: string | null,
218
218
  hidden: boolean,
219
219
  ): AttRec | null {
220
+ const cell = { line, namespace, hidden };
220
221
  if (text.startsWith("=")) {
221
- return att(column.name, column.list ? "List" : column.type, null, text, line, namespace, hidden, true);
222
+ return att(column.name, column.list ? "List" : column.type, null, text, true, cell);
222
223
  }
223
224
  if (!column.list) {
224
225
  if (text.length === 0 && column.type !== "String") {
225
226
  return null;
226
227
  }
227
- return att(column.name, column.type, null, text, line, namespace, hidden, false);
228
+ return att(column.name, column.type, null, text, false, cell);
228
229
  }
229
230
  if (text.length === 0 && column.type !== "String") {
230
231
  return null;
231
232
  }
232
- const list = att(column.name, "List", column.type, null, line, namespace, hidden, false);
233
+ const list = att(column.name, "List", column.type, null, false, cell);
233
234
  for (const item of javaSplitLines(text)) {
234
235
  if (item.length === 0 && column.type !== "String") {
235
236
  continue;
236
237
  }
237
- list.children.push(att(column.name, column.type, null, item, line, namespace, hidden, false));
238
+ list.children.push(att(column.name, column.type, null, item, false, cell));
238
239
  }
239
240
  return list;
240
241
  }
@@ -262,10 +263,11 @@ function javaSplitLines(text: string): string[] {
262
263
  * @param cyType - the Cytoscape type
263
264
  * @param elementType - a list's item type, or null
264
265
  * @param value - the value, or null
265
- * @param line - the line
266
- * @param namespace - the table namespace, or null
267
- * @param hidden - a hidden column
268
266
  * @param equation - the value is a formula
267
+ * @param cell - where the cell sits
268
+ * @param cell.line - the line
269
+ * @param cell.namespace - the table namespace, or null
270
+ * @param cell.hidden - a hidden column
269
271
  * @returns the record
270
272
  */
271
273
  function att(
@@ -273,11 +275,10 @@ function att(
273
275
  cyType: string,
274
276
  elementType: string | null,
275
277
  value: string | null,
276
- line: number,
277
- namespace: string | null,
278
- hidden: boolean,
279
278
  equation: boolean,
279
+ cell: { readonly line: number; readonly namespace: string | null; readonly hidden: boolean },
280
280
  ): AttRec {
281
+ const { line, namespace, hidden } = cell;
281
282
  return {
282
283
  name,
283
284
  type: null,