@graphty/graph-io 0.3.9 → 0.3.10
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
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* forward reference is legal; element lifetimes (`start` / `end` / `timestamp`, `<spells>`, 1.3
|
|
11
11
|
* `timestamps` / `intervals`, 1.2 `startopen` / `endopen` holding the open bound's time) become
|
|
12
12
|
* the temporal role columns; dynamic attribute values go into the temporal extension tables of
|
|
13
|
-
* design section 5.10
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* design section 5.10, and a spell's open bounds go to the spells.open column; the graph header's
|
|
14
|
+
* `mode`, `timeformat`, `timerepresentation` and `idtype` land in the graph meta, and its
|
|
15
|
+
* `defaultedgetype`, `start`, `end` and `timestamp` as written in `meta.extra.gexf` (`idtype` is
|
|
16
|
+
* informational: Gephi writes `idtype="string"` for every file, so ids follow the `ids` rule,
|
|
17
|
+
* canonical by default).
|
|
16
18
|
*
|
|
17
19
|
* Direction follows design section 8.4: `defaultedgetype` (undirected when absent, per the spec and
|
|
18
20
|
* the `defaultDirected` option) sets the sink's direction once, and every per-edge `type` override,
|
|
@@ -136,7 +138,10 @@ export const WEIGHT_IGNORED_CODE = "W_GEXF_WEIGHT_IGNORED";
|
|
|
136
138
|
export const VIZ_SKIPPED_CODE = "W_GEXF_VIZ_SKIPPED";
|
|
137
139
|
/** Issue code: a 1.2 dynamic viz element (its bounds are dropped, the value kept). */
|
|
138
140
|
export const VIZ_DYNAMIC_CODE = "W_GEXF_VIZ_DYNAMIC_DROPPED";
|
|
139
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Issue code: `startopen` / `endopen` on a `<spell>` stored closed. No longer recorded: the open
|
|
143
|
+
* bits of each spell are kept in the spells.open column. Kept so the exported code table is stable.
|
|
144
|
+
*/
|
|
140
145
|
export const SPELL_OPEN_CODE = "W_GEXF_SPELL_OPEN_DROPPED";
|
|
141
146
|
/** Issue code: a viz element with a value that does not parse (the element is skipped). */
|
|
142
147
|
export const VIZ_VALUE_CODE = "W_GEXF_VIZ_VALUE";
|
|
@@ -153,6 +158,16 @@ export {
|
|
|
153
158
|
};
|
|
154
159
|
|
|
155
160
|
const EDGE_TYPES: ReadonlySet<string> = new Set(["directed", "undirected", "mutual"]);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* An edge type as a keyword of {@link EDGE_TYPES}: the schema spells them in lower case, but files
|
|
164
|
+
* in the wild write `Directed`, and the keyword is read whatever its case or surrounding space.
|
|
165
|
+
* @param text - a `defaultedgetype` or edge `type` value
|
|
166
|
+
* @returns the text trimmed and in lower case
|
|
167
|
+
*/
|
|
168
|
+
function edgeTypeKeyword(text: string): string {
|
|
169
|
+
return text.trim().toLowerCase();
|
|
170
|
+
}
|
|
156
171
|
const ABORT_CHECK_INTERVAL = 64;
|
|
157
172
|
|
|
158
173
|
/** The XML attributes the importer reads on `<node>`; any other one is reported once. */
|
|
@@ -351,6 +366,7 @@ interface DomainColumns {
|
|
|
351
366
|
readonly end: TemporalColumn;
|
|
352
367
|
readonly timestamp: TemporalColumn;
|
|
353
368
|
readonly spells: LazyColumn;
|
|
369
|
+
readonly spellsOpen: LazyColumn;
|
|
354
370
|
readonly timestamps: LazyColumn;
|
|
355
371
|
readonly open: LazyColumn;
|
|
356
372
|
readonly color: LazyColumn;
|
|
@@ -502,6 +518,9 @@ class GexfReader implements XmlHandler {
|
|
|
502
518
|
|
|
503
519
|
private spellPairs: number[][] | null = null;
|
|
504
520
|
|
|
521
|
+
/** The open bits of each spell in spellPairs, in the same order. */
|
|
522
|
+
private spellOpen: number[] = [];
|
|
523
|
+
|
|
505
524
|
private spellsWhere: IssueLocation | null = null;
|
|
506
525
|
|
|
507
526
|
private parentIds: NodeId[] | null = null;
|
|
@@ -532,6 +551,7 @@ class GexfReader implements XmlHandler {
|
|
|
532
551
|
end: new TemporalColumn("node", NODE_DECLS.end),
|
|
533
552
|
timestamp: new TemporalColumn("node", NODE_DECLS.timestamp),
|
|
534
553
|
spells: new LazyColumn("node", NODE_DECLS.spells),
|
|
554
|
+
spellsOpen: new LazyColumn("node", NODE_DECLS.spellsOpen),
|
|
535
555
|
timestamps: new LazyColumn("node", NODE_DECLS.timestamps),
|
|
536
556
|
open: new LazyColumn("node", NODE_DECLS.open),
|
|
537
557
|
color: new LazyColumn("node", NODE_DECLS.color),
|
|
@@ -543,6 +563,7 @@ class GexfReader implements XmlHandler {
|
|
|
543
563
|
end: new TemporalColumn("edge", EDGE_DECLS.end),
|
|
544
564
|
timestamp: new TemporalColumn("edge", EDGE_DECLS.timestamp),
|
|
545
565
|
spells: new LazyColumn("edge", EDGE_DECLS.spells),
|
|
566
|
+
spellsOpen: new LazyColumn("edge", EDGE_DECLS.spellsOpen),
|
|
546
567
|
timestamps: new LazyColumn("edge", EDGE_DECLS.timestamps),
|
|
547
568
|
open: new LazyColumn("edge", EDGE_DECLS.open),
|
|
548
569
|
color: new LazyColumn("edge", EDGE_DECLS.color),
|
|
@@ -801,8 +822,8 @@ class GexfReader implements XmlHandler {
|
|
|
801
822
|
const edgeType = attrs.get("defaultedgetype");
|
|
802
823
|
if (edgeType === undefined) {
|
|
803
824
|
this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
|
|
804
|
-
} else if (EDGE_TYPES.has(edgeType)) {
|
|
805
|
-
this.defaultKind = edgeType as GexfEdgeType;
|
|
825
|
+
} else if (EDGE_TYPES.has(edgeTypeKeyword(edgeType))) {
|
|
826
|
+
this.defaultKind = edgeTypeKeyword(edgeType) as GexfEdgeType;
|
|
806
827
|
} else {
|
|
807
828
|
this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
|
|
808
829
|
this.warnHeader("defaultedgetype", edgeType, where);
|
|
@@ -846,7 +867,7 @@ class GexfReader implements XmlHandler {
|
|
|
846
867
|
}
|
|
847
868
|
}
|
|
848
869
|
const extra: Record<string, unknown> = {};
|
|
849
|
-
for (const key of ["start", "end", "timestamp"]) {
|
|
870
|
+
for (const key of ["defaultedgetype", "start", "end", "timestamp"]) {
|
|
850
871
|
const value = attrs.get(key);
|
|
851
872
|
if (value !== undefined) {
|
|
852
873
|
extra[key] = value;
|
|
@@ -1256,6 +1277,7 @@ class GexfReader implements XmlHandler {
|
|
|
1256
1277
|
return;
|
|
1257
1278
|
case "spells":
|
|
1258
1279
|
this.spellPairs = [];
|
|
1280
|
+
this.spellOpen = [];
|
|
1259
1281
|
this.spellsWhere = frame.where;
|
|
1260
1282
|
this.ctx.push(Ctx.Spells);
|
|
1261
1283
|
return;
|
|
@@ -1408,13 +1430,14 @@ class GexfReader implements XmlHandler {
|
|
|
1408
1430
|
return;
|
|
1409
1431
|
}
|
|
1410
1432
|
let kind: EdgeKind = this.defaultKind;
|
|
1411
|
-
const
|
|
1412
|
-
if (
|
|
1433
|
+
const typeText = attrs.get("type");
|
|
1434
|
+
if (typeText !== undefined) {
|
|
1435
|
+
const type = edgeTypeKeyword(typeText);
|
|
1413
1436
|
if (!EDGE_TYPES.has(type)) {
|
|
1414
1437
|
report.error(
|
|
1415
1438
|
"validation-error",
|
|
1416
1439
|
EDGE_TYPE_CODE,
|
|
1417
|
-
`edge type "${
|
|
1440
|
+
`edge type "${typeText}" is not directed, undirected or mutual`,
|
|
1418
1441
|
where,
|
|
1419
1442
|
);
|
|
1420
1443
|
report.counts.skippedEdges++;
|
|
@@ -1525,6 +1548,7 @@ class GexfReader implements XmlHandler {
|
|
|
1525
1548
|
return;
|
|
1526
1549
|
case "spells":
|
|
1527
1550
|
this.spellPairs = [];
|
|
1551
|
+
this.spellOpen = [];
|
|
1528
1552
|
this.spellsWhere = edge.where;
|
|
1529
1553
|
this.ctx.push(Ctx.Spells);
|
|
1530
1554
|
return;
|
|
@@ -1970,36 +1994,34 @@ class GexfReader implements XmlHandler {
|
|
|
1970
1994
|
const where = this.spellsWhere ?? { line };
|
|
1971
1995
|
try {
|
|
1972
1996
|
const bounds = this.readBounds(attrs, where);
|
|
1973
|
-
if (bounds.open !== 0) {
|
|
1974
|
-
this.report.warnOnce(
|
|
1975
|
-
"unsupported",
|
|
1976
|
-
SPELL_OPEN_CODE,
|
|
1977
|
-
"startopen / endopen on a <spell> cannot be kept; the spell is stored closed",
|
|
1978
|
-
where,
|
|
1979
|
-
);
|
|
1980
|
-
}
|
|
1981
1997
|
this.spellPairs?.push([bounds.start.value, bounds.end.value]);
|
|
1998
|
+
this.spellOpen.push(bounds.open);
|
|
1982
1999
|
} catch (err) {
|
|
1983
2000
|
this.report.recordError(err, where);
|
|
1984
2001
|
}
|
|
1985
2002
|
}
|
|
1986
2003
|
|
|
1987
|
-
/**
|
|
2004
|
+
/**
|
|
2005
|
+
* Close `<spells>`: write the pairs into the open element's spells column, and their open bits
|
|
2006
|
+
* into the spells.open column when any spell has an open bound.
|
|
2007
|
+
*/
|
|
1988
2008
|
private finishSpells(): void {
|
|
1989
2009
|
const pairs = this.spellPairs;
|
|
2010
|
+
const open = this.spellOpen;
|
|
1990
2011
|
const where = this.spellsWhere ?? undefined;
|
|
1991
2012
|
this.spellPairs = null;
|
|
2013
|
+
this.spellOpen = [];
|
|
1992
2014
|
this.spellsWhere = null;
|
|
1993
2015
|
if (pairs === null) {
|
|
1994
2016
|
return;
|
|
1995
2017
|
}
|
|
1996
2018
|
try {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
if (
|
|
2002
|
-
|
|
2019
|
+
const columns = this.edge === null ? this.nodeColumns : this.edgeColumns;
|
|
2020
|
+
const { index } = this.edge ?? this.nodeStack[this.nodeStack.length - 1];
|
|
2021
|
+
if (index !== INVALID_INDEX) {
|
|
2022
|
+
columns.spells.set(this.sink, this.report, index, pairs, where);
|
|
2023
|
+
if (open.some((bits) => bits !== 0)) {
|
|
2024
|
+
columns.spellsOpen.set(this.sink, this.report, index, open, where);
|
|
2003
2025
|
}
|
|
2004
2026
|
}
|
|
2005
2027
|
} catch (err) {
|
|
@@ -128,7 +128,7 @@ export const GEXF_ISSUE = Object.freeze({
|
|
|
128
128
|
VIZ_SKIPPED: VIZ_SKIPPED_CODE,
|
|
129
129
|
/** A 1.2 dynamic viz element whose bounds were dropped. */
|
|
130
130
|
VIZ_DYNAMIC_DROPPED: VIZ_DYNAMIC_CODE,
|
|
131
|
-
/**
|
|
131
|
+
/** Deprecated and no longer recorded: open spells are kept in the spells.open column. */
|
|
132
132
|
SPELL_OPEN_DROPPED: SPELL_OPEN_CODE,
|
|
133
133
|
/** A viz value that could not be read. */
|
|
134
134
|
VIZ_VALUE: VIZ_VALUE_CODE,
|
|
@@ -56,6 +56,7 @@ export const NODE_COLUMNS = Object.freeze({
|
|
|
56
56
|
end: "end",
|
|
57
57
|
timestamp: "timestamp",
|
|
58
58
|
spells: "spells",
|
|
59
|
+
spellsOpen: "spells.open",
|
|
59
60
|
timestamps: "timestamps",
|
|
60
61
|
open: "open",
|
|
61
62
|
position: "position",
|
|
@@ -74,6 +75,7 @@ const EDGE_COLUMNS = Object.freeze({
|
|
|
74
75
|
end: "end",
|
|
75
76
|
timestamp: "timestamp",
|
|
76
77
|
spells: "spells",
|
|
78
|
+
spellsOpen: "spells.open",
|
|
77
79
|
timestamps: "timestamps",
|
|
78
80
|
open: "open",
|
|
79
81
|
color: "color",
|
|
@@ -138,6 +140,14 @@ export const NODE_DECLS: Readonly<Record<keyof typeof NODE_COLUMNS, ColumnDecl>>
|
|
|
138
140
|
origin: XML_ORIGIN,
|
|
139
141
|
},
|
|
140
142
|
open: { name: NODE_COLUMNS.open, dtype: "u8", role: "open", nullable: true, origin: XML_ORIGIN },
|
|
143
|
+
spellsOpen: {
|
|
144
|
+
name: NODE_COLUMNS.spellsOpen,
|
|
145
|
+
dtype: "list",
|
|
146
|
+
itemDtype: "u8",
|
|
147
|
+
role: "spellsOpen",
|
|
148
|
+
nullable: true,
|
|
149
|
+
origin: XML_ORIGIN,
|
|
150
|
+
},
|
|
141
151
|
position: {
|
|
142
152
|
name: NODE_COLUMNS.position,
|
|
143
153
|
dtype: "f32",
|
|
@@ -187,6 +197,14 @@ export const EDGE_DECLS: Readonly<Record<keyof typeof EDGE_COLUMNS, ColumnDecl>>
|
|
|
187
197
|
origin: XML_ORIGIN,
|
|
188
198
|
},
|
|
189
199
|
open: { name: EDGE_COLUMNS.open, dtype: "u8", role: "open", nullable: true, origin: XML_ORIGIN },
|
|
200
|
+
spellsOpen: {
|
|
201
|
+
name: EDGE_COLUMNS.spellsOpen,
|
|
202
|
+
dtype: "list",
|
|
203
|
+
itemDtype: "u8",
|
|
204
|
+
role: "spellsOpen",
|
|
205
|
+
nullable: true,
|
|
206
|
+
origin: XML_ORIGIN,
|
|
207
|
+
},
|
|
190
208
|
color: {
|
|
191
209
|
name: EDGE_COLUMNS.color,
|
|
192
210
|
dtype: "f32",
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* a `graphics [ x y z ]` node record mapped to the `position` role (note 07 section 9) with the
|
|
15
15
|
* remaining graphics keys kept as a json column.
|
|
16
16
|
*
|
|
17
|
-
* Node ids are the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Node ids are the `id` keys under `nodeIdFrom: "id"`, the `label` under `"label"`, and the node's
|
|
18
|
+
* ordinal under `"index"`; `source` / `target` always resolve through the `id` keys. The GML spec
|
|
19
|
+
* types ids as integers; a string id (written by NetworkX and Gephi) is accepted under the `ids`
|
|
20
|
+
* rule with one warning per file, and a real or record id is a validation error. A node's
|
|
20
21
|
* `graphty_originalId` key written by the exporter's `sanitizeIds: "mangle"` restores the
|
|
21
22
|
* original id under `restoreMangledIds` (the default).
|
|
22
23
|
*/
|
|
@@ -96,8 +97,10 @@ export const MISSING_ID_CODE = SHARED_MISSING_ID_CODE;
|
|
|
96
97
|
export const MISSING_LABEL_CODE = "E_GML_MISSING_LABEL";
|
|
97
98
|
/** Issue code: an edge block has no `source` or no `target` key. */
|
|
98
99
|
export const MISSING_ENDPOINT_CODE = SHARED_MISSING_ENDPOINT_CODE;
|
|
99
|
-
/** Issue code: an `id`, `source` or `target` value is
|
|
100
|
+
/** Issue code: an `id`, `source` or `target` value is neither an integer nor a string. */
|
|
100
101
|
export const ID_TYPE_CODE = "E_GML_ID_TYPE";
|
|
102
|
+
/** Issue code: string `id`, `source` or `target` values, outside the GML spec's integer ids; warned once per file. */
|
|
103
|
+
export const STRING_ID_CODE = "W_GML_STRING_ID";
|
|
101
104
|
/** Issue code: a node id (or label under `nodeIdFrom: "label"`) is declared twice; later keys overwrite. */
|
|
102
105
|
export const DUPLICATE_NODE_CODE = SHARED_DUPLICATE_NODE_CODE;
|
|
103
106
|
/** Issue code: a structural key (`id`, `source`, `target`) appears twice in one block. */
|
|
@@ -301,7 +304,7 @@ class GmlImport {
|
|
|
301
304
|
private elementsSinceCheck = 0;
|
|
302
305
|
|
|
303
306
|
/** The sink id every file id maps to when they differ (label / index / restored ids), else null. */
|
|
304
|
-
private idMap: Map<
|
|
307
|
+
private idMap: Map<NodeId, NodeId> | null = null;
|
|
305
308
|
|
|
306
309
|
/** Node indices declared by a node block of this import, for duplicate detection. */
|
|
307
310
|
private declared = new Uint32Array(64);
|
|
@@ -866,7 +869,9 @@ class GmlImport {
|
|
|
866
869
|
if (idTok < 0) {
|
|
867
870
|
throw new GraphFormatError("E_INVALID_ID", "node has no id", { reason: "missing id" });
|
|
868
871
|
}
|
|
869
|
-
|
|
872
|
+
// the file id under the ids rule, so `id 1` and `source "1"` meet in idMap (except under
|
|
873
|
+
// "keep", where the integer 1 and the string "1" are different ids by design)
|
|
874
|
+
const fileId = this.coerceFileId(idTok, this.fileIdOf(idTok, "id"));
|
|
870
875
|
if (originalTok >= 0) {
|
|
871
876
|
sinkId = this.restoredId(originalTok);
|
|
872
877
|
} else {
|
|
@@ -883,7 +888,7 @@ class GmlImport {
|
|
|
883
888
|
sinkId = ordinal;
|
|
884
889
|
break;
|
|
885
890
|
default:
|
|
886
|
-
sinkId =
|
|
891
|
+
sinkId = fileId;
|
|
887
892
|
}
|
|
888
893
|
}
|
|
889
894
|
if (this.idMap !== null) {
|
|
@@ -1019,24 +1024,48 @@ class GmlImport {
|
|
|
1019
1024
|
}
|
|
1020
1025
|
|
|
1021
1026
|
/**
|
|
1022
|
-
* The
|
|
1023
|
-
*
|
|
1027
|
+
* The value of an id / source / target token: an integer (the GML spec's type) as a number, a
|
|
1028
|
+
* string (NetworkX and Gephi write them) as its text with one warning per file.
|
|
1024
1029
|
* @param v - the value token
|
|
1025
1030
|
* @param key - the key name, for the error
|
|
1026
|
-
* @returns the number
|
|
1031
|
+
* @returns the number or the string
|
|
1027
1032
|
*/
|
|
1028
|
-
private
|
|
1033
|
+
private fileIdOf(v: number, key: string): number | string {
|
|
1029
1034
|
const t = this.requireTokens();
|
|
1035
|
+
if (t.kind[v] === TOKEN_STRING) {
|
|
1036
|
+
this.report.warnOnce(
|
|
1037
|
+
"validation-error",
|
|
1038
|
+
STRING_ID_CODE,
|
|
1039
|
+
`${key} is the string "${t.textOf(v)}"; GML ids are integers, string ids are kept under the ids rule`,
|
|
1040
|
+
{ line: t.line[v], element: t.stringOf(v) },
|
|
1041
|
+
);
|
|
1042
|
+
return t.stringOf(v);
|
|
1043
|
+
}
|
|
1030
1044
|
if (t.kind[v] !== TOKEN_INT) {
|
|
1031
|
-
throw new GraphFormatError(
|
|
1032
|
-
|
|
1033
|
-
key,
|
|
1034
|
-
|
|
1035
|
-
|
|
1045
|
+
throw new GraphFormatError(
|
|
1046
|
+
"E_INVALID_ID",
|
|
1047
|
+
`${key} must be an integer or a string, found ${describeValue(t, v)}`,
|
|
1048
|
+
{
|
|
1049
|
+
reason: "not an integer",
|
|
1050
|
+
key,
|
|
1051
|
+
value: t.kind[v] === TOKEN_OPEN ? "[...]" : t.textOf(v),
|
|
1052
|
+
},
|
|
1053
|
+
);
|
|
1036
1054
|
}
|
|
1037
1055
|
return Number(t.textOf(v));
|
|
1038
1056
|
}
|
|
1039
1057
|
|
|
1058
|
+
/**
|
|
1059
|
+
* Coerce an id / source / target value under the `ids` rule: a string by its text, an integer
|
|
1060
|
+
* through coerceInteger().
|
|
1061
|
+
* @param v - the value token
|
|
1062
|
+
* @param id - its value (fileIdOf())
|
|
1063
|
+
* @returns the id
|
|
1064
|
+
*/
|
|
1065
|
+
private coerceFileId(v: number, id: number | string): NodeId {
|
|
1066
|
+
return typeof id === "string" ? this.coercer.text(id) : this.coerceInteger(v, id);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1040
1069
|
/**
|
|
1041
1070
|
* Coerce an integer id token under the `ids` rule: a safe integer passes as a typed number
|
|
1042
1071
|
* (so `01` is 1 and `-0` is 0), a larger one goes through the text rule (a string under
|
|
@@ -1099,14 +1128,8 @@ class GmlImport {
|
|
|
1099
1128
|
* @returns the id
|
|
1100
1129
|
*/
|
|
1101
1130
|
private endpoint(v: number, key: string): NodeId {
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
const mapped = this.idMap.get(n);
|
|
1105
|
-
if (mapped !== undefined) {
|
|
1106
|
-
return mapped;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
return this.coerceInteger(v, n);
|
|
1131
|
+
const id = this.coerceFileId(v, this.fileIdOf(v, key));
|
|
1132
|
+
return this.idMap?.get(id) ?? id;
|
|
1110
1133
|
}
|
|
1111
1134
|
|
|
1112
1135
|
/**
|
package/src/formats/gml/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
REPEATED_KEY_CODE,
|
|
46
46
|
ROLE_TAKEN_CODE,
|
|
47
47
|
SECOND_GRAPH_CODE,
|
|
48
|
+
STRING_ID_CODE,
|
|
48
49
|
UNKNOWN_ENTITY_CODE,
|
|
49
50
|
} from "./importer.js";
|
|
50
51
|
|
|
@@ -77,8 +78,10 @@ export const GML_ISSUE = Object.freeze({
|
|
|
77
78
|
MISSING_LABEL: MISSING_LABEL_CODE,
|
|
78
79
|
/** An edge without `source` or `target`. */
|
|
79
80
|
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
80
|
-
/** A node id, source or target that is
|
|
81
|
+
/** A node id, source or target that is neither an integer nor a string. */
|
|
81
82
|
ID_TYPE: ID_TYPE_CODE,
|
|
83
|
+
/** String node ids, sources or targets (outside the spec's integers), kept under the ids rule; warned once. */
|
|
84
|
+
STRING_ID: STRING_ID_CODE,
|
|
82
85
|
/** A node id declared twice (later keys overwrite). */
|
|
83
86
|
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
84
87
|
/** A structural key repeated in one element. */
|
|
@@ -233,6 +233,8 @@ export const GRAPHML_LOSS = Object.freeze({
|
|
|
233
233
|
ID_TEXT_TYPE: ID_TEXT_TYPE_CODE,
|
|
234
234
|
/** A numeric edge id column reads back as string. */
|
|
235
235
|
EDGE_ID_TEXT: "W_GRAPHML_EDGE_ID_TEXT",
|
|
236
|
+
/** A `yfiles.*` graphics column that no longer matches its yFiles tree: only the tree is written. */
|
|
237
|
+
YFILES_GRAPHICS_STALE: "W_GRAPHML_YFILES_GRAPHICS_STALE",
|
|
236
238
|
/** A yfiles json value that is not a serialisable tree: export() will throw E_COLUMN_TYPE. */
|
|
237
239
|
YFILES_TREE: "E_GRAPHML_YFILES_TREE",
|
|
238
240
|
});
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
YFILES_NAMESPACE,
|
|
57
57
|
} from "./constants.js";
|
|
58
58
|
import { treeProblem, writeXmlTree } from "./tree.js";
|
|
59
|
+
import { isGraphicsColumn, staleGraphicsRows } from "./yfiles.js";
|
|
59
60
|
|
|
60
61
|
/** The format-specific options of the GraphML exporter. */
|
|
61
62
|
export interface GraphmlExportOptions {
|
|
@@ -110,6 +111,7 @@ const DROPPED_ROLES: ReadonlySet<string> = new Set([
|
|
|
110
111
|
"timestamps",
|
|
111
112
|
"spells",
|
|
112
113
|
"open",
|
|
114
|
+
"spellsOpen",
|
|
113
115
|
]);
|
|
114
116
|
|
|
115
117
|
/**
|
|
@@ -294,11 +296,16 @@ function graphmlMetaOf(snapshot: GraphSnapshot): GraphmlMeta | null {
|
|
|
294
296
|
* @returns the plan
|
|
295
297
|
*/
|
|
296
298
|
function planExport(snapshot: GraphSnapshot, options: ResolvedExportOptions, format: FormatOptions): Plan {
|
|
297
|
-
// the generic json note is replaced by planTable()'s (yfiles trees are kept, other json is
|
|
299
|
+
// the generic json note is replaced by planTable()'s (yfiles trees are kept, other json is
|
|
300
|
+
// text); columns mapped from a yfiles tree are never written: planTable() notes the ones
|
|
301
|
+
// that no longer match their tree, the rest lose nothing
|
|
302
|
+
const graphics = new Set(
|
|
303
|
+
[...snapshot.nodes, ...snapshot.edges].filter((c) => isGraphicsColumn(c.meta)).map((c) => c.meta.name),
|
|
304
|
+
);
|
|
298
305
|
const notes = checkCapabilities(snapshot, CAPABILITIES, options, {
|
|
299
306
|
roles: SLOT_ROLES,
|
|
300
307
|
roleNames: ROLE_NAMES,
|
|
301
|
-
}).filter((n) => n.code !== LOSS.JSON);
|
|
308
|
+
}).filter((n) => n.code !== LOSS.JSON && (n.column === null || !graphics.has(n.column)));
|
|
302
309
|
const note: NoteFn = (code, message, column = null, count = null): void => {
|
|
303
310
|
notes.push(Object.freeze({ code, message, column, count }));
|
|
304
311
|
};
|
|
@@ -649,6 +656,18 @@ function planTable(table: Iterable<Column>, domain: Domain, notes: LossNote[], n
|
|
|
649
656
|
for (const column of columns) {
|
|
650
657
|
const { meta } = column;
|
|
651
658
|
const { role, name } = meta;
|
|
659
|
+
if (isGraphicsColumn(meta)) {
|
|
660
|
+
const stale = domain === "graph" ? 0 : staleGraphicsRows(column, columns, domain);
|
|
661
|
+
if (stale > 0) {
|
|
662
|
+
note(
|
|
663
|
+
GRAPHML_LOSS.YFILES_GRAPHICS_STALE,
|
|
664
|
+
`${stale} value(s) of ${domain} column "${name}" differ from the yFiles tree it was read from; only the tree is written, so they are lost`,
|
|
665
|
+
name,
|
|
666
|
+
stale,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
652
671
|
if (role === "parents") {
|
|
653
672
|
note(
|
|
654
673
|
GRAPHML_LOSS.PARENTS_DROPPED,
|