@graphty/graph-io 0.3.9 → 0.3.11

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 (125) hide show
  1. package/README.md +29 -5
  2. package/dist/chunks/{escape-BjmIaFFo.js → escape-D1f9-cwf.js} +4 -4
  3. package/dist/chunks/{escape-BjmIaFFo.js.map → escape-D1f9-cwf.js.map} +1 -1
  4. package/dist/chunks/{importer-BddBW3sf.js → importer-9PxhqH4v.js} +336 -92
  5. package/dist/chunks/importer-9PxhqH4v.js.map +1 -0
  6. package/dist/chunks/{importer-DfI8POXb.js → importer-B8lsjFWx.js} +6 -5
  7. package/dist/chunks/importer-B8lsjFWx.js.map +1 -0
  8. package/dist/chunks/{importer-BtHFICnf.js → importer-C3HcYWAn.js} +5 -3
  9. package/dist/chunks/importer-C3HcYWAn.js.map +1 -0
  10. package/dist/chunks/{importer-BOxwmef_.js → importer-C7mnGdr_.js} +125 -8
  11. package/dist/chunks/importer-C7mnGdr_.js.map +1 -0
  12. package/dist/chunks/{records-BKSMowhR.js → records-IHsCfv7s.js} +20 -7
  13. package/dist/chunks/records-IHsCfv7s.js.map +1 -0
  14. package/dist/chunks/{writer-BdMak4_J.js → writer-BtWpUaiH.js} +4 -4
  15. package/dist/chunks/{writer-BdMak4_J.js.map → writer-BtWpUaiH.js.map} +1 -1
  16. package/dist/csv.js +239 -18
  17. package/dist/csv.js.map +1 -1
  18. package/dist/dot.js +1 -1
  19. package/dist/gexf.js +72 -33
  20. package/dist/gexf.js.map +1 -1
  21. package/dist/gml.js +44 -23
  22. package/dist/gml.js.map +1 -1
  23. package/dist/graph-io.js +11 -11
  24. package/dist/graphml.js +1 -1
  25. package/dist/json.js +1 -1
  26. package/dist/neo4j.js +5 -4
  27. package/dist/neo4j.js.map +1 -1
  28. package/dist/pajek.js +1 -1
  29. package/dist/src/common/export.js +1 -1
  30. package/dist/src/common/export.js.map +1 -1
  31. package/dist/src/formats/csv/exporter.d.ts +15 -4
  32. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  33. package/dist/src/formats/csv/exporter.js +125 -6
  34. package/dist/src/formats/csv/exporter.js.map +1 -1
  35. package/dist/src/formats/csv/importer.d.ts +36 -4
  36. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  37. package/dist/src/formats/csv/importer.js +169 -8
  38. package/dist/src/formats/csv/importer.js.map +1 -1
  39. package/dist/src/formats/csv/records.d.ts +14 -1
  40. package/dist/src/formats/csv/records.d.ts.map +1 -1
  41. package/dist/src/formats/csv/records.js +28 -7
  42. package/dist/src/formats/csv/records.js.map +1 -1
  43. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  44. package/dist/src/formats/dot/exporter.js +2 -0
  45. package/dist/src/formats/dot/exporter.js.map +1 -1
  46. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  47. package/dist/src/formats/gexf/exporter.js +36 -10
  48. package/dist/src/formats/gexf/exporter.js.map +1 -1
  49. package/dist/src/formats/gexf/importer.d.ts +9 -4
  50. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  51. package/dist/src/formats/gexf/importer.js +44 -21
  52. package/dist/src/formats/gexf/importer.js.map +1 -1
  53. package/dist/src/formats/gexf/index.d.ts +1 -1
  54. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  55. package/dist/src/formats/gexf/index.js +1 -1
  56. package/dist/src/formats/gexf/index.js.map +1 -1
  57. package/dist/src/formats/gexf/schema.d.ts +2 -0
  58. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  59. package/dist/src/formats/gexf/schema.js +18 -0
  60. package/dist/src/formats/gexf/schema.js.map +1 -1
  61. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  62. package/dist/src/formats/gml/exporter.js +1 -0
  63. package/dist/src/formats/gml/exporter.js.map +1 -1
  64. package/dist/src/formats/gml/importer.d.ts +7 -4
  65. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  66. package/dist/src/formats/gml/importer.js +32 -19
  67. package/dist/src/formats/gml/importer.js.map +1 -1
  68. package/dist/src/formats/gml/index.d.ts +3 -1
  69. package/dist/src/formats/gml/index.d.ts.map +1 -1
  70. package/dist/src/formats/gml/index.js +4 -2
  71. package/dist/src/formats/gml/index.js.map +1 -1
  72. package/dist/src/formats/graphml/constants.d.ts +2 -0
  73. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  74. package/dist/src/formats/graphml/constants.js +2 -0
  75. package/dist/src/formats/graphml/constants.js.map +1 -1
  76. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  77. package/dist/src/formats/graphml/exporter.js +14 -2
  78. package/dist/src/formats/graphml/exporter.js.map +1 -1
  79. package/dist/src/formats/graphml/importer.d.ts +2 -1
  80. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  81. package/dist/src/formats/graphml/importer.js +91 -27
  82. package/dist/src/formats/graphml/importer.js.map +1 -1
  83. package/dist/src/formats/graphml/yfiles.d.ts +62 -0
  84. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -0
  85. package/dist/src/formats/graphml/yfiles.js +269 -0
  86. package/dist/src/formats/graphml/yfiles.js.map +1 -0
  87. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/json/exporter.js +9 -1
  89. package/dist/src/formats/json/exporter.js.map +1 -1
  90. package/dist/src/formats/json/importer.d.ts +14 -0
  91. package/dist/src/formats/json/importer.d.ts.map +1 -1
  92. package/dist/src/formats/json/importer.js +165 -4
  93. package/dist/src/formats/json/importer.js.map +1 -1
  94. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  95. package/dist/src/formats/neo4j/exporter.js +1 -0
  96. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  97. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  98. package/dist/src/formats/pajek/exporter.js +3 -2
  99. package/dist/src/formats/pajek/exporter.js.map +1 -1
  100. package/package.json +2 -2
  101. package/src/common/export.ts +1 -1
  102. package/src/formats/csv/exporter.ts +140 -10
  103. package/src/formats/csv/importer.ts +207 -18
  104. package/src/formats/csv/records.ts +41 -6
  105. package/src/formats/dot/exporter.ts +2 -0
  106. package/src/formats/gexf/exporter.ts +40 -11
  107. package/src/formats/gexf/importer.ts +47 -25
  108. package/src/formats/gexf/index.ts +1 -1
  109. package/src/formats/gexf/schema.ts +18 -0
  110. package/src/formats/gml/exporter.ts +1 -0
  111. package/src/formats/gml/importer.ts +47 -24
  112. package/src/formats/gml/index.ts +4 -1
  113. package/src/formats/graphml/constants.ts +2 -0
  114. package/src/formats/graphml/exporter.ts +21 -2
  115. package/src/formats/graphml/importer.ts +99 -28
  116. package/src/formats/graphml/yfiles.ts +292 -0
  117. package/src/formats/json/exporter.ts +9 -1
  118. package/src/formats/json/importer.ts +202 -4
  119. package/src/formats/neo4j/exporter.ts +1 -0
  120. package/src/formats/pajek/exporter.ts +3 -1
  121. package/dist/chunks/importer-BOxwmef_.js.map +0 -1
  122. package/dist/chunks/importer-BddBW3sf.js.map +0 -1
  123. package/dist/chunks/importer-BtHFICnf.js.map +0 -1
  124. package/dist/chunks/importer-DfI8POXb.js.map +0 -1
  125. package/dist/chunks/records-BKSMowhR.js.map +0 -1
