@graphty/graph-io 0.3.28 → 0.3.29

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 (73) hide show
  1. package/dist/chunks/{choose-DrXtu_96.js → choose-DT5da6OC.js} +2 -2
  2. package/dist/chunks/{choose-DrXtu_96.js.map → choose-DT5da6OC.js.map} +1 -1
  3. package/dist/chunks/{constants-ikG85dT8.js → constants-DowRD9ai.js} +2 -2
  4. package/dist/chunks/{constants-ikG85dT8.js.map → constants-DowRD9ai.js.map} +1 -1
  5. package/dist/chunks/{escape-D1HPTUrP.js → escape-DQC5HWqF.js} +2 -2
  6. package/dist/chunks/{escape-D1HPTUrP.js.map → escape-DQC5HWqF.js.map} +1 -1
  7. package/dist/chunks/{export-CHd4tQE2.js → export-CVi12U7J.js} +81 -60
  8. package/dist/chunks/{export-CHd4tQE2.js.map → export-CVi12U7J.js.map} +1 -1
  9. package/dist/chunks/{exporter-D23CKNqF.js → exporter-C3Pa2PSd.js} +5 -5
  10. package/dist/chunks/{exporter-D23CKNqF.js.map → exporter-C3Pa2PSd.js.map} +1 -1
  11. package/dist/chunks/{exporter-DfWTgrti.js → exporter-CgsO2nSl.js} +4 -4
  12. package/dist/chunks/{exporter-DfWTgrti.js.map → exporter-CgsO2nSl.js.map} +1 -1
  13. package/dist/chunks/{exporter-Cl021ZIE.js → exporter-Cy_1da4c.js} +4 -4
  14. package/dist/chunks/{exporter-Cl021ZIE.js.map → exporter-Cy_1da4c.js.map} +1 -1
  15. package/dist/chunks/{exporter-DhtW6FxK.js → exporter-JubAlNWi.js} +34 -34
  16. package/dist/chunks/exporter-JubAlNWi.js.map +1 -0
  17. package/dist/chunks/{importer-CIAzt-wo.js → importer-BA_hZKo7.js} +4 -4
  18. package/dist/chunks/{importer-CIAzt-wo.js.map → importer-BA_hZKo7.js.map} +1 -1
  19. package/dist/chunks/{importer-BZB7iP9r.js → importer-CDXlxXlS.js} +5 -5
  20. package/dist/chunks/{importer-BZB7iP9r.js.map → importer-CDXlxXlS.js.map} +1 -1
  21. package/dist/chunks/{importer-BVRE--Un.js → importer-DLz2Vb4q.js} +3 -3
  22. package/dist/chunks/{importer-BVRE--Un.js.map → importer-DLz2Vb4q.js.map} +1 -1
  23. package/dist/chunks/{importer-B2W_58pz.js → importer-DVxzzibh.js} +4 -4
  24. package/dist/chunks/{importer-B2W_58pz.js.map → importer-DVxzzibh.js.map} +1 -1
  25. package/dist/chunks/{importer-BSALvkui.js → importer-Dej72hIF.js} +6 -6
  26. package/dist/chunks/{importer-BSALvkui.js.map → importer-Dej72hIF.js.map} +1 -1
  27. package/dist/chunks/{importer-z_FL4VMb.js → importer-lLH9gQF_.js} +5 -5
  28. package/dist/chunks/{importer-z_FL4VMb.js.map → importer-lLH9gQF_.js.map} +1 -1
  29. package/dist/chunks/{importer-DpEW5zeG.js → importer-q-R-Q98w.js} +4 -4
  30. package/dist/chunks/{importer-DpEW5zeG.js.map → importer-q-R-Q98w.js.map} +1 -1
  31. package/dist/chunks/{json-elements-BAmNBZok.js → json-elements-1IUzkRNy.js} +3 -3
  32. package/dist/chunks/{json-elements-BAmNBZok.js.map → json-elements-1IUzkRNy.js.map} +1 -1
  33. package/dist/chunks/{ontology-export-Bab4eUA8.js → ontology-export-b0GLNIj-.js} +2 -2
  34. package/dist/chunks/{ontology-export-Bab4eUA8.js.map → ontology-export-b0GLNIj-.js.map} +1 -1
  35. package/dist/chunks/{records-CdlvL4RO.js → records-CdyqwXeY.js} +2 -2
  36. package/dist/chunks/{records-CdlvL4RO.js.map → records-CdyqwXeY.js.map} +1 -1
  37. package/dist/chunks/{text-DzXjAiTl.js → text-CDgjbkLC.js} +2 -2
  38. package/dist/chunks/{text-DzXjAiTl.js.map → text-CDgjbkLC.js.map} +1 -1
  39. package/dist/csv.js +22 -15
  40. package/dist/csv.js.map +1 -1
  41. package/dist/cx.js +1 -1
  42. package/dist/cx2.js +2 -2
  43. package/dist/cys.js +1 -1
  44. package/dist/dot.js +1 -1
  45. package/dist/gexf.js +2 -2
  46. package/dist/gml.js +3 -3
  47. package/dist/graph-io.js +26 -26
  48. package/dist/graphml.js +2 -2
  49. package/dist/json.js +1 -1
  50. package/dist/neo4j.js +3 -3
  51. package/dist/obo.js +1 -1
  52. package/dist/pajek.js +1 -1
  53. package/dist/src/common/export.d.ts +6 -0
  54. package/dist/src/common/export.d.ts.map +1 -1
  55. package/dist/src/common/export.js +62 -28
  56. package/dist/src/common/export.js.map +1 -1
  57. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  58. package/dist/src/formats/csv/exporter.js +18 -7
  59. package/dist/src/formats/csv/exporter.js.map +1 -1
  60. package/dist/src/formats/obo/syntax.d.ts +4 -0
  61. package/dist/src/formats/obo/syntax.d.ts.map +1 -1
  62. package/dist/src/formats/obo/syntax.js +35 -31
  63. package/dist/src/formats/obo/syntax.js.map +1 -1
  64. package/dist/src/types.d.ts +6 -0
  65. package/dist/src/types.d.ts.map +1 -1
  66. package/dist/src/types.js.map +1 -1
  67. package/dist/xgmml.js +2 -2
  68. package/package.json +1 -1
  69. package/src/common/export.ts +105 -62
  70. package/src/formats/csv/exporter.ts +19 -15
  71. package/src/formats/obo/syntax.ts +35 -31
  72. package/src/types.ts +6 -0
  73. package/dist/chunks/exporter-DhtW6FxK.js.map +0 -1
