@graphty/graph-io 0.3.26 → 0.3.28

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 (150) hide show
  1. package/dist/chunks/{choose-BbJI55dx.js → choose-DrXtu_96.js} +2 -2
  2. package/dist/chunks/{choose-BbJI55dx.js.map → choose-DrXtu_96.js.map} +1 -1
  3. package/dist/chunks/{constants-BCsFsMj4.js → constants-ikG85dT8.js} +2 -2
  4. package/dist/chunks/{constants-BCsFsMj4.js.map → constants-ikG85dT8.js.map} +1 -1
  5. package/dist/chunks/{escape-D0g6NNUy.js → escape-D1HPTUrP.js} +10 -9
  6. package/dist/chunks/escape-D1HPTUrP.js.map +1 -0
  7. package/dist/chunks/{export-BTI4yQgy.js → export-CHd4tQE2.js} +4 -4
  8. package/dist/chunks/export-CHd4tQE2.js.map +1 -0
  9. package/dist/chunks/{exporter-Dxh231ZF.js → exporter-Cl021ZIE.js} +72 -48
  10. package/dist/chunks/exporter-Cl021ZIE.js.map +1 -0
  11. package/dist/chunks/{exporter-Cra2GYcN.js → exporter-D23CKNqF.js} +5 -5
  12. package/dist/chunks/exporter-D23CKNqF.js.map +1 -0
  13. package/dist/chunks/{exporter-tne475KK.js → exporter-DfWTgrti.js} +5 -5
  14. package/dist/chunks/exporter-DfWTgrti.js.map +1 -0
  15. package/dist/chunks/{exporter-BMqk5oxW.js → exporter-DhtW6FxK.js} +3 -3
  16. package/dist/chunks/{exporter-BMqk5oxW.js.map → exporter-DhtW6FxK.js.map} +1 -1
  17. package/dist/chunks/{importer-Bcl1d4xH.js → importer-B2W_58pz.js} +5 -5
  18. package/dist/chunks/importer-B2W_58pz.js.map +1 -0
  19. package/dist/chunks/{importer-90YWhJoF.js → importer-BSALvkui.js} +9 -9
  20. package/dist/chunks/importer-BSALvkui.js.map +1 -0
  21. package/dist/chunks/{importer-DfVu6j59.js → importer-BVRE--Un.js} +4 -4
  22. package/dist/chunks/importer-BVRE--Un.js.map +1 -0
  23. package/dist/chunks/{importer-BfIsp1f9.js → importer-BZB7iP9r.js} +35 -9
  24. package/dist/chunks/importer-BZB7iP9r.js.map +1 -0
  25. package/dist/chunks/{importer-DCjOvO7U.js → importer-CIAzt-wo.js} +6 -6
  26. package/dist/chunks/importer-CIAzt-wo.js.map +1 -0
  27. package/dist/chunks/{importer-Dku_VAIY.js → importer-DpEW5zeG.js} +4 -4
  28. package/dist/chunks/{importer-Dku_VAIY.js.map → importer-DpEW5zeG.js.map} +1 -1
  29. package/dist/chunks/{importer-C_TEA1zJ.js → importer-z_FL4VMb.js} +9 -9
  30. package/dist/chunks/importer-z_FL4VMb.js.map +1 -0
  31. package/dist/chunks/{json-elements-3tnyt5uZ.js → json-elements-BAmNBZok.js} +4 -4
  32. package/dist/chunks/json-elements-BAmNBZok.js.map +1 -0
  33. package/dist/chunks/{ontology-export-B9Tcf37D.js → ontology-export-Bab4eUA8.js} +2 -2
  34. package/dist/chunks/{ontology-export-B9Tcf37D.js.map → ontology-export-Bab4eUA8.js.map} +1 -1
  35. package/dist/chunks/{records-CQXT__mY.js → records-CdlvL4RO.js} +2 -2
  36. package/dist/chunks/{records-CQXT__mY.js.map → records-CdlvL4RO.js.map} +1 -1
  37. package/dist/chunks/{text-DmcUs5_Q.js → text-DzXjAiTl.js} +2 -2
  38. package/dist/chunks/{text-DmcUs5_Q.js.map → text-DzXjAiTl.js.map} +1 -1
  39. package/dist/csv.js +16 -6
  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 +20 -11
  47. package/dist/gml.js.map +1 -1
  48. package/dist/graph-io.js +26 -26
  49. package/dist/graph-io.js.map +1 -1
  50. package/dist/graphml.js +2 -2
  51. package/dist/json.js +1 -1
  52. package/dist/neo4j.js +4 -4
  53. package/dist/neo4j.js.map +1 -1
  54. package/dist/obo.js +1 -1
  55. package/dist/pajek.js +1 -1
  56. package/dist/src/common/declared-types.d.ts.map +1 -1
  57. package/dist/src/common/declared-types.js +2 -2
  58. package/dist/src/common/declared-types.js.map +1 -1
  59. package/dist/src/common/escape.d.ts.map +1 -1
  60. package/dist/src/common/escape.js +6 -5
  61. package/dist/src/common/escape.js.map +1 -1
  62. package/dist/src/common/input.js.map +1 -1
  63. package/dist/src/common/json-elements.js +1 -1
  64. package/dist/src/common/json-elements.js.map +1 -1
  65. package/dist/src/common/lists.js +1 -1
  66. package/dist/src/common/lists.js.map +1 -1
  67. package/dist/src/common/xml.js +2 -2
  68. package/dist/src/common/xml.js.map +1 -1
  69. package/dist/src/common/zip.js.map +1 -1
  70. package/dist/src/formats/csv/exporter.js +1 -1
  71. package/dist/src/formats/csv/exporter.js.map +1 -1
  72. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  73. package/dist/src/formats/csv/importer.js +20 -2
  74. package/dist/src/formats/csv/importer.js.map +1 -1
  75. package/dist/src/formats/cx/importer.js +2 -2
  76. package/dist/src/formats/cx/importer.js.map +1 -1
  77. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  78. package/dist/src/formats/cx2/exporter.js +84 -33
  79. package/dist/src/formats/cx2/exporter.js.map +1 -1
  80. package/dist/src/formats/cx2/importer.js +1 -1
  81. package/dist/src/formats/cx2/importer.js.map +1 -1
  82. package/dist/src/formats/cys/session.js +3 -3
  83. package/dist/src/formats/cys/session.js.map +1 -1
  84. package/dist/src/formats/dot/exporter.js +30 -4
  85. package/dist/src/formats/dot/exporter.js.map +1 -1
  86. package/dist/src/formats/dot/importer.js.map +1 -1
  87. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/gml/exporter.js +23 -6
  89. package/dist/src/formats/gml/exporter.js.map +1 -1
  90. package/dist/src/formats/gml/syntax.js +2 -2
  91. package/dist/src/formats/gml/syntax.js.map +1 -1
  92. package/dist/src/formats/json/dialect.js.map +1 -1
  93. package/dist/src/formats/neo4j/exporter.js +1 -1
  94. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  95. package/dist/src/formats/pajek/exporter.js +1 -1
  96. package/dist/src/formats/pajek/exporter.js.map +1 -1
  97. package/dist/src/formats/pajek/importer.js.map +1 -1
  98. package/dist/src/formats/pajek/syntax.js +3 -3
  99. package/dist/src/formats/pajek/syntax.js.map +1 -1
  100. package/dist/src/formats/xgmml/exporter.js +1 -1
  101. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  102. package/dist/src/formats/xgmml/values.js +1 -1
  103. package/dist/src/formats/xgmml/values.js.map +1 -1
  104. package/dist/src/registry.d.ts.map +1 -1
  105. package/dist/src/registry.js.map +1 -1
  106. package/dist/src/sniff.d.ts.map +1 -1
  107. package/dist/src/sniff.js.map +1 -1
  108. package/dist/src/types.d.ts.map +1 -1
  109. package/dist/src/types.js.map +1 -1
  110. package/dist/xgmml.js +2 -2
  111. package/package.json +2 -2
  112. package/src/common/declared-types.ts +3 -12
  113. package/src/common/escape.ts +6 -5
  114. package/src/common/input.ts +2 -2
  115. package/src/common/json-elements.ts +1 -1
  116. package/src/common/lists.ts +1 -1
  117. package/src/common/xml.ts +2 -2
  118. package/src/common/zip.ts +1 -1
  119. package/src/formats/csv/exporter.ts +1 -1
  120. package/src/formats/csv/importer.ts +21 -2
  121. package/src/formats/cx/importer.ts +2 -2
  122. package/src/formats/cx2/exporter.ts +124 -50
  123. package/src/formats/cx2/importer.ts +1 -1
  124. package/src/formats/cys/session.ts +3 -3
  125. package/src/formats/dot/exporter.ts +37 -4
  126. package/src/formats/dot/importer.ts +1 -1
  127. package/src/formats/gml/exporter.ts +24 -6
  128. package/src/formats/gml/syntax.ts +2 -2
  129. package/src/formats/json/dialect.ts +1 -1
  130. package/src/formats/neo4j/exporter.ts +2 -2
  131. package/src/formats/pajek/exporter.ts +1 -1
  132. package/src/formats/pajek/importer.ts +1 -1
  133. package/src/formats/pajek/syntax.ts +3 -3
  134. package/src/formats/xgmml/exporter.ts +1 -1
  135. package/src/formats/xgmml/values.ts +1 -1
  136. package/src/registry.ts +9 -10
  137. package/src/sniff.ts +2 -21
  138. package/src/types.ts +1 -7
  139. package/dist/chunks/escape-D0g6NNUy.js.map +0 -1
  140. package/dist/chunks/export-BTI4yQgy.js.map +0 -1
  141. package/dist/chunks/exporter-Cra2GYcN.js.map +0 -1
  142. package/dist/chunks/exporter-Dxh231ZF.js.map +0 -1
  143. package/dist/chunks/exporter-tne475KK.js.map +0 -1
  144. package/dist/chunks/importer-90YWhJoF.js.map +0 -1
  145. package/dist/chunks/importer-Bcl1d4xH.js.map +0 -1
  146. package/dist/chunks/importer-BfIsp1f9.js.map +0 -1
  147. package/dist/chunks/importer-C_TEA1zJ.js.map +0 -1
  148. package/dist/chunks/importer-DCjOvO7U.js.map +0 -1
  149. package/dist/chunks/importer-DfVu6j59.js.map +0 -1
  150. package/dist/chunks/json-elements-3tnyt5uZ.js.map +0 -1