@@ -114,6 +114,20 @@ export interface JsonImportOptions {
114
114
  indexLinks?: boolean | "auto" | undefined;
115
115
  /** jgf: which graph of a `graphs` array to read; 0 by default. */
116
116
  graphIndex?: number | undefined;
117
+ /**
118
+ * node-link / d3 / vis / graphology: where the node array is, as a dotted path of object keys
119
+ * from the document root (`"data.nodes"`); the object holding it is read as the graph record
120
+ * (its `directed`, `multigraph`, `graph` and edge keys). "nodes" by default. A path that names
121
+ * nothing is an E_MISSING_SECTION issue and the graph has no node records.
122
+ */
123
+ nodesPath?: string | undefined;
124
+ /**
125
+ * node-link / d3 / vis / graphology: where the edge array is, as a dotted path of object keys
126
+ * from the document root (`"data.links"`); by default the edges or links key of the object
127
+ * holding the nodes. A path that names nothing is an E_MISSING_SECTION issue and the graph has
128
+ * no edge records.
129
+ */
130
+ edgesPath?: string | undefined;
117
131
  }
118
132
 
119
133
  /**
@@ -254,6 +268,14 @@ interface EdgeIdColumn {
254
268
  readonly seen: Set<string> | null;
255
269
  }
256
270
 
271
+ /** The dialects whose node and edge arrays nodesPath and edgesPath can point at. */
272
+ const PATH_DIALECTS: ReadonlySet<JsonImportDialect> = new Set<JsonImportDialect>([
273
+ "node-link",
274
+ "d3",
275
+ "vis",
276
+ "graphology",
277
+ ]);
278
+
257
279
  /** The resolved format-specific options. */