@@ -318,6 +318,12 @@ export interface CheckExtras {
318
318
  * W_WEIGHTS_DROPPED note. Default true.
319
319
  */
320
320
  readonly weights?: boolean | undefined;
321
+ /**
322
+ * The tables the export writes, of "node", "edge" and "graph"; a gap in a table it does not
323
+ * write is not a loss of this export and gets no note. Default all three. A CSV node table is
324
+ * `["node"]`, so its notes are about node ids and node columns only.
325
+ */
326
+ readonly tables?: readonly ("node" | "edge" | "graph")[] | undefined;
321
327
  /**
322
328
  * How the format's importer turns id text back into ids ("canonical" for most text formats), so
323
329
  * an id that comes back with another type (the text "7" as the number 7) is reported as
@@ -353,60 +359,11 @@ export function checkCapabilities(
353
359
  const note = (code: string, message: string, column: string | null = null, count: number | null = null): void => {
354
360
  notes.push(Object.freeze({ code, message, column, count }));
355
361
  };
362
+ const tables = extras.tables ?? ["node", "edge", "graph"];
363
+ const edges = tables.includes("edge");
356
364
 
357
- // mixed direction (design section 3.6): an expanded snapshot carries the pair / directed roles
358
- const mixed = countMixedEdges(snapshot);
359
- if (mixed > 0 && !caps.mixedDirection) {
360
- if (options.onMixedDirection === "error") {
361
- note(
362
- LOSS.MIXED_DIRECTION_ERROR,
363
- `${mixed} undirected edge${plural(mixed)} in a directed graph, and the format holds one direction per file, so the save fails unless onMixedDirection is "directed" or "undirected"`,
364
- null,
365
- mixed,
366
- );
367
- } else {
368
- note(
369
- LOSS.MIXED_DIRECTION,
370
- `${mixed} undirected edge${plural(mixed)} in a directed graph ${agree(mixed, "is", "are")} written as ${options.onMixedDirection} (onMixedDirection), and the format holds one direction per file`,
371
- null,
372
- mixed,
373
- );
374
- }
375
- }
376
-
377
- if (snapshot.flags.multigraph && !caps.multiEdges) {
378
- const parallel = countParallelArcs(snapshot);
379
- note(
380
- LOSS.MULTI_EDGES,
381
- `${parallel} parallel edge${plural(parallel)}; the format has no parallel edges`,
382
- null,
383
- parallel,
384
- );
385
- }
386
- if (snapshot.selfLoopCount > 0 && !caps.selfLoops) {
387
- note(
388
- LOSS.SELF_LOOPS,
389
- `${snapshot.selfLoopCount} self-loop${plural(snapshot.selfLoopCount)}; the format has no self-loops`,
390
- null,
391
- snapshot.selfLoopCount,
392
- );
393
- }
394
-
395
- const edgeIds = snapshot.edges.byRole("id");
396
- if (caps.edgeIds === "required" && edgeIds === null && snapshot.edgeCount > 0) {
397
- note(
398
- LOSS.EDGE_IDS_GENERATED,
399
- "the format requires edge ids, and the graph has none; the edges are written with generated ids, which read back as a new edge id column",
400
- null,
401
- snapshot.edgeCount,
402
- );
403
- } else if (caps.edgeIds === "none" && edgeIds !== null) {
404
- note(
405
- LOSS.EDGE_IDS_DROPPED,
406
- `edge id column "${edgeIds.meta.name}" cannot be written`,
407
- edgeIds.meta.name,
408
- snapshot.edgeCount,
409
- );
365
+ if (edges) {
366
+ checkEdgeStructure(snapshot, caps, options, note);
410
367
  }
411
368
 
412
369
  const unrepresentable = countUnrepresentableIds(snapshot, caps.idCharset);
@@ -455,24 +412,40 @@ export function checkCapabilities(
455
412
  }
456
413
  }
457
414
 
458
- checkWeights(snapshot, extras, note);
459
- checkColumns(snapshot.nodes, "node", caps, extras, note);
460
- checkColumns(snapshot.edges, "edge", caps, extras, note);
461
- const graphColumns = snapshot.graph.names();
415
+ if (edges) {
416
+ checkWeights(snapshot, extras, note);
417
+ }
418
+ if (tables.includes("node")) {
419
+ checkColumns(snapshot.nodes, "node", caps, extras, note);
420
+ }
421
+ if (edges) {
422
+ checkColumns(snapshot.edges, "edge", caps, extras, note);
423
+ }
424
+ const graphColumns = tables.includes("graph") ? snapshot.graph.names() : [];
462
425
  if (graphColumns.length > 0) {
463
426
  if (caps.graphAttributes) {
464
427
  checkColumns(snapshot.graph, "graph", caps, extras, note);
465
428
  } else {
466
- note(
467
- LOSS.GRAPH_ATTRIBUTES,
468
- `${graphColumns.length} graph attribute${plural(graphColumns.length)} cannot be written`,
469
- null,
470
- graphColumns.length,
429
+ notes.push(
430
+ Object.freeze({
431
+ code: LOSS.GRAPH_ATTRIBUTES,
432
+ message: `${graphColumns.length} graph attribute${plural(graphColumns.length)} cannot be written`,
433
+ column: null,
434
+ count: graphColumns.length,
435
+ columns: Object.freeze([...graphColumns]),
436
+ }),
471
437
  );
472
438
  }
473
439
  }
474
440
 
475
441
  for (const [name, table] of snapshot.extensions) {
442
+ // "temporal:node:<column>" is the node table's, "temporal:edge:<column>" the edge table's;
443
+ // any other extension table is the graph's.
444
+ const [, kind] = name.split(":");
445
+ const owner = name.startsWith("temporal:") && (kind === "node" || kind === "edge") ? kind : "graph";
446
+ if (!tables.includes(owner)) {
447
+ continue;
448
+ }
476
449
  if (name.startsWith("temporal:")) {
477
450
  if (caps.temporal !== "dynamic-values") {
478
451
  note(
@@ -494,6 +467,76 @@ export function checkCapabilities(
494
467
  return notes;
495
468
  }
496
469
 
470
+ /**
471
+ * The edge-structure checks of checkCapabilities(): direction, parallel edges, self-loops and
472
+ * edge ids. Skipped for an export that writes no edge table.
473
+ * @param snapshot - the snapshot about to be exported
474
+ * @param caps - the exporter's capabilities
475
+ * @param options - the resolved common export options
476
+ * @param note - the recorder
477
+ */
478
+ function checkEdgeStructure(
479
+ snapshot: GraphSnapshot,
480
+ caps: ExportCapabilities,
481
+ options: ResolvedExportOptions,
482
+ note: NoteFn,
483
+ ): void {
484
+ // mixed direction (design section 3.6): an expanded snapshot carries the pair / directed roles
485
+ const mixed = countMixedEdges(snapshot);
486
+ if (mixed > 0 && !caps.mixedDirection) {
487
+ if (options.onMixedDirection === "error") {
488
+ note(
489
+ LOSS.MIXED_DIRECTION_ERROR,
490
+ `${mixed} undirected edge${plural(mixed)} in a directed graph, and the format holds one direction per file, so the save fails unless onMixedDirection is "directed" or "undirected"`,
491
+ null,
492
+ mixed,
493
+ );
494
+ } else {
495
+ note(
496
+ LOSS.MIXED_DIRECTION,
497
+ `${mixed} undirected edge${plural(mixed)} in a directed graph ${agree(mixed, "is", "are")} written as ${options.onMixedDirection} (onMixedDirection), and the format holds one direction per file`,
498
+ null,
499
+ mixed,
500
+ );
501
+ }
502
+ }
503
+
504
+ if (snapshot.flags.multigraph && !caps.multiEdges) {
505
+ const parallel = countParallelArcs(snapshot);
506
+ note(
507
+ LOSS.MULTI_EDGES,
508
+ `${parallel} parallel edge${plural(parallel)}; the format has no parallel edges`,
509
+ null,
510
+ parallel,
511
+ );
512
+ }
513
+ if (snapshot.selfLoopCount > 0 && !caps.selfLoops) {
514
+ note(
515
+ LOSS.SELF_LOOPS,
516
+ `${snapshot.selfLoopCount} self-loop${plural(snapshot.selfLoopCount)}; the format has no self-loops`,
517
+ null,
518
+ snapshot.selfLoopCount,
519
+ );
520
+ }
521
+
522
+ const edgeIds = snapshot.edges.byRole("id");
523
+ if (caps.edgeIds === "required" && edgeIds === null && snapshot.edgeCount > 0) {
524
+ note(
525
+ LOSS.EDGE_IDS_GENERATED,
526
+ "the format requires edge ids, and the graph has none; the edges are written with generated ids, which read back as a new edge id column",
527
+ null,
528
+ snapshot.edgeCount,
529
+ );
530
+ } else if (caps.edgeIds === "none" && edgeIds !== null) {
531
+ note(
532
+ LOSS.EDGE_IDS_DROPPED,
533
+ `edge id column "${edgeIds.meta.name}" cannot be written`,
534
+ edgeIds.meta.name,
535
+ snapshot.edgeCount,
536
+ );
537
+ }
538
+ }
539
+
497
540
  /**
498
541
  * The ids a charset cannot hold, as a message phrase.
499
542
  * @param charset - the format's id charset
@@ -385,13 +385,9 @@ function cellText(column: Column, row: number): string | null {
385
385
  * W_CSV_NODE_TABLE: the node columns an edge-table or adjacency export leaves for a second, node-table export.
386
386
  * @param names - the node columns, label first
387
387
  * @param table - the table being written ("edges" or "adjacency")
388
- * @param note - the recorder
388
+ * @param notes - receives the note
389
389
  */
390
- function nodeTableNote(
391
- names: readonly string[],
392
- table: string,
393
- note: (code: string, message: string, column?: string | null, count?: number | null) => void,
394
- ): void {
390
+ function nodeTableNote(names: readonly string[], table: string, notes: LossNote[]): void {
395
391
  const written = names.length;
396
392
  if (written > 0) {
397
393
  const shown =
@@ -399,11 +395,14 @@ function nodeTableNote(
399
395
  .slice(0, 3)
400
396
  .map((n) => JSON.stringify(n))
401
397
  .join(", ") + (written > 3 ? ", ..." : "");
402
- note(
403
- CSV_LOSS.NODE_TABLE,
404
- `the ${table === "adjacency" ? "adjacency" : "edge"} table has no room for node attributes: ${written} node column${plural(written)} (${shown}) ${agree(written, "is", "are")} written only by a second export with table: "nodes"`,
405
- null,
406
- written,
398
+ notes.push(
399
+ Object.freeze({
400
+ code: CSV_LOSS.NODE_TABLE,
401
+ message: `the ${table === "adjacency" ? "adjacency" : "edge"} table has no room for node attributes: ${written} node column${plural(written)} (${shown}) ${agree(written, "is", "are")} written only by a second export with table: "nodes"`,
402
+ column: null,
403
+ count: written,
404
+ columns: Object.freeze([...names]),
405
+ }),
407
406
  );
408
407
  }
409
408
  }
@@ -483,7 +482,8 @@ function planExport(
483
482
  edgeRows.push(e);
484
483
  }
485
484
  }
486
- if (csv.dialect.type === null || csv.table === "adjacency") {
485
+ // A node table holds no edge: the direction and mutual notes are the edge table's.
486
+ if (csv.table !== "nodes" && (csv.dialect.type === null || csv.table === "adjacency")) {
487
487
  const where = csv.table === "adjacency" ? "an adjacency table" : "the generic dialect";
488
488
  const mixed = countMixedEdges(snapshot);
489
489
  if (!snapshot.directed) {
@@ -509,7 +509,7 @@ function planExport(
509
509
  0,
510
510
  );
511
511
  }
512
- if (folding.mutualCount > 0) {
512
+ if (folding.mutualCount > 0 && csv.table !== "nodes") {
513
513
  note(
514
514
  CSV_LOSS.MUTUAL_EXPANDED,
515
515
  `${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
@@ -586,7 +586,7 @@ function planExport(
586
586
  nodeTableNote(
587
587
  [...(nodeLabel === null ? [] : [nodeLabel.meta.name]), ...nodeColumns.map((c) => c.column.meta.name)],
588
588
  csv.table,
589
- note,
589
+ notes,
590
590
  );
591
591
  }
592
592
 
@@ -1116,6 +1116,7 @@ function headerlessNotes(plan: Plan): LossNote[] {
1116
1116
  message: `without a header the file is read back by position (${csv.table === "nodes" ? "id" : "source, target, weight"}, then unnamed columns), so ${names.length === 1 ? "the column" : "the columns"} ${shown} ${agree(names.length, "reads", "read")} back unnamed or in another column's place; write the header, or for an edge list pass dialect: "generic" and write only source, target and weight`,
1117
1117
  column: null,
1118
1118
  count: names.length,
1119
+ columns: Object.freeze([...names]),
1119
1120
  }),
1120
1121
  ];
1121
1122
  }
@@ -1166,7 +1167,10 @@ function headerlessEdgeColumns(plan: Plan): string[] {
1166
1167
  function check(snapshot: GraphSnapshot, options?: CsvExportOptions & CommonExportOptions): readonly LossNote[] {
1167
1168
  const plan = planExport(snapshot, options, true);
1168
1169
  return Object.freeze([
1169
- ...checkCapabilities(snapshot, CSV_CAPABILITIES, plan.common, { roles: KEPT_ROLES }),
1170
+ ...checkCapabilities(snapshot, CSV_CAPABILITIES, plan.common, {
1171
+ roles: KEPT_ROLES,
1172
+ ...(plan.csv.table === "nodes" ? { tables: ["node"] as const } : {}),
1173
+ }),
1170
1174
  ...plan.notes,
1171
1175
  ...headerlessNotes(plan),
1172
1176
  ]);
@@ -6,6 +6,10 @@
6
6
  *
7
7
  * Every function works on the RAW text (escapes still in place) and unescapes only the pieces it
8
8
  * hands back, so an escaped quote, colon, comma, brace or `!` never ends a construct.
9
+ *
10
+ * The scanners read characters with charAt(), never `text[i]`: the importer's linear-time tests
11
+ * count the characters the import reads through the string methods, and an index read is
12
+ * invisible to them, so a quadratic scan written with it would pass unnoticed.
9
13
  */
10
14
 
11
15
  /** The OBO escapes with a meaning of their own; any other `\x` is `x` (the guides). */
@@ -29,14 +33,14 @@ export function unescapeObo(text: string): string {
29
33
  }
30
34
  let out = "";
31
35
  for (let i = 0; i < text.length; i++) {
32
- const ch = text[i];
36
+ const ch = text.charAt(i);
33
37
  if (ch !== "\\") {
34
38
  out += ch;
35
39
  continue;
36
40
  }
37
41
  i++;
38
42
  if (i < text.length) {
39
- const next = text[i];
43
+ const next = text.charAt(i);
40
44
  out += ESCAPES[next] ?? next;
41
45
  }
42
46
  }
@@ -51,7 +55,7 @@ export function unescapeObo(text: string): string {
51
55
  */
52
56
  function isEscaped(text: string, index: number): boolean {
53
57
  let run = 0;
54
- for (let i = index - 1; i >= 0 && text[i] === "\\"; i--) {
58
+ for (let i = index - 1; i >= 0 && text.charAt(i) === "\\"; i--) {
55
59
  run++;
56
60
  }
57
61
  return run % 2 === 1;
@@ -71,8 +75,8 @@ export function splitFormFeeds(line: string): string[] {
71
75
  const pieces: string[] = [];
72
76
  let quoted = false;
73
77
  let start = 0;
74
- for (let i = 0; i < line.length; i += line[i] === "\\" ? 2 : 1) {
75
- const ch = line[i];
78
+ for (let i = 0; i < line.length; i += line.charAt(i) === "\\" ? 2 : 1) {
79
+ const ch = line.charAt(i);
76
80
  if (ch === '"') {
77
81
  quoted = !quoted;
78
82
  } else if (ch === "\f" && !quoted) {
@@ -110,11 +114,11 @@ interface TagValue {
110
114
  */
111
115
  export function splitTagValue(line: string): TagValue | null {
112
116
  for (let i = 0; i < line.length; i++) {
113
- if (line[i] === "\\") {
117
+ if (line.charAt(i) === "\\") {
114
118
  i++;
115
119
  continue;
116
120
  }
117
- if (line[i] === ":") {
121
+ if (line.charAt(i) === ":") {
118
122
  return { tag: unescapeObo(line.slice(0, i).trim()), rest: line.slice(i + 1).trimStart() };
119
123
  }
120
124
  }
@@ -134,12 +138,12 @@ export function splitTagValue(line: string): TagValue | null {
134
138
  export function stripComment(rest: string): string {
135
139
  let quoted = false;
136
140
  for (let i = 0; i < rest.length; i++) {
137
- const ch = rest[i];
141
+ const ch = rest.charAt(i);
138
142
  if (ch === "\\") {
139
143
  i++;
140
144
  continue;
141
145
  }
142
- const atStart = i === 0 || rest[i - 1] === " " || rest[i - 1] === "\t";
146
+ const atStart = i === 0 || rest.charAt(i - 1) === " " || rest.charAt(i - 1) === "\t";
143
147
  if (ch === '"' && (quoted || atStart)) {
144
148
  quoted = !quoted;
145
149
  } else if (ch === "!" && !quoted && atStart) {
@@ -183,7 +187,7 @@ export function splitQualifiers(value: string): QualifiedValue {
183
187
  const parsed: Qualifiers[] = [];
184
188
  let end = value.length;
185
189
  let badBlock: string | null = null;
186
- while (end > 0 && value[end - 1] === "}" && !isEscaped(value, end - 1)) {
190
+ while (end > 0 && value.charAt(end - 1) === "}" && !isEscaped(value, end - 1)) {
187
191
  const open = blocks.get(end - 1) ?? -1;
188
192
  const block = open < 0 ? null : parseQualifiers(value.slice(open + 1, end - 1));
189
193
  if (block === null) {
@@ -192,7 +196,7 @@ export function splitQualifiers(value: string): QualifiedValue {
192
196
  }
193
197
  parsed.push(block);
194
198
  end = open;
195
- while (end > 0 && /\s/.test(value[end - 1])) {
199
+ while (end > 0 && /\s/.test(value.charAt(end - 1))) {
196
200
  end--;
197
201
  }
198
202
  }
@@ -217,7 +221,7 @@ function braceBlocks(value: string): Map<number, number> {
217
221
  let quoted = false;
218
222
  let open = -1;
219
223
  for (let i = 0; i < value.length; i++) {
220
- const ch = value[i];
224
+ const ch = value.charAt(i);
221
225
  if (ch === "\\") {
222
226
  i++;
223
227
  continue;
@@ -244,8 +248,8 @@ function braceBlocks(value: string): Map<number, number> {
244
248
  export function unclosedBlock(value: string): number {
245
249
  let quoted = false;
246
250
  let open = -1;
247
- for (let i = 0; i < value.length; i += value[i] === "\\" ? 2 : 1) {
248
- const ch = value[i];
251
+ for (let i = 0; i < value.length; i += value.charAt(i) === "\\" ? 2 : 1) {
252
+ const ch = value.charAt(i);
249
253
  if (ch === "\\") {
250
254
  continue;
251
255
  }
@@ -297,7 +301,7 @@ export function parseQualifiers(inner: string): Qualifiers | null {
297
301
  const n = inner.length;
298
302
  let any = false;
299
303
  while (i < n) {
300
- while (i < n && (inner[i] === " " || inner[i] === "\t" || inner[i] === ",")) {
304
+ while (i < n && (inner.charAt(i) === " " || inner.charAt(i) === "\t" || inner.charAt(i) === ",")) {
301
305
  i++;
302
306
  }
303
307
  if (i >= n) {
@@ -312,11 +316,11 @@ export function parseQualifiers(inner: string): Qualifiers | null {
312
316
  return null;
313
317
  }
314
318
  i = eq + 1;
315
- while (i < n && (inner[i] === " " || inner[i] === "\t")) {
319
+ while (i < n && (inner.charAt(i) === " " || inner.charAt(i) === "\t")) {
316
320
  i++;
317
321
  }
318
322
  let value: string;
319
- if (inner[i] === '"') {
323
+ if (inner.charAt(i) === '"') {
320
324
  const end = closingQuote(inner, i + 1);
321
325
  if (end < 0) {
322
326
  return null;
@@ -325,8 +329,8 @@ export function parseQualifiers(inner: string): Qualifiers | null {
325
329
  i = end + 1;
326
330
  } else {
327
331
  let end = i;
328
- while (end < n && inner[end] !== ",") {
329
- end += inner[end] === "\\" ? 2 : 1;
332
+ while (end < n && inner.charAt(end) !== ",") {
333
+ end += inner.charAt(end) === "\\" ? 2 : 1;
330
334
  }
331
335
  value = unescapeObo(inner.slice(i, end).trim());
332
336
  i = end;
@@ -345,9 +349,9 @@ export function parseQualifiers(inner: string): Qualifiers | null {
345
349
  */
346
350
  function closingQuote(text: string, from: number): number {
347
351
  for (let i = from; i < text.length; i++) {
348
- if (text[i] === "\\") {
352
+ if (text.charAt(i) === "\\") {
349
353
  i++;
350
- } else if (text[i] === '"') {
354
+ } else if (text.charAt(i) === '"') {
351
355
  return i;
352
356
  }
353
357
  }
@@ -379,7 +383,7 @@ export function tokenize(value: string): Token[] {
379
383
  const n = value.length;
380
384
  let i = 0;
381
385
  while (i < n) {
382
- const ch = value[i];
386
+ const ch = value.charAt(i);
383
387
  if (ch === " " || ch === "\t") {
384
388
  i++;
385
389
  continue;
@@ -390,8 +394,8 @@ export function tokenize(value: string): Token[] {
390
394
  // a 1.4 language tag glued to the closing quote ("chat"@fr) is kept with the text, as an
391
395
  // unquoted chat@fr is (research-obo.md 4.1: no language is split out)
392
396
  let after = stop + 1;
393
- if (end >= 0 && value[after] === "@") {
394
- while (after < n && value[after] !== " " && value[after] !== "\t") {
397
+ if (end >= 0 && value.charAt(after) === "@") {
398
+ while (after < n && value.charAt(after) !== " " && value.charAt(after) !== "\t") {
395
399
  after++;
396
400
  }
397
401
  }
@@ -412,8 +416,8 @@ export function tokenize(value: string): Token[] {
412
416
  continue;
413
417
  }
414
418
  let end = i;
415
- while (end < n && value[end] !== " " && value[end] !== "\t") {
416
- end += value[end] === "\\" ? 2 : 1;
419
+ while (end < n && value.charAt(end) !== " " && value.charAt(end) !== "\t") {
420
+ end += value.charAt(end) === "\\" ? 2 : 1;
417
421
  }
418
422
  tokens.push({ kind: "word", text: unescapeObo(value.slice(i, Math.min(end, n))), unterminated: false });
419
423
  i = end;
@@ -430,7 +434,7 @@ export function tokenize(value: string): Token[] {
430
434
  function closingBracket(text: string, from: number): number {
431
435
  let quoted = false;
432
436
  for (let i = from; i < text.length; i++) {
433
- const ch = text[i];
437
+ const ch = text.charAt(i);
434
438
  if (ch === "\\") {
435
439
  i++;
436
440
  } else if (ch === '"') {
@@ -468,9 +472,9 @@ export function parseXref(raw: string): Xref | null {
468
472
  const { value, qualifiers } = splitQualifiers(raw.trim());
469
473
  let quote = -1;
470
474
  for (let i = 0; i < value.length; i++) {
471
- if (value[i] === "\\") {
475
+ if (value.charAt(i) === "\\") {
472
476
  i++;
473
- } else if (value[i] === '"') {
477
+ } else if (value.charAt(i) === '"') {
474
478
  quote = i;
475
479
  break;
476
480
  }
@@ -508,7 +512,7 @@ export function parseXrefList(inner: string): Xref[] {
508
512
  }
509
513
  };
510
514
  for (let i = 0; i < inner.length; i++) {
511
- const ch = inner[i];
515
+ const ch = inner.charAt(i);
512
516
  if (ch === "\\") {
513
517
  i++;
514
518
  } else if (ch === '"') {
@@ -538,7 +542,7 @@ export function hasStrayBrace(value: string): boolean {
538
542
  let quoted = false;
539
543
  let listed = false;
540
544
  for (let i = 0; i < value.length; i++) {
541
- const ch = value[i];
545
+ const ch = value.charAt(i);
542
546
  if (ch === "\\") {
543
547
  i++;
544
548
  } else if (ch === '"') {
package/src/types.ts CHANGED
@@ -368,6 +368,12 @@ export interface LossNote {
368
368
  readonly column: string | null;
369
369
  /** How many nodes, edges or values are affected, or null when not counted. */
370
370
  readonly count: number | null;
371
+ /**
372
+ * Every column the note is about, when it is about several and `column` is null: the graph
373
+ * attributes a format has no place for, or the node columns a CSV edge table leaves for the node
374
+ * table. Absent on a note about one column or none.
375
+ */
376
+ readonly columns?: readonly string[];
371
377
  }
372
378
 
373
379
  /**