@@ -229,7 +229,7 @@ function nonFiniteCount(column: Column): number {
229
229
  if ((value as readonly number[]).some((v) => !Number.isFinite(v))) {
230
230
  count++;
231
231
  }
232
- } else if (!Number.isFinite(value as number)) {
232
+ } else if (!Number.isFinite(value)) {
233
233
  count++;
234
234
  }
235
235
  }
@@ -270,18 +270,26 @@ function planAttributes(
270
270
  return plans;
271
271
  }
272
272
 
273
+ /** A note-recording callback. */
274
+ type Note = (code: string, message: string, column?: string | null, count?: number | null) => void;
275
+
273
276
  /**
274
- * Plan an export: the notes, the fatal condition and what is written.
277
+ * The generic notes of checkCapabilities(), adapted to CX2: bypass columns hold any JSON value,
278
+ * ids are counted by planNodeIds(), nested values are written as JSON text, and only the node
279
+ * label is written into the "n" slot (an edge label keeps its own name but loses its role).
275
280
  * @param snapshot - the snapshot
276
281
  * @param common - the resolved common options
277
- * @returns the plan
282
+ * @param notes - the notes so far, appended to
283
+ * @param note - the recorder
284
+ * @returns the fatal condition, or null
278
285
  */
279
- function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
280
- const notes: LossNote[] = [];
286
+ function capabilityNotes(
287
+ snapshot: GraphSnapshot,
288
+ common: ResolvedExportOptions,
289
+ notes: LossNote[],
290
+ note: Note,
291
+ ): GraphFormatError | null {
281
292
  let fatal: GraphFormatError | null = null;
282
- const note = (code: string, message: string, column: string | null = null, count: number | null = null): void => {
283
- notes.push(Object.freeze({ code, message, column, count }));
284
- };
285
293
  // bypass columns are written as bypasses, which hold any JSON value
286
294
  const bypassNames = new Set(
287
295
  [...snapshot.nodes, ...snapshot.edges].filter((c) => isBypass(c) || isZ(c)).map((c) => c.meta.name),
@@ -290,11 +298,7 @@ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
290
298
  roles: SLOT_ROLES,
291
299
  roleNames: ROLE_NAMES,
292
300
  })) {
293
- if (gen.column !== null && bypassNames.has(gen.column) && (gen.code === LOSS.JSON || gen.code === LOSS.DTYPE)) {
294
- continue;
295
- }
296
- if (gen.code === LOSS.ID_CHARSET || gen.code === LOSS.ID_MANGLED) {
297
- // counted below: CX2 also keeps integer ids beyond 2^53, which the generic rule refuses
301
+ if (!keepGenericNote(gen, bypassNames)) {
298
302
  continue;
299
303
  }
300
304
  if (gen.code === LOSS.JSON) {
@@ -311,27 +315,83 @@ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
311
315
  }
312
316
  notes.push(gen);
313
317
  }
318
+ const edgeLabel = snapshot.edges.byRole("label");
319
+ if (edgeLabel !== null) {
320
+ note(
321
+ LOSS.ROLE,
322
+ `edge column "${edgeLabel.meta.name}" (label) is written as a plain attribute; CX2 edges have no label slot and the role is lost`,
323
+ edgeLabel.meta.name,
324
+ edgeLabel.length - edgeLabel.nullCount,
325
+ );
326
+ }
327
+ return fatal;
328
+ }
314
329
 
