@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,1458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GraphML exporter (design section 8.5; research note 07 section 9): `<key>` declarations
|
|
3
|
+
* regenerated from ColumnMeta.origin when the column came from GraphML (key id, attr.name,
|
|
4
|
+
* attr.type, yfiles.type) and derived from the dtype otherwise; node ids sanitised to NMTOKEN
|
|
5
|
+
* (`sanitizeIds: "error"` refuses, `"mangle"` rewrites and keeps the original in a
|
|
6
|
+
* `graphty:originalId` data attribute the importer restores); mixed direction written with
|
|
7
|
+
* `edgedefault` plus per-edge `directed` attributes, folding expanded pairs back through the
|
|
8
|
+
* `pair` / `directed` role columns; explicit weights only (the role-weight column's validity);
|
|
9
|
+
* containment (`parent` role) as nested graphs; yFiles json columns as nested XML again.
|
|
10
|
+
*
|
|
11
|
+
* check() lists every loss before anything is written: the generic capability gaps of
|
|
12
|
+
* checkCapabilities() (dict / u32 / u8 dtypes, lists, components, options, positions, visual and
|
|
13
|
+
* temporal roles, extension tables) plus the GraphML-specific ones (mutual edges written as
|
|
14
|
+
* undirected, json columns without a yfiles origin written as JSON text, multi-parent columns,
|
|
15
|
+
* containment order, ids that change type under the canonical rule, edge ids that read back as
|
|
16
|
+
* strings, roles GraphML cannot express, yfiles values that are not serialisable trees).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { type Column, GraphFormatError, type GraphSnapshot, INVALID_INDEX } from "@graphty/graph-format";
|
|
20
|
+
|
|
21
|
+
import { childrenCsr } from "../../children.js";
|
|
22
|
+
import { type PairFolding, pairFolding } from "../../common/direction.js";
|
|
23
|
+
import { escapeXmlAttribute, escapeXmlText } from "../../common/escape.js";
|
|
24
|
+
import {
|
|
25
|
+
capabilities,
|
|
26
|
+
checkCapabilities,
|
|
27
|
+
isNmtoken,
|
|
28
|
+
LOSS,
|
|
29
|
+
mangleNmtoken,
|
|
30
|
+
type SanitizedIds,
|
|
31
|
+
sanitizeIds,
|
|
32
|
+
} from "../../common/export.js";
|
|
33
|
+
import { formatF32, formatF64, formatInteger } from "../../common/format.js";
|
|
34
|
+
import { isCanonicalIntegerText } from "../../common/ids.js";
|
|
35
|
+
import { type ResolvedExportOptions, resolveExportOptions } from "../../common/options.js";
|
|
36
|
+
import { type ExplicitWeights, explicitWeights } from "../../common/weights.js";
|
|
37
|
+
import { encodeChunks, joinText } from "../../common/writer.js";
|
|
38
|
+
import { xmlIllegalTextNotes } from "../../common/xml.js";
|
|
39
|
+
import { type CommonExportOptions, type ExportCapabilities, type GraphExporter, type LossNote } from "../../types.js";
|
|
40
|
+
import {
|
|
41
|
+
EDGE_ID_COLUMN,
|
|
42
|
+
FORMAT,
|
|
43
|
+
GRAPHML_LOSS,
|
|
44
|
+
GRAPHML_NAMESPACE,
|
|
45
|
+
type GraphmlMeta,
|
|
46
|
+
LABEL_COLUMN,
|
|
47
|
+
META_KEY,
|
|
48
|
+
ORIGINAL_ID_ATTRIBUTE,
|
|
49
|
+
PARENT_COLUMN,
|
|
50
|
+
RESERVED_EDGE_NAMES,
|
|
51
|
+
RESERVED_NODE_NAMES,
|
|
52
|
+
SCHEMA_LOCATION,
|
|
53
|
+
SOURCE_PORT_COLUMN,
|
|
54
|
+
TARGET_PORT_COLUMN,
|
|
55
|
+
XSI_NAMESPACE,
|
|
56
|
+
YFILES_NAMESPACE,
|
|
57
|
+
} from "./constants.js";
|
|
58
|
+
import { treeProblem, writeXmlTree } from "./tree.js";
|
|
59
|
+
|
|
60
|
+
/** The format-specific options of the GraphML exporter. */
|
|
61
|
+
export interface GraphmlExportOptions {
|
|
62
|
+
/** Indent nested elements (default true); false writes one element per line without indentation. */
|
|
63
|
+
pretty?: boolean | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* The top-level `edgedefault`. By default the one the importer recorded in
|
|
66
|
+
* `meta.extra.graphml` (so a mixed file re-imports with the same edge layout), else the
|
|
67
|
+
* snapshot's direction, with the majority direction for a mixed snapshot.
|
|
68
|
+
*/
|
|
69
|
+
edgedefault?: "directed" | "undirected" | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What GraphML keeps as declared (research note 07 section 9). */
|
|
73
|
+
const CAPABILITIES: ExportCapabilities = capabilities({
|
|
74
|
+
mixedDirection: true,
|
|
75
|
+
multiEdges: true,
|
|
76
|
+
selfLoops: true,
|
|
77
|
+
edgeIds: "optional",
|
|
78
|
+
idCharset: "nmtoken",
|
|
79
|
+
dtypes: ["bool", "i32", "f32", "f64", "string"],
|
|
80
|
+
components: false,
|
|
81
|
+
lists: false,
|
|
82
|
+
json: false,
|
|
83
|
+
defaults: true,
|
|
84
|
+
options: false,
|
|
85
|
+
hierarchy: true,
|
|
86
|
+
temporal: "none",
|
|
87
|
+
graphAttributes: true,
|
|
88
|
+
positions: false,
|
|
89
|
+
viz: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** Roles handled structurally (never written as a key) or reported by checkCapabilities() and not written. */
|
|
93
|
+
const STRUCTURAL_ROLES: ReadonlySet<string> = new Set([
|
|
94
|
+
"directed",
|
|
95
|
+
"pair",
|
|
96
|
+
"mutual",
|
|
97
|
+
"weight",
|
|
98
|
+
"timeText",
|
|
99
|
+
"originalId",
|
|
100
|
+
]);
|
|
101
|
+
const DROPPED_ROLES: ReadonlySet<string> = new Set([
|
|
102
|
+
"position",
|
|
103
|
+
"color",
|
|
104
|
+
"size",
|
|
105
|
+
"shape",
|
|
106
|
+
"thickness",
|
|
107
|
+
"start",
|
|
108
|
+
"end",
|
|
109
|
+
"timestamp",
|
|
110
|
+
"timestamps",
|
|
111
|
+
"spells",
|
|
112
|
+
"open",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The roles GraphML has a slot for (checkCapabilities() reports every other role as lost): the
|
|
117
|
+
* label key, the edge `id`, `sourceport` and `targetport` attributes and nested graphs.
|
|
118
|
+
*/
|
|
119
|
+
const SLOT_ROLES: ReadonlySet<string> = new Set(["label", "id", "sourcePort", "targetPort", "parent"]);
|
|
120
|
+
|
|
121
|
+
/** The slot roles written structurally per domain (never as a key); a label is a key titled `label`. */
|
|
122
|
+
const SLOT_ROLES_BY_DOMAIN: Readonly<Record<Domain, ReadonlySet<string>>> = {
|
|
123
|
+
graph: new Set(),
|
|
124
|
+
node: new Set(["parent"]),
|
|
125
|
+
edge: new Set(["id", "sourcePort", "targetPort"]),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** The names the importer gives the slot columns (design section 5.6), for the name-change notes. */
|
|
129
|
+
const ROLE_NAMES: Readonly<Record<string, string>> = Object.freeze({
|
|
130
|
+
label: LABEL_COLUMN,
|
|
131
|
+
id: EDGE_ID_COLUMN,
|
|
132
|
+
sourcePort: SOURCE_PORT_COLUMN,
|
|
133
|
+
targetPort: TARGET_PORT_COLUMN,
|
|
134
|
+
parent: PARENT_COLUMN,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/** The attr.name of the weight key when the weight did not come from GraphML (the importer's weightFrom default). */
|
|
138
|
+
const DEFAULT_WEIGHT_NAME = "weight";
|
|
139
|
+
|
|
140
|
+
/** A note-recording callback. */
|
|
141
|
+
type NoteFn = (code: string, message: string, column?: string | null, count?: number | null) => void;
|
|
142
|
+
|
|
143
|
+
/** The GraphML attr.type values and the dtype each maps to, for restoring origin.type. */
|
|
144
|
+
const TYPE_DTYPES: Readonly<Record<string, string>> = {
|
|
145
|
+
boolean: "bool",
|
|
146
|
+
int: "i32",
|
|
147
|
+
long: "f64",
|
|
148
|
+
float: "f32",
|
|
149
|
+
double: "f64",
|
|
150
|
+
string: "string",
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const I32_MAX = 2147483647;
|
|
154
|
+
|
|
155
|
+
/** The yfiles.type written for a yfiles json column whose origin records none. */
|
|
156
|
+
const DEFAULT_YFILES_TYPES: Readonly<Record<Domain, string>> = {
|
|
157
|
+
node: "nodegraphics",
|
|
158
|
+
edge: "edgegraphics",
|
|
159
|
+
graph: "resources",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** The element domains. */
|
|
163
|
+
type Domain = "graph" | "node" | "edge";
|
|
164
|
+
|
|
165
|
+
/** One column written as a key. */
|
|
166
|
+
interface ColumnPlan {
|
|
167
|
+
readonly column: Column;
|
|
168
|
+
readonly domain: Domain;
|
|
169
|
+
/** The key id. */
|
|
170
|
+
keyId: string;
|
|
171
|
+
/** The attr.type, or null for a yfiles key. */
|
|
172
|
+
readonly attrType: string | null;
|
|
173
|
+
/** The attr.name, or null for a yfiles key. */
|
|
174
|
+
readonly attrName: string | null;
|
|
175
|
+
/** The yfiles.type, or null. */
|
|
176
|
+
readonly yfilesType: string | null;
|
|
177
|
+
/** Whether values are written as nested XML trees. */
|
|
178
|
+
readonly yfiles: boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** One `<key>` element (a for="all" key merges up to three column plans). */
|
|
182
|
+
interface KeyPlan {
|
|
183
|
+
readonly id: string;
|
|
184
|
+
readonly domains: readonly Domain[];
|
|
185
|
+
readonly attrName: string | null;
|
|
186
|
+
readonly attrType: string | null;
|
|
187
|
+
readonly yfilesType: string | null;
|
|
188
|
+
/** The column whose default and desc are written; null for the weight key. */
|
|
189
|
+
readonly column: Column | null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Containment as the exporter writes it. */
|
|
193
|
+
interface Hierarchy {
|
|
194
|
+
/** The parent column, or null. */
|
|
195
|
+
readonly parent: Column | null;
|
|
196
|
+
/** Nodes written at the top level, in order (roots, then the roots forced out of cycles). */
|
|
197
|
+
readonly roots: Uint32Array;
|
|
198
|
+
/** CSR of children by parent: childStart[u]..childStart[u+1] over childList. */
|
|
199
|
+
readonly childStart: Uint32Array;
|
|
200
|
+
readonly childList: Uint32Array;
|
|
201
|
+
/** Whether the written order differs from index order. */
|
|
202
|
+
readonly reordered: boolean;
|
|
203
|
+
/** Nodes whose parent chain never reaches a root. */
|
|
204
|
+
readonly unreachable: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Everything check() and export() agree on. */
|
|
208
|
+
interface Plan {
|
|
209
|
+
readonly options: ResolvedExportOptions;
|
|
210
|
+
readonly pretty: boolean;
|
|
211
|
+
readonly notes: LossNote[];
|
|
212
|
+
readonly keys: KeyPlan[];
|
|
213
|
+
readonly graphColumns: ColumnPlan[];
|
|
214
|
+
readonly nodeColumns: ColumnPlan[];
|
|
215
|
+
readonly edgeColumns: ColumnPlan[];
|
|
216
|
+
/** The weight key id, attr.type and the explicit weights, or null for an unweighted snapshot. */
|
|
217
|
+
readonly weight: { readonly keyId: string; readonly attrType: string; readonly weights: ExplicitWeights } | null;
|
|
218
|
+
/** The key id of the originalId attribute, when mangled ids are written. */
|
|
219
|
+
readonly originalIdKey: string | null;
|
|
220
|
+
readonly edgedefault: "directed" | "undirected";
|
|
221
|
+
/** The pair folding: mirrors skipped, source directions. */
|
|
222
|
+
readonly folding: PairFolding;
|
|
223
|
+
readonly edgeIdColumn: Column | null;
|
|
224
|
+
readonly sourcePortColumn: Column | null;
|
|
225
|
+
readonly targetPortColumn: Column | null;
|
|
226
|
+
readonly hierarchy: Hierarchy;
|
|
227
|
+
readonly meta: GraphmlMeta | null;
|
|
228
|
+
readonly needsOriginalIdKey: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The resolved format-specific options. */
|
|
232
|
+
interface FormatOptions {
|
|
233
|
+
/** Whether to indent. */
|
|
234
|
+
readonly pretty: boolean;
|
|
235
|
+
/** The edgedefault override, or null. */
|
|
236
|
+
readonly edgedefault: "directed" | "undirected" | null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolve the format-specific options.
|
|
241
|
+
* @param options - the caller's options
|
|
242
|
+
* @returns the pretty flag and the edgedefault override
|
|
243
|
+
*/
|
|
244
|
+
function resolveFormatOptions(options: GraphmlExportOptions | undefined): FormatOptions {
|
|
245
|
+
const pretty = options?.pretty ?? true;
|
|
246
|
+
if (typeof pretty !== "boolean") {
|
|
247
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option pretty must be a boolean", {
|
|
248
|
+
option: "pretty",
|
|
249
|
+
found: typeof pretty,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const edgedefault = options?.edgedefault ?? null;
|
|
253
|
+
if (edgedefault !== null && edgedefault !== "directed" && edgedefault !== "undirected") {
|
|
254
|
+
throw new GraphFormatError(
|
|
255
|
+
"E_UNSUPPORTED",
|
|
256
|
+
`option edgedefault: ${JSON.stringify(edgedefault)} is not "directed" or "undirected"`,
|
|
257
|
+
{ option: "edgedefault", found: edgedefault },
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return { pretty, edgedefault };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The importer's `meta.extra.graphml`, when the snapshot has one of the expected shape.
|
|
265
|
+
* @param snapshot - the snapshot
|
|
266
|
+
* @returns the meta, or null
|
|
267
|
+
*/
|
|
268
|
+
function graphmlMetaOf(snapshot: GraphSnapshot): GraphmlMeta | null {
|
|
269
|
+
const value = snapshot.meta.extra[META_KEY];
|
|
270
|
+
if (typeof value !== "object" || value === null) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const record = value as Record<string, unknown>;
|
|
274
|
+
const graphId = typeof record.graphId === "string" ? record.graphId : null;
|
|
275
|
+
const edgedefault =
|
|
276
|
+
record.edgedefault === "directed" || record.edgedefault === "undirected" ? record.edgedefault : null;
|
|
277
|
+
const namespaces: Record<string, string> = {};
|
|
278
|
+
if (typeof record.namespaces === "object" && record.namespaces !== null) {
|
|
279
|
+
for (const [prefix, uri] of Object.entries(record.namespaces as Record<string, unknown>)) {
|
|
280
|
+
if (typeof uri === "string") {
|
|
281
|
+
namespaces[prefix] = uri;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { graphId, edgedefault, namespaces };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build the plan: classify every column, assign key ids, decide the edgedefault and the
|
|
290
|
+
* containment order, and collect every loss note.
|
|
291
|
+
* @param snapshot - the snapshot
|
|
292
|
+
* @param options - the resolved common options
|
|
293
|
+
* @param format - the resolved format options
|
|
294
|
+
* @returns the plan
|
|
295
|
+
*/
|
|
296
|
+
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 text)
|
|
298
|
+
const notes = checkCapabilities(snapshot, CAPABILITIES, options, {
|
|
299
|
+
roles: SLOT_ROLES,
|
|
300
|
+
roleNames: ROLE_NAMES,
|
|
301
|
+
}).filter((n) => n.code !== LOSS.JSON);
|
|
302
|
+
const note: NoteFn = (code, message, column = null, count = null): void => {
|
|
303
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
304
|
+
};
|
|
305
|
+
notes.push(...xmlIllegalTextNotes(snapshot));
|
|
306
|
+
const meta = graphmlMetaOf(snapshot);
|
|
307
|
+
const keyIds = new KeyIds();
|
|
308
|
+
|
|
309
|
+
const graphColumns = planTable(snapshot.graph, "graph", notes, note);
|
|
310
|
+
const nodeColumns = planTable(snapshot.nodes, "node", notes, note);
|
|
311
|
+
const edgeColumns = planTable(snapshot.edges, "edge", notes, note);
|
|
312
|
+
const keys = planKeys([...graphColumns, ...nodeColumns, ...edgeColumns], keyIds);
|
|
313
|
+
reservedNameNotes(nodeColumns, RESERVED_NODE_NAMES, note);
|
|
314
|
+
reservedNameNotes(edgeColumns, RESERVED_EDGE_NAMES, note);
|
|
315
|
+
const weight = planWeight(snapshot, edgeColumns, keyIds, keys, note);
|
|
316
|
+
|
|
317
|
+
// ids
|
|
318
|
+
const idTypeChanges = countIdTypeChanges(snapshot);
|
|
319
|
+
if (idTypeChanges > 0) {
|
|
320
|
+
note(
|
|
321
|
+
GRAPHML_LOSS.ID_TEXT_TYPE,
|
|
322
|
+
`${idTypeChanges} node id(s) change type when read back under ids: "canonical" (string ids that are integer text, non-integer numbers)`,
|
|
323
|
+
null,
|
|
324
|
+
idTypeChanges,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const edgeIdColumn = snapshot.edges.byRole("id");
|
|
328
|
+
if (edgeIdColumn !== null) {
|
|
329
|
+
planEdgeIds(snapshot, edgeIdColumn, options, note);
|
|
330
|
+
}
|
|
331
|
+
const unrepresentable = countUnrepresentableNodeIds(snapshot);
|
|
332
|
+
const needsOriginalIdKey = unrepresentable > 0 && options.sanitizeIds === "mangle";
|
|
333
|
+
const originalIdKey = needsOriginalIdKey ? keyIds.next(null) : null;
|
|
334
|
+
|
|
335
|
+
// direction: an undirected pair folds to one undirected edge, a mutual pair too (the mark is lost)
|
|
336
|
+
const folding = pairFolding(snapshot, { foldMutual: true });
|
|
337
|
+
if (folding.mutualCount > 0) {
|
|
338
|
+
note(
|
|
339
|
+
GRAPHML_LOSS.MUTUAL_AS_UNDIRECTED,
|
|
340
|
+
`${folding.mutualCount} mutual pair(s) are written as undirected edges; the mutual mark is lost`,
|
|
341
|
+
null,
|
|
342
|
+
folding.mutualCount,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const edgedefault = planEdgedefault(snapshot, folding, format, meta);
|
|
346
|
+
|
|
347
|
+
// containment
|
|
348
|
+
const hierarchy = planHierarchy(snapshot);
|
|
349
|
+
if (hierarchy.reordered) {
|
|
350
|
+
note(
|
|
351
|
+
GRAPHML_LOSS.HIERARCHY_REORDERED,
|
|
352
|
+
"nodes are written in containment order (children nested under their parent); node indices change after a round trip",
|
|
353
|
+
hierarchy.parent?.meta.name ?? null,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (hierarchy.unreachable > 0) {
|
|
357
|
+
note(
|
|
358
|
+
GRAPHML_LOSS.PARENT_CYCLE,
|
|
359
|
+
`${hierarchy.unreachable} node(s) whose parent chain never reaches a root are written at the top level`,
|
|
360
|
+
hierarchy.parent?.meta.name ?? null,
|
|
361
|
+
hierarchy.unreachable,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
options,
|
|
367
|
+
pretty: format.pretty,
|
|
368
|
+
notes,
|
|
369
|
+
keys,
|
|
370
|
+
graphColumns,
|
|
371
|
+
nodeColumns,
|
|
372
|
+
edgeColumns,
|
|
373
|
+
weight,
|
|
374
|
+
originalIdKey,
|
|
375
|
+
edgedefault,
|
|
376
|
+
folding,
|
|
377
|
+
edgeIdColumn,
|
|
378
|
+
sourcePortColumn: snapshot.edges.byRole("sourcePort"),
|
|
379
|
+
targetPortColumn: snapshot.edges.byRole("targetPort"),
|
|
380
|
+
hierarchy,
|
|
381
|
+
meta,
|
|
382
|
+
needsOriginalIdKey,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* A plain column titled like one of the importer's XML-derived columns (`id`, `sourceport`,
|
|
388
|
+
* `targetport` on edges, `parent` on nodes) reads back renamed `<name>#<key id>` (design section
|
|
389
|
+
* 5.6); the note says so.
|
|
390
|
+
* @param plans - the column plans of one domain, key ids assigned
|
|
391
|
+
* @param reserved - the reserved names of that domain
|
|
392
|
+
* @param note - the note recorder
|
|
393
|
+
*/
|
|
394
|
+
function reservedNameNotes(plans: readonly ColumnPlan[], reserved: ReadonlySet<string>, note: NoteFn): void {
|
|
395
|
+
for (const plan of plans) {
|
|
396
|
+
const { column, attrName, keyId } = plan;
|
|
397
|
+
if (attrName === null || !reserved.has(attrName)) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
note(
|
|
401
|
+
LOSS.COLUMN_NAME_CHANGED,
|
|
402
|
+
`${plan.domain} column "${column.meta.name}" is titled like the importer's ${attrName} column and reads back as "${attrName}#${keyId}"`,
|
|
403
|
+
column.meta.name,
|
|
404
|
+
column.length - column.nullCount,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The key ids handed out so far: a preferred NMTOKEN when free, else the next free `d<n>`. */
|
|
410
|
+
class KeyIds {
|
|
411
|
+
private readonly used = new Set<string>();
|
|
412
|
+
private counter = 0;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Take a key id.
|
|
416
|
+
* @param preferred - the id to keep when it is an NMTOKEN not handed out yet, or null
|
|
417
|
+
* @returns the id
|
|
418
|
+
*/
|
|
419
|
+
next(preferred: string | null): string {
|
|
420
|
+
if (preferred !== null && isNmtoken(preferred) && !this.used.has(preferred)) {
|
|
421
|
+
this.used.add(preferred);
|
|
422
|
+
return preferred;
|
|
423
|
+
}
|
|
424
|
+
for (;;) {
|
|
425
|
+
const candidate = `d${this.counter}`;
|
|
426
|
+
this.counter++;
|
|
427
|
+
if (!this.used.has(candidate)) {
|
|
428
|
+
this.used.add(candidate);
|
|
429
|
+
return candidate;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The `<key>` elements: columns sharing a GraphML origin.id across domains become one for="all"
|
|
437
|
+
* key when their name, type and default agree; every other column is its own key.
|
|
438
|
+
* @param all - the column plans of the three domains, in writing order
|
|
439
|
+
* @param keyIds - the key id allocator
|
|
440
|
+
* @returns the keys, in declaration order
|
|
441
|
+
*/
|
|
442
|
+
function planKeys(all: readonly ColumnPlan[], keyIds: KeyIds): KeyPlan[] {
|
|
443
|
+
const keys: KeyPlan[] = [];
|
|
444
|
+
const byOrigin = new Map<string, ColumnPlan[]>();
|
|
445
|
+
for (const plan of all) {
|
|
446
|
+
const { origin } = plan.column.meta;
|
|
447
|
+
if (origin !== null && origin.format === FORMAT && origin.id !== null) {
|
|
448
|
+
const group = byOrigin.get(origin.id) ?? [];
|
|
449
|
+
group.push(plan);
|
|
450
|
+
byOrigin.set(origin.id, group);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const planned = new Set<ColumnPlan>();
|
|
454
|
+
for (const plan of all) {
|
|
455
|
+
if (planned.has(plan)) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const { origin } = plan.column.meta;
|
|
459
|
+
const group =
|
|
460
|
+
origin !== null && origin.format === FORMAT && origin.id !== null
|
|
461
|
+
? (byOrigin.get(origin.id) ?? [plan])
|
|
462
|
+
: [plan];
|
|
463
|
+
const shared = group.length > 1 && group.every((other) => other === plan || sameKey(plan, other));
|
|
464
|
+
const members = shared ? group : [plan];
|
|
465
|
+
const id = keyIds.next(origin !== null && origin.format === FORMAT ? origin.id : null);
|
|
466
|
+
for (const member of members) {
|
|
467
|
+
member.keyId = id;
|
|
468
|
+
planned.add(member);
|
|
469
|
+
}
|
|
470
|
+
const domains = members.map((member) => member.domain);
|
|
471
|
+
// a yfiles key carries attr.name only when the column name is not the key id (yEd writes none)
|
|
472
|
+
const yfilesName = plan.column.meta.origin?.title ?? plan.column.meta.name;
|
|
473
|
+
let { attrName } = plan;
|
|
474
|
+
if (plan.yfiles) {
|
|
475
|
+
attrName = yfilesName === id ? null : yfilesName;
|
|
476
|
+
}
|
|
477
|
+
keys.push({
|
|
478
|
+
id,
|
|
479
|
+
domains: domains.length > 1 ? ["graph", "node", "edge"] : domains,
|
|
480
|
+
attrName,
|
|
481
|
+
attrType: plan.attrType,
|
|
482
|
+
yfilesType: plan.yfilesType,
|
|
483
|
+
column: plan.column,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return keys;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The weight key of a weighted snapshot (its attr.name and attr.type restored from
|
|
491
|
+
* meta.weightOrigin when the weight came from GraphML), and the note for an edge column the
|
|
492
|
+
* importer would read as THE weight: every edge key titled like the weight key (`weight` by
|
|
493
|
+
* default) is a weight key on import, so such a column reads back as the weight, not as a column.
|
|
494
|
+
* @param snapshot - the snapshot
|
|
495
|
+
* @param edgeColumns - the edge column plans
|
|
496
|
+
* @param keyIds - the key id allocator
|
|
497
|
+
* @param keys - receives the weight key
|
|
498
|
+
* @param note - the note recorder
|
|
499
|
+
* @returns the weight plan, or null for an unweighted snapshot
|
|
500
|
+
*/
|
|
501
|
+
function planWeight(
|
|
502
|
+
snapshot: GraphSnapshot,
|
|
503
|
+
edgeColumns: readonly ColumnPlan[],
|
|
504
|
+
keyIds: KeyIds,
|
|
505
|
+
keys: KeyPlan[],
|
|
506
|
+
note: NoteFn,
|
|
507
|
+
): Plan["weight"] {
|
|
508
|
+
const weights = explicitWeights(snapshot);
|
|
509
|
+
const origin = snapshot.meta.weightOrigin;
|
|
510
|
+
const fromGraphml = weights.weighted && origin !== null && origin.format === FORMAT;
|
|
511
|
+
const attrName = fromGraphml && origin.title !== null ? origin.title : DEFAULT_WEIGHT_NAME;
|
|
512
|
+
for (const plan of edgeColumns) {
|
|
513
|
+
if (plan.attrName === attrName) {
|
|
514
|
+
note(
|
|
515
|
+
LOSS.WEIGHT_KEY_CLASH,
|
|
516
|
+
`edge column "${plan.column.meta.name}" is written as a key titled "${attrName}", which the importer reads as THE weight (weightFrom); it reads back as the weight, not as a column`,
|
|
517
|
+
plan.column.meta.name,
|
|
518
|
+
plan.column.length - plan.column.nullCount,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (!weights.weighted) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
let attrType = "double";
|
|
526
|
+
if (fromGraphml && origin.type !== null) {
|
|
527
|
+
const declared = origin.type.trim().toLowerCase();
|
|
528
|
+
if (declared === "float" || declared === "double" || declared === "long" || declared === "int") {
|
|
529
|
+
attrType = declared;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if ((attrType === "int" || attrType === "long") && !weightsIntegral(snapshot, weights)) {
|
|
533
|
+
// a declared integer weight only survives when every explicit weight is integral
|
|
534
|
+
attrType = "double";
|
|
535
|
+
}
|
|
536
|
+
const id = keyIds.next(fromGraphml ? origin.id : null);
|
|
537
|
+
keys.push({ id, domains: ["edge"], attrName, attrType, yfilesType: null, column: null });
|
|
538
|
+
return { keyId: id, attrType, weights };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The notes about the edge id column: a numeric column reads back as string; ids outside the
|
|
543
|
+
* NMTOKEN charset are refused or mangled per sanitizeIds.
|
|
544
|
+
* @param snapshot - the snapshot
|
|
545
|
+
* @param column - the edge id column
|
|
546
|
+
* @param options - the resolved options
|
|
547
|
+
* @param note - the note recorder
|
|
548
|
+
*/
|
|
549
|
+
function planEdgeIds(snapshot: GraphSnapshot, column: Column, options: ResolvedExportOptions, note: NoteFn): void {
|
|
550
|
+
if (column.dtype !== "string" && column.dtype !== "dict") {
|
|
551
|
+
note(
|
|
552
|
+
GRAPHML_LOSS.EDGE_ID_TEXT,
|
|
553
|
+
`edge id column "${column.meta.name}" is ${column.dtype}; GraphML edge ids read back as strings`,
|
|
554
|
+
column.meta.name,
|
|
555
|
+
snapshot.edgeCount - column.nullCount,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const bad = countBadEdgeIds(column);
|
|
559
|
+
if (bad === 0) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (options.sanitizeIds === "mangle") {
|
|
563
|
+
note(LOSS.ID_MANGLED, `${bad} edge id(s) outside the nmtoken charset are rewritten`, column.meta.name, bad);
|
|
564
|
+
} else {
|
|
565
|
+
note(
|
|
566
|
+
LOSS.ID_CHARSET,
|
|
567
|
+
`${bad} edge id(s) outside the nmtoken charset; export() will throw unless sanitizeIds is "mangle"`,
|
|
568
|
+
column.meta.name,
|
|
569
|
+
bad,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* The top-level edgedefault: the override, else undirected for an undirected snapshot, else
|
|
576
|
+
* directed when no written edge is undirected, else the one the importer recorded, else the
|
|
577
|
+
* majority direction.
|
|
578
|
+
* @param snapshot - the snapshot
|
|
579
|
+
* @param folding - the pair folding
|
|
580
|
+
* @param format - the resolved format options
|
|
581
|
+
* @param meta - the importer's meta, or null
|
|
582
|
+
* @returns the edgedefault
|
|
583
|
+
*/
|
|
584
|
+
function planEdgedefault(
|
|
585
|
+
snapshot: GraphSnapshot,
|
|
586
|
+
folding: PairFolding,
|
|
587
|
+
format: FormatOptions,
|
|
588
|
+
meta: GraphmlMeta | null,
|
|
589
|
+
): "directed" | "undirected" {
|
|
590
|
+
if (format.edgedefault !== null) {
|
|
591
|
+
return format.edgedefault;
|
|
592
|
+
}
|
|
593
|
+
if (!snapshot.directed) {
|
|
594
|
+
return "undirected";
|
|
595
|
+
}
|
|
596
|
+
let directedCount = 0;
|
|
597
|
+
let undirectedCount = 0;
|
|
598
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
599
|
+
if (folding.folded(e)) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (isDirectedEdge(folding, e)) {
|
|
603
|
+
directedCount++;
|
|
604
|
+
} else {
|
|
605
|
+
undirectedCount++;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (undirectedCount === 0) {
|
|
609
|
+
return "directed";
|
|
610
|
+
}
|
|
611
|
+
const recorded = meta === null ? null : meta.edgedefault;
|
|
612
|
+
if (recorded !== null) {
|
|
613
|
+
return recorded;
|
|
614
|
+
}
|
|
615
|
+
return undirectedCount > directedCount ? "undirected" : "directed";
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Whether two column plans can share one for="all" key.
|
|
620
|
+
* @param a - one plan
|
|
621
|
+
* @param b - another
|
|
622
|
+
* @returns true when name, type and default agree
|
|
623
|
+
*/
|
|
624
|
+
function sameKey(a: ColumnPlan, b: ColumnPlan): boolean {
|
|
625
|
+
return (
|
|
626
|
+
a.attrName === b.attrName &&
|
|
627
|
+
a.attrType === b.attrType &&
|
|
628
|
+
a.yfilesType === b.yfilesType &&
|
|
629
|
+
a.domain !== b.domain &&
|
|
630
|
+
JSON.stringify(a.column.meta.default ?? null) === JSON.stringify(b.column.meta.default ?? null)
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Classify the columns of one table: which are written as keys and what each loses. A label
|
|
636
|
+
* column is written into the label slot (a key titled `label`, which the importer reads back
|
|
637
|
+
* with the role) unless the table holds another column of that name, in which case it keeps
|
|
638
|
+
* its own title and the role is lost.
|
|
639
|
+
* @param table - the table
|
|
640
|
+
* @param domain - its domain
|
|
641
|
+
* @param notes - the notes so far (the generic name-change note of a label column that cannot take the slot is withdrawn)
|
|
642
|
+
* @param note - the note recorder
|
|
643
|
+
* @returns the plans of the written columns, in declaration order
|
|
644
|
+
*/
|
|
645
|
+
function planTable(table: Iterable<Column>, domain: Domain, notes: LossNote[], note: NoteFn): ColumnPlan[] {
|
|
646
|
+
const plans: ColumnPlan[] = [];
|
|
647
|
+
const columns = [...table];
|
|
648
|
+
const names = new Set(columns.map((column) => column.meta.name));
|
|
649
|
+
for (const column of columns) {
|
|
650
|
+
const { meta } = column;
|
|
651
|
+
const { role, name } = meta;
|
|
652
|
+
if (role === "parents") {
|
|
653
|
+
note(
|
|
654
|
+
GRAPHML_LOSS.PARENTS_DROPPED,
|
|
655
|
+
`${domain} column "${name}" (parents) cannot be written: nested graphs hold one parent per node`,
|
|
656
|
+
name,
|
|
657
|
+
column.length - column.nullCount,
|
|
658
|
+
);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (
|
|
662
|
+
role !== null &&
|
|
663
|
+
(STRUCTURAL_ROLES.has(role) || DROPPED_ROLES.has(role) || SLOT_ROLES_BY_DOMAIN[domain].has(role))
|
|
664
|
+
) {
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (role !== null && role !== "label" && SLOT_ROLES.has(role)) {
|
|
668
|
+
// the slot belongs to another domain (an edge id on a node, a port on a graph)
|
|
669
|
+
withdrawNameChange(notes, name);
|
|
670
|
+
note(
|
|
671
|
+
LOSS.ROLE,
|
|
672
|
+
`${domain} column "${name}" (${role}) is written as a plain attribute; GraphML has no ${role} slot for a ${domain} and the role is lost`,
|
|
673
|
+
name,
|
|
674
|
+
column.length - column.nullCount,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const yfiles = meta.dtype === "json" && meta.origin?.namespace === "yfiles";
|
|
678
|
+
if (meta.dtype === "json" && !yfiles) {
|
|
679
|
+
note(
|
|
680
|
+
LOSS.JSON,
|
|
681
|
+
`${domain} column "${name}" holds nested values; written as JSON text, which reads back as string`,
|
|
682
|
+
name,
|
|
683
|
+
column.length - column.nullCount,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (yfiles) {
|
|
687
|
+
const bad = countBadTrees(column);
|
|
688
|
+
if (bad > 0) {
|
|
689
|
+
note(
|
|
690
|
+
GRAPHML_LOSS.YFILES_TREE,
|
|
691
|
+
`${bad} value(s) of yfiles column "${name}" are not XML trees; export() will throw E_COLUMN_TYPE`,
|
|
692
|
+
name,
|
|
693
|
+
bad,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const { origin } = meta;
|
|
698
|
+
const fromGraphml = origin !== null && origin.format === FORMAT;
|
|
699
|
+
let attrName: string | null = null;
|
|
700
|
+
if (!yfiles) {
|
|
701
|
+
attrName = fromGraphml && origin.title !== null ? origin.title : name;
|
|
702
|
+
if (role === "label" && domain !== "graph" && attrName !== LABEL_COLUMN) {
|
|
703
|
+
attrName = labelSlot(column, domain, names, notes, note);
|
|
704
|
+
} else if (role === null && domain !== "graph" && attrName === LABEL_COLUMN) {
|
|
705
|
+
note(
|
|
706
|
+
LOSS.ROLE_ASSUMED,
|
|
707
|
+
`${domain} column "${name}" has no role but is titled "${LABEL_COLUMN}", which the importer reads back with the label role`,
|
|
708
|
+
name,
|
|
709
|
+
column.length - column.nullCount,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
plans.push({
|
|
714
|
+
column,
|
|
715
|
+
domain,
|
|
716
|
+
keyId: "",
|
|
717
|
+
attrType: yfiles ? null : attrTypeFor(column),
|
|
718
|
+
attrName,
|
|
719
|
+
yfilesType: yfiles ? (origin?.type ?? DEFAULT_YFILES_TYPES[domain]) : null,
|
|
720
|
+
yfiles,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
return plans;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* The title a label column not named `label` is written under: the label slot when the table has
|
|
728
|
+
* no other column of that name (checkCapabilities() has noted the name change), else its own
|
|
729
|
+
* name with the role lost (the name-change note is withdrawn and a role note recorded).
|
|
730
|
+
* @param column - the label column
|
|
731
|
+
* @param domain - its domain
|
|
732
|
+
* @param names - the column names of the table
|
|
733
|
+
* @param notes - the notes so far
|
|
734
|
+
* @param note - the note recorder
|
|
735
|
+
* @returns the attr.name
|
|
736
|
+
*/
|
|
737
|
+
function labelSlot(
|
|
738
|
+
column: Column,
|
|
739
|
+
domain: Domain,
|
|
740
|
+
names: ReadonlySet<string>,
|
|
741
|
+
notes: LossNote[],
|
|
742
|
+
note: NoteFn,
|
|
743
|
+
): string {
|
|
744
|
+
const { name } = column.meta;
|
|
745
|
+
if (!names.has(LABEL_COLUMN)) {
|
|
746
|
+
return LABEL_COLUMN;
|
|
747
|
+
}
|
|
748
|
+
withdrawNameChange(notes, name);
|
|
749
|
+
note(
|
|
750
|
+
LOSS.ROLE,
|
|
751
|
+
`${domain} column "${name}" (label) is written as a plain attribute: the label slot (a key titled "${LABEL_COLUMN}") is taken by column "${LABEL_COLUMN}" and the role is lost`,
|
|
752
|
+
name,
|
|
753
|
+
column.length - column.nullCount,
|
|
754
|
+
);
|
|
755
|
+
return name;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Withdraw the name-change note checkCapabilities() recorded for a role column that does not
|
|
760
|
+
* take the slot after all.
|
|
761
|
+
* @param notes - the notes so far
|
|
762
|
+
* @param name - the column name
|
|
763
|
+
*/
|
|
764
|
+
function withdrawNameChange(notes: LossNote[], name: string): void {
|
|
765
|
+
const at = notes.findIndex((n) => n.code === LOSS.COLUMN_NAME_CHANGED && n.column === name);
|
|
766
|
+
if (at >= 0) {
|
|
767
|
+
notes.splice(at, 1);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* The attr.type a column is declared with: the GraphML origin.type when it still describes the
|
|
773
|
+
* column, else the type of the dtype (u32 / u8 as int or long, dict as string, list / json /
|
|
774
|
+
* multi-component as string holding JSON text).
|
|
775
|
+
* @param column - the column
|
|
776
|
+
* @returns the attr.type text
|
|
777
|
+
*/
|
|
778
|
+
function attrTypeFor(column: Column): string {
|
|
779
|
+
const { meta } = column;
|
|
780
|
+
const { origin } = meta;
|
|
781
|
+
const declared = origin !== null && origin.format === FORMAT && origin.type !== null ? origin.type : null;
|
|
782
|
+
if (meta.components > 1) {
|
|
783
|
+
return "string";
|
|
784
|
+
}
|
|
785
|
+
switch (meta.dtype) {
|
|
786
|
+
case "bool":
|
|
787
|
+
return "boolean";
|
|
788
|
+
case "i32":
|
|
789
|
+
return "int";
|
|
790
|
+
case "f32":
|
|
791
|
+
return "float";
|
|
792
|
+
case "f64": {
|
|
793
|
+
if (declared !== null && declared.trim().toLowerCase() === "long" && columnIntegral(column)) {
|
|
794
|
+
return "long";
|
|
795
|
+
}
|
|
796
|
+
return "double";
|
|
797
|
+
}
|
|
798
|
+
case "u8":
|
|
799
|
+
return "int";
|
|
800
|
+
case "u32":
|
|
801
|
+
return columnMax(column) <= I32_MAX ? "int" : "long";
|
|
802
|
+
case "string":
|
|
803
|
+
if (declared !== null && TYPE_DTYPES[declared.trim().toLowerCase()] === undefined) {
|
|
804
|
+
// an unknown declared type kept as text (W_UNKNOWN_ATTR_TYPE on import) is restored as declared
|
|
805
|
+
return declared;
|
|
806
|
+
}
|
|
807
|
+
return "string";
|
|
808
|
+
case "dict":
|
|
809
|
+
case "list":
|
|
810
|
+
case "json":
|
|
811
|
+
return "string";
|
|
812
|
+
default: {
|
|
813
|
+
const { dtype } = meta;
|
|
814
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${String(dtype)}`, { dtype });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Whether every set value of a numeric column is an integer.
|
|
821
|
+
* @param column - an f64 column
|
|
822
|
+
* @returns true when all set values are integral
|
|
823
|
+
*/
|
|
824
|
+
function columnIntegral(column: Column): boolean {
|
|
825
|
+
if (column.dtype !== "f64") {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
const { data } = column;
|
|
829
|
+
for (let r = 0; r < column.length; r++) {
|
|
830
|
+
if (column.isSet(r) && !Number.isInteger(data[r])) {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* The largest set value of a u32 column.
|
|
839
|
+
* @param column - a u32 column
|
|
840
|
+
* @returns the maximum, 0 for an empty column
|
|
841
|
+
*/
|
|
842
|
+
function columnMax(column: Column): number {
|
|
843
|
+
if (column.dtype !== "u32") {
|
|
844
|
+
return 0;
|
|
845
|
+
}
|
|
846
|
+
let max = 0;
|
|
847
|
+
const { data } = column;
|
|
848
|
+
for (let r = 0; r < column.length; r++) {
|
|
849
|
+
if (column.isSet(r) && data[r] > max) {
|
|
850
|
+
max = data[r];
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return max;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Whether every explicit weight is integral (so a declared int / long weight key survives).
|
|
858
|
+
* @param snapshot - the snapshot
|
|
859
|
+
* @param weights - the explicit weights
|
|
860
|
+
* @returns true when all explicit weights are integers
|
|
861
|
+
*/
|
|
862
|
+
function weightsIntegral(snapshot: GraphSnapshot, weights: ExplicitWeights): boolean {
|
|
863
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
864
|
+
if (weights.isExplicit(e) && !Number.isInteger(weights.value(e))) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* How many values of a yfiles column are not serialisable trees.
|
|
873
|
+
* @param column - a json column
|
|
874
|
+
* @returns the count
|
|
875
|
+
*/
|
|
876
|
+
function countBadTrees(column: Column): number {
|
|
877
|
+
if (column.dtype !== "json") {
|
|
878
|
+
return 0;
|
|
879
|
+
}
|
|
880
|
+
let bad = 0;
|
|
881
|
+
for (let r = 0; r < column.length; r++) {
|
|
882
|
+
if (column.isSet(r) && treeProblem(column.values[r]) !== null) {
|
|
883
|
+
bad++;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return bad;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Node ids that read back as another type under the canonical rule: string ids that are
|
|
891
|
+
* canonical integer text, and numbers that are not safe integers.
|
|
892
|
+
* @param snapshot - the snapshot
|
|
893
|
+
* @returns the count
|
|
894
|
+
*/
|
|
895
|
+
function countIdTypeChanges(snapshot: GraphSnapshot): number {
|
|
896
|
+
const { ids } = snapshot;
|
|
897
|
+
if (ids.kind === "identity" || ids.kind === "dense") {
|
|
898
|
+
return 0;
|
|
899
|
+
}
|
|
900
|
+
let count = 0;
|
|
901
|
+
for (let i = 0; i < ids.size; i++) {
|
|
902
|
+
const id = ids.idOf(i);
|
|
903
|
+
if (typeof id === "string" ? isNmtoken(id) && isCanonicalIntegerText(id) : !Number.isSafeInteger(id)) {
|
|
904
|
+
count++;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return count;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Node ids the nmtoken charset cannot hold.
|
|
912
|
+
* @param snapshot - the snapshot
|
|
913
|
+
* @returns the count
|
|
914
|
+
*/
|
|
915
|
+
function countUnrepresentableNodeIds(snapshot: GraphSnapshot): number {
|
|
916
|
+
const { ids } = snapshot;
|
|
917
|
+
if (ids.kind === "identity" || ids.kind === "dense") {
|
|
918
|
+
return 0;
|
|
919
|
+
}
|
|
920
|
+
let count = 0;
|
|
921
|
+
for (let i = 0; i < ids.size; i++) {
|
|
922
|
+
if (!isNmtoken(String(ids.idOf(i)))) {
|
|
923
|
+
count++;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return count;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Edge ids that are not NMTOKENs.
|
|
931
|
+
* @param column - the edge id column
|
|
932
|
+
* @returns the count
|
|
933
|
+
*/
|
|
934
|
+
function countBadEdgeIds(column: Column): number {
|
|
935
|
+
let bad = 0;
|
|
936
|
+
for (let e = 0; e < column.length; e++) {
|
|
937
|
+
if (column.isSet(e) && !isNmtoken(edgeIdText(column, e))) {
|
|
938
|
+
bad++;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return bad;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* The text of an edge id cell.
|
|
946
|
+
* @param column - the edge id column
|
|
947
|
+
* @param e - the edge
|
|
948
|
+
* @returns the id as text
|
|
949
|
+
*/
|
|
950
|
+
function edgeIdText(column: Column, e: number): string {
|
|
951
|
+
const value = column.value(e);
|
|
952
|
+
if (typeof value === "string") {
|
|
953
|
+
return value;
|
|
954
|
+
}
|
|
955
|
+
if (typeof value === "number") {
|
|
956
|
+
return Number.isInteger(value) ? formatInteger(value) : formatF64(value);
|
|
957
|
+
}
|
|
958
|
+
return String(value);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Whether a logical edge is written as directed: never in an undirected snapshot; never for a
|
|
963
|
+
* half of an expanded pair (an undirected or a mutual source edge, whose halves the resolver
|
|
964
|
+
* flags directed); otherwise per the directed role column, with directed as the default.
|
|
965
|
+
* @param folding - the pair folding
|
|
966
|
+
* @param e - the edge
|
|
967
|
+
* @returns false for a paired edge or an edge flagged undirected
|
|
968
|
+
*/
|
|
969
|
+
function isDirectedEdge(folding: PairFolding, e: number): boolean {
|
|
970
|
+
return folding.mateOf(e) === INVALID_INDEX && folding.sourceDirected(e);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* The containment order: the children CSR over the parent column (src/children.ts), roots in
|
|
975
|
+
* index order, unreachable nodes (cycles) forced to the top level.
|
|
976
|
+
* @param snapshot - the snapshot
|
|
977
|
+
* @returns the hierarchy
|
|
978
|
+
*/
|
|
979
|
+
function planHierarchy(snapshot: GraphSnapshot): Hierarchy {
|
|
980
|
+
const parent = snapshot.nodes.byRole("parent");
|
|
981
|
+
const csr = childrenCsr(snapshot, { column: parent !== null && parent.dtype === "u32" ? parent : null });
|
|
982
|
+
const walk = csr.depthFirst();
|
|
983
|
+
// the top-level nodes in written order: the roots, then the cycle members forced out
|
|
984
|
+
const roots = walk.order.filter((u) => walk.depth[u] === 0);
|
|
985
|
+
return {
|
|
986
|
+
parent: csr.column,
|
|
987
|
+
roots,
|
|
988
|
+
childStart: csr.rowPtr,
|
|
989
|
+
childList: csr.children,
|
|
990
|
+
reordered: walk.reordered,
|
|
991
|
+
unreachable: csr.unreachable,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// ============================================================ writing
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* The text of one set cell for a `<data>` element or a `<default>`.
|
|
999
|
+
* @param column - the column
|
|
1000
|
+
* @param row - the row
|
|
1001
|
+
* @param attrType - the declared attr.type
|
|
1002
|
+
* @returns the text
|
|
1003
|
+
*/
|
|
1004
|
+
function cellText(column: Column, row: number, attrType: string | null): string {
|
|
1005
|
+
if (column.meta.components > 1) {
|
|
1006
|
+
const value = column.value(row);
|
|
1007
|
+
return JSON.stringify(Array.from(value as ArrayLike<number>));
|
|
1008
|
+
}
|
|
1009
|
+
switch (column.dtype) {
|
|
1010
|
+
case "bool":
|
|
1011
|
+
return column.value(row) === true ? "true" : "false";
|
|
1012
|
+
case "i32":
|
|
1013
|
+
case "u32":
|
|
1014
|
+
case "u8":
|
|
1015
|
+
return String(column.data[row]);
|
|
1016
|
+
case "f32":
|
|
1017
|
+
return formatF32(column.data[row]);
|
|
1018
|
+
case "f64": {
|
|
1019
|
+
const value = column.data[row];
|
|
1020
|
+
return (attrType === "long" || attrType === "int") && Number.isInteger(value)
|
|
1021
|
+
? formatInteger(value)
|
|
1022
|
+
: formatF64(value);
|
|
1023
|
+
}
|
|
1024
|
+
case "string":
|
|
1025
|
+
return column.valueAt(row);
|
|
1026
|
+
case "dict":
|
|
1027
|
+
return column.dictionary[column.codes[row]];
|
|
1028
|
+
case "list":
|
|
1029
|
+
return JSON.stringify(Array.from(column.sliceOf(row)));
|
|
1030
|
+
case "json":
|
|
1031
|
+
return JSON.stringify(column.values[row]) ?? "";
|
|
1032
|
+
default: {
|
|
1033
|
+
const dtype: never = column;
|
|
1034
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${String(dtype)}`, {});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* The text of a declared default.
|
|
1041
|
+
* @param column - the column
|
|
1042
|
+
* @param attrType - the declared attr.type
|
|
1043
|
+
* @returns the text, or null when the column has no default
|
|
1044
|
+
*/
|
|
1045
|
+
function defaultText(column: Column, attrType: string | null): string | null {
|
|
1046
|
+
const value = column.meta.default;
|
|
1047
|
+
if (value === undefined) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
switch (typeof value) {
|
|
1051
|
+
case "boolean":
|
|
1052
|
+
return value ? "true" : "false";
|
|
1053
|
+
case "number":
|
|
1054
|
+
if (column.dtype === "f32") {
|
|
1055
|
+
return formatF32(value);
|
|
1056
|
+
}
|
|
1057
|
+
return (attrType === "long" || attrType === "int") && Number.isInteger(value)
|
|
1058
|
+
? formatInteger(value)
|
|
1059
|
+
: formatF64(value);
|
|
1060
|
+
case "string":
|
|
1061
|
+
return value;
|
|
1062
|
+
default:
|
|
1063
|
+
return JSON.stringify(value) ?? "";
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Write one `<data>` element.
|
|
1069
|
+
* @param plan - the column plan
|
|
1070
|
+
* @param row - the row
|
|
1071
|
+
* @param indent - the indentation
|
|
1072
|
+
* @param out - receives the parts
|
|
1073
|
+
*/
|
|
1074
|
+
function writeData(plan: ColumnPlan, row: number, indent: string, out: string[]): void {
|
|
1075
|
+
const { column } = plan;
|
|
1076
|
+
if (!column.isSet(row)) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (plan.yfiles && column.dtype === "json") {
|
|
1080
|
+
writeTreeData(plan.keyId, column.values[row], indent, out);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
out.push(`\n${indent}<data key="${plan.keyId}">${escapeXmlText(cellText(column, row, plan.attrType))}</data>`);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Write a `<data>` holding a yfiles tree.
|
|
1088
|
+
* @param keyId - the key id
|
|
1089
|
+
* @param value - the tree
|
|
1090
|
+
* @param indent - the indentation
|
|
1091
|
+
* @param out - receives the parts
|
|
1092
|
+
*/
|
|
1093
|
+
function writeTreeData(keyId: string, value: unknown, indent: string, out: string[]): void {
|
|
1094
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1095
|
+
const parts: string[] = [];
|
|
1096
|
+
writeXmlTree(value, indent, parts);
|
|
1097
|
+
out.push(`\n${indent}<data key="${keyId}">${parts.join("")}</data>`);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
out.push(`\n${indent}<data key="${keyId}">`);
|
|
1101
|
+
writeXmlTree(value, `${indent} `, out);
|
|
1102
|
+
out.push(`\n${indent}</data>`);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* The `<key>` element of a plan.
|
|
1107
|
+
* @param key - the key plan
|
|
1108
|
+
* @param indent - the indentation
|
|
1109
|
+
* @returns the element text
|
|
1110
|
+
*/
|
|
1111
|
+
function keyElement(key: KeyPlan, indent: string): string {
|
|
1112
|
+
const domain = key.domains.length === 3 ? "all" : key.domains[0];
|
|
1113
|
+
let text = `${indent}<key id="${escapeXmlAttribute(key.id)}" for="${domain}"`;
|
|
1114
|
+
if (key.attrName !== null) {
|
|
1115
|
+
text += ` attr.name="${escapeXmlAttribute(key.attrName)}"`;
|
|
1116
|
+
}
|
|
1117
|
+
if (key.attrType !== null) {
|
|
1118
|
+
text += ` attr.type="${escapeXmlAttribute(key.attrType)}"`;
|
|
1119
|
+
}
|
|
1120
|
+
if (key.yfilesType !== null) {
|
|
1121
|
+
text += ` yfiles.type="${escapeXmlAttribute(key.yfilesType)}"`;
|
|
1122
|
+
}
|
|
1123
|
+
const { column } = key;
|
|
1124
|
+
const desc = column?.meta.extra.desc;
|
|
1125
|
+
const hasDefault = column !== null && column.meta.default !== undefined;
|
|
1126
|
+
if (column === null || (typeof desc !== "string" && !hasDefault)) {
|
|
1127
|
+
return `${text}/>\n`;
|
|
1128
|
+
}
|
|
1129
|
+
const parts: string[] = [`${text}>`];
|
|
1130
|
+
const inner = `${indent} `;
|
|
1131
|
+
if (typeof desc === "string") {
|
|
1132
|
+
parts.push(`\n${inner}<desc>${escapeXmlText(desc)}</desc>`);
|
|
1133
|
+
}
|
|
1134
|
+
if (hasDefault) {
|
|
1135
|
+
if (key.yfilesType !== null) {
|
|
1136
|
+
const value = column.meta.default;
|
|
1137
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1138
|
+
parts.push(`\n${inner}<default>`);
|
|
1139
|
+
writeXmlTree(value, `${inner} `, parts);
|
|
1140
|
+
parts.push(`\n${inner}</default>`);
|
|
1141
|
+
} else {
|
|
1142
|
+
const tree: string[] = [];
|
|
1143
|
+
writeXmlTree(value, inner, tree);
|
|
1144
|
+
parts.push(`\n${inner}<default>${tree.join("")}</default>`);
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
parts.push(`\n${inner}<default>${escapeXmlText(defaultText(column, key.attrType) ?? "")}</default>`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
parts.push(`\n${indent}</key>\n`);
|
|
1151
|
+
return parts.join("");
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Sanitised edge ids: NMTOKENs as they are; the rest refused or mangled per sanitizeIds.
|
|
1156
|
+
* @param column - the edge id column, or null
|
|
1157
|
+
* @param mode - the resolved sanitizeIds option
|
|
1158
|
+
* @param edgeCount - the edge count
|
|
1159
|
+
* @returns the id text per edge (null for edges without one)
|
|
1160
|
+
*/
|
|
1161
|
+
function sanitizeEdgeIds(column: Column | null, mode: "error" | "mangle", edgeCount: number): (string | null)[] {
|
|
1162
|
+
const out: (string | null)[] = new Array<string | null>(edgeCount).fill(null);
|
|
1163
|
+
if (column === null) {
|
|
1164
|
+
return out;
|
|
1165
|
+
}
|
|
1166
|
+
const used = new Set<string>();
|
|
1167
|
+
const bad: number[] = [];
|
|
1168
|
+
for (let e = 0; e < edgeCount; e++) {
|
|
1169
|
+
if (!column.isSet(e)) {
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
const text = edgeIdText(column, e);
|
|
1173
|
+
if (isNmtoken(text)) {
|
|
1174
|
+
out[e] = text;
|
|
1175
|
+
used.add(text);
|
|
1176
|
+
} else {
|
|
1177
|
+
bad.push(e);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (bad.length > 0 && mode === "error") {
|
|
1181
|
+
throw new GraphFormatError(
|
|
1182
|
+
"E_INVALID_ID",
|
|
1183
|
+
`${bad.length} edge id(s) cannot be written as nmtoken (first: ${JSON.stringify(edgeIdText(column, bad[0]))} at edge ${bad[0]}); pass sanitizeIds: "mangle" to rewrite them`,
|
|
1184
|
+
{ reason: "charset", charset: "nmtoken", count: bad.length, edge: bad[0] },
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
for (const e of bad) {
|
|
1188
|
+
const base = mangleNmtoken(edgeIdText(column, e));
|
|
1189
|
+
let candidate = base;
|
|
1190
|
+
for (let k = 2; used.has(candidate); k++) {
|
|
1191
|
+
candidate = `${base}_${k}`;
|
|
1192
|
+
}
|
|
1193
|
+
used.add(candidate);
|
|
1194
|
+
out[e] = candidate;
|
|
1195
|
+
}
|
|
1196
|
+
return out;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* The document as text parts.
|
|
1201
|
+
* @param snapshot - the snapshot
|
|
1202
|
+
* @param plan - the plan
|
|
1203
|
+
* @yields one element (or a group of small ones) at a time
|
|
1204
|
+
* @returns nothing
|
|
1205
|
+
*/
|
|
1206
|
+
function* writeGraphml(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, undefined> {
|
|
1207
|
+
const ids: SanitizedIds = sanitizeIds(snapshot, "nmtoken", plan.options.sanitizeIds);
|
|
1208
|
+
const edgeIds = sanitizeEdgeIds(plan.edgeIdColumn, plan.options.sanitizeIds, snapshot.edgeCount);
|
|
1209
|
+
const i1 = plan.pretty ? " " : "";
|
|
1210
|
+
const i2 = plan.pretty ? " " : "";
|
|
1211
|
+
const i3 = plan.pretty ? " " : "";
|
|
1212
|
+
const step = plan.pretty ? " " : "";
|
|
1213
|
+
|
|
1214
|
+
yield '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
1215
|
+
let root = `<graphml xmlns="${GRAPHML_NAMESPACE}"`;
|
|
1216
|
+
const namespaces: Record<string, string> = { ...(plan.meta?.namespaces ?? {}) };
|
|
1217
|
+
if (
|
|
1218
|
+
(plan.nodeColumns.some((c) => c.yfiles) ||
|
|
1219
|
+
plan.edgeColumns.some((c) => c.yfiles) ||
|
|
1220
|
+
plan.graphColumns.some((c) => c.yfiles)) &&
|
|
1221
|
+
!Object.values(namespaces).includes(YFILES_NAMESPACE)
|
|
1222
|
+
) {
|
|
1223
|
+
namespaces.y = YFILES_NAMESPACE;
|
|
1224
|
+
}
|
|
1225
|
+
if (!Object.values(namespaces).includes(XSI_NAMESPACE)) {
|
|
1226
|
+
namespaces.xsi = XSI_NAMESPACE;
|
|
1227
|
+
}
|
|
1228
|
+
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
1229
|
+
root += ` xmlns:${prefix}="${escapeXmlAttribute(uri)}"`;
|
|
1230
|
+
}
|
|
1231
|
+
const xsiPrefix = Object.entries(namespaces).find(([, uri]) => uri === XSI_NAMESPACE)?.[0] ?? "xsi";
|
|
1232
|
+
root += ` ${xsiPrefix}:schemaLocation="${SCHEMA_LOCATION}">\n`;
|
|
1233
|
+
yield root;
|
|
1234
|
+
|
|
1235
|
+
for (const key of plan.keys) {
|
|
1236
|
+
yield keyElement(key, i1);
|
|
1237
|
+
}
|
|
1238
|
+
if (plan.originalIdKey !== null) {
|
|
1239
|
+
yield `${i1}<key id="${plan.originalIdKey}" for="node" attr.name="${ORIGINAL_ID_ATTRIBUTE}" attr.type="string"/>\n`;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const graphId = plan.meta?.graphId ?? "G";
|
|
1243
|
+
yield `${i1}<graph id="${escapeXmlAttribute(graphId)}" edgedefault="${plan.edgedefault}">`;
|
|
1244
|
+
const { description } = snapshot.meta;
|
|
1245
|
+
if (description !== null) {
|
|
1246
|
+
yield `\n${i2}<desc>${escapeXmlText(description)}</desc>`;
|
|
1247
|
+
}
|
|
1248
|
+
const graphParts: string[] = [];
|
|
1249
|
+
for (const column of plan.graphColumns) {
|
|
1250
|
+
writeData(column, 0, i2, graphParts);
|
|
1251
|
+
}
|
|
1252
|
+
if (graphParts.length > 0) {
|
|
1253
|
+
yield graphParts.join("");
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// nodes in containment order, children nested under their parent
|
|
1257
|
+
const { hierarchy } = plan;
|
|
1258
|
+
const nested = hierarchy.childList.length > 0;
|
|
1259
|
+
const stackNode: number[] = [];
|
|
1260
|
+
const stackCursor: number[] = [];
|
|
1261
|
+
const written = nested ? new Uint8Array(snapshot.nodeCount) : null;
|
|
1262
|
+
for (const rootNode of hierarchy.roots) {
|
|
1263
|
+
yield* writeNodeSubtree(rootNode, plan, ids, i2, step, stackNode, stackCursor, written);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// edges: primaries only, in index order
|
|
1267
|
+
const list = snapshot.edgeList();
|
|
1268
|
+
const { folding } = plan;
|
|
1269
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1270
|
+
if (folding.folded(e)) {
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
const parts: string[] = [`\n${i2}<edge`];
|
|
1274
|
+
const id = edgeIds[e];
|
|
1275
|
+
if (id !== null) {
|
|
1276
|
+
parts.push(` id="${escapeXmlAttribute(id)}"`);
|
|
1277
|
+
}
|
|
1278
|
+
parts.push(
|
|
1279
|
+
` source="${escapeXmlAttribute(String(ids.idAt(list.src[e])))}" target="${escapeXmlAttribute(String(ids.idAt(list.dst[e])))}"`,
|
|
1280
|
+
);
|
|
1281
|
+
const directed = snapshot.directed && isDirectedEdge(folding, e);
|
|
1282
|
+
if (directed !== (plan.edgedefault === "directed")) {
|
|
1283
|
+
parts.push(` directed="${directed ? "true" : "false"}"`);
|
|
1284
|
+
}
|
|
1285
|
+
const sourcePort = portText(plan.sourcePortColumn, e);
|
|
1286
|
+
if (sourcePort !== null) {
|
|
1287
|
+
parts.push(` sourceport="${escapeXmlAttribute(sourcePort)}"`);
|
|
1288
|
+
}
|
|
1289
|
+
const targetPort = portText(plan.targetPortColumn, e);
|
|
1290
|
+
if (targetPort !== null) {
|
|
1291
|
+
parts.push(` targetport="${escapeXmlAttribute(targetPort)}"`);
|
|
1292
|
+
}
|
|
1293
|
+
const open = parts.length;
|
|
1294
|
+
if (plan.weight !== null) {
|
|
1295
|
+
const { keyId, attrType } = plan.weight;
|
|
1296
|
+
const weight = plan.weight.weights.text(e, attrType === "int" || attrType === "long");
|
|
1297
|
+
if (weight !== null) {
|
|
1298
|
+
parts.push(`\n${i3}<data key="${keyId}">${escapeXmlText(weight)}</data>`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
for (const column of plan.edgeColumns) {
|
|
1302
|
+
writeData(column, e, i3, parts);
|
|
1303
|
+
}
|
|
1304
|
+
if (parts.length === open) {
|
|
1305
|
+
parts.push("/>");
|
|
1306
|
+
} else {
|
|
1307
|
+
parts.splice(open, 0, ">");
|
|
1308
|
+
parts.push(`\n${i2}</edge>`);
|
|
1309
|
+
}
|
|
1310
|
+
yield parts.join("");
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
yield `\n${i1}</graph>\n</graphml>\n`;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* The text of a port reference cell.
|
|
1318
|
+
* @param column - the sourcePort / targetPort column, or null
|
|
1319
|
+
* @param e - the edge
|
|
1320
|
+
* @returns the port name, or null when unset
|
|
1321
|
+
*/
|
|
1322
|
+
function portText(column: Column | null, e: number): string | null {
|
|
1323
|
+
if (column === null || !column.isSet(e)) {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
const value = column.value(e);
|
|
1327
|
+
return typeof value === "string" ? value : String(value);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Write a top-level node and, nested, every node contained in it (iteratively, so a deep chain
|
|
1332
|
+
* cannot overflow the call stack).
|
|
1333
|
+
* @param root - the top-level node
|
|
1334
|
+
* @param plan - the plan
|
|
1335
|
+
* @param ids - the sanitised ids
|
|
1336
|
+
* @param indent - the indentation of top-level nodes
|
|
1337
|
+
* @param step - one indentation step
|
|
1338
|
+
* @param stackNode - a reusable stack of open nodes
|
|
1339
|
+
* @param stackCursor - a reusable stack of child cursors
|
|
1340
|
+
* @param written - nodes already written (null when nothing is nested)
|
|
1341
|
+
* @yields the node elements
|
|
1342
|
+
* @returns nothing
|
|
1343
|
+
*/
|
|
1344
|
+
function* writeNodeSubtree(
|
|
1345
|
+
root: number,
|
|
1346
|
+
plan: Plan,
|
|
1347
|
+
ids: SanitizedIds,
|
|
1348
|
+
indent: string,
|
|
1349
|
+
step: string,
|
|
1350
|
+
stackNode: number[],
|
|
1351
|
+
stackCursor: number[],
|
|
1352
|
+
written: Uint8Array | null,
|
|
1353
|
+
): Generator<string, void, undefined> {
|
|
1354
|
+
const { hierarchy } = plan;
|
|
1355
|
+
const hasChildren = (u: number): boolean => hierarchy.childStart[u + 1] > hierarchy.childStart[u];
|
|
1356
|
+
const openNode = (u: number, depth: number): string => {
|
|
1357
|
+
const pad = indent + step.repeat(depth * 2);
|
|
1358
|
+
const parts: string[] = [`\n${pad}<node id="${escapeXmlAttribute(String(ids.idAt(u)))}"`];
|
|
1359
|
+
const open = parts.length;
|
|
1360
|
+
if (plan.originalIdKey !== null && ids.isChanged(u)) {
|
|
1361
|
+
parts.push(
|
|
1362
|
+
`\n${pad}${step}<data key="${plan.originalIdKey}">${escapeXmlText(String(ids.originalAt(u)))}</data>`,
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
for (const column of plan.nodeColumns) {
|
|
1366
|
+
writeData(column, u, pad + step, parts);
|
|
1367
|
+
}
|
|
1368
|
+
if (hasChildren(u)) {
|
|
1369
|
+
parts.splice(open, 0, ">");
|
|
1370
|
+
parts.push(
|
|
1371
|
+
`\n${pad}${step}<graph id="${escapeXmlAttribute(`${String(ids.idAt(u))}:`)}" edgedefault="${plan.edgedefault}">`,
|
|
1372
|
+
);
|
|
1373
|
+
return parts.join("");
|
|
1374
|
+
}
|
|
1375
|
+
if (parts.length === open) {
|
|
1376
|
+
parts.push("/>");
|
|
1377
|
+
} else {
|
|
1378
|
+
parts.splice(open, 0, ">");
|
|
1379
|
+
parts.push(`\n${pad}</node>`);
|
|
1380
|
+
}
|
|
1381
|
+
return parts.join("");
|
|
1382
|
+
};
|
|
1383
|
+
const closeNode = (depth: number): string => {
|
|
1384
|
+
const pad = indent + step.repeat(depth * 2);
|
|
1385
|
+
return `\n${pad}${step}</graph>\n${pad}</node>`;
|
|
1386
|
+
};
|
|
1387
|
+
if (written !== null) {
|
|
1388
|
+
written[root] = 1;
|
|
1389
|
+
}
|
|
1390
|
+
yield openNode(root, 0);
|
|
1391
|
+
if (!hasChildren(root)) {
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
stackNode.push(root);
|
|
1395
|
+
stackCursor.push(hierarchy.childStart[root]);
|
|
1396
|
+
while (stackNode.length > 0) {
|
|
1397
|
+
const top = stackNode.length - 1;
|
|
1398
|
+
const u = stackNode[top];
|
|
1399
|
+
const cursor = stackCursor[top];
|
|
1400
|
+
if (cursor >= hierarchy.childStart[u + 1]) {
|
|
1401
|
+
stackNode.pop();
|
|
1402
|
+
stackCursor.pop();
|
|
1403
|
+
yield closeNode(top);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
stackCursor[top] = cursor + 1;
|
|
1407
|
+
const v = hierarchy.childList[cursor];
|
|
1408
|
+
if (written !== null && written[v] === 1) {
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
if (written !== null) {
|
|
1412
|
+
written[v] = 1;
|
|
1413
|
+
}
|
|
1414
|
+
yield openNode(v, top + 1);
|
|
1415
|
+
if (hasChildren(v)) {
|
|
1416
|
+
stackNode.push(v);
|
|
1417
|
+
stackCursor.push(hierarchy.childStart[v]);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* The GraphML exporter (design section 8.5).
|
|
1424
|
+
*/
|
|
1425
|
+
export const graphmlExporter: GraphExporter<GraphmlExportOptions> = Object.freeze({
|
|
1426
|
+
format: FORMAT,
|
|
1427
|
+
capabilities: CAPABILITIES,
|
|
1428
|
+
/**
|
|
1429
|
+
* Pre-flight: every loss the export would incur.
|
|
1430
|
+
* @param snapshot - the snapshot
|
|
1431
|
+
* @param options - format-specific and common options
|
|
1432
|
+
* @returns the notes
|
|
1433
|
+
*/
|
|
1434
|
+
check(snapshot: GraphSnapshot, options?: GraphmlExportOptions & CommonExportOptions): readonly LossNote[] {
|
|
1435
|
+
const plan = planExport(snapshot, resolveExportOptions(options), resolveFormatOptions(options));
|
|
1436
|
+
return Object.freeze(plan.notes);
|
|
1437
|
+
},
|
|
1438
|
+
/**
|
|
1439
|
+
* Write the document as UTF-8 chunks.
|
|
1440
|
+
* @param snapshot - the snapshot
|
|
1441
|
+
* @param options - format-specific and common options
|
|
1442
|
+
* @returns the chunks
|
|
1443
|
+
*/
|
|
1444
|
+
export(snapshot: GraphSnapshot, options?: GraphmlExportOptions & CommonExportOptions): AsyncIterable<Uint8Array> {
|
|
1445
|
+
const plan = planExport(snapshot, resolveExportOptions(options), resolveFormatOptions(options));
|
|
1446
|
+
return encodeChunks(writeGraphml(snapshot, plan));
|
|
1447
|
+
},
|
|
1448
|
+
/**
|
|
1449
|
+
* Write the document as one string.
|
|
1450
|
+
* @param snapshot - the snapshot
|
|
1451
|
+
* @param options - format-specific and common options
|
|
1452
|
+
* @returns the document
|
|
1453
|
+
*/
|
|
1454
|
+
exportToString(snapshot: GraphSnapshot, options?: GraphmlExportOptions & CommonExportOptions): Promise<string> {
|
|
1455
|
+
const plan = planExport(snapshot, resolveExportOptions(options), resolveFormatOptions(options));
|
|
1456
|
+
return joinText(writeGraphml(snapshot, plan));
|
|
1457
|
+
},
|
|
1458
|
+
});
|