@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,3050 @@
|
|
|
1
|
+
import { GraphFormatError, INVALID_INDEX, MAX_COUNT } from "@graphty/graph-format";
|
|
2
|
+
import { c as childrenCsr } from "./children-CL3Cy0ez.js";
|
|
3
|
+
import { x as MIXED_DIRECTION_CODE, g as DIRECTION_FORCED_CODE, h as DIRECTION_REFUSED_CODE, W as SINK_OPTION_CODE, R as PRECISION_CODE, B as BAD_DEFAULT_CODE, a1 as UNKNOWN_ATTR_TYPE_CODE, V as ROLE_TAKEN_CODE, C as COLUMN_RENAMED_CODE, J as OPTION_IGNORED_CODE, n as ID_MERGED_CODE, Y as STRAY_TEXT_CODE, a2 as UNKNOWN_ELEMENT_CODE, H as HYPEREDGE_CODE, d as COUNT_HINT_CODE, y as MULTIPLE_GRAPHS_CODE, i as DUPLICATE_EDGE_ID_CODE, k as DUPLICATE_NODE_CODE, M as MISSING_ENDPOINT_CODE, w as MISSING_ID_CODE, j as DUPLICATE_KEY_CODE, G as NO_GRAPH_CODE, r as INVALID_UTF8_CODE, a7 as XML_SYNTAX_CODE, p as ID_TEXT_TYPE_CODE, U as ROLE_DROPPED_CODE, K as PARENTS_DROPPED_CODE, z as MUTUAL_AS_UNDIRECTED_CODE, aB as resolveExportOptions, au as joinText, ah as encodeChunks, a9 as capabilities, aa as checkCapabilities, L as LOSS, aw as pairFolding, aD as sanitizeIds, ai as explicitWeights, as as isNmtoken, aq as isCanonicalIntegerText, av as mangleNmtoken, an as formatInteger, al as formatF64, ak as formatF32, aC as resolveImportOptions, I as ImportReportBuilder, az as reportSinkOptions, aA as reportUnusedOptions, t as textChunks, a as throwIfAborted, s as IdCoercer, l as DirectionResolver, at as isWeightField, aL as declareAttribute, aI as uniqueColumnName, af as declareResolved, aZ as takenIn, ax as parseWeightText, aN as parseDeclaredValue, aO as losesPrecision } from "./writer-DxSKC7TL.js";
|
|
4
|
+
import { e as escapeXmlText, d as isWhitespace, k as isXmlName, f as escapeXmlAttribute, x as xmlIllegalTextNotes, t as tokenizeXml, X as XmlSyntaxError, l as localName } from "./escape-DyI8JofU.js";
|
|
5
|
+
const GRAPHML_NAMESPACE = "http://graphml.graphdrawing.org/xmlns";
|
|
6
|
+
const YFILES_NAMESPACE = "http://www.yworks.com/xml/graphml";
|
|
7
|
+
const XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance";
|
|
8
|
+
const SCHEMA_LOCATION = "http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd";
|
|
9
|
+
const FORMAT = "graphml";
|
|
10
|
+
const EXTENSIONS = Object.freeze([".graphml", ".xml"]);
|
|
11
|
+
const MIME_TYPES = Object.freeze(["application/graphml+xml", "application/xml", "text/xml"]);
|
|
12
|
+
const ORIGINAL_ID_ATTRIBUTE = "graphty:originalId";
|
|
13
|
+
const ORIGINAL_ID_COLUMN = "graphty.originalId";
|
|
14
|
+
const EDGE_ID_COLUMN = "id";
|
|
15
|
+
const SOURCE_PORT_COLUMN = "sourceport";
|
|
16
|
+
const TARGET_PORT_COLUMN = "targetport";
|
|
17
|
+
const PARENT_COLUMN = "parent";
|
|
18
|
+
const HYPEREDGE_HUB_COLUMN = "graphty.hyperedge";
|
|
19
|
+
const LABEL_COLUMN = "label";
|
|
20
|
+
const RESERVED_EDGE_NAMES = /* @__PURE__ */ new Set([
|
|
21
|
+
EDGE_ID_COLUMN,
|
|
22
|
+
SOURCE_PORT_COLUMN,
|
|
23
|
+
TARGET_PORT_COLUMN
|
|
24
|
+
]);
|
|
25
|
+
const RESERVED_NODE_NAMES = /* @__PURE__ */ new Set([PARENT_COLUMN]);
|
|
26
|
+
const META_KEY = "graphml";
|
|
27
|
+
const GRAPHML_ISSUE = Object.freeze({
|
|
28
|
+
/** Fatal: the input is not well-formed XML. */
|
|
29
|
+
XML_SYNTAX: XML_SYNTAX_CODE,
|
|
30
|
+
/** Fatal: the input holds invalid UTF-8. */
|
|
31
|
+
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
32
|
+
/** Fatal: the root element is not `<graphml>`. */
|
|
33
|
+
NOT_GRAPHML: "E_NOT_GRAPHML",
|
|
34
|
+
/** Fatal: the document has no `<graph>`. */
|
|
35
|
+
NO_GRAPH: NO_GRAPH_CODE,
|
|
36
|
+
/** A `<key>` without an id. */
|
|
37
|
+
KEY_MISSING_ID: "E_GRAPHML_KEY_MISSING_ID",
|
|
38
|
+
/** Two `<key>` elements with the same id. */
|
|
39
|
+
DUPLICATE_KEY: DUPLICATE_KEY_CODE,
|
|
40
|
+
/** A key declared for hyperedges, ports or endpoints (never used). */
|
|
41
|
+
KEY_DOMAIN_UNSUPPORTED: "W_GRAPHML_KEY_DOMAIN_UNSUPPORTED",
|
|
42
|
+
/** A `<node>` without an id. */
|
|
43
|
+
MISSING_ID: MISSING_ID_CODE,
|
|
44
|
+
/** An `<edge>` without a source or a target. */
|
|
45
|
+
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
46
|
+
/** A `<data>` whose key was never declared. */
|
|
47
|
+
UNKNOWN_KEY: "E_GRAPHML_UNKNOWN_KEY",
|
|
48
|
+
/** A `<data>` whose key is declared for another domain. */
|
|
49
|
+
KEY_DOMAIN: "W_GRAPHML_KEY_DOMAIN",
|
|
50
|
+
/** A `<data>` without a key attribute. */
|
|
51
|
+
DATA_MISSING_KEY: "E_GRAPHML_DATA_MISSING_KEY",
|
|
52
|
+
/** A `<key>` whose `for` is not a GraphML domain. */
|
|
53
|
+
KEY_FOR_INVALID: "E_GRAPHML_KEY_FOR_INVALID",
|
|
54
|
+
/** Data of a nested `<graph>`; only the top-level graph has attributes. */
|
|
55
|
+
NESTED_GRAPH_DATA: "W_GRAPHML_NESTED_GRAPH_DATA",
|
|
56
|
+
/** Data of a hyperedge expanded to a star or clique. */
|
|
57
|
+
HYPEREDGE_DATA_DROPPED: "W_GRAPHML_HYPEREDGE_DATA_DROPPED",
|
|
58
|
+
/** Nested elements in the `<data>` of a typed (non-yfiles) key. */
|
|
59
|
+
DATA_NESTED: "E_GRAPHML_DATA_NESTED",
|
|
60
|
+
/** A `<node>` declared twice. */
|
|
61
|
+
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
62
|
+
/** An edge id already used by another edge. */
|
|
63
|
+
DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
|
|
64
|
+
/** A `directed` attribute that is neither true nor false. */
|
|
65
|
+
INVALID_DIRECTED: "E_GRAPHML_INVALID_DIRECTED",
|
|
66
|
+
/** An `edgedefault` that is neither directed nor undirected. */
|
|
67
|
+
INVALID_EDGEDEFAULT: "E_GRAPHML_INVALID_EDGEDEFAULT",
|
|
68
|
+
/** A `<graph>` without edgedefault; the `defaultDirected` option applies. */
|
|
69
|
+
EDGEDEFAULT_MISSING: "W_GRAPHML_EDGEDEFAULT_MISSING",
|
|
70
|
+
/** A second top-level `<graph>`; its nodes and edges are merged into the first. */
|
|
71
|
+
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
72
|
+
/** A `parse.nodes` / `parse.edges` hint the sink cannot reserve (ignored). */
|
|
73
|
+
COUNT_HINT: COUNT_HINT_CODE,
|
|
74
|
+
/** A hyperedge under `hyperedges: "error"`. */
|
|
75
|
+
HYPEREDGE: HYPEREDGE_CODE,
|
|
76
|
+
/** Hyperedges skipped under `hyperedges: "skip"`. */
|
|
77
|
+
HYPEREDGE_SKIPPED: "W_GRAPHML_HYPEREDGE_SKIPPED",
|
|
78
|
+
/** An endpoint of a hyperedge without a node, or with an unknown type. */
|
|
79
|
+
HYPEREDGE_ENDPOINT: "E_GRAPHML_HYPEREDGE_ENDPOINT",
|
|
80
|
+
/** `<port>` declarations (and their data) are not kept; sourceport / targetport edge attributes are. */
|
|
81
|
+
PORT_DECLARATION: "W_GRAPHML_PORT_DECLARATION",
|
|
82
|
+
/** A `<locator>` element. */
|
|
83
|
+
LOCATOR_DROPPED: "W_GRAPHML_LOCATOR_DROPPED",
|
|
84
|
+
/** A `<desc>` of a node, an edge or a hyperedge. */
|
|
85
|
+
DESC_DROPPED: "W_GRAPHML_DESC_DROPPED",
|
|
86
|
+
/** An element the GraphML schema does not define at that place. */
|
|
87
|
+
UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
|
|
88
|
+
/** Non-whitespace text where the schema allows only elements. */
|
|
89
|
+
STRAY_TEXT: STRAY_TEXT_CODE,
|
|
90
|
+
/** Two distinct id texts merged into one number under `ids: "number"`. */
|
|
91
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
92
|
+
/** An option GraphML has no use for (`nodeIdFrom`: nodes are identified by their id attribute). */
|
|
93
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
94
|
+
/** A yFiles key under `yfiles: "skip"`. */
|
|
95
|
+
YFILES_SKIPPED: "W_GRAPHML_YFILES_SKIPPED",
|
|
96
|
+
/** A key renamed `<name>#<id>` because the name was taken (design section 5.6). */
|
|
97
|
+
COLUMN_RENAMED: COLUMN_RENAMED_CODE,
|
|
98
|
+
/** A key declared without its role because the table already holds it. */
|
|
99
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
100
|
+
/** A declared type the format does not define (kept as string). */
|
|
101
|
+
UNKNOWN_ATTR_TYPE: UNKNOWN_ATTR_TYPE_CODE,
|
|
102
|
+
/** A default that does not parse as the declared type. */
|
|
103
|
+
BAD_DEFAULT: BAD_DEFAULT_CODE,
|
|
104
|
+
/** A long value beyond 2^53 rounded. */
|
|
105
|
+
PRECISION: PRECISION_CODE,
|
|
106
|
+
/** A builder-policy option the sink does not honour. */
|
|
107
|
+
SINK_OPTION: SINK_OPTION_CODE,
|
|
108
|
+
/** The sink refused the file's direction. */
|
|
109
|
+
DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
|
|
110
|
+
/** Edges forced to the policy's direction. */
|
|
111
|
+
DIRECTION_FORCED: DIRECTION_FORCED_CODE,
|
|
112
|
+
/** A mixed file under onMixedDirection "error" (fatal). */
|
|
113
|
+
MIXED_DIRECTION: MIXED_DIRECTION_CODE
|
|
114
|
+
});
|
|
115
|
+
const GRAPHML_LOSS = Object.freeze({
|
|
116
|
+
/** yFiles nested XML kept as a JSON tree: structure preserved, not byte-exact. */
|
|
117
|
+
YFILES_JSON: "W_GRAPHML_YFILES_JSON",
|
|
118
|
+
/** A mutual pair written as one undirected edge; the mark is lost. */
|
|
119
|
+
MUTUAL_AS_UNDIRECTED: MUTUAL_AS_UNDIRECTED_CODE,
|
|
120
|
+
/** A `parents` (multi-parent) column cannot be written as nested graphs. */
|
|
121
|
+
PARENTS_DROPPED: PARENTS_DROPPED_CODE,
|
|
122
|
+
/** A role column GraphML has no slot for (kind, node ids, ...) written as a plain attribute. */
|
|
123
|
+
ROLE_DROPPED: ROLE_DROPPED_CODE,
|
|
124
|
+
/** Containment order differs from index order; node indices change after a round trip. */
|
|
125
|
+
HIERARCHY_REORDERED: "W_GRAPHML_HIERARCHY_REORDERED",
|
|
126
|
+
/** Nodes whose parent chain never reaches a root are written at the top level. */
|
|
127
|
+
PARENT_CYCLE: "W_GRAPHML_PARENT_CYCLE",
|
|
128
|
+
/** Node ids that change type after a round trip under the canonical rule (design section 4.1). */
|
|
129
|
+
ID_TEXT_TYPE: ID_TEXT_TYPE_CODE,
|
|
130
|
+
/** A numeric edge id column reads back as string. */
|
|
131
|
+
EDGE_ID_TEXT: "W_GRAPHML_EDGE_ID_TEXT",
|
|
132
|
+
/** A yfiles json value that is not a serialisable tree: export() will throw E_COLUMN_TYPE. */
|
|
133
|
+
YFILES_TREE: "E_GRAPHML_YFILES_TREE"
|
|
134
|
+
});
|
|
135
|
+
const ATTRIBUTE_PREFIX = "@_";
|
|
136
|
+
const TEXT_KEY = "#text";
|
|
137
|
+
class XmlTreeBuilder {
|
|
138
|
+
/** Create a builder whose implicit root is the containing element. */
|
|
139
|
+
constructor() {
|
|
140
|
+
this.frames = [];
|
|
141
|
+
this.root = { name: "", node: {}, hasAttrs: false, hasChildren: false, text: "" };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Whether the builder is inside a child element (as opposed to at the root level).
|
|
145
|
+
* @returns true while at least one child element is open
|
|
146
|
+
*/
|
|
147
|
+
get open() {
|
|
148
|
+
return this.frames.length > 0;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* An element starts.
|
|
152
|
+
* @param name - the element name as written
|
|
153
|
+
* @param attrs - its attributes
|
|
154
|
+
*/
|
|
155
|
+
start(name, attrs) {
|
|
156
|
+
const node = {};
|
|
157
|
+
let hasAttrs = false;
|
|
158
|
+
for (const [key, value] of attrs) {
|
|
159
|
+
node[ATTRIBUTE_PREFIX + key] = value;
|
|
160
|
+
hasAttrs = true;
|
|
161
|
+
}
|
|
162
|
+
this.frames.push({ name, node, hasAttrs, hasChildren: false, text: "" });
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Character data inside the current element.
|
|
166
|
+
* @param text - the text
|
|
167
|
+
*/
|
|
168
|
+
text(text) {
|
|
169
|
+
const frame = this.frames.length > 0 ? this.frames[this.frames.length - 1] : this.root;
|
|
170
|
+
frame.text += text;
|
|
171
|
+
}
|
|
172
|
+
/** The current element ends; its value is attached to its parent. */
|
|
173
|
+
end() {
|
|
174
|
+
const frame = this.frames.pop();
|
|
175
|
+
if (frame === void 0) {
|
|
176
|
+
throw new GraphFormatError("E_COLUMN_TYPE", "XmlTreeBuilder.end() without a matching start()", {
|
|
177
|
+
reason: "tree builder underflow"
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const parent = this.frames.length > 0 ? this.frames[this.frames.length - 1] : this.root;
|
|
181
|
+
attachChild(parent, frame.name, valueOf(frame));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The content of the containing element: its text when it has no child elements, otherwise
|
|
185
|
+
* the object of its children (with `#text` for non-whitespace mixed content).
|
|
186
|
+
* @returns the value to store in the json column
|
|
187
|
+
*/
|
|
188
|
+
finish() {
|
|
189
|
+
if (this.frames.length > 0) {
|
|
190
|
+
throw new GraphFormatError("E_COLUMN_TYPE", "XmlTreeBuilder.finish() with open elements", {
|
|
191
|
+
reason: "tree builder open"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const { root } = this;
|
|
195
|
+
if (!root.hasChildren) {
|
|
196
|
+
return root.text;
|
|
197
|
+
}
|
|
198
|
+
if (!isWhitespace(root.text)) {
|
|
199
|
+
root.node[TEXT_KEY] = root.text;
|
|
200
|
+
}
|
|
201
|
+
return root.node;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function valueOf(frame) {
|
|
205
|
+
if (!frame.hasAttrs && !frame.hasChildren) {
|
|
206
|
+
return frame.text;
|
|
207
|
+
}
|
|
208
|
+
if (!isWhitespace(frame.text)) {
|
|
209
|
+
frame.node[TEXT_KEY] = frame.text;
|
|
210
|
+
}
|
|
211
|
+
return frame.node;
|
|
212
|
+
}
|
|
213
|
+
function attachChild(parent, name, value) {
|
|
214
|
+
parent.hasChildren = true;
|
|
215
|
+
const existing = parent.node[name];
|
|
216
|
+
if (existing === void 0) {
|
|
217
|
+
parent.node[name] = value;
|
|
218
|
+
} else if (Array.isArray(existing)) {
|
|
219
|
+
existing.push(value);
|
|
220
|
+
} else {
|
|
221
|
+
parent.node[name] = [existing, value];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function treeProblem(value) {
|
|
225
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
if (Array.isArray(value)) {
|
|
229
|
+
for (const item of value) {
|
|
230
|
+
if (Array.isArray(item)) {
|
|
231
|
+
return "nested arrays cannot be written as elements";
|
|
232
|
+
}
|
|
233
|
+
const problem = treeProblem(item);
|
|
234
|
+
if (problem !== null) {
|
|
235
|
+
return problem;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (typeof value !== "object") {
|
|
241
|
+
return `a ${typeof value} cannot be written as XML`;
|
|
242
|
+
}
|
|
243
|
+
for (const [key, child] of Object.entries(value)) {
|
|
244
|
+
if (key === TEXT_KEY) {
|
|
245
|
+
if (child !== null && typeof child === "object") {
|
|
246
|
+
return "#text must be a scalar";
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (key.startsWith(ATTRIBUTE_PREFIX)) {
|
|
251
|
+
const name = key.slice(ATTRIBUTE_PREFIX.length);
|
|
252
|
+
if (!isXmlName(name)) {
|
|
253
|
+
return `"${name}" is not an XML attribute name`;
|
|
254
|
+
}
|
|
255
|
+
if (child !== null && typeof child === "object") {
|
|
256
|
+
return `attribute ${name} must be a scalar`;
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!isXmlName(key)) {
|
|
261
|
+
return `"${key}" is not an XML element name`;
|
|
262
|
+
}
|
|
263
|
+
const problem = treeProblem(child);
|
|
264
|
+
if (problem !== null) {
|
|
265
|
+
return problem;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
function writeXmlTree(value, indent, out) {
|
|
271
|
+
const problem = treeProblem(value);
|
|
272
|
+
if (problem !== null) {
|
|
273
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `a yfiles value cannot be written as XML: ${problem}`, {
|
|
274
|
+
reason: "xml tree"
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
278
|
+
out.push(escapeXmlText(scalarText(value)));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const object = value;
|
|
282
|
+
let text = null;
|
|
283
|
+
for (const [key, child] of Object.entries(object)) {
|
|
284
|
+
if (key === TEXT_KEY) {
|
|
285
|
+
text = scalarText(child);
|
|
286
|
+
} else if (!key.startsWith(ATTRIBUTE_PREFIX)) {
|
|
287
|
+
writeElements(key, child, indent, out);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (text !== null) {
|
|
291
|
+
out.push(escapeXmlText(text));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function writeElements(name, value, indent, out) {
|
|
295
|
+
if (Array.isArray(value)) {
|
|
296
|
+
for (const item of value) {
|
|
297
|
+
writeElements(name, item, indent, out);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (value === null || typeof value !== "object") {
|
|
302
|
+
const text2 = scalarText(value);
|
|
303
|
+
out.push(text2.length === 0 ? `
|
|
304
|
+
${indent}<${name}/>` : `
|
|
305
|
+
${indent}<${name}>${escapeXmlText(text2)}</${name}>`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const object = value;
|
|
309
|
+
let attrs = "";
|
|
310
|
+
let text = null;
|
|
311
|
+
let hasChildren = false;
|
|
312
|
+
for (const [key, child] of Object.entries(object)) {
|
|
313
|
+
if (key === TEXT_KEY) {
|
|
314
|
+
text = scalarText(child);
|
|
315
|
+
} else if (key.startsWith(ATTRIBUTE_PREFIX)) {
|
|
316
|
+
attrs += ` ${key.slice(ATTRIBUTE_PREFIX.length)}="${escapeXmlAttribute(scalarText(child))}"`;
|
|
317
|
+
} else {
|
|
318
|
+
hasChildren = true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!hasChildren) {
|
|
322
|
+
out.push(
|
|
323
|
+
text === null || text.length === 0 ? `
|
|
324
|
+
${indent}<${name}${attrs}/>` : `
|
|
325
|
+
${indent}<${name}${attrs}>${escapeXmlText(text)}</${name}>`
|
|
326
|
+
);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
out.push(`
|
|
330
|
+
${indent}<${name}${attrs}>`);
|
|
331
|
+
const inner = `${indent} `;
|
|
332
|
+
for (const [key, child] of Object.entries(object)) {
|
|
333
|
+
if (key !== TEXT_KEY && !key.startsWith(ATTRIBUTE_PREFIX)) {
|
|
334
|
+
writeElements(key, child, inner, out);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (text !== null) {
|
|
338
|
+
out.push(escapeXmlText(text));
|
|
339
|
+
}
|
|
340
|
+
out.push(`
|
|
341
|
+
${indent}</${name}>`);
|
|
342
|
+
}
|
|
343
|
+
function scalarText(value) {
|
|
344
|
+
switch (typeof value) {
|
|
345
|
+
case "string":
|
|
346
|
+
return value;
|
|
347
|
+
case "number":
|
|
348
|
+
case "boolean":
|
|
349
|
+
case "bigint":
|
|
350
|
+
return String(value);
|
|
351
|
+
default:
|
|
352
|
+
return "";
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const CAPABILITIES = capabilities({
|
|
356
|
+
mixedDirection: true,
|
|
357
|
+
multiEdges: true,
|
|
358
|
+
selfLoops: true,
|
|
359
|
+
edgeIds: "optional",
|
|
360
|
+
idCharset: "nmtoken",
|
|
361
|
+
dtypes: ["bool", "i32", "f32", "f64", "string"],
|
|
362
|
+
components: false,
|
|
363
|
+
lists: false,
|
|
364
|
+
json: false,
|
|
365
|
+
defaults: true,
|
|
366
|
+
options: false,
|
|
367
|
+
hierarchy: true,
|
|
368
|
+
temporal: "none",
|
|
369
|
+
graphAttributes: true,
|
|
370
|
+
positions: false,
|
|
371
|
+
viz: false
|
|
372
|
+
});
|
|
373
|
+
const STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
374
|
+
"directed",
|
|
375
|
+
"pair",
|
|
376
|
+
"mutual",
|
|
377
|
+
"weight",
|
|
378
|
+
"timeText",
|
|
379
|
+
"originalId"
|
|
380
|
+
]);
|
|
381
|
+
const DROPPED_ROLES = /* @__PURE__ */ new Set([
|
|
382
|
+
"position",
|
|
383
|
+
"color",
|
|
384
|
+
"size",
|
|
385
|
+
"shape",
|
|
386
|
+
"thickness",
|
|
387
|
+
"start",
|
|
388
|
+
"end",
|
|
389
|
+
"timestamp",
|
|
390
|
+
"timestamps",
|
|
391
|
+
"spells",
|
|
392
|
+
"open"
|
|
393
|
+
]);
|
|
394
|
+
const SLOT_ROLES = /* @__PURE__ */ new Set(["label", "id", "sourcePort", "targetPort", "parent"]);
|
|
395
|
+
const SLOT_ROLES_BY_DOMAIN = {
|
|
396
|
+
graph: /* @__PURE__ */ new Set(),
|
|
397
|
+
node: /* @__PURE__ */ new Set(["parent"]),
|
|
398
|
+
edge: /* @__PURE__ */ new Set(["id", "sourcePort", "targetPort"])
|
|
399
|
+
};
|
|
400
|
+
const ROLE_NAMES = Object.freeze({
|
|
401
|
+
label: LABEL_COLUMN,
|
|
402
|
+
id: EDGE_ID_COLUMN,
|
|
403
|
+
sourcePort: SOURCE_PORT_COLUMN,
|
|
404
|
+
targetPort: TARGET_PORT_COLUMN,
|
|
405
|
+
parent: PARENT_COLUMN
|
|
406
|
+
});
|
|
407
|
+
const DEFAULT_WEIGHT_NAME = "weight";
|
|
408
|
+
const TYPE_DTYPES = {
|
|
409
|
+
boolean: "bool",
|
|
410
|
+
int: "i32",
|
|
411
|
+
long: "f64",
|
|
412
|
+
float: "f32",
|
|
413
|
+
double: "f64",
|
|
414
|
+
string: "string"
|
|
415
|
+
};
|
|
416
|
+
const I32_MAX = 2147483647;
|
|
417
|
+
const DEFAULT_YFILES_TYPES = {
|
|
418
|
+
node: "nodegraphics",
|
|
419
|
+
edge: "edgegraphics",
|
|
420
|
+
graph: "resources"
|
|
421
|
+
};
|
|
422
|
+
function resolveFormatOptions(options) {
|
|
423
|
+
const pretty = options?.pretty ?? true;
|
|
424
|
+
if (typeof pretty !== "boolean") {
|
|
425
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option pretty must be a boolean", {
|
|
426
|
+
option: "pretty",
|
|
427
|
+
found: typeof pretty
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const edgedefault = options?.edgedefault ?? null;
|
|
431
|
+
if (edgedefault !== null && edgedefault !== "directed" && edgedefault !== "undirected") {
|
|
432
|
+
throw new GraphFormatError(
|
|
433
|
+
"E_UNSUPPORTED",
|
|
434
|
+
`option edgedefault: ${JSON.stringify(edgedefault)} is not "directed" or "undirected"`,
|
|
435
|
+
{ option: "edgedefault", found: edgedefault }
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
return { pretty, edgedefault };
|
|
439
|
+
}
|
|
440
|
+
function graphmlMetaOf(snapshot) {
|
|
441
|
+
const value = snapshot.meta.extra[META_KEY];
|
|
442
|
+
if (typeof value !== "object" || value === null) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
const record = value;
|
|
446
|
+
const graphId = typeof record.graphId === "string" ? record.graphId : null;
|
|
447
|
+
const edgedefault = record.edgedefault === "directed" || record.edgedefault === "undirected" ? record.edgedefault : null;
|
|
448
|
+
const namespaces = {};
|
|
449
|
+
if (typeof record.namespaces === "object" && record.namespaces !== null) {
|
|
450
|
+
for (const [prefix, uri] of Object.entries(record.namespaces)) {
|
|
451
|
+
if (typeof uri === "string") {
|
|
452
|
+
namespaces[prefix] = uri;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { graphId, edgedefault, namespaces };
|
|
457
|
+
}
|
|
458
|
+
function planExport(snapshot, options, format) {
|
|
459
|
+
const notes = checkCapabilities(snapshot, CAPABILITIES, options, {
|
|
460
|
+
roles: SLOT_ROLES,
|
|
461
|
+
roleNames: ROLE_NAMES
|
|
462
|
+
}).filter((n) => n.code !== LOSS.JSON);
|
|
463
|
+
const note = (code, message, column = null, count = null) => {
|
|
464
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
465
|
+
};
|
|
466
|
+
notes.push(...xmlIllegalTextNotes(snapshot));
|
|
467
|
+
const meta = graphmlMetaOf(snapshot);
|
|
468
|
+
const keyIds = new KeyIds();
|
|
469
|
+
const graphColumns = planTable(snapshot.graph, "graph", notes, note);
|
|
470
|
+
const nodeColumns = planTable(snapshot.nodes, "node", notes, note);
|
|
471
|
+
const edgeColumns = planTable(snapshot.edges, "edge", notes, note);
|
|
472
|
+
const keys = planKeys([...graphColumns, ...nodeColumns, ...edgeColumns], keyIds);
|
|
473
|
+
reservedNameNotes(nodeColumns, RESERVED_NODE_NAMES, note);
|
|
474
|
+
reservedNameNotes(edgeColumns, RESERVED_EDGE_NAMES, note);
|
|
475
|
+
const weight = planWeight(snapshot, edgeColumns, keyIds, keys, note);
|
|
476
|
+
const idTypeChanges = countIdTypeChanges(snapshot);
|
|
477
|
+
if (idTypeChanges > 0) {
|
|
478
|
+
note(
|
|
479
|
+
GRAPHML_LOSS.ID_TEXT_TYPE,
|
|
480
|
+
`${idTypeChanges} node id(s) change type when read back under ids: "canonical" (string ids that are integer text, non-integer numbers)`,
|
|
481
|
+
null,
|
|
482
|
+
idTypeChanges
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const edgeIdColumn = snapshot.edges.byRole("id");
|
|
486
|
+
if (edgeIdColumn !== null) {
|
|
487
|
+
planEdgeIds(snapshot, edgeIdColumn, options, note);
|
|
488
|
+
}
|
|
489
|
+
const unrepresentable = countUnrepresentableNodeIds(snapshot);
|
|
490
|
+
const needsOriginalIdKey = unrepresentable > 0 && options.sanitizeIds === "mangle";
|
|
491
|
+
const originalIdKey = needsOriginalIdKey ? keyIds.next(null) : null;
|
|
492
|
+
const folding = pairFolding(snapshot, { foldMutual: true });
|
|
493
|
+
if (folding.mutualCount > 0) {
|
|
494
|
+
note(
|
|
495
|
+
GRAPHML_LOSS.MUTUAL_AS_UNDIRECTED,
|
|
496
|
+
`${folding.mutualCount} mutual pair(s) are written as undirected edges; the mutual mark is lost`,
|
|
497
|
+
null,
|
|
498
|
+
folding.mutualCount
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const edgedefault = planEdgedefault(snapshot, folding, format, meta);
|
|
502
|
+
const hierarchy = planHierarchy(snapshot);
|
|
503
|
+
if (hierarchy.reordered) {
|
|
504
|
+
note(
|
|
505
|
+
GRAPHML_LOSS.HIERARCHY_REORDERED,
|
|
506
|
+
"nodes are written in containment order (children nested under their parent); node indices change after a round trip",
|
|
507
|
+
hierarchy.parent?.meta.name ?? null
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (hierarchy.unreachable > 0) {
|
|
511
|
+
note(
|
|
512
|
+
GRAPHML_LOSS.PARENT_CYCLE,
|
|
513
|
+
`${hierarchy.unreachable} node(s) whose parent chain never reaches a root are written at the top level`,
|
|
514
|
+
hierarchy.parent?.meta.name ?? null,
|
|
515
|
+
hierarchy.unreachable
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
options,
|
|
520
|
+
pretty: format.pretty,
|
|
521
|
+
notes,
|
|
522
|
+
keys,
|
|
523
|
+
graphColumns,
|
|
524
|
+
nodeColumns,
|
|
525
|
+
edgeColumns,
|
|
526
|
+
weight,
|
|
527
|
+
originalIdKey,
|
|
528
|
+
edgedefault,
|
|
529
|
+
folding,
|
|
530
|
+
edgeIdColumn,
|
|
531
|
+
sourcePortColumn: snapshot.edges.byRole("sourcePort"),
|
|
532
|
+
targetPortColumn: snapshot.edges.byRole("targetPort"),
|
|
533
|
+
hierarchy,
|
|
534
|
+
meta,
|
|
535
|
+
needsOriginalIdKey
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function reservedNameNotes(plans, reserved, note) {
|
|
539
|
+
for (const plan of plans) {
|
|
540
|
+
const { column, attrName, keyId } = plan;
|
|
541
|
+
if (attrName === null || !reserved.has(attrName)) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
note(
|
|
545
|
+
LOSS.COLUMN_NAME_CHANGED,
|
|
546
|
+
`${plan.domain} column "${column.meta.name}" is titled like the importer's ${attrName} column and reads back as "${attrName}#${keyId}"`,
|
|
547
|
+
column.meta.name,
|
|
548
|
+
column.length - column.nullCount
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
class KeyIds {
|
|
553
|
+
constructor() {
|
|
554
|
+
this.used = /* @__PURE__ */ new Set();
|
|
555
|
+
this.counter = 0;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Take a key id.
|
|
559
|
+
* @param preferred - the id to keep when it is an NMTOKEN not handed out yet, or null
|
|
560
|
+
* @returns the id
|
|
561
|
+
*/
|
|
562
|
+
next(preferred) {
|
|
563
|
+
if (preferred !== null && isNmtoken(preferred) && !this.used.has(preferred)) {
|
|
564
|
+
this.used.add(preferred);
|
|
565
|
+
return preferred;
|
|
566
|
+
}
|
|
567
|
+
for (; ; ) {
|
|
568
|
+
const candidate = `d${this.counter}`;
|
|
569
|
+
this.counter++;
|
|
570
|
+
if (!this.used.has(candidate)) {
|
|
571
|
+
this.used.add(candidate);
|
|
572
|
+
return candidate;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function planKeys(all, keyIds) {
|
|
578
|
+
const keys = [];
|
|
579
|
+
const byOrigin = /* @__PURE__ */ new Map();
|
|
580
|
+
for (const plan of all) {
|
|
581
|
+
const { origin } = plan.column.meta;
|
|
582
|
+
if (origin !== null && origin.format === FORMAT && origin.id !== null) {
|
|
583
|
+
const group = byOrigin.get(origin.id) ?? [];
|
|
584
|
+
group.push(plan);
|
|
585
|
+
byOrigin.set(origin.id, group);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const planned = /* @__PURE__ */ new Set();
|
|
589
|
+
for (const plan of all) {
|
|
590
|
+
if (planned.has(plan)) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const { origin } = plan.column.meta;
|
|
594
|
+
const group = origin !== null && origin.format === FORMAT && origin.id !== null ? byOrigin.get(origin.id) ?? [plan] : [plan];
|
|
595
|
+
const shared = group.length > 1 && group.every((other) => other === plan || sameKey(plan, other));
|
|
596
|
+
const members = shared ? group : [plan];
|
|
597
|
+
const id = keyIds.next(origin !== null && origin.format === FORMAT ? origin.id : null);
|
|
598
|
+
for (const member of members) {
|
|
599
|
+
member.keyId = id;
|
|
600
|
+
planned.add(member);
|
|
601
|
+
}
|
|
602
|
+
const domains = members.map((member) => member.domain);
|
|
603
|
+
const yfilesName = plan.column.meta.origin?.title ?? plan.column.meta.name;
|
|
604
|
+
let { attrName } = plan;
|
|
605
|
+
if (plan.yfiles) {
|
|
606
|
+
attrName = yfilesName === id ? null : yfilesName;
|
|
607
|
+
}
|
|
608
|
+
keys.push({
|
|
609
|
+
id,
|
|
610
|
+
domains: domains.length > 1 ? ["graph", "node", "edge"] : domains,
|
|
611
|
+
attrName,
|
|
612
|
+
attrType: plan.attrType,
|
|
613
|
+
yfilesType: plan.yfilesType,
|
|
614
|
+
column: plan.column
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return keys;
|
|
618
|
+
}
|
|
619
|
+
function planWeight(snapshot, edgeColumns, keyIds, keys, note) {
|
|
620
|
+
const weights = explicitWeights(snapshot);
|
|
621
|
+
const origin = snapshot.meta.weightOrigin;
|
|
622
|
+
const fromGraphml = weights.weighted && origin !== null && origin.format === FORMAT;
|
|
623
|
+
const attrName = fromGraphml && origin.title !== null ? origin.title : DEFAULT_WEIGHT_NAME;
|
|
624
|
+
for (const plan of edgeColumns) {
|
|
625
|
+
if (plan.attrName === attrName) {
|
|
626
|
+
note(
|
|
627
|
+
LOSS.WEIGHT_KEY_CLASH,
|
|
628
|
+
`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`,
|
|
629
|
+
plan.column.meta.name,
|
|
630
|
+
plan.column.length - plan.column.nullCount
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!weights.weighted) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
let attrType = "double";
|
|
638
|
+
if (fromGraphml && origin.type !== null) {
|
|
639
|
+
const declared = origin.type.trim().toLowerCase();
|
|
640
|
+
if (declared === "float" || declared === "double" || declared === "long" || declared === "int") {
|
|
641
|
+
attrType = declared;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if ((attrType === "int" || attrType === "long") && !weightsIntegral(snapshot, weights)) {
|
|
645
|
+
attrType = "double";
|
|
646
|
+
}
|
|
647
|
+
const id = keyIds.next(fromGraphml ? origin.id : null);
|
|
648
|
+
keys.push({ id, domains: ["edge"], attrName, attrType, yfilesType: null, column: null });
|
|
649
|
+
return { keyId: id, attrType, weights };
|
|
650
|
+
}
|
|
651
|
+
function planEdgeIds(snapshot, column, options, note) {
|
|
652
|
+
if (column.dtype !== "string" && column.dtype !== "dict") {
|
|
653
|
+
note(
|
|
654
|
+
GRAPHML_LOSS.EDGE_ID_TEXT,
|
|
655
|
+
`edge id column "${column.meta.name}" is ${column.dtype}; GraphML edge ids read back as strings`,
|
|
656
|
+
column.meta.name,
|
|
657
|
+
snapshot.edgeCount - column.nullCount
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const bad = countBadEdgeIds(column);
|
|
661
|
+
if (bad === 0) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (options.sanitizeIds === "mangle") {
|
|
665
|
+
note(LOSS.ID_MANGLED, `${bad} edge id(s) outside the nmtoken charset are rewritten`, column.meta.name, bad);
|
|
666
|
+
} else {
|
|
667
|
+
note(
|
|
668
|
+
LOSS.ID_CHARSET,
|
|
669
|
+
`${bad} edge id(s) outside the nmtoken charset; export() will throw unless sanitizeIds is "mangle"`,
|
|
670
|
+
column.meta.name,
|
|
671
|
+
bad
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function planEdgedefault(snapshot, folding, format, meta) {
|
|
676
|
+
if (format.edgedefault !== null) {
|
|
677
|
+
return format.edgedefault;
|
|
678
|
+
}
|
|
679
|
+
if (!snapshot.directed) {
|
|
680
|
+
return "undirected";
|
|
681
|
+
}
|
|
682
|
+
let directedCount = 0;
|
|
683
|
+
let undirectedCount = 0;
|
|
684
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
685
|
+
if (folding.folded(e)) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (isDirectedEdge(folding, e)) {
|
|
689
|
+
directedCount++;
|
|
690
|
+
} else {
|
|
691
|
+
undirectedCount++;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (undirectedCount === 0) {
|
|
695
|
+
return "directed";
|
|
696
|
+
}
|
|
697
|
+
const recorded = meta === null ? null : meta.edgedefault;
|
|
698
|
+
if (recorded !== null) {
|
|
699
|
+
return recorded;
|
|
700
|
+
}
|
|
701
|
+
return undirectedCount > directedCount ? "undirected" : "directed";
|
|
702
|
+
}
|
|
703
|
+
function sameKey(a, b) {
|
|
704
|
+
return a.attrName === b.attrName && a.attrType === b.attrType && a.yfilesType === b.yfilesType && a.domain !== b.domain && JSON.stringify(a.column.meta.default ?? null) === JSON.stringify(b.column.meta.default ?? null);
|
|
705
|
+
}
|
|
706
|
+
function planTable(table, domain, notes, note) {
|
|
707
|
+
const plans = [];
|
|
708
|
+
const columns = [...table];
|
|
709
|
+
const names = new Set(columns.map((column) => column.meta.name));
|
|
710
|
+
for (const column of columns) {
|
|
711
|
+
const { meta } = column;
|
|
712
|
+
const { role, name } = meta;
|
|
713
|
+
if (role === "parents") {
|
|
714
|
+
note(
|
|
715
|
+
GRAPHML_LOSS.PARENTS_DROPPED,
|
|
716
|
+
`${domain} column "${name}" (parents) cannot be written: nested graphs hold one parent per node`,
|
|
717
|
+
name,
|
|
718
|
+
column.length - column.nullCount
|
|
719
|
+
);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (role !== null && (STRUCTURAL_ROLES.has(role) || DROPPED_ROLES.has(role) || SLOT_ROLES_BY_DOMAIN[domain].has(role))) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (role !== null && role !== "label" && SLOT_ROLES.has(role)) {
|
|
726
|
+
withdrawNameChange(notes, name);
|
|
727
|
+
note(
|
|
728
|
+
LOSS.ROLE,
|
|
729
|
+
`${domain} column "${name}" (${role}) is written as a plain attribute; GraphML has no ${role} slot for a ${domain} and the role is lost`,
|
|
730
|
+
name,
|
|
731
|
+
column.length - column.nullCount
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
const yfiles = meta.dtype === "json" && meta.origin?.namespace === "yfiles";
|
|
735
|
+
if (meta.dtype === "json" && !yfiles) {
|
|
736
|
+
note(
|
|
737
|
+
LOSS.JSON,
|
|
738
|
+
`${domain} column "${name}" holds nested values; written as JSON text, which reads back as string`,
|
|
739
|
+
name,
|
|
740
|
+
column.length - column.nullCount
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
if (yfiles) {
|
|
744
|
+
const bad = countBadTrees(column);
|
|
745
|
+
if (bad > 0) {
|
|
746
|
+
note(
|
|
747
|
+
GRAPHML_LOSS.YFILES_TREE,
|
|
748
|
+
`${bad} value(s) of yfiles column "${name}" are not XML trees; export() will throw E_COLUMN_TYPE`,
|
|
749
|
+
name,
|
|
750
|
+
bad
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const { origin } = meta;
|
|
755
|
+
const fromGraphml = origin !== null && origin.format === FORMAT;
|
|
756
|
+
let attrName = null;
|
|
757
|
+
if (!yfiles) {
|
|
758
|
+
attrName = fromGraphml && origin.title !== null ? origin.title : name;
|
|
759
|
+
if (role === "label" && domain !== "graph" && attrName !== LABEL_COLUMN) {
|
|
760
|
+
attrName = labelSlot(column, domain, names, notes, note);
|
|
761
|
+
} else if (role === null && domain !== "graph" && attrName === LABEL_COLUMN) {
|
|
762
|
+
note(
|
|
763
|
+
LOSS.ROLE_ASSUMED,
|
|
764
|
+
`${domain} column "${name}" has no role but is titled "${LABEL_COLUMN}", which the importer reads back with the label role`,
|
|
765
|
+
name,
|
|
766
|
+
column.length - column.nullCount
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
plans.push({
|
|
771
|
+
column,
|
|
772
|
+
domain,
|
|
773
|
+
keyId: "",
|
|
774
|
+
attrType: yfiles ? null : attrTypeFor(column),
|
|
775
|
+
attrName,
|
|
776
|
+
yfilesType: yfiles ? origin?.type ?? DEFAULT_YFILES_TYPES[domain] : null,
|
|
777
|
+
yfiles
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return plans;
|
|
781
|
+
}
|
|
782
|
+
function labelSlot(column, domain, names, notes, note) {
|
|
783
|
+
const { name } = column.meta;
|
|
784
|
+
if (!names.has(LABEL_COLUMN)) {
|
|
785
|
+
return LABEL_COLUMN;
|
|
786
|
+
}
|
|
787
|
+
withdrawNameChange(notes, name);
|
|
788
|
+
note(
|
|
789
|
+
LOSS.ROLE,
|
|
790
|
+
`${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`,
|
|
791
|
+
name,
|
|
792
|
+
column.length - column.nullCount
|
|
793
|
+
);
|
|
794
|
+
return name;
|
|
795
|
+
}
|
|
796
|
+
function withdrawNameChange(notes, name) {
|
|
797
|
+
const at = notes.findIndex((n) => n.code === LOSS.COLUMN_NAME_CHANGED && n.column === name);
|
|
798
|
+
if (at >= 0) {
|
|
799
|
+
notes.splice(at, 1);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function attrTypeFor(column) {
|
|
803
|
+
const { meta } = column;
|
|
804
|
+
const { origin } = meta;
|
|
805
|
+
const declared = origin !== null && origin.format === FORMAT && origin.type !== null ? origin.type : null;
|
|
806
|
+
if (meta.components > 1) {
|
|
807
|
+
return "string";
|
|
808
|
+
}
|
|
809
|
+
switch (meta.dtype) {
|
|
810
|
+
case "bool":
|
|
811
|
+
return "boolean";
|
|
812
|
+
case "i32":
|
|
813
|
+
return "int";
|
|
814
|
+
case "f32":
|
|
815
|
+
return "float";
|
|
816
|
+
case "f64": {
|
|
817
|
+
if (declared !== null && declared.trim().toLowerCase() === "long" && columnIntegral(column)) {
|
|
818
|
+
return "long";
|
|
819
|
+
}
|
|
820
|
+
return "double";
|
|
821
|
+
}
|
|
822
|
+
case "u8":
|
|
823
|
+
return "int";
|
|
824
|
+
case "u32":
|
|
825
|
+
return columnMax(column) <= I32_MAX ? "int" : "long";
|
|
826
|
+
case "string":
|
|
827
|
+
if (declared !== null && TYPE_DTYPES[declared.trim().toLowerCase()] === void 0) {
|
|
828
|
+
return declared;
|
|
829
|
+
}
|
|
830
|
+
return "string";
|
|
831
|
+
case "dict":
|
|
832
|
+
case "list":
|
|
833
|
+
case "json":
|
|
834
|
+
return "string";
|
|
835
|
+
default: {
|
|
836
|
+
const { dtype } = meta;
|
|
837
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${String(dtype)}`, { dtype });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function columnIntegral(column) {
|
|
842
|
+
if (column.dtype !== "f64") {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
const { data } = column;
|
|
846
|
+
for (let r = 0; r < column.length; r++) {
|
|
847
|
+
if (column.isSet(r) && !Number.isInteger(data[r])) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
function columnMax(column) {
|
|
854
|
+
if (column.dtype !== "u32") {
|
|
855
|
+
return 0;
|
|
856
|
+
}
|
|
857
|
+
let max = 0;
|
|
858
|
+
const { data } = column;
|
|
859
|
+
for (let r = 0; r < column.length; r++) {
|
|
860
|
+
if (column.isSet(r) && data[r] > max) {
|
|
861
|
+
max = data[r];
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return max;
|
|
865
|
+
}
|
|
866
|
+
function weightsIntegral(snapshot, weights) {
|
|
867
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
868
|
+
if (weights.isExplicit(e) && !Number.isInteger(weights.value(e))) {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
function countBadTrees(column) {
|
|
875
|
+
if (column.dtype !== "json") {
|
|
876
|
+
return 0;
|
|
877
|
+
}
|
|
878
|
+
let bad = 0;
|
|
879
|
+
for (let r = 0; r < column.length; r++) {
|
|
880
|
+
if (column.isSet(r) && treeProblem(column.values[r]) !== null) {
|
|
881
|
+
bad++;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return bad;
|
|
885
|
+
}
|
|
886
|
+
function countIdTypeChanges(snapshot) {
|
|
887
|
+
const { ids } = snapshot;
|
|
888
|
+
if (ids.kind === "identity" || ids.kind === "dense") {
|
|
889
|
+
return 0;
|
|
890
|
+
}
|
|
891
|
+
let count = 0;
|
|
892
|
+
for (let i = 0; i < ids.size; i++) {
|
|
893
|
+
const id = ids.idOf(i);
|
|
894
|
+
if (typeof id === "string" ? isNmtoken(id) && isCanonicalIntegerText(id) : !Number.isSafeInteger(id)) {
|
|
895
|
+
count++;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return count;
|
|
899
|
+
}
|
|
900
|
+
function countUnrepresentableNodeIds(snapshot) {
|
|
901
|
+
const { ids } = snapshot;
|
|
902
|
+
if (ids.kind === "identity" || ids.kind === "dense") {
|
|
903
|
+
return 0;
|
|
904
|
+
}
|
|
905
|
+
let count = 0;
|
|
906
|
+
for (let i = 0; i < ids.size; i++) {
|
|
907
|
+
if (!isNmtoken(String(ids.idOf(i)))) {
|
|
908
|
+
count++;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return count;
|
|
912
|
+
}
|
|
913
|
+
function countBadEdgeIds(column) {
|
|
914
|
+
let bad = 0;
|
|
915
|
+
for (let e = 0; e < column.length; e++) {
|
|
916
|
+
if (column.isSet(e) && !isNmtoken(edgeIdText(column, e))) {
|
|
917
|
+
bad++;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return bad;
|
|
921
|
+
}
|
|
922
|
+
function edgeIdText(column, e) {
|
|
923
|
+
const value = column.value(e);
|
|
924
|
+
if (typeof value === "string") {
|
|
925
|
+
return value;
|
|
926
|
+
}
|
|
927
|
+
if (typeof value === "number") {
|
|
928
|
+
return Number.isInteger(value) ? formatInteger(value) : formatF64(value);
|
|
929
|
+
}
|
|
930
|
+
return String(value);
|
|
931
|
+
}
|
|
932
|
+
function isDirectedEdge(folding, e) {
|
|
933
|
+
return folding.mateOf(e) === INVALID_INDEX && folding.sourceDirected(e);
|
|
934
|
+
}
|
|
935
|
+
function planHierarchy(snapshot) {
|
|
936
|
+
const parent = snapshot.nodes.byRole("parent");
|
|
937
|
+
const csr = childrenCsr(snapshot, { column: parent !== null && parent.dtype === "u32" ? parent : null });
|
|
938
|
+
const walk = csr.depthFirst();
|
|
939
|
+
const roots = walk.order.filter((u) => walk.depth[u] === 0);
|
|
940
|
+
return {
|
|
941
|
+
parent: csr.column,
|
|
942
|
+
roots,
|
|
943
|
+
childStart: csr.rowPtr,
|
|
944
|
+
childList: csr.children,
|
|
945
|
+
reordered: walk.reordered,
|
|
946
|
+
unreachable: csr.unreachable
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function cellText(column, row, attrType) {
|
|
950
|
+
if (column.meta.components > 1) {
|
|
951
|
+
const value = column.value(row);
|
|
952
|
+
return JSON.stringify(Array.from(value));
|
|
953
|
+
}
|
|
954
|
+
switch (column.dtype) {
|
|
955
|
+
case "bool":
|
|
956
|
+
return column.value(row) === true ? "true" : "false";
|
|
957
|
+
case "i32":
|
|
958
|
+
case "u32":
|
|
959
|
+
case "u8":
|
|
960
|
+
return String(column.data[row]);
|
|
961
|
+
case "f32":
|
|
962
|
+
return formatF32(column.data[row]);
|
|
963
|
+
case "f64": {
|
|
964
|
+
const value = column.data[row];
|
|
965
|
+
return (attrType === "long" || attrType === "int") && Number.isInteger(value) ? formatInteger(value) : formatF64(value);
|
|
966
|
+
}
|
|
967
|
+
case "string":
|
|
968
|
+
return column.valueAt(row);
|
|
969
|
+
case "dict":
|
|
970
|
+
return column.dictionary[column.codes[row]];
|
|
971
|
+
case "list":
|
|
972
|
+
return JSON.stringify(Array.from(column.sliceOf(row)));
|
|
973
|
+
case "json":
|
|
974
|
+
return JSON.stringify(column.values[row]) ?? "";
|
|
975
|
+
default: {
|
|
976
|
+
const dtype = column;
|
|
977
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${String(dtype)}`, {});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function defaultText(column, attrType) {
|
|
982
|
+
const value = column.meta.default;
|
|
983
|
+
if (value === void 0) {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
switch (typeof value) {
|
|
987
|
+
case "boolean":
|
|
988
|
+
return value ? "true" : "false";
|
|
989
|
+
case "number":
|
|
990
|
+
if (column.dtype === "f32") {
|
|
991
|
+
return formatF32(value);
|
|
992
|
+
}
|
|
993
|
+
return (attrType === "long" || attrType === "int") && Number.isInteger(value) ? formatInteger(value) : formatF64(value);
|
|
994
|
+
case "string":
|
|
995
|
+
return value;
|
|
996
|
+
default:
|
|
997
|
+
return JSON.stringify(value) ?? "";
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function writeData(plan, row, indent, out) {
|
|
1001
|
+
const { column } = plan;
|
|
1002
|
+
if (!column.isSet(row)) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (plan.yfiles && column.dtype === "json") {
|
|
1006
|
+
writeTreeData(plan.keyId, column.values[row], indent, out);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
out.push(`
|
|
1010
|
+
${indent}<data key="${plan.keyId}">${escapeXmlText(cellText(column, row, plan.attrType))}</data>`);
|
|
1011
|
+
}
|
|
1012
|
+
function writeTreeData(keyId, value, indent, out) {
|
|
1013
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1014
|
+
const parts = [];
|
|
1015
|
+
writeXmlTree(value, indent, parts);
|
|
1016
|
+
out.push(`
|
|
1017
|
+
${indent}<data key="${keyId}">${parts.join("")}</data>`);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
out.push(`
|
|
1021
|
+
${indent}<data key="${keyId}">`);
|
|
1022
|
+
writeXmlTree(value, `${indent} `, out);
|
|
1023
|
+
out.push(`
|
|
1024
|
+
${indent}</data>`);
|
|
1025
|
+
}
|
|
1026
|
+
function keyElement(key, indent) {
|
|
1027
|
+
const domain = key.domains.length === 3 ? "all" : key.domains[0];
|
|
1028
|
+
let text = `${indent}<key id="${escapeXmlAttribute(key.id)}" for="${domain}"`;
|
|
1029
|
+
if (key.attrName !== null) {
|
|
1030
|
+
text += ` attr.name="${escapeXmlAttribute(key.attrName)}"`;
|
|
1031
|
+
}
|
|
1032
|
+
if (key.attrType !== null) {
|
|
1033
|
+
text += ` attr.type="${escapeXmlAttribute(key.attrType)}"`;
|
|
1034
|
+
}
|
|
1035
|
+
if (key.yfilesType !== null) {
|
|
1036
|
+
text += ` yfiles.type="${escapeXmlAttribute(key.yfilesType)}"`;
|
|
1037
|
+
}
|
|
1038
|
+
const { column } = key;
|
|
1039
|
+
const desc = column?.meta.extra.desc;
|
|
1040
|
+
const hasDefault = column !== null && column.meta.default !== void 0;
|
|
1041
|
+
if (column === null || typeof desc !== "string" && !hasDefault) {
|
|
1042
|
+
return `${text}/>
|
|
1043
|
+
`;
|
|
1044
|
+
}
|
|
1045
|
+
const parts = [`${text}>`];
|
|
1046
|
+
const inner = `${indent} `;
|
|
1047
|
+
if (typeof desc === "string") {
|
|
1048
|
+
parts.push(`
|
|
1049
|
+
${inner}<desc>${escapeXmlText(desc)}</desc>`);
|
|
1050
|
+
}
|
|
1051
|
+
if (hasDefault) {
|
|
1052
|
+
if (key.yfilesType !== null) {
|
|
1053
|
+
const value = column.meta.default;
|
|
1054
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1055
|
+
parts.push(`
|
|
1056
|
+
${inner}<default>`);
|
|
1057
|
+
writeXmlTree(value, `${inner} `, parts);
|
|
1058
|
+
parts.push(`
|
|
1059
|
+
${inner}</default>`);
|
|
1060
|
+
} else {
|
|
1061
|
+
const tree = [];
|
|
1062
|
+
writeXmlTree(value, inner, tree);
|
|
1063
|
+
parts.push(`
|
|
1064
|
+
${inner}<default>${tree.join("")}</default>`);
|
|
1065
|
+
}
|
|
1066
|
+
} else {
|
|
1067
|
+
parts.push(`
|
|
1068
|
+
${inner}<default>${escapeXmlText(defaultText(column, key.attrType) ?? "")}</default>`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
parts.push(`
|
|
1072
|
+
${indent}</key>
|
|
1073
|
+
`);
|
|
1074
|
+
return parts.join("");
|
|
1075
|
+
}
|
|
1076
|
+
function sanitizeEdgeIds(column, mode, edgeCount) {
|
|
1077
|
+
const out = new Array(edgeCount).fill(null);
|
|
1078
|
+
if (column === null) {
|
|
1079
|
+
return out;
|
|
1080
|
+
}
|
|
1081
|
+
const used = /* @__PURE__ */ new Set();
|
|
1082
|
+
const bad = [];
|
|
1083
|
+
for (let e = 0; e < edgeCount; e++) {
|
|
1084
|
+
if (!column.isSet(e)) {
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
const text = edgeIdText(column, e);
|
|
1088
|
+
if (isNmtoken(text)) {
|
|
1089
|
+
out[e] = text;
|
|
1090
|
+
used.add(text);
|
|
1091
|
+
} else {
|
|
1092
|
+
bad.push(e);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (bad.length > 0 && mode === "error") {
|
|
1096
|
+
throw new GraphFormatError(
|
|
1097
|
+
"E_INVALID_ID",
|
|
1098
|
+
`${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`,
|
|
1099
|
+
{ reason: "charset", charset: "nmtoken", count: bad.length, edge: bad[0] }
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
for (const e of bad) {
|
|
1103
|
+
const base = mangleNmtoken(edgeIdText(column, e));
|
|
1104
|
+
let candidate = base;
|
|
1105
|
+
for (let k = 2; used.has(candidate); k++) {
|
|
1106
|
+
candidate = `${base}_${k}`;
|
|
1107
|
+
}
|
|
1108
|
+
used.add(candidate);
|
|
1109
|
+
out[e] = candidate;
|
|
1110
|
+
}
|
|
1111
|
+
return out;
|
|
1112
|
+
}
|
|
1113
|
+
function* writeGraphml(snapshot, plan) {
|
|
1114
|
+
const ids = sanitizeIds(snapshot, "nmtoken", plan.options.sanitizeIds);
|
|
1115
|
+
const edgeIds = sanitizeEdgeIds(plan.edgeIdColumn, plan.options.sanitizeIds, snapshot.edgeCount);
|
|
1116
|
+
const i1 = plan.pretty ? " " : "";
|
|
1117
|
+
const i2 = plan.pretty ? " " : "";
|
|
1118
|
+
const i3 = plan.pretty ? " " : "";
|
|
1119
|
+
const step = plan.pretty ? " " : "";
|
|
1120
|
+
yield '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
1121
|
+
let root = `<graphml xmlns="${GRAPHML_NAMESPACE}"`;
|
|
1122
|
+
const namespaces = { ...plan.meta?.namespaces ?? {} };
|
|
1123
|
+
if ((plan.nodeColumns.some((c) => c.yfiles) || plan.edgeColumns.some((c) => c.yfiles) || plan.graphColumns.some((c) => c.yfiles)) && !Object.values(namespaces).includes(YFILES_NAMESPACE)) {
|
|
1124
|
+
namespaces.y = YFILES_NAMESPACE;
|
|
1125
|
+
}
|
|
1126
|
+
if (!Object.values(namespaces).includes(XSI_NAMESPACE)) {
|
|
1127
|
+
namespaces.xsi = XSI_NAMESPACE;
|
|
1128
|
+
}
|
|
1129
|
+
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
1130
|
+
root += ` xmlns:${prefix}="${escapeXmlAttribute(uri)}"`;
|
|
1131
|
+
}
|
|
1132
|
+
const xsiPrefix = Object.entries(namespaces).find(([, uri]) => uri === XSI_NAMESPACE)?.[0] ?? "xsi";
|
|
1133
|
+
root += ` ${xsiPrefix}:schemaLocation="${SCHEMA_LOCATION}">
|
|
1134
|
+
`;
|
|
1135
|
+
yield root;
|
|
1136
|
+
for (const key of plan.keys) {
|
|
1137
|
+
yield keyElement(key, i1);
|
|
1138
|
+
}
|
|
1139
|
+
if (plan.originalIdKey !== null) {
|
|
1140
|
+
yield `${i1}<key id="${plan.originalIdKey}" for="node" attr.name="${ORIGINAL_ID_ATTRIBUTE}" attr.type="string"/>
|
|
1141
|
+
`;
|
|
1142
|
+
}
|
|
1143
|
+
const graphId = plan.meta?.graphId ?? "G";
|
|
1144
|
+
yield `${i1}<graph id="${escapeXmlAttribute(graphId)}" edgedefault="${plan.edgedefault}">`;
|
|
1145
|
+
const { description } = snapshot.meta;
|
|
1146
|
+
if (description !== null) {
|
|
1147
|
+
yield `
|
|
1148
|
+
${i2}<desc>${escapeXmlText(description)}</desc>`;
|
|
1149
|
+
}
|
|
1150
|
+
const graphParts = [];
|
|
1151
|
+
for (const column of plan.graphColumns) {
|
|
1152
|
+
writeData(column, 0, i2, graphParts);
|
|
1153
|
+
}
|
|
1154
|
+
if (graphParts.length > 0) {
|
|
1155
|
+
yield graphParts.join("");
|
|
1156
|
+
}
|
|
1157
|
+
const { hierarchy } = plan;
|
|
1158
|
+
const nested = hierarchy.childList.length > 0;
|
|
1159
|
+
const stackNode = [];
|
|
1160
|
+
const stackCursor = [];
|
|
1161
|
+
const written = nested ? new Uint8Array(snapshot.nodeCount) : null;
|
|
1162
|
+
for (const rootNode of hierarchy.roots) {
|
|
1163
|
+
yield* writeNodeSubtree(rootNode, plan, ids, i2, step, stackNode, stackCursor, written);
|
|
1164
|
+
}
|
|
1165
|
+
const list = snapshot.edgeList();
|
|
1166
|
+
const { folding } = plan;
|
|
1167
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1168
|
+
if (folding.folded(e)) {
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
const parts = [`
|
|
1172
|
+
${i2}<edge`];
|
|
1173
|
+
const id = edgeIds[e];
|
|
1174
|
+
if (id !== null) {
|
|
1175
|
+
parts.push(` id="${escapeXmlAttribute(id)}"`);
|
|
1176
|
+
}
|
|
1177
|
+
parts.push(
|
|
1178
|
+
` source="${escapeXmlAttribute(String(ids.idAt(list.src[e])))}" target="${escapeXmlAttribute(String(ids.idAt(list.dst[e])))}"`
|
|
1179
|
+
);
|
|
1180
|
+
const directed = snapshot.directed && isDirectedEdge(folding, e);
|
|
1181
|
+
if (directed !== (plan.edgedefault === "directed")) {
|
|
1182
|
+
parts.push(` directed="${directed ? "true" : "false"}"`);
|
|
1183
|
+
}
|
|
1184
|
+
const sourcePort = portText(plan.sourcePortColumn, e);
|
|
1185
|
+
if (sourcePort !== null) {
|
|
1186
|
+
parts.push(` sourceport="${escapeXmlAttribute(sourcePort)}"`);
|
|
1187
|
+
}
|
|
1188
|
+
const targetPort = portText(plan.targetPortColumn, e);
|
|
1189
|
+
if (targetPort !== null) {
|
|
1190
|
+
parts.push(` targetport="${escapeXmlAttribute(targetPort)}"`);
|
|
1191
|
+
}
|
|
1192
|
+
const open = parts.length;
|
|
1193
|
+
if (plan.weight !== null) {
|
|
1194
|
+
const { keyId, attrType } = plan.weight;
|
|
1195
|
+
const weight = plan.weight.weights.text(e, attrType === "int" || attrType === "long");
|
|
1196
|
+
if (weight !== null) {
|
|
1197
|
+
parts.push(`
|
|
1198
|
+
${i3}<data key="${keyId}">${escapeXmlText(weight)}</data>`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
for (const column of plan.edgeColumns) {
|
|
1202
|
+
writeData(column, e, i3, parts);
|
|
1203
|
+
}
|
|
1204
|
+
if (parts.length === open) {
|
|
1205
|
+
parts.push("/>");
|
|
1206
|
+
} else {
|
|
1207
|
+
parts.splice(open, 0, ">");
|
|
1208
|
+
parts.push(`
|
|
1209
|
+
${i2}</edge>`);
|
|
1210
|
+
}
|
|
1211
|
+
yield parts.join("");
|
|
1212
|
+
}
|
|
1213
|
+
yield `
|
|
1214
|
+
${i1}</graph>
|
|
1215
|
+
</graphml>
|
|
1216
|
+
`;
|
|
1217
|
+
}
|
|
1218
|
+
function portText(column, e) {
|
|
1219
|
+
if (column === null || !column.isSet(e)) {
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
const value = column.value(e);
|
|
1223
|
+
return typeof value === "string" ? value : String(value);
|
|
1224
|
+
}
|
|
1225
|
+
function* writeNodeSubtree(root, plan, ids, indent, step, stackNode, stackCursor, written) {
|
|
1226
|
+
const { hierarchy } = plan;
|
|
1227
|
+
const hasChildren = (u) => hierarchy.childStart[u + 1] > hierarchy.childStart[u];
|
|
1228
|
+
const openNode = (u, depth) => {
|
|
1229
|
+
const pad = indent + step.repeat(depth * 2);
|
|
1230
|
+
const parts = [`
|
|
1231
|
+
${pad}<node id="${escapeXmlAttribute(String(ids.idAt(u)))}"`];
|
|
1232
|
+
const open = parts.length;
|
|
1233
|
+
if (plan.originalIdKey !== null && ids.isChanged(u)) {
|
|
1234
|
+
parts.push(
|
|
1235
|
+
`
|
|
1236
|
+
${pad}${step}<data key="${plan.originalIdKey}">${escapeXmlText(String(ids.originalAt(u)))}</data>`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
for (const column of plan.nodeColumns) {
|
|
1240
|
+
writeData(column, u, pad + step, parts);
|
|
1241
|
+
}
|
|
1242
|
+
if (hasChildren(u)) {
|
|
1243
|
+
parts.splice(open, 0, ">");
|
|
1244
|
+
parts.push(
|
|
1245
|
+
`
|
|
1246
|
+
${pad}${step}<graph id="${escapeXmlAttribute(`${String(ids.idAt(u))}:`)}" edgedefault="${plan.edgedefault}">`
|
|
1247
|
+
);
|
|
1248
|
+
return parts.join("");
|
|
1249
|
+
}
|
|
1250
|
+
if (parts.length === open) {
|
|
1251
|
+
parts.push("/>");
|
|
1252
|
+
} else {
|
|
1253
|
+
parts.splice(open, 0, ">");
|
|
1254
|
+
parts.push(`
|
|
1255
|
+
${pad}</node>`);
|
|
1256
|
+
}
|
|
1257
|
+
return parts.join("");
|
|
1258
|
+
};
|
|
1259
|
+
const closeNode = (depth) => {
|
|
1260
|
+
const pad = indent + step.repeat(depth * 2);
|
|
1261
|
+
return `
|
|
1262
|
+
${pad}${step}</graph>
|
|
1263
|
+
${pad}</node>`;
|
|
1264
|
+
};
|
|
1265
|
+
if (written !== null) {
|
|
1266
|
+
written[root] = 1;
|
|
1267
|
+
}
|
|
1268
|
+
yield openNode(root, 0);
|
|
1269
|
+
if (!hasChildren(root)) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
stackNode.push(root);
|
|
1273
|
+
stackCursor.push(hierarchy.childStart[root]);
|
|
1274
|
+
while (stackNode.length > 0) {
|
|
1275
|
+
const top = stackNode.length - 1;
|
|
1276
|
+
const u = stackNode[top];
|
|
1277
|
+
const cursor = stackCursor[top];
|
|
1278
|
+
if (cursor >= hierarchy.childStart[u + 1]) {
|
|
1279
|
+
stackNode.pop();
|
|
1280
|
+
stackCursor.pop();
|
|
1281
|
+
yield closeNode(top);
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
stackCursor[top] = cursor + 1;
|
|
1285
|
+
const v = hierarchy.childList[cursor];
|
|
1286
|
+
if (written !== null && written[v] === 1) {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (written !== null) {
|
|
1290
|
+
written[v] = 1;
|
|
1291
|
+
}
|
|
1292
|
+
yield openNode(v, top + 1);
|
|
1293
|
+
if (hasChildren(v)) {
|
|
1294
|
+
stackNode.push(v);
|
|
1295
|
+
stackCursor.push(hierarchy.childStart[v]);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const graphmlExporter = Object.freeze({
|
|
1300
|
+
format: FORMAT,
|
|
1301
|
+
capabilities: CAPABILITIES,
|
|
1302
|
+
/**
|
|
1303
|
+
* Pre-flight: every loss the export would incur.
|
|
1304
|
+
* @param snapshot - the snapshot
|
|
1305
|
+
* @param options - format-specific and common options
|
|
1306
|
+
* @returns the notes
|
|
1307
|
+
*/
|
|
1308
|
+
check(snapshot, options) {
|
|
1309
|
+
const plan = planExport(snapshot, resolveExportOptions(options), resolveFormatOptions(options));
|
|
1310
|
+
return Object.freeze(plan.notes);
|
|
1311
|
+
},
|
|
1312
|
+
/**
|
|
1313
|
+
* Write the document as UTF-8 chunks.
|
|
1314
|
+
* @param snapshot - the snapshot
|
|
1315
|
+
* @param options - format-specific and common options
|
|
1316
|
+
* @returns the chunks
|
|
1317
|
+
*/
|
|
1318
|
+
export(snapshot, options) {
|
|
1319
|
+
const plan = planExport(snapshot, resolveExportOptions(options), resolveFormatOptions(options));
|
|
1320
|
+
return encodeChunks(writeGraphml(snapshot, plan));
|
|
1321
|
+
},
|
|
1322
|
+
/**
|
|
1323
|
+
* Write the document as one string.
|
|
1324
|
+
* @param snapshot - the snapshot
|
|
1325
|
+
* @param options - format-specific and common options
|
|
1326
|
+
* @returns the document
|
|
1327
|
+
*/
|
|
1328
|
+
exportToString(snapshot, options) {
|
|
1329
|
+
const plan = planExport(snapshot, resolveExportOptions(options), resolveFormatOptions(options));
|
|
1330
|
+
return joinText(writeGraphml(snapshot, plan));
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
const FORMAT_DEFAULTS = { ids: "canonical", defaultDirected: false, weightFrom: "weight" };
|
|
1334
|
+
const SNIFF_BYTES = 4096;
|
|
1335
|
+
const YFILES_MODES = /* @__PURE__ */ new Set(["json", "skip"]);
|
|
1336
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
1337
|
+
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
1338
|
+
"ids",
|
|
1339
|
+
"addMissingNodes",
|
|
1340
|
+
"duplicateEdges",
|
|
1341
|
+
"selfLoops",
|
|
1342
|
+
"onMixedDirection",
|
|
1343
|
+
"defaultDirected",
|
|
1344
|
+
"weightFrom",
|
|
1345
|
+
"weightDtype",
|
|
1346
|
+
"long",
|
|
1347
|
+
"restoreMangledIds",
|
|
1348
|
+
"hyperedges",
|
|
1349
|
+
"errorLimit",
|
|
1350
|
+
"signal",
|
|
1351
|
+
"onProgress"
|
|
1352
|
+
]);
|
|
1353
|
+
class IndexFlags {
|
|
1354
|
+
constructor() {
|
|
1355
|
+
this.words = new Uint32Array(64);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Whether an index is marked.
|
|
1359
|
+
* @param index - the node index
|
|
1360
|
+
* @returns true when set
|
|
1361
|
+
*/
|
|
1362
|
+
has(index) {
|
|
1363
|
+
const w = index >>> 5;
|
|
1364
|
+
return w < this.words.length && (this.words[w] >>> (index & 31) & 1) === 1;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Mark an index.
|
|
1368
|
+
* @param index - the node index
|
|
1369
|
+
*/
|
|
1370
|
+
add(index) {
|
|
1371
|
+
const w = index >>> 5;
|
|
1372
|
+
if (w >= this.words.length) {
|
|
1373
|
+
let size = this.words.length * 2;
|
|
1374
|
+
while (size <= w) {
|
|
1375
|
+
size *= 2;
|
|
1376
|
+
}
|
|
1377
|
+
const grown = new Uint32Array(size);
|
|
1378
|
+
grown.set(this.words);
|
|
1379
|
+
this.words = grown;
|
|
1380
|
+
}
|
|
1381
|
+
this.words[w] |= 1 << (index & 31);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
class GraphmlReader {
|
|
1385
|
+
/**
|
|
1386
|
+
* Create a reader for one import call.
|
|
1387
|
+
* @param sink - the sink
|
|
1388
|
+
* @param report - the report
|
|
1389
|
+
* @param options - the resolved common options
|
|
1390
|
+
* @param yfilesMode - the resolved yfiles option
|
|
1391
|
+
*/
|
|
1392
|
+
constructor(sink, report, options, yfilesMode) {
|
|
1393
|
+
this.ctx = [];
|
|
1394
|
+
this.keys = /* @__PURE__ */ new Map();
|
|
1395
|
+
this.graphs = [];
|
|
1396
|
+
this.nodes = [];
|
|
1397
|
+
this.declared = new IndexFlags();
|
|
1398
|
+
this.edgeIds = /* @__PURE__ */ new Set();
|
|
1399
|
+
this.elementsSinceCheck = 0;
|
|
1400
|
+
this.restored = /* @__PURE__ */ new Map();
|
|
1401
|
+
this.graphColumnNames = /* @__PURE__ */ new Set();
|
|
1402
|
+
this.pendingKey = null;
|
|
1403
|
+
this.edge = null;
|
|
1404
|
+
this.hyperedge = null;
|
|
1405
|
+
this.content = {
|
|
1406
|
+
key: null,
|
|
1407
|
+
pendingKey: null,
|
|
1408
|
+
domain: "node",
|
|
1409
|
+
text: "",
|
|
1410
|
+
tree: null,
|
|
1411
|
+
line: 0
|
|
1412
|
+
};
|
|
1413
|
+
this.descTarget = null;
|
|
1414
|
+
this.descText = "";
|
|
1415
|
+
this.description = null;
|
|
1416
|
+
this.graphSeen = false;
|
|
1417
|
+
this.graphId = null;
|
|
1418
|
+
this.edgedefault = null;
|
|
1419
|
+
this.namespaces = {};
|
|
1420
|
+
this.weightOrigin = null;
|
|
1421
|
+
this.yfilesLossNoted = false;
|
|
1422
|
+
this.hyperedgeCount = 0;
|
|
1423
|
+
this.edgeIdHandle = INVALID_INDEX;
|
|
1424
|
+
this.sourcePortHandle = INVALID_INDEX;
|
|
1425
|
+
this.targetPortHandle = INVALID_INDEX;
|
|
1426
|
+
this.parentHandle = INVALID_INDEX;
|
|
1427
|
+
this.hubHandle = INVALID_INDEX;
|
|
1428
|
+
this.sink = sink;
|
|
1429
|
+
this.report = report;
|
|
1430
|
+
this.options = options;
|
|
1431
|
+
this.yfilesMode = yfilesMode;
|
|
1432
|
+
this.coercer = new IdCoercer(options.ids);
|
|
1433
|
+
this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
1434
|
+
}
|
|
1435
|
+
// ------------------------------------------------------------------ tokenizer events
|
|
1436
|
+
/**
|
|
1437
|
+
* An element starts.
|
|
1438
|
+
* @param name - the element name
|
|
1439
|
+
* @param attrs - its attributes
|
|
1440
|
+
* @param line - the line
|
|
1441
|
+
*/
|
|
1442
|
+
start(name, attrs, line) {
|
|
1443
|
+
const parent = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
|
|
1444
|
+
switch (parent) {
|
|
1445
|
+
case 9:
|
|
1446
|
+
this.captureStart(name, attrs);
|
|
1447
|
+
return;
|
|
1448
|
+
case 10:
|
|
1449
|
+
case 1:
|
|
1450
|
+
this.ctx.push(
|
|
1451
|
+
10
|
|
1452
|
+
/* Skip */
|
|
1453
|
+
);
|
|
1454
|
+
return;
|
|
1455
|
+
case 8:
|
|
1456
|
+
case 3:
|
|
1457
|
+
this.captureStart(name, attrs);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const local = localName(name);
|
|
1461
|
+
switch (parent) {
|
|
1462
|
+
case null:
|
|
1463
|
+
if (local !== "graphml") {
|
|
1464
|
+
this.report.fail(GRAPHML_ISSUE.NOT_GRAPHML, `the root element is <${name}>, not <graphml>`, {
|
|
1465
|
+
line
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
this.readRoot(attrs);
|
|
1469
|
+
this.ctx.push(
|
|
1470
|
+
0
|
|
1471
|
+
/* Graphml */
|
|
1472
|
+
);
|
|
1473
|
+
return;
|
|
1474
|
+
case 0:
|
|
1475
|
+
this.startInGraphml(name, local, attrs, line);
|
|
1476
|
+
return;
|
|
1477
|
+
case 2:
|
|
1478
|
+
this.startInKey(name, local, line);
|
|
1479
|
+
return;
|
|
1480
|
+
case 4:
|
|
1481
|
+
this.startInGraph(name, local, attrs, line);
|
|
1482
|
+
return;
|
|
1483
|
+
case 5:
|
|
1484
|
+
this.startInNode(name, local, attrs, line);
|
|
1485
|
+
return;
|
|
1486
|
+
case 6:
|
|
1487
|
+
this.startInEdge(name, local, attrs, line);
|
|
1488
|
+
return;
|
|
1489
|
+
case 7:
|
|
1490
|
+
this.startInHyperedge(name, local, attrs, line);
|
|
1491
|
+
return;
|
|
1492
|
+
default:
|
|
1493
|
+
this.unknownElement(name, line);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* An element ends.
|
|
1498
|
+
* @param name - the element name
|
|
1499
|
+
* @param _line - the line (unused: issues of an element cite its start line)
|
|
1500
|
+
*/
|
|
1501
|
+
end(name, _line) {
|
|
1502
|
+
const ctx = this.ctx.pop();
|
|
1503
|
+
switch (ctx) {
|
|
1504
|
+
case 9:
|
|
1505
|
+
this.captureEnd();
|
|
1506
|
+
return;
|
|
1507
|
+
case 10:
|
|
1508
|
+
case 0:
|
|
1509
|
+
case void 0:
|
|
1510
|
+
return;
|
|
1511
|
+
case 1:
|
|
1512
|
+
this.finishDesc();
|
|
1513
|
+
return;
|
|
1514
|
+
case 2:
|
|
1515
|
+
this.finishKey();
|
|
1516
|
+
return;
|
|
1517
|
+
case 3:
|
|
1518
|
+
this.finishDefault();
|
|
1519
|
+
return;
|
|
1520
|
+
case 8:
|
|
1521
|
+
this.finishData();
|
|
1522
|
+
return;
|
|
1523
|
+
case 5:
|
|
1524
|
+
this.finishNode();
|
|
1525
|
+
return;
|
|
1526
|
+
case 6:
|
|
1527
|
+
this.finishEdge();
|
|
1528
|
+
return;
|
|
1529
|
+
case 7:
|
|
1530
|
+
this.finishHyperedge();
|
|
1531
|
+
return;
|
|
1532
|
+
case 4:
|
|
1533
|
+
this.graphs.pop();
|
|
1534
|
+
return;
|
|
1535
|
+
default: {
|
|
1536
|
+
const value = ctx;
|
|
1537
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown reader state ${String(value)}`, { name });
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Character data.
|
|
1543
|
+
* @param text - the text
|
|
1544
|
+
* @param line - the line
|
|
1545
|
+
*/
|
|
1546
|
+
text(text, line) {
|
|
1547
|
+
const ctx = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
|
|
1548
|
+
switch (ctx) {
|
|
1549
|
+
case 9:
|
|
1550
|
+
this.content.tree?.text(text);
|
|
1551
|
+
return;
|
|
1552
|
+
case 8:
|
|
1553
|
+
case 3:
|
|
1554
|
+
if (this.content.tree === null) {
|
|
1555
|
+
this.content.text += text;
|
|
1556
|
+
} else {
|
|
1557
|
+
this.content.tree.text(text);
|
|
1558
|
+
}
|
|
1559
|
+
return;
|
|
1560
|
+
case 1:
|
|
1561
|
+
this.descText += text;
|
|
1562
|
+
return;
|
|
1563
|
+
case 10:
|
|
1564
|
+
return;
|
|
1565
|
+
default:
|
|
1566
|
+
if (!isWhitespace(text)) {
|
|
1567
|
+
this.report.warnOnce(
|
|
1568
|
+
"validation-error",
|
|
1569
|
+
GRAPHML_ISSUE.STRAY_TEXT,
|
|
1570
|
+
"text where GraphML allows only elements was ignored",
|
|
1571
|
+
{ line }
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* After the last event: the document must have held a graph; the metadata is written.
|
|
1578
|
+
*/
|
|
1579
|
+
finish() {
|
|
1580
|
+
if (!this.graphSeen) {
|
|
1581
|
+
this.report.fail(GRAPHML_ISSUE.NO_GRAPH, "the document has no <graph> element");
|
|
1582
|
+
}
|
|
1583
|
+
const meta = { graphId: this.graphId, edgedefault: this.edgedefault, namespaces: this.namespaces };
|
|
1584
|
+
this.sink.setMeta({
|
|
1585
|
+
sourceFormat: FORMAT,
|
|
1586
|
+
description: this.description ?? void 0,
|
|
1587
|
+
weightOrigin: this.weightOrigin === null ? void 0 : {
|
|
1588
|
+
format: FORMAT,
|
|
1589
|
+
id: this.weightOrigin.id,
|
|
1590
|
+
title: this.weightOrigin.title,
|
|
1591
|
+
type: this.weightOrigin.type,
|
|
1592
|
+
namespace: null
|
|
1593
|
+
},
|
|
1594
|
+
extra: { [META_KEY]: meta }
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
// ------------------------------------------------------------------ structure
|
|
1598
|
+
/**
|
|
1599
|
+
* Record the root element's namespace declarations.
|
|
1600
|
+
* @param attrs - the root attributes
|
|
1601
|
+
*/
|
|
1602
|
+
readRoot(attrs) {
|
|
1603
|
+
for (const [key, value] of attrs) {
|
|
1604
|
+
if (key.startsWith("xmlns:") && value !== GRAPHML_NAMESPACE && value !== XSI_NAMESPACE) {
|
|
1605
|
+
this.namespaces[key.slice(6)] = value;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* A child of `<graphml>`.
|
|
1611
|
+
* @param name - the element name
|
|
1612
|
+
* @param local - its local name
|
|
1613
|
+
* @param attrs - its attributes
|
|
1614
|
+
* @param line - the line
|
|
1615
|
+
*/
|
|
1616
|
+
startInGraphml(name, local, attrs, line) {
|
|
1617
|
+
switch (local) {
|
|
1618
|
+
case "desc":
|
|
1619
|
+
this.beginDesc("graph");
|
|
1620
|
+
return;
|
|
1621
|
+
case "key":
|
|
1622
|
+
this.beginKey(attrs, line);
|
|
1623
|
+
return;
|
|
1624
|
+
case "graph":
|
|
1625
|
+
this.beginGraph(attrs, line, INVALID_INDEX);
|
|
1626
|
+
return;
|
|
1627
|
+
case "data":
|
|
1628
|
+
this.beginData(attrs, line, "graph");
|
|
1629
|
+
return;
|
|
1630
|
+
default:
|
|
1631
|
+
this.unknownElement(name, line);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* A child of `<key>`.
|
|
1636
|
+
* @param name - the element name
|
|
1637
|
+
* @param local - its local name
|
|
1638
|
+
* @param line - the line
|
|
1639
|
+
*/
|
|
1640
|
+
startInKey(name, local, line) {
|
|
1641
|
+
switch (local) {
|
|
1642
|
+
case "desc":
|
|
1643
|
+
this.beginDesc("key");
|
|
1644
|
+
return;
|
|
1645
|
+
case "default":
|
|
1646
|
+
this.content.key = null;
|
|
1647
|
+
this.content.pendingKey = this.pendingKey;
|
|
1648
|
+
this.content.text = "";
|
|
1649
|
+
this.content.tree = null;
|
|
1650
|
+
this.content.line = line;
|
|
1651
|
+
this.ctx.push(
|
|
1652
|
+
3
|
|
1653
|
+
/* Default */
|
|
1654
|
+
);
|
|
1655
|
+
return;
|
|
1656
|
+
default:
|
|
1657
|
+
this.unknownElement(name, line);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* A child of `<graph>`.
|
|
1662
|
+
* @param name - the element name
|
|
1663
|
+
* @param local - its local name
|
|
1664
|
+
* @param attrs - its attributes
|
|
1665
|
+
* @param line - the line
|
|
1666
|
+
*/
|
|
1667
|
+
startInGraph(name, local, attrs, line) {
|
|
1668
|
+
switch (local) {
|
|
1669
|
+
case "desc":
|
|
1670
|
+
if (this.graphs.length === 1) {
|
|
1671
|
+
this.beginDesc("graph");
|
|
1672
|
+
} else {
|
|
1673
|
+
this.ctx.push(
|
|
1674
|
+
10
|
|
1675
|
+
/* Skip */
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
return;
|
|
1679
|
+
case "data":
|
|
1680
|
+
if (this.graphs.length === 1) {
|
|
1681
|
+
this.beginData(attrs, line, "graph");
|
|
1682
|
+
} else {
|
|
1683
|
+
this.report.warnOnce(
|
|
1684
|
+
"unsupported",
|
|
1685
|
+
GRAPHML_ISSUE.NESTED_GRAPH_DATA,
|
|
1686
|
+
"data of a nested <graph> cannot be kept; only the top-level graph has attributes",
|
|
1687
|
+
{ line }
|
|
1688
|
+
);
|
|
1689
|
+
this.ctx.push(
|
|
1690
|
+
10
|
|
1691
|
+
/* Skip */
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
return;
|
|
1695
|
+
case "node":
|
|
1696
|
+
this.beginNode(attrs, line);
|
|
1697
|
+
return;
|
|
1698
|
+
case "edge":
|
|
1699
|
+
this.beginEdge(attrs, line);
|
|
1700
|
+
return;
|
|
1701
|
+
case "hyperedge":
|
|
1702
|
+
this.beginHyperedge(attrs, line);
|
|
1703
|
+
return;
|
|
1704
|
+
case "locator":
|
|
1705
|
+
this.report.warnOnce(
|
|
1706
|
+
"unsupported",
|
|
1707
|
+
GRAPHML_ISSUE.LOCATOR_DROPPED,
|
|
1708
|
+
"<locator> references are not followed",
|
|
1709
|
+
{ line }
|
|
1710
|
+
);
|
|
1711
|
+
this.ctx.push(
|
|
1712
|
+
10
|
|
1713
|
+
/* Skip */
|
|
1714
|
+
);
|
|
1715
|
+
return;
|
|
1716
|
+
default:
|
|
1717
|
+
this.unknownElement(name, line);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* A child of `<node>`.
|
|
1722
|
+
* @param name - the element name
|
|
1723
|
+
* @param local - its local name
|
|
1724
|
+
* @param attrs - its attributes
|
|
1725
|
+
* @param line - the line
|
|
1726
|
+
*/
|
|
1727
|
+
startInNode(name, local, attrs, line) {
|
|
1728
|
+
switch (local) {
|
|
1729
|
+
case "desc":
|
|
1730
|
+
this.report.warnOnce(
|
|
1731
|
+
"unsupported",
|
|
1732
|
+
GRAPHML_ISSUE.DESC_DROPPED,
|
|
1733
|
+
"node and edge <desc> texts are not kept",
|
|
1734
|
+
{
|
|
1735
|
+
line
|
|
1736
|
+
}
|
|
1737
|
+
);
|
|
1738
|
+
this.ctx.push(
|
|
1739
|
+
10
|
|
1740
|
+
/* Skip */
|
|
1741
|
+
);
|
|
1742
|
+
return;
|
|
1743
|
+
case "data":
|
|
1744
|
+
this.beginData(attrs, line, "node");
|
|
1745
|
+
return;
|
|
1746
|
+
case "port":
|
|
1747
|
+
this.report.warnOnce(
|
|
1748
|
+
"unsupported",
|
|
1749
|
+
GRAPHML_ISSUE.PORT_DECLARATION,
|
|
1750
|
+
"<port> declarations are not kept; sourceport / targetport edge attributes are",
|
|
1751
|
+
{ line }
|
|
1752
|
+
);
|
|
1753
|
+
this.ctx.push(
|
|
1754
|
+
10
|
|
1755
|
+
/* Skip */
|
|
1756
|
+
);
|
|
1757
|
+
return;
|
|
1758
|
+
case "graph": {
|
|
1759
|
+
const node = this.nodes[this.nodes.length - 1];
|
|
1760
|
+
this.materializeNode(node);
|
|
1761
|
+
this.beginGraph(attrs, line, node.index);
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
case "locator":
|
|
1765
|
+
this.report.warnOnce(
|
|
1766
|
+
"unsupported",
|
|
1767
|
+
GRAPHML_ISSUE.LOCATOR_DROPPED,
|
|
1768
|
+
"<locator> references are not followed",
|
|
1769
|
+
{ line }
|
|
1770
|
+
);
|
|
1771
|
+
this.ctx.push(
|
|
1772
|
+
10
|
|
1773
|
+
/* Skip */
|
|
1774
|
+
);
|
|
1775
|
+
return;
|
|
1776
|
+
default:
|
|
1777
|
+
this.unknownElement(name, line);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* A child of `<edge>`.
|
|
1782
|
+
* @param name - the element name
|
|
1783
|
+
* @param local - its local name
|
|
1784
|
+
* @param attrs - its attributes
|
|
1785
|
+
* @param line - the line
|
|
1786
|
+
*/
|
|
1787
|
+
startInEdge(name, local, attrs, line) {
|
|
1788
|
+
switch (local) {
|
|
1789
|
+
case "desc":
|
|
1790
|
+
this.report.warnOnce(
|
|
1791
|
+
"unsupported",
|
|
1792
|
+
GRAPHML_ISSUE.DESC_DROPPED,
|
|
1793
|
+
"node and edge <desc> texts are not kept",
|
|
1794
|
+
{
|
|
1795
|
+
line
|
|
1796
|
+
}
|
|
1797
|
+
);
|
|
1798
|
+
this.ctx.push(
|
|
1799
|
+
10
|
|
1800
|
+
/* Skip */
|
|
1801
|
+
);
|
|
1802
|
+
return;
|
|
1803
|
+
case "data":
|
|
1804
|
+
this.beginData(attrs, line, "edge");
|
|
1805
|
+
return;
|
|
1806
|
+
default:
|
|
1807
|
+
this.unknownElement(name, line);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* A child of `<hyperedge>` (under the star / clique policies).
|
|
1812
|
+
* @param name - the element name
|
|
1813
|
+
* @param local - its local name
|
|
1814
|
+
* @param attrs - its attributes
|
|
1815
|
+
* @param line - the line
|
|
1816
|
+
*/
|
|
1817
|
+
startInHyperedge(name, local, attrs, line) {
|
|
1818
|
+
switch (local) {
|
|
1819
|
+
case "endpoint":
|
|
1820
|
+
this.readEndpoint(attrs, line);
|
|
1821
|
+
this.ctx.push(
|
|
1822
|
+
10
|
|
1823
|
+
/* Skip */
|
|
1824
|
+
);
|
|
1825
|
+
return;
|
|
1826
|
+
case "data":
|
|
1827
|
+
this.report.warnOnce(
|
|
1828
|
+
"unsupported",
|
|
1829
|
+
GRAPHML_ISSUE.HYPEREDGE_DATA_DROPPED,
|
|
1830
|
+
"hyperedge data are not kept by the star / clique expansions",
|
|
1831
|
+
{ line }
|
|
1832
|
+
);
|
|
1833
|
+
this.ctx.push(
|
|
1834
|
+
10
|
|
1835
|
+
/* Skip */
|
|
1836
|
+
);
|
|
1837
|
+
return;
|
|
1838
|
+
case "desc":
|
|
1839
|
+
this.ctx.push(
|
|
1840
|
+
10
|
|
1841
|
+
/* Skip */
|
|
1842
|
+
);
|
|
1843
|
+
return;
|
|
1844
|
+
default:
|
|
1845
|
+
this.unknownElement(name, line);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Report an element the schema does not define at this place and skip its subtree.
|
|
1850
|
+
* @param name - the element name
|
|
1851
|
+
* @param line - the line
|
|
1852
|
+
*/
|
|
1853
|
+
unknownElement(name, line) {
|
|
1854
|
+
this.report.warnOnce(
|
|
1855
|
+
"unsupported",
|
|
1856
|
+
GRAPHML_ISSUE.UNKNOWN_ELEMENT,
|
|
1857
|
+
`element <${name}> is not GraphML and was skipped`,
|
|
1858
|
+
{
|
|
1859
|
+
line,
|
|
1860
|
+
element: name
|
|
1861
|
+
}
|
|
1862
|
+
);
|
|
1863
|
+
this.ctx.push(
|
|
1864
|
+
10
|
|
1865
|
+
/* Skip */
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Open a `<desc>`.
|
|
1870
|
+
* @param target - what the text describes
|
|
1871
|
+
*/
|
|
1872
|
+
beginDesc(target) {
|
|
1873
|
+
this.descTarget = target;
|
|
1874
|
+
this.descText = "";
|
|
1875
|
+
this.ctx.push(
|
|
1876
|
+
1
|
|
1877
|
+
/* Desc */
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
/** Close a `<desc>`. */
|
|
1881
|
+
finishDesc() {
|
|
1882
|
+
const text = this.descText.trim();
|
|
1883
|
+
if (this.descTarget === "key" && this.pendingKey !== null) {
|
|
1884
|
+
this.pendingKey.desc = text;
|
|
1885
|
+
} else if (this.descTarget === "graph" && text.length > 0) {
|
|
1886
|
+
this.description = text;
|
|
1887
|
+
}
|
|
1888
|
+
this.descTarget = null;
|
|
1889
|
+
this.descText = "";
|
|
1890
|
+
}
|
|
1891
|
+
// ------------------------------------------------------------------ graphs
|
|
1892
|
+
/**
|
|
1893
|
+
* Open a `<graph>`: the top-level one sets the sink's direction (rule 1 of design section
|
|
1894
|
+
* 8.4); a nested one records its container as the parent of its nodes.
|
|
1895
|
+
* @param attrs - the graph attributes
|
|
1896
|
+
* @param line - the line
|
|
1897
|
+
* @param parent - the containing node's index, or INVALID_INDEX
|
|
1898
|
+
*/
|
|
1899
|
+
beginGraph(attrs, line, parent) {
|
|
1900
|
+
const top = this.graphs.length === 0;
|
|
1901
|
+
const edgedefault = attrs.get("edgedefault");
|
|
1902
|
+
let directed;
|
|
1903
|
+
if (edgedefault === "directed" || edgedefault === "undirected") {
|
|
1904
|
+
directed = edgedefault === "directed";
|
|
1905
|
+
} else if (edgedefault === void 0) {
|
|
1906
|
+
directed = top ? this.options.defaultDirected : this.graphs[this.graphs.length - 1].directed;
|
|
1907
|
+
this.report.warning(
|
|
1908
|
+
"validation-error",
|
|
1909
|
+
GRAPHML_ISSUE.EDGEDEFAULT_MISSING,
|
|
1910
|
+
`<graph> has no edgedefault; read as ${directed ? "directed" : "undirected"}`,
|
|
1911
|
+
{ line }
|
|
1912
|
+
);
|
|
1913
|
+
} else {
|
|
1914
|
+
directed = top ? this.options.defaultDirected : this.graphs[this.graphs.length - 1].directed;
|
|
1915
|
+
this.report.error(
|
|
1916
|
+
"validation-error",
|
|
1917
|
+
GRAPHML_ISSUE.INVALID_EDGEDEFAULT,
|
|
1918
|
+
`edgedefault "${edgedefault}" is neither directed nor undirected; read as ${directed ? "directed" : "undirected"}`,
|
|
1919
|
+
{ line }
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
if (top && this.graphSeen) {
|
|
1923
|
+
this.report.warnOnce(
|
|
1924
|
+
"unsupported",
|
|
1925
|
+
GRAPHML_ISSUE.MULTIPLE_GRAPHS,
|
|
1926
|
+
"the document holds more than one top-level <graph>; their nodes and edges are merged into one graph",
|
|
1927
|
+
{ line, element: attrs.get("id") ?? null }
|
|
1928
|
+
);
|
|
1929
|
+
} else if (top) {
|
|
1930
|
+
this.graphSeen = true;
|
|
1931
|
+
this.graphId = attrs.get("id") ?? null;
|
|
1932
|
+
this.edgedefault = directed ? "directed" : "undirected";
|
|
1933
|
+
this.direction.setHeader(directed, { line });
|
|
1934
|
+
const nodes = this.hint(attrs.get("parse.nodes"), "parse.nodes", line);
|
|
1935
|
+
const edges = this.hint(attrs.get("parse.edges"), "parse.edges", line);
|
|
1936
|
+
if (nodes !== null || edges !== null) {
|
|
1937
|
+
try {
|
|
1938
|
+
this.sink.reserve(nodes ?? void 0, edges ?? void 0);
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
if (!(err instanceof GraphFormatError)) {
|
|
1941
|
+
throw err;
|
|
1942
|
+
}
|
|
1943
|
+
this.report.warning(
|
|
1944
|
+
"validation-error",
|
|
1945
|
+
GRAPHML_ISSUE.COUNT_HINT,
|
|
1946
|
+
`count hint refused: ${err.message}`,
|
|
1947
|
+
{
|
|
1948
|
+
line
|
|
1949
|
+
}
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
this.graphs.push({ directed, parent });
|
|
1955
|
+
this.ctx.push(
|
|
1956
|
+
4
|
|
1957
|
+
/* Graph */
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* A `parse.nodes` / `parse.edges` hint the sink can reserve; anything else is reported and ignored.
|
|
1962
|
+
* @param text - the attribute text, or undefined
|
|
1963
|
+
* @param name - the attribute name
|
|
1964
|
+
* @param line - the line
|
|
1965
|
+
* @returns the count, or null
|
|
1966
|
+
*/
|
|
1967
|
+
hint(text, name, line) {
|
|
1968
|
+
if (text === void 0) {
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
const n = hint(text);
|
|
1972
|
+
if (n === null || n > MAX_COUNT) {
|
|
1973
|
+
this.report.warning(
|
|
1974
|
+
"validation-error",
|
|
1975
|
+
GRAPHML_ISSUE.COUNT_HINT,
|
|
1976
|
+
`<graph ${name}="${text}"> is not a count the sink can reserve; ignored`,
|
|
1977
|
+
{ line, element: name }
|
|
1978
|
+
);
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1981
|
+
return n;
|
|
1982
|
+
}
|
|
1983
|
+
// ------------------------------------------------------------------ keys
|
|
1984
|
+
/**
|
|
1985
|
+
* Open a `<key>`; the column is declared at `</key>` once its default is known.
|
|
1986
|
+
* @param attrs - the key attributes
|
|
1987
|
+
* @param line - the line
|
|
1988
|
+
*/
|
|
1989
|
+
beginKey(attrs, line) {
|
|
1990
|
+
const id = attrs.get("id") ?? null;
|
|
1991
|
+
const forText = attrs.get("for") ?? "all";
|
|
1992
|
+
let domains;
|
|
1993
|
+
switch (forText) {
|
|
1994
|
+
case "all":
|
|
1995
|
+
domains = ["graph", "node", "edge"];
|
|
1996
|
+
break;
|
|
1997
|
+
case "graph":
|
|
1998
|
+
case "node":
|
|
1999
|
+
case "edge":
|
|
2000
|
+
domains = [forText];
|
|
2001
|
+
break;
|
|
2002
|
+
case "hyperedge":
|
|
2003
|
+
case "port":
|
|
2004
|
+
case "endpoint":
|
|
2005
|
+
this.report.warning(
|
|
2006
|
+
"unsupported",
|
|
2007
|
+
GRAPHML_ISSUE.KEY_DOMAIN_UNSUPPORTED,
|
|
2008
|
+
`key "${id ?? ""}" is declared for ${forText} elements, which are not kept`,
|
|
2009
|
+
{ line, element: id }
|
|
2010
|
+
);
|
|
2011
|
+
domains = [];
|
|
2012
|
+
break;
|
|
2013
|
+
default:
|
|
2014
|
+
this.report.error(
|
|
2015
|
+
"validation-error",
|
|
2016
|
+
GRAPHML_ISSUE.KEY_FOR_INVALID,
|
|
2017
|
+
`key "${id ?? ""}" has an unknown for="${forText}"`,
|
|
2018
|
+
{ line, element: id }
|
|
2019
|
+
);
|
|
2020
|
+
domains = [];
|
|
2021
|
+
}
|
|
2022
|
+
this.pendingKey = {
|
|
2023
|
+
id,
|
|
2024
|
+
attrName: attrs.get("attr.name") ?? null,
|
|
2025
|
+
attrType: attrs.get("attr.type") ?? null,
|
|
2026
|
+
yfilesType: attrs.get("yfiles.type") ?? null,
|
|
2027
|
+
domains,
|
|
2028
|
+
line,
|
|
2029
|
+
desc: "",
|
|
2030
|
+
defaultText: null,
|
|
2031
|
+
defaultTree: void 0
|
|
2032
|
+
};
|
|
2033
|
+
this.ctx.push(
|
|
2034
|
+
2
|
|
2035
|
+
/* Key */
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
/** Close a `<key>`: declare its columns. */
|
|
2039
|
+
finishKey() {
|
|
2040
|
+
const key = this.pendingKey;
|
|
2041
|
+
this.pendingKey = null;
|
|
2042
|
+
if (key === null) {
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
const where = { line: key.line, element: key.id };
|
|
2046
|
+
if (key.id === null || key.id.length === 0) {
|
|
2047
|
+
this.report.error("validation-error", GRAPHML_ISSUE.KEY_MISSING_ID, "<key> without an id", where);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (this.keys.has(key.id)) {
|
|
2051
|
+
this.report.error(
|
|
2052
|
+
"validation-error",
|
|
2053
|
+
GRAPHML_ISSUE.DUPLICATE_KEY,
|
|
2054
|
+
`key "${key.id}" is declared twice`,
|
|
2055
|
+
where
|
|
2056
|
+
);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
const yfiles = key.yfilesType !== null;
|
|
2060
|
+
const name = key.attrName !== null && key.attrName.length > 0 ? key.attrName : key.id;
|
|
2061
|
+
const skipped = yfiles && this.yfilesMode === "skip";
|
|
2062
|
+
if (skipped) {
|
|
2063
|
+
this.report.warning(
|
|
2064
|
+
"unsupported",
|
|
2065
|
+
GRAPHML_ISSUE.YFILES_SKIPPED,
|
|
2066
|
+
`yFiles key "${key.id}" (${key.yfilesType ?? ""}) skipped per the yfiles option`,
|
|
2067
|
+
where
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
let weight = false;
|
|
2071
|
+
let originalId = false;
|
|
2072
|
+
let node = null;
|
|
2073
|
+
let edge = null;
|
|
2074
|
+
let graph = null;
|
|
2075
|
+
for (const domain of key.domains) {
|
|
2076
|
+
if (skipped) {
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
if (domain === "edge" && !yfiles && isWeightField(name, this.options.weightFrom)) {
|
|
2080
|
+
weight = true;
|
|
2081
|
+
this.weightOrigin ??= { id: key.id, title: key.attrName, type: key.attrType };
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (domain === "node" && !yfiles && name === ORIGINAL_ID_ATTRIBUTE) {
|
|
2085
|
+
if (this.options.restoreMangledIds) {
|
|
2086
|
+
originalId = true;
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
node = this.declareOriginalIdColumn(key);
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
try {
|
|
2093
|
+
const target = yfiles ? this.declareYfiles(key, name, domain) : this.declareTyped(key, name, domain);
|
|
2094
|
+
if (domain === "node") {
|
|
2095
|
+
node = target;
|
|
2096
|
+
} else if (domain === "edge") {
|
|
2097
|
+
edge = target;
|
|
2098
|
+
} else {
|
|
2099
|
+
graph = target;
|
|
2100
|
+
}
|
|
2101
|
+
} catch (err) {
|
|
2102
|
+
this.report.recordError(err, where);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
this.keys.set(key.id, {
|
|
2106
|
+
id: key.id,
|
|
2107
|
+
name,
|
|
2108
|
+
node,
|
|
2109
|
+
edge,
|
|
2110
|
+
graph,
|
|
2111
|
+
weight,
|
|
2112
|
+
originalId,
|
|
2113
|
+
yfiles,
|
|
2114
|
+
skipped,
|
|
2115
|
+
domains: key.domains
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Declare the column of a typed (or untyped, then string) key in one domain.
|
|
2120
|
+
* @param key - the key
|
|
2121
|
+
* @param name - the resolved attribute name
|
|
2122
|
+
* @param domain - the domain
|
|
2123
|
+
* @returns the column target
|
|
2124
|
+
*/
|
|
2125
|
+
declareTyped(key, name, domain) {
|
|
2126
|
+
const id = key.id ?? "";
|
|
2127
|
+
const declared = declareAttribute({
|
|
2128
|
+
format: FORMAT,
|
|
2129
|
+
id,
|
|
2130
|
+
title: key.attrName !== null && key.attrName.length > 0 ? key.attrName : null,
|
|
2131
|
+
type: key.attrType,
|
|
2132
|
+
defaultText: key.defaultTree === void 0 ? key.defaultText : null,
|
|
2133
|
+
listSyntax: "comma",
|
|
2134
|
+
role: domain !== "graph" && name === LABEL_COLUMN ? "label" : null,
|
|
2135
|
+
long: this.options.long,
|
|
2136
|
+
taken: this.takenIn(domain)
|
|
2137
|
+
});
|
|
2138
|
+
for (const issue of declared.issues) {
|
|
2139
|
+
this.report.warning(issue.category, issue.code, issue.message, { line: key.line, element: id });
|
|
2140
|
+
}
|
|
2141
|
+
if (key.defaultTree !== void 0) {
|
|
2142
|
+
this.report.warning(
|
|
2143
|
+
"validation-error",
|
|
2144
|
+
GRAPHML_ISSUE.DATA_NESTED,
|
|
2145
|
+
`the <default> of key "${id}" holds elements; a ${key.attrType ?? "string"} default must be text`,
|
|
2146
|
+
{ line: key.line, element: id }
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
const { decl } = declared;
|
|
2150
|
+
if (key.desc.length > 0) {
|
|
2151
|
+
decl.extra = { desc: key.desc };
|
|
2152
|
+
}
|
|
2153
|
+
return this.declareTarget(domain, decl, declared.spec, declared.listSyntax);
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Declare the json column of a yFiles key in one domain.
|
|
2157
|
+
* @param key - the key
|
|
2158
|
+
* @param name - the resolved attribute name
|
|
2159
|
+
* @param domain - the domain
|
|
2160
|
+
* @returns the column target
|
|
2161
|
+
*/
|
|
2162
|
+
declareYfiles(key, name, domain) {
|
|
2163
|
+
const id = key.id ?? "";
|
|
2164
|
+
const columnName = uniqueColumnName(name, id, this.takenIn(domain));
|
|
2165
|
+
const decl = {
|
|
2166
|
+
name: columnName,
|
|
2167
|
+
dtype: "json",
|
|
2168
|
+
nullable: true,
|
|
2169
|
+
origin: {
|
|
2170
|
+
format: FORMAT,
|
|
2171
|
+
id,
|
|
2172
|
+
title: key.attrName !== null && key.attrName !== columnName ? key.attrName : null,
|
|
2173
|
+
type: key.yfilesType,
|
|
2174
|
+
namespace: "yfiles"
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
if (key.defaultTree !== void 0) {
|
|
2178
|
+
decl.default = key.defaultTree;
|
|
2179
|
+
} else if (key.defaultText !== null) {
|
|
2180
|
+
decl.default = key.defaultText;
|
|
2181
|
+
}
|
|
2182
|
+
if (key.desc.length > 0) {
|
|
2183
|
+
decl.extra = { desc: key.desc };
|
|
2184
|
+
}
|
|
2185
|
+
return this.declareTarget(domain, decl, null, "comma");
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Declare the `graphty.originalId` column that keeps the exporter's original ids when
|
|
2189
|
+
* restoreMangledIds is off.
|
|
2190
|
+
* @param key - the key
|
|
2191
|
+
* @returns the column target
|
|
2192
|
+
*/
|
|
2193
|
+
declareOriginalIdColumn(key) {
|
|
2194
|
+
const decl = {
|
|
2195
|
+
name: uniqueColumnName(ORIGINAL_ID_COLUMN, key.id, this.takenIn("node")),
|
|
2196
|
+
dtype: "string",
|
|
2197
|
+
nullable: true,
|
|
2198
|
+
role: "originalId",
|
|
2199
|
+
origin: { format: FORMAT, id: key.id, title: key.attrName, type: key.attrType, namespace: null }
|
|
2200
|
+
};
|
|
2201
|
+
return this.declareTarget("node", decl, null, "comma");
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Push a declaration into the sink for one domain through the shared design section 5.6 rule:
|
|
2205
|
+
* a role another column already holds is dropped (reported), a name the sink holds with
|
|
2206
|
+
* another shape is renamed `<name>#<id>` (reported).
|
|
2207
|
+
* @param domain - the domain
|
|
2208
|
+
* @param decl - the declaration
|
|
2209
|
+
* @param spec - the value spec, or null for json
|
|
2210
|
+
* @param listSyntax - the list syntax
|
|
2211
|
+
* @returns the target
|
|
2212
|
+
*/
|
|
2213
|
+
declareTarget(domain, decl, spec, listSyntax) {
|
|
2214
|
+
if (domain === "graph") {
|
|
2215
|
+
this.sink.setGraphValue(decl.name, void 0, decl);
|
|
2216
|
+
this.graphColumnNames.add(decl.name);
|
|
2217
|
+
return { name: decl.name, handle: INVALID_INDEX, decl, spec, listSyntax };
|
|
2218
|
+
}
|
|
2219
|
+
const resolved = declareResolved(this.sink, domain, decl, this.report, { element: decl.name });
|
|
2220
|
+
return { name: resolved.decl.name, handle: resolved.handle, decl: resolved.decl, spec, listSyntax };
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* The `taken` predicate of a domain: the reserved XML-derived names plus the sink's columns.
|
|
2224
|
+
* @param domain - the domain
|
|
2225
|
+
* @returns the predicate
|
|
2226
|
+
*/
|
|
2227
|
+
takenIn(domain) {
|
|
2228
|
+
if (domain === "graph") {
|
|
2229
|
+
return (name) => this.graphColumnNames.has(name);
|
|
2230
|
+
}
|
|
2231
|
+
const reserved = domain === "node" ? RESERVED_NODE_NAMES : RESERVED_EDGE_NAMES;
|
|
2232
|
+
const inSink = takenIn(this.sink, domain);
|
|
2233
|
+
return (name) => reserved.has(name) || inSink(name);
|
|
2234
|
+
}
|
|
2235
|
+
// ------------------------------------------------------------------ data and defaults
|
|
2236
|
+
/**
|
|
2237
|
+
* Open a `<data>`.
|
|
2238
|
+
* @param attrs - its attributes
|
|
2239
|
+
* @param line - the line
|
|
2240
|
+
* @param domain - the domain of the element holding it
|
|
2241
|
+
*/
|
|
2242
|
+
beginData(attrs, line, domain) {
|
|
2243
|
+
const keyId = attrs.get("key");
|
|
2244
|
+
const { content } = this;
|
|
2245
|
+
content.key = null;
|
|
2246
|
+
content.pendingKey = null;
|
|
2247
|
+
content.domain = domain;
|
|
2248
|
+
content.text = "";
|
|
2249
|
+
content.tree = null;
|
|
2250
|
+
content.line = line;
|
|
2251
|
+
if (keyId === void 0) {
|
|
2252
|
+
this.report.error("validation-error", GRAPHML_ISSUE.DATA_MISSING_KEY, "<data> without a key attribute", {
|
|
2253
|
+
line
|
|
2254
|
+
});
|
|
2255
|
+
} else {
|
|
2256
|
+
const key = this.keys.get(keyId);
|
|
2257
|
+
if (key === void 0) {
|
|
2258
|
+
this.report.error(
|
|
2259
|
+
"validation-error",
|
|
2260
|
+
GRAPHML_ISSUE.UNKNOWN_KEY,
|
|
2261
|
+
`<data> references undeclared key "${keyId}"`,
|
|
2262
|
+
{ line, element: keyId }
|
|
2263
|
+
);
|
|
2264
|
+
} else if (!key.skipped && !key.domains.includes(domain)) {
|
|
2265
|
+
this.report.warnOnce(
|
|
2266
|
+
"validation-error",
|
|
2267
|
+
GRAPHML_ISSUE.KEY_DOMAIN,
|
|
2268
|
+
`key "${keyId}" is declared for ${key.domains.join(" / ")} elements but used on a ${domain}; ignored`,
|
|
2269
|
+
{ line, element: keyId }
|
|
2270
|
+
);
|
|
2271
|
+
} else {
|
|
2272
|
+
content.key = key;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
this.ctx.push(
|
|
2276
|
+
8
|
|
2277
|
+
/* Data */
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* A child element inside `<data>` / `<default>`: the content becomes a tree.
|
|
2282
|
+
* @param name - the element name
|
|
2283
|
+
* @param attrs - its attributes
|
|
2284
|
+
*/
|
|
2285
|
+
captureStart(name, attrs) {
|
|
2286
|
+
const { content } = this;
|
|
2287
|
+
if (content.tree === null) {
|
|
2288
|
+
content.tree = new XmlTreeBuilder();
|
|
2289
|
+
if (content.text.length > 0) {
|
|
2290
|
+
content.tree.text(content.text);
|
|
2291
|
+
content.text = "";
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
content.tree.start(name, attrs);
|
|
2295
|
+
this.ctx.push(
|
|
2296
|
+
9
|
|
2297
|
+
/* Capture */
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
/** A captured element ends. */
|
|
2301
|
+
captureEnd() {
|
|
2302
|
+
this.content.tree?.end();
|
|
2303
|
+
}
|
|
2304
|
+
/** Close a `<default>`: keep its text or tree for the key. */
|
|
2305
|
+
finishDefault() {
|
|
2306
|
+
const { content } = this;
|
|
2307
|
+
const key = content.pendingKey;
|
|
2308
|
+
if (key === null) {
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
if (content.tree !== null) {
|
|
2312
|
+
key.defaultTree = content.tree.finish();
|
|
2313
|
+
} else {
|
|
2314
|
+
key.defaultText = content.text;
|
|
2315
|
+
}
|
|
2316
|
+
content.tree = null;
|
|
2317
|
+
content.text = "";
|
|
2318
|
+
content.pendingKey = null;
|
|
2319
|
+
}
|
|
2320
|
+
/** Close a `<data>`: parse the value and route it to its element. */
|
|
2321
|
+
finishData() {
|
|
2322
|
+
const { content } = this;
|
|
2323
|
+
const { key } = content;
|
|
2324
|
+
const value = content.tree === null ? content.text : content.tree.finish();
|
|
2325
|
+
content.tree = null;
|
|
2326
|
+
content.text = "";
|
|
2327
|
+
content.key = null;
|
|
2328
|
+
if (key === null || key.skipped) {
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
const where = { line: content.line, element: key.id };
|
|
2332
|
+
switch (content.domain) {
|
|
2333
|
+
case "node": {
|
|
2334
|
+
const node = this.nodes[this.nodes.length - 1];
|
|
2335
|
+
if (key.originalId) {
|
|
2336
|
+
if (typeof value === "string") {
|
|
2337
|
+
node.restoredId = value;
|
|
2338
|
+
}
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (key.node === null) {
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
const parsed = this.parseValue(key.node, value, where);
|
|
2345
|
+
if (parsed === void 0) {
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
if (node.index === INVALID_INDEX) {
|
|
2349
|
+
node.pending.push(key.node.handle, parsed);
|
|
2350
|
+
} else if (!node.failed) {
|
|
2351
|
+
this.sink.setNodeValue(key.node.handle, node.index, parsed);
|
|
2352
|
+
}
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
case "edge": {
|
|
2356
|
+
const { edge } = this;
|
|
2357
|
+
if (edge === null) {
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
if (key.weight) {
|
|
2361
|
+
if (typeof value !== "string") {
|
|
2362
|
+
this.report.error(
|
|
2363
|
+
"validation-error",
|
|
2364
|
+
GRAPHML_ISSUE.DATA_NESTED,
|
|
2365
|
+
"a weight value must be text",
|
|
2366
|
+
where
|
|
2367
|
+
);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
try {
|
|
2371
|
+
edge.weight = parseWeightText(value);
|
|
2372
|
+
} catch (err) {
|
|
2373
|
+
this.skipEdge(edge);
|
|
2374
|
+
this.report.recordError(err, where);
|
|
2375
|
+
}
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
if (key.edge === null) {
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const parsed = this.parseValue(key.edge, value, where);
|
|
2382
|
+
if (parsed !== void 0) {
|
|
2383
|
+
edge.pending.push(key.edge.handle, parsed);
|
|
2384
|
+
}
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
case "graph": {
|
|
2388
|
+
if (key.graph === null) {
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
const parsed = this.parseValue(key.graph, value, where);
|
|
2392
|
+
if (parsed !== void 0) {
|
|
2393
|
+
try {
|
|
2394
|
+
this.sink.setGraphValue(key.graph.name, parsed, key.graph.decl);
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
this.report.recordError(err, where);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
default: {
|
|
2402
|
+
const { domain } = content;
|
|
2403
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown data domain ${String(domain)}`, {});
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Parse the content of a `<data>` by its column's declaration.
|
|
2409
|
+
* @param target - the column
|
|
2410
|
+
* @param value - the text, or the tree of a nested subtree
|
|
2411
|
+
* @param where - the location for issues
|
|
2412
|
+
* @returns the value to write, or undefined when there is none (blank, or an error was recorded)
|
|
2413
|
+
*/
|
|
2414
|
+
parseValue(target, value, where) {
|
|
2415
|
+
if (target.spec === null) {
|
|
2416
|
+
if (!this.yfilesLossNoted) {
|
|
2417
|
+
this.yfilesLossNoted = true;
|
|
2418
|
+
this.report.loss(
|
|
2419
|
+
GRAPHML_LOSS.YFILES_JSON,
|
|
2420
|
+
"yFiles nested XML is kept as a JSON tree: structure preserved, not byte-exact",
|
|
2421
|
+
target.name
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
return value;
|
|
2425
|
+
}
|
|
2426
|
+
if (typeof value !== "string") {
|
|
2427
|
+
this.report.error(
|
|
2428
|
+
"validation-error",
|
|
2429
|
+
GRAPHML_ISSUE.DATA_NESTED,
|
|
2430
|
+
`<data> of the ${target.spec.declared} attribute "${target.name}" holds elements, not text`,
|
|
2431
|
+
where
|
|
2432
|
+
);
|
|
2433
|
+
return void 0;
|
|
2434
|
+
}
|
|
2435
|
+
const { spec } = target;
|
|
2436
|
+
if (spec.kind !== "string" && isWhitespace(value)) {
|
|
2437
|
+
return void 0;
|
|
2438
|
+
}
|
|
2439
|
+
try {
|
|
2440
|
+
const parsed = parseDeclaredValue(value, spec, target.listSyntax);
|
|
2441
|
+
if (spec.precision && losesPrecision(spec, value)) {
|
|
2442
|
+
this.report.warnOnce(
|
|
2443
|
+
"precision",
|
|
2444
|
+
PRECISION_CODE,
|
|
2445
|
+
`long values of "${target.name}" beyond 2^53 are stored as the nearest double`,
|
|
2446
|
+
{ line: where.line, element: target.name }
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
return parsed;
|
|
2450
|
+
} catch (err) {
|
|
2451
|
+
this.report.recordError(err, where);
|
|
2452
|
+
return void 0;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
// ------------------------------------------------------------------ nodes
|
|
2456
|
+
/**
|
|
2457
|
+
* Open a `<node>`; it is added to the sink when its data is complete (or a nested graph
|
|
2458
|
+
* starts), so a `graphty:originalId` value can replace its id.
|
|
2459
|
+
* @param attrs - the node attributes
|
|
2460
|
+
* @param line - the line
|
|
2461
|
+
*/
|
|
2462
|
+
beginNode(attrs, line) {
|
|
2463
|
+
const graph = this.graphs[this.graphs.length - 1];
|
|
2464
|
+
const state = {
|
|
2465
|
+
id: null,
|
|
2466
|
+
idText: null,
|
|
2467
|
+
index: INVALID_INDEX,
|
|
2468
|
+
parent: graph.parent,
|
|
2469
|
+
pending: [],
|
|
2470
|
+
restoredId: null,
|
|
2471
|
+
failed: false,
|
|
2472
|
+
line
|
|
2473
|
+
};
|
|
2474
|
+
const idText = attrs.get("id");
|
|
2475
|
+
if (idText === void 0 || idText.length === 0) {
|
|
2476
|
+
this.skipNode(state);
|
|
2477
|
+
this.report.error("missing-value", GRAPHML_ISSUE.MISSING_ID, "<node> without an id", { line });
|
|
2478
|
+
} else {
|
|
2479
|
+
try {
|
|
2480
|
+
state.id = this.coercer.text(idText);
|
|
2481
|
+
state.idText = idText;
|
|
2482
|
+
this.reportMerge(idText, line);
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
this.skipNode(state);
|
|
2485
|
+
this.report.recordError(err, { line, element: idText });
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
this.nodes.push(state);
|
|
2489
|
+
this.ctx.push(
|
|
2490
|
+
5
|
|
2491
|
+
/* Node */
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Add a node to the sink and flush its pending values.
|
|
2496
|
+
* @param node - the node state
|
|
2497
|
+
*/
|
|
2498
|
+
materializeNode(node) {
|
|
2499
|
+
if (node.index !== INVALID_INDEX || node.failed) {
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
const id = node.restoredId ?? node.id;
|
|
2503
|
+
if (id === null) {
|
|
2504
|
+
node.failed = true;
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
if (node.restoredId !== null && node.idText !== null) {
|
|
2508
|
+
this.restored.set(node.idText, node.restoredId);
|
|
2509
|
+
}
|
|
2510
|
+
try {
|
|
2511
|
+
const existing = this.sink.indexOf(id);
|
|
2512
|
+
const index = this.sink.addNode(id);
|
|
2513
|
+
if (existing !== INVALID_INDEX && this.declared.has(index)) {
|
|
2514
|
+
this.report.warning(
|
|
2515
|
+
"validation-error",
|
|
2516
|
+
GRAPHML_ISSUE.DUPLICATE_NODE,
|
|
2517
|
+
`node "${String(id)}" is declared twice; the declarations are merged`,
|
|
2518
|
+
{ line: node.line, element: String(id) }
|
|
2519
|
+
);
|
|
2520
|
+
} else {
|
|
2521
|
+
this.report.counts.nodes++;
|
|
2522
|
+
}
|
|
2523
|
+
this.declared.add(index);
|
|
2524
|
+
node.index = index;
|
|
2525
|
+
this.checkAbort();
|
|
2526
|
+
if (node.parent !== INVALID_INDEX) {
|
|
2527
|
+
this.sink.setNodeValue(this.parentColumn(), index, node.parent);
|
|
2528
|
+
}
|
|
2529
|
+
const { pending } = node;
|
|
2530
|
+
for (let i = 0; i < pending.length; i += 2) {
|
|
2531
|
+
this.sink.setNodeValue(pending[i], index, pending[i + 1]);
|
|
2532
|
+
}
|
|
2533
|
+
pending.length = 0;
|
|
2534
|
+
} catch (err) {
|
|
2535
|
+
this.skipNode(node);
|
|
2536
|
+
this.report.recordError(err, { line: node.line, element: String(id) });
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Mark a node element skipped (once) before its error is recorded, so the report's counts
|
|
2541
|
+
* include it even when that error is the one that exceeds the limit.
|
|
2542
|
+
* @param node - the node state
|
|
2543
|
+
*/
|
|
2544
|
+
skipNode(node) {
|
|
2545
|
+
if (!node.failed) {
|
|
2546
|
+
node.failed = true;
|
|
2547
|
+
this.report.counts.skippedNodes++;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
/** Close a `<node>`. */
|
|
2551
|
+
finishNode() {
|
|
2552
|
+
const node = this.nodes.pop();
|
|
2553
|
+
if (node !== void 0) {
|
|
2554
|
+
this.materializeNode(node);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* The `parent` column, declared on first use.
|
|
2559
|
+
* @returns the handle
|
|
2560
|
+
*/
|
|
2561
|
+
parentColumn() {
|
|
2562
|
+
if (this.parentHandle === INVALID_INDEX) {
|
|
2563
|
+
this.parentHandle = this.sink.declareNodeColumn({
|
|
2564
|
+
name: PARENT_COLUMN,
|
|
2565
|
+
dtype: "u32",
|
|
2566
|
+
role: "parent",
|
|
2567
|
+
refersTo: "node",
|
|
2568
|
+
nullable: true,
|
|
2569
|
+
origin: { format: FORMAT, id: null, title: null, type: "graph", namespace: null }
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
return this.parentHandle;
|
|
2573
|
+
}
|
|
2574
|
+
// ------------------------------------------------------------------ edges
|
|
2575
|
+
/**
|
|
2576
|
+
* Open an `<edge>`; it is pushed at `</edge>` once its weight and data are known.
|
|
2577
|
+
* @param attrs - the edge attributes
|
|
2578
|
+
* @param line - the line
|
|
2579
|
+
*/
|
|
2580
|
+
beginEdge(attrs, line) {
|
|
2581
|
+
const graph = this.graphs[this.graphs.length - 1];
|
|
2582
|
+
const edge = {
|
|
2583
|
+
source: null,
|
|
2584
|
+
target: null,
|
|
2585
|
+
id: attrs.get("id") ?? null,
|
|
2586
|
+
kind: graph.directed ? "directed" : "undirected",
|
|
2587
|
+
sourcePort: attrs.get("sourceport") ?? null,
|
|
2588
|
+
targetPort: attrs.get("targetport") ?? null,
|
|
2589
|
+
weight: void 0,
|
|
2590
|
+
pending: [],
|
|
2591
|
+
failed: false,
|
|
2592
|
+
line
|
|
2593
|
+
};
|
|
2594
|
+
const where = { line, element: edge.id };
|
|
2595
|
+
const sourceText = attrs.get("source");
|
|
2596
|
+
const targetText = attrs.get("target");
|
|
2597
|
+
if (sourceText === void 0 || sourceText.length === 0) {
|
|
2598
|
+
this.skipEdge(edge);
|
|
2599
|
+
this.report.error("missing-value", GRAPHML_ISSUE.MISSING_ENDPOINT, "<edge> without a source", where);
|
|
2600
|
+
}
|
|
2601
|
+
if (targetText === void 0 || targetText.length === 0) {
|
|
2602
|
+
this.skipEdge(edge);
|
|
2603
|
+
this.report.error("missing-value", GRAPHML_ISSUE.MISSING_ENDPOINT, "<edge> without a target", where);
|
|
2604
|
+
}
|
|
2605
|
+
if (!edge.failed && sourceText !== void 0 && targetText !== void 0) {
|
|
2606
|
+
try {
|
|
2607
|
+
edge.source = this.endpointId(sourceText, line);
|
|
2608
|
+
edge.target = this.endpointId(targetText, line);
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
this.skipEdge(edge);
|
|
2611
|
+
this.report.recordError(err, where);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
const directedText = attrs.get("directed");
|
|
2615
|
+
if (directedText !== void 0) {
|
|
2616
|
+
if (directedText === "true") {
|
|
2617
|
+
edge.kind = "directed";
|
|
2618
|
+
} else if (directedText === "false") {
|
|
2619
|
+
edge.kind = "undirected";
|
|
2620
|
+
} else {
|
|
2621
|
+
this.skipEdge(edge);
|
|
2622
|
+
this.report.error(
|
|
2623
|
+
"validation-error",
|
|
2624
|
+
GRAPHML_ISSUE.INVALID_DIRECTED,
|
|
2625
|
+
`directed="${directedText}" is neither true nor false`,
|
|
2626
|
+
where
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
this.edge = edge;
|
|
2631
|
+
this.ctx.push(
|
|
2632
|
+
6
|
|
2633
|
+
/* Edge */
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Close an `<edge>`: push it through the direction resolver and write its values.
|
|
2638
|
+
*/
|
|
2639
|
+
finishEdge() {
|
|
2640
|
+
const { edge } = this;
|
|
2641
|
+
this.edge = null;
|
|
2642
|
+
if (edge === null) {
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
if (edge.failed || edge.source === null || edge.target === null) {
|
|
2646
|
+
this.skipEdge(edge);
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
const element = edge.id ?? `${String(edge.source)} -> ${String(edge.target)}`;
|
|
2650
|
+
const where = { line: edge.line, element };
|
|
2651
|
+
try {
|
|
2652
|
+
if (!this.options.addMissingNodes) {
|
|
2653
|
+
this.requireNode(edge.source);
|
|
2654
|
+
this.requireNode(edge.target);
|
|
2655
|
+
}
|
|
2656
|
+
if (edge.id !== null && this.edgeIds.has(edge.id)) {
|
|
2657
|
+
throw new GraphFormatError("E_DUPLICATE_EDGE_ID", `edge id "${edge.id}" is used twice`, {
|
|
2658
|
+
id: edge.id
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
const before = this.sink.edgeCount;
|
|
2662
|
+
this.countEndpoints(edge.source, edge.target);
|
|
2663
|
+
const e = this.direction.addEdge(edge.source, edge.target, edge.kind, edge.weight, where);
|
|
2664
|
+
this.report.counts.edges += this.sink.edgeCount - before;
|
|
2665
|
+
this.checkAbort();
|
|
2666
|
+
if (edge.id !== null) {
|
|
2667
|
+
this.edgeIds.add(edge.id);
|
|
2668
|
+
this.sink.setEdgeValue(this.edgeIdColumn(), e, edge.id);
|
|
2669
|
+
}
|
|
2670
|
+
if (edge.sourcePort !== null) {
|
|
2671
|
+
this.sink.setEdgeValue(this.portColumn("source"), e, edge.sourcePort);
|
|
2672
|
+
}
|
|
2673
|
+
if (edge.targetPort !== null) {
|
|
2674
|
+
this.sink.setEdgeValue(this.portColumn("target"), e, edge.targetPort);
|
|
2675
|
+
}
|
|
2676
|
+
const { pending } = edge;
|
|
2677
|
+
for (let i = 0; i < pending.length; i += 2) {
|
|
2678
|
+
this.sink.setEdgeValue(pending[i], e, pending[i + 1]);
|
|
2679
|
+
}
|
|
2680
|
+
} catch (err) {
|
|
2681
|
+
this.skipEdge(edge);
|
|
2682
|
+
this.report.recordError(err, where);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Mark an edge (or hyperedge) element skipped once, before its error is recorded.
|
|
2687
|
+
* @param edge - the edge state
|
|
2688
|
+
*/
|
|
2689
|
+
skipEdge(edge) {
|
|
2690
|
+
if (!edge.failed) {
|
|
2691
|
+
edge.failed = true;
|
|
2692
|
+
this.report.counts.skippedEdges++;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
/**
|
|
2696
|
+
* E_UNKNOWN_NODE when an endpoint was never declared (addMissingNodes false enforced by the
|
|
2697
|
+
* importer, whatever the sink allows).
|
|
2698
|
+
* @param id - the endpoint id
|
|
2699
|
+
*/
|
|
2700
|
+
requireNode(id) {
|
|
2701
|
+
if (this.sink.indexOf(id) === INVALID_INDEX) {
|
|
2702
|
+
throw new GraphFormatError("E_UNKNOWN_NODE", `edge endpoint "${String(id)}" is not a declared node`, {
|
|
2703
|
+
id
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* The edge `id` column (role id, unique), declared on first use.
|
|
2709
|
+
* @returns the handle
|
|
2710
|
+
*/
|
|
2711
|
+
edgeIdColumn() {
|
|
2712
|
+
if (this.edgeIdHandle === INVALID_INDEX) {
|
|
2713
|
+
this.edgeIdHandle = this.sink.declareEdgeColumn({
|
|
2714
|
+
name: EDGE_ID_COLUMN,
|
|
2715
|
+
dtype: "string",
|
|
2716
|
+
role: "id",
|
|
2717
|
+
unique: true,
|
|
2718
|
+
nullable: true,
|
|
2719
|
+
origin: { format: FORMAT, id: null, title: null, type: "id", namespace: null }
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
return this.edgeIdHandle;
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* The sourceport / targetport column, declared on first use.
|
|
2726
|
+
* @param side - which end
|
|
2727
|
+
* @returns the handle
|
|
2728
|
+
*/
|
|
2729
|
+
portColumn(side) {
|
|
2730
|
+
if (side === "source") {
|
|
2731
|
+
if (this.sourcePortHandle === INVALID_INDEX) {
|
|
2732
|
+
this.sourcePortHandle = this.sink.declareEdgeColumn({
|
|
2733
|
+
name: SOURCE_PORT_COLUMN,
|
|
2734
|
+
dtype: "string",
|
|
2735
|
+
role: "sourcePort",
|
|
2736
|
+
nullable: true,
|
|
2737
|
+
origin: { format: FORMAT, id: null, title: null, type: "sourceport", namespace: null }
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
return this.sourcePortHandle;
|
|
2741
|
+
}
|
|
2742
|
+
if (this.targetPortHandle === INVALID_INDEX) {
|
|
2743
|
+
this.targetPortHandle = this.sink.declareEdgeColumn({
|
|
2744
|
+
name: TARGET_PORT_COLUMN,
|
|
2745
|
+
dtype: "string",
|
|
2746
|
+
role: "targetPort",
|
|
2747
|
+
nullable: true,
|
|
2748
|
+
origin: { format: FORMAT, id: null, title: null, type: "targetport", namespace: null }
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
return this.targetPortHandle;
|
|
2752
|
+
}
|
|
2753
|
+
// ------------------------------------------------------------------ hyperedges
|
|
2754
|
+
/**
|
|
2755
|
+
* Open a `<hyperedge>` per the hyperedges option.
|
|
2756
|
+
* @param attrs - its attributes
|
|
2757
|
+
* @param line - the line
|
|
2758
|
+
*/
|
|
2759
|
+
beginHyperedge(attrs, line) {
|
|
2760
|
+
const id = attrs.get("id") ?? null;
|
|
2761
|
+
const where = { line, element: id };
|
|
2762
|
+
switch (this.options.hyperedges) {
|
|
2763
|
+
case "error":
|
|
2764
|
+
this.report.error(
|
|
2765
|
+
"unsupported",
|
|
2766
|
+
GRAPHML_ISSUE.HYPEREDGE,
|
|
2767
|
+
'hyperedge refused (hyperedges: "error")',
|
|
2768
|
+
where
|
|
2769
|
+
);
|
|
2770
|
+
throw this.report.abort("hyperedge refused", { code: GRAPHML_ISSUE.HYPEREDGE });
|
|
2771
|
+
case "skip":
|
|
2772
|
+
this.report.warnOnce(
|
|
2773
|
+
"unsupported",
|
|
2774
|
+
GRAPHML_ISSUE.HYPEREDGE_SKIPPED,
|
|
2775
|
+
'hyperedges are skipped (hyperedges: "skip"); use "star" or "clique" to expand them',
|
|
2776
|
+
where
|
|
2777
|
+
);
|
|
2778
|
+
this.report.counts.skippedEdges++;
|
|
2779
|
+
this.ctx.push(
|
|
2780
|
+
10
|
|
2781
|
+
/* Skip */
|
|
2782
|
+
);
|
|
2783
|
+
return;
|
|
2784
|
+
case "star":
|
|
2785
|
+
case "clique":
|
|
2786
|
+
this.hyperedge = { id, endpoints: [], failed: false, line };
|
|
2787
|
+
this.ctx.push(
|
|
2788
|
+
7
|
|
2789
|
+
/* Hyperedge */
|
|
2790
|
+
);
|
|
2791
|
+
return;
|
|
2792
|
+
default: {
|
|
2793
|
+
const policy = this.options.hyperedges;
|
|
2794
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown hyperedges policy ${String(policy)}`, {
|
|
2795
|
+
option: "hyperedges"
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* An `<endpoint>` of the open hyperedge.
|
|
2802
|
+
* @param attrs - its attributes
|
|
2803
|
+
* @param line - the line
|
|
2804
|
+
*/
|
|
2805
|
+
readEndpoint(attrs, line) {
|
|
2806
|
+
const { hyperedge } = this;
|
|
2807
|
+
if (hyperedge === null) {
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
const where = { line, element: hyperedge.id };
|
|
2811
|
+
const nodeText = attrs.get("node");
|
|
2812
|
+
const typeText = attrs.get("type") ?? "undir";
|
|
2813
|
+
if (nodeText === void 0 || nodeText.length === 0) {
|
|
2814
|
+
this.skipEdge(hyperedge);
|
|
2815
|
+
this.report.error("missing-value", GRAPHML_ISSUE.HYPEREDGE_ENDPOINT, "<endpoint> without a node", where);
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
if (typeText !== "in" && typeText !== "out" && typeText !== "undir") {
|
|
2819
|
+
this.skipEdge(hyperedge);
|
|
2820
|
+
this.report.error(
|
|
2821
|
+
"validation-error",
|
|
2822
|
+
GRAPHML_ISSUE.HYPEREDGE_ENDPOINT,
|
|
2823
|
+
`endpoint type "${typeText}" is not in, out or undir`,
|
|
2824
|
+
where
|
|
2825
|
+
);
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
try {
|
|
2829
|
+
const node = this.endpointId(nodeText, line);
|
|
2830
|
+
hyperedge.endpoints.push({ node, type: typeText });
|
|
2831
|
+
} catch (err) {
|
|
2832
|
+
this.skipEdge(hyperedge);
|
|
2833
|
+
this.report.recordError(err, where);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
/** Close a `<hyperedge>`: expand it to a star or a clique. */
|
|
2837
|
+
finishHyperedge() {
|
|
2838
|
+
const { hyperedge } = this;
|
|
2839
|
+
this.hyperedge = null;
|
|
2840
|
+
if (hyperedge === null) {
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
if (hyperedge.failed) {
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
const where = { line: hyperedge.line, element: hyperedge.id };
|
|
2847
|
+
try {
|
|
2848
|
+
if (this.options.hyperedges === "star") {
|
|
2849
|
+
this.expandStar(hyperedge, where);
|
|
2850
|
+
} else {
|
|
2851
|
+
this.expandClique(hyperedge, where);
|
|
2852
|
+
}
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
this.skipEdge(hyperedge);
|
|
2855
|
+
this.report.recordError(err, where);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Star expansion: a synthetic hub node (marked in the `graphty.hyperedge` column) joined to
|
|
2860
|
+
* every endpoint; `in` endpoints point at the hub, `out` endpoints are pointed at by it,
|
|
2861
|
+
* `undir` endpoints are undirected.
|
|
2862
|
+
* @param hyperedge - the hyperedge
|
|
2863
|
+
* @param where - the location for issues
|
|
2864
|
+
*/
|
|
2865
|
+
expandStar(hyperedge, where) {
|
|
2866
|
+
this.hyperedgeCount++;
|
|
2867
|
+
let hubId = hyperedge.id ?? `hyperedge${this.hyperedgeCount}`;
|
|
2868
|
+
for (let n = 2; this.sink.indexOf(hubId) !== INVALID_INDEX; n++) {
|
|
2869
|
+
hubId = `${hyperedge.id ?? `hyperedge${this.hyperedgeCount}`}#${n}`;
|
|
2870
|
+
}
|
|
2871
|
+
const hub = this.sink.addNode(hubId);
|
|
2872
|
+
this.declared.add(hub);
|
|
2873
|
+
this.report.counts.nodes++;
|
|
2874
|
+
if (this.hubHandle === INVALID_INDEX) {
|
|
2875
|
+
this.hubHandle = this.sink.declareNodeColumn({
|
|
2876
|
+
name: HYPEREDGE_HUB_COLUMN,
|
|
2877
|
+
dtype: "bool",
|
|
2878
|
+
nullable: true,
|
|
2879
|
+
origin: { format: FORMAT, id: null, title: null, type: "hyperedge", namespace: null }
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
this.sink.setNodeValue(this.hubHandle, hub, true);
|
|
2883
|
+
for (const endpoint of hyperedge.endpoints) {
|
|
2884
|
+
const before = this.sink.edgeCount;
|
|
2885
|
+
switch (endpoint.type) {
|
|
2886
|
+
case "in":
|
|
2887
|
+
this.direction.addEdge(endpoint.node, hubId, "directed", void 0, where);
|
|
2888
|
+
break;
|
|
2889
|
+
case "out":
|
|
2890
|
+
this.direction.addEdge(hubId, endpoint.node, "directed", void 0, where);
|
|
2891
|
+
break;
|
|
2892
|
+
case "undir":
|
|
2893
|
+
this.direction.addEdge(endpoint.node, hubId, "undirected", void 0, where);
|
|
2894
|
+
break;
|
|
2895
|
+
default: {
|
|
2896
|
+
const { type } = endpoint;
|
|
2897
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown endpoint type ${String(type)}`, {});
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
this.report.counts.edges += this.sink.edgeCount - before;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* Clique expansion: every `in` endpoint points at every `out` endpoint; `undir` endpoints are
|
|
2905
|
+
* joined undirected to every other endpoint; two endpoints of the same direction are not joined.
|
|
2906
|
+
* @param hyperedge - the hyperedge
|
|
2907
|
+
* @param where - the location for issues
|
|
2908
|
+
*/
|
|
2909
|
+
expandClique(hyperedge, where) {
|
|
2910
|
+
const { endpoints } = hyperedge;
|
|
2911
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
2912
|
+
for (let j = i + 1; j < endpoints.length; j++) {
|
|
2913
|
+
const a = endpoints[i];
|
|
2914
|
+
const b = endpoints[j];
|
|
2915
|
+
const before = this.sink.edgeCount;
|
|
2916
|
+
if (a.type === "undir" || b.type === "undir") {
|
|
2917
|
+
this.direction.addEdge(a.node, b.node, "undirected", void 0, where);
|
|
2918
|
+
} else if (a.type === "in" && b.type === "out") {
|
|
2919
|
+
this.direction.addEdge(a.node, b.node, "directed", void 0, where);
|
|
2920
|
+
} else if (a.type === "out" && b.type === "in") {
|
|
2921
|
+
this.direction.addEdge(b.node, a.node, "directed", void 0, where);
|
|
2922
|
+
} else {
|
|
2923
|
+
continue;
|
|
2924
|
+
}
|
|
2925
|
+
this.report.counts.edges += this.sink.edgeCount - before;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
// ------------------------------------------------------------------ options and ids
|
|
2930
|
+
/**
|
|
2931
|
+
* Count the endpoints of an edge the file never declared as nodes of the sink (created under
|
|
2932
|
+
* addMissingNodes), so the report describes what the sink holds (design section 8.6).
|
|
2933
|
+
* @param source - the source id
|
|
2934
|
+
* @param target - the target id
|
|
2935
|
+
*/
|
|
2936
|
+
countEndpoints(source, target) {
|
|
2937
|
+
if (!this.options.addMissingNodes) {
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
let created = 0;
|
|
2941
|
+
if (this.sink.indexOf(source) === INVALID_INDEX) {
|
|
2942
|
+
created++;
|
|
2943
|
+
}
|
|
2944
|
+
if (source !== target && this.sink.indexOf(target) === INVALID_INDEX) {
|
|
2945
|
+
created++;
|
|
2946
|
+
}
|
|
2947
|
+
this.report.counts.nodes += created;
|
|
2948
|
+
}
|
|
2949
|
+
/** Check the cancellation signal every ABORT_CHECK_INTERVAL elements. */
|
|
2950
|
+
checkAbort() {
|
|
2951
|
+
if (++this.elementsSinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
2952
|
+
this.elementsSinceCheck = 0;
|
|
2953
|
+
throwIfAborted(this.options.signal);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* The id an edge endpoint refers to: the original id when the text is a mangled id restored
|
|
2958
|
+
* from `graphty:originalId` (the exporter writes endpoints with the mangled id), else the
|
|
2959
|
+
* coerced text.
|
|
2960
|
+
* @param text - the source / target / endpoint node text
|
|
2961
|
+
* @param line - the line
|
|
2962
|
+
* @returns the node id
|
|
2963
|
+
*/
|
|
2964
|
+
endpointId(text, line) {
|
|
2965
|
+
const restored = this.restored.get(text);
|
|
2966
|
+
if (restored !== void 0) {
|
|
2967
|
+
return restored;
|
|
2968
|
+
}
|
|
2969
|
+
const id = this.coercer.text(text);
|
|
2970
|
+
this.reportMerge(text, line);
|
|
2971
|
+
return id;
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Report a merge the coercer detected under ids: "number".
|
|
2975
|
+
* @param text - the id text just coerced
|
|
2976
|
+
* @param line - the line
|
|
2977
|
+
*/
|
|
2978
|
+
reportMerge(text, line) {
|
|
2979
|
+
const merge = this.coercer.lastMerge;
|
|
2980
|
+
if (merge !== null) {
|
|
2981
|
+
this.report.warning(
|
|
2982
|
+
"coercion",
|
|
2983
|
+
GRAPHML_ISSUE.ID_MERGED,
|
|
2984
|
+
`id "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids: "number"`,
|
|
2985
|
+
{ line, element: text }
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
function hint(text) {
|
|
2991
|
+
if (text === void 0 || !/^[0-9]+$/.test(text)) {
|
|
2992
|
+
return null;
|
|
2993
|
+
}
|
|
2994
|
+
const n = Number(text);
|
|
2995
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
2996
|
+
}
|
|
2997
|
+
function resolveYfiles(options) {
|
|
2998
|
+
const value = options?.yfiles;
|
|
2999
|
+
if (value === void 0) {
|
|
3000
|
+
return "json";
|
|
3001
|
+
}
|
|
3002
|
+
if (typeof value !== "string" || !YFILES_MODES.has(value)) {
|
|
3003
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option yfiles: ${JSON.stringify(value)} is not "json" or "skip"`, {
|
|
3004
|
+
option: "yfiles",
|
|
3005
|
+
found: value
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
return value;
|
|
3009
|
+
}
|
|
3010
|
+
function sniffGraphml(head) {
|
|
3011
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(head.subarray(0, SNIFF_BYTES));
|
|
3012
|
+
const root = text.indexOf("<graphml");
|
|
3013
|
+
if (root >= 0) {
|
|
3014
|
+
return text.includes(GRAPHML_NAMESPACE, root) ? 1 : 0.9;
|
|
3015
|
+
}
|
|
3016
|
+
return /^\s*<(\?xml|!--|!DOCTYPE)/.test(text) ? 0.05 : 0;
|
|
3017
|
+
}
|
|
3018
|
+
async function importGraphml(input, sink, options) {
|
|
3019
|
+
const common = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
3020
|
+
const yfilesMode = resolveYfiles(options);
|
|
3021
|
+
const report = new ImportReportBuilder(FORMAT, common.errorLimit);
|
|
3022
|
+
reportSinkOptions(sink, options, report, true);
|
|
3023
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
3024
|
+
const reader = new GraphmlReader(sink, report, common, yfilesMode);
|
|
3025
|
+
try {
|
|
3026
|
+
await tokenizeXml(textChunks(input, report, common), reader);
|
|
3027
|
+
} catch (err) {
|
|
3028
|
+
if (err instanceof XmlSyntaxError) {
|
|
3029
|
+
report.fail(GRAPHML_ISSUE.XML_SYNTAX, err.message, { line: err.line });
|
|
3030
|
+
}
|
|
3031
|
+
throw err;
|
|
3032
|
+
}
|
|
3033
|
+
reader.finish();
|
|
3034
|
+
throwIfAborted(common.signal);
|
|
3035
|
+
return report.finish();
|
|
3036
|
+
}
|
|
3037
|
+
const graphmlImporter = Object.freeze({
|
|
3038
|
+
format: FORMAT,
|
|
3039
|
+
extensions: EXTENSIONS,
|
|
3040
|
+
mimeTypes: MIME_TYPES,
|
|
3041
|
+
sniff: sniffGraphml,
|
|
3042
|
+
import: importGraphml
|
|
3043
|
+
});
|
|
3044
|
+
export {
|
|
3045
|
+
GRAPHML_ISSUE as G,
|
|
3046
|
+
GRAPHML_LOSS as a,
|
|
3047
|
+
graphmlImporter as b,
|
|
3048
|
+
graphmlExporter as g
|
|
3049
|
+
};
|
|
3050
|
+
//# sourceMappingURL=importer-GozH8DkN.js.map
|