258
280
  interface ResolvedJsonOptions {
259
281
  readonly dialect: JsonImportDialect | "auto";
@@ -263,6 +285,10 @@ interface ResolvedJsonOptions {
263
285
  readonly targetKey: string | null;
264
286
  readonly indexLinks: boolean | "auto";
265
287
  readonly graphIndex: number;
288
+ /** The dotted path segments of the node array, or null for the root's own nodes key. */
289
+ readonly nodesPath: readonly string[] | null;
290
+ /** The dotted path segments of the edge array, or null for the edges / links key beside the nodes. */
291
+ readonly edgesPath: readonly string[] | null;
266
292
  /** Set by importAll(): every graph of a `graphs` array is read, so none is reported as skipped. */
267
293
  readonly all?: boolean;
268
294
  }
@@ -286,6 +312,11 @@ function resolveJsonOptions(options: (JsonImportOptions & CommonImportOptions) |
286
312
  if (!Number.isInteger(graphIndex) || graphIndex < 0) {
287
313
  throw unsupportedOption("graphIndex", graphIndex, ["a non-negative integer"]);
288
314
  }
315
+ const nodesPath = pathOption("nodesPath", o.nodesPath);
316
+ const edgesPath = pathOption("edgesPath", o.edgesPath);
317
+ if ((nodesPath !== null || edgesPath !== null) && dialect !== "auto" && !PATH_DIALECTS.has(dialect)) {
318
+ throw unsupportedOption(nodesPath === null ? "edgesPath" : "nodesPath", dialect, [...PATH_DIALECTS]);
319
+ }
289
320
  return {
290
321
  dialect,
291
322
  nodeIdKey: keyOption("nodeIdKey", o.nodeIdKey),
@@ -294,7 +325,176 @@ function resolveJsonOptions(options: (JsonImportOptions & CommonImportOptions) |
294
325
  targetKey: keyOption("targetKey", o.targetKey),
295
326
  indexLinks,
296
327
  graphIndex,
328
+ nodesPath,
329
+ edgesPath,
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Check a dotted path option: object keys joined by dots, none of them empty.
335
+ * @param name - the option name
336
+ * @param value - the caller's value
337
+ * @returns the segments, or null when absent
338
+ */
339
+ function pathOption(name: string, value: unknown): readonly string[] | null {
340
+ if (value === undefined) {
341
+ return null;
342
+ }
343
+ const segments = typeof value === "string" ? value.split(".") : [];
344
+ if (segments.length === 0 || segments.some((segment) => segment.length === 0)) {
345
+ throw unsupportedOption(name, value, ["a dotted path of non-empty keys"]);
346
+ }
347
+ return segments;
348
+ }
349
+
350
+ /**
351
+ * The value at a path of object keys, or undefined when a step is missing or not an object.
352
+ * @param root - the document
353
+ * @param segments - the keys
354
+ * @returns the value
355
+ */
356
+ function valueAt(root: unknown, segments: readonly string[]): unknown {
357
+ let value = root;
358
+ for (const segment of segments) {
359
+ if (!isJsonObject(value) || !hasKey(value, segment)) {
360
+ return undefined;
361
+ }
362
+ value = value[segment];
363
+ }
364
+ return value;
365
+ }
366
+
367
+ /**
368
+ * The graph record nodesPath and edgesPath describe: the object holding the node array (the
369
+ * document itself by default) with its nodes key and its edges / links key replaced by the arrays
370
+ * the paths name. A path that names nothing is recorded as E_MISSING_SECTION and stands for an
371
+ * empty array, so the import goes on.
372
+ * @param root - the parsed document
373
+ * @param json - the resolved options
374
+ * @param report - the report
375
+ * @returns the document unchanged when no path is given, else the graph record
376
+ */
377
+ function applyPaths(root: unknown, json: ResolvedJsonOptions, report: ImportReportBuilder): unknown {
378
+ const { nodesPath, edgesPath } = json;
379
+ if (nodesPath === null && edgesPath === null) {
380
+ return root;
381
+ }
382
+ const lookup = (option: string, segments: readonly string[]): unknown => {
383
+ const value = valueAt(root, segments);
384
+ if (value === undefined) {
385
+ const path = segments.join(".");
386
+ report.error(
387
+ "missing-value",
388
+ JSON_ISSUE.MISSING_SECTION,
389
+ `${option} ${JSON.stringify(path)} names nothing in the document`,
390
+ { element: path },
391
+ );
392
+ return [];
393
+ }
394
+ return value;
297
395
  };
396
+ const holderPath = nodesPath === null ? [] : nodesPath.slice(0, -1);
397
+ const holder = valueAt(root, holderPath);
398
+ // the keys the paths replace: the nodes key, and every edge key when edgesPath names the edges
399
+ const replaced = new Set<string>();
400
+ if (nodesPath !== null) {
401
+ replaced.add(nodesPath[nodesPath.length - 1]);
402
+ }
403
+ if (edgesPath !== null) {
404
+ replaced.add("edges");
405
+ replaced.add("links");
406
+ }
407
+ let record: JsonRecord = Object.fromEntries(
408
+ Object.entries(isJsonObject(holder) ? holder : {}).filter(([key]) => !replaced.has(key)),
409
+ );
410
+ // edges inside the holder (`data.graph.links` under `data`) leave the holder, and only they do:
411
+ // the rest of the key they sit in (the `graph` attributes) is still read
412
+ if (edgesPath !== null && edgesPath.length > holderPath.length && holderPath.every((k, i) => edgesPath[i] === k)) {
413
+ const rest = withoutPath(record, edgesPath.slice(holderPath.length));
414
+ record = isJsonObject(rest) ? rest : {};
415
+ }
416
+ if (nodesPath !== null) {
417
+ record.nodes = lookup("nodesPath", nodesPath);
418
+ }
419
+ if (edgesPath !== null) {
420
+ record[pathEdgesKey(json, edgesPath)] = lookup("edgesPath", edgesPath);
421
+ }
422
+ return record;
423
+ }
424
+
425
+ /**
426
+ * A value with the entry at a path of object keys removed; undefined when the path is empty (the
427
+ * value itself goes) or nothing but that entry is left.
428
+ * @param value - the value
429
+ * @param segments - the keys down to the entry
430
+ * @returns the value without the entry
431
+ */
432
+ function withoutPath(value: unknown, segments: readonly string[]): unknown {
433
+ if (segments.length === 0) {
434
+ return undefined;
435
+ }
436
+ const [head, ...tail] = segments;
437
+ if (!isJsonObject(value) || !hasKey(value, head)) {
438
+ return value;
439
+ }
440
+ // a parsed JSON value is never undefined, so undefined marks the entries to drop
441
+ const entries = Object.entries(value)
442
+ .map(([key, v]) => [key, key === head ? withoutPath(v, tail) : v] as const)
443
+ .filter(([, v]) => v !== undefined);
444
+ return entries.length === 0 ? undefined : Object.fromEntries(entries);
445
+ }
446
+
447
+ /**
448
+ * The key applyPaths() stores the edge array under: the caller's edgesKey, else "links" when
449
+ * edgesPath ends in links (so the d3 sniff still sees it), else "edges".
450
+ * @param json - the resolved options
451
+ * @param edgesPath - the edgesPath segments
452
+ * @returns the key
453
+ */
454
+ function pathEdgesKey(json: ResolvedJsonOptions, edgesPath: readonly string[]): string {
455
+ return json.edgesKey ?? (edgesPath[edgesPath.length - 1] === "links" ? "links" : "edges");
456
+ }
457
+
458
+ /**
459
+ * The dialect of a graph record built from nodesPath / edgesPath: the forced one, else the shape
460
+ * rule over the record, else node-link.
461
+ * @param record - the graph record
462
+ * @param forced - the caller's dialect option
463
+ * @returns the dialect
464
+ */
465
+ function pathDialect(record: unknown, forced: JsonImportDialect | "auto"): JsonImportDialect {
466
+ if (forced !== "auto") {
467
+ return forced;
468
+ }
469
+ const sniffed = sniffJsonDialect(record);
470
+ return sniffed !== null && PATH_DIALECTS.has(sniffed) ? sniffed : "node-link";
471
+ }
472
+
473
+ /**
474
+ * The document to read and its dialect: the graph record of nodesPath / edgesPath when either is
475
+ * given, the document itself otherwise.
476
+ * @param parsed - the parsed document
477
+ * @param json - the resolved options
478
+ * @param report - the report
479
+ * @returns the root and its dialect
480
+ */
481
+ function documentOf(
482
+ parsed: unknown,
483
+ json: ResolvedJsonOptions,
484
+ report: ImportReportBuilder,
485
+ ): { readonly root: unknown; readonly dialect: JsonImportDialect } {
486
+ if (json.nodesPath === null && json.edgesPath === null) {
487
+ return { root: parsed, dialect: detectDialect(parsed, json.dialect, report) };
488
+ }
489
+ const root = applyPaths(parsed, json, report);
490
+ const dialect = pathDialect(root, json.dialect);
491
+ // vis and graphology read their edges from the edges key only
492
+ if (json.edgesPath !== null && (dialect === "vis" || dialect === "graphology") && isJsonObject(root)) {
493
+ const key = pathEdgesKey(json, json.edgesPath);
494
+ const renamed = Object.fromEntries(Object.entries(root).map(([k, v]) => [k === key ? "edges" : k, v]));
495
+ return { root: renamed, dialect };
496
+ }
497
+ return { root, dialect };
298
498
  }
299
499
 
300
500
  /**
@@ -2641,8 +2841,7 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
2641
2841
  const json = resolveJsonOptions(options);
2642
2842
  const report = new ImportReportBuilder("json", resolved.errorLimit);
2643
2843
  const text = await readText(input, report, resolved);
2644
- const root = parseDocument(text, report);
2645
- const dialect = detectDialect(root, json.dialect, report);
2844
+ const { root, dialect } = documentOf(parseDocument(text, report), json, report);
2646
2845
  readGraph(root, dialect, sink, report, resolved, json, options);
2647
2846
  return report.finish();
2648
2847
  },
@@ -2664,8 +2863,7 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
2664
2863
  const json = resolveJsonOptions(options);
2665
2864
  const first = new ImportReportBuilder("json", resolved.errorLimit);
2666
2865
  const text = await readText(input, first, resolved);
2667
- const root = parseDocument(text, first);
2668
- const dialect = detectDialect(root, json.dialect, first);
2866
+ const { root, dialect } = documentOf(parseDocument(text, first), json, first);
2669
2867
  const graphs =
2670
2868
  dialect === "jgf" && isJsonObject(root) && !isJsonObject(root.graph) && Array.isArray(root.graphs)
2671
2869
  ? root.graphs.length
@@ -150,6 +150,7 @@ const SKIPPED_ROLES: ReadonlySet<string> = new Set([
150
150
  "timestamps",
151
151
  "spells",
152
152
  "open",
153
+ "spellsOpen",
153
154
  ]);
154
155
 
155
156
  /** Notes about relationships only, dropped when only nodes are written. */
@@ -30,6 +30,7 @@ import {
30
30
  isParameterKey,
31
31
  LABEL_COLUMN,
32
32
  ORIGINAL_ID_KEY,
33
+ POSITION_COLUMN,
33
34
  RELATION_COLUMN,
34
35
  SHAPE_COLUMN,
35
36
  SHAPES,
@@ -84,7 +85,7 @@ export const PAJEK_LOSS = Object.freeze({
84
85
  const SLOT_ROLES: ReadonlySet<string> = new Set(["label"]);
85
86
 
86
87
  /** The names the importer gives the slot columns, for the name-change notes. */
87
- const ROLE_NAMES: Readonly<Record<string, string>> = Object.freeze({ label: "label" });
88
+ const ROLE_NAMES: Readonly<Record<string, string>> = Object.freeze({ label: "label", position: POSITION_COLUMN });
88
89
 
89
90
  /** The roles the exporter handles structurally rather than as parameters. */
90
91
  const STRUCTURAL_ROLES: ReadonlySet<string> = new Set([
@@ -105,6 +106,7 @@ const CHECKED_ROLES: ReadonlySet<string> = new Set([
105
106
  "parent",
106
107
  "parents",
107
108
  "open",
109
+ "spellsOpen",
108
110
  ]);
109
111
 
110
112
  /** Roles the generic checker lets through under temporal "spells" that Pajek cannot write. */