@graphty/graph-io 0.0.0 → 0.2.1
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,1607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GraphML importer (design sections 8.4 and 5.1; research note 07 section 2.3): a single-pass
|
|
3
|
+
* SAX-style reader over the streaming XML tokenizer that pushes scalars into the sink as elements
|
|
4
|
+
* close. `<key>` declarations become typed columns up front (boolean / int / long / float / double
|
|
5
|
+
* / string, with `<default>`, for the node, edge and graph domains; `for="all"` declares in all
|
|
6
|
+
* three with the same origin.id); `edgedefault` and per-edge `directed` go through the common
|
|
7
|
+
* DirectionResolver; a nested `<graph>` inside a node becomes the `parent` column (containment,
|
|
8
|
+
* never topology); `<port>` declarations and `<locator>` elements are reported and dropped while
|
|
9
|
+
* `sourceport` / `targetport` edge attributes are kept as role columns; hyperedges follow the
|
|
10
|
+
* `hyperedges` option (refuse, skip with a report entry, star, clique); yFiles `yfiles.type` keys
|
|
11
|
+
* become `json` columns holding the nested XML as a tree (origin.namespace "yfiles"), reported as
|
|
12
|
+
* a loss note because the structure, not the bytes, is preserved.
|
|
13
|
+
*
|
|
14
|
+
* Ids are coerced with the common rule (`ids: "canonical"` by default, design section 4.1); the
|
|
15
|
+
* edge attribute whose `attr.name` is `weightFrom` ("weight" by default) is THE weight and is
|
|
16
|
+
* passed to `addEdge` rather than stored as a column, with its declaration kept in
|
|
17
|
+
* `meta.weightOrigin`; a node key named `graphty:originalId` restores ids the exporter mangled.
|
|
18
|
+
*/
|
|
19
|
+
import { GraphFormatError, INVALID_INDEX, MAX_COUNT, } from "@graphty/graph-format";
|
|
20
|
+
import { declareAttribute, declareResolved, losesPrecision, parseDeclaredValue, PRECISION_CODE, takenIn, uniqueColumnName, } from "../../common/attributes.js";
|
|
21
|
+
import { DirectionResolver } from "../../common/direction.js";
|
|
22
|
+
import { IdCoercer } from "../../common/ids.js";
|
|
23
|
+
import { textChunks, throwIfAborted } from "../../common/input.js";
|
|
24
|
+
import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, } from "../../common/options.js";
|
|
25
|
+
import { ImportReportBuilder } from "../../common/report.js";
|
|
26
|
+
import { isWeightField, parseWeightText } from "../../common/weights.js";
|
|
27
|
+
import { isWhitespace, localName, tokenizeXml, XmlSyntaxError } from "../../common/xml.js";
|
|
28
|
+
import { EDGE_ID_COLUMN, EXTENSIONS, FORMAT, GRAPHML_ISSUE, GRAPHML_LOSS, GRAPHML_NAMESPACE, HYPEREDGE_HUB_COLUMN, LABEL_COLUMN, META_KEY, MIME_TYPES, ORIGINAL_ID_ATTRIBUTE, ORIGINAL_ID_COLUMN, PARENT_COLUMN, RESERVED_EDGE_NAMES, RESERVED_NODE_NAMES, SOURCE_PORT_COLUMN, TARGET_PORT_COLUMN, XSI_NAMESPACE, } from "./constants.js";
|
|
29
|
+
import { XmlTreeBuilder } from "./tree.js";
|
|
30
|
+
/** The per-format defaults of design section 8.4: text-cell ids, undirected when edgedefault is missing, weight key. */
|
|
31
|
+
const FORMAT_DEFAULTS = { ids: "canonical", defaultDirected: false, weightFrom: "weight" };
|
|
32
|
+
/** Bytes inspected by sniff(). */
|
|
33
|
+
const SNIFF_BYTES = 4096;
|
|
34
|
+
const YFILES_MODES = new Set(["json", "skip"]);
|
|
35
|
+
/** Elements between two checks of the cancellation signal. */
|
|
36
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
37
|
+
/** The common options the GraphML importer reads (the rest is reported by reportUnusedOptions). */
|
|
38
|
+
const USED_OPTIONS = new Set([
|
|
39
|
+
"ids",
|
|
40
|
+
"addMissingNodes",
|
|
41
|
+
"duplicateEdges",
|
|
42
|
+
"selfLoops",
|
|
43
|
+
"onMixedDirection",
|
|
44
|
+
"defaultDirected",
|
|
45
|
+
"weightFrom",
|
|
46
|
+
"weightDtype",
|
|
47
|
+
"long",
|
|
48
|
+
"restoreMangledIds",
|
|
49
|
+
"hyperedges",
|
|
50
|
+
"errorLimit",
|
|
51
|
+
"signal",
|
|
52
|
+
"onProgress",
|
|
53
|
+
]);
|
|
54
|
+
/** Where the reader is, by the innermost open element. */
|
|
55
|
+
var Ctx;
|
|
56
|
+
(function (Ctx) {
|
|
57
|
+
Ctx[Ctx["Graphml"] = 0] = "Graphml";
|
|
58
|
+
Ctx[Ctx["Desc"] = 1] = "Desc";
|
|
59
|
+
Ctx[Ctx["Key"] = 2] = "Key";
|
|
60
|
+
Ctx[Ctx["Default"] = 3] = "Default";
|
|
61
|
+
Ctx[Ctx["Graph"] = 4] = "Graph";
|
|
62
|
+
Ctx[Ctx["Node"] = 5] = "Node";
|
|
63
|
+
Ctx[Ctx["Edge"] = 6] = "Edge";
|
|
64
|
+
Ctx[Ctx["Hyperedge"] = 7] = "Hyperedge";
|
|
65
|
+
Ctx[Ctx["Data"] = 8] = "Data";
|
|
66
|
+
Ctx[Ctx["Capture"] = 9] = "Capture";
|
|
67
|
+
Ctx[Ctx["Skip"] = 10] = "Skip";
|
|
68
|
+
})(Ctx || (Ctx = {}));
|
|
69
|
+
/**
|
|
70
|
+
* A growable bit set over node indices, marking the indices declared by a `<node>` element so a
|
|
71
|
+
* second declaration is reported and a node created by an edge reference is not.
|
|
72
|
+
*/
|
|
73
|
+
class IndexFlags {
|
|
74
|
+
constructor() {
|
|
75
|
+
this.words = new Uint32Array(64);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether an index is marked.
|
|
79
|
+
* @param index - the node index
|
|
80
|
+
* @returns true when set
|
|
81
|
+
*/
|
|
82
|
+
has(index) {
|
|
83
|
+
const w = index >>> 5;
|
|
84
|
+
return w < this.words.length && ((this.words[w] >>> (index & 31)) & 1) === 1;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Mark an index.
|
|
88
|
+
* @param index - the node index
|
|
89
|
+
*/
|
|
90
|
+
add(index) {
|
|
91
|
+
const w = index >>> 5;
|
|
92
|
+
if (w >= this.words.length) {
|
|
93
|
+
let size = this.words.length * 2;
|
|
94
|
+
while (size <= w) {
|
|
95
|
+
size *= 2;
|
|
96
|
+
}
|
|
97
|
+
const grown = new Uint32Array(size);
|
|
98
|
+
grown.set(this.words);
|
|
99
|
+
this.words = grown;
|
|
100
|
+
}
|
|
101
|
+
this.words[w] |= 1 << (index & 31);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The event handler that turns the tokenizer's events into sink calls.
|
|
106
|
+
*/
|
|
107
|
+
class GraphmlReader {
|
|
108
|
+
/**
|
|
109
|
+
* Create a reader for one import call.
|
|
110
|
+
* @param sink - the sink
|
|
111
|
+
* @param report - the report
|
|
112
|
+
* @param options - the resolved common options
|
|
113
|
+
* @param yfilesMode - the resolved yfiles option
|
|
114
|
+
*/
|
|
115
|
+
constructor(sink, report, options, yfilesMode) {
|
|
116
|
+
this.ctx = [];
|
|
117
|
+
this.keys = new Map();
|
|
118
|
+
this.graphs = [];
|
|
119
|
+
this.nodes = [];
|
|
120
|
+
this.declared = new IndexFlags();
|
|
121
|
+
this.edgeIds = new Set();
|
|
122
|
+
this.elementsSinceCheck = 0;
|
|
123
|
+
/** Mangled id text -> the original id restored from `graphty:originalId`, for edge endpoints. */
|
|
124
|
+
this.restored = new Map();
|
|
125
|
+
this.graphColumnNames = new Set();
|
|
126
|
+
this.pendingKey = null;
|
|
127
|
+
this.edge = null;
|
|
128
|
+
this.hyperedge = null;
|
|
129
|
+
this.content = {
|
|
130
|
+
key: null,
|
|
131
|
+
pendingKey: null,
|
|
132
|
+
domain: "node",
|
|
133
|
+
text: "",
|
|
134
|
+
tree: null,
|
|
135
|
+
line: 0,
|
|
136
|
+
};
|
|
137
|
+
this.descTarget = null;
|
|
138
|
+
this.descText = "";
|
|
139
|
+
this.description = null;
|
|
140
|
+
this.graphSeen = false;
|
|
141
|
+
this.graphId = null;
|
|
142
|
+
this.edgedefault = null;
|
|
143
|
+
this.namespaces = {};
|
|
144
|
+
this.weightOrigin = null;
|
|
145
|
+
this.yfilesLossNoted = false;
|
|
146
|
+
this.hyperedgeCount = 0;
|
|
147
|
+
this.edgeIdHandle = INVALID_INDEX;
|
|
148
|
+
this.sourcePortHandle = INVALID_INDEX;
|
|
149
|
+
this.targetPortHandle = INVALID_INDEX;
|
|
150
|
+
this.parentHandle = INVALID_INDEX;
|
|
151
|
+
this.hubHandle = INVALID_INDEX;
|
|
152
|
+
this.sink = sink;
|
|
153
|
+
this.report = report;
|
|
154
|
+
this.options = options;
|
|
155
|
+
this.yfilesMode = yfilesMode;
|
|
156
|
+
this.coercer = new IdCoercer(options.ids);
|
|
157
|
+
this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
158
|
+
}
|
|
159
|
+
// ------------------------------------------------------------------ tokenizer events
|
|
160
|
+
/**
|
|
161
|
+
* An element starts.
|
|
162
|
+
* @param name - the element name
|
|
163
|
+
* @param attrs - its attributes
|
|
164
|
+
* @param line - the line
|
|
165
|
+
*/
|
|
166
|
+
start(name, attrs, line) {
|
|
167
|
+
const parent = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
|
|
168
|
+
switch (parent) {
|
|
169
|
+
case Ctx.Capture:
|
|
170
|
+
this.captureStart(name, attrs);
|
|
171
|
+
return;
|
|
172
|
+
case Ctx.Skip:
|
|
173
|
+
case Ctx.Desc:
|
|
174
|
+
this.ctx.push(Ctx.Skip);
|
|
175
|
+
return;
|
|
176
|
+
case Ctx.Data:
|
|
177
|
+
case Ctx.Default:
|
|
178
|
+
this.captureStart(name, attrs);
|
|
179
|
+
return;
|
|
180
|
+
default:
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
const local = localName(name);
|
|
184
|
+
switch (parent) {
|
|
185
|
+
case null:
|
|
186
|
+
if (local !== "graphml") {
|
|
187
|
+
this.report.fail(GRAPHML_ISSUE.NOT_GRAPHML, `the root element is <${name}>, not <graphml>`, {
|
|
188
|
+
line,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
this.readRoot(attrs);
|
|
192
|
+
this.ctx.push(Ctx.Graphml);
|
|
193
|
+
return;
|
|
194
|
+
case Ctx.Graphml:
|
|
195
|
+
this.startInGraphml(name, local, attrs, line);
|
|
196
|
+
return;
|
|
197
|
+
case Ctx.Key:
|
|
198
|
+
this.startInKey(name, local, line);
|
|
199
|
+
return;
|
|
200
|
+
case Ctx.Graph:
|
|
201
|
+
this.startInGraph(name, local, attrs, line);
|
|
202
|
+
return;
|
|
203
|
+
case Ctx.Node:
|
|
204
|
+
this.startInNode(name, local, attrs, line);
|
|
205
|
+
return;
|
|
206
|
+
case Ctx.Edge:
|
|
207
|
+
this.startInEdge(name, local, attrs, line);
|
|
208
|
+
return;
|
|
209
|
+
case Ctx.Hyperedge:
|
|
210
|
+
this.startInHyperedge(name, local, attrs, line);
|
|
211
|
+
return;
|
|
212
|
+
default:
|
|
213
|
+
this.unknownElement(name, line);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* An element ends.
|
|
218
|
+
* @param name - the element name
|
|
219
|
+
* @param _line - the line (unused: issues of an element cite its start line)
|
|
220
|
+
*/
|
|
221
|
+
end(name, _line) {
|
|
222
|
+
const ctx = this.ctx.pop();
|
|
223
|
+
switch (ctx) {
|
|
224
|
+
case Ctx.Capture:
|
|
225
|
+
this.captureEnd();
|
|
226
|
+
return;
|
|
227
|
+
case Ctx.Skip:
|
|
228
|
+
case Ctx.Graphml:
|
|
229
|
+
case undefined:
|
|
230
|
+
return;
|
|
231
|
+
case Ctx.Desc:
|
|
232
|
+
this.finishDesc();
|
|
233
|
+
return;
|
|
234
|
+
case Ctx.Key:
|
|
235
|
+
this.finishKey();
|
|
236
|
+
return;
|
|
237
|
+
case Ctx.Default:
|
|
238
|
+
this.finishDefault();
|
|
239
|
+
return;
|
|
240
|
+
case Ctx.Data:
|
|
241
|
+
this.finishData();
|
|
242
|
+
return;
|
|
243
|
+
case Ctx.Node:
|
|
244
|
+
this.finishNode();
|
|
245
|
+
return;
|
|
246
|
+
case Ctx.Edge:
|
|
247
|
+
this.finishEdge();
|
|
248
|
+
return;
|
|
249
|
+
case Ctx.Hyperedge:
|
|
250
|
+
this.finishHyperedge();
|
|
251
|
+
return;
|
|
252
|
+
case Ctx.Graph:
|
|
253
|
+
this.graphs.pop();
|
|
254
|
+
return;
|
|
255
|
+
default: {
|
|
256
|
+
const value = ctx;
|
|
257
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown reader state ${String(value)}`, { name });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Character data.
|
|
263
|
+
* @param text - the text
|
|
264
|
+
* @param line - the line
|
|
265
|
+
*/
|
|
266
|
+
text(text, line) {
|
|
267
|
+
const ctx = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
|
|
268
|
+
switch (ctx) {
|
|
269
|
+
case Ctx.Capture:
|
|
270
|
+
this.content.tree?.text(text);
|
|
271
|
+
return;
|
|
272
|
+
case Ctx.Data:
|
|
273
|
+
case Ctx.Default:
|
|
274
|
+
if (this.content.tree === null) {
|
|
275
|
+
this.content.text += text;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
this.content.tree.text(text);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
case Ctx.Desc:
|
|
282
|
+
this.descText += text;
|
|
283
|
+
return;
|
|
284
|
+
case Ctx.Skip:
|
|
285
|
+
return;
|
|
286
|
+
default:
|
|
287
|
+
if (!isWhitespace(text)) {
|
|
288
|
+
this.report.warnOnce("validation-error", GRAPHML_ISSUE.STRAY_TEXT, "text where GraphML allows only elements was ignored", { line });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* After the last event: the document must have held a graph; the metadata is written.
|
|
294
|
+
*/
|
|
295
|
+
finish() {
|
|
296
|
+
if (!this.graphSeen) {
|
|
297
|
+
this.report.fail(GRAPHML_ISSUE.NO_GRAPH, "the document has no <graph> element");
|
|
298
|
+
}
|
|
299
|
+
const meta = { graphId: this.graphId, edgedefault: this.edgedefault, namespaces: this.namespaces };
|
|
300
|
+
this.sink.setMeta({
|
|
301
|
+
sourceFormat: FORMAT,
|
|
302
|
+
description: this.description ?? undefined,
|
|
303
|
+
weightOrigin: this.weightOrigin === null
|
|
304
|
+
? undefined
|
|
305
|
+
: {
|
|
306
|
+
format: FORMAT,
|
|
307
|
+
id: this.weightOrigin.id,
|
|
308
|
+
title: this.weightOrigin.title,
|
|
309
|
+
type: this.weightOrigin.type,
|
|
310
|
+
namespace: null,
|
|
311
|
+
},
|
|
312
|
+
extra: { [META_KEY]: meta },
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// ------------------------------------------------------------------ structure
|
|
316
|
+
/**
|
|
317
|
+
* Record the root element's namespace declarations.
|
|
318
|
+
* @param attrs - the root attributes
|
|
319
|
+
*/
|
|
320
|
+
readRoot(attrs) {
|
|
321
|
+
for (const [key, value] of attrs) {
|
|
322
|
+
if (key.startsWith("xmlns:") && value !== GRAPHML_NAMESPACE && value !== XSI_NAMESPACE) {
|
|
323
|
+
this.namespaces[key.slice(6)] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* A child of `<graphml>`.
|
|
329
|
+
* @param name - the element name
|
|
330
|
+
* @param local - its local name
|
|
331
|
+
* @param attrs - its attributes
|
|
332
|
+
* @param line - the line
|
|
333
|
+
*/
|
|
334
|
+
startInGraphml(name, local, attrs, line) {
|
|
335
|
+
switch (local) {
|
|
336
|
+
case "desc":
|
|
337
|
+
this.beginDesc("graph");
|
|
338
|
+
return;
|
|
339
|
+
case "key":
|
|
340
|
+
this.beginKey(attrs, line);
|
|
341
|
+
return;
|
|
342
|
+
case "graph":
|
|
343
|
+
this.beginGraph(attrs, line, INVALID_INDEX);
|
|
344
|
+
return;
|
|
345
|
+
case "data":
|
|
346
|
+
this.beginData(attrs, line, "graph");
|
|
347
|
+
return;
|
|
348
|
+
default:
|
|
349
|
+
this.unknownElement(name, line);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* A child of `<key>`.
|
|
354
|
+
* @param name - the element name
|
|
355
|
+
* @param local - its local name
|
|
356
|
+
* @param line - the line
|
|
357
|
+
*/
|
|
358
|
+
startInKey(name, local, line) {
|
|
359
|
+
switch (local) {
|
|
360
|
+
case "desc":
|
|
361
|
+
this.beginDesc("key");
|
|
362
|
+
return;
|
|
363
|
+
case "default":
|
|
364
|
+
this.content.key = null;
|
|
365
|
+
this.content.pendingKey = this.pendingKey;
|
|
366
|
+
this.content.text = "";
|
|
367
|
+
this.content.tree = null;
|
|
368
|
+
this.content.line = line;
|
|
369
|
+
this.ctx.push(Ctx.Default);
|
|
370
|
+
return;
|
|
371
|
+
default:
|
|
372
|
+
this.unknownElement(name, line);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* A child of `<graph>`.
|
|
377
|
+
* @param name - the element name
|
|
378
|
+
* @param local - its local name
|
|
379
|
+
* @param attrs - its attributes
|
|
380
|
+
* @param line - the line
|
|
381
|
+
*/
|
|
382
|
+
startInGraph(name, local, attrs, line) {
|
|
383
|
+
switch (local) {
|
|
384
|
+
case "desc":
|
|
385
|
+
if (this.graphs.length === 1) {
|
|
386
|
+
this.beginDesc("graph");
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
this.ctx.push(Ctx.Skip);
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
case "data":
|
|
393
|
+
if (this.graphs.length === 1) {
|
|
394
|
+
this.beginData(attrs, line, "graph");
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.NESTED_GRAPH_DATA, "data of a nested <graph> cannot be kept; only the top-level graph has attributes", { line });
|
|
398
|
+
this.ctx.push(Ctx.Skip);
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
case "node":
|
|
402
|
+
this.beginNode(attrs, line);
|
|
403
|
+
return;
|
|
404
|
+
case "edge":
|
|
405
|
+
this.beginEdge(attrs, line);
|
|
406
|
+
return;
|
|
407
|
+
case "hyperedge":
|
|
408
|
+
this.beginHyperedge(attrs, line);
|
|
409
|
+
return;
|
|
410
|
+
case "locator":
|
|
411
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.LOCATOR_DROPPED, "<locator> references are not followed", { line });
|
|
412
|
+
this.ctx.push(Ctx.Skip);
|
|
413
|
+
return;
|
|
414
|
+
default:
|
|
415
|
+
this.unknownElement(name, line);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* A child of `<node>`.
|
|
420
|
+
* @param name - the element name
|
|
421
|
+
* @param local - its local name
|
|
422
|
+
* @param attrs - its attributes
|
|
423
|
+
* @param line - the line
|
|
424
|
+
*/
|
|
425
|
+
startInNode(name, local, attrs, line) {
|
|
426
|
+
switch (local) {
|
|
427
|
+
case "desc":
|
|
428
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.DESC_DROPPED, "node and edge <desc> texts are not kept", {
|
|
429
|
+
line,
|
|
430
|
+
});
|
|
431
|
+
this.ctx.push(Ctx.Skip);
|
|
432
|
+
return;
|
|
433
|
+
case "data":
|
|
434
|
+
this.beginData(attrs, line, "node");
|
|
435
|
+
return;
|
|
436
|
+
case "port":
|
|
437
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.PORT_DECLARATION, "<port> declarations are not kept; sourceport / targetport edge attributes are", { line });
|
|
438
|
+
this.ctx.push(Ctx.Skip);
|
|
439
|
+
return;
|
|
440
|
+
case "graph": {
|
|
441
|
+
const node = this.nodes[this.nodes.length - 1];
|
|
442
|
+
this.materializeNode(node);
|
|
443
|
+
this.beginGraph(attrs, line, node.index);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
case "locator":
|
|
447
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.LOCATOR_DROPPED, "<locator> references are not followed", { line });
|
|
448
|
+
this.ctx.push(Ctx.Skip);
|
|
449
|
+
return;
|
|
450
|
+
default:
|
|
451
|
+
this.unknownElement(name, line);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* A child of `<edge>`.
|
|
456
|
+
* @param name - the element name
|
|
457
|
+
* @param local - its local name
|
|
458
|
+
* @param attrs - its attributes
|
|
459
|
+
* @param line - the line
|
|
460
|
+
*/
|
|
461
|
+
startInEdge(name, local, attrs, line) {
|
|
462
|
+
switch (local) {
|
|
463
|
+
case "desc":
|
|
464
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.DESC_DROPPED, "node and edge <desc> texts are not kept", {
|
|
465
|
+
line,
|
|
466
|
+
});
|
|
467
|
+
this.ctx.push(Ctx.Skip);
|
|
468
|
+
return;
|
|
469
|
+
case "data":
|
|
470
|
+
this.beginData(attrs, line, "edge");
|
|
471
|
+
return;
|
|
472
|
+
default:
|
|
473
|
+
this.unknownElement(name, line);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* A child of `<hyperedge>` (under the star / clique policies).
|
|
478
|
+
* @param name - the element name
|
|
479
|
+
* @param local - its local name
|
|
480
|
+
* @param attrs - its attributes
|
|
481
|
+
* @param line - the line
|
|
482
|
+
*/
|
|
483
|
+
startInHyperedge(name, local, attrs, line) {
|
|
484
|
+
switch (local) {
|
|
485
|
+
case "endpoint":
|
|
486
|
+
this.readEndpoint(attrs, line);
|
|
487
|
+
this.ctx.push(Ctx.Skip);
|
|
488
|
+
return;
|
|
489
|
+
case "data":
|
|
490
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.HYPEREDGE_DATA_DROPPED, "hyperedge data are not kept by the star / clique expansions", { line });
|
|
491
|
+
this.ctx.push(Ctx.Skip);
|
|
492
|
+
return;
|
|
493
|
+
case "desc":
|
|
494
|
+
this.ctx.push(Ctx.Skip);
|
|
495
|
+
return;
|
|
496
|
+
default:
|
|
497
|
+
this.unknownElement(name, line);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Report an element the schema does not define at this place and skip its subtree.
|
|
502
|
+
* @param name - the element name
|
|
503
|
+
* @param line - the line
|
|
504
|
+
*/
|
|
505
|
+
unknownElement(name, line) {
|
|
506
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.UNKNOWN_ELEMENT, `element <${name}> is not GraphML and was skipped`, {
|
|
507
|
+
line,
|
|
508
|
+
element: name,
|
|
509
|
+
});
|
|
510
|
+
this.ctx.push(Ctx.Skip);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Open a `<desc>`.
|
|
514
|
+
* @param target - what the text describes
|
|
515
|
+
*/
|
|
516
|
+
beginDesc(target) {
|
|
517
|
+
this.descTarget = target;
|
|
518
|
+
this.descText = "";
|
|
519
|
+
this.ctx.push(Ctx.Desc);
|
|
520
|
+
}
|
|
521
|
+
/** Close a `<desc>`. */
|
|
522
|
+
finishDesc() {
|
|
523
|
+
const text = this.descText.trim();
|
|
524
|
+
if (this.descTarget === "key" && this.pendingKey !== null) {
|
|
525
|
+
this.pendingKey.desc = text;
|
|
526
|
+
}
|
|
527
|
+
else if (this.descTarget === "graph" && text.length > 0) {
|
|
528
|
+
this.description = text;
|
|
529
|
+
}
|
|
530
|
+
this.descTarget = null;
|
|
531
|
+
this.descText = "";
|
|
532
|
+
}
|
|
533
|
+
// ------------------------------------------------------------------ graphs
|
|
534
|
+
/**
|
|
535
|
+
* Open a `<graph>`: the top-level one sets the sink's direction (rule 1 of design section
|
|
536
|
+
* 8.4); a nested one records its container as the parent of its nodes.
|
|
537
|
+
* @param attrs - the graph attributes
|
|
538
|
+
* @param line - the line
|
|
539
|
+
* @param parent - the containing node's index, or INVALID_INDEX
|
|
540
|
+
*/
|
|
541
|
+
beginGraph(attrs, line, parent) {
|
|
542
|
+
const top = this.graphs.length === 0;
|
|
543
|
+
const edgedefault = attrs.get("edgedefault");
|
|
544
|
+
let directed;
|
|
545
|
+
if (edgedefault === "directed" || edgedefault === "undirected") {
|
|
546
|
+
directed = edgedefault === "directed";
|
|
547
|
+
}
|
|
548
|
+
else if (edgedefault === undefined) {
|
|
549
|
+
directed = top ? this.options.defaultDirected : this.graphs[this.graphs.length - 1].directed;
|
|
550
|
+
this.report.warning("validation-error", GRAPHML_ISSUE.EDGEDEFAULT_MISSING, `<graph> has no edgedefault; read as ${directed ? "directed" : "undirected"}`, { line });
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
directed = top ? this.options.defaultDirected : this.graphs[this.graphs.length - 1].directed;
|
|
554
|
+
this.report.error("validation-error", GRAPHML_ISSUE.INVALID_EDGEDEFAULT, `edgedefault "${edgedefault}" is neither directed nor undirected; read as ${directed ? "directed" : "undirected"}`, { line });
|
|
555
|
+
}
|
|
556
|
+
if (top && this.graphSeen) {
|
|
557
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.MULTIPLE_GRAPHS, "the document holds more than one top-level <graph>; their nodes and edges are merged into one graph", { line, element: attrs.get("id") ?? null });
|
|
558
|
+
}
|
|
559
|
+
else if (top) {
|
|
560
|
+
this.graphSeen = true;
|
|
561
|
+
this.graphId = attrs.get("id") ?? null;
|
|
562
|
+
this.edgedefault = directed ? "directed" : "undirected";
|
|
563
|
+
this.direction.setHeader(directed, { line });
|
|
564
|
+
const nodes = this.hint(attrs.get("parse.nodes"), "parse.nodes", line);
|
|
565
|
+
const edges = this.hint(attrs.get("parse.edges"), "parse.edges", line);
|
|
566
|
+
if (nodes !== null || edges !== null) {
|
|
567
|
+
try {
|
|
568
|
+
this.sink.reserve(nodes ?? undefined, edges ?? undefined);
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
if (!(err instanceof GraphFormatError)) {
|
|
572
|
+
throw err;
|
|
573
|
+
}
|
|
574
|
+
this.report.warning("validation-error", GRAPHML_ISSUE.COUNT_HINT, `count hint refused: ${err.message}`, {
|
|
575
|
+
line,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
this.graphs.push({ directed, parent });
|
|
581
|
+
this.ctx.push(Ctx.Graph);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* A `parse.nodes` / `parse.edges` hint the sink can reserve; anything else is reported and ignored.
|
|
585
|
+
* @param text - the attribute text, or undefined
|
|
586
|
+
* @param name - the attribute name
|
|
587
|
+
* @param line - the line
|
|
588
|
+
* @returns the count, or null
|
|
589
|
+
*/
|
|
590
|
+
hint(text, name, line) {
|
|
591
|
+
if (text === undefined) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
const n = hint(text);
|
|
595
|
+
if (n === null || n > MAX_COUNT) {
|
|
596
|
+
this.report.warning("validation-error", GRAPHML_ISSUE.COUNT_HINT, `<graph ${name}="${text}"> is not a count the sink can reserve; ignored`, { line, element: name });
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
return n;
|
|
600
|
+
}
|
|
601
|
+
// ------------------------------------------------------------------ keys
|
|
602
|
+
/**
|
|
603
|
+
* Open a `<key>`; the column is declared at `</key>` once its default is known.
|
|
604
|
+
* @param attrs - the key attributes
|
|
605
|
+
* @param line - the line
|
|
606
|
+
*/
|
|
607
|
+
beginKey(attrs, line) {
|
|
608
|
+
const id = attrs.get("id") ?? null;
|
|
609
|
+
const forText = attrs.get("for") ?? "all";
|
|
610
|
+
let domains;
|
|
611
|
+
switch (forText) {
|
|
612
|
+
case "all":
|
|
613
|
+
domains = ["graph", "node", "edge"];
|
|
614
|
+
break;
|
|
615
|
+
case "graph":
|
|
616
|
+
case "node":
|
|
617
|
+
case "edge":
|
|
618
|
+
domains = [forText];
|
|
619
|
+
break;
|
|
620
|
+
case "hyperedge":
|
|
621
|
+
case "port":
|
|
622
|
+
case "endpoint":
|
|
623
|
+
this.report.warning("unsupported", GRAPHML_ISSUE.KEY_DOMAIN_UNSUPPORTED, `key "${id ?? ""}" is declared for ${forText} elements, which are not kept`, { line, element: id });
|
|
624
|
+
domains = [];
|
|
625
|
+
break;
|
|
626
|
+
default:
|
|
627
|
+
this.report.error("validation-error", GRAPHML_ISSUE.KEY_FOR_INVALID, `key "${id ?? ""}" has an unknown for="${forText}"`, { line, element: id });
|
|
628
|
+
domains = [];
|
|
629
|
+
}
|
|
630
|
+
this.pendingKey = {
|
|
631
|
+
id,
|
|
632
|
+
attrName: attrs.get("attr.name") ?? null,
|
|
633
|
+
attrType: attrs.get("attr.type") ?? null,
|
|
634
|
+
yfilesType: attrs.get("yfiles.type") ?? null,
|
|
635
|
+
domains,
|
|
636
|
+
line,
|
|
637
|
+
desc: "",
|
|
638
|
+
defaultText: null,
|
|
639
|
+
defaultTree: undefined,
|
|
640
|
+
};
|
|
641
|
+
this.ctx.push(Ctx.Key);
|
|
642
|
+
}
|
|
643
|
+
/** Close a `<key>`: declare its columns. */
|
|
644
|
+
finishKey() {
|
|
645
|
+
const key = this.pendingKey;
|
|
646
|
+
this.pendingKey = null;
|
|
647
|
+
if (key === null) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const where = { line: key.line, element: key.id };
|
|
651
|
+
if (key.id === null || key.id.length === 0) {
|
|
652
|
+
this.report.error("validation-error", GRAPHML_ISSUE.KEY_MISSING_ID, "<key> without an id", where);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (this.keys.has(key.id)) {
|
|
656
|
+
this.report.error("validation-error", GRAPHML_ISSUE.DUPLICATE_KEY, `key "${key.id}" is declared twice`, where);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const yfiles = key.yfilesType !== null;
|
|
660
|
+
const name = key.attrName !== null && key.attrName.length > 0 ? key.attrName : key.id;
|
|
661
|
+
const skipped = yfiles && this.yfilesMode === "skip";
|
|
662
|
+
if (skipped) {
|
|
663
|
+
this.report.warning("unsupported", GRAPHML_ISSUE.YFILES_SKIPPED, `yFiles key "${key.id}" (${key.yfilesType ?? ""}) skipped per the yfiles option`, where);
|
|
664
|
+
}
|
|
665
|
+
let weight = false;
|
|
666
|
+
let originalId = false;
|
|
667
|
+
let node = null;
|
|
668
|
+
let edge = null;
|
|
669
|
+
let graph = null;
|
|
670
|
+
for (const domain of key.domains) {
|
|
671
|
+
if (skipped) {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (domain === "edge" && !yfiles && isWeightField(name, this.options.weightFrom)) {
|
|
675
|
+
weight = true;
|
|
676
|
+
this.weightOrigin ?? (this.weightOrigin = { id: key.id, title: key.attrName, type: key.attrType });
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (domain === "node" && !yfiles && name === ORIGINAL_ID_ATTRIBUTE) {
|
|
680
|
+
if (this.options.restoreMangledIds) {
|
|
681
|
+
originalId = true;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
node = this.declareOriginalIdColumn(key);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
const target = yfiles ? this.declareYfiles(key, name, domain) : this.declareTyped(key, name, domain);
|
|
689
|
+
if (domain === "node") {
|
|
690
|
+
node = target;
|
|
691
|
+
}
|
|
692
|
+
else if (domain === "edge") {
|
|
693
|
+
edge = target;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
graph = target;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch (err) {
|
|
700
|
+
this.report.recordError(err, where);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
this.keys.set(key.id, {
|
|
704
|
+
id: key.id,
|
|
705
|
+
name,
|
|
706
|
+
node,
|
|
707
|
+
edge,
|
|
708
|
+
graph,
|
|
709
|
+
weight,
|
|
710
|
+
originalId,
|
|
711
|
+
yfiles,
|
|
712
|
+
skipped,
|
|
713
|
+
domains: key.domains,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Declare the column of a typed (or untyped, then string) key in one domain.
|
|
718
|
+
* @param key - the key
|
|
719
|
+
* @param name - the resolved attribute name
|
|
720
|
+
* @param domain - the domain
|
|
721
|
+
* @returns the column target
|
|
722
|
+
*/
|
|
723
|
+
declareTyped(key, name, domain) {
|
|
724
|
+
const id = key.id ?? "";
|
|
725
|
+
const declared = declareAttribute({
|
|
726
|
+
format: FORMAT,
|
|
727
|
+
id,
|
|
728
|
+
title: key.attrName !== null && key.attrName.length > 0 ? key.attrName : null,
|
|
729
|
+
type: key.attrType,
|
|
730
|
+
defaultText: key.defaultTree === undefined ? key.defaultText : null,
|
|
731
|
+
listSyntax: "comma",
|
|
732
|
+
role: domain !== "graph" && name === LABEL_COLUMN ? "label" : null,
|
|
733
|
+
long: this.options.long,
|
|
734
|
+
taken: this.takenIn(domain),
|
|
735
|
+
});
|
|
736
|
+
for (const issue of declared.issues) {
|
|
737
|
+
this.report.warning(issue.category, issue.code, issue.message, { line: key.line, element: id });
|
|
738
|
+
}
|
|
739
|
+
if (key.defaultTree !== undefined) {
|
|
740
|
+
this.report.warning("validation-error", GRAPHML_ISSUE.DATA_NESTED, `the <default> of key "${id}" holds elements; a ${key.attrType ?? "string"} default must be text`, { line: key.line, element: id });
|
|
741
|
+
}
|
|
742
|
+
const { decl } = declared;
|
|
743
|
+
if (key.desc.length > 0) {
|
|
744
|
+
decl.extra = { desc: key.desc };
|
|
745
|
+
}
|
|
746
|
+
return this.declareTarget(domain, decl, declared.spec, declared.listSyntax);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Declare the json column of a yFiles key in one domain.
|
|
750
|
+
* @param key - the key
|
|
751
|
+
* @param name - the resolved attribute name
|
|
752
|
+
* @param domain - the domain
|
|
753
|
+
* @returns the column target
|
|
754
|
+
*/
|
|
755
|
+
declareYfiles(key, name, domain) {
|
|
756
|
+
const id = key.id ?? "";
|
|
757
|
+
const columnName = uniqueColumnName(name, id, this.takenIn(domain));
|
|
758
|
+
const decl = {
|
|
759
|
+
name: columnName,
|
|
760
|
+
dtype: "json",
|
|
761
|
+
nullable: true,
|
|
762
|
+
origin: {
|
|
763
|
+
format: FORMAT,
|
|
764
|
+
id,
|
|
765
|
+
title: key.attrName !== null && key.attrName !== columnName ? key.attrName : null,
|
|
766
|
+
type: key.yfilesType,
|
|
767
|
+
namespace: "yfiles",
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
if (key.defaultTree !== undefined) {
|
|
771
|
+
decl.default = key.defaultTree;
|
|
772
|
+
}
|
|
773
|
+
else if (key.defaultText !== null) {
|
|
774
|
+
decl.default = key.defaultText;
|
|
775
|
+
}
|
|
776
|
+
if (key.desc.length > 0) {
|
|
777
|
+
decl.extra = { desc: key.desc };
|
|
778
|
+
}
|
|
779
|
+
return this.declareTarget(domain, decl, null, "comma");
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Declare the `graphty.originalId` column that keeps the exporter's original ids when
|
|
783
|
+
* restoreMangledIds is off.
|
|
784
|
+
* @param key - the key
|
|
785
|
+
* @returns the column target
|
|
786
|
+
*/
|
|
787
|
+
declareOriginalIdColumn(key) {
|
|
788
|
+
const decl = {
|
|
789
|
+
name: uniqueColumnName(ORIGINAL_ID_COLUMN, key.id, this.takenIn("node")),
|
|
790
|
+
dtype: "string",
|
|
791
|
+
nullable: true,
|
|
792
|
+
role: "originalId",
|
|
793
|
+
origin: { format: FORMAT, id: key.id, title: key.attrName, type: key.attrType, namespace: null },
|
|
794
|
+
};
|
|
795
|
+
return this.declareTarget("node", decl, null, "comma");
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Push a declaration into the sink for one domain through the shared design section 5.6 rule:
|
|
799
|
+
* a role another column already holds is dropped (reported), a name the sink holds with
|
|
800
|
+
* another shape is renamed `<name>#<id>` (reported).
|
|
801
|
+
* @param domain - the domain
|
|
802
|
+
* @param decl - the declaration
|
|
803
|
+
* @param spec - the value spec, or null for json
|
|
804
|
+
* @param listSyntax - the list syntax
|
|
805
|
+
* @returns the target
|
|
806
|
+
*/
|
|
807
|
+
declareTarget(domain, decl, spec, listSyntax) {
|
|
808
|
+
if (domain === "graph") {
|
|
809
|
+
this.sink.setGraphValue(decl.name, undefined, decl);
|
|
810
|
+
this.graphColumnNames.add(decl.name);
|
|
811
|
+
return { name: decl.name, handle: INVALID_INDEX, decl, spec, listSyntax };
|
|
812
|
+
}
|
|
813
|
+
const resolved = declareResolved(this.sink, domain, decl, this.report, { element: decl.name });
|
|
814
|
+
return { name: resolved.decl.name, handle: resolved.handle, decl: resolved.decl, spec, listSyntax };
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* The `taken` predicate of a domain: the reserved XML-derived names plus the sink's columns.
|
|
818
|
+
* @param domain - the domain
|
|
819
|
+
* @returns the predicate
|
|
820
|
+
*/
|
|
821
|
+
takenIn(domain) {
|
|
822
|
+
if (domain === "graph") {
|
|
823
|
+
return (name) => this.graphColumnNames.has(name);
|
|
824
|
+
}
|
|
825
|
+
const reserved = domain === "node" ? RESERVED_NODE_NAMES : RESERVED_EDGE_NAMES;
|
|
826
|
+
const inSink = takenIn(this.sink, domain);
|
|
827
|
+
return (name) => reserved.has(name) || inSink(name);
|
|
828
|
+
}
|
|
829
|
+
// ------------------------------------------------------------------ data and defaults
|
|
830
|
+
/**
|
|
831
|
+
* Open a `<data>`.
|
|
832
|
+
* @param attrs - its attributes
|
|
833
|
+
* @param line - the line
|
|
834
|
+
* @param domain - the domain of the element holding it
|
|
835
|
+
*/
|
|
836
|
+
beginData(attrs, line, domain) {
|
|
837
|
+
const keyId = attrs.get("key");
|
|
838
|
+
const { content } = this;
|
|
839
|
+
content.key = null;
|
|
840
|
+
content.pendingKey = null;
|
|
841
|
+
content.domain = domain;
|
|
842
|
+
content.text = "";
|
|
843
|
+
content.tree = null;
|
|
844
|
+
content.line = line;
|
|
845
|
+
if (keyId === undefined) {
|
|
846
|
+
this.report.error("validation-error", GRAPHML_ISSUE.DATA_MISSING_KEY, "<data> without a key attribute", {
|
|
847
|
+
line,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
const key = this.keys.get(keyId);
|
|
852
|
+
if (key === undefined) {
|
|
853
|
+
this.report.error("validation-error", GRAPHML_ISSUE.UNKNOWN_KEY, `<data> references undeclared key "${keyId}"`, { line, element: keyId });
|
|
854
|
+
}
|
|
855
|
+
else if (!key.skipped && !key.domains.includes(domain)) {
|
|
856
|
+
this.report.warnOnce("validation-error", GRAPHML_ISSUE.KEY_DOMAIN, `key "${keyId}" is declared for ${key.domains.join(" / ")} elements but used on a ${domain}; ignored`, { line, element: keyId });
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
content.key = key;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
this.ctx.push(Ctx.Data);
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* A child element inside `<data>` / `<default>`: the content becomes a tree.
|
|
866
|
+
* @param name - the element name
|
|
867
|
+
* @param attrs - its attributes
|
|
868
|
+
*/
|
|
869
|
+
captureStart(name, attrs) {
|
|
870
|
+
const { content } = this;
|
|
871
|
+
if (content.tree === null) {
|
|
872
|
+
content.tree = new XmlTreeBuilder();
|
|
873
|
+
if (content.text.length > 0) {
|
|
874
|
+
content.tree.text(content.text);
|
|
875
|
+
content.text = "";
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
content.tree.start(name, attrs);
|
|
879
|
+
this.ctx.push(Ctx.Capture);
|
|
880
|
+
}
|
|
881
|
+
/** A captured element ends. */
|
|
882
|
+
captureEnd() {
|
|
883
|
+
this.content.tree?.end();
|
|
884
|
+
}
|
|
885
|
+
/** Close a `<default>`: keep its text or tree for the key. */
|
|
886
|
+
finishDefault() {
|
|
887
|
+
const { content } = this;
|
|
888
|
+
const key = content.pendingKey;
|
|
889
|
+
if (key === null) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (content.tree !== null) {
|
|
893
|
+
key.defaultTree = content.tree.finish();
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
key.defaultText = content.text;
|
|
897
|
+
}
|
|
898
|
+
content.tree = null;
|
|
899
|
+
content.text = "";
|
|
900
|
+
content.pendingKey = null;
|
|
901
|
+
}
|
|
902
|
+
/** Close a `<data>`: parse the value and route it to its element. */
|
|
903
|
+
finishData() {
|
|
904
|
+
const { content } = this;
|
|
905
|
+
const { key } = content;
|
|
906
|
+
const value = content.tree === null ? content.text : content.tree.finish();
|
|
907
|
+
content.tree = null;
|
|
908
|
+
content.text = "";
|
|
909
|
+
content.key = null;
|
|
910
|
+
if (key === null || key.skipped) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const where = { line: content.line, element: key.id };
|
|
914
|
+
switch (content.domain) {
|
|
915
|
+
case "node": {
|
|
916
|
+
const node = this.nodes[this.nodes.length - 1];
|
|
917
|
+
if (key.originalId) {
|
|
918
|
+
if (typeof value === "string") {
|
|
919
|
+
node.restoredId = value;
|
|
920
|
+
}
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (key.node === null) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const parsed = this.parseValue(key.node, value, where);
|
|
927
|
+
if (parsed === undefined) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (node.index === INVALID_INDEX) {
|
|
931
|
+
node.pending.push(key.node.handle, parsed);
|
|
932
|
+
}
|
|
933
|
+
else if (!node.failed) {
|
|
934
|
+
this.sink.setNodeValue(key.node.handle, node.index, parsed);
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
case "edge": {
|
|
939
|
+
const { edge } = this;
|
|
940
|
+
if (edge === null) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (key.weight) {
|
|
944
|
+
if (typeof value !== "string") {
|
|
945
|
+
this.report.error("validation-error", GRAPHML_ISSUE.DATA_NESTED, "a weight value must be text", where);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
try {
|
|
949
|
+
edge.weight = parseWeightText(value);
|
|
950
|
+
}
|
|
951
|
+
catch (err) {
|
|
952
|
+
this.skipEdge(edge);
|
|
953
|
+
this.report.recordError(err, where);
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (key.edge === null) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const parsed = this.parseValue(key.edge, value, where);
|
|
961
|
+
if (parsed !== undefined) {
|
|
962
|
+
edge.pending.push(key.edge.handle, parsed);
|
|
963
|
+
}
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
case "graph": {
|
|
967
|
+
if (key.graph === null) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const parsed = this.parseValue(key.graph, value, where);
|
|
971
|
+
if (parsed !== undefined) {
|
|
972
|
+
try {
|
|
973
|
+
this.sink.setGraphValue(key.graph.name, parsed, key.graph.decl);
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
this.report.recordError(err, where);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
default: {
|
|
982
|
+
const { domain } = content;
|
|
983
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown data domain ${String(domain)}`, {});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Parse the content of a `<data>` by its column's declaration.
|
|
989
|
+
* @param target - the column
|
|
990
|
+
* @param value - the text, or the tree of a nested subtree
|
|
991
|
+
* @param where - the location for issues
|
|
992
|
+
* @returns the value to write, or undefined when there is none (blank, or an error was recorded)
|
|
993
|
+
*/
|
|
994
|
+
parseValue(target, value, where) {
|
|
995
|
+
if (target.spec === null) {
|
|
996
|
+
if (!this.yfilesLossNoted) {
|
|
997
|
+
this.yfilesLossNoted = true;
|
|
998
|
+
this.report.loss(GRAPHML_LOSS.YFILES_JSON, "yFiles nested XML is kept as a JSON tree: structure preserved, not byte-exact", target.name);
|
|
999
|
+
}
|
|
1000
|
+
return value;
|
|
1001
|
+
}
|
|
1002
|
+
if (typeof value !== "string") {
|
|
1003
|
+
this.report.error("validation-error", GRAPHML_ISSUE.DATA_NESTED, `<data> of the ${target.spec.declared} attribute "${target.name}" holds elements, not text`, where);
|
|
1004
|
+
return undefined;
|
|
1005
|
+
}
|
|
1006
|
+
const { spec } = target;
|
|
1007
|
+
if (spec.kind !== "string" && isWhitespace(value)) {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
try {
|
|
1011
|
+
const parsed = parseDeclaredValue(value, spec, target.listSyntax);
|
|
1012
|
+
if (spec.precision && losesPrecision(spec, value)) {
|
|
1013
|
+
this.report.warnOnce("precision", PRECISION_CODE, `long values of "${target.name}" beyond 2^53 are stored as the nearest double`, { line: where.line, element: target.name });
|
|
1014
|
+
}
|
|
1015
|
+
return parsed;
|
|
1016
|
+
}
|
|
1017
|
+
catch (err) {
|
|
1018
|
+
this.report.recordError(err, where);
|
|
1019
|
+
return undefined;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// ------------------------------------------------------------------ nodes
|
|
1023
|
+
/**
|
|
1024
|
+
* Open a `<node>`; it is added to the sink when its data is complete (or a nested graph
|
|
1025
|
+
* starts), so a `graphty:originalId` value can replace its id.
|
|
1026
|
+
* @param attrs - the node attributes
|
|
1027
|
+
* @param line - the line
|
|
1028
|
+
*/
|
|
1029
|
+
beginNode(attrs, line) {
|
|
1030
|
+
const graph = this.graphs[this.graphs.length - 1];
|
|
1031
|
+
const state = {
|
|
1032
|
+
id: null,
|
|
1033
|
+
idText: null,
|
|
1034
|
+
index: INVALID_INDEX,
|
|
1035
|
+
parent: graph.parent,
|
|
1036
|
+
pending: [],
|
|
1037
|
+
restoredId: null,
|
|
1038
|
+
failed: false,
|
|
1039
|
+
line,
|
|
1040
|
+
};
|
|
1041
|
+
const idText = attrs.get("id");
|
|
1042
|
+
if (idText === undefined || idText.length === 0) {
|
|
1043
|
+
this.skipNode(state);
|
|
1044
|
+
this.report.error("missing-value", GRAPHML_ISSUE.MISSING_ID, "<node> without an id", { line });
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
try {
|
|
1048
|
+
state.id = this.coercer.text(idText);
|
|
1049
|
+
state.idText = idText;
|
|
1050
|
+
this.reportMerge(idText, line);
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
this.skipNode(state);
|
|
1054
|
+
this.report.recordError(err, { line, element: idText });
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
this.nodes.push(state);
|
|
1058
|
+
this.ctx.push(Ctx.Node);
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Add a node to the sink and flush its pending values.
|
|
1062
|
+
* @param node - the node state
|
|
1063
|
+
*/
|
|
1064
|
+
materializeNode(node) {
|
|
1065
|
+
if (node.index !== INVALID_INDEX || node.failed) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const id = node.restoredId ?? node.id;
|
|
1069
|
+
if (id === null) {
|
|
1070
|
+
node.failed = true;
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (node.restoredId !== null && node.idText !== null) {
|
|
1074
|
+
this.restored.set(node.idText, node.restoredId);
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
const existing = this.sink.indexOf(id);
|
|
1078
|
+
const index = this.sink.addNode(id);
|
|
1079
|
+
if (existing !== INVALID_INDEX && this.declared.has(index)) {
|
|
1080
|
+
this.report.warning("validation-error", GRAPHML_ISSUE.DUPLICATE_NODE, `node "${String(id)}" is declared twice; the declarations are merged`, { line: node.line, element: String(id) });
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
this.report.counts.nodes++;
|
|
1084
|
+
}
|
|
1085
|
+
this.declared.add(index);
|
|
1086
|
+
node.index = index;
|
|
1087
|
+
this.checkAbort();
|
|
1088
|
+
if (node.parent !== INVALID_INDEX) {
|
|
1089
|
+
this.sink.setNodeValue(this.parentColumn(), index, node.parent);
|
|
1090
|
+
}
|
|
1091
|
+
const { pending } = node;
|
|
1092
|
+
for (let i = 0; i < pending.length; i += 2) {
|
|
1093
|
+
this.sink.setNodeValue(pending[i], index, pending[i + 1]);
|
|
1094
|
+
}
|
|
1095
|
+
pending.length = 0;
|
|
1096
|
+
}
|
|
1097
|
+
catch (err) {
|
|
1098
|
+
this.skipNode(node);
|
|
1099
|
+
this.report.recordError(err, { line: node.line, element: String(id) });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Mark a node element skipped (once) before its error is recorded, so the report's counts
|
|
1104
|
+
* include it even when that error is the one that exceeds the limit.
|
|
1105
|
+
* @param node - the node state
|
|
1106
|
+
*/
|
|
1107
|
+
skipNode(node) {
|
|
1108
|
+
if (!node.failed) {
|
|
1109
|
+
node.failed = true;
|
|
1110
|
+
this.report.counts.skippedNodes++;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/** Close a `<node>`. */
|
|
1114
|
+
finishNode() {
|
|
1115
|
+
const node = this.nodes.pop();
|
|
1116
|
+
if (node !== undefined) {
|
|
1117
|
+
this.materializeNode(node);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* The `parent` column, declared on first use.
|
|
1122
|
+
* @returns the handle
|
|
1123
|
+
*/
|
|
1124
|
+
parentColumn() {
|
|
1125
|
+
if (this.parentHandle === INVALID_INDEX) {
|
|
1126
|
+
this.parentHandle = this.sink.declareNodeColumn({
|
|
1127
|
+
name: PARENT_COLUMN,
|
|
1128
|
+
dtype: "u32",
|
|
1129
|
+
role: "parent",
|
|
1130
|
+
refersTo: "node",
|
|
1131
|
+
nullable: true,
|
|
1132
|
+
origin: { format: FORMAT, id: null, title: null, type: "graph", namespace: null },
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
return this.parentHandle;
|
|
1136
|
+
}
|
|
1137
|
+
// ------------------------------------------------------------------ edges
|
|
1138
|
+
/**
|
|
1139
|
+
* Open an `<edge>`; it is pushed at `</edge>` once its weight and data are known.
|
|
1140
|
+
* @param attrs - the edge attributes
|
|
1141
|
+
* @param line - the line
|
|
1142
|
+
*/
|
|
1143
|
+
beginEdge(attrs, line) {
|
|
1144
|
+
const graph = this.graphs[this.graphs.length - 1];
|
|
1145
|
+
const edge = {
|
|
1146
|
+
source: null,
|
|
1147
|
+
target: null,
|
|
1148
|
+
id: attrs.get("id") ?? null,
|
|
1149
|
+
kind: graph.directed ? "directed" : "undirected",
|
|
1150
|
+
sourcePort: attrs.get("sourceport") ?? null,
|
|
1151
|
+
targetPort: attrs.get("targetport") ?? null,
|
|
1152
|
+
weight: undefined,
|
|
1153
|
+
pending: [],
|
|
1154
|
+
failed: false,
|
|
1155
|
+
line,
|
|
1156
|
+
};
|
|
1157
|
+
const where = { line, element: edge.id };
|
|
1158
|
+
const sourceText = attrs.get("source");
|
|
1159
|
+
const targetText = attrs.get("target");
|
|
1160
|
+
if (sourceText === undefined || sourceText.length === 0) {
|
|
1161
|
+
this.skipEdge(edge);
|
|
1162
|
+
this.report.error("missing-value", GRAPHML_ISSUE.MISSING_ENDPOINT, "<edge> without a source", where);
|
|
1163
|
+
}
|
|
1164
|
+
if (targetText === undefined || targetText.length === 0) {
|
|
1165
|
+
this.skipEdge(edge);
|
|
1166
|
+
this.report.error("missing-value", GRAPHML_ISSUE.MISSING_ENDPOINT, "<edge> without a target", where);
|
|
1167
|
+
}
|
|
1168
|
+
if (!edge.failed && sourceText !== undefined && targetText !== undefined) {
|
|
1169
|
+
try {
|
|
1170
|
+
edge.source = this.endpointId(sourceText, line);
|
|
1171
|
+
edge.target = this.endpointId(targetText, line);
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
this.skipEdge(edge);
|
|
1175
|
+
this.report.recordError(err, where);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const directedText = attrs.get("directed");
|
|
1179
|
+
if (directedText !== undefined) {
|
|
1180
|
+
if (directedText === "true") {
|
|
1181
|
+
edge.kind = "directed";
|
|
1182
|
+
}
|
|
1183
|
+
else if (directedText === "false") {
|
|
1184
|
+
edge.kind = "undirected";
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
this.skipEdge(edge);
|
|
1188
|
+
this.report.error("validation-error", GRAPHML_ISSUE.INVALID_DIRECTED, `directed="${directedText}" is neither true nor false`, where);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
this.edge = edge;
|
|
1192
|
+
this.ctx.push(Ctx.Edge);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Close an `<edge>`: push it through the direction resolver and write its values.
|
|
1196
|
+
*/
|
|
1197
|
+
finishEdge() {
|
|
1198
|
+
const { edge } = this;
|
|
1199
|
+
this.edge = null;
|
|
1200
|
+
if (edge === null) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (edge.failed || edge.source === null || edge.target === null) {
|
|
1204
|
+
this.skipEdge(edge);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const element = edge.id ?? `${String(edge.source)} -> ${String(edge.target)}`;
|
|
1208
|
+
const where = { line: edge.line, element };
|
|
1209
|
+
try {
|
|
1210
|
+
if (!this.options.addMissingNodes) {
|
|
1211
|
+
this.requireNode(edge.source);
|
|
1212
|
+
this.requireNode(edge.target);
|
|
1213
|
+
}
|
|
1214
|
+
if (edge.id !== null && this.edgeIds.has(edge.id)) {
|
|
1215
|
+
throw new GraphFormatError("E_DUPLICATE_EDGE_ID", `edge id "${edge.id}" is used twice`, {
|
|
1216
|
+
id: edge.id,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
const before = this.sink.edgeCount;
|
|
1220
|
+
this.countEndpoints(edge.source, edge.target);
|
|
1221
|
+
const e = this.direction.addEdge(edge.source, edge.target, edge.kind, edge.weight, where);
|
|
1222
|
+
this.report.counts.edges += this.sink.edgeCount - before;
|
|
1223
|
+
this.checkAbort();
|
|
1224
|
+
if (edge.id !== null) {
|
|
1225
|
+
this.edgeIds.add(edge.id);
|
|
1226
|
+
this.sink.setEdgeValue(this.edgeIdColumn(), e, edge.id);
|
|
1227
|
+
}
|
|
1228
|
+
if (edge.sourcePort !== null) {
|
|
1229
|
+
this.sink.setEdgeValue(this.portColumn("source"), e, edge.sourcePort);
|
|
1230
|
+
}
|
|
1231
|
+
if (edge.targetPort !== null) {
|
|
1232
|
+
this.sink.setEdgeValue(this.portColumn("target"), e, edge.targetPort);
|
|
1233
|
+
}
|
|
1234
|
+
const { pending } = edge;
|
|
1235
|
+
for (let i = 0; i < pending.length; i += 2) {
|
|
1236
|
+
this.sink.setEdgeValue(pending[i], e, pending[i + 1]);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
catch (err) {
|
|
1240
|
+
this.skipEdge(edge);
|
|
1241
|
+
this.report.recordError(err, where);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Mark an edge (or hyperedge) element skipped once, before its error is recorded.
|
|
1246
|
+
* @param edge - the edge state
|
|
1247
|
+
*/
|
|
1248
|
+
skipEdge(edge) {
|
|
1249
|
+
if (!edge.failed) {
|
|
1250
|
+
edge.failed = true;
|
|
1251
|
+
this.report.counts.skippedEdges++;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* E_UNKNOWN_NODE when an endpoint was never declared (addMissingNodes false enforced by the
|
|
1256
|
+
* importer, whatever the sink allows).
|
|
1257
|
+
* @param id - the endpoint id
|
|
1258
|
+
*/
|
|
1259
|
+
requireNode(id) {
|
|
1260
|
+
if (this.sink.indexOf(id) === INVALID_INDEX) {
|
|
1261
|
+
throw new GraphFormatError("E_UNKNOWN_NODE", `edge endpoint "${String(id)}" is not a declared node`, {
|
|
1262
|
+
id,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* The edge `id` column (role id, unique), declared on first use.
|
|
1268
|
+
* @returns the handle
|
|
1269
|
+
*/
|
|
1270
|
+
edgeIdColumn() {
|
|
1271
|
+
if (this.edgeIdHandle === INVALID_INDEX) {
|
|
1272
|
+
this.edgeIdHandle = this.sink.declareEdgeColumn({
|
|
1273
|
+
name: EDGE_ID_COLUMN,
|
|
1274
|
+
dtype: "string",
|
|
1275
|
+
role: "id",
|
|
1276
|
+
unique: true,
|
|
1277
|
+
nullable: true,
|
|
1278
|
+
origin: { format: FORMAT, id: null, title: null, type: "id", namespace: null },
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
return this.edgeIdHandle;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* The sourceport / targetport column, declared on first use.
|
|
1285
|
+
* @param side - which end
|
|
1286
|
+
* @returns the handle
|
|
1287
|
+
*/
|
|
1288
|
+
portColumn(side) {
|
|
1289
|
+
if (side === "source") {
|
|
1290
|
+
if (this.sourcePortHandle === INVALID_INDEX) {
|
|
1291
|
+
this.sourcePortHandle = this.sink.declareEdgeColumn({
|
|
1292
|
+
name: SOURCE_PORT_COLUMN,
|
|
1293
|
+
dtype: "string",
|
|
1294
|
+
role: "sourcePort",
|
|
1295
|
+
nullable: true,
|
|
1296
|
+
origin: { format: FORMAT, id: null, title: null, type: "sourceport", namespace: null },
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
return this.sourcePortHandle;
|
|
1300
|
+
}
|
|
1301
|
+
if (this.targetPortHandle === INVALID_INDEX) {
|
|
1302
|
+
this.targetPortHandle = this.sink.declareEdgeColumn({
|
|
1303
|
+
name: TARGET_PORT_COLUMN,
|
|
1304
|
+
dtype: "string",
|
|
1305
|
+
role: "targetPort",
|
|
1306
|
+
nullable: true,
|
|
1307
|
+
origin: { format: FORMAT, id: null, title: null, type: "targetport", namespace: null },
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
return this.targetPortHandle;
|
|
1311
|
+
}
|
|
1312
|
+
// ------------------------------------------------------------------ hyperedges
|
|
1313
|
+
/**
|
|
1314
|
+
* Open a `<hyperedge>` per the hyperedges option.
|
|
1315
|
+
* @param attrs - its attributes
|
|
1316
|
+
* @param line - the line
|
|
1317
|
+
*/
|
|
1318
|
+
beginHyperedge(attrs, line) {
|
|
1319
|
+
const id = attrs.get("id") ?? null;
|
|
1320
|
+
const where = { line, element: id };
|
|
1321
|
+
switch (this.options.hyperedges) {
|
|
1322
|
+
case "error":
|
|
1323
|
+
this.report.error("unsupported", GRAPHML_ISSUE.HYPEREDGE, 'hyperedge refused (hyperedges: "error")', where);
|
|
1324
|
+
throw this.report.abort("hyperedge refused", { code: GRAPHML_ISSUE.HYPEREDGE });
|
|
1325
|
+
case "skip":
|
|
1326
|
+
this.report.warnOnce("unsupported", GRAPHML_ISSUE.HYPEREDGE_SKIPPED, 'hyperedges are skipped (hyperedges: "skip"); use "star" or "clique" to expand them', where);
|
|
1327
|
+
this.report.counts.skippedEdges++;
|
|
1328
|
+
this.ctx.push(Ctx.Skip);
|
|
1329
|
+
return;
|
|
1330
|
+
case "star":
|
|
1331
|
+
case "clique":
|
|
1332
|
+
this.hyperedge = { id, endpoints: [], failed: false, line };
|
|
1333
|
+
this.ctx.push(Ctx.Hyperedge);
|
|
1334
|
+
return;
|
|
1335
|
+
default: {
|
|
1336
|
+
const policy = this.options.hyperedges;
|
|
1337
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown hyperedges policy ${String(policy)}`, {
|
|
1338
|
+
option: "hyperedges",
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* An `<endpoint>` of the open hyperedge.
|
|
1345
|
+
* @param attrs - its attributes
|
|
1346
|
+
* @param line - the line
|
|
1347
|
+
*/
|
|
1348
|
+
readEndpoint(attrs, line) {
|
|
1349
|
+
const { hyperedge } = this;
|
|
1350
|
+
if (hyperedge === null) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const where = { line, element: hyperedge.id };
|
|
1354
|
+
const nodeText = attrs.get("node");
|
|
1355
|
+
const typeText = attrs.get("type") ?? "undir";
|
|
1356
|
+
if (nodeText === undefined || nodeText.length === 0) {
|
|
1357
|
+
this.skipEdge(hyperedge);
|
|
1358
|
+
this.report.error("missing-value", GRAPHML_ISSUE.HYPEREDGE_ENDPOINT, "<endpoint> without a node", where);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (typeText !== "in" && typeText !== "out" && typeText !== "undir") {
|
|
1362
|
+
this.skipEdge(hyperedge);
|
|
1363
|
+
this.report.error("validation-error", GRAPHML_ISSUE.HYPEREDGE_ENDPOINT, `endpoint type "${typeText}" is not in, out or undir`, where);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
try {
|
|
1367
|
+
const node = this.endpointId(nodeText, line);
|
|
1368
|
+
hyperedge.endpoints.push({ node, type: typeText });
|
|
1369
|
+
}
|
|
1370
|
+
catch (err) {
|
|
1371
|
+
this.skipEdge(hyperedge);
|
|
1372
|
+
this.report.recordError(err, where);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/** Close a `<hyperedge>`: expand it to a star or a clique. */
|
|
1376
|
+
finishHyperedge() {
|
|
1377
|
+
const { hyperedge } = this;
|
|
1378
|
+
this.hyperedge = null;
|
|
1379
|
+
if (hyperedge === null) {
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (hyperedge.failed) {
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
const where = { line: hyperedge.line, element: hyperedge.id };
|
|
1386
|
+
try {
|
|
1387
|
+
if (this.options.hyperedges === "star") {
|
|
1388
|
+
this.expandStar(hyperedge, where);
|
|
1389
|
+
}
|
|
1390
|
+
else {
|
|
1391
|
+
this.expandClique(hyperedge, where);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
catch (err) {
|
|
1395
|
+
this.skipEdge(hyperedge);
|
|
1396
|
+
this.report.recordError(err, where);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Star expansion: a synthetic hub node (marked in the `graphty.hyperedge` column) joined to
|
|
1401
|
+
* every endpoint; `in` endpoints point at the hub, `out` endpoints are pointed at by it,
|
|
1402
|
+
* `undir` endpoints are undirected.
|
|
1403
|
+
* @param hyperedge - the hyperedge
|
|
1404
|
+
* @param where - the location for issues
|
|
1405
|
+
*/
|
|
1406
|
+
expandStar(hyperedge, where) {
|
|
1407
|
+
this.hyperedgeCount++;
|
|
1408
|
+
let hubId = hyperedge.id ?? `hyperedge${this.hyperedgeCount}`;
|
|
1409
|
+
for (let n = 2; this.sink.indexOf(hubId) !== INVALID_INDEX; n++) {
|
|
1410
|
+
hubId = `${hyperedge.id ?? `hyperedge${this.hyperedgeCount}`}#${n}`;
|
|
1411
|
+
}
|
|
1412
|
+
const hub = this.sink.addNode(hubId);
|
|
1413
|
+
this.declared.add(hub);
|
|
1414
|
+
this.report.counts.nodes++;
|
|
1415
|
+
if (this.hubHandle === INVALID_INDEX) {
|
|
1416
|
+
this.hubHandle = this.sink.declareNodeColumn({
|
|
1417
|
+
name: HYPEREDGE_HUB_COLUMN,
|
|
1418
|
+
dtype: "bool",
|
|
1419
|
+
nullable: true,
|
|
1420
|
+
origin: { format: FORMAT, id: null, title: null, type: "hyperedge", namespace: null },
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
this.sink.setNodeValue(this.hubHandle, hub, true);
|
|
1424
|
+
for (const endpoint of hyperedge.endpoints) {
|
|
1425
|
+
const before = this.sink.edgeCount;
|
|
1426
|
+
switch (endpoint.type) {
|
|
1427
|
+
case "in":
|
|
1428
|
+
this.direction.addEdge(endpoint.node, hubId, "directed", undefined, where);
|
|
1429
|
+
break;
|
|
1430
|
+
case "out":
|
|
1431
|
+
this.direction.addEdge(hubId, endpoint.node, "directed", undefined, where);
|
|
1432
|
+
break;
|
|
1433
|
+
case "undir":
|
|
1434
|
+
this.direction.addEdge(endpoint.node, hubId, "undirected", undefined, where);
|
|
1435
|
+
break;
|
|
1436
|
+
default: {
|
|
1437
|
+
const { type } = endpoint;
|
|
1438
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown endpoint type ${String(type)}`, {});
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
this.report.counts.edges += this.sink.edgeCount - before;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Clique expansion: every `in` endpoint points at every `out` endpoint; `undir` endpoints are
|
|
1446
|
+
* joined undirected to every other endpoint; two endpoints of the same direction are not joined.
|
|
1447
|
+
* @param hyperedge - the hyperedge
|
|
1448
|
+
* @param where - the location for issues
|
|
1449
|
+
*/
|
|
1450
|
+
expandClique(hyperedge, where) {
|
|
1451
|
+
const { endpoints } = hyperedge;
|
|
1452
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
1453
|
+
for (let j = i + 1; j < endpoints.length; j++) {
|
|
1454
|
+
const a = endpoints[i];
|
|
1455
|
+
const b = endpoints[j];
|
|
1456
|
+
const before = this.sink.edgeCount;
|
|
1457
|
+
if (a.type === "undir" || b.type === "undir") {
|
|
1458
|
+
this.direction.addEdge(a.node, b.node, "undirected", undefined, where);
|
|
1459
|
+
}
|
|
1460
|
+
else if (a.type === "in" && b.type === "out") {
|
|
1461
|
+
this.direction.addEdge(a.node, b.node, "directed", undefined, where);
|
|
1462
|
+
}
|
|
1463
|
+
else if (a.type === "out" && b.type === "in") {
|
|
1464
|
+
this.direction.addEdge(b.node, a.node, "directed", undefined, where);
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
this.report.counts.edges += this.sink.edgeCount - before;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
// ------------------------------------------------------------------ options and ids
|
|
1474
|
+
/**
|
|
1475
|
+
* Count the endpoints of an edge the file never declared as nodes of the sink (created under
|
|
1476
|
+
* addMissingNodes), so the report describes what the sink holds (design section 8.6).
|
|
1477
|
+
* @param source - the source id
|
|
1478
|
+
* @param target - the target id
|
|
1479
|
+
*/
|
|
1480
|
+
countEndpoints(source, target) {
|
|
1481
|
+
if (!this.options.addMissingNodes) {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
let created = 0;
|
|
1485
|
+
if (this.sink.indexOf(source) === INVALID_INDEX) {
|
|
1486
|
+
created++;
|
|
1487
|
+
}
|
|
1488
|
+
if (source !== target && this.sink.indexOf(target) === INVALID_INDEX) {
|
|
1489
|
+
created++;
|
|
1490
|
+
}
|
|
1491
|
+
this.report.counts.nodes += created;
|
|
1492
|
+
}
|
|
1493
|
+
/** Check the cancellation signal every ABORT_CHECK_INTERVAL elements. */
|
|
1494
|
+
checkAbort() {
|
|
1495
|
+
if (++this.elementsSinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
1496
|
+
this.elementsSinceCheck = 0;
|
|
1497
|
+
throwIfAborted(this.options.signal);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* The id an edge endpoint refers to: the original id when the text is a mangled id restored
|
|
1502
|
+
* from `graphty:originalId` (the exporter writes endpoints with the mangled id), else the
|
|
1503
|
+
* coerced text.
|
|
1504
|
+
* @param text - the source / target / endpoint node text
|
|
1505
|
+
* @param line - the line
|
|
1506
|
+
* @returns the node id
|
|
1507
|
+
*/
|
|
1508
|
+
endpointId(text, line) {
|
|
1509
|
+
const restored = this.restored.get(text);
|
|
1510
|
+
if (restored !== undefined) {
|
|
1511
|
+
return restored;
|
|
1512
|
+
}
|
|
1513
|
+
const id = this.coercer.text(text);
|
|
1514
|
+
this.reportMerge(text, line);
|
|
1515
|
+
return id;
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Report a merge the coercer detected under ids: "number".
|
|
1519
|
+
* @param text - the id text just coerced
|
|
1520
|
+
* @param line - the line
|
|
1521
|
+
*/
|
|
1522
|
+
reportMerge(text, line) {
|
|
1523
|
+
const merge = this.coercer.lastMerge;
|
|
1524
|
+
if (merge !== null) {
|
|
1525
|
+
this.report.warning("coercion", GRAPHML_ISSUE.ID_MERGED, `id "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids: "number"`, { line, element: text });
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* A `parse.nodes` / `parse.edges` hint as a count.
|
|
1531
|
+
* @param text - the attribute text
|
|
1532
|
+
* @returns the count, or null when absent or not a non-negative integer
|
|
1533
|
+
*/
|
|
1534
|
+
function hint(text) {
|
|
1535
|
+
if (text === undefined || !/^[0-9]+$/.test(text)) {
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
const n = Number(text);
|
|
1539
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Resolve the format-specific options.
|
|
1543
|
+
* @param options - the caller's options
|
|
1544
|
+
* @returns the yfiles mode
|
|
1545
|
+
*/
|
|
1546
|
+
function resolveYfiles(options) {
|
|
1547
|
+
const value = options?.yfiles;
|
|
1548
|
+
if (value === undefined) {
|
|
1549
|
+
return "json";
|
|
1550
|
+
}
|
|
1551
|
+
if (typeof value !== "string" || !YFILES_MODES.has(value)) {
|
|
1552
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option yfiles: ${JSON.stringify(value)} is not "json" or "skip"`, {
|
|
1553
|
+
option: "yfiles",
|
|
1554
|
+
found: value,
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
return value;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Confidence that a head of bytes is GraphML.
|
|
1561
|
+
* @param head - the first bytes
|
|
1562
|
+
* @returns 1 for a `<graphml` root in the GraphML namespace, 0.9 for a `<graphml` root, 0.05 for other XML, 0 otherwise
|
|
1563
|
+
*/
|
|
1564
|
+
function sniffGraphml(head) {
|
|
1565
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(head.subarray(0, SNIFF_BYTES));
|
|
1566
|
+
const root = text.indexOf("<graphml");
|
|
1567
|
+
if (root >= 0) {
|
|
1568
|
+
return text.includes(GRAPHML_NAMESPACE, root) ? 1 : 0.9;
|
|
1569
|
+
}
|
|
1570
|
+
return /^\s*<(\?xml|!--|!DOCTYPE)/.test(text) ? 0.05 : 0;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Import GraphML into a sink.
|
|
1574
|
+
* @param input - the document
|
|
1575
|
+
* @param sink - the sink
|
|
1576
|
+
* @param options - format-specific and common options
|
|
1577
|
+
* @returns the report; ImportError when the document cannot be read or the error limit is exceeded
|
|
1578
|
+
*/
|
|
1579
|
+
async function importGraphml(input, sink, options) {
|
|
1580
|
+
const common = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
1581
|
+
const yfilesMode = resolveYfiles(options);
|
|
1582
|
+
const report = new ImportReportBuilder(FORMAT, common.errorLimit);
|
|
1583
|
+
reportSinkOptions(sink, options, report, true);
|
|
1584
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
1585
|
+
const reader = new GraphmlReader(sink, report, common, yfilesMode);
|
|
1586
|
+
try {
|
|
1587
|
+
await tokenizeXml(textChunks(input, report, common), reader);
|
|
1588
|
+
}
|
|
1589
|
+
catch (err) {
|
|
1590
|
+
if (err instanceof XmlSyntaxError) {
|
|
1591
|
+
report.fail(GRAPHML_ISSUE.XML_SYNTAX, err.message, { line: err.line });
|
|
1592
|
+
}
|
|
1593
|
+
throw err;
|
|
1594
|
+
}
|
|
1595
|
+
reader.finish();
|
|
1596
|
+
throwIfAborted(common.signal);
|
|
1597
|
+
return report.finish();
|
|
1598
|
+
}
|
|
1599
|
+
/** The GraphML importer (design section 8.4). */
|
|
1600
|
+
export const graphmlImporter = Object.freeze({
|
|
1601
|
+
format: FORMAT,
|
|
1602
|
+
extensions: EXTENSIONS,
|
|
1603
|
+
mimeTypes: MIME_TYPES,
|
|
1604
|
+
sniff: sniffGraphml,
|
|
1605
|
+
import: importGraphml,
|
|
1606
|
+
});
|
|
1607
|
+
//# sourceMappingURL=importer.js.map
|