@graphty/graph-io 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/chunks/{escape-BjmIaFFo.js → escape-D1f9-cwf.js} +4 -4
- package/dist/chunks/{escape-BjmIaFFo.js.map → escape-D1f9-cwf.js.map} +1 -1
- package/dist/chunks/{importer-BddBW3sf.js → importer-9PxhqH4v.js} +336 -92
- package/dist/chunks/importer-9PxhqH4v.js.map +1 -0
- package/dist/chunks/{importer-DfI8POXb.js → importer-B8lsjFWx.js} +6 -5
- package/dist/chunks/importer-B8lsjFWx.js.map +1 -0
- package/dist/chunks/{importer-BtHFICnf.js → importer-C3HcYWAn.js} +5 -3
- package/dist/chunks/importer-C3HcYWAn.js.map +1 -0
- package/dist/chunks/{importer-BOxwmef_.js → importer-C7mnGdr_.js} +125 -8
- package/dist/chunks/importer-C7mnGdr_.js.map +1 -0
- package/dist/chunks/{records-BKSMowhR.js → records-IHsCfv7s.js} +20 -7
- package/dist/chunks/records-IHsCfv7s.js.map +1 -0
- package/dist/chunks/{writer-BdMak4_J.js → writer-BtWpUaiH.js} +4 -4
- package/dist/chunks/{writer-BdMak4_J.js.map → writer-BtWpUaiH.js.map} +1 -1
- package/dist/csv.js +239 -18
- package/dist/csv.js.map +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +72 -33
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +44 -23
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +11 -11
- package/dist/graphml.js +1 -1
- package/dist/json.js +1 -1
- package/dist/neo4j.js +5 -4
- package/dist/neo4j.js.map +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/export.js +1 -1
- package/dist/src/common/export.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts +15 -4
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +125 -6
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts +36 -4
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +169 -8
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts +14 -1
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +28 -7
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +2 -0
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +36 -10
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts +9 -4
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +44 -21
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gexf/index.d.ts +1 -1
- package/dist/src/formats/gexf/index.d.ts.map +1 -1
- package/dist/src/formats/gexf/index.js +1 -1
- package/dist/src/formats/gexf/index.js.map +1 -1
- package/dist/src/formats/gexf/schema.d.ts +2 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -1
- package/dist/src/formats/gexf/schema.js +18 -0
- package/dist/src/formats/gexf/schema.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +1 -0
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts +7 -4
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +32 -19
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/index.d.ts +3 -1
- package/dist/src/formats/gml/index.d.ts.map +1 -1
- package/dist/src/formats/gml/index.js +4 -2
- package/dist/src/formats/gml/index.js.map +1 -1
- package/dist/src/formats/graphml/constants.d.ts +2 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -1
- package/dist/src/formats/graphml/constants.js +2 -0
- package/dist/src/formats/graphml/constants.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +14 -2
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts +2 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +91 -27
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts +62 -0
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -0
- package/dist/src/formats/graphml/yfiles.js +269 -0
- package/dist/src/formats/graphml/yfiles.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +9 -1
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +14 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +165 -4
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +1 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +3 -2
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/package.json +2 -2
- package/src/common/export.ts +1 -1
- package/src/formats/csv/exporter.ts +140 -10
- package/src/formats/csv/importer.ts +207 -18
- package/src/formats/csv/records.ts +41 -6
- package/src/formats/dot/exporter.ts +2 -0
- package/src/formats/gexf/exporter.ts +40 -11
- package/src/formats/gexf/importer.ts +47 -25
- package/src/formats/gexf/index.ts +1 -1
- package/src/formats/gexf/schema.ts +18 -0
- package/src/formats/gml/exporter.ts +1 -0
- package/src/formats/gml/importer.ts +47 -24
- package/src/formats/gml/index.ts +4 -1
- package/src/formats/graphml/constants.ts +2 -0
- package/src/formats/graphml/exporter.ts +21 -2
- package/src/formats/graphml/importer.ts +99 -28
- package/src/formats/graphml/yfiles.ts +292 -0
- package/src/formats/json/exporter.ts +9 -1
- package/src/formats/json/importer.ts +202 -4
- package/src/formats/neo4j/exporter.ts +1 -0
- package/src/formats/pajek/exporter.ts +3 -1
- package/dist/chunks/importer-BOxwmef_.js.map +0 -1
- package/dist/chunks/importer-BddBW3sf.js.map +0 -1
- package/dist/chunks/importer-BtHFICnf.js.map +0 -1
- package/dist/chunks/importer-DfI8POXb.js.map +0 -1
- package/dist/chunks/records-BKSMowhR.js.map +0 -1
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* `sourceport` / `targetport` edge attributes are kept as role columns; hyperedges follow the
|
|
10
10
|
* `hyperedges` option (refuse, skip with a report entry, star, clique); yFiles `yfiles.type` keys
|
|
11
11
|
* become `json` columns holding the nested XML as a tree (origin.namespace "yfiles"), reported as
|
|
12
|
-
* a loss note because the structure, not the bytes, is preserved
|
|
12
|
+
* a loss note because the structure, not the bytes, is preserved; a `y:ShapeNode` or
|
|
13
|
+
* `y:PolyLineEdge` in such a tree is also read into typed `yfiles.*` columns (yfiles.ts).
|
|
13
14
|
*
|
|
14
15
|
* Ids are coerced with the common rule (`ids: "canonical"` by default, design section 4.1); the
|
|
15
16
|
* edge attribute whose `attr.name` is `weightFrom` ("weight" by default) is THE weight and is
|
|
@@ -82,12 +83,19 @@ import {
|
|
|
82
83
|
XSI_NAMESPACE,
|
|
83
84
|
} from "./constants.js";
|
|
84
85
|
import { XmlTreeBuilder } from "./tree.js";
|
|
85
|
-
|
|
86
|
+
import { graphicsDecl, graphicsValues } from "./yfiles.js";
|
|
86
87
|
|
|
87
88
|
/** The XML attributes the importer reads on `<graph>`, `<node>` and `<edge>`; any other is reported. */
|
|
88
89
|
const GRAPH_ATTRIBUTES: ReadonlySet<string> = new Set(["id", "edgedefault", "parse.nodes", "parse.edges"]);
|
|
89
90
|
const NODE_ATTRIBUTES: ReadonlySet<string> = new Set(["id"]);
|
|
90
|
-
const EDGE_ATTRIBUTES: ReadonlySet<string> = new Set([
|
|
91
|
+
const EDGE_ATTRIBUTES: ReadonlySet<string> = new Set([
|
|
92
|
+
"id",
|
|
93
|
+
"source",
|
|
94
|
+
"target",
|
|
95
|
+
"directed",
|
|
96
|
+
"sourceport",
|
|
97
|
+
"targetport",
|
|
98
|
+
]);
|
|
91
99
|
|
|
92
100
|
/** The format-specific options of the GraphML importer. */
|
|
93
101
|
export interface GraphmlImportOptions {
|
|
@@ -316,7 +324,8 @@ class GraphmlReader implements XmlHandler {
|
|
|
316
324
|
|
|
317
325
|
private readonly ctx: Ctx[] = [];
|
|
318
326
|
|
|
319
|
-
|
|
327
|
+
/** Keys by id; one id may be declared once per domain (igraph writes `name` for graph and node). */
|
|
328
|
+
private readonly keys = new Map<string, KeyEntry[]>();
|
|
320
329
|
|
|
321
330
|
private readonly graphs: GraphState[] = [];
|
|
322
331
|
|
|
@@ -366,6 +375,9 @@ class GraphmlReader implements XmlHandler {
|
|
|
366
375
|
|
|
367
376
|
private yfilesLossNoted = false;
|
|
368
377
|
|
|
378
|
+
/** The mapped yFiles graphics columns declared so far, by domain and field. */
|
|
379
|
+
private readonly graphicsColumns = new Map<string, ColumnHandle>();
|
|
380
|
+
|
|
369
381
|
private hyperedgeCount = 0;
|
|
370
382
|
|
|
371
383
|
private edgeIdHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
|
|
@@ -999,8 +1011,9 @@ class GraphmlReader implements XmlHandler {
|
|
|
999
1011
|
}
|
|
1000
1012
|
this.pendingKey = {
|
|
1001
1013
|
id,
|
|
1002
|
-
|
|
1003
|
-
|
|
1014
|
+
// `name` / `type` without the `attr.` prefix are written by some tools; the spec's spelling wins
|
|
1015
|
+
attrName: attrs.get("attr.name") ?? attrs.get("name") ?? null,
|
|
1016
|
+
attrType: attrs.get("attr.type") ?? attrs.get("type") ?? null,
|
|
1004
1017
|
yfilesType: attrs.get("yfiles.type") ?? null,
|
|
1005
1018
|
domains,
|
|
1006
1019
|
line,
|
|
@@ -1023,11 +1036,12 @@ class GraphmlReader implements XmlHandler {
|
|
|
1023
1036
|
this.report.error("validation-error", GRAPHML_ISSUE.KEY_MISSING_ID, "<key> without an id", where);
|
|
1024
1037
|
return;
|
|
1025
1038
|
}
|
|
1026
|
-
|
|
1039
|
+
const declared = this.keys.get(key.id) ?? [];
|
|
1040
|
+
if (declared.some((entry) => entry.domains.some((domain) => key.domains.includes(domain)))) {
|
|
1027
1041
|
this.report.error(
|
|
1028
1042
|
"validation-error",
|
|
1029
1043
|
GRAPHML_ISSUE.DUPLICATE_KEY,
|
|
1030
|
-
`key "${key.id}" is declared twice`,
|
|
1044
|
+
`key "${key.id}" is declared twice for the same kind of element`,
|
|
1031
1045
|
where,
|
|
1032
1046
|
);
|
|
1033
1047
|
return;
|
|
@@ -1078,18 +1092,21 @@ class GraphmlReader implements XmlHandler {
|
|
|
1078
1092
|
this.report.recordError(err, where);
|
|
1079
1093
|
}
|
|
1080
1094
|
}
|
|
1081
|
-
this.keys.set(key.id,
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1095
|
+
this.keys.set(key.id, [
|
|
1096
|
+
...declared,
|
|
1097
|
+
{
|
|
1098
|
+
id: key.id,
|
|
1099
|
+
name,
|
|
1100
|
+
node,
|
|
1101
|
+
edge,
|
|
1102
|
+
graph,
|
|
1103
|
+
weight,
|
|
1104
|
+
originalId,
|
|
1105
|
+
yfiles,
|
|
1106
|
+
skipped,
|
|
1107
|
+
domains: key.domains,
|
|
1108
|
+
},
|
|
1109
|
+
]);
|
|
1093
1110
|
}
|
|
1094
1111
|
|
|
1095
1112
|
/**
|
|
@@ -1241,7 +1258,8 @@ class GraphmlReader implements XmlHandler {
|
|
|
1241
1258
|
line,
|
|
1242
1259
|
});
|
|
1243
1260
|
} else {
|
|
1244
|
-
const
|
|
1261
|
+
const entries = this.keys.get(keyId);
|
|
1262
|
+
const key = entries?.find((entry) => entry.domains.includes(domain)) ?? entries?.[0];
|
|
1245
1263
|
if (key === undefined) {
|
|
1246
1264
|
this.report.error(
|
|
1247
1265
|
"validation-error",
|
|
@@ -1324,17 +1342,17 @@ class GraphmlReader implements XmlHandler {
|
|
|
1324
1342
|
}
|
|
1325
1343
|
return;
|
|
1326
1344
|
}
|
|
1345
|
+
if (key.yfiles) {
|
|
1346
|
+
for (const [handle, mapped] of this.graphics("node", key, value, where)) {
|
|
1347
|
+
this.setNodeData(node, handle, mapped);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1327
1350
|
if (key.node === null) {
|
|
1328
1351
|
return;
|
|
1329
1352
|
}
|
|
1330
1353
|
const parsed = this.parseValue(key.node, value, where);
|
|
1331
|
-
if (parsed
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1334
|
-
if (node.index === INVALID_INDEX) {
|
|
1335
|
-
node.pending.push(key.node.handle, parsed);
|
|
1336
|
-
} else if (!node.failed) {
|
|
1337
|
-
this.sink.setNodeValue(key.node.handle, node.index, parsed);
|
|
1354
|
+
if (parsed !== undefined) {
|
|
1355
|
+
this.setNodeData(node, key.node.handle, parsed);
|
|
1338
1356
|
}
|
|
1339
1357
|
return;
|
|
1340
1358
|
}
|
|
@@ -1361,6 +1379,11 @@ class GraphmlReader implements XmlHandler {
|
|
|
1361
1379
|
}
|
|
1362
1380
|
return;
|
|
1363
1381
|
}
|
|
1382
|
+
if (key.yfiles) {
|
|
1383
|
+
for (const [handle, mapped] of this.graphics("edge", key, value, where)) {
|
|
1384
|
+
edge.pending.push(handle, mapped);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1364
1387
|
if (key.edge === null) {
|
|
1365
1388
|
return;
|
|
1366
1389
|
}
|
|
@@ -1391,6 +1414,54 @@ class GraphmlReader implements XmlHandler {
|
|
|
1391
1414
|
}
|
|
1392
1415
|
}
|
|
1393
1416
|
|
|
1417
|
+
/**
|
|
1418
|
+
* Write a node value now, or keep it until the node is added.
|
|
1419
|
+
* @param node - the node state
|
|
1420
|
+
* @param handle - the column
|
|
1421
|
+
* @param value - the value
|
|
1422
|
+
*/
|
|
1423
|
+
private setNodeData(node: NodeState, handle: ColumnHandle, value: unknown): void {
|
|
1424
|
+
if (node.index === INVALID_INDEX) {
|
|
1425
|
+
node.pending.push(handle, value);
|
|
1426
|
+
} else if (!node.failed) {
|
|
1427
|
+
this.sink.setNodeValue(handle, node.index, value);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* The typed values mapped from a yFiles graphics tree (yfiles.ts), each with its column,
|
|
1433
|
+
* declared on first use.
|
|
1434
|
+
* @param domain - node or edge
|
|
1435
|
+
* @param key - the yFiles key
|
|
1436
|
+
* @param tree - the `<data>` content
|
|
1437
|
+
* @param where - the location for issues
|
|
1438
|
+
* @returns column handle and value pairs
|
|
1439
|
+
*/
|
|
1440
|
+
private graphics(domain: "node" | "edge", key: KeyEntry, tree: unknown, where: Where): [ColumnHandle, unknown][] {
|
|
1441
|
+
const out: [ColumnHandle, unknown][] = [];
|
|
1442
|
+
for (const [field, value] of graphicsValues(domain, tree)) {
|
|
1443
|
+
const id = `${domain}:${field}`;
|
|
1444
|
+
let handle = this.graphicsColumns.get(id);
|
|
1445
|
+
if (handle === undefined) {
|
|
1446
|
+
try {
|
|
1447
|
+
({ handle } = declareResolved(
|
|
1448
|
+
this.sink,
|
|
1449
|
+
domain,
|
|
1450
|
+
graphicsDecl(domain, field, key.id),
|
|
1451
|
+
this.report,
|
|
1452
|
+
where,
|
|
1453
|
+
));
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
this.report.recordError(err, where);
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
this.graphicsColumns.set(id, handle);
|
|
1459
|
+
}
|
|
1460
|
+
out.push([handle, value]);
|
|
1461
|
+
}
|
|
1462
|
+
return out;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1394
1465
|
/**
|
|
1395
1466
|
* Parse the content of a `<data>` by its column's declaration.
|
|
1396
1467
|
* @param target - the column
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yFiles / yEd graphics read out of the JSON tree of a `yfiles.type` key (tree.ts) into typed
|
|
3
|
+
* columns, beside the tree itself: a `y:ShapeNode` gives the node's position, size, fill and
|
|
4
|
+
* border colours, label and shape, and a `y:PolyLineEdge` gives the edge's line colour and width,
|
|
5
|
+
* its arrows and the direction they draw. The values are the ones graphty-element's own GraphML
|
|
6
|
+
* parser produced, so a consumer rebuilds its records by dropping the `yfiles.` prefix: colours
|
|
7
|
+
* are `#RRGGBB` in upper case (a `#RGB` is expanded, any other text is kept as written), numbers
|
|
8
|
+
* are parsed with parseFloat, the position is `[x, y, 0]` and the shape is the yFiles shape type
|
|
9
|
+
* as written. One difference: a label whose text is a number (`<y:NodeLabel>0</y:NodeLabel>`) is
|
|
10
|
+
* kept as that text, where the element's parser turned it into a number and stored `""`, nothing
|
|
11
|
+
* or the number.
|
|
12
|
+
*
|
|
13
|
+
* The columns are views of the tree: the exporter writes the tree and never these columns, and
|
|
14
|
+
* re-importing the tree gives them back. A value edited after import (a layout writing the
|
|
15
|
+
* position) or a tree column removed is therefore not written, and the exporter reports it as a
|
|
16
|
+
* loss (staleGraphicsRows()).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { type Column, type ColumnDecl, type JsonColumn } from "@graphty/graph-format";
|
|
20
|
+
|
|
21
|
+
import { localName } from "../../common/xml.js";
|
|
22
|
+
|
|
23
|
+
/** The name prefix of every column mapped from yFiles graphics. */
|
|
24
|
+
const YFILES_COLUMN_PREFIX = "yfiles.";
|
|
25
|
+
|
|
26
|
+
type Tree = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
/** The mapped node columns, by the field name graphty-element's records use. */
|
|
29
|
+
const NODE_FIELDS: Readonly<Record<string, Omit<ColumnDecl, "name">>> = {
|
|
30
|
+
position: { dtype: "f64", components: 3, role: "position", mutable: true },
|
|
31
|
+
width: { dtype: "f64" },
|
|
32
|
+
height: { dtype: "f64" },
|
|
33
|
+
color: { dtype: "string" },
|
|
34
|
+
borderColor: { dtype: "string" },
|
|
35
|
+
borderWidth: { dtype: "f64" },
|
|
36
|
+
label: { dtype: "string", role: "label" },
|
|
37
|
+
shape: { dtype: "string" },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** The mapped edge columns, by field name. */
|
|
41
|
+
const EDGE_FIELDS: Readonly<Record<string, Omit<ColumnDecl, "name">>> = {
|
|
42
|
+
color: { dtype: "string" },
|
|
43
|
+
width: { dtype: "f64" },
|
|
44
|
+
directed: { dtype: "bool" },
|
|
45
|
+
targetArrow: { dtype: "string" },
|
|
46
|
+
sourceArrow: { dtype: "string" },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The declaration of a mapped column.
|
|
51
|
+
* @param domain - node or edge
|
|
52
|
+
* @param field - the field name (a key of the values graphicsValues() returns)
|
|
53
|
+
* @param keyId - the id of the GraphML key whose tree it is read from
|
|
54
|
+
* @returns the declaration
|
|
55
|
+
*/
|
|
56
|
+
export function graphicsDecl(domain: "node" | "edge", field: string, keyId: string): ColumnDecl {
|
|
57
|
+
const fields = domain === "node" ? NODE_FIELDS : EDGE_FIELDS;
|
|
58
|
+
return {
|
|
59
|
+
...fields[field],
|
|
60
|
+
name: YFILES_COLUMN_PREFIX + field,
|
|
61
|
+
nullable: true,
|
|
62
|
+
origin: {
|
|
63
|
+
format: "graphml",
|
|
64
|
+
id: keyId,
|
|
65
|
+
title: null,
|
|
66
|
+
type: domain === "node" ? "ShapeNode" : "PolyLineEdge",
|
|
67
|
+
namespace: "yfiles",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether a column is one graphicsDecl() declared (a view of a yFiles tree, never exported).
|
|
74
|
+
* @param meta - the column's name, dtype and origin
|
|
75
|
+
* @param meta.name - the column name
|
|
76
|
+
* @param meta.dtype - the column dtype
|
|
77
|
+
* @param meta.origin - the column origin
|
|
78
|
+
* @returns true for a mapped graphics column
|
|
79
|
+
*/
|
|
80
|
+
export function isGraphicsColumn(meta: {
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly dtype: string;
|
|
83
|
+
readonly origin: { readonly namespace: string | null } | null;
|
|
84
|
+
}): boolean {
|
|
85
|
+
return meta.dtype !== "json" && meta.origin?.namespace === "yfiles" && meta.name.startsWith(YFILES_COLUMN_PREFIX);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The rows of a mapped graphics column whose value is not one its yFiles tree columns (in the
|
|
90
|
+
* same table) give: every set row when no tree column gives a value for the row. A file may
|
|
91
|
+
* declare several yFiles keys for one domain, and the mapped column holds the values of all of
|
|
92
|
+
* them, so a row is current when any tree's value matches. These are the values an export loses,
|
|
93
|
+
* since only the trees are written.
|
|
94
|
+
* @param column - the mapped column (isGraphicsColumn())
|
|
95
|
+
* @param table - the table it is in
|
|
96
|
+
* @param domain - node or edge
|
|
97
|
+
* @returns the number of rows that differ from the trees
|
|
98
|
+
*/
|
|
99
|
+
export function staleGraphicsRows(column: Column, table: Iterable<Column>, domain: "node" | "edge"): number {
|
|
100
|
+
const field = column.meta.name.slice(YFILES_COLUMN_PREFIX.length);
|
|
101
|
+
const trees: JsonColumn[] = [];
|
|
102
|
+
for (const c of table) {
|
|
103
|
+
if (c.dtype === "json" && c.meta.origin?.namespace === "yfiles") {
|
|
104
|
+
trees.push(c);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let stale = 0;
|
|
108
|
+
for (let r = 0; r < column.length; r++) {
|
|
109
|
+
const wants = new Set<string>();
|
|
110
|
+
for (const tree of trees) {
|
|
111
|
+
const want = graphicsValues(domain, tree.values[r]).find(([f]) => f === field);
|
|
112
|
+
if (want !== undefined) {
|
|
113
|
+
wants.add(JSON.stringify(want[1]));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (wants.size === 0) {
|
|
117
|
+
wants.add(JSON.stringify(undefined));
|
|
118
|
+
}
|
|
119
|
+
const value = column.value(r);
|
|
120
|
+
const have = column.meta.components > 1 && value !== undefined ? Array.from(value as ArrayLike<number>) : value;
|
|
121
|
+
if (!wants.has(JSON.stringify(have))) {
|
|
122
|
+
stale++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return stale;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The mapped values of one `<data>` tree of a yFiles key: a ShapeNode's for a node, a
|
|
130
|
+
* PolyLineEdge's for an edge; nothing for any other graphics (GenericNode, BezierEdge, ...).
|
|
131
|
+
* @param domain - node or edge
|
|
132
|
+
* @param tree - the `<data>` content as tree.ts builds it
|
|
133
|
+
* @returns field name -> value, in declaration order
|
|
134
|
+
*/
|
|
135
|
+
export function graphicsValues(domain: "node" | "edge", tree: unknown): [string, unknown][] {
|
|
136
|
+
const out: [string, unknown][] = [];
|
|
137
|
+
if (domain === "node") {
|
|
138
|
+
const shape = child(tree, "ShapeNode");
|
|
139
|
+
if (shape !== undefined) {
|
|
140
|
+
shapeNodeValues(shape, out);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
const edge = child(tree, "PolyLineEdge");
|
|
144
|
+
if (edge !== undefined) {
|
|
145
|
+
polyLineEdgeValues(edge, out);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Read a ShapeNode.
|
|
153
|
+
* @param shape - the ShapeNode element
|
|
154
|
+
* @param out - receives the values
|
|
155
|
+
*/
|
|
156
|
+
function shapeNodeValues(shape: unknown, out: [string, unknown][]): void {
|
|
157
|
+
const geometry = child(shape, "Geometry");
|
|
158
|
+
// yFiles requires x, y, width and height on Geometry; a position needs both coordinates
|
|
159
|
+
const x = number(attr(geometry, "x"));
|
|
160
|
+
const y = number(attr(geometry, "y"));
|
|
161
|
+
if (x !== undefined && y !== undefined) {
|
|
162
|
+
out.push(["position", [x, y, 0]]);
|
|
163
|
+
}
|
|
164
|
+
push(out, "width", number(attr(geometry, "width")));
|
|
165
|
+
push(out, "height", number(attr(geometry, "height")));
|
|
166
|
+
push(out, "color", color(attr(child(shape, "Fill"), "color")));
|
|
167
|
+
const border = child(shape, "BorderStyle");
|
|
168
|
+
push(out, "borderColor", color(attr(border, "color")));
|
|
169
|
+
push(out, "borderWidth", number(attr(border, "width")));
|
|
170
|
+
const label = child(shape, "NodeLabel");
|
|
171
|
+
// an element with no text and no attributes is "" (no label); one with attributes but no text is a blank label
|
|
172
|
+
if (typeof label === "string" ? label.length > 0 : label !== undefined) {
|
|
173
|
+
// trimmed, as the element's parser (trimValues) reads it: pretty-printed yEd puts whitespace before the label's children
|
|
174
|
+
out.push(["label", (typeof label === "string" ? label : (text(label) ?? "")).trim()]);
|
|
175
|
+
}
|
|
176
|
+
push(out, "shape", nonEmpty(attr(child(shape, "Shape"), "type")));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read a PolyLineEdge.
|
|
181
|
+
* @param edge - the PolyLineEdge element
|
|
182
|
+
* @param out - receives the values
|
|
183
|
+
*/
|
|
184
|
+
function polyLineEdgeValues(edge: unknown, out: [string, unknown][]): void {
|
|
185
|
+
const line = child(edge, "LineStyle");
|
|
186
|
+
push(out, "color", color(attr(line, "color")));
|
|
187
|
+
push(out, "width", number(attr(line, "width")));
|
|
188
|
+
const arrows = child(edge, "Arrows");
|
|
189
|
+
if (arrows !== undefined && typeof arrows === "object") {
|
|
190
|
+
const target = attr(arrows, "target");
|
|
191
|
+
const source = attr(arrows, "source");
|
|
192
|
+
// yEd draws direction with the target arrow; the file's edgedefault still decides topology
|
|
193
|
+
out.push(["directed", target !== undefined && target !== "none"]);
|
|
194
|
+
push(out, "targetArrow", target === "none" ? undefined : nonEmpty(target));
|
|
195
|
+
push(out, "sourceArrow", source === "none" ? undefined : nonEmpty(source));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Append a value when it is defined.
|
|
201
|
+
* @param out - the values
|
|
202
|
+
* @param field - the field name
|
|
203
|
+
* @param value - the value
|
|
204
|
+
*/
|
|
205
|
+
function push(out: [string, unknown][], field: string, value: unknown): void {
|
|
206
|
+
if (value !== undefined) {
|
|
207
|
+
out.push([field, value]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The first child element of a tree node by local name, whatever its namespace prefix.
|
|
213
|
+
* @param node - the tree node
|
|
214
|
+
* @param local - the local name
|
|
215
|
+
* @returns the child, or undefined
|
|
216
|
+
*/
|
|
217
|
+
function child(node: unknown, local: string): unknown {
|
|
218
|
+
if (typeof node !== "object" || node === null) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
for (const [key, value] of Object.entries(node as Tree)) {
|
|
222
|
+
if (!key.startsWith("@_") && key !== "#text" && localName(key) === local) {
|
|
223
|
+
return Array.isArray(value) ? value[0] : value;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* An attribute of a tree node.
|
|
231
|
+
* @param node - the tree node
|
|
232
|
+
* @param name - the attribute name
|
|
233
|
+
* @returns the text, or undefined
|
|
234
|
+
*/
|
|
235
|
+
function attr(node: unknown, name: string): string | undefined {
|
|
236
|
+
if (typeof node !== "object" || node === null) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const value = (node as Tree)[`@_${name}`];
|
|
240
|
+
return typeof value === "string" ? value : undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The character data of an element that also has attributes or children.
|
|
245
|
+
* @param node - the tree node
|
|
246
|
+
* @returns the text, or undefined
|
|
247
|
+
*/
|
|
248
|
+
function text(node: unknown): string | undefined {
|
|
249
|
+
const value = typeof node === "object" && node !== null ? (node as Tree)["#text"] : undefined;
|
|
250
|
+
return typeof value === "string" ? value : undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* A non-empty text.
|
|
255
|
+
* @param value - the text
|
|
256
|
+
* @returns the text, or undefined when absent or empty
|
|
257
|
+
*/
|
|
258
|
+
function nonEmpty(value: string | undefined): string | undefined {
|
|
259
|
+
return value === undefined || value.length === 0 ? undefined : value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* A number attribute, parsed as graphty-element's parser parsed it.
|
|
264
|
+
* @param value - the text
|
|
265
|
+
* @returns the number, or undefined when absent, empty or not a number
|
|
266
|
+
*/
|
|
267
|
+
function number(value: string | undefined): number | undefined {
|
|
268
|
+
if (value === undefined || value.length === 0) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
const n = Number.parseFloat(value);
|
|
272
|
+
return Number.isNaN(n) ? undefined : n;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A colour attribute: `#RRGGBB` upper-cased, `#RGB` expanded, anything else as written.
|
|
277
|
+
* @param value - the text
|
|
278
|
+
* @returns the colour, or undefined when absent or empty
|
|
279
|
+
*/
|
|
280
|
+
function color(value: string | undefined): string | undefined {
|
|
281
|
+
if (value === undefined || value.length === 0) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
|
|
285
|
+
return value.toUpperCase();
|
|
286
|
+
}
|
|
287
|
+
if (/^#[0-9A-Fa-f]{3}$/.test(value)) {
|
|
288
|
+
const [, r, g, b] = value;
|
|
289
|
+
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
|
|
290
|
+
}
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
@@ -124,7 +124,15 @@ const STRUCTURAL_ROLES: ReadonlySet<string> = new Set([
|
|
|
124
124
|
]);
|
|
125
125
|
|
|
126
126
|
/** The roles no JSON dialect can carry; skipped (checkCapabilities reports them). */
|
|
127
|
-
const TEMPORAL_ROLES: ReadonlySet<string> = new Set([
|
|
127
|
+
const TEMPORAL_ROLES: ReadonlySet<string> = new Set([
|
|
128
|
+
"start",
|
|
129
|
+
"end",
|
|
130
|
+
"timestamp",
|
|
131
|
+
"timestamps",
|
|
132
|
+
"spells",
|
|
133
|
+
"open",
|
|
134
|
+
"spellsOpen",
|
|
135
|
+
]);
|
|
128
136
|
|
|
129
137
|
/** The dialects whose attributes live in a nested dict (data / metadata / attributes). */
|
|
130
138
|
const NESTED_DIALECTS: ReadonlySet<JsonDialect> = new Set(["jgf", "cytoscape", "graphology"]);
|