315
- const folding = pairFolding(snapshot);
316
- directionNotes(snapshot, folding, common, CX2_LOSS.UNDIRECTED_AS_DIRECTED, note);
330
+ /**
331
+ * Whether a generic note applies to CX2 as it stands.
332
+ * @param gen - the generic note
333
+ * @param bypassNames - the names of the columns written as bypasses
334
+ * @returns false for a note CX2 drops or replaces
335
+ */
336
+ function keepGenericNote(gen: LossNote, bypassNames: ReadonlySet<string>): boolean {
337
+ if (gen.column !== null && bypassNames.has(gen.column) && (gen.code === LOSS.JSON || gen.code === LOSS.DTYPE)) {
338
+ return false;
339
+ }
340
+ if (gen.code === LOSS.COLUMN_NAME_CHANGED && gen.message.startsWith("edge column")) {
341
+ // only the node label is written into the "n" slot; an edge label keeps its own name
342
+ return false;
343
+ }
344
+ // ids are counted by planNodeIds(): CX2 also keeps integer ids beyond 2^53, which the generic rule refuses
345
+ return gen.code !== LOSS.ID_CHARSET && gen.code !== LOSS.ID_MANGLED;
346
+ }
317
347
 
318
- const planned = planNodeIds(snapshot, common, "CX2", ORIGINAL_ID_ATTRIBUTE, note);
319
- const ids = planned instanceof GraphFormatError ? null : planned;
320
- if (planned instanceof GraphFormatError) {
321
- fatal ??= planned;
348
+ /**
349
+ * Note the NaN and infinite values written as null: attribute cells, explicit weights, positions.
350
+ * @param snapshot - the snapshot
351
+ * @param attrs - every planned attribute
352
+ * @param weights - the explicit weights
353
+ * @param position - the position column, or null
354
+ * @param note - the recorder
355
+ */
356
+ function nonFiniteNote(
357
+ snapshot: GraphSnapshot,
358
+ attrs: readonly AttributePlan[],
359
+ weights: ExplicitWeights,
360
+ position: Column | null,
361
+ note: Note,
362
+ ): void {
363
+ let nonfinite = 0;
364
+ for (const p of attrs) {
365
+ nonfinite += nonFiniteCount(p.column);
366
+ }
367
+ for (let e = 0; e < snapshot.edgeCount; e++) {
368
+ if (weights.isExplicit(e) && !Number.isFinite(weights.value(e))) {
369
+ nonfinite++;
370
+ }
322
371
  }
372
+ if (position !== null) {
373
+ nonfinite += nonFinitePositions(position);
374
+ }
375
+ if (nonfinite > 0) {
376
+ note(
377
+ CX2_LOSS.NONFINITE_AS_NULL,
378
+ `${nonfinite} NaN or infinite value${plural(nonfinite)} cannot be written in CX2; written as null, they read back unset`,
379
+ null,
380
+ nonfinite,
381
+ );
382
+ }
383
+ }
323
384
 
324
- const weights = explicitWeights(snapshot);
325
- const label = snapshot.nodes.byRole("label");
326
- const nodeAttrs = planAttributes(
327
- snapshot.nodes,
328
- (c) => (c.meta.role !== null && SKIPPED_ROLES.has(c.meta.role)) || isBypass(c) || isZ(c),
329
- (c) => (c === label && !snapshot.nodes.has("name") ? "name" : c.meta.name),
330
- ids !== null && ids.changed > 0 ? new Set([ORIGINAL_ID_ATTRIBUTE]) : new Set(),
331
- );
385
+ /**
386
+ * Plan the edge attributes, noting a plain `weight` column that clashes with the weight key.
387
+ * @param snapshot - the snapshot
388
+ * @param weights - the explicit weights
389
+ * @param note - the recorder
390
+ * @returns the edge attribute plans
391
+ */
392
+ function planEdgeAttributes(snapshot: GraphSnapshot, weights: ExplicitWeights, note: Note): AttributePlan[] {
332
393
  const plainWeight = snapshot.edges.get(WEIGHT_ATTRIBUTE);
333
- const weightColumnClash = plainWeight !== null && plainWeight.meta.role === null;
334
- if (weightColumnClash) {
394
+ if (plainWeight !== null && plainWeight.meta.role === null) {
335
395
  note(
336
396
  LOSS.WEIGHT_KEY_CLASH,
337
397
  weights.weighted
@@ -341,7 +401,7 @@ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
341
401
  null,
342
402
  );
343
403
  }
344
- const edgeAttrs = planAttributes(
404
+ return planAttributes(
345
405
  snapshot.edges,
346
406
  (c) =>
347
407
  (c.meta.role !== null && (SKIPPED_ROLES.has(c.meta.role) || c.meta.role === "id")) ||
@@ -350,33 +410,47 @@ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
350
410
  (c) => c.meta.name,
351
411
  weights.weighted ? new Set([WEIGHT_ATTRIBUTE]) : new Set(),
352
412
  );
413
+ }
414
+
415
+ /**
416
+ * Plan an export: the notes, the fatal condition and what is written.
417
+ * @param snapshot - the snapshot
418
+ * @param common - the resolved common options
419
+ * @returns the plan
420
+ */
421
+ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
422
+ const notes: LossNote[] = [];
423
+ const note: Note = (code, message, column = null, count = null) => {
424
+ notes.push(Object.freeze({ code, message, column, count }));
425
+ };
426
+ let fatal = capabilityNotes(snapshot, common, notes, note);
427
+
428
+ const folding = pairFolding(snapshot);
429
+ directionNotes(snapshot, folding, common, CX2_LOSS.UNDIRECTED_AS_DIRECTED, note);
430
+
431
+ const planned = planNodeIds(snapshot, common, "CX2", ORIGINAL_ID_ATTRIBUTE, note);
432
+ const ids = planned instanceof GraphFormatError ? null : planned;
433
+ if (planned instanceof GraphFormatError) {
434
+ fatal ??= planned;
435
+ }
436
+
437
+ const weights = explicitWeights(snapshot);
438
+ const label = snapshot.nodes.byRole("label");
439
+ const nodeAttrs = planAttributes(
440
+ snapshot.nodes,
441
+ (c) => (c.meta.role !== null && SKIPPED_ROLES.has(c.meta.role)) || isBypass(c) || isZ(c),
442
+ (c) => (c === label && !snapshot.nodes.has("name") ? "name" : c.meta.name),
443
+ ids !== null && ids.changed > 0 ? new Set([ORIGINAL_ID_ATTRIBUTE]) : new Set(),
444
+ );
445
+ const edgeAttrs = planEdgeAttributes(snapshot, weights, note);
353
446
  const graphAttrs = planAttributes(
354
447
  snapshot.graph,
355
448
  () => false,
356
449
  (c) => c.meta.name,
357
450
  ).map((p) => ({ ...p, key: p.name }));
358
451
 
359
- let nonfinite = 0;
360
- for (const p of [...nodeAttrs, ...edgeAttrs, ...graphAttrs]) {
361
- nonfinite += nonFiniteCount(p.column);
362
- }
363
- for (let e = 0; e < snapshot.edgeCount; e++) {
364
- if (weights.isExplicit(e) && !Number.isFinite(weights.value(e))) {
365
- nonfinite++;
366
- }
367
- }
368
452
  const position = snapshot.nodes.byRole("position");
369
- if (position !== null) {
370
- nonfinite += nonFinitePositions(position);
371
- }
372
- if (nonfinite > 0) {
373
- note(
374
- CX2_LOSS.NONFINITE_AS_NULL,
375
- `${nonfinite} NaN or infinite value${plural(nonfinite)} cannot be written in CX2; written as null, they read back unset`,
376
- null,
377
- nonfinite,
378
- );
379
- }
453
+ nonFiniteNote(snapshot, [...nodeAttrs, ...edgeAttrs, ...graphAttrs], weights, position, note);
380
454
 
381
455
  const edgeIds = planEdgeIds(snapshot, note);
382
456
  const z = [...snapshot.nodes].find(isZ) ?? null;
@@ -709,7 +709,7 @@ function readDescriptor(value: unknown, line: number, doc: Cx2Document, report:
709
709
  const text = typeof raw === "string" || typeof raw === "number" ? String(raw) : "";
710
710
  // the major version: "2.0", "2.1", "2.0.1", "2.1-beta", " 2.0" are all CX2
711
711
  const match = /^\s*(\d+)(?=$|[.\s-])/.exec(text);
712
- const major = match === null ? NaN : Number(match[1]);
712
+ const major = match === null ? Number.NaN : Number(match[1]);
713
713
  if (major === 1) {
714
714
  report.fail(
715
715
  CX2_ISSUE.VERSION,
@@ -113,7 +113,7 @@ export interface Session {
113
113
  * @category Plugin helpers
114
114
  */
115
115
  export function urlDecode(text: string): string {
116
- const plus = text.replace(/\+/g, " ");
116
+ const plus = text.replaceAll("+", " ");
117
117
  try {
118
118
  return decodeURIComponent(plus);
119
119
  } catch {
@@ -282,13 +282,13 @@ function layoutOf(zipEntries: readonly ZipEntry[], report: ImportReportBuilder):
282
282
  `the session has ${markers.length} version markers (${listed(markers)}); ${markers[0]} is used and the others are not read`,
283
283
  );
284
284
  }
285
- if (marker === undefined && cysession === undefined) {
285
+ const anchor = marker ?? cysession;
286
+ if (anchor === undefined) {
286
287
  report.fail(
287
288
  CYS_ISSUE.NOT_SESSION,
288
289
  "the zip holds neither a session version marker (<x.y.z>.version) nor cysession.xml: it is not a Cytoscape session",
289
290
  );
290
291
  }
291
- const anchor = marker ?? (cysession as string);
292
292
  const root = anchor.slice(0, anchor.lastIndexOf("/") + 1);
293
293
  let era: "2" | "3" = "2";
294
294
  let version = "2.0.0";
@@ -377,6 +377,11 @@ class ExportPlan {
377
377
 
378
378
  private readonly targetPort: Column | null;
379
379
 
380
+ /** The node and edge label role columns, written under `label` whatever their name. */
381
+ private readonly nodeLabel: Column | null;
382
+
383
+ private readonly edgeLabel: Column | null;
384
+
380
385
  private readonly children: ChildrenCsr;
381
386
 
382
387
  private readonly extraNotes: LossNote[] = [];
@@ -417,6 +422,8 @@ class ExportPlan {
417
422
  this.folding = pairFolding(snapshot);
418
423
  this.sourcePort = edges.byRole("sourcePort");
419
424
  this.targetPort = edges.byRole("targetPort");
425
+ this.nodeLabel = nodes.byRole("label");
426
+ this.edgeLabel = edges.byRole("label");
420
427
  this.nodeColumns = [...nodes].filter((c) => this.writesNodeColumn(c));
421
428
  this.edgeColumns = [...edges].filter((c) => this.writesEdgeColumn(c));
422
429
  this.graphColumns = [...graph].filter(
@@ -444,7 +451,7 @@ class ExportPlan {
444
451
  this.clash("node", name, "the position role is written as pos");
445
452
  return false;
446
453
  }
447
- return true;
454
+ return this.labelFree("node", column, this.nodeLabel);
448
455
  }
449
456
 
450
457
  /**
@@ -468,7 +475,23 @@ class ExportPlan {
468
475
  this.clash("edge", name, "the weight is written as weight");
469
476
  return false;
470
477
  }
471
- return true;
478
+ return this.labelFree("edge", column, this.edgeLabel);
479
+ }
480
+
481
+ /**
482
+ * Whether a column may be written: false (reported) for a plain `label` column when the label
483
+ * role column is named otherwise, since that one is written as `label`.
484
+ * @param domain - the table
485
+ * @param column - the column
486
+ * @param labelRole - the table's label role column, or null
487
+ * @returns true when written
488
+ */
489
+ private labelFree(domain: string, column: Column, labelRole: Column | null): boolean {
490
+ if (column.meta.name !== LABEL_ATTRIBUTE || labelRole === null || labelRole === column) {
491
+ return true;
492
+ }
493
+ this.clash(domain, column.meta.name, "the label role is written as label");
494
+ return false;
472
495
  }
473
496
 
474
497
  /**
@@ -703,7 +726,7 @@ class ExportPlan {
703
726
  }
704
727
  for (const columns of [this.nodeColumns, this.edgeColumns, this.graphColumns]) {
705
728
  for (const column of columns) {
706
- visit(column.meta.name, "attribute name");
729
+ visit(this.attributeName(column), "attribute name");
707
730
  if (column.dtype === "string" || column.dtype === "dict") {
708
731
  for (let r = 0; r < column.length; r++) {
709
732
  if (column.isSet(r)) {
@@ -844,6 +867,16 @@ class ExportPlan {
844
867
  return parts.join(",");
845
868
  }
846
869
 
870
+ /**
871
+ * The attribute a column is written as: `label` for a node or edge label role column (the
872
+ * importer gives that attribute the role back), the column name otherwise.
873
+ * @param column - the column
874
+ * @returns the attribute name
875
+ */
876
+ private attributeName(column: Column): string {
877
+ return column === this.nodeLabel || column === this.edgeLabel ? LABEL_ATTRIBUTE : column.meta.name;
878
+ }
879
+
847
880
  /**
848
881
  * The `name=value` attributes of one row over a column list (set cells only).
849
882
  * @param columns - the columns
@@ -858,7 +891,7 @@ class ExportPlan {
858
891
  }
859
892
  const text = cellText(column, row);
860
893
  if (text !== null) {
861
- out.push(`${quoteDotId(column.meta.name)}=${writeId(text)}`);
894
+ out.push(`${quoteDotId(this.attributeName(column))}=${writeId(text)}`);
862
895
  }
863
896
  }
864
897
  return out;
@@ -514,7 +514,7 @@ function countGraphs(lexer: DotTokenizer, first: DotToken): number {
514
514
  if (!isPunct(token, "{")) {
515
515
  throw new DotSyntaxError(`expected "{" after the graph header, found ${describeToken(token)}`, token.line);
516
516
  }
517
- for (let depth = 1; depth > 0; ) {
517
+ for (let depth = 1; depth > 0;) {
518
518
  token = lexer.next();
519
519
  if (token.kind === "eof") {
520
520
  throw new DotSyntaxError('missing "}" at the end of a graph', token.line);
@@ -305,6 +305,25 @@ function versionText(text: string): string {
305
305
  return NUMBER_TEXT.test(text) ? text : quoteGmlString(text);
306
306
  }
307
307
 
308
+ /**
309
+ * A node or edge table's label role column, which is written under "label" (the key the importer
310
+ * gives the role back to): the key is marked used so a plain column of that name cannot have it.
311
+ * @param columns - the written columns
312
+ * @param label - the table name
313
+ * @param used - the keys already taken, gaining "label" when there is a label role column
314
+ * @returns the label role column, or undefined
315
+ */
316
+ function labelRoleColumn(columns: readonly Column[], label: string, used: Set<string>): Column | undefined {
317
+ if (label !== "node" && label !== "edge") {
318
+ return undefined;
319
+ }
320
+ const column = columns.find((c) => c.meta.role === "label");
321
+ if (column !== undefined) {
322
+ used.add("label");
323
+ }
324
+ return column;
325
+ }
326
+
308
327
  /**
309
328
  * Select the written columns of a table and assign their keys, recording invalid and reserved
310
329
  * keys as notes (or rewriting them under mangle).
@@ -326,14 +345,13 @@ function selectColumns(
326
345
  ): WrittenColumn[] {
327
346
  const used = new Set<string>(reserved);
328
347
  const out: WrittenColumn[] = [];
329
- for (const column of table) {
330
- if (!isWritten(column) || !filter(column)) {
331
- continue;
332
- }
333
- const preferred = preferredKey(column.meta);
348
+ const columns = [...table].filter((c) => isWritten(c) && filter(c));
349
+ const labelRole = labelRoleColumn(columns, label, used);
350
+ for (const column of columns) {
351
+ const preferred = column === labelRole ? "label" : preferredKey(column.meta);
334
352
  let key = preferred;
335
353
  const valid = isGmlKey(preferred);
336
- const taken = used.has(preferred);
354
+ const taken = column !== labelRole && used.has(preferred);
337
355
  if (!valid || taken) {
338
356
  const code = valid ? RESERVED_KEY_CODE : INVALID_KEY_CODE;
339
357
  let why = "is not a GML key";
@@ -499,7 +499,7 @@ export function numberOfText(text: string): number {
499
499
  if (NON_FINITE_TEXT.test(text)) {
500
500
  const lower = text.toLowerCase();
501
501
  if (lower.endsWith("nan")) {
502
- return NaN;
502
+ return Number.NaN;
503
503
  }
504
504
  return lower.startsWith("-") ? -Infinity : Infinity;
505
505
  }
@@ -536,7 +536,7 @@ export function isGmlKey(text: string): boolean {
536
536
  * @category Plugin helpers
537
537
  */
538
538
  export function mangleGmlKey(text: string): string {
539
- const body = text.replace(/[^0-9A-Za-z_]/g, "_");
539
+ const body = text.replaceAll(/\W/g, "_");
540
540
  return /^[A-Za-z]/.test(body) ? body : `x${body}`;
541
541
  }
542
542
 
@@ -102,7 +102,7 @@ export interface JsonShapeMeta {
102
102
  */
103
103
  export function jsonShapeOf(snapshot: GraphSnapshot): JsonShapeMeta | null {
104
104
  const shape = snapshot.meta.extra[META_KEY];
105
- return isJsonObject(shape) ? (shape as JsonShapeMeta) : null;
105
+ return isJsonObject(shape) ? shape : null;
106
106
  }
107
107
 
108
108
  /**
@@ -1374,7 +1374,7 @@ function formatPoint(value: unknown): string {
1374
1374
  if (typeof item === "number") {
1375
1375
  parts.push(`${key}:${formatF64(item)}`);
1376
1376
  } else if (typeof item === "string") {
1377
- parts.push(`${key}:'${item.replace(/'/g, "")}'`);
1377
+ parts.push(`${key}:'${item.replaceAll("'", "")}'`);
1378
1378
  } else {
1379
1379
  return JSON.stringify(value) ?? "";
1380
1380
  }
@@ -1514,7 +1514,7 @@ export function exportNeo4jFiles(
1514
1514
  try {
1515
1515
  return Promise.resolve(plan(snapshot, options).files());
1516
1516
  } catch (err) {
1517
- return Promise.reject(err as Error);
1517
+ return Promise.reject(err);
1518
1518
  }
1519
1519
  }
1520
1520
 
@@ -528,7 +528,7 @@ function firstModeOf(snapshot: GraphSnapshot): number | null {
528
528
  if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= snapshot.nodeCount) {
529
529
  return value;
530
530
  }
531
- return NaN;
531
+ return Number.NaN;
532
532
  }
533
533
 
534
534
  /**
@@ -1607,7 +1607,7 @@ class PajekParser {
1607
1607
  private extras(tokens: readonly string[], start: number, domain: "node" | "edge", line: number): RowExtras {
1608
1608
  const extras: RowExtras = { keys: [], values: [], spells: null };
1609
1609
  const seen = new Set<string>();
1610
- for (let i = start; i < tokens.length; ) {
1610
+ for (let i = start; i < tokens.length;) {
1611
1611
  const token = tokens[i];
1612
1612
  if (token.startsWith("[")) {
1613
1613
  extras.spells = this.intervals(token, domain);
@@ -438,7 +438,7 @@ export function parseIntervals(token: string): [number, number][] {
438
438
  }
439
439
  const spells: [number, number][] = [];
440
440
  for (const part of body.split(",")) {
441
- const match = TIME_POINT.exec(part.replace(/\s+/g, ""));
441
+ const match = TIME_POINT.exec(part.replaceAll(/\s+/g, ""));
442
442
  if (match === null) {
443
443
  throw new Error(`malformed time interval ${token}: "${part}" is not a-b, a-* or a`);
444
444
  }
@@ -485,7 +485,7 @@ export function decodeCharacterReferences(text: string, onUnknown?: (reference:
485
485
  if (!text.includes("&#")) {
486
486
  return text;
487
487
  }
488
- return text.replace(CHARACTER_REFERENCE, (whole, hex: string | undefined, dec: string | undefined) => {
488
+ return text.replaceAll(CHARACTER_REFERENCE, (whole, hex: string | undefined, dec: string | undefined) => {
489
489
  const code = hex === undefined ? Number(dec) : Number.parseInt(hex, 16);
490
490
  if (code <= 0x10ffff) {
491
491
  return String.fromCodePoint(code);
@@ -506,7 +506,7 @@ export function encodeCharacterReferences(text: string): string {
506
506
  if (!text.includes("&#")) {
507
507
  return text;
508
508
  }
509
- return text.replace(CHARACTER_REFERENCE, (whole) => `&#38;${whole.slice(1)}`);
509
+ return text.replaceAll(CHARACTER_REFERENCE, (whole) => `&#38;${whole.slice(1)}`);
510
510
  }
511
511
 
512
512
  /**
@@ -772,7 +772,7 @@ function valueText(value: unknown, dtype: string | null, escapes: boolean): stri
772
772
  } else if (typeof value === "number") {
773
773
  text = dtype === "f32" ? formatF32(value) : formatF64(value);
774
774
  } else if (typeof value === "string") {
775
- text = escapes ? value.replace(/\n/g, "\\n").replace(/\t/g, "\\t") : value;
775
+ text = escapes ? value.replaceAll("\n", String.raw`\n`).replaceAll("\t", String.raw`\t`) : value;
776
776
  } else if (ArrayBuffer.isView(value)) {
777
777
  text = JSON.stringify(Array.from(value as unknown as ArrayLike<number>));
778
778
  } else {
@@ -187,5 +187,5 @@ export function parseScalar(text: string, kind: ScalarKind, unescape: boolean):
187
187
  * @returns the text with `\n` and `\t` decoded
188
188
  */
189
189
  function unescapeCytoscape(text: string): string {
190
- return text.includes("\\") ? text.replace(/\\t/g, "\t").replace(/\\n/g, "\n") : text;
190
+ return text.includes("\\") ? text.replaceAll(String.raw`\t`, "\t").replaceAll(String.raw`\n`, "\n") : text;
191
191
  }
package/src/registry.ts CHANGED
@@ -421,16 +421,15 @@ export class FormatRegistry {
421
421
  ): Promise<ImportGraphResult> {
422
422
  const opts = options as ImportGraphOptions;
423
423
  const picked = await this.choose(input, opts);
424
- const unknown = this.unknownOptions(opts).map(
425
- (name): ImportIssue =>
426
- Object.freeze({
427
- category: "unsupported",
428
- severity: "warning",
429
- code: UNKNOWN_OPTION_CODE,
430
- message: unknownOptionText(name),
431
- line: null,
432
- element: name,
433
- }),
424
+ const unknown = this.unknownOptions(opts).map((name): ImportIssue =>
425
+ Object.freeze({
426
+ category: "unsupported",
427
+ severity: "warning",
428
+ code: UNKNOWN_OPTION_CODE,
429
+ message: unknownOptionText(name),
430
+ line: null,
431
+ element: name,
432
+ }),
434
433
  );
435
434
  const chosen = unknown.length === 0 ? picked : { ...picked, warnings: [...picked.warnings, ...unknown] };
436
435
  const { importer } = chosen;
package/src/sniff.ts CHANGED
@@ -32,19 +32,7 @@ import { type GraphImporter } from "./types.js";
32
32
  * @category Formats and detection
33
33
  */
34
34
  export type GraphFormatName =
35
- | "gexf"
36
- | "graphml"
37
- | "gml"
38
- | "dot"
39
- | "pajek"
40
- | "csv"
41
- | "json"
42
- | "neo4j"
43
- | "xgmml"
44
- | "cx2"
45
- | "cx"
46
- | "obo"
47
- | "cys";
35
+ "gexf" | "graphml" | "gml" | "dot" | "pajek" | "csv" | "json" | "neo4j" | "xgmml" | "cx2" | "cx" | "obo" | "cys";
48
36
 
49
37
  /**
50
38
  * A format name: one of the built-in names (with editor autocomplete) or the name of a format you
@@ -345,14 +333,7 @@ export function sniffJsonDialectHead(head: Uint8Array | string): JsonImportDiale
345
333
 
346
334
  /** Where a key was seen while scanning a truncated head. */
347
335
  type KeyPath =
348
- | ""
349
- | "graph"
350
- | "options"
351
- | "nodes[0]"
352
- | "edges[0]"
353
- | "links[0]"
354
- | "graphs[0].nodes[]"
355
- | "graphs[0].edges[]";
336
+ "" | "graph" | "options" | "nodes[0]" | "edges[0]" | "links[0]" | "graphs[0].nodes[]" | "graphs[0].edges[]";
356
337
 
357
338
  /**
358
339
  * A partial document rebuilt from the keys a truncated head reveals: every key gets a placeholder
package/src/types.ts CHANGED
@@ -458,13 +458,7 @@ export interface GraphExporter<Opts = unknown> {
458
458
  * @category Reports and errors
459
459
  */
460
460
  export type IssueCategory =
461
- | "parse-error"
462
- | "missing-value"
463
- | "validation-error"
464
- | "unsupported"
465
- | "precision"
466
- | "coercion"
467
- | "merged";
461
+ "parse-error" | "missing-value" | "validation-error" | "unsupported" | "precision" | "coercion" | "merged";
468
462
 
469
463
  /**
470
464
  * One problem found while importing.