@graphty/graph-io 0.0.0 → 0.2.0
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/LICENSE +21 -0
- package/README.md +250 -28
- package/dist/chunks/children-CL3Cy0ez.js +238 -0
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
- package/dist/chunks/escape-DyI8JofU.js +938 -0
- package/dist/chunks/escape-DyI8JofU.js.map +1 -0
- package/dist/chunks/importer-CQnJuWJw.js +2987 -0
- package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
- package/dist/chunks/importer-CpCpfbxr.js +2015 -0
- package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
- package/dist/chunks/importer-DbnGYr3_.js +2342 -0
- package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
- package/dist/chunks/importer-GozH8DkN.js +3050 -0
- package/dist/chunks/importer-GozH8DkN.js.map +1 -0
- package/dist/chunks/records-CGpxszm1.js +605 -0
- package/dist/chunks/records-CGpxszm1.js.map +1 -0
- package/dist/chunks/text-CajMdVFy.js +189 -0
- package/dist/chunks/text-CajMdVFy.js.map +1 -0
- package/dist/chunks/writer-DxSKC7TL.js +2842 -0
- package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1702 -0
- package/dist/csv.js.map +1 -0
- package/dist/dot.d.ts +1 -0
- package/dist/dot.js +8 -0
- package/dist/dot.js.map +1 -0
- package/dist/gexf.d.ts +1 -0
- package/dist/gexf.js +3466 -0
- package/dist/gexf.js.map +1 -0
- package/dist/gml.d.ts +1 -0
- package/dist/gml.js +2647 -0
- package/dist/gml.js.map +1 -0
- package/dist/graph-io.d.ts +1 -0
- package/dist/graph-io.js +790 -0
- package/dist/graph-io.js.map +1 -0
- package/dist/graphml.d.ts +1 -0
- package/dist/graphml.js +8 -0
- package/dist/graphml.js.map +1 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +11 -0
- package/dist/json.js.map +1 -0
- package/dist/neo4j.d.ts +1 -0
- package/dist/neo4j.js +2046 -0
- package/dist/neo4j.js.map +1 -0
- package/dist/pajek.d.ts +1 -0
- package/dist/pajek.js +8 -0
- package/dist/pajek.js.map +1 -0
- package/dist/src/children.d.ts +134 -0
- package/dist/src/children.d.ts.map +1 -0
- package/dist/src/children.js +274 -0
- package/dist/src/children.js.map +1 -0
- package/dist/src/common/attributes.d.ts +229 -0
- package/dist/src/common/attributes.d.ts.map +1 -0
- package/dist/src/common/attributes.js +368 -0
- package/dist/src/common/attributes.js.map +1 -0
- package/dist/src/common/codes.d.ts +105 -0
- package/dist/src/common/codes.d.ts.map +1 -0
- package/dist/src/common/codes.js +107 -0
- package/dist/src/common/codes.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +84 -0
- package/dist/src/common/declared-types.d.ts.map +1 -0
- package/dist/src/common/declared-types.js +326 -0
- package/dist/src/common/declared-types.js.map +1 -0
- package/dist/src/common/direction.d.ts +206 -0
- package/dist/src/common/direction.d.ts.map +1 -0
- package/dist/src/common/direction.js +370 -0
- package/dist/src/common/direction.js.map +1 -0
- package/dist/src/common/escape.d.ts +92 -0
- package/dist/src/common/escape.d.ts.map +1 -0
- package/dist/src/common/escape.js +212 -0
- package/dist/src/common/escape.js.map +1 -0
- package/dist/src/common/export.d.ts +249 -0
- package/dist/src/common/export.d.ts.map +1 -0
- package/dist/src/common/export.js +594 -0
- package/dist/src/common/export.js.map +1 -0
- package/dist/src/common/format.d.ts +59 -0
- package/dist/src/common/format.d.ts.map +1 -0
- package/dist/src/common/format.js +106 -0
- package/dist/src/common/format.js.map +1 -0
- package/dist/src/common/ids.d.ts +83 -0
- package/dist/src/common/ids.d.ts.map +1 -0
- package/dist/src/common/ids.js +158 -0
- package/dist/src/common/ids.js.map +1 -0
- package/dist/src/common/input.d.ts +100 -0
- package/dist/src/common/input.d.ts.map +1 -0
- package/dist/src/common/input.js +335 -0
- package/dist/src/common/input.js.map +1 -0
- package/dist/src/common/lists.d.ts +34 -0
- package/dist/src/common/lists.d.ts.map +1 -0
- package/dist/src/common/lists.js +185 -0
- package/dist/src/common/lists.js.map +1 -0
- package/dist/src/common/options.d.ts +108 -0
- package/dist/src/common/options.d.ts.map +1 -0
- package/dist/src/common/options.js +265 -0
- package/dist/src/common/options.js.map +1 -0
- package/dist/src/common/report.d.ts +187 -0
- package/dist/src/common/report.d.ts.map +1 -0
- package/dist/src/common/report.js +274 -0
- package/dist/src/common/report.js.map +1 -0
- package/dist/src/common/temporal.d.ts +71 -0
- package/dist/src/common/temporal.d.ts.map +1 -0
- package/dist/src/common/temporal.js +266 -0
- package/dist/src/common/temporal.js.map +1 -0
- package/dist/src/common/text.d.ts +104 -0
- package/dist/src/common/text.d.ts.map +1 -0
- package/dist/src/common/text.js +255 -0
- package/dist/src/common/text.js.map +1 -0
- package/dist/src/common/weights.d.ts +77 -0
- package/dist/src/common/weights.d.ts.map +1 -0
- package/dist/src/common/weights.js +156 -0
- package/dist/src/common/weights.js.map +1 -0
- package/dist/src/common/writer.d.ts +51 -0
- package/dist/src/common/writer.d.ts.map +1 -0
- package/dist/src/common/writer.js +108 -0
- package/dist/src/common/writer.js.map +1 -0
- package/dist/src/common/xml.d.ts +245 -0
- package/dist/src/common/xml.d.ts.map +1 -0
- package/dist/src/common/xml.js +942 -0
- package/dist/src/common/xml.js.map +1 -0
- package/dist/src/formats/csv/exporter.d.ts +70 -0
- package/dist/src/formats/csv/exporter.d.ts.map +1 -0
- package/dist/src/formats/csv/exporter.js +682 -0
- package/dist/src/formats/csv/exporter.js.map +1 -0
- package/dist/src/formats/csv/header.d.ts +66 -0
- package/dist/src/formats/csv/header.d.ts.map +1 -0
- package/dist/src/formats/csv/header.js +152 -0
- package/dist/src/formats/csv/header.js.map +1 -0
- package/dist/src/formats/csv/importer.d.ts +82 -0
- package/dist/src/formats/csv/importer.d.ts.map +1 -0
- package/dist/src/formats/csv/importer.js +849 -0
- package/dist/src/formats/csv/importer.js.map +1 -0
- package/dist/src/formats/csv/index.d.ts +60 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -0
- package/dist/src/formats/csv/index.js +63 -0
- package/dist/src/formats/csv/index.js.map +1 -0
- package/dist/src/formats/csv/records.d.ts +188 -0
- package/dist/src/formats/csv/records.d.ts.map +1 -0
- package/dist/src/formats/csv/records.js +702 -0
- package/dist/src/formats/csv/records.js.map +1 -0
- package/dist/src/formats/csv/values.d.ts +105 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -0
- package/dist/src/formats/csv/values.js +192 -0
- package/dist/src/formats/csv/values.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +52 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -0
- package/dist/src/formats/dot/exporter.js +836 -0
- package/dist/src/formats/dot/exporter.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts +102 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -0
- package/dist/src/formats/dot/importer.js +1291 -0
- package/dist/src/formats/dot/importer.js.map +1 -0
- package/dist/src/formats/dot/index.d.ts +7 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -0
- package/dist/src/formats/dot/index.js +7 -0
- package/dist/src/formats/dot/index.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +29 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -0
- package/dist/src/formats/dot/names.js +28 -0
- package/dist/src/formats/dot/names.js.map +1 -0
- package/dist/src/formats/dot/tokenizer.d.ts +114 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
- package/dist/src/formats/dot/tokenizer.js +341 -0
- package/dist/src/formats/dot/tokenizer.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts +56 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
- package/dist/src/formats/gexf/exporter.js +1395 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +73 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -0
- package/dist/src/formats/gexf/importer.js +1880 -0
- package/dist/src/formats/gexf/importer.js.map +1 -0
- package/dist/src/formats/gexf/index.d.ts +96 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -0
- package/dist/src/formats/gexf/index.js +97 -0
- package/dist/src/formats/gexf/index.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts +135 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -0
- package/dist/src/formats/gexf/schema.js +323 -0
- package/dist/src/formats/gexf/schema.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +69 -0
- package/dist/src/formats/gml/exporter.d.ts.map +1 -0
- package/dist/src/formats/gml/exporter.js +1093 -0
- package/dist/src/formats/gml/exporter.js.map +1 -0
- package/dist/src/formats/gml/importer.d.ts +66 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -0
- package/dist/src/formats/gml/importer.js +1331 -0
- package/dist/src/formats/gml/importer.js.map +1 -0
- package/dist/src/formats/gml/index.d.ts +85 -0
- package/dist/src/formats/gml/index.d.ts.map +1 -0
- package/dist/src/formats/gml/index.js +88 -0
- package/dist/src/formats/gml/index.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +186 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -0
- package/dist/src/formats/gml/syntax.js +467 -0
- package/dist/src/formats/gml/syntax.js.map +1 -0
- package/dist/src/formats/graphml/constants.d.ts +169 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -0
- package/dist/src/formats/graphml/constants.js +165 -0
- package/dist/src/formats/graphml/constants.js.map +1 -0
- package/dist/src/formats/graphml/exporter.d.ts +34 -0
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
- package/dist/src/formats/graphml/exporter.js +1176 -0
- package/dist/src/formats/graphml/exporter.js.map +1 -0
- package/dist/src/formats/graphml/importer.d.ts +31 -0
- package/dist/src/formats/graphml/importer.d.ts.map +1 -0
- package/dist/src/formats/graphml/importer.js +1607 -0
- package/dist/src/formats/graphml/importer.js.map +1 -0
- package/dist/src/formats/graphml/index.d.ts +8 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -0
- package/dist/src/formats/graphml/index.js +8 -0
- package/dist/src/formats/graphml/index.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts +72 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -0
- package/dist/src/formats/graphml/tree.js +290 -0
- package/dist/src/formats/graphml/tree.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +125 -0
- package/dist/src/formats/json/dialect.d.ts.map +1 -0
- package/dist/src/formats/json/dialect.js +262 -0
- package/dist/src/formats/json/dialect.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts +89 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -0
- package/dist/src/formats/json/exporter.js +1358 -0
- package/dist/src/formats/json/exporter.js.map +1 -0
- package/dist/src/formats/json/importer.d.ts +108 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -0
- package/dist/src/formats/json/importer.js +1838 -0
- package/dist/src/formats/json/importer.js.map +1 -0
- package/dist/src/formats/json/index.d.ts +8 -0
- package/dist/src/formats/json/index.d.ts.map +1 -0
- package/dist/src/formats/json/index.js +8 -0
- package/dist/src/formats/json/index.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts +68 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
- package/dist/src/formats/neo4j/exporter.js +1055 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -0
- package/dist/src/formats/neo4j/header.d.ts +52 -0
- package/dist/src/formats/neo4j/header.d.ts.map +1 -0
- package/dist/src/formats/neo4j/header.js +131 -0
- package/dist/src/formats/neo4j/header.js.map +1 -0
- package/dist/src/formats/neo4j/importer.d.ts +73 -0
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
- package/dist/src/formats/neo4j/importer.js +932 -0
- package/dist/src/formats/neo4j/importer.js.map +1 -0
- package/dist/src/formats/neo4j/index.d.ts +79 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -0
- package/dist/src/formats/neo4j/index.js +83 -0
- package/dist/src/formats/neo4j/index.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts +58 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
- package/dist/src/formats/pajek/exporter.js +825 -0
- package/dist/src/formats/pajek/exporter.js.map +1 -0
- package/dist/src/formats/pajek/importer.d.ts +88 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -0
- package/dist/src/formats/pajek/importer.js +1047 -0
- package/dist/src/formats/pajek/importer.js.map +1 -0
- package/dist/src/formats/pajek/index.d.ts +7 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -0
- package/dist/src/formats/pajek/index.js +7 -0
- package/dist/src/formats/pajek/index.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +112 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
- package/dist/src/formats/pajek/syntax.js +269 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +207 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +481 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/sniff.d.ts +104 -0
- package/dist/src/sniff.d.ts.map +1 -0
- package/dist/src/sniff.js +357 -0
- package/dist/src/sniff.js.map +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +29 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +122 -7
- package/src/children.ts +335 -0
- package/src/common/attributes.ts +520 -0
- package/src/common/codes.ts +153 -0
- package/src/common/declared-types.ts +374 -0
- package/src/common/direction.ts +518 -0
- package/src/common/escape.ts +231 -0
- package/src/common/export.ts +817 -0
- package/src/common/format.ts +111 -0
- package/src/common/ids.ts +176 -0
- package/src/common/input.ts +378 -0
- package/src/common/lists.ts +196 -0
- package/src/common/options.ts +377 -0
- package/src/common/report.ts +352 -0
- package/src/common/temporal.ts +302 -0
- package/src/common/text.ts +294 -0
- package/src/common/weights.ts +202 -0
- package/src/common/writer.ts +123 -0
- package/src/common/xml.ts +1053 -0
- package/src/formats/csv/exporter.ts +894 -0
- package/src/formats/csv/header.ts +172 -0
- package/src/formats/csv/importer.ts +1104 -0
- package/src/formats/csv/index.ts +88 -0
- package/src/formats/csv/records.ts +813 -0
- package/src/formats/csv/values.ts +224 -0
- package/src/formats/dot/exporter.ts +1014 -0
- package/src/formats/dot/importer.ts +1549 -0
- package/src/formats/dot/index.ts +7 -0
- package/src/formats/dot/names.ts +40 -0
- package/src/formats/dot/tokenizer.ts +384 -0
- package/src/formats/gexf/exporter.ts +1696 -0
- package/src/formats/gexf/importer.ts +2333 -0
- package/src/formats/gexf/index.ts +142 -0
- package/src/formats/gexf/schema.ts +361 -0
- package/src/formats/gml/exporter.ts +1404 -0
- package/src/formats/gml/importer.ts +1591 -0
- package/src/formats/gml/index.ts +128 -0
- package/src/formats/gml/syntax.ts +545 -0
- package/src/formats/graphml/constants.ts +225 -0
- package/src/formats/graphml/exporter.ts +1458 -0
- package/src/formats/graphml/importer.ts +2027 -0
- package/src/formats/graphml/index.ts +8 -0
- package/src/formats/graphml/tree.ts +318 -0
- package/src/formats/json/dialect.ts +317 -0
- package/src/formats/json/exporter.ts +1616 -0
- package/src/formats/json/importer.ts +2271 -0
- package/src/formats/json/index.ts +8 -0
- package/src/formats/neo4j/exporter.ts +1287 -0
- package/src/formats/neo4j/header.ts +156 -0
- package/src/formats/neo4j/importer.ts +1220 -0
- package/src/formats/neo4j/index.ts +116 -0
- package/src/formats/pajek/exporter.ts +1000 -0
- package/src/formats/pajek/importer.ts +1311 -0
- package/src/formats/pajek/index.ts +7 -0
- package/src/formats/pajek/syntax.ts +307 -0
- package/src/index.ts +244 -0
- package/src/registry.ts +617 -0
- package/src/sniff.ts +397 -0
- package/src/types.ts +262 -0
|
@@ -0,0 +1,2987 @@
|
|
|
1
|
+
import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
|
|
2
|
+
import { a9 as capabilities, L as LOSS, au as joinText, ah as encodeChunks, aa as checkCapabilities, aB as resolveExportOptions, aG as countMixedEdges, ai as explicitWeights, aw as pairFolding, ak as formatF32, J as OPTION_IGNORED_CODE, W as SINK_OPTION_CODE, H as HYPEREDGE_CODE, y as MULTIPLE_GRAPHS_CODE, n as ID_MERGED_CODE, i as DUPLICATE_EDGE_ID_CODE, k as DUPLICATE_NODE_CODE, a3 as UNKNOWN_PARENT_CODE, M as MISSING_ENDPOINT_CODE, w as MISSING_ID_CODE, Z as SYNTAX_CODE, m as EMPTY_INPUT_CODE, aC as resolveImportOptions, I as ImportReportBuilder, ay as readText, az as reportSinkOptions, aA as reportUnusedOptions, a as throwIfAborted, s as IdCoercer, l as DirectionResolver, aH as weightFromValue, aI as uniqueColumnName } from "./writer-DxSKC7TL.js";
|
|
3
|
+
const JSON_DIALECTS = Object.freeze([
|
|
4
|
+
"node-link",
|
|
5
|
+
"d3",
|
|
6
|
+
"jgf",
|
|
7
|
+
"cytoscape",
|
|
8
|
+
"graphology",
|
|
9
|
+
"vis"
|
|
10
|
+
]);
|
|
11
|
+
const META_KEY = "json";
|
|
12
|
+
function isJsonObject(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function hasKey(record, key) {
|
|
16
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
17
|
+
}
|
|
18
|
+
function isJsonDialect(value) {
|
|
19
|
+
return typeof value === "string" && JSON_DIALECTS.includes(value);
|
|
20
|
+
}
|
|
21
|
+
function sniffJsonDialect(root) {
|
|
22
|
+
if (Array.isArray(root)) {
|
|
23
|
+
const first = firstJsonObject(root);
|
|
24
|
+
return root.length === 0 || first !== null && isJsonObject(first.data) ? "cytoscape" : null;
|
|
25
|
+
}
|
|
26
|
+
if (!isJsonObject(root)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (hasKey(root, "elements")) {
|
|
30
|
+
return "cytoscape";
|
|
31
|
+
}
|
|
32
|
+
if (isJsonObject(root.graph) && (hasKey(root.graph, "nodes") || hasKey(root.graph, "edges"))) {
|
|
33
|
+
return "jgf";
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(root.graphs)) {
|
|
36
|
+
return "jgf";
|
|
37
|
+
}
|
|
38
|
+
if (!hasKey(root, "nodes") && !hasKey(root, "edges") && !hasKey(root, "links")) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const firstNode = firstJsonObject(root.nodes);
|
|
42
|
+
const firstEdge = firstJsonObject(hasKey(root, "edges") ? root.edges : root.links);
|
|
43
|
+
if (isJsonObject(root.options) && (hasKey(root.options, "type") || hasKey(root.options, "multi"))) {
|
|
44
|
+
return "graphology";
|
|
45
|
+
}
|
|
46
|
+
if (firstNode !== null && hasKey(firstNode, "key") && !hasKey(firstNode, "id")) {
|
|
47
|
+
return "graphology";
|
|
48
|
+
}
|
|
49
|
+
if (firstEdge !== null && !hasKey(firstEdge, "from")) {
|
|
50
|
+
if (hasKey(firstEdge, "undirected") || isJsonObject(firstEdge.attributes)) {
|
|
51
|
+
return "graphology";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (firstEdge !== null && hasKey(firstEdge, "from") && hasKey(firstEdge, "to") && !hasKey(firstEdge, "source")) {
|
|
55
|
+
return "vis";
|
|
56
|
+
}
|
|
57
|
+
const bare = !hasKey(root, "directed") && !hasKey(root, "multigraph") && !hasKey(root, "graph");
|
|
58
|
+
return bare && hasKey(root, "links") ? "d3" : "node-link";
|
|
59
|
+
}
|
|
60
|
+
function firstJsonObject(value) {
|
|
61
|
+
if (!Array.isArray(value)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const found = value.find((item) => isJsonObject(item));
|
|
65
|
+
return isJsonObject(found) ? found : null;
|
|
66
|
+
}
|
|
67
|
+
function shapeMetaOf(meta) {
|
|
68
|
+
const raw = meta.extra[META_KEY];
|
|
69
|
+
if (!isJsonObject(raw)) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
const text = (key) => typeof raw[key] === "string" ? raw[key] : void 0;
|
|
73
|
+
const flag = (key) => typeof raw[key] === "boolean" ? raw[key] : void 0;
|
|
74
|
+
const { nodeIdKey } = raw;
|
|
75
|
+
return {
|
|
76
|
+
dialect: isJsonDialect(raw.dialect) ? raw.dialect : void 0,
|
|
77
|
+
edgesKey: text("edgesKey"),
|
|
78
|
+
nodeIdKey: nodeIdKey === null || typeof nodeIdKey === "string" ? nodeIdKey : void 0,
|
|
79
|
+
indexLinks: flag("indexLinks"),
|
|
80
|
+
sourceKey: text("sourceKey"),
|
|
81
|
+
targetKey: text("targetKey"),
|
|
82
|
+
id: text("id"),
|
|
83
|
+
type: text("type"),
|
|
84
|
+
allowSelfLoops: flag("allowSelfLoops"),
|
|
85
|
+
cytoscape: isJsonObject(raw.cytoscape) ? raw.cytoscape : void 0
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const DIALECT_DEFAULT_DIRECTED = Object.freeze({
|
|
89
|
+
"node-link": false,
|
|
90
|
+
d3: false,
|
|
91
|
+
jgf: true,
|
|
92
|
+
cytoscape: true,
|
|
93
|
+
graphology: true,
|
|
94
|
+
vis: false
|
|
95
|
+
});
|
|
96
|
+
const JSON_DTYPES = Object.freeze(["f64", "i32", "bool", "string"]);
|
|
97
|
+
const COMMON = {
|
|
98
|
+
multiEdges: true,
|
|
99
|
+
selfLoops: true,
|
|
100
|
+
idCharset: "any",
|
|
101
|
+
dtypes: JSON_DTYPES,
|
|
102
|
+
components: false,
|
|
103
|
+
lists: false,
|
|
104
|
+
json: true,
|
|
105
|
+
defaults: false,
|
|
106
|
+
options: false,
|
|
107
|
+
temporal: "none",
|
|
108
|
+
positions: false,
|
|
109
|
+
viz: false
|
|
110
|
+
};
|
|
111
|
+
const TABLES = Object.freeze({
|
|
112
|
+
"node-link": capabilities({
|
|
113
|
+
...COMMON,
|
|
114
|
+
mixedDirection: false,
|
|
115
|
+
edgeIds: "none",
|
|
116
|
+
hierarchy: false,
|
|
117
|
+
graphAttributes: true
|
|
118
|
+
}),
|
|
119
|
+
d3: capabilities({
|
|
120
|
+
...COMMON,
|
|
121
|
+
mixedDirection: false,
|
|
122
|
+
edgeIds: "none",
|
|
123
|
+
hierarchy: false,
|
|
124
|
+
graphAttributes: false
|
|
125
|
+
}),
|
|
126
|
+
jgf: capabilities({
|
|
127
|
+
...COMMON,
|
|
128
|
+
mixedDirection: true,
|
|
129
|
+
edgeIds: "optional",
|
|
130
|
+
hierarchy: false,
|
|
131
|
+
graphAttributes: true
|
|
132
|
+
}),
|
|
133
|
+
cytoscape: capabilities({
|
|
134
|
+
...COMMON,
|
|
135
|
+
positions: true,
|
|
136
|
+
mixedDirection: false,
|
|
137
|
+
edgeIds: "required",
|
|
138
|
+
hierarchy: true,
|
|
139
|
+
graphAttributes: true
|
|
140
|
+
}),
|
|
141
|
+
graphology: capabilities({
|
|
142
|
+
...COMMON,
|
|
143
|
+
mixedDirection: true,
|
|
144
|
+
edgeIds: "optional",
|
|
145
|
+
hierarchy: false,
|
|
146
|
+
graphAttributes: true
|
|
147
|
+
}),
|
|
148
|
+
vis: capabilities({
|
|
149
|
+
...COMMON,
|
|
150
|
+
mixedDirection: false,
|
|
151
|
+
edgeIds: "optional",
|
|
152
|
+
hierarchy: false,
|
|
153
|
+
graphAttributes: false
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
function dialectCapabilities(dialect) {
|
|
157
|
+
return TABLES[dialect];
|
|
158
|
+
}
|
|
159
|
+
const CYTOSCAPE_ELEMENT_KEYS = /* @__PURE__ */ new Set([
|
|
160
|
+
"selected",
|
|
161
|
+
"selectable",
|
|
162
|
+
"locked",
|
|
163
|
+
"grabbable",
|
|
164
|
+
"pannable",
|
|
165
|
+
"removed",
|
|
166
|
+
"scratch",
|
|
167
|
+
"renderedPosition"
|
|
168
|
+
]);
|
|
169
|
+
const CYTOSCAPE_STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["data", "group", "position", "classes"]);
|
|
170
|
+
const SUFFIX = Object.freeze({
|
|
171
|
+
/** A Cytoscape `data` key or a JGF / graphology attribute that collides with a structural column. */
|
|
172
|
+
data: "#data",
|
|
173
|
+
/** An element-level key the dialect does not define (Cytoscape, JGF, graphology). */
|
|
174
|
+
element: "#element"
|
|
175
|
+
});
|
|
176
|
+
const NODE_LINK_SOURCE_KEYS = Object.freeze(["source", "src", "from"]);
|
|
177
|
+
const NODE_LINK_TARGET_KEYS = Object.freeze(["target", "dst", "to"]);
|
|
178
|
+
const POSITION_COLUMN = "position";
|
|
179
|
+
const CLASSES_COLUMN = "classes";
|
|
180
|
+
const PARENT_COLUMN = "parent";
|
|
181
|
+
const JSON_LOSS = Object.freeze({
|
|
182
|
+
/** Non-finite numbers (columns, weights) are written as null. */
|
|
183
|
+
NONFINITE_AS_NULL: "W_NONFINITE_AS_NULL",
|
|
184
|
+
/** Cytoscape, vis and d3 carry no direction; the file re-imports with the dialect's default direction. */
|
|
185
|
+
DIRECTION_DROPPED: "W_DIRECTION_DROPPED",
|
|
186
|
+
/** GEXF mutual pairs are written as two directed edges. */
|
|
187
|
+
MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
|
|
188
|
+
/** JGF keys its nodes by string; numeric ids re-import as text unless ids: "canonical". */
|
|
189
|
+
NUMERIC_IDS_STRINGIFIED: "W_NUMERIC_IDS_STRINGIFIED",
|
|
190
|
+
/** JGF: two ids have the same text; export() throws E_INVALID_ID. */
|
|
191
|
+
ID_TEXT_COLLISION: LOSS.ID_TEXT_COLLISION,
|
|
192
|
+
/** JGF: integer-like id keys are enumerated first and ascending by JSON parsers; node order changes on re-import. */
|
|
193
|
+
NODE_ORDER: "W_NODE_ORDER",
|
|
194
|
+
/** A column named like a reserved key of the dialect (id, source, target, ...) is skipped. */
|
|
195
|
+
RESERVED_KEY: "W_RESERVED_KEY",
|
|
196
|
+
/** A plain edge column named like the weight key reads back as THE weight (or is skipped when weights are written). */
|
|
197
|
+
WEIGHT_KEY_CLASH: LOSS.WEIGHT_KEY_CLASH,
|
|
198
|
+
/** A position column without a slot (every dialect but Cytoscape) is a plain array attribute; the role is lost. */
|
|
199
|
+
POSITIONS_DROPPED: LOSS.POSITIONS,
|
|
200
|
+
/** Cytoscape positions are 2D; non-zero z values are dropped. */
|
|
201
|
+
POSITION_Z_DROPPED: "W_POSITION_Z_DROPPED",
|
|
202
|
+
/** Cytoscape has a single parent; a `parents` list column cannot be written. */
|
|
203
|
+
PARENTS_DROPPED: LOSS.PARENTS,
|
|
204
|
+
/** node-link / d3 have no edge id slot; the id column is written as a plain attribute. */
|
|
205
|
+
EDGE_IDS_DROPPED: LOSS.EDGE_IDS_DROPPED,
|
|
206
|
+
/** An f64 column of integral values reads back as i32 (JSON declares no types). */
|
|
207
|
+
INTEGRAL_F64_AS_I32: LOSS.INTEGRAL_F64,
|
|
208
|
+
/** A role column the dialect has no slot for is a plain attribute; the role is lost. */
|
|
209
|
+
ROLE_DROPPED: LOSS.ROLE,
|
|
210
|
+
/** A column without a set cell is not written (JSON declares no columns). */
|
|
211
|
+
EMPTY_COLUMN_DROPPED: LOSS.EMPTY_COLUMN
|
|
212
|
+
});
|
|
213
|
+
const SLOT_ROLES = Object.freeze({
|
|
214
|
+
"node-link": /* @__PURE__ */ new Set(),
|
|
215
|
+
d3: /* @__PURE__ */ new Set(),
|
|
216
|
+
jgf: /* @__PURE__ */ new Set(["label", "kind"]),
|
|
217
|
+
cytoscape: /* @__PURE__ */ new Set(["classes"]),
|
|
218
|
+
graphology: /* @__PURE__ */ new Set(),
|
|
219
|
+
vis: /* @__PURE__ */ new Set()
|
|
220
|
+
});
|
|
221
|
+
const STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
222
|
+
"directed",
|
|
223
|
+
"pair",
|
|
224
|
+
"mutual",
|
|
225
|
+
"weight",
|
|
226
|
+
"timeText",
|
|
227
|
+
"originalId"
|
|
228
|
+
]);
|
|
229
|
+
const TEMPORAL_ROLES = /* @__PURE__ */ new Set(["start", "end", "timestamp", "timestamps", "spells", "open"]);
|
|
230
|
+
const NESTED_DIALECTS = /* @__PURE__ */ new Set(["jgf", "cytoscape", "graphology"]);
|
|
231
|
+
function resolve(snapshot, options) {
|
|
232
|
+
const o = options ?? {};
|
|
233
|
+
const common = resolveExportOptions(options);
|
|
234
|
+
const shape = shapeMetaOf(snapshot.meta);
|
|
235
|
+
let dialect;
|
|
236
|
+
if (o.dialect === void 0) {
|
|
237
|
+
dialect = shape.dialect ?? "node-link";
|
|
238
|
+
} else if (isJsonDialect(o.dialect)) {
|
|
239
|
+
({ dialect } = o);
|
|
240
|
+
} else {
|
|
241
|
+
throw unsupported("dialect", o.dialect, JSON_DIALECTS);
|
|
242
|
+
}
|
|
243
|
+
const indent = o.indent ?? 0;
|
|
244
|
+
if (!Number.isInteger(indent) || indent < 0 || indent > 16) {
|
|
245
|
+
throw unsupported("indent", indent, ["an integer 0..16"]);
|
|
246
|
+
}
|
|
247
|
+
const sameDialect = shape.dialect === dialect;
|
|
248
|
+
const recorded = (value, fallback) => sameDialect && value !== void 0 ? value : fallback;
|
|
249
|
+
const edgesKey = keyOption$1("edgesKey", o.edgesKey) ?? recorded(shape.edgesKey, dialect === "d3" ? "links" : "edges");
|
|
250
|
+
let nodeIdKey;
|
|
251
|
+
if (o.nodeIdKey !== void 0) {
|
|
252
|
+
nodeIdKey = keyOption$1("nodeIdKey", o.nodeIdKey);
|
|
253
|
+
} else if (sameDialect && shape.nodeIdKey !== void 0) {
|
|
254
|
+
({ nodeIdKey } = shape);
|
|
255
|
+
} else {
|
|
256
|
+
nodeIdKey = "id";
|
|
257
|
+
}
|
|
258
|
+
let indexLinks = o.indexLinks ?? recorded(shape.indexLinks, false);
|
|
259
|
+
if (typeof indexLinks !== "boolean") {
|
|
260
|
+
throw unsupported("indexLinks", indexLinks, ["true", "false"]);
|
|
261
|
+
}
|
|
262
|
+
if (nodeIdKey === null) {
|
|
263
|
+
indexLinks = true;
|
|
264
|
+
}
|
|
265
|
+
const sourceKey = keyOption$1("sourceKey", o.sourceKey) ?? recorded(shape.sourceKey, dialect === "vis" ? "from" : "source");
|
|
266
|
+
const targetKey = keyOption$1("targetKey", o.targetKey) ?? recorded(shape.targetKey, dialect === "vis" ? "to" : "target");
|
|
267
|
+
const { weightOrigin } = snapshot.meta;
|
|
268
|
+
const weightKey = keyOption$1("weightKey", o.weightKey) ?? (weightOrigin !== null && weightOrigin.format === "json" && weightOrigin.id !== null ? weightOrigin.id : "weight");
|
|
269
|
+
return { common, dialect, indent, edgesKey, nodeIdKey, indexLinks, sourceKey, targetKey, weightKey, shape };
|
|
270
|
+
}
|
|
271
|
+
function keyOption$1(name, value) {
|
|
272
|
+
if (value === void 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
276
|
+
throw unsupported(name, value, ["a non-empty key"]);
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
function unsupported(name, found, supported) {
|
|
281
|
+
const shown = typeof found === "string" ? JSON.stringify(found) : String(found);
|
|
282
|
+
return new GraphFormatError("E_UNSUPPORTED", `option ${name}: ${shown} is not one of ${supported.join(", ")}`, {
|
|
283
|
+
option: name,
|
|
284
|
+
found: typeof found === "string" || typeof found === "number" ? found : typeof found,
|
|
285
|
+
supported: [...supported]
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function numberText(value, f32, nonfinite) {
|
|
289
|
+
if (!Number.isFinite(value)) {
|
|
290
|
+
nonfinite.count++;
|
|
291
|
+
return "null";
|
|
292
|
+
}
|
|
293
|
+
if (f32) {
|
|
294
|
+
return formatF32(value);
|
|
295
|
+
}
|
|
296
|
+
return Object.is(value, -0) ? "0" : String(value);
|
|
297
|
+
}
|
|
298
|
+
function valueText(value, f32, nonfinite) {
|
|
299
|
+
switch (typeof value) {
|
|
300
|
+
case "number":
|
|
301
|
+
return numberText(value, f32, nonfinite);
|
|
302
|
+
case "boolean":
|
|
303
|
+
return value ? "true" : "false";
|
|
304
|
+
case "string":
|
|
305
|
+
return JSON.stringify(value);
|
|
306
|
+
case "undefined":
|
|
307
|
+
return "null";
|
|
308
|
+
}
|
|
309
|
+
if (value === null) {
|
|
310
|
+
return "null";
|
|
311
|
+
}
|
|
312
|
+
if (Array.isArray(value) || ArrayBuffer.isView(value)) {
|
|
313
|
+
const items = Array.from(value, (item) => valueText(item, f32, nonfinite));
|
|
314
|
+
return `[${items.join(",")}]`;
|
|
315
|
+
}
|
|
316
|
+
const text = JSON.stringify(value);
|
|
317
|
+
return text === void 0 ? "null" : text;
|
|
318
|
+
}
|
|
319
|
+
function cellText(column, row, ids, nonfinite) {
|
|
320
|
+
switch (column.dtype) {
|
|
321
|
+
case "list":
|
|
322
|
+
return valueText(column.sliceOf(row), column.child.dtype === "f32", nonfinite);
|
|
323
|
+
case "json":
|
|
324
|
+
return valueText(column.values[row], false, nonfinite);
|
|
325
|
+
case "u32":
|
|
326
|
+
if (column.meta.refersTo === "node" && column.meta.components === 1) {
|
|
327
|
+
const index = column.data[row];
|
|
328
|
+
return index === INVALID_INDEX ? "null" : JSON.stringify(ids.idOf(index));
|
|
329
|
+
}
|
|
330
|
+
return valueText(column.value(row), false, nonfinite);
|
|
331
|
+
case "f32":
|
|
332
|
+
return valueText(column.value(row), true, nonfinite);
|
|
333
|
+
default:
|
|
334
|
+
return valueText(column.value(row), false, nonfinite);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function countNonFinite(column) {
|
|
338
|
+
let data;
|
|
339
|
+
switch (column.dtype) {
|
|
340
|
+
case "f32":
|
|
341
|
+
case "f64":
|
|
342
|
+
({ data } = column);
|
|
343
|
+
break;
|
|
344
|
+
case "list":
|
|
345
|
+
if (column.child.dtype !== "f32" && column.child.dtype !== "f64") {
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
({ data } = column.child);
|
|
349
|
+
break;
|
|
350
|
+
default:
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
let count = 0;
|
|
354
|
+
for (let i = 0; i < data.length; i++) {
|
|
355
|
+
if (!Number.isFinite(data[i])) {
|
|
356
|
+
count++;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return count;
|
|
360
|
+
}
|
|
361
|
+
class JsonWriter {
|
|
362
|
+
/**
|
|
363
|
+
* Create a writer.
|
|
364
|
+
* @param indent - spaces per level; 0 for compact output
|
|
365
|
+
*/
|
|
366
|
+
constructor(indent) {
|
|
367
|
+
this.parts = [];
|
|
368
|
+
this.depth = 0;
|
|
369
|
+
this.firstAtDepth = [];
|
|
370
|
+
this.indent = indent;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Open an object or an array.
|
|
374
|
+
* @param bracket - "{" or "["
|
|
375
|
+
*/
|
|
376
|
+
open(bracket) {
|
|
377
|
+
this.parts.push(bracket);
|
|
378
|
+
this.depth++;
|
|
379
|
+
this.firstAtDepth.push(true);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Close the innermost object or array.
|
|
383
|
+
* @param bracket - "}" or "]"
|
|
384
|
+
*/
|
|
385
|
+
close(bracket) {
|
|
386
|
+
const wasEmpty = this.firstAtDepth.pop() ?? true;
|
|
387
|
+
this.depth--;
|
|
388
|
+
if (!wasEmpty) {
|
|
389
|
+
this.parts.push(this.newline());
|
|
390
|
+
}
|
|
391
|
+
this.parts.push(bracket);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Start an object member: the separator and the quoted key.
|
|
395
|
+
* @param name - the key
|
|
396
|
+
*/
|
|
397
|
+
key(name) {
|
|
398
|
+
this.separator();
|
|
399
|
+
this.parts.push(JSON.stringify(name), this.indent > 0 ? ": " : ":");
|
|
400
|
+
}
|
|
401
|
+
/** Start an array element: the separator only. */
|
|
402
|
+
item() {
|
|
403
|
+
this.separator();
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Append raw JSON text (a value).
|
|
407
|
+
* @param text - the text
|
|
408
|
+
*/
|
|
409
|
+
raw(text) {
|
|
410
|
+
this.parts.push(text);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Write a member with a value in one call.
|
|
414
|
+
* @param name - the key
|
|
415
|
+
* @param text - the value's JSON text
|
|
416
|
+
*/
|
|
417
|
+
member(name, text) {
|
|
418
|
+
this.key(name);
|
|
419
|
+
this.parts.push(text);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Hand out the text accumulated since the last take.
|
|
423
|
+
* @returns the text
|
|
424
|
+
*/
|
|
425
|
+
take() {
|
|
426
|
+
const text = this.parts.length === 1 ? this.parts[0] : this.parts.join("");
|
|
427
|
+
this.parts = [];
|
|
428
|
+
return text;
|
|
429
|
+
}
|
|
430
|
+
/** The comma and newline before a member or element. */
|
|
431
|
+
separator() {
|
|
432
|
+
const top = this.firstAtDepth.length - 1;
|
|
433
|
+
if (this.firstAtDepth[top]) {
|
|
434
|
+
this.firstAtDepth[top] = false;
|
|
435
|
+
} else {
|
|
436
|
+
this.parts.push(",");
|
|
437
|
+
}
|
|
438
|
+
this.parts.push(this.newline());
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* A newline plus the current indentation, or nothing in compact mode.
|
|
442
|
+
* @returns the text
|
|
443
|
+
*/
|
|
444
|
+
newline() {
|
|
445
|
+
return this.indent > 0 ? `
|
|
446
|
+
${" ".repeat(this.depth * this.indent)}` : "";
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function plan(snapshot, resolved) {
|
|
450
|
+
const { dialect } = resolved;
|
|
451
|
+
const caps = dialectCapabilities(dialect);
|
|
452
|
+
const notes = checkCapabilities(snapshot, caps, resolved.common, {
|
|
453
|
+
roles: SLOT_ROLES[dialect],
|
|
454
|
+
positionDtype: "f32"
|
|
455
|
+
}).filter((n) => n.code !== LOSS.POSITIONS && n.code !== LOSS.EDGE_IDS_DROPPED);
|
|
456
|
+
const note = (code, message, column = null, count = null) => {
|
|
457
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
458
|
+
};
|
|
459
|
+
const ctx = {
|
|
460
|
+
snapshot,
|
|
461
|
+
resolved,
|
|
462
|
+
dialect,
|
|
463
|
+
caps,
|
|
464
|
+
notes,
|
|
465
|
+
note,
|
|
466
|
+
reserved: reservedKeys(snapshot, resolved),
|
|
467
|
+
nonfinite: { count: 0 }
|
|
468
|
+
};
|
|
469
|
+
const nodes = planTable(ctx, snapshot.nodes, "node");
|
|
470
|
+
const edges = planTable(ctx, snapshot.edges, "edge");
|
|
471
|
+
const graph = caps.graphAttributes ? planTable(ctx, snapshot.graph, "graph") : [];
|
|
472
|
+
withdrawSlotNotes(notes, [...nodes, ...edges]);
|
|
473
|
+
const weights = planWeights(ctx, edges);
|
|
474
|
+
if (ctx.nonfinite.count > 0) {
|
|
475
|
+
note(
|
|
476
|
+
JSON_LOSS.NONFINITE_AS_NULL,
|
|
477
|
+
`${ctx.nonfinite.count} non-finite number(s) are written as null`,
|
|
478
|
+
null,
|
|
479
|
+
ctx.nonfinite.count
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
const { folding, directed } = planDirection(ctx);
|
|
483
|
+
const multigraph = snapshot.flags.multigraph || snapshot.meta.declaredMultigraph === true;
|
|
484
|
+
if (dialect === "jgf") {
|
|
485
|
+
jgfIdNotes(snapshot, note);
|
|
486
|
+
}
|
|
487
|
+
const edgeIds = snapshot.edges.byRole("id");
|
|
488
|
+
if (dialect === "cytoscape" && edgeIds !== null && edgeIds.nullCount > 0) {
|
|
489
|
+
note(
|
|
490
|
+
LOSS.EDGE_IDS_GENERATED,
|
|
491
|
+
`${edgeIds.nullCount} edge(s) have no id; canonical e<index> ids are generated for them`,
|
|
492
|
+
edgeIds.meta.name,
|
|
493
|
+
edgeIds.nullCount
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return { resolved, notes, nodes, edges, graph, edgeIds, weights, folding, directed, multigraph };
|
|
497
|
+
}
|
|
498
|
+
function withdrawSlotNotes(notes, plans) {
|
|
499
|
+
for (const plan2 of plans) {
|
|
500
|
+
if (plan2.slot !== "classes") {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const at = notes.findIndex((n) => n.code === LOSS.LIST && n.column === plan2.column.meta.name);
|
|
504
|
+
if (at >= 0) {
|
|
505
|
+
notes.splice(at, 1);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function reservedKeys(snapshot, resolved) {
|
|
510
|
+
const { dialect } = resolved;
|
|
511
|
+
const node = /* @__PURE__ */ new Set();
|
|
512
|
+
const edge = new Set(snapshot.flags.weighted ? [resolved.weightKey] : []);
|
|
513
|
+
const graph = /* @__PURE__ */ new Set();
|
|
514
|
+
switch (dialect) {
|
|
515
|
+
case "node-link":
|
|
516
|
+
case "d3":
|
|
517
|
+
if (resolved.nodeIdKey !== null) {
|
|
518
|
+
node.add(resolved.nodeIdKey);
|
|
519
|
+
}
|
|
520
|
+
edge.add(resolved.sourceKey).add(resolved.targetKey);
|
|
521
|
+
break;
|
|
522
|
+
case "vis":
|
|
523
|
+
node.add(resolved.nodeIdKey ?? "id");
|
|
524
|
+
edge.add(resolved.sourceKey).add(resolved.targetKey);
|
|
525
|
+
break;
|
|
526
|
+
case "cytoscape":
|
|
527
|
+
node.add("id").add("parent");
|
|
528
|
+
edge.add("id").add("source").add("target");
|
|
529
|
+
break;
|
|
530
|
+
case "jgf":
|
|
531
|
+
case "graphology":
|
|
532
|
+
break;
|
|
533
|
+
default: {
|
|
534
|
+
const name = dialect;
|
|
535
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, { option: "dialect", found: name });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return { node, edge, graph };
|
|
539
|
+
}
|
|
540
|
+
function planTable(ctx, table, domain) {
|
|
541
|
+
const { dialect, note } = ctx;
|
|
542
|
+
const plans = [];
|
|
543
|
+
const reserved = ctx.reserved[domain];
|
|
544
|
+
const used = /* @__PURE__ */ new Set();
|
|
545
|
+
for (const column of table) {
|
|
546
|
+
const { meta } = column;
|
|
547
|
+
const { role } = meta;
|
|
548
|
+
const setRows = column.length - column.nullCount;
|
|
549
|
+
if (role !== null && (STRUCTURAL_ROLES.has(role) || TEMPORAL_ROLES.has(role))) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (domain !== "graph") {
|
|
553
|
+
ctx.nonfinite.count += countNonFinite(column);
|
|
554
|
+
}
|
|
555
|
+
const slotted = planSlot(ctx, column, domain);
|
|
556
|
+
if (slotted !== null) {
|
|
557
|
+
if (slotted !== SKIPPED) {
|
|
558
|
+
plans.push(slotted);
|
|
559
|
+
}
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
let key = meta.name;
|
|
563
|
+
let slot = "attribute";
|
|
564
|
+
if (NESTED_DIALECTS.has(dialect) && domain !== "graph") {
|
|
565
|
+
if (key.endsWith(SUFFIX.element)) {
|
|
566
|
+
key = key.slice(0, -SUFFIX.element.length);
|
|
567
|
+
slot = "element";
|
|
568
|
+
} else if (dialect === "cytoscape" && CYTOSCAPE_ELEMENT_KEYS.has(key)) {
|
|
569
|
+
slot = "element";
|
|
570
|
+
} else if (key.endsWith(SUFFIX.data)) {
|
|
571
|
+
key = key.slice(0, -SUFFIX.data.length);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (slot === "attribute" && reserved.has(key)) {
|
|
575
|
+
note(
|
|
576
|
+
JSON_LOSS.RESERVED_KEY,
|
|
577
|
+
`${domain} column "${meta.name}" cannot be written: "${key}" is a reserved key of ${dialect}`,
|
|
578
|
+
meta.name,
|
|
579
|
+
setRows
|
|
580
|
+
);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const slotKey = `${slot}:${key}`;
|
|
584
|
+
if (used.has(slotKey)) {
|
|
585
|
+
note(
|
|
586
|
+
JSON_LOSS.RESERVED_KEY,
|
|
587
|
+
`${domain} column "${meta.name}" cannot be written: key "${key}" is already used by another column`,
|
|
588
|
+
meta.name,
|
|
589
|
+
setRows
|
|
590
|
+
);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
used.add(slotKey);
|
|
594
|
+
if (setRows === 0 && domain !== "graph") {
|
|
595
|
+
note(
|
|
596
|
+
LOSS.EMPTY_COLUMN,
|
|
597
|
+
`${domain} column "${meta.name}" has no set cell and is not written: JSON writes values, never declarations`,
|
|
598
|
+
meta.name,
|
|
599
|
+
0
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
inferenceNotes(ctx, column, domain, setRows);
|
|
603
|
+
plans.push({ column, key, slot });
|
|
604
|
+
}
|
|
605
|
+
return plans;
|
|
606
|
+
}
|
|
607
|
+
const SKIPPED = /* @__PURE__ */ Symbol("skipped");
|
|
608
|
+
function planSlot(ctx, column, domain) {
|
|
609
|
+
const { dialect, note } = ctx;
|
|
610
|
+
const { meta } = column;
|
|
611
|
+
const { role } = meta;
|
|
612
|
+
const setRows = column.length - column.nullCount;
|
|
613
|
+
if (role === "parent" || role === "parents") {
|
|
614
|
+
if (dialect === "cytoscape" && domain === "node") {
|
|
615
|
+
if (role === "parent" && column.dtype === "u32" && meta.refersTo === "node") {
|
|
616
|
+
return { column, key: "parent", slot: "parent" };
|
|
617
|
+
}
|
|
618
|
+
note(
|
|
619
|
+
LOSS.PARENTS,
|
|
620
|
+
`node column "${meta.name}" (${role}) cannot be written: Cytoscape has a single parent`,
|
|
621
|
+
meta.name,
|
|
622
|
+
setRows
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
return SKIPPED;
|
|
626
|
+
}
|
|
627
|
+
if (role === "position" && domain === "node") {
|
|
628
|
+
if (dialect === "cytoscape" && (column.dtype === "f32" || column.dtype === "f64") && meta.components >= 2) {
|
|
629
|
+
const z = countNonZeroZ(column);
|
|
630
|
+
if (z > 0) {
|
|
631
|
+
note(
|
|
632
|
+
JSON_LOSS.POSITION_Z_DROPPED,
|
|
633
|
+
`${z} position(s) have a non-zero z; Cytoscape positions are 2D`,
|
|
634
|
+
meta.name,
|
|
635
|
+
z
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return { column, key: "position", slot: "cytoscapePosition" };
|
|
639
|
+
}
|
|
640
|
+
note(
|
|
641
|
+
LOSS.POSITIONS,
|
|
642
|
+
`node column "${meta.name}" (position) is written as a plain array attribute; its role is lost on re-import`,
|
|
643
|
+
meta.name,
|
|
644
|
+
setRows
|
|
645
|
+
);
|
|
646
|
+
return { column, key: meta.name, slot: "position" };
|
|
647
|
+
}
|
|
648
|
+
if (role === "id" && domain === "edge") {
|
|
649
|
+
if (dialect === "node-link" || dialect === "d3") {
|
|
650
|
+
note(
|
|
651
|
+
LOSS.EDGE_IDS_DROPPED,
|
|
652
|
+
`edge column "${meta.name}" (id) is written as a plain attribute; ${dialect} has no edge id`,
|
|
653
|
+
meta.name,
|
|
654
|
+
setRows
|
|
655
|
+
);
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
return SKIPPED;
|
|
659
|
+
}
|
|
660
|
+
if (dialect === "cytoscape" && role === "classes" && column.dtype === "list" && column.child.dtype === "string") {
|
|
661
|
+
return { column, key: "classes", slot: "classes" };
|
|
662
|
+
}
|
|
663
|
+
if (dialect === "jgf" && role === "label" && (column.dtype === "string" || column.dtype === "dict")) {
|
|
664
|
+
return { column, key: "label", slot: "label" };
|
|
665
|
+
}
|
|
666
|
+
if (dialect === "jgf" && role === "kind" && domain === "edge" && (column.dtype === "string" || column.dtype === "dict")) {
|
|
667
|
+
return { column, key: "relation", slot: "relation" };
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
function inferenceNotes(ctx, column, domain, setRows) {
|
|
672
|
+
if (column.dtype !== "f64" || column.meta.components > 1 || setRows === 0) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const { data } = column;
|
|
676
|
+
for (let r = 0; r < column.length; r++) {
|
|
677
|
+
if (column.isSet(r) && !Number.isInteger(data[r])) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
ctx.note(
|
|
682
|
+
LOSS.INTEGRAL_F64,
|
|
683
|
+
`${domain} column "${column.meta.name}" is f64 with integral values only; JSON declares no types and it reads back as i32`,
|
|
684
|
+
column.meta.name,
|
|
685
|
+
setRows
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
function planWeights(ctx, edges) {
|
|
689
|
+
const { snapshot, nonfinite } = ctx;
|
|
690
|
+
const weights = explicitWeights(snapshot);
|
|
691
|
+
if (!weights.weighted) {
|
|
692
|
+
const clash = edges.find((p) => p.slot === "attribute" && p.key === ctx.resolved.weightKey);
|
|
693
|
+
if (clash !== void 0) {
|
|
694
|
+
ctx.note(
|
|
695
|
+
LOSS.WEIGHT_KEY_CLASH,
|
|
696
|
+
`edge column "${clash.column.meta.name}" is written under "${clash.key}", the key the importer reads THE weight from; it reads back as the weight, not as a column`,
|
|
697
|
+
clash.column.meta.name,
|
|
698
|
+
clash.column.length - clash.column.nullCount
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
return () => null;
|
|
702
|
+
}
|
|
703
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
704
|
+
if (weights.isExplicit(e) && !Number.isFinite(weights.value(e))) {
|
|
705
|
+
nonfinite.count++;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return (e) => {
|
|
709
|
+
if (!weights.isExplicit(e)) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return Number.isFinite(weights.value(e)) ? weights.text(e) : "null";
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function planDirection(ctx) {
|
|
716
|
+
const { snapshot, dialect, caps, note } = ctx;
|
|
717
|
+
const folding = pairFolding(snapshot);
|
|
718
|
+
if (folding.mutualCount > 0) {
|
|
719
|
+
note(
|
|
720
|
+
LOSS.MUTUAL_EXPANDED,
|
|
721
|
+
`${folding.mutualCount} mutual pair(s) are written as two directed edges; the mutual mark is lost`,
|
|
722
|
+
null,
|
|
723
|
+
folding.mutualCount
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
let { directed } = snapshot;
|
|
727
|
+
if (!caps.mixedDirection && countMixedEdges(snapshot) > 0 && ctx.resolved.common.onMixedDirection === "undirected") {
|
|
728
|
+
directed = false;
|
|
729
|
+
}
|
|
730
|
+
if (dialect === "cytoscape" || dialect === "vis" || dialect === "d3") {
|
|
731
|
+
const assumed = DIALECT_DEFAULT_DIRECTED[dialect];
|
|
732
|
+
if (directed !== assumed) {
|
|
733
|
+
note(
|
|
734
|
+
JSON_LOSS.DIRECTION_DROPPED,
|
|
735
|
+
`${dialect} has no direction flag; the ${directed ? "directed" : "undirected"} graph re-imports as ${assumed ? "directed" : "undirected"} unless defaultDirected is passed`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return { folding, directed };
|
|
740
|
+
}
|
|
741
|
+
function countNonZeroZ(column) {
|
|
742
|
+
if (column.dtype !== "f32" && column.dtype !== "f64" || column.meta.components < 3) {
|
|
743
|
+
return 0;
|
|
744
|
+
}
|
|
745
|
+
const { components } = column.meta;
|
|
746
|
+
let count = 0;
|
|
747
|
+
for (let r = 0; r < column.length; r++) {
|
|
748
|
+
if (column.isSet(r) && column.data[r * components + 2] !== 0) {
|
|
749
|
+
count++;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return count;
|
|
753
|
+
}
|
|
754
|
+
function jgfIdNotes(snapshot, note) {
|
|
755
|
+
const { ids } = snapshot;
|
|
756
|
+
const seen = /* @__PURE__ */ new Set();
|
|
757
|
+
let numeric = 0;
|
|
758
|
+
let collisions = 0;
|
|
759
|
+
let indexLike = 0;
|
|
760
|
+
let lastIndex = -1;
|
|
761
|
+
let ordered = true;
|
|
762
|
+
let sawOther = false;
|
|
763
|
+
for (let i = 0; i < ids.size; i++) {
|
|
764
|
+
const id = ids.idOf(i);
|
|
765
|
+
const text = String(id);
|
|
766
|
+
if (typeof id === "number") {
|
|
767
|
+
numeric++;
|
|
768
|
+
}
|
|
769
|
+
if (seen.has(text)) {
|
|
770
|
+
collisions++;
|
|
771
|
+
}
|
|
772
|
+
seen.add(text);
|
|
773
|
+
if (isArrayIndexKey(text)) {
|
|
774
|
+
indexLike++;
|
|
775
|
+
const n = Number(text);
|
|
776
|
+
if (sawOther || n <= lastIndex) {
|
|
777
|
+
ordered = false;
|
|
778
|
+
}
|
|
779
|
+
lastIndex = n;
|
|
780
|
+
} else {
|
|
781
|
+
sawOther = true;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (numeric > 0) {
|
|
785
|
+
note(
|
|
786
|
+
JSON_LOSS.NUMERIC_IDS_STRINGIFIED,
|
|
787
|
+
`${numeric} numeric node id(s) become JGF object keys (strings); pass ids: "canonical" on re-import`,
|
|
788
|
+
null,
|
|
789
|
+
numeric
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
if (collisions > 0) {
|
|
793
|
+
note(
|
|
794
|
+
JSON_LOSS.ID_TEXT_COLLISION,
|
|
795
|
+
`${collisions} node id(s) share their text with another id; export() will throw`,
|
|
796
|
+
null,
|
|
797
|
+
collisions
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
if (!ordered) {
|
|
801
|
+
note(
|
|
802
|
+
JSON_LOSS.NODE_ORDER,
|
|
803
|
+
`${indexLike} integer-like node id(s) are enumerated first and ascending by JSON parsers; node order changes on re-import`,
|
|
804
|
+
null,
|
|
805
|
+
indexLike
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function isArrayIndexKey(text) {
|
|
810
|
+
return /^(0|[1-9][0-9]*)$/.test(text) && Number(text) < 4294967295;
|
|
811
|
+
}
|
|
812
|
+
function* write(snapshot, p) {
|
|
813
|
+
const { dialect } = p.resolved;
|
|
814
|
+
if (p.notes.some((n) => n.code === LOSS.MIXED_DIRECTION_ERROR)) {
|
|
815
|
+
throw new GraphFormatError(
|
|
816
|
+
"E_DIRECTED",
|
|
817
|
+
`${dialect} has no mixed direction; pass onMixedDirection "directed" or "undirected"`,
|
|
818
|
+
{ reason: "mixed direction", dialect }
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
if (p.notes.some((n) => n.code === JSON_LOSS.ID_TEXT_COLLISION)) {
|
|
822
|
+
throw new GraphFormatError("E_INVALID_ID", "two node ids have the same text; JGF keys nodes by text", {
|
|
823
|
+
reason: "collision"
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
switch (dialect) {
|
|
827
|
+
case "node-link":
|
|
828
|
+
case "d3":
|
|
829
|
+
yield* writeNodeLink(snapshot, p);
|
|
830
|
+
return;
|
|
831
|
+
case "vis":
|
|
832
|
+
yield* writeVis(snapshot, p);
|
|
833
|
+
return;
|
|
834
|
+
case "graphology":
|
|
835
|
+
yield* writeGraphology(snapshot, p);
|
|
836
|
+
return;
|
|
837
|
+
case "jgf":
|
|
838
|
+
yield* writeJgf(snapshot, p);
|
|
839
|
+
return;
|
|
840
|
+
case "cytoscape":
|
|
841
|
+
yield* writeCytoscape(snapshot, p);
|
|
842
|
+
return;
|
|
843
|
+
default: {
|
|
844
|
+
const name = dialect;
|
|
845
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, { option: "dialect", found: name });
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function writeSlots(c, plans, row, slots) {
|
|
850
|
+
for (const item of plans) {
|
|
851
|
+
if (!slots.has(item.slot) || !item.column.isSet(row)) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
let text;
|
|
855
|
+
switch (item.slot) {
|
|
856
|
+
case "position": {
|
|
857
|
+
const value = item.column.value(row);
|
|
858
|
+
const dims = dimsOf(item.column);
|
|
859
|
+
const values = Array.from(value).slice(0, dims);
|
|
860
|
+
text = valueText(values, item.column.dtype === "f32", c.nonfinite);
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
case "classes":
|
|
864
|
+
text = JSON.stringify(item.column.sliceOf(row).join(" "));
|
|
865
|
+
break;
|
|
866
|
+
default:
|
|
867
|
+
text = cellText(item.column, row, c.ids, c.nonfinite);
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
c.w.member(item.key, text);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const ATTRIBUTE_SLOTS = /* @__PURE__ */ new Set(["attribute", "position"]);
|
|
874
|
+
const ELEMENT_SLOTS = /* @__PURE__ */ new Set(["element", "classes"]);
|
|
875
|
+
function dimsOf(column) {
|
|
876
|
+
const dims = column.meta.extra.sourceDims;
|
|
877
|
+
if (typeof dims === "number" && dims >= 1 && dims <= column.meta.components) {
|
|
878
|
+
return dims;
|
|
879
|
+
}
|
|
880
|
+
return column.meta.components;
|
|
881
|
+
}
|
|
882
|
+
function anySet(plans, row, slots) {
|
|
883
|
+
return plans.some((item) => slots.has(item.slot) && item.column.isSet(row));
|
|
884
|
+
}
|
|
885
|
+
function idText(id) {
|
|
886
|
+
return typeof id === "number" && Object.is(id, -0) ? "0" : JSON.stringify(id);
|
|
887
|
+
}
|
|
888
|
+
function edgeIdText(p, e, c) {
|
|
889
|
+
const column = p.edgeIds;
|
|
890
|
+
if (column === null || !column.isSet(e)) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
return cellText(column, e, c.ids, c.nonfinite);
|
|
894
|
+
}
|
|
895
|
+
function edgeDisposition(p, e) {
|
|
896
|
+
const { folding } = p;
|
|
897
|
+
if (folding.folded(e)) {
|
|
898
|
+
return "skip";
|
|
899
|
+
}
|
|
900
|
+
if (!p.directed) {
|
|
901
|
+
return "undirected";
|
|
902
|
+
}
|
|
903
|
+
if (folding.sourceDirected(e)) {
|
|
904
|
+
return "directed";
|
|
905
|
+
}
|
|
906
|
+
if (dialectCapabilities(p.resolved.dialect).mixedDirection) {
|
|
907
|
+
return "undirected";
|
|
908
|
+
}
|
|
909
|
+
return p.resolved.common.onMixedDirection === "directed" ? "directed" : "undirected";
|
|
910
|
+
}
|
|
911
|
+
function writeGraphObject(c, p) {
|
|
912
|
+
c.w.open("{");
|
|
913
|
+
for (const item of p.graph) {
|
|
914
|
+
if (item.column.isSet(0)) {
|
|
915
|
+
c.w.member(item.key, cellText(item.column, 0, c.ids, c.nonfinite));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
c.w.close("}");
|
|
919
|
+
}
|
|
920
|
+
function endpoints(snapshot, p, e) {
|
|
921
|
+
const list = snapshot.edgeList();
|
|
922
|
+
const u = list.src[e];
|
|
923
|
+
const v = list.dst[e];
|
|
924
|
+
if (p.resolved.indexLinks) {
|
|
925
|
+
return [String(u), String(v)];
|
|
926
|
+
}
|
|
927
|
+
return [idText(snapshot.ids.idOf(u)), idText(snapshot.ids.idOf(v))];
|
|
928
|
+
}
|
|
929
|
+
function* writeNodeLink(snapshot, p) {
|
|
930
|
+
const { resolved } = p;
|
|
931
|
+
const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
|
|
932
|
+
const { w } = c;
|
|
933
|
+
w.open("{");
|
|
934
|
+
if (resolved.dialect !== "d3") {
|
|
935
|
+
w.member("directed", p.directed ? "true" : "false");
|
|
936
|
+
w.member("multigraph", p.multigraph ? "true" : "false");
|
|
937
|
+
w.key("graph");
|
|
938
|
+
writeGraphObject(c, p);
|
|
939
|
+
}
|
|
940
|
+
w.key("nodes");
|
|
941
|
+
w.open("[");
|
|
942
|
+
yield w.take();
|
|
943
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
944
|
+
w.item();
|
|
945
|
+
w.open("{");
|
|
946
|
+
if (resolved.nodeIdKey !== null) {
|
|
947
|
+
w.member(resolved.nodeIdKey, idText(snapshot.ids.idOf(i)));
|
|
948
|
+
}
|
|
949
|
+
writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
|
|
950
|
+
w.close("}");
|
|
951
|
+
yield w.take();
|
|
952
|
+
}
|
|
953
|
+
w.close("]");
|
|
954
|
+
w.key(resolved.edgesKey);
|
|
955
|
+
w.open("[");
|
|
956
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
957
|
+
const disposition = edgeDisposition(p, e);
|
|
958
|
+
if (disposition === "skip") {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const [s, t] = endpoints(snapshot, p, e);
|
|
962
|
+
w.item();
|
|
963
|
+
w.open("{");
|
|
964
|
+
w.member(resolved.sourceKey, s);
|
|
965
|
+
w.member(resolved.targetKey, t);
|
|
966
|
+
const weight = p.weights(e);
|
|
967
|
+
if (weight !== null) {
|
|
968
|
+
w.member(resolved.weightKey, weight);
|
|
969
|
+
}
|
|
970
|
+
writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
|
|
971
|
+
w.close("}");
|
|
972
|
+
yield w.take();
|
|
973
|
+
}
|
|
974
|
+
w.close("]");
|
|
975
|
+
w.close("}");
|
|
976
|
+
yield w.take();
|
|
977
|
+
}
|
|
978
|
+
function* writeVis(snapshot, p) {
|
|
979
|
+
const { resolved } = p;
|
|
980
|
+
const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
|
|
981
|
+
const { w } = c;
|
|
982
|
+
const nodeIdKey = resolved.nodeIdKey ?? "id";
|
|
983
|
+
w.open("{");
|
|
984
|
+
w.key("nodes");
|
|
985
|
+
w.open("[");
|
|
986
|
+
yield w.take();
|
|
987
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
988
|
+
w.item();
|
|
989
|
+
w.open("{");
|
|
990
|
+
w.member(nodeIdKey, idText(snapshot.ids.idOf(i)));
|
|
991
|
+
writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
|
|
992
|
+
w.close("}");
|
|
993
|
+
yield w.take();
|
|
994
|
+
}
|
|
995
|
+
w.close("]");
|
|
996
|
+
w.key("edges");
|
|
997
|
+
w.open("[");
|
|
998
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
999
|
+
if (edgeDisposition(p, e) === "skip") {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const list = snapshot.edgeList();
|
|
1003
|
+
w.item();
|
|
1004
|
+
w.open("{");
|
|
1005
|
+
const id = edgeIdText(p, e, c);
|
|
1006
|
+
if (id !== null) {
|
|
1007
|
+
w.member("id", id);
|
|
1008
|
+
}
|
|
1009
|
+
w.member(resolved.sourceKey, idText(snapshot.ids.idOf(list.src[e])));
|
|
1010
|
+
w.member(resolved.targetKey, idText(snapshot.ids.idOf(list.dst[e])));
|
|
1011
|
+
const weight = p.weights(e);
|
|
1012
|
+
if (weight !== null) {
|
|
1013
|
+
w.member(resolved.weightKey, weight);
|
|
1014
|
+
}
|
|
1015
|
+
writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
|
|
1016
|
+
w.close("}");
|
|
1017
|
+
yield w.take();
|
|
1018
|
+
}
|
|
1019
|
+
w.close("]");
|
|
1020
|
+
w.close("}");
|
|
1021
|
+
yield w.take();
|
|
1022
|
+
}
|
|
1023
|
+
function* writeGraphology(snapshot, p) {
|
|
1024
|
+
const { resolved } = p;
|
|
1025
|
+
const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
|
|
1026
|
+
const { w } = c;
|
|
1027
|
+
let type;
|
|
1028
|
+
if (!p.directed) {
|
|
1029
|
+
type = "undirected";
|
|
1030
|
+
} else {
|
|
1031
|
+
type = countMixedEdges(snapshot) > 0 ? "mixed" : "directed";
|
|
1032
|
+
}
|
|
1033
|
+
w.open("{");
|
|
1034
|
+
w.key("attributes");
|
|
1035
|
+
writeGraphObject(c, p);
|
|
1036
|
+
w.key("options");
|
|
1037
|
+
w.open("{");
|
|
1038
|
+
w.member("type", JSON.stringify(type));
|
|
1039
|
+
if (snapshot.meta.declaredMultigraph !== null || p.multigraph) {
|
|
1040
|
+
w.member("multi", p.multigraph ? "true" : "false");
|
|
1041
|
+
}
|
|
1042
|
+
if (resolved.shape.allowSelfLoops !== void 0) {
|
|
1043
|
+
w.member("allowSelfLoops", !resolved.shape.allowSelfLoops && snapshot.selfLoopCount === 0 ? "false" : "true");
|
|
1044
|
+
}
|
|
1045
|
+
w.close("}");
|
|
1046
|
+
w.key("nodes");
|
|
1047
|
+
w.open("[");
|
|
1048
|
+
yield w.take();
|
|
1049
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1050
|
+
w.item();
|
|
1051
|
+
w.open("{");
|
|
1052
|
+
w.member("key", idText(snapshot.ids.idOf(i)));
|
|
1053
|
+
if (anySet(p.nodes, i, ATTRIBUTE_SLOTS)) {
|
|
1054
|
+
w.key("attributes");
|
|
1055
|
+
w.open("{");
|
|
1056
|
+
writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
|
|
1057
|
+
w.close("}");
|
|
1058
|
+
}
|
|
1059
|
+
writeSlots(c, p.nodes, i, ELEMENT_SLOTS);
|
|
1060
|
+
w.close("}");
|
|
1061
|
+
yield w.take();
|
|
1062
|
+
}
|
|
1063
|
+
w.close("]");
|
|
1064
|
+
w.key("edges");
|
|
1065
|
+
w.open("[");
|
|
1066
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1067
|
+
const disposition = edgeDisposition(p, e);
|
|
1068
|
+
if (disposition === "skip") {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
const list = snapshot.edgeList();
|
|
1072
|
+
w.item();
|
|
1073
|
+
w.open("{");
|
|
1074
|
+
const id = edgeIdText(p, e, c);
|
|
1075
|
+
if (id !== null) {
|
|
1076
|
+
w.member("key", id);
|
|
1077
|
+
}
|
|
1078
|
+
w.member("source", idText(snapshot.ids.idOf(list.src[e])));
|
|
1079
|
+
w.member("target", idText(snapshot.ids.idOf(list.dst[e])));
|
|
1080
|
+
if (type === "mixed" && disposition === "undirected") {
|
|
1081
|
+
w.member("undirected", "true");
|
|
1082
|
+
}
|
|
1083
|
+
const weight = p.weights(e);
|
|
1084
|
+
if (weight !== null || anySet(p.edges, e, ATTRIBUTE_SLOTS)) {
|
|
1085
|
+
w.key("attributes");
|
|
1086
|
+
w.open("{");
|
|
1087
|
+
if (weight !== null) {
|
|
1088
|
+
w.member(resolved.weightKey, weight);
|
|
1089
|
+
}
|
|
1090
|
+
writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
|
|
1091
|
+
w.close("}");
|
|
1092
|
+
}
|
|
1093
|
+
writeSlots(c, p.edges, e, ELEMENT_SLOTS);
|
|
1094
|
+
w.close("}");
|
|
1095
|
+
yield w.take();
|
|
1096
|
+
}
|
|
1097
|
+
w.close("]");
|
|
1098
|
+
w.close("}");
|
|
1099
|
+
yield w.take();
|
|
1100
|
+
}
|
|
1101
|
+
function* writeJgf(snapshot, p) {
|
|
1102
|
+
const { resolved } = p;
|
|
1103
|
+
const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
|
|
1104
|
+
const { w } = c;
|
|
1105
|
+
const labelSlots = /* @__PURE__ */ new Set(["label"]);
|
|
1106
|
+
const relationSlots = /* @__PURE__ */ new Set(["relation"]);
|
|
1107
|
+
w.open("{");
|
|
1108
|
+
w.key("graph");
|
|
1109
|
+
w.open("{");
|
|
1110
|
+
if (resolved.shape.id !== void 0) {
|
|
1111
|
+
w.member("id", JSON.stringify(resolved.shape.id));
|
|
1112
|
+
}
|
|
1113
|
+
if (snapshot.meta.name !== null) {
|
|
1114
|
+
w.member("label", JSON.stringify(snapshot.meta.name));
|
|
1115
|
+
}
|
|
1116
|
+
if (resolved.shape.type !== void 0) {
|
|
1117
|
+
w.member("type", JSON.stringify(resolved.shape.type));
|
|
1118
|
+
}
|
|
1119
|
+
w.member("directed", p.directed ? "true" : "false");
|
|
1120
|
+
if (p.graph.some((item) => item.column.isSet(0))) {
|
|
1121
|
+
w.key("metadata");
|
|
1122
|
+
writeGraphObject(c, p);
|
|
1123
|
+
}
|
|
1124
|
+
w.key("nodes");
|
|
1125
|
+
w.open("{");
|
|
1126
|
+
yield w.take();
|
|
1127
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1128
|
+
w.key(String(snapshot.ids.idOf(i)));
|
|
1129
|
+
w.open("{");
|
|
1130
|
+
writeSlots(c, p.nodes, i, labelSlots);
|
|
1131
|
+
if (anySet(p.nodes, i, ATTRIBUTE_SLOTS)) {
|
|
1132
|
+
w.key("metadata");
|
|
1133
|
+
w.open("{");
|
|
1134
|
+
writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
|
|
1135
|
+
w.close("}");
|
|
1136
|
+
}
|
|
1137
|
+
writeSlots(c, p.nodes, i, ELEMENT_SLOTS);
|
|
1138
|
+
w.close("}");
|
|
1139
|
+
yield w.take();
|
|
1140
|
+
}
|
|
1141
|
+
w.close("}");
|
|
1142
|
+
w.key("edges");
|
|
1143
|
+
w.open("[");
|
|
1144
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1145
|
+
const disposition = edgeDisposition(p, e);
|
|
1146
|
+
if (disposition === "skip") {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
const list = snapshot.edgeList();
|
|
1150
|
+
w.item();
|
|
1151
|
+
w.open("{");
|
|
1152
|
+
const id = edgeIdText(p, e, c);
|
|
1153
|
+
if (id !== null) {
|
|
1154
|
+
w.member("id", id);
|
|
1155
|
+
}
|
|
1156
|
+
w.member("source", JSON.stringify(String(snapshot.ids.idOf(list.src[e]))));
|
|
1157
|
+
w.member("target", JSON.stringify(String(snapshot.ids.idOf(list.dst[e]))));
|
|
1158
|
+
writeSlots(c, p.edges, e, relationSlots);
|
|
1159
|
+
if (p.directed && disposition === "undirected") {
|
|
1160
|
+
w.member("directed", "false");
|
|
1161
|
+
}
|
|
1162
|
+
writeSlots(c, p.edges, e, labelSlots);
|
|
1163
|
+
const weight = p.weights(e);
|
|
1164
|
+
if (weight !== null || anySet(p.edges, e, ATTRIBUTE_SLOTS)) {
|
|
1165
|
+
w.key("metadata");
|
|
1166
|
+
w.open("{");
|
|
1167
|
+
if (weight !== null) {
|
|
1168
|
+
w.member(resolved.weightKey, weight);
|
|
1169
|
+
}
|
|
1170
|
+
writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
|
|
1171
|
+
w.close("}");
|
|
1172
|
+
}
|
|
1173
|
+
writeSlots(c, p.edges, e, ELEMENT_SLOTS);
|
|
1174
|
+
w.close("}");
|
|
1175
|
+
yield w.take();
|
|
1176
|
+
}
|
|
1177
|
+
w.close("]");
|
|
1178
|
+
w.close("}");
|
|
1179
|
+
w.close("}");
|
|
1180
|
+
yield w.take();
|
|
1181
|
+
}
|
|
1182
|
+
function* writeCytoscape(snapshot, p) {
|
|
1183
|
+
const { resolved } = p;
|
|
1184
|
+
const c = { w: new JsonWriter(resolved.indent), ids: snapshot.ids, nonfinite: { count: 0 } };
|
|
1185
|
+
const { w } = c;
|
|
1186
|
+
const parentSlots = /* @__PURE__ */ new Set(["parent"]);
|
|
1187
|
+
const positionSlots = /* @__PURE__ */ new Set(["cytoscapePosition"]);
|
|
1188
|
+
w.open("{");
|
|
1189
|
+
w.key("elements");
|
|
1190
|
+
w.open("{");
|
|
1191
|
+
w.key("nodes");
|
|
1192
|
+
w.open("[");
|
|
1193
|
+
yield w.take();
|
|
1194
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1195
|
+
w.item();
|
|
1196
|
+
w.open("{");
|
|
1197
|
+
w.key("data");
|
|
1198
|
+
w.open("{");
|
|
1199
|
+
w.member("id", idText(snapshot.ids.idOf(i)));
|
|
1200
|
+
writeSlots(c, p.nodes, i, parentSlots);
|
|
1201
|
+
writeSlots(c, p.nodes, i, ATTRIBUTE_SLOTS);
|
|
1202
|
+
w.close("}");
|
|
1203
|
+
for (const item of p.nodes) {
|
|
1204
|
+
if (positionSlots.has(item.slot) && item.column.isSet(i)) {
|
|
1205
|
+
const value = item.column.value(i);
|
|
1206
|
+
const f32 = item.column.dtype === "f32";
|
|
1207
|
+
w.key("position");
|
|
1208
|
+
w.open("{");
|
|
1209
|
+
w.member("x", numberText(value[0], f32, c.nonfinite));
|
|
1210
|
+
w.member("y", numberText(value[1], f32, c.nonfinite));
|
|
1211
|
+
w.close("}");
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
writeSlots(c, p.nodes, i, ELEMENT_SLOTS);
|
|
1215
|
+
w.close("}");
|
|
1216
|
+
yield w.take();
|
|
1217
|
+
}
|
|
1218
|
+
w.close("]");
|
|
1219
|
+
w.key("edges");
|
|
1220
|
+
w.open("[");
|
|
1221
|
+
const generated = edgeIdGenerator(snapshot, p);
|
|
1222
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1223
|
+
if (edgeDisposition(p, e) === "skip") {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const list = snapshot.edgeList();
|
|
1227
|
+
w.item();
|
|
1228
|
+
w.open("{");
|
|
1229
|
+
w.key("data");
|
|
1230
|
+
w.open("{");
|
|
1231
|
+
w.member("id", edgeIdText(p, e, c) ?? generated(e));
|
|
1232
|
+
w.member("source", idText(snapshot.ids.idOf(list.src[e])));
|
|
1233
|
+
w.member("target", idText(snapshot.ids.idOf(list.dst[e])));
|
|
1234
|
+
const weight = p.weights(e);
|
|
1235
|
+
if (weight !== null) {
|
|
1236
|
+
w.member(resolved.weightKey, weight);
|
|
1237
|
+
}
|
|
1238
|
+
writeSlots(c, p.edges, e, ATTRIBUTE_SLOTS);
|
|
1239
|
+
w.close("}");
|
|
1240
|
+
writeSlots(c, p.edges, e, ELEMENT_SLOTS);
|
|
1241
|
+
w.close("}");
|
|
1242
|
+
yield w.take();
|
|
1243
|
+
}
|
|
1244
|
+
w.close("]");
|
|
1245
|
+
w.close("}");
|
|
1246
|
+
if (p.graph.some((item) => item.column.isSet(0))) {
|
|
1247
|
+
w.key("data");
|
|
1248
|
+
writeGraphObject(c, p);
|
|
1249
|
+
}
|
|
1250
|
+
const extra = resolved.shape.cytoscape;
|
|
1251
|
+
if (extra !== void 0) {
|
|
1252
|
+
for (const key of Object.keys(extra)) {
|
|
1253
|
+
if (key !== "elements" && key !== "data") {
|
|
1254
|
+
w.member(key, valueText(extra[key], false, c.nonfinite));
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
w.close("}");
|
|
1259
|
+
yield w.take();
|
|
1260
|
+
}
|
|
1261
|
+
function edgeIdGenerator(snapshot, p) {
|
|
1262
|
+
const used = /* @__PURE__ */ new Set();
|
|
1263
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1264
|
+
used.add(String(snapshot.ids.idOf(i)));
|
|
1265
|
+
}
|
|
1266
|
+
const column = p.edgeIds;
|
|
1267
|
+
if (column !== null) {
|
|
1268
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1269
|
+
if (column.isSet(e)) {
|
|
1270
|
+
used.add(String(column.value(e)));
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return (e) => {
|
|
1275
|
+
let candidate = `e${e}`;
|
|
1276
|
+
for (let k = 2; used.has(candidate); k++) {
|
|
1277
|
+
candidate = `e${e}_${k}`;
|
|
1278
|
+
}
|
|
1279
|
+
used.add(candidate);
|
|
1280
|
+
return JSON.stringify(candidate);
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
const jsonExporter = Object.freeze({
|
|
1284
|
+
format: "json",
|
|
1285
|
+
capabilities: dialectCapabilities("node-link"),
|
|
1286
|
+
/**
|
|
1287
|
+
* Pre-flight: every loss of the selected dialect, without writing anything.
|
|
1288
|
+
* @param snapshot - the snapshot
|
|
1289
|
+
* @param options - format-specific and common options
|
|
1290
|
+
* @returns the notes, empty when the export is exact
|
|
1291
|
+
*/
|
|
1292
|
+
check(snapshot, options) {
|
|
1293
|
+
return Object.freeze([...plan(snapshot, resolve(snapshot, options)).notes]);
|
|
1294
|
+
},
|
|
1295
|
+
/**
|
|
1296
|
+
* Write the document as UTF-8 chunks.
|
|
1297
|
+
* @param snapshot - the snapshot
|
|
1298
|
+
* @param options - format-specific and common options
|
|
1299
|
+
* @returns the chunks
|
|
1300
|
+
*/
|
|
1301
|
+
export(snapshot, options) {
|
|
1302
|
+
return encodeChunks(write(snapshot, plan(snapshot, resolve(snapshot, options))));
|
|
1303
|
+
},
|
|
1304
|
+
/**
|
|
1305
|
+
* Write the document as one string.
|
|
1306
|
+
* @param snapshot - the snapshot
|
|
1307
|
+
* @param options - format-specific and common options
|
|
1308
|
+
* @returns the document
|
|
1309
|
+
*/
|
|
1310
|
+
exportToString(snapshot, options) {
|
|
1311
|
+
return joinText(write(snapshot, plan(snapshot, resolve(snapshot, options))));
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
function jsonCapabilities(dialect) {
|
|
1315
|
+
return dialectCapabilities(dialect);
|
|
1316
|
+
}
|
|
1317
|
+
const JSON_ISSUE = Object.freeze({
|
|
1318
|
+
/** The text is empty or whitespace (fatal). */
|
|
1319
|
+
EMPTY_INPUT: EMPTY_INPUT_CODE,
|
|
1320
|
+
/** JSON.parse refused the text (fatal). */
|
|
1321
|
+
SYNTAX: SYNTAX_CODE,
|
|
1322
|
+
/** No dialect matches the document's top-level shape. */
|
|
1323
|
+
DIALECT: "E_JSON_DIALECT",
|
|
1324
|
+
/** A section (nodes, edges, elements, graph) has the wrong JSON type. */
|
|
1325
|
+
SHAPE: "E_JSON_SHAPE",
|
|
1326
|
+
/** A node-link document lacks its nodes or its edges array, or a Cytoscape document its elements. */
|
|
1327
|
+
MISSING_SECTION: "E_MISSING_SECTION",
|
|
1328
|
+
/** A node record or an element is not an object. */
|
|
1329
|
+
BAD_ELEMENT: "E_BAD_ELEMENT",
|
|
1330
|
+
/** A node record has no id. */
|
|
1331
|
+
MISSING_ID: MISSING_ID_CODE,
|
|
1332
|
+
/** A node id is a JSON boolean or null (legal in NetworkX, not a NodeId); coerced only under ids "string". */
|
|
1333
|
+
UNSUPPORTED_ID: "E_UNSUPPORTED_ID",
|
|
1334
|
+
/** An edge record has no source or no target. */
|
|
1335
|
+
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
1336
|
+
/** An index endpoint is not an integer below the node count, or names a skipped node. */
|
|
1337
|
+
BAD_INDEX: "E_BAD_INDEX",
|
|
1338
|
+
/** A declared field has the wrong JSON type (JGF label / relation / metadata, Cytoscape position / classes). */
|
|
1339
|
+
BAD_VALUE: "E_BAD_VALUE",
|
|
1340
|
+
/** A graph-level flag (`directed`, `multigraph`, graphology `options`) has the wrong type; the default is used. */
|
|
1341
|
+
BAD_FLAG: "W_BAD_FLAG",
|
|
1342
|
+
/** A Cytoscape `data.parent` names an unknown node. */
|
|
1343
|
+
UNKNOWN_PARENT: UNKNOWN_PARENT_CODE,
|
|
1344
|
+
/** A node id repeated by a later record; the records are merged (the later attributes win). */
|
|
1345
|
+
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
1346
|
+
/** An edge id (Cytoscape data.id, graphology key, vis id) repeated by a later edge; the edge is skipped. */
|
|
1347
|
+
DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
|
|
1348
|
+
/** Two distinct id texts merged into one number under ids "number". */
|
|
1349
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
1350
|
+
/** Edge ids of mixed JSON types were stored as text. */
|
|
1351
|
+
EDGE_ID_STRINGIFIED: "W_EDGE_ID_STRINGIFIED",
|
|
1352
|
+
/** A JGF `graphs` array holds more than one graph; only `graphIndex` is read. */
|
|
1353
|
+
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
1354
|
+
/** JGF hyperedges under the "error" policy. */
|
|
1355
|
+
HYPEREDGE: HYPEREDGE_CODE,
|
|
1356
|
+
/** JGF hyperedges skipped under the default "skip" policy. */
|
|
1357
|
+
HYPEREDGES_SKIPPED: "W_HYPEREDGES_SKIPPED",
|
|
1358
|
+
/** A JGF hyperedge with neither a nodes array nor source / target arrays. */
|
|
1359
|
+
HYPEREDGE_SHAPE: "E_HYPEREDGE_SHAPE",
|
|
1360
|
+
/** The nodes have no id key at all; array positions became the ids. */
|
|
1361
|
+
POSITIONAL_NODES: "W_POSITIONAL_NODES",
|
|
1362
|
+
/** A builder-policy option (addMissingNodes, duplicateEdges, selfLoops, weightDtype) differs from the sink's (the shared W_SINK_OPTION). */
|
|
1363
|
+
SINK_OPTION: SINK_OPTION_CODE,
|
|
1364
|
+
/** A common option the dialect has no use for (nodeIdFrom outside node-link, long, restoreMangledIds). */
|
|
1365
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE
|
|
1366
|
+
});
|
|
1367
|
+
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
1368
|
+
"ids",
|
|
1369
|
+
"nodeIdFrom",
|
|
1370
|
+
"addMissingNodes",
|
|
1371
|
+
"duplicateEdges",
|
|
1372
|
+
"selfLoops",
|
|
1373
|
+
"onMixedDirection",
|
|
1374
|
+
"defaultDirected",
|
|
1375
|
+
"weightFrom",
|
|
1376
|
+
"weightDtype",
|
|
1377
|
+
"hyperedges",
|
|
1378
|
+
"errorLimit",
|
|
1379
|
+
"signal",
|
|
1380
|
+
"onProgress"
|
|
1381
|
+
]);
|
|
1382
|
+
const FORMAT_DEFAULTS = { ids: "keep", defaultDirected: false, weightFrom: "weight" };
|
|
1383
|
+
const SNIFF_BYTES = 4096;
|
|
1384
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
1385
|
+
const SNIFF_KEYS = ['"nodes"', '"links"', '"edges"', '"elements"', '"graph"', '"graphs"'];
|
|
1386
|
+
const BOM = String.fromCharCode(65279);
|
|
1387
|
+
const JGF_EDGE_KEYS = /* @__PURE__ */ new Set([
|
|
1388
|
+
"id",
|
|
1389
|
+
"source",
|
|
1390
|
+
"target",
|
|
1391
|
+
"relation",
|
|
1392
|
+
"directed",
|
|
1393
|
+
"label",
|
|
1394
|
+
"metadata"
|
|
1395
|
+
]);
|
|
1396
|
+
const JGF_HYPEREDGE_KEYS = /* @__PURE__ */ new Set([...JGF_EDGE_KEYS, "nodes"]);
|
|
1397
|
+
const JGF_NODE_KEYS = /* @__PURE__ */ new Set(["label", "metadata"]);
|
|
1398
|
+
const GRAPHOLOGY_NODE_KEYS = /* @__PURE__ */ new Set(["key", "attributes"]);
|
|
1399
|
+
const GRAPHOLOGY_EDGE_KEYS = /* @__PURE__ */ new Set(["key", "source", "target", "attributes", "undirected"]);
|
|
1400
|
+
const VIS_SOURCE_KEYS = Object.freeze(["from"]);
|
|
1401
|
+
const VIS_TARGET_KEYS = Object.freeze(["to"]);
|
|
1402
|
+
function resolveJsonOptions(options) {
|
|
1403
|
+
const o = options ?? {};
|
|
1404
|
+
const dialect = o.dialect ?? "auto";
|
|
1405
|
+
if (dialect !== "auto" && !isJsonDialect(dialect)) {
|
|
1406
|
+
throw unsupportedOption("dialect", dialect, [...JSON_DIALECTS, "auto"]);
|
|
1407
|
+
}
|
|
1408
|
+
const indexLinks = o.indexLinks ?? "auto";
|
|
1409
|
+
if (indexLinks !== "auto" && typeof indexLinks !== "boolean") {
|
|
1410
|
+
throw unsupportedOption("indexLinks", indexLinks, ["true", "false", "auto"]);
|
|
1411
|
+
}
|
|
1412
|
+
const graphIndex = o.graphIndex ?? 0;
|
|
1413
|
+
if (!Number.isInteger(graphIndex) || graphIndex < 0) {
|
|
1414
|
+
throw unsupportedOption("graphIndex", graphIndex, ["a non-negative integer"]);
|
|
1415
|
+
}
|
|
1416
|
+
return {
|
|
1417
|
+
dialect,
|
|
1418
|
+
nodeIdKey: keyOption("nodeIdKey", o.nodeIdKey),
|
|
1419
|
+
edgesKey: keyOption("edgesKey", o.edgesKey),
|
|
1420
|
+
sourceKey: keyOption("sourceKey", o.sourceKey),
|
|
1421
|
+
targetKey: keyOption("targetKey", o.targetKey),
|
|
1422
|
+
indexLinks,
|
|
1423
|
+
graphIndex
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
function keyOption(name, value) {
|
|
1427
|
+
if (value === void 0) {
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1431
|
+
throw unsupportedOption(name, value, ["a non-empty key"]);
|
|
1432
|
+
}
|
|
1433
|
+
return value;
|
|
1434
|
+
}
|
|
1435
|
+
function unsupportedOption(name, found, supported) {
|
|
1436
|
+
return new GraphFormatError(
|
|
1437
|
+
"E_UNSUPPORTED",
|
|
1438
|
+
`option ${name}: ${describe(found)} is not one of ${supported.join(", ")}`,
|
|
1439
|
+
{
|
|
1440
|
+
option: name,
|
|
1441
|
+
found: typeof found === "string" ? found : typeof found,
|
|
1442
|
+
supported: [...supported]
|
|
1443
|
+
}
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
function describe(value) {
|
|
1447
|
+
if (value === null) {
|
|
1448
|
+
return "null";
|
|
1449
|
+
}
|
|
1450
|
+
if (Array.isArray(value)) {
|
|
1451
|
+
return "array";
|
|
1452
|
+
}
|
|
1453
|
+
switch (typeof value) {
|
|
1454
|
+
case "string":
|
|
1455
|
+
case "number":
|
|
1456
|
+
case "boolean":
|
|
1457
|
+
return JSON.stringify(value);
|
|
1458
|
+
default:
|
|
1459
|
+
return typeof value;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
class AttributeWriter {
|
|
1463
|
+
/**
|
|
1464
|
+
* Create a writer.
|
|
1465
|
+
* @param sink - the sink
|
|
1466
|
+
* @param domain - node or edge
|
|
1467
|
+
*/
|
|
1468
|
+
constructor(sink, domain) {
|
|
1469
|
+
this.handles = /* @__PURE__ */ new Map();
|
|
1470
|
+
this.reservedNames = /* @__PURE__ */ new Set();
|
|
1471
|
+
this.sink = sink;
|
|
1472
|
+
this.domain = domain;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Declare a structural column up front; attribute keys with its name are suffixed from now on.
|
|
1476
|
+
* @param decl - the declaration
|
|
1477
|
+
* @returns the handle
|
|
1478
|
+
*/
|
|
1479
|
+
declare(decl) {
|
|
1480
|
+
this.reservedNames.add(decl.name);
|
|
1481
|
+
const handle = this.domain === "node" ? this.sink.declareNodeColumn(decl) : this.sink.declareEdgeColumn(decl);
|
|
1482
|
+
this.handles.set(decl.name, handle);
|
|
1483
|
+
return handle;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Declare a structural column only when the file uses it.
|
|
1487
|
+
* @param decl - the declaration
|
|
1488
|
+
* @param present - whether any element carries the field
|
|
1489
|
+
* @returns the handle, or INVALID_INDEX when not declared
|
|
1490
|
+
*/
|
|
1491
|
+
declareIf(decl, present) {
|
|
1492
|
+
return present ? this.declare(decl) : INVALID_INDEX;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Whether a name is taken in the sink's table (for the deterministic rename rule).
|
|
1496
|
+
* @param name - the column name
|
|
1497
|
+
* @returns true when a column of that name exists
|
|
1498
|
+
*/
|
|
1499
|
+
taken(name) {
|
|
1500
|
+
return this.lookup(name) !== INVALID_INDEX;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Write one attribute cell by its source key; null and undefined leave the row unset.
|
|
1504
|
+
* @param row - the node or edge index
|
|
1505
|
+
* @param key - the source key
|
|
1506
|
+
* @param value - the JSON value
|
|
1507
|
+
* @param suffix - the suffix applied when the key collides with a structural column
|
|
1508
|
+
*/
|
|
1509
|
+
write(row, key, value, suffix) {
|
|
1510
|
+
if (value === void 0 || value === null) {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const name = this.reservedNames.has(key) ? `${key}${suffix}` : key;
|
|
1514
|
+
const cached = this.handles.get(name);
|
|
1515
|
+
if (cached !== void 0) {
|
|
1516
|
+
this.set(cached, row, value);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
this.set(name, row, value);
|
|
1520
|
+
const handle = this.lookup(name);
|
|
1521
|
+
if (handle !== INVALID_INDEX) {
|
|
1522
|
+
this.handles.set(name, handle);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Write through a handle or a name.
|
|
1527
|
+
* @param column - the handle or name
|
|
1528
|
+
* @param row - the row
|
|
1529
|
+
* @param value - the value
|
|
1530
|
+
*/
|
|
1531
|
+
set(column, row, value) {
|
|
1532
|
+
if (this.domain === "node") {
|
|
1533
|
+
this.sink.setNodeValue(column, row, value);
|
|
1534
|
+
} else {
|
|
1535
|
+
this.sink.setEdgeValue(column, row, value);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Look a column up by name.
|
|
1540
|
+
* @param name - the column name
|
|
1541
|
+
* @returns the handle, or INVALID_INDEX
|
|
1542
|
+
*/
|
|
1543
|
+
lookup(name) {
|
|
1544
|
+
return this.domain === "node" ? this.sink.nodeColumn(name) : this.sink.edgeColumn(name);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
class ImportContext {
|
|
1548
|
+
/**
|
|
1549
|
+
* Create the context.
|
|
1550
|
+
* @param sink - the sink
|
|
1551
|
+
* @param report - the report
|
|
1552
|
+
* @param options - the resolved common options
|
|
1553
|
+
* @param json - the resolved format options
|
|
1554
|
+
* @param explicitDefaultDirected - whether the caller passed defaultDirected
|
|
1555
|
+
*/
|
|
1556
|
+
constructor(sink, report, options, json, explicitDefaultDirected) {
|
|
1557
|
+
this.elementsSinceCheck = 0;
|
|
1558
|
+
this.fileDirected = false;
|
|
1559
|
+
this.sink = sink;
|
|
1560
|
+
this.report = report;
|
|
1561
|
+
this.options = options;
|
|
1562
|
+
this.json = json;
|
|
1563
|
+
this.ids = new IdCoercer(options.ids);
|
|
1564
|
+
this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
1565
|
+
this.nodes = new AttributeWriter(sink, "node");
|
|
1566
|
+
this.edges = new AttributeWriter(sink, "edge");
|
|
1567
|
+
this.explicitDefaultDirected = explicitDefaultDirected;
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Report a `nodeIdFrom` other than "id" for a dialect whose ids are unambiguous.
|
|
1571
|
+
* @param dialect - the dialect
|
|
1572
|
+
* @param idField - where the dialect's ids come from, for the message
|
|
1573
|
+
*/
|
|
1574
|
+
reportNodeIdFrom(dialect, idField) {
|
|
1575
|
+
if (this.options.nodeIdFrom !== "id") {
|
|
1576
|
+
this.report.warning(
|
|
1577
|
+
"unsupported",
|
|
1578
|
+
JSON_ISSUE.OPTION_IGNORED,
|
|
1579
|
+
`nodeIdFrom "${this.options.nodeIdFrom}" does not apply to ${dialect}; ids are read from ${idField}`,
|
|
1580
|
+
{ element: "nodeIdFrom" }
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* The direction assumed when the file declares none: the caller's `defaultDirected` when given,
|
|
1586
|
+
* else the dialect's convention.
|
|
1587
|
+
* @param dialect - the dialect
|
|
1588
|
+
* @returns the direction
|
|
1589
|
+
*/
|
|
1590
|
+
defaultDirected(dialect) {
|
|
1591
|
+
return this.explicitDefaultDirected ? this.options.defaultDirected : DIALECT_DEFAULT_DIRECTED[dialect];
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Rule 1 of design section 8.4: set the sink's direction from the file's header (or the
|
|
1595
|
+
* dialect's default) before the first edge, remembering the file's direction for uniformKind().
|
|
1596
|
+
* @param directed - the file's direction
|
|
1597
|
+
*/
|
|
1598
|
+
setHeader(directed) {
|
|
1599
|
+
this.fileDirected = directed;
|
|
1600
|
+
this.direction.setHeader(directed);
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* The edge kind every edge of a dialect without per-edge direction has: the file's direction
|
|
1604
|
+
* (the resolver expands it when the sink's direction differs).
|
|
1605
|
+
* @returns the kind
|
|
1606
|
+
*/
|
|
1607
|
+
uniformKind() {
|
|
1608
|
+
return this.fileDirected ? "directed" : "undirected";
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Coerce a node id value per the `ids` option, reporting what cannot be an id. A JSON boolean or
|
|
1612
|
+
* null is `unsupported` unless `ids` is "string" (design section 8.5); anything the rule rejects
|
|
1613
|
+
* is recorded through the per-element catch.
|
|
1614
|
+
* @param raw - the JSON value; undefined when the record has no id key
|
|
1615
|
+
* @param element - the element name for the issue
|
|
1616
|
+
* @returns the id, or null when the value was reported and the element must be skipped
|
|
1617
|
+
*/
|
|
1618
|
+
coerceId(raw, element) {
|
|
1619
|
+
if (raw === void 0) {
|
|
1620
|
+
this.report.error("missing-value", JSON_ISSUE.MISSING_ID, `${element} has no id`, { element });
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
if ((typeof raw === "boolean" || raw === null) && this.options.ids !== "string") {
|
|
1624
|
+
const kind = raw === null ? "null" : "boolean";
|
|
1625
|
+
this.report.error(
|
|
1626
|
+
"unsupported",
|
|
1627
|
+
JSON_ISSUE.UNSUPPORTED_ID,
|
|
1628
|
+
`${element}: a JSON ${kind} is not a node id (pass ids: "string" to coerce it)`,
|
|
1629
|
+
{ element }
|
|
1630
|
+
);
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
let id;
|
|
1634
|
+
try {
|
|
1635
|
+
id = this.ids.value(this.options.ids === "string" && typeof raw === "number" ? String(raw) : raw);
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
this.report.recordError(err, { element });
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
const merge = this.ids.lastMerge;
|
|
1641
|
+
if (merge !== null) {
|
|
1642
|
+
this.report.warning(
|
|
1643
|
+
"coercion",
|
|
1644
|
+
JSON_ISSUE.ID_MERGED,
|
|
1645
|
+
`id text ${JSON.stringify(merge.text)} merged with ${JSON.stringify(merge.previousText)} as ${merge.id}`,
|
|
1646
|
+
{ element }
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
return id;
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Coerce an id that must be valid for the caller to proceed (hyperedge members): the rejections
|
|
1653
|
+
* of coerceId() are thrown instead of recorded.
|
|
1654
|
+
* @param raw - the JSON value
|
|
1655
|
+
* @param element - the element name
|
|
1656
|
+
* @returns the id
|
|
1657
|
+
*/
|
|
1658
|
+
requireId(raw, element) {
|
|
1659
|
+
if ((typeof raw === "boolean" || raw === null) && this.options.ids !== "string") {
|
|
1660
|
+
const kind = raw === null ? "null" : "boolean";
|
|
1661
|
+
throw new GraphFormatError("E_INVALID_ID", `${element}: a JSON ${kind} is not a node id`, {
|
|
1662
|
+
reason: "unsupported id"
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
return this.ids.value(this.options.ids === "string" && typeof raw === "number" ? String(raw) : raw);
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Add a node, counting it or recording the failure.
|
|
1669
|
+
* @param id - the node id
|
|
1670
|
+
* @param element - the element name
|
|
1671
|
+
* @returns the node index, or -1 when the sink refused the node
|
|
1672
|
+
*/
|
|
1673
|
+
pushNode(id, element) {
|
|
1674
|
+
this.checkAbort();
|
|
1675
|
+
const existing = this.sink.indexOf(id);
|
|
1676
|
+
if (existing !== INVALID_INDEX) {
|
|
1677
|
+
this.report.warning(
|
|
1678
|
+
"merged",
|
|
1679
|
+
JSON_ISSUE.DUPLICATE_NODE,
|
|
1680
|
+
`${element}: node ${JSON.stringify(id)} already exists; its attributes are merged (the later values win)`,
|
|
1681
|
+
{ element }
|
|
1682
|
+
);
|
|
1683
|
+
return existing;
|
|
1684
|
+
}
|
|
1685
|
+
let index;
|
|
1686
|
+
try {
|
|
1687
|
+
index = this.sink.addNode(id);
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
this.skip(err, "node", element);
|
|
1690
|
+
return -1;
|
|
1691
|
+
}
|
|
1692
|
+
this.report.counts.nodes++;
|
|
1693
|
+
return index;
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Push one edge through the direction resolver, counting every logical edge the sink gained
|
|
1697
|
+
* (both halves of an expanded edge, and the mirrors of an in-place expansion).
|
|
1698
|
+
* @param source - the source id
|
|
1699
|
+
* @param target - the target id
|
|
1700
|
+
* @param kind - the edge's direction in the file
|
|
1701
|
+
* @param weight - the weight, or undefined
|
|
1702
|
+
* @param element - the element name for issues
|
|
1703
|
+
* @returns the primary edge index
|
|
1704
|
+
*/
|
|
1705
|
+
pushEdge(source, target, kind, weight, element) {
|
|
1706
|
+
this.checkAbort();
|
|
1707
|
+
const before = this.sink.edgeCount;
|
|
1708
|
+
let created = this.sink.indexOf(source) === INVALID_INDEX ? 1 : 0;
|
|
1709
|
+
if (source !== target && this.sink.indexOf(target) === INVALID_INDEX) {
|
|
1710
|
+
created++;
|
|
1711
|
+
}
|
|
1712
|
+
const edge = this.direction.addEdge(source, target, kind, weight, { element });
|
|
1713
|
+
this.report.counts.edges += this.sink.edgeCount - before;
|
|
1714
|
+
this.report.counts.nodes += created;
|
|
1715
|
+
return edge;
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Check the cancellation signal every ABORT_CHECK_INTERVAL pushed elements, so an abort raised
|
|
1719
|
+
* while the whole in-memory document is being walked rejects promptly.
|
|
1720
|
+
*/
|
|
1721
|
+
checkAbort() {
|
|
1722
|
+
if (++this.elementsSinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
1723
|
+
this.elementsSinceCheck = 0;
|
|
1724
|
+
throwIfAborted(this.options.signal);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Record a per-element failure and count the skipped element.
|
|
1729
|
+
* @param err - the thrown value
|
|
1730
|
+
* @param domain - which counter to bump
|
|
1731
|
+
* @param element - the element name
|
|
1732
|
+
*/
|
|
1733
|
+
skip(err, domain, element) {
|
|
1734
|
+
this.report.recordError(err, { element });
|
|
1735
|
+
if (domain === "node") {
|
|
1736
|
+
this.report.counts.skippedNodes++;
|
|
1737
|
+
} else {
|
|
1738
|
+
this.report.counts.skippedEdges++;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Report an element that is not an object and count it as skipped.
|
|
1743
|
+
* @param domain - node or edge
|
|
1744
|
+
* @param element - the element name
|
|
1745
|
+
* @param what - what was expected
|
|
1746
|
+
*/
|
|
1747
|
+
badElement(domain, element, what = "an object") {
|
|
1748
|
+
this.report.error("validation-error", JSON_ISSUE.BAD_ELEMENT, `${element} is not ${what}`, { element });
|
|
1749
|
+
this.countSkipped(domain);
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Count a skipped element whose issue was already recorded.
|
|
1753
|
+
* @param domain - node or edge
|
|
1754
|
+
*/
|
|
1755
|
+
countSkipped(domain) {
|
|
1756
|
+
if (domain === "node") {
|
|
1757
|
+
this.report.counts.skippedNodes++;
|
|
1758
|
+
} else {
|
|
1759
|
+
this.report.counts.skippedEdges++;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Report an edge record without an endpoint and count it as skipped.
|
|
1764
|
+
* @param element - the element name
|
|
1765
|
+
* @param field - the missing field
|
|
1766
|
+
*/
|
|
1767
|
+
missingEndpoint(element, field) {
|
|
1768
|
+
this.report.error("missing-value", JSON_ISSUE.MISSING_ENDPOINT, `${element} has no ${field}`, { element });
|
|
1769
|
+
this.report.counts.skippedEdges++;
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Record the shape metadata, the source format and further metadata fields in one setMeta()
|
|
1773
|
+
* call (the sink replaces `extra` as a whole).
|
|
1774
|
+
* @param shape - the shape record
|
|
1775
|
+
* @param patch - further metadata fields
|
|
1776
|
+
*/
|
|
1777
|
+
setMeta(shape, patch = {}) {
|
|
1778
|
+
const extra = {};
|
|
1779
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
1780
|
+
if (value !== void 0) {
|
|
1781
|
+
extra[key] = value;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
this.sink.setMeta({ sourceFormat: "json", ...patch, extra: { [META_KEY]: extra } });
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Record which source field the weight came from, so the exporter writes it back under the same
|
|
1788
|
+
* key (design section 3.7, `meta.weightOrigin`).
|
|
1789
|
+
* @returns the metadata patch, empty for an unweighted import
|
|
1790
|
+
*/
|
|
1791
|
+
weightOriginPatch() {
|
|
1792
|
+
const { weightFrom } = this.options;
|
|
1793
|
+
if (weightFrom === null) {
|
|
1794
|
+
return {};
|
|
1795
|
+
}
|
|
1796
|
+
return { weightOrigin: { format: "json", id: weightFrom, title: null, type: null, namespace: null } };
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Write the graph-level attributes of a dict (NetworkX `graph`, JGF `metadata`, graphology
|
|
1800
|
+
* `attributes`, Cytoscape `data`), one column per key.
|
|
1801
|
+
* @param dict - the dict, or anything else (then reported)
|
|
1802
|
+
* @param what - the dict's name for the issue
|
|
1803
|
+
*/
|
|
1804
|
+
writeGraphDict(dict, what) {
|
|
1805
|
+
if (dict === void 0 || dict === null) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
if (!isJsonObject(dict)) {
|
|
1809
|
+
this.report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `${what} is not an object; ignored`, {
|
|
1810
|
+
element: what
|
|
1811
|
+
});
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
for (const key of Object.keys(dict)) {
|
|
1815
|
+
const value = dict[key];
|
|
1816
|
+
if (value === void 0 || value === null) {
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
try {
|
|
1820
|
+
this.sink.setGraphValue(key, value);
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
this.report.recordError(err, { element: `${what}.${key}` });
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Read the weight field of an edge record.
|
|
1828
|
+
* @param record - the record holding the attributes
|
|
1829
|
+
* @returns the weight, or undefined when absent or null; E_INVALID_WEIGHT otherwise
|
|
1830
|
+
*/
|
|
1831
|
+
weightOf(record) {
|
|
1832
|
+
const { weightFrom } = this.options;
|
|
1833
|
+
if (weightFrom === null || !hasKey(record, weightFrom)) {
|
|
1834
|
+
return void 0;
|
|
1835
|
+
}
|
|
1836
|
+
return weightFromValue(record[weightFrom]);
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Write the attributes of a nested dict plus the element-level keys the dialect does not
|
|
1840
|
+
* define (kept with the `#element` suffix).
|
|
1841
|
+
* @param writer - the table writer
|
|
1842
|
+
* @param row - the row
|
|
1843
|
+
* @param record - the element record
|
|
1844
|
+
* @param dict - the nested attribute dict
|
|
1845
|
+
* @param structural - the element keys that are not attributes
|
|
1846
|
+
* @param weightFrom - the weight key to skip in the dict, or null
|
|
1847
|
+
*/
|
|
1848
|
+
writeNested(writer, row, record, dict, structural, weightFrom) {
|
|
1849
|
+
for (const key of Object.keys(dict)) {
|
|
1850
|
+
if (key !== weightFrom) {
|
|
1851
|
+
writer.write(row, key, dict[key], SUFFIX.data);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
for (const key of Object.keys(record)) {
|
|
1855
|
+
if (!structural.has(key)) {
|
|
1856
|
+
writer.write(row, `${key}${SUFFIX.element}`, record[key], SUFFIX.data);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Write a spec-typed string field (JGF label / relation), reporting a value of another type.
|
|
1862
|
+
* @param writer - the table writer
|
|
1863
|
+
* @param column - the declared column, or INVALID_INDEX when the file has no such field
|
|
1864
|
+
* @param row - the row
|
|
1865
|
+
* @param value - the value
|
|
1866
|
+
* @param field - the field name
|
|
1867
|
+
* @param element - the element name
|
|
1868
|
+
*/
|
|
1869
|
+
writeStringField(writer, column, row, value, field, element) {
|
|
1870
|
+
if (column === INVALID_INDEX || value === void 0 || value === null) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
if (typeof value !== "string") {
|
|
1874
|
+
this.report.error(
|
|
1875
|
+
"validation-error",
|
|
1876
|
+
JSON_ISSUE.BAD_VALUE,
|
|
1877
|
+
`${element}: ${field} must be a string, found ${describe(value)}`,
|
|
1878
|
+
{ element }
|
|
1879
|
+
);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
writer.set(column, row, value);
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Declare an edge id column with role "id" from a scan of the file's edge ids: f64 when every
|
|
1886
|
+
* id is a number, string otherwise (numbers are then stored as their text and reported once).
|
|
1887
|
+
* @param edges - the edge records
|
|
1888
|
+
* @param read - how to read an edge's raw id
|
|
1889
|
+
* @param name - the column name
|
|
1890
|
+
* @param unique - whether uniqueness is enforced at freeze
|
|
1891
|
+
* @returns the column, or null when no edge has an id
|
|
1892
|
+
*/
|
|
1893
|
+
declareEdgeIds(edges, read, name, unique) {
|
|
1894
|
+
let numbers = 0;
|
|
1895
|
+
let strings = 0;
|
|
1896
|
+
for (const edge of edges) {
|
|
1897
|
+
if (!isJsonObject(edge)) {
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
const raw = read(edge);
|
|
1901
|
+
if (typeof raw === "number") {
|
|
1902
|
+
numbers++;
|
|
1903
|
+
} else if (typeof raw === "string") {
|
|
1904
|
+
strings++;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (numbers + strings === 0) {
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
const dtype = strings === 0 ? "f64" : "string";
|
|
1911
|
+
const columnName = uniqueColumnName(name, "id", (candidate) => this.edges.taken(candidate));
|
|
1912
|
+
const handle = this.edges.declare({ name: columnName, dtype, role: "id", nullable: true, unique });
|
|
1913
|
+
const stringify = strings > 0 && numbers > 0;
|
|
1914
|
+
const seen = unique ? /* @__PURE__ */ new Set() : null;
|
|
1915
|
+
if (stringify) {
|
|
1916
|
+
this.report.warning(
|
|
1917
|
+
"coercion",
|
|
1918
|
+
JSON_ISSUE.EDGE_ID_STRINGIFIED,
|
|
1919
|
+
`edge ids mix numbers and strings; ${numbers} numeric id(s) stored as text`,
|
|
1920
|
+
{ element: columnName }
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
return { handle, stringify, seen };
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* The value an edge id column stores for a raw id, checked BEFORE the edge is pushed so a bad
|
|
1927
|
+
* id skips the edge without touching the sink (design section 11.1).
|
|
1928
|
+
* @param column - the column, or null when the file has no edge ids
|
|
1929
|
+
* @param raw - the raw id value
|
|
1930
|
+
* @returns the value to store, or null when there is nothing to store
|
|
1931
|
+
*/
|
|
1932
|
+
edgeIdValue(column, raw) {
|
|
1933
|
+
if (column === null || raw === void 0 || raw === null) {
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
let value;
|
|
1937
|
+
if (typeof raw === "number") {
|
|
1938
|
+
value = column.stringify ? String(raw) : raw;
|
|
1939
|
+
} else if (typeof raw === "string") {
|
|
1940
|
+
value = raw;
|
|
1941
|
+
} else {
|
|
1942
|
+
throw new GraphFormatError(
|
|
1943
|
+
"E_COLUMN_TYPE",
|
|
1944
|
+
`an edge id must be a string or a number, found ${describe(raw)}`,
|
|
1945
|
+
{ found: typeof raw }
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
if (column.seen !== null) {
|
|
1949
|
+
const text = String(value);
|
|
1950
|
+
if (column.seen.has(text)) {
|
|
1951
|
+
throw new GraphFormatError(
|
|
1952
|
+
JSON_ISSUE.DUPLICATE_EDGE_ID,
|
|
1953
|
+
`edge id ${JSON.stringify(value)} is declared more than once; the edge is skipped`,
|
|
1954
|
+
{ id: value }
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
column.seen.add(text);
|
|
1958
|
+
}
|
|
1959
|
+
return value;
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Write an edge id value from edgeIdValue().
|
|
1963
|
+
* @param column - the column, or null
|
|
1964
|
+
* @param edge - the edge index
|
|
1965
|
+
* @param value - the value, or null for none
|
|
1966
|
+
*/
|
|
1967
|
+
setEdgeId(column, edge, value) {
|
|
1968
|
+
if (column !== null && value !== null) {
|
|
1969
|
+
this.edges.set(column.handle, edge, value);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
function parseDocument(text, report) {
|
|
1974
|
+
if (text.trim().length === 0) {
|
|
1975
|
+
report.fail(JSON_ISSUE.EMPTY_INPUT, "the input is empty");
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
return JSON.parse(text);
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1981
|
+
return report.fail(JSON_ISSUE.SYNTAX, `invalid JSON: ${message}`);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
function detectDialect(root, forced, report) {
|
|
1985
|
+
if (forced !== "auto") {
|
|
1986
|
+
return forced;
|
|
1987
|
+
}
|
|
1988
|
+
const dialect = sniffJsonDialect(root);
|
|
1989
|
+
if (dialect !== null) {
|
|
1990
|
+
return dialect;
|
|
1991
|
+
}
|
|
1992
|
+
if (Array.isArray(root)) {
|
|
1993
|
+
return report.fail(
|
|
1994
|
+
JSON_ISSUE.DIALECT,
|
|
1995
|
+
"a top-level array is only read as Cytoscape elements (objects with a data record)"
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
if (!isJsonObject(root)) {
|
|
1999
|
+
return report.fail(JSON_ISSUE.DIALECT, `the document is a JSON ${describe(root)}, not a graph object`);
|
|
2000
|
+
}
|
|
2001
|
+
return report.fail(
|
|
2002
|
+
JSON_ISSUE.DIALECT,
|
|
2003
|
+
"no known dialect: expected nodes / links / edges (node-link), elements (Cytoscape) or graph (JGF)"
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
function arraySection(value, what, report) {
|
|
2007
|
+
if (value === void 0 || value === null) {
|
|
2008
|
+
return null;
|
|
2009
|
+
}
|
|
2010
|
+
if (!Array.isArray(value)) {
|
|
2011
|
+
return report.fail(JSON_ISSUE.SHAPE, `${what} must be an array, found ${describe(value)}`, { element: what });
|
|
2012
|
+
}
|
|
2013
|
+
return value;
|
|
2014
|
+
}
|
|
2015
|
+
function flagOf(value, what, fallback, report) {
|
|
2016
|
+
if (value === void 0 || value === null) {
|
|
2017
|
+
return fallback;
|
|
2018
|
+
}
|
|
2019
|
+
if (typeof value === "boolean") {
|
|
2020
|
+
return value;
|
|
2021
|
+
}
|
|
2022
|
+
report.warning(
|
|
2023
|
+
"validation-error",
|
|
2024
|
+
JSON_ISSUE.BAD_FLAG,
|
|
2025
|
+
`${what} is ${describe(value)}, not a boolean; ${fallback} assumed`,
|
|
2026
|
+
{ element: what }
|
|
2027
|
+
);
|
|
2028
|
+
return fallback;
|
|
2029
|
+
}
|
|
2030
|
+
function endpointOf(record, explicit, defaults) {
|
|
2031
|
+
if (explicit !== null) {
|
|
2032
|
+
return { key: hasKey(record, explicit) ? explicit : null, value: record[explicit] };
|
|
2033
|
+
}
|
|
2034
|
+
for (const key of defaults) {
|
|
2035
|
+
if (hasKey(record, key)) {
|
|
2036
|
+
return { key, value: record[key] };
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
return { key: null, value: void 0 };
|
|
2040
|
+
}
|
|
2041
|
+
function anyHas(items, key) {
|
|
2042
|
+
return items.some((item) => isJsonObject(item) && item[key] !== void 0 && item[key] !== null);
|
|
2043
|
+
}
|
|
2044
|
+
function isIndexBelow(value, bound) {
|
|
2045
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value < bound;
|
|
2046
|
+
}
|
|
2047
|
+
function importNodeLink(ctx, root, dialect) {
|
|
2048
|
+
const { report, json } = ctx;
|
|
2049
|
+
let { edgesKey } = json;
|
|
2050
|
+
if (edgesKey === null) {
|
|
2051
|
+
edgesKey = hasKey(root, "edges") || !hasKey(root, "links") ? "edges" : "links";
|
|
2052
|
+
}
|
|
2053
|
+
const nodes = arraySection(root.nodes, "nodes", report);
|
|
2054
|
+
const edges = arraySection(root[edgesKey], edgesKey, report);
|
|
2055
|
+
if (nodes === null) {
|
|
2056
|
+
report.error(
|
|
2057
|
+
"missing-value",
|
|
2058
|
+
JSON_ISSUE.MISSING_SECTION,
|
|
2059
|
+
"the document has no nodes array; nodes come from the edges",
|
|
2060
|
+
{ element: "nodes" }
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
if (edges === null) {
|
|
2064
|
+
report.error(
|
|
2065
|
+
"missing-value",
|
|
2066
|
+
JSON_ISSUE.MISSING_SECTION,
|
|
2067
|
+
`the document has no ${edgesKey} array; the graph has no edges`,
|
|
2068
|
+
{ element: edgesKey }
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
const directed = flagOf(root.directed, "directed", ctx.defaultDirected(dialect), report);
|
|
2072
|
+
const multigraph = hasKey(root, "multigraph") ? flagOf(root.multigraph, "multigraph", false, report) : null;
|
|
2073
|
+
ctx.setHeader(directed);
|
|
2074
|
+
ctx.writeGraphDict(root.graph, "graph");
|
|
2075
|
+
const nodeList = nodes ?? [];
|
|
2076
|
+
const edgeList = edges ?? [];
|
|
2077
|
+
ctx.sink.reserve(nodeList.length, edgeList.length);
|
|
2078
|
+
let { nodeIdKey } = json;
|
|
2079
|
+
let positional = ctx.options.nodeIdFrom === "index";
|
|
2080
|
+
if (!positional && nodeIdKey === null) {
|
|
2081
|
+
const candidates = ctx.options.nodeIdFrom === "label" ? ["label", "name", "id"] : ["id", "name"];
|
|
2082
|
+
nodeIdKey = candidates.find((key) => anyHas(nodeList, key)) ?? null;
|
|
2083
|
+
if (ctx.options.nodeIdFrom === "label" && nodeIdKey === "id") {
|
|
2084
|
+
report.warning(
|
|
2085
|
+
"unsupported",
|
|
2086
|
+
JSON_ISSUE.OPTION_IGNORED,
|
|
2087
|
+
'nodeIdFrom "label": no node has a label or name key; ids are read from "id"',
|
|
2088
|
+
{ element: "nodeIdFrom" }
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
if (nodeIdKey === null) {
|
|
2092
|
+
if (nodeList.length > 0) {
|
|
2093
|
+
positional = true;
|
|
2094
|
+
report.warning(
|
|
2095
|
+
"missing-value",
|
|
2096
|
+
JSON_ISSUE.POSITIONAL_NODES,
|
|
2097
|
+
"no node has an id or name key; array positions are the node ids",
|
|
2098
|
+
{ element: "nodes" }
|
|
2099
|
+
);
|
|
2100
|
+
} else {
|
|
2101
|
+
nodeIdKey = "id";
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (positional) {
|
|
2106
|
+
nodeIdKey = null;
|
|
2107
|
+
}
|
|
2108
|
+
let indexLinks;
|
|
2109
|
+
if (positional) {
|
|
2110
|
+
indexLinks = true;
|
|
2111
|
+
} else if (json.indexLinks === "auto") {
|
|
2112
|
+
indexLinks = nodeIdKey !== null && looksIndexLinked(nodeList, edgeList, nodeIdKey, json);
|
|
2113
|
+
} else {
|
|
2114
|
+
({ indexLinks } = json);
|
|
2115
|
+
}
|
|
2116
|
+
const positionIds = indexLinks ? [] : null;
|
|
2117
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
2118
|
+
const element = `nodes[${i}]`;
|
|
2119
|
+
const record = nodeList[i];
|
|
2120
|
+
let pushed = null;
|
|
2121
|
+
if (!isJsonObject(record)) {
|
|
2122
|
+
ctx.badElement("node", element);
|
|
2123
|
+
} else {
|
|
2124
|
+
let id;
|
|
2125
|
+
if (nodeIdKey === null) {
|
|
2126
|
+
id = i;
|
|
2127
|
+
} else {
|
|
2128
|
+
id = ctx.coerceId(hasKey(record, nodeIdKey) ? record[nodeIdKey] : void 0, element);
|
|
2129
|
+
}
|
|
2130
|
+
if (id === null) {
|
|
2131
|
+
ctx.countSkipped("node");
|
|
2132
|
+
} else {
|
|
2133
|
+
const index = ctx.pushNode(id, element);
|
|
2134
|
+
if (index >= 0) {
|
|
2135
|
+
pushed = id;
|
|
2136
|
+
writeFlat(ctx, ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
positionIds?.push(pushed);
|
|
2141
|
+
}
|
|
2142
|
+
throwIfAborted(ctx.options.signal);
|
|
2143
|
+
const endpointKeys = importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds);
|
|
2144
|
+
ctx.setMeta(
|
|
2145
|
+
{
|
|
2146
|
+
dialect,
|
|
2147
|
+
edgesKey,
|
|
2148
|
+
nodeIdKey,
|
|
2149
|
+
indexLinks,
|
|
2150
|
+
sourceKey: endpointKeys.source ?? void 0,
|
|
2151
|
+
targetKey: endpointKeys.target ?? void 0
|
|
2152
|
+
},
|
|
2153
|
+
{ declaredMultigraph: multigraph, ...ctx.weightOriginPatch() }
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
function importNodeLinkEdges(ctx, edgeList, edgesKey, positionIds) {
|
|
2157
|
+
const { json } = ctx;
|
|
2158
|
+
const kind = ctx.uniformKind();
|
|
2159
|
+
let sourceKey = null;
|
|
2160
|
+
let targetKey = null;
|
|
2161
|
+
for (let i = 0; i < edgeList.length; i++) {
|
|
2162
|
+
const element = `${edgesKey}[${i}]`;
|
|
2163
|
+
const record = edgeList[i];
|
|
2164
|
+
if (!isJsonObject(record)) {
|
|
2165
|
+
ctx.badElement("edge", element);
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
const source = endpointOf(record, json.sourceKey, NODE_LINK_SOURCE_KEYS);
|
|
2169
|
+
const target = endpointOf(record, json.targetKey, NODE_LINK_TARGET_KEYS);
|
|
2170
|
+
if (source.key === null || target.key === null) {
|
|
2171
|
+
ctx.missingEndpoint(element, source.key === null ? "source" : "target");
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2174
|
+
sourceKey ??= source.key;
|
|
2175
|
+
targetKey ??= target.key;
|
|
2176
|
+
try {
|
|
2177
|
+
const u = resolveEndpoint(ctx, source.value, positionIds, element, "source");
|
|
2178
|
+
const v = resolveEndpoint(ctx, target.value, positionIds, element, "target");
|
|
2179
|
+
if (u === null || v === null) {
|
|
2180
|
+
ctx.countSkipped("edge");
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
const weight = ctx.weightOf(record);
|
|
2184
|
+
const edge = ctx.pushEdge(u, v, kind, weight, element);
|
|
2185
|
+
const { weightFrom } = ctx.options;
|
|
2186
|
+
for (const key of Object.keys(record)) {
|
|
2187
|
+
if (key !== source.key && key !== target.key && key !== weightFrom) {
|
|
2188
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
} catch (err) {
|
|
2192
|
+
ctx.skip(err, "edge", element);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
return { source: sourceKey, target: targetKey };
|
|
2196
|
+
}
|
|
2197
|
+
function writeFlat(ctx, writer, index, record, id, keep) {
|
|
2198
|
+
try {
|
|
2199
|
+
for (const key of Object.keys(record)) {
|
|
2200
|
+
if (keep(key)) {
|
|
2201
|
+
writer.write(index, key, record[key], SUFFIX.data);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
} catch (err) {
|
|
2205
|
+
ctx.report.recordError(err, { element: String(id) });
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
function looksIndexLinked(nodes, edges, nodeIdKey, json) {
|
|
2209
|
+
if (edges.length === 0 || nodes.length === 0) {
|
|
2210
|
+
return false;
|
|
2211
|
+
}
|
|
2212
|
+
for (const node of nodes) {
|
|
2213
|
+
if (isJsonObject(node) && typeof node[nodeIdKey] === "number") {
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
let seen = 0;
|
|
2218
|
+
for (const edge of edges) {
|
|
2219
|
+
if (!isJsonObject(edge)) {
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
const s = endpointOf(edge, json.sourceKey, NODE_LINK_SOURCE_KEYS).value;
|
|
2223
|
+
const t = endpointOf(edge, json.targetKey, NODE_LINK_TARGET_KEYS).value;
|
|
2224
|
+
if (!isIndexBelow(s, Infinity) || !isIndexBelow(t, Infinity)) {
|
|
2225
|
+
return false;
|
|
2226
|
+
}
|
|
2227
|
+
seen++;
|
|
2228
|
+
}
|
|
2229
|
+
return seen > 0;
|
|
2230
|
+
}
|
|
2231
|
+
function resolveEndpoint(ctx, raw, positionIds, element, field) {
|
|
2232
|
+
if (positionIds === null) {
|
|
2233
|
+
return ctx.coerceId(raw, `${element}.${field}`);
|
|
2234
|
+
}
|
|
2235
|
+
if (!isIndexBelow(raw, positionIds.length)) {
|
|
2236
|
+
ctx.report.error(
|
|
2237
|
+
"validation-error",
|
|
2238
|
+
JSON_ISSUE.BAD_INDEX,
|
|
2239
|
+
`${element}: ${field} ${describe(raw)} is not a node index below ${positionIds.length}`,
|
|
2240
|
+
{ element }
|
|
2241
|
+
);
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
2244
|
+
const id = positionIds[raw];
|
|
2245
|
+
if (id === null) {
|
|
2246
|
+
ctx.report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${field} names a node that was skipped`, {
|
|
2247
|
+
element
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
return id;
|
|
2251
|
+
}
|
|
2252
|
+
function importVis(ctx, root) {
|
|
2253
|
+
const { report, json } = ctx;
|
|
2254
|
+
const nodes = arraySection(root.nodes, "nodes", report) ?? [];
|
|
2255
|
+
const edges = arraySection(root.edges, "edges", report) ?? [];
|
|
2256
|
+
ctx.setHeader(ctx.defaultDirected("vis"));
|
|
2257
|
+
ctx.sink.reserve(nodes.length, edges.length);
|
|
2258
|
+
const nodeIdKey = json.nodeIdKey ?? "id";
|
|
2259
|
+
ctx.reportNodeIdFrom("vis", `the ${JSON.stringify(nodeIdKey)} key`);
|
|
2260
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2261
|
+
const element = `nodes[${i}]`;
|
|
2262
|
+
const record = nodes[i];
|
|
2263
|
+
if (!isJsonObject(record)) {
|
|
2264
|
+
ctx.badElement("node", element);
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
const id = ctx.coerceId(hasKey(record, nodeIdKey) ? record[nodeIdKey] : void 0, element);
|
|
2268
|
+
if (id === null) {
|
|
2269
|
+
ctx.countSkipped("node");
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
const index = ctx.pushNode(id, element);
|
|
2273
|
+
if (index >= 0) {
|
|
2274
|
+
writeFlat(ctx, ctx.nodes, index, record, id, (key) => key !== nodeIdKey);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
throwIfAborted(ctx.options.signal);
|
|
2278
|
+
const ids = ctx.declareEdgeIds(edges, (edge) => edge.id, "id", true);
|
|
2279
|
+
const kind = ctx.uniformKind();
|
|
2280
|
+
let sourceKey = null;
|
|
2281
|
+
let targetKey = null;
|
|
2282
|
+
for (let i = 0; i < edges.length; i++) {
|
|
2283
|
+
const element = `edges[${i}]`;
|
|
2284
|
+
const record = edges[i];
|
|
2285
|
+
if (!isJsonObject(record)) {
|
|
2286
|
+
ctx.badElement("edge", element);
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
const source = endpointOf(record, json.sourceKey, VIS_SOURCE_KEYS);
|
|
2290
|
+
const target = endpointOf(record, json.targetKey, VIS_TARGET_KEYS);
|
|
2291
|
+
if (source.key === null || target.key === null) {
|
|
2292
|
+
ctx.missingEndpoint(element, source.key === null ? "from" : "to");
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
sourceKey ??= source.key;
|
|
2296
|
+
targetKey ??= target.key;
|
|
2297
|
+
try {
|
|
2298
|
+
const u = ctx.coerceId(source.value, `${element}.from`);
|
|
2299
|
+
const v = ctx.coerceId(target.value, `${element}.to`);
|
|
2300
|
+
if (u === null || v === null) {
|
|
2301
|
+
ctx.countSkipped("edge");
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
const idValue = ctx.edgeIdValue(ids, record.id);
|
|
2305
|
+
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(record), element);
|
|
2306
|
+
ctx.setEdgeId(ids, edge, idValue);
|
|
2307
|
+
const { weightFrom } = ctx.options;
|
|
2308
|
+
for (const key of Object.keys(record)) {
|
|
2309
|
+
if (key !== source.key && key !== target.key && key !== "id" && key !== weightFrom) {
|
|
2310
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
} catch (err) {
|
|
2314
|
+
ctx.skip(err, "edge", element);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
ctx.setMeta(
|
|
2318
|
+
{ dialect: "vis", nodeIdKey, sourceKey: sourceKey ?? void 0, targetKey: targetKey ?? void 0 },
|
|
2319
|
+
ctx.weightOriginPatch()
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
function importGraphology(ctx, root) {
|
|
2323
|
+
const { report } = ctx;
|
|
2324
|
+
const nodes = arraySection(root.nodes, "nodes", report) ?? [];
|
|
2325
|
+
const edges = arraySection(root.edges, "edges", report) ?? [];
|
|
2326
|
+
const options = isJsonObject(root.options) ? root.options : {};
|
|
2327
|
+
if (hasKey(root, "options") && !isJsonObject(root.options)) {
|
|
2328
|
+
report.warning("validation-error", JSON_ISSUE.BAD_FLAG, "options is not an object; ignored", {
|
|
2329
|
+
element: "options"
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
let type;
|
|
2333
|
+
if (options.type === "directed" || options.type === "undirected" || options.type === "mixed") {
|
|
2334
|
+
({ type } = options);
|
|
2335
|
+
} else {
|
|
2336
|
+
if (options.type !== void 0 && options.type !== null) {
|
|
2337
|
+
report.warning(
|
|
2338
|
+
"validation-error",
|
|
2339
|
+
JSON_ISSUE.BAD_FLAG,
|
|
2340
|
+
`options.type ${describe(options.type)} is not directed, undirected or mixed; mixed assumed`,
|
|
2341
|
+
{ element: "options.type" }
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
type = "mixed";
|
|
2345
|
+
}
|
|
2346
|
+
let directed;
|
|
2347
|
+
if (type === "mixed") {
|
|
2348
|
+
const objects = edges.filter((edge) => isJsonObject(edge));
|
|
2349
|
+
const undirectedEdges = objects.filter((edge) => edge.undirected === true).length;
|
|
2350
|
+
directed = objects.length === 0 ? ctx.defaultDirected("graphology") : undirectedEdges < objects.length;
|
|
2351
|
+
} else {
|
|
2352
|
+
directed = type === "directed";
|
|
2353
|
+
}
|
|
2354
|
+
ctx.setHeader(directed);
|
|
2355
|
+
const multi = hasKey(options, "multi") ? flagOf(options.multi, "options.multi", false, report) : null;
|
|
2356
|
+
const allowSelfLoops = hasKey(options, "allowSelfLoops") ? flagOf(options.allowSelfLoops, "options.allowSelfLoops", true, report) : void 0;
|
|
2357
|
+
ctx.writeGraphDict(root.attributes, "attributes");
|
|
2358
|
+
ctx.reportNodeIdFrom("graphology", "the node key");
|
|
2359
|
+
ctx.sink.reserve(nodes.length, edges.length);
|
|
2360
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2361
|
+
const element = `nodes[${i}]`;
|
|
2362
|
+
const record = nodes[i];
|
|
2363
|
+
if (!isJsonObject(record)) {
|
|
2364
|
+
ctx.badElement("node", element);
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2367
|
+
const id = ctx.coerceId(hasKey(record, "key") ? record.key : void 0, element);
|
|
2368
|
+
if (id === null) {
|
|
2369
|
+
ctx.countSkipped("node");
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
pushNestedNode(ctx, id, record, "attributes", GRAPHOLOGY_NODE_KEYS, element);
|
|
2373
|
+
}
|
|
2374
|
+
throwIfAborted(ctx.options.signal);
|
|
2375
|
+
const ids = ctx.declareEdgeIds(edges, (edge) => edge.key, "key", true);
|
|
2376
|
+
for (let i = 0; i < edges.length; i++) {
|
|
2377
|
+
const element = `edges[${i}]`;
|
|
2378
|
+
const record = edges[i];
|
|
2379
|
+
if (!isJsonObject(record)) {
|
|
2380
|
+
ctx.badElement("edge", element);
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
if (!hasKey(record, "source") || !hasKey(record, "target")) {
|
|
2384
|
+
ctx.missingEndpoint(element, hasKey(record, "source") ? "target" : "source");
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
try {
|
|
2388
|
+
const u = ctx.coerceId(record.source, `${element}.source`);
|
|
2389
|
+
const v = ctx.coerceId(record.target, `${element}.target`);
|
|
2390
|
+
if (u === null || v === null) {
|
|
2391
|
+
ctx.countSkipped("edge");
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
let kind;
|
|
2395
|
+
if (type === "mixed") {
|
|
2396
|
+
kind = flagOf(record.undirected, `${element}.undirected`, false, report) ? "undirected" : "directed";
|
|
2397
|
+
} else {
|
|
2398
|
+
kind = type;
|
|
2399
|
+
}
|
|
2400
|
+
const attributes = isJsonObject(record.attributes) ? record.attributes : {};
|
|
2401
|
+
const idValue = ctx.edgeIdValue(ids, record.key);
|
|
2402
|
+
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
|
|
2403
|
+
ctx.setEdgeId(ids, edge, idValue);
|
|
2404
|
+
ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom);
|
|
2405
|
+
} catch (err) {
|
|
2406
|
+
ctx.skip(err, "edge", element);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
ctx.setMeta({ dialect: "graphology", allowSelfLoops }, { declaredMultigraph: multi, ...ctx.weightOriginPatch() });
|
|
2410
|
+
}
|
|
2411
|
+
function pushNestedNode(ctx, id, record, dictKey, structural, element) {
|
|
2412
|
+
const index = ctx.pushNode(id, element);
|
|
2413
|
+
if (index < 0) {
|
|
2414
|
+
return index;
|
|
2415
|
+
}
|
|
2416
|
+
try {
|
|
2417
|
+
const dict = record[dictKey];
|
|
2418
|
+
if (dict !== void 0 && dict !== null && !isJsonObject(dict)) {
|
|
2419
|
+
ctx.report.error(
|
|
2420
|
+
"validation-error",
|
|
2421
|
+
JSON_ISSUE.BAD_VALUE,
|
|
2422
|
+
`${element}: ${dictKey} must be an object, found ${describe(dict)}`,
|
|
2423
|
+
{ element }
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
ctx.writeNested(ctx.nodes, index, record, isJsonObject(dict) ? dict : {}, structural, null);
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
ctx.report.recordError(err, { element: String(id) });
|
|
2429
|
+
}
|
|
2430
|
+
return index;
|
|
2431
|
+
}
|
|
2432
|
+
function importJgf(ctx, root) {
|
|
2433
|
+
const { report } = ctx;
|
|
2434
|
+
const graph = jgfGraphOf(ctx, root);
|
|
2435
|
+
const edges = arraySection(graph.edges, "graph.edges", report) ?? [];
|
|
2436
|
+
const hyperedges = arraySection(graph.hyperedges, "graph.hyperedges", report) ?? [];
|
|
2437
|
+
const nodesRaw = graph.nodes;
|
|
2438
|
+
if (nodesRaw !== void 0 && nodesRaw !== null && !isJsonObject(nodesRaw) && !Array.isArray(nodesRaw)) {
|
|
2439
|
+
report.fail(
|
|
2440
|
+
JSON_ISSUE.SHAPE,
|
|
2441
|
+
`graph.nodes must be an object keyed by id or an array, found ${describe(nodesRaw)}`
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
const nodeRecords = Array.isArray(nodesRaw) ? nodesRaw : Object.values(nodesRaw ?? {});
|
|
2445
|
+
let directed;
|
|
2446
|
+
if (typeof graph.directed === "boolean") {
|
|
2447
|
+
({ directed } = graph);
|
|
2448
|
+
} else if (ctx.explicitDefaultDirected) {
|
|
2449
|
+
directed = ctx.options.defaultDirected;
|
|
2450
|
+
} else {
|
|
2451
|
+
if (graph.directed !== void 0 && graph.directed !== null) {
|
|
2452
|
+
report.warning(
|
|
2453
|
+
"validation-error",
|
|
2454
|
+
JSON_ISSUE.BAD_FLAG,
|
|
2455
|
+
`graph.directed is ${describe(graph.directed)}, not a boolean`,
|
|
2456
|
+
{ element: "graph.directed" }
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
const objects = edges.filter((edge) => isJsonObject(edge));
|
|
2460
|
+
directed = objects.length === 0 || !objects.every((edge) => edge.directed === false);
|
|
2461
|
+
}
|
|
2462
|
+
ctx.setHeader(directed);
|
|
2463
|
+
ctx.writeGraphDict(graph.metadata, "graph.metadata");
|
|
2464
|
+
const shape = {
|
|
2465
|
+
dialect: "jgf",
|
|
2466
|
+
id: typeof graph.id === "string" ? graph.id : void 0,
|
|
2467
|
+
type: typeof graph.type === "string" ? graph.type : void 0
|
|
2468
|
+
};
|
|
2469
|
+
const label = typeof graph.label === "string" ? graph.label : null;
|
|
2470
|
+
ctx.reportNodeIdFrom("jgf", "the node keys");
|
|
2471
|
+
ctx.sink.reserve(nodeRecords.length, edges.length);
|
|
2472
|
+
const labelColumn = ctx.nodes.declareIf(
|
|
2473
|
+
{ name: "label", dtype: "string", role: "label", nullable: true },
|
|
2474
|
+
anyHas(nodeRecords, "label")
|
|
2475
|
+
);
|
|
2476
|
+
if (isJsonObject(nodesRaw)) {
|
|
2477
|
+
for (const key of Object.keys(nodesRaw)) {
|
|
2478
|
+
const element = `nodes[${JSON.stringify(key)}]`;
|
|
2479
|
+
const record = nodesRaw[key];
|
|
2480
|
+
if (record !== null && !isJsonObject(record)) {
|
|
2481
|
+
ctx.badElement("node", element);
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
const id = ctx.coerceId(key, element);
|
|
2485
|
+
if (id === null) {
|
|
2486
|
+
ctx.countSkipped("node");
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
|
|
2490
|
+
}
|
|
2491
|
+
} else {
|
|
2492
|
+
for (let i = 0; i < nodeRecords.length; i++) {
|
|
2493
|
+
const element = `nodes[${i}]`;
|
|
2494
|
+
const record = nodeRecords[i];
|
|
2495
|
+
if (!isJsonObject(record)) {
|
|
2496
|
+
ctx.badElement("node", element);
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
const id = ctx.coerceId(hasKey(record, "id") ? record.id : void 0, element);
|
|
2500
|
+
if (id === null) {
|
|
2501
|
+
ctx.countSkipped("node");
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
pushJgfNode(ctx, id, record, labelColumn, element);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
throwIfAborted(ctx.options.signal);
|
|
2508
|
+
const ids = ctx.declareEdgeIds([...edges, ...hyperedges], (edge) => edge.id, "id", false);
|
|
2509
|
+
const relationColumn = ctx.edges.declareIf(
|
|
2510
|
+
{ name: "relation", dtype: "string", role: "kind", nullable: true },
|
|
2511
|
+
anyHas(edges, "relation") || anyHas(hyperedges, "relation")
|
|
2512
|
+
);
|
|
2513
|
+
const edgeLabelColumn = ctx.edges.declareIf(
|
|
2514
|
+
{ name: "label", dtype: "string", role: "label", nullable: true },
|
|
2515
|
+
anyHas(edges, "label") || anyHas(hyperedges, "label")
|
|
2516
|
+
);
|
|
2517
|
+
const writeJgfEdge = (record, u, v, kind, element, structural = JGF_EDGE_KEYS) => {
|
|
2518
|
+
const { metadata } = record;
|
|
2519
|
+
const dict = isJsonObject(metadata) ? metadata : {};
|
|
2520
|
+
const idValue = ctx.edgeIdValue(ids, record.id);
|
|
2521
|
+
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(dict), element);
|
|
2522
|
+
ctx.setEdgeId(ids, edge, idValue);
|
|
2523
|
+
ctx.writeStringField(ctx.edges, relationColumn, edge, record.relation, "relation", element);
|
|
2524
|
+
ctx.writeStringField(ctx.edges, edgeLabelColumn, edge, record.label, "label", element);
|
|
2525
|
+
if (metadata !== void 0 && metadata !== null && !isJsonObject(metadata)) {
|
|
2526
|
+
report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
|
|
2527
|
+
element
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom);
|
|
2531
|
+
};
|
|
2532
|
+
for (let i = 0; i < edges.length; i++) {
|
|
2533
|
+
const element = `edges[${i}]`;
|
|
2534
|
+
const record = edges[i];
|
|
2535
|
+
if (!isJsonObject(record)) {
|
|
2536
|
+
ctx.badElement("edge", element);
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
if (!hasKey(record, "source") || !hasKey(record, "target")) {
|
|
2540
|
+
ctx.missingEndpoint(element, hasKey(record, "source") ? "target" : "source");
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
try {
|
|
2544
|
+
const u = ctx.coerceId(record.source, `${element}.source`);
|
|
2545
|
+
const v = ctx.coerceId(record.target, `${element}.target`);
|
|
2546
|
+
if (u === null || v === null) {
|
|
2547
|
+
ctx.countSkipped("edge");
|
|
2548
|
+
continue;
|
|
2549
|
+
}
|
|
2550
|
+
const edgeDirected = flagOf(record.directed, `${element}.directed`, directed, report);
|
|
2551
|
+
writeJgfEdge(record, u, v, edgeDirected ? "directed" : "undirected", element);
|
|
2552
|
+
} catch (err) {
|
|
2553
|
+
ctx.skip(err, "edge", element);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
importHyperedges(ctx, hyperedges, directed, writeJgfEdge);
|
|
2557
|
+
ctx.setMeta(shape, { name: label, ...ctx.weightOriginPatch() });
|
|
2558
|
+
}
|
|
2559
|
+
function jgfGraphOf(ctx, root) {
|
|
2560
|
+
const { report } = ctx;
|
|
2561
|
+
if (isJsonObject(root.graph)) {
|
|
2562
|
+
return root.graph;
|
|
2563
|
+
}
|
|
2564
|
+
const graphs = arraySection(root.graphs, "graphs", report) ?? [];
|
|
2565
|
+
if (graphs.length === 0) {
|
|
2566
|
+
report.fail(JSON_ISSUE.SHAPE, "a JGF document needs a graph object or a non-empty graphs array");
|
|
2567
|
+
}
|
|
2568
|
+
if (graphs.length > 1) {
|
|
2569
|
+
report.warning(
|
|
2570
|
+
"unsupported",
|
|
2571
|
+
JSON_ISSUE.MULTIPLE_GRAPHS,
|
|
2572
|
+
`the document holds ${graphs.length} graphs; only graphs[${ctx.json.graphIndex}] is read`,
|
|
2573
|
+
{ element: "graphs" }
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
if (ctx.json.graphIndex >= graphs.length) {
|
|
2577
|
+
report.fail(JSON_ISSUE.SHAPE, `graphIndex ${ctx.json.graphIndex} is beyond the ${graphs.length} graph(s)`);
|
|
2578
|
+
}
|
|
2579
|
+
const graph = graphs[ctx.json.graphIndex];
|
|
2580
|
+
if (!isJsonObject(graph)) {
|
|
2581
|
+
return report.fail(JSON_ISSUE.SHAPE, `graphs[${ctx.json.graphIndex}] is not an object`);
|
|
2582
|
+
}
|
|
2583
|
+
return graph;
|
|
2584
|
+
}
|
|
2585
|
+
function pushJgfNode(ctx, id, record, labelColumn, element) {
|
|
2586
|
+
const index = ctx.pushNode(id, element);
|
|
2587
|
+
if (index < 0) {
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
try {
|
|
2591
|
+
ctx.writeStringField(ctx.nodes, labelColumn, index, record.label, "label", element);
|
|
2592
|
+
const { metadata } = record;
|
|
2593
|
+
if (metadata !== void 0 && metadata !== null && !isJsonObject(metadata)) {
|
|
2594
|
+
ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
|
|
2595
|
+
element
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
ctx.writeNested(ctx.nodes, index, record, isJsonObject(metadata) ? metadata : {}, JGF_NODE_KEYS, null);
|
|
2599
|
+
} catch (err) {
|
|
2600
|
+
ctx.report.recordError(err, { element: String(id) });
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function importHyperedges(ctx, hyperedges, directed, push) {
|
|
2604
|
+
if (hyperedges.length === 0) {
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
const { report } = ctx;
|
|
2608
|
+
const policy = ctx.options.hyperedges;
|
|
2609
|
+
if (policy === "error") {
|
|
2610
|
+
report.error("unsupported", JSON_ISSUE.HYPEREDGE, `${hyperedges.length} hyperedge(s) (hyperedges: "error")`, {
|
|
2611
|
+
element: "hyperedges"
|
|
2612
|
+
});
|
|
2613
|
+
throw report.abort("hyperedges refused", { code: JSON_ISSUE.HYPEREDGE, count: hyperedges.length });
|
|
2614
|
+
}
|
|
2615
|
+
if (policy === "skip") {
|
|
2616
|
+
report.warning("unsupported", JSON_ISSUE.HYPEREDGES_SKIPPED, `${hyperedges.length} hyperedge(s) skipped`, {
|
|
2617
|
+
element: "hyperedges"
|
|
2618
|
+
});
|
|
2619
|
+
report.loss(
|
|
2620
|
+
JSON_ISSUE.HYPEREDGES_SKIPPED,
|
|
2621
|
+
`${hyperedges.length} hyperedge(s) were not imported`,
|
|
2622
|
+
null,
|
|
2623
|
+
hyperedges.length
|
|
2624
|
+
);
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
for (let i = 0; i < hyperedges.length; i++) {
|
|
2628
|
+
const element = `hyperedges[${i}]`;
|
|
2629
|
+
const record = hyperedges[i];
|
|
2630
|
+
if (!isJsonObject(record)) {
|
|
2631
|
+
ctx.badElement("edge", element);
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
try {
|
|
2635
|
+
if (Array.isArray(record.nodes)) {
|
|
2636
|
+
const members = record.nodes.map((raw) => ctx.requireId(raw, element));
|
|
2637
|
+
if (members.length < 2) {
|
|
2638
|
+
throw new GraphFormatError(
|
|
2639
|
+
"E_INVALID_ID",
|
|
2640
|
+
`${element}: an undirected hyperedge needs two or more nodes`,
|
|
2641
|
+
{ reason: "hyperedge shape" }
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
const kind = directed ? "directed" : "undirected";
|
|
2645
|
+
if (policy === "star") {
|
|
2646
|
+
for (let k = 1; k < members.length; k++) {
|
|
2647
|
+
push(record, members[0], members[k], kind, element, JGF_HYPEREDGE_KEYS);
|
|
2648
|
+
}
|
|
2649
|
+
} else {
|
|
2650
|
+
for (let a = 0; a < members.length; a++) {
|
|
2651
|
+
for (let b = a + 1; b < members.length; b++) {
|
|
2652
|
+
push(record, members[a], members[b], kind, element, JGF_HYPEREDGE_KEYS);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
} else if (Array.isArray(record.source) && Array.isArray(record.target)) {
|
|
2657
|
+
const sources = record.source.map((raw) => ctx.requireId(raw, element));
|
|
2658
|
+
const targets = record.target.map((raw) => ctx.requireId(raw, element));
|
|
2659
|
+
if (sources.length === 0 || targets.length === 0) {
|
|
2660
|
+
throw new GraphFormatError(
|
|
2661
|
+
"E_INVALID_ID",
|
|
2662
|
+
`${element}: a directed hyperedge needs sources and targets`,
|
|
2663
|
+
{ reason: "hyperedge shape" }
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
for (const s of sources) {
|
|
2667
|
+
for (const t of targets) {
|
|
2668
|
+
push(record, s, t, "directed", element, JGF_HYPEREDGE_KEYS);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
} else {
|
|
2672
|
+
report.error(
|
|
2673
|
+
"validation-error",
|
|
2674
|
+
JSON_ISSUE.HYPEREDGE_SHAPE,
|
|
2675
|
+
`${element} has neither a nodes array nor source / target arrays`,
|
|
2676
|
+
{ element }
|
|
2677
|
+
);
|
|
2678
|
+
ctx.countSkipped("edge");
|
|
2679
|
+
}
|
|
2680
|
+
} catch (err) {
|
|
2681
|
+
ctx.skip(err, "edge", element);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
function importCytoscape(ctx, root) {
|
|
2686
|
+
const { report } = ctx;
|
|
2687
|
+
let elements;
|
|
2688
|
+
let extra;
|
|
2689
|
+
if (Array.isArray(root)) {
|
|
2690
|
+
elements = root;
|
|
2691
|
+
} else if (isJsonObject(root)) {
|
|
2692
|
+
({ elements } = root);
|
|
2693
|
+
ctx.writeGraphDict(root.data, "data");
|
|
2694
|
+
const rest = {};
|
|
2695
|
+
for (const key of Object.keys(root)) {
|
|
2696
|
+
if (key !== "elements" && key !== "data") {
|
|
2697
|
+
rest[key] = root[key];
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
if (Object.keys(rest).length > 0) {
|
|
2701
|
+
extra = rest;
|
|
2702
|
+
}
|
|
2703
|
+
} else {
|
|
2704
|
+
report.fail(JSON_ISSUE.SHAPE, `a Cytoscape document must be an object or an array, found ${describe(root)}`);
|
|
2705
|
+
}
|
|
2706
|
+
const { nodes, edges } = cytoscapeSections(ctx, elements);
|
|
2707
|
+
ctx.setHeader(ctx.defaultDirected("cytoscape"));
|
|
2708
|
+
ctx.reportNodeIdFrom("cytoscape", "data.id");
|
|
2709
|
+
ctx.sink.reserve(nodes.length, edges.length);
|
|
2710
|
+
const dataHas = (items, key) => items.some((item) => isJsonObject(item) && isJsonObject(item.data) && item.data[key] !== void 0);
|
|
2711
|
+
const positionColumn = ctx.nodes.declareIf(
|
|
2712
|
+
{
|
|
2713
|
+
name: POSITION_COLUMN,
|
|
2714
|
+
dtype: "f32",
|
|
2715
|
+
components: 3,
|
|
2716
|
+
role: "position",
|
|
2717
|
+
mutable: true,
|
|
2718
|
+
nullable: true,
|
|
2719
|
+
extra: { sourceDims: 2, units: "file" },
|
|
2720
|
+
origin: { format: "json", namespace: "cytoscape" }
|
|
2721
|
+
},
|
|
2722
|
+
anyHas(nodes, "position")
|
|
2723
|
+
);
|
|
2724
|
+
const classesColumn = ctx.nodes.declareIf(
|
|
2725
|
+
{ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true },
|
|
2726
|
+
anyHas(nodes, "classes")
|
|
2727
|
+
);
|
|
2728
|
+
const parentColumn = ctx.nodes.declareIf(
|
|
2729
|
+
{ name: PARENT_COLUMN, dtype: "u32", role: "parent", refersTo: "node", nullable: true },
|
|
2730
|
+
dataHas(nodes, "parent")
|
|
2731
|
+
);
|
|
2732
|
+
const edgeClassesColumn = ctx.edges.declareIf(
|
|
2733
|
+
{ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true },
|
|
2734
|
+
anyHas(edges, "classes")
|
|
2735
|
+
);
|
|
2736
|
+
const parents = [];
|
|
2737
|
+
const point = [0, 0, 0];
|
|
2738
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2739
|
+
const element = `nodes[${i}]`;
|
|
2740
|
+
const record = nodes[i];
|
|
2741
|
+
if (!isJsonObject(record) || !isJsonObject(record.data)) {
|
|
2742
|
+
ctx.badElement("node", element, "an element with a data object");
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
const { data } = record;
|
|
2746
|
+
const id = ctx.coerceId(hasKey(data, "id") ? data.id : void 0, element);
|
|
2747
|
+
if (id === null) {
|
|
2748
|
+
ctx.countSkipped("node");
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
const index = ctx.pushNode(id, element);
|
|
2752
|
+
if (index < 0) {
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
try {
|
|
2756
|
+
for (const key of Object.keys(data)) {
|
|
2757
|
+
if (key === "id") {
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
if (key === "parent") {
|
|
2761
|
+
const raw = data.parent;
|
|
2762
|
+
if (raw !== void 0 && raw !== null) {
|
|
2763
|
+
const parent = ctx.coerceId(raw, `${element}.data.parent`);
|
|
2764
|
+
if (parent !== null) {
|
|
2765
|
+
parents.push({ index, parent, element });
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
ctx.nodes.write(index, key, data[key], SUFFIX.data);
|
|
2771
|
+
}
|
|
2772
|
+
const { position } = record;
|
|
2773
|
+
if (position !== void 0 && position !== null) {
|
|
2774
|
+
if (isJsonObject(position) && typeof position.x === "number" && typeof position.y === "number") {
|
|
2775
|
+
point[0] = position.x;
|
|
2776
|
+
point[1] = position.y;
|
|
2777
|
+
ctx.nodes.set(positionColumn, index, point);
|
|
2778
|
+
} else {
|
|
2779
|
+
report.error(
|
|
2780
|
+
"validation-error",
|
|
2781
|
+
JSON_ISSUE.BAD_VALUE,
|
|
2782
|
+
`${element}: position must be an object with numeric x and y`,
|
|
2783
|
+
{ element }
|
|
2784
|
+
);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
writeClasses(ctx, ctx.nodes, classesColumn, index, record.classes, element);
|
|
2788
|
+
writeElementKeys(ctx.nodes, index, record);
|
|
2789
|
+
} catch (err) {
|
|
2790
|
+
report.recordError(err, { element: String(id) });
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
for (const { index, parent, element } of parents) {
|
|
2794
|
+
const parentIndex = ctx.sink.indexOf(parent);
|
|
2795
|
+
if (parentIndex === INVALID_INDEX) {
|
|
2796
|
+
report.error(
|
|
2797
|
+
"missing-value",
|
|
2798
|
+
JSON_ISSUE.UNKNOWN_PARENT,
|
|
2799
|
+
`${element}: parent ${JSON.stringify(parent)} is not a node`,
|
|
2800
|
+
{ element }
|
|
2801
|
+
);
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
ctx.nodes.set(parentColumn, index, parentIndex);
|
|
2805
|
+
}
|
|
2806
|
+
throwIfAborted(ctx.options.signal);
|
|
2807
|
+
importCytoscapeEdges(ctx, edges, edgeClassesColumn);
|
|
2808
|
+
ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
|
|
2809
|
+
}
|
|
2810
|
+
function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
|
|
2811
|
+
const ids = ctx.declareEdgeIds(edges, (edge) => isJsonObject(edge.data) ? edge.data.id : void 0, "id", true);
|
|
2812
|
+
const kind = ctx.uniformKind();
|
|
2813
|
+
for (let i = 0; i < edges.length; i++) {
|
|
2814
|
+
const element = `edges[${i}]`;
|
|
2815
|
+
const record = edges[i];
|
|
2816
|
+
if (!isJsonObject(record) || !isJsonObject(record.data)) {
|
|
2817
|
+
ctx.badElement("edge", element, "an element with a data object");
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
const { data } = record;
|
|
2821
|
+
if (!hasKey(data, "source") || !hasKey(data, "target")) {
|
|
2822
|
+
ctx.missingEndpoint(element, `data.${hasKey(data, "source") ? "target" : "source"}`);
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
try {
|
|
2826
|
+
const u = ctx.coerceId(data.source, `${element}.data.source`);
|
|
2827
|
+
const v = ctx.coerceId(data.target, `${element}.data.target`);
|
|
2828
|
+
if (u === null || v === null) {
|
|
2829
|
+
ctx.countSkipped("edge");
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
const idValue = ctx.edgeIdValue(ids, data.id);
|
|
2833
|
+
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(data), element);
|
|
2834
|
+
ctx.setEdgeId(ids, edge, idValue);
|
|
2835
|
+
const { weightFrom } = ctx.options;
|
|
2836
|
+
for (const key of Object.keys(data)) {
|
|
2837
|
+
if (key !== "id" && key !== "source" && key !== "target" && key !== weightFrom) {
|
|
2838
|
+
ctx.edges.write(edge, key, data[key], SUFFIX.data);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
|
|
2842
|
+
writeElementKeys(ctx.edges, edge, record);
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
ctx.skip(err, "edge", element);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
function cytoscapeSections(ctx, elements) {
|
|
2849
|
+
const { report } = ctx;
|
|
2850
|
+
if (Array.isArray(elements)) {
|
|
2851
|
+
return {
|
|
2852
|
+
nodes: elements.filter((item) => isJsonObject(item) && isNodeElement(item)),
|
|
2853
|
+
edges: elements.filter((item) => isJsonObject(item) && !isNodeElement(item))
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
if (isJsonObject(elements)) {
|
|
2857
|
+
return {
|
|
2858
|
+
nodes: arraySection(elements.nodes, "elements.nodes", report) ?? [],
|
|
2859
|
+
edges: arraySection(elements.edges, "elements.edges", report) ?? []
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
if (elements === void 0 || elements === null) {
|
|
2863
|
+
report.error("missing-value", JSON_ISSUE.MISSING_SECTION, "the document has no elements", {
|
|
2864
|
+
element: "elements"
|
|
2865
|
+
});
|
|
2866
|
+
return { nodes: [], edges: [] };
|
|
2867
|
+
}
|
|
2868
|
+
return report.fail(JSON_ISSUE.SHAPE, `elements must be an object or an array, found ${describe(elements)}`);
|
|
2869
|
+
}
|
|
2870
|
+
function isNodeElement(element) {
|
|
2871
|
+
if (element.group === "nodes") {
|
|
2872
|
+
return true;
|
|
2873
|
+
}
|
|
2874
|
+
if (element.group === "edges") {
|
|
2875
|
+
return false;
|
|
2876
|
+
}
|
|
2877
|
+
const { data } = element;
|
|
2878
|
+
return !(isJsonObject(data) && hasKey(data, "source") && hasKey(data, "target"));
|
|
2879
|
+
}
|
|
2880
|
+
function writeClasses(ctx, writer, column, row, raw, element) {
|
|
2881
|
+
if (column === INVALID_INDEX || raw === void 0 || raw === null) {
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
let classes;
|
|
2885
|
+
if (typeof raw === "string") {
|
|
2886
|
+
classes = raw.split(/\s+/).filter((c) => c.length > 0);
|
|
2887
|
+
} else if (Array.isArray(raw) && raw.every((c) => typeof c === "string")) {
|
|
2888
|
+
classes = raw;
|
|
2889
|
+
} else {
|
|
2890
|
+
ctx.report.error(
|
|
2891
|
+
"validation-error",
|
|
2892
|
+
JSON_ISSUE.BAD_VALUE,
|
|
2893
|
+
`${element}: classes must be a string or an array of strings`,
|
|
2894
|
+
{ element }
|
|
2895
|
+
);
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
writer.set(column, row, classes);
|
|
2899
|
+
}
|
|
2900
|
+
function writeElementKeys(writer, row, record) {
|
|
2901
|
+
for (const key of Object.keys(record)) {
|
|
2902
|
+
if (CYTOSCAPE_STRUCTURAL_KEYS.has(key)) {
|
|
2903
|
+
continue;
|
|
2904
|
+
}
|
|
2905
|
+
const name = CYTOSCAPE_ELEMENT_KEYS.has(key) ? key : `${key}${SUFFIX.element}`;
|
|
2906
|
+
writer.write(row, name, record[key], SUFFIX.data);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
const jsonImporter = Object.freeze({
|
|
2910
|
+
format: "json",
|
|
2911
|
+
extensions: Object.freeze([".json"]),
|
|
2912
|
+
mimeTypes: Object.freeze(["application/json"]),
|
|
2913
|
+
/**
|
|
2914
|
+
* Confidence that the head is a JSON graph document: 0 unless it starts with `{` or `[`, 0.5
|
|
2915
|
+
* for any JSON, 0.9 when a graph key (nodes, links, edges, elements, graph, graphs) appears in
|
|
2916
|
+
* the head.
|
|
2917
|
+
* @param head - the first bytes
|
|
2918
|
+
* @returns the confidence
|
|
2919
|
+
*/
|
|
2920
|
+
sniff(head) {
|
|
2921
|
+
const text = new TextDecoder("utf-8").decode(head.subarray(0, SNIFF_BYTES));
|
|
2922
|
+
const trimmed = (text.startsWith(BOM) ? text.slice(1) : text).trimStart();
|
|
2923
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
2924
|
+
return 0;
|
|
2925
|
+
}
|
|
2926
|
+
return SNIFF_KEYS.some((key) => trimmed.includes(key)) ? 0.9 : 0.5;
|
|
2927
|
+
},
|
|
2928
|
+
/**
|
|
2929
|
+
* Read a JSON graph document into the sink.
|
|
2930
|
+
* @param input - the text, bytes or stream
|
|
2931
|
+
* @param sink - the sink
|
|
2932
|
+
* @param options - format-specific and common options
|
|
2933
|
+
* @returns the import report; ImportError on a fatal error or beyond the error limit
|
|
2934
|
+
*/
|
|
2935
|
+
async import(input, sink, options) {
|
|
2936
|
+
const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
2937
|
+
const json = resolveJsonOptions(options);
|
|
2938
|
+
const report = new ImportReportBuilder("json", resolved.errorLimit);
|
|
2939
|
+
const text = await readText(input, report, resolved);
|
|
2940
|
+
const root = parseDocument(text, report);
|
|
2941
|
+
const dialect = detectDialect(root, json.dialect, report);
|
|
2942
|
+
const ctx = new ImportContext(sink, report, resolved, json, options?.defaultDirected !== void 0);
|
|
2943
|
+
reportSinkOptions(sink, options, report);
|
|
2944
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
2945
|
+
if (dialect === "cytoscape") {
|
|
2946
|
+
importCytoscape(ctx, root);
|
|
2947
|
+
throwIfAborted(resolved.signal);
|
|
2948
|
+
return report.finish();
|
|
2949
|
+
}
|
|
2950
|
+
const doc = isJsonObject(root) ? root : report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
|
|
2951
|
+
switch (dialect) {
|
|
2952
|
+
case "node-link":
|
|
2953
|
+
case "d3":
|
|
2954
|
+
importNodeLink(ctx, doc, dialect);
|
|
2955
|
+
break;
|
|
2956
|
+
case "jgf":
|
|
2957
|
+
importJgf(ctx, doc);
|
|
2958
|
+
break;
|
|
2959
|
+
case "graphology":
|
|
2960
|
+
importGraphology(ctx, doc);
|
|
2961
|
+
break;
|
|
2962
|
+
case "vis":
|
|
2963
|
+
importVis(ctx, doc);
|
|
2964
|
+
break;
|
|
2965
|
+
default: {
|
|
2966
|
+
const name = dialect;
|
|
2967
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, {
|
|
2968
|
+
option: "dialect",
|
|
2969
|
+
found: name
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
throwIfAborted(resolved.signal);
|
|
2974
|
+
return report.finish();
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2977
|
+
export {
|
|
2978
|
+
JSON_DIALECTS as J,
|
|
2979
|
+
JSON_ISSUE as a,
|
|
2980
|
+
JSON_LOSS as b,
|
|
2981
|
+
jsonExporter as c,
|
|
2982
|
+
dialectCapabilities as d,
|
|
2983
|
+
jsonImporter as e,
|
|
2984
|
+
jsonCapabilities as j,
|
|
2985
|
+
sniffJsonDialect as s
|
|
2986
|
+
};
|
|
2987
|
+
//# sourceMappingURL=importer-CQnJuWJw.js.map
|