@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.
- package/dist/chunks/{choose-DrXtu_96.js → choose-DT5da6OC.js} +2 -2
- package/dist/chunks/{choose-DrXtu_96.js.map → choose-DT5da6OC.js.map} +1 -1
- package/dist/chunks/{constants-ikG85dT8.js → constants-DowRD9ai.js} +2 -2
- package/dist/chunks/{constants-ikG85dT8.js.map → constants-DowRD9ai.js.map} +1 -1
- package/dist/chunks/{escape-D1HPTUrP.js → escape-DQC5HWqF.js} +2 -2
- package/dist/chunks/{escape-D1HPTUrP.js.map → escape-DQC5HWqF.js.map} +1 -1
- package/dist/chunks/{export-CHd4tQE2.js → export-CVi12U7J.js} +81 -60
- package/dist/chunks/{export-CHd4tQE2.js.map → export-CVi12U7J.js.map} +1 -1
- package/dist/chunks/{exporter-D23CKNqF.js → exporter-C3Pa2PSd.js} +5 -5
- package/dist/chunks/{exporter-D23CKNqF.js.map → exporter-C3Pa2PSd.js.map} +1 -1
- package/dist/chunks/{exporter-DfWTgrti.js → exporter-CgsO2nSl.js} +4 -4
- package/dist/chunks/{exporter-DfWTgrti.js.map → exporter-CgsO2nSl.js.map} +1 -1
- package/dist/chunks/{exporter-Cl021ZIE.js → exporter-Cy_1da4c.js} +4 -4
- package/dist/chunks/{exporter-Cl021ZIE.js.map → exporter-Cy_1da4c.js.map} +1 -1
- package/dist/chunks/{exporter-DhtW6FxK.js → exporter-JubAlNWi.js} +34 -34
- package/dist/chunks/exporter-JubAlNWi.js.map +1 -0
- package/dist/chunks/{importer-CIAzt-wo.js → importer-BA_hZKo7.js} +4 -4
- package/dist/chunks/{importer-CIAzt-wo.js.map → importer-BA_hZKo7.js.map} +1 -1
- package/dist/chunks/{importer-BZB7iP9r.js → importer-CDXlxXlS.js} +5 -5
- package/dist/chunks/{importer-BZB7iP9r.js.map → importer-CDXlxXlS.js.map} +1 -1
- package/dist/chunks/{importer-BVRE--Un.js → importer-DLz2Vb4q.js} +3 -3
- package/dist/chunks/{importer-BVRE--Un.js.map → importer-DLz2Vb4q.js.map} +1 -1
- package/dist/chunks/{importer-B2W_58pz.js → importer-DVxzzibh.js} +4 -4
- package/dist/chunks/{importer-B2W_58pz.js.map → importer-DVxzzibh.js.map} +1 -1
- package/dist/chunks/{importer-BSALvkui.js → importer-Dej72hIF.js} +6 -6
- package/dist/chunks/{importer-BSALvkui.js.map → importer-Dej72hIF.js.map} +1 -1
- package/dist/chunks/{importer-z_FL4VMb.js → importer-lLH9gQF_.js} +5 -5
- package/dist/chunks/{importer-z_FL4VMb.js.map → importer-lLH9gQF_.js.map} +1 -1
- package/dist/chunks/{importer-DpEW5zeG.js → importer-q-R-Q98w.js} +4 -4
- package/dist/chunks/{importer-DpEW5zeG.js.map → importer-q-R-Q98w.js.map} +1 -1
- package/dist/chunks/{json-elements-BAmNBZok.js → json-elements-1IUzkRNy.js} +3 -3
- package/dist/chunks/{json-elements-BAmNBZok.js.map → json-elements-1IUzkRNy.js.map} +1 -1
- package/dist/chunks/{ontology-export-Bab4eUA8.js → ontology-export-b0GLNIj-.js} +2 -2
- package/dist/chunks/{ontology-export-Bab4eUA8.js.map → ontology-export-b0GLNIj-.js.map} +1 -1
- package/dist/chunks/{records-CdlvL4RO.js → records-CdyqwXeY.js} +2 -2
- package/dist/chunks/{records-CdlvL4RO.js.map → records-CdyqwXeY.js.map} +1 -1
- package/dist/chunks/{text-DzXjAiTl.js → text-CDgjbkLC.js} +2 -2
- package/dist/chunks/{text-DzXjAiTl.js.map → text-CDgjbkLC.js.map} +1 -1
- package/dist/csv.js +22 -15
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +2 -2
- package/dist/gml.js +3 -3
- package/dist/graph-io.js +26 -26
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +3 -3
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/export.d.ts +6 -0
- package/dist/src/common/export.d.ts.map +1 -1
- package/dist/src/common/export.js +62 -28
- package/dist/src/common/export.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +18 -7
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/obo/syntax.d.ts +4 -0
- package/dist/src/formats/obo/syntax.d.ts.map +1 -1
- package/dist/src/formats/obo/syntax.js +35 -31
- package/dist/src/formats/obo/syntax.js.map +1 -1
- package/dist/src/types.d.ts +6 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +1 -1
- package/src/common/export.ts +105 -62
- package/src/formats/csv/exporter.ts +19 -15
- package/src/formats/obo/syntax.ts +35 -31
- package/src/types.ts +6 -0
- package/dist/chunks/exporter-DhtW6FxK.js.map +0 -1
package/src/common/export.ts
CHANGED
|
@@ -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
|
-
|
|
358
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
75
|
-
const ch = line
|
|
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
|
|
117
|
+
if (line.charAt(i) === "\\") {
|
|
114
118
|
i++;
|
|
115
119
|
continue;
|
|
116
120
|
}
|
|
117
|
-
if (line
|
|
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
|
|
141
|
+
const ch = rest.charAt(i);
|
|
138
142
|
if (ch === "\\") {
|
|
139
143
|
i++;
|
|
140
144
|
continue;
|
|
141
145
|
}
|
|
142
|
-
const atStart = i === 0 || rest
|
|
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
|
|
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
|
|
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
|
|
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
|
|
248
|
-
const ch = value
|
|
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
|
|
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
|
|
319
|
+
while (i < n && (inner.charAt(i) === " " || inner.charAt(i) === "\t")) {
|
|
316
320
|
i++;
|
|
317
321
|
}
|
|
318
322
|
let value: string;
|
|
319
|
-
if (inner
|
|
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
|
|
329
|
-
end += inner
|
|
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
|
|
352
|
+
if (text.charAt(i) === "\\") {
|
|
349
353
|
i++;
|
|
350
|
-
} else if (text
|
|
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
|
|
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
|
|
394
|
-
while (after < n && value
|
|
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
|
|
416
|
-
end += value
|
|
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
|
|
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
|
|
475
|
+
if (value.charAt(i) === "\\") {
|
|
472
476
|
i++;
|
|
473
|
-
} else if (value
|
|
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
|
|
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
|
|
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
|
/**
|