@graphty/graph-io 0.0.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -28
- package/dist/chunks/children-CL3Cy0ez.js +238 -0
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
- package/dist/chunks/escape-DyI8JofU.js +938 -0
- package/dist/chunks/escape-DyI8JofU.js.map +1 -0
- package/dist/chunks/importer-CQnJuWJw.js +2987 -0
- package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
- package/dist/chunks/importer-CpCpfbxr.js +2015 -0
- package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
- package/dist/chunks/importer-DbnGYr3_.js +2342 -0
- package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
- package/dist/chunks/importer-GozH8DkN.js +3050 -0
- package/dist/chunks/importer-GozH8DkN.js.map +1 -0
- package/dist/chunks/records-CGpxszm1.js +605 -0
- package/dist/chunks/records-CGpxszm1.js.map +1 -0
- package/dist/chunks/text-CajMdVFy.js +189 -0
- package/dist/chunks/text-CajMdVFy.js.map +1 -0
- package/dist/chunks/writer-DxSKC7TL.js +2842 -0
- package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1702 -0
- package/dist/csv.js.map +1 -0
- package/dist/dot.d.ts +1 -0
- package/dist/dot.js +8 -0
- package/dist/dot.js.map +1 -0
- package/dist/gexf.d.ts +1 -0
- package/dist/gexf.js +3466 -0
- package/dist/gexf.js.map +1 -0
- package/dist/gml.d.ts +1 -0
- package/dist/gml.js +2647 -0
- package/dist/gml.js.map +1 -0
- package/dist/graph-io.d.ts +1 -0
- package/dist/graph-io.js +790 -0
- package/dist/graph-io.js.map +1 -0
- package/dist/graphml.d.ts +1 -0
- package/dist/graphml.js +8 -0
- package/dist/graphml.js.map +1 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +11 -0
- package/dist/json.js.map +1 -0
- package/dist/neo4j.d.ts +1 -0
- package/dist/neo4j.js +2046 -0
- package/dist/neo4j.js.map +1 -0
- package/dist/pajek.d.ts +1 -0
- package/dist/pajek.js +8 -0
- package/dist/pajek.js.map +1 -0
- package/dist/src/children.d.ts +134 -0
- package/dist/src/children.d.ts.map +1 -0
- package/dist/src/children.js +274 -0
- package/dist/src/children.js.map +1 -0
- package/dist/src/common/attributes.d.ts +229 -0
- package/dist/src/common/attributes.d.ts.map +1 -0
- package/dist/src/common/attributes.js +368 -0
- package/dist/src/common/attributes.js.map +1 -0
- package/dist/src/common/codes.d.ts +105 -0
- package/dist/src/common/codes.d.ts.map +1 -0
- package/dist/src/common/codes.js +107 -0
- package/dist/src/common/codes.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +84 -0
- package/dist/src/common/declared-types.d.ts.map +1 -0
- package/dist/src/common/declared-types.js +326 -0
- package/dist/src/common/declared-types.js.map +1 -0
- package/dist/src/common/direction.d.ts +206 -0
- package/dist/src/common/direction.d.ts.map +1 -0
- package/dist/src/common/direction.js +370 -0
- package/dist/src/common/direction.js.map +1 -0
- package/dist/src/common/escape.d.ts +92 -0
- package/dist/src/common/escape.d.ts.map +1 -0
- package/dist/src/common/escape.js +212 -0
- package/dist/src/common/escape.js.map +1 -0
- package/dist/src/common/export.d.ts +249 -0
- package/dist/src/common/export.d.ts.map +1 -0
- package/dist/src/common/export.js +594 -0
- package/dist/src/common/export.js.map +1 -0
- package/dist/src/common/format.d.ts +59 -0
- package/dist/src/common/format.d.ts.map +1 -0
- package/dist/src/common/format.js +106 -0
- package/dist/src/common/format.js.map +1 -0
- package/dist/src/common/ids.d.ts +83 -0
- package/dist/src/common/ids.d.ts.map +1 -0
- package/dist/src/common/ids.js +158 -0
- package/dist/src/common/ids.js.map +1 -0
- package/dist/src/common/input.d.ts +100 -0
- package/dist/src/common/input.d.ts.map +1 -0
- package/dist/src/common/input.js +335 -0
- package/dist/src/common/input.js.map +1 -0
- package/dist/src/common/lists.d.ts +34 -0
- package/dist/src/common/lists.d.ts.map +1 -0
- package/dist/src/common/lists.js +185 -0
- package/dist/src/common/lists.js.map +1 -0
- package/dist/src/common/options.d.ts +108 -0
- package/dist/src/common/options.d.ts.map +1 -0
- package/dist/src/common/options.js +265 -0
- package/dist/src/common/options.js.map +1 -0
- package/dist/src/common/report.d.ts +187 -0
- package/dist/src/common/report.d.ts.map +1 -0
- package/dist/src/common/report.js +274 -0
- package/dist/src/common/report.js.map +1 -0
- package/dist/src/common/temporal.d.ts +71 -0
- package/dist/src/common/temporal.d.ts.map +1 -0
- package/dist/src/common/temporal.js +266 -0
- package/dist/src/common/temporal.js.map +1 -0
- package/dist/src/common/text.d.ts +104 -0
- package/dist/src/common/text.d.ts.map +1 -0
- package/dist/src/common/text.js +255 -0
- package/dist/src/common/text.js.map +1 -0
- package/dist/src/common/weights.d.ts +77 -0
- package/dist/src/common/weights.d.ts.map +1 -0
- package/dist/src/common/weights.js +156 -0
- package/dist/src/common/weights.js.map +1 -0
- package/dist/src/common/writer.d.ts +51 -0
- package/dist/src/common/writer.d.ts.map +1 -0
- package/dist/src/common/writer.js +108 -0
- package/dist/src/common/writer.js.map +1 -0
- package/dist/src/common/xml.d.ts +245 -0
- package/dist/src/common/xml.d.ts.map +1 -0
- package/dist/src/common/xml.js +942 -0
- package/dist/src/common/xml.js.map +1 -0
- package/dist/src/formats/csv/exporter.d.ts +70 -0
- package/dist/src/formats/csv/exporter.d.ts.map +1 -0
- package/dist/src/formats/csv/exporter.js +682 -0
- package/dist/src/formats/csv/exporter.js.map +1 -0
- package/dist/src/formats/csv/header.d.ts +66 -0
- package/dist/src/formats/csv/header.d.ts.map +1 -0
- package/dist/src/formats/csv/header.js +152 -0
- package/dist/src/formats/csv/header.js.map +1 -0
- package/dist/src/formats/csv/importer.d.ts +82 -0
- package/dist/src/formats/csv/importer.d.ts.map +1 -0
- package/dist/src/formats/csv/importer.js +849 -0
- package/dist/src/formats/csv/importer.js.map +1 -0
- package/dist/src/formats/csv/index.d.ts +60 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -0
- package/dist/src/formats/csv/index.js +63 -0
- package/dist/src/formats/csv/index.js.map +1 -0
- package/dist/src/formats/csv/records.d.ts +188 -0
- package/dist/src/formats/csv/records.d.ts.map +1 -0
- package/dist/src/formats/csv/records.js +702 -0
- package/dist/src/formats/csv/records.js.map +1 -0
- package/dist/src/formats/csv/values.d.ts +105 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -0
- package/dist/src/formats/csv/values.js +192 -0
- package/dist/src/formats/csv/values.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +52 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -0
- package/dist/src/formats/dot/exporter.js +836 -0
- package/dist/src/formats/dot/exporter.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts +102 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -0
- package/dist/src/formats/dot/importer.js +1291 -0
- package/dist/src/formats/dot/importer.js.map +1 -0
- package/dist/src/formats/dot/index.d.ts +7 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -0
- package/dist/src/formats/dot/index.js +7 -0
- package/dist/src/formats/dot/index.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +29 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -0
- package/dist/src/formats/dot/names.js +28 -0
- package/dist/src/formats/dot/names.js.map +1 -0
- package/dist/src/formats/dot/tokenizer.d.ts +114 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
- package/dist/src/formats/dot/tokenizer.js +341 -0
- package/dist/src/formats/dot/tokenizer.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts +56 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
- package/dist/src/formats/gexf/exporter.js +1395 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +73 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -0
- package/dist/src/formats/gexf/importer.js +1880 -0
- package/dist/src/formats/gexf/importer.js.map +1 -0
- package/dist/src/formats/gexf/index.d.ts +96 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -0
- package/dist/src/formats/gexf/index.js +97 -0
- package/dist/src/formats/gexf/index.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts +135 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -0
- package/dist/src/formats/gexf/schema.js +323 -0
- package/dist/src/formats/gexf/schema.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +69 -0
- package/dist/src/formats/gml/exporter.d.ts.map +1 -0
- package/dist/src/formats/gml/exporter.js +1093 -0
- package/dist/src/formats/gml/exporter.js.map +1 -0
- package/dist/src/formats/gml/importer.d.ts +66 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -0
- package/dist/src/formats/gml/importer.js +1331 -0
- package/dist/src/formats/gml/importer.js.map +1 -0
- package/dist/src/formats/gml/index.d.ts +85 -0
- package/dist/src/formats/gml/index.d.ts.map +1 -0
- package/dist/src/formats/gml/index.js +88 -0
- package/dist/src/formats/gml/index.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +186 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -0
- package/dist/src/formats/gml/syntax.js +467 -0
- package/dist/src/formats/gml/syntax.js.map +1 -0
- package/dist/src/formats/graphml/constants.d.ts +169 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -0
- package/dist/src/formats/graphml/constants.js +165 -0
- package/dist/src/formats/graphml/constants.js.map +1 -0
- package/dist/src/formats/graphml/exporter.d.ts +34 -0
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
- package/dist/src/formats/graphml/exporter.js +1176 -0
- package/dist/src/formats/graphml/exporter.js.map +1 -0
- package/dist/src/formats/graphml/importer.d.ts +31 -0
- package/dist/src/formats/graphml/importer.d.ts.map +1 -0
- package/dist/src/formats/graphml/importer.js +1607 -0
- package/dist/src/formats/graphml/importer.js.map +1 -0
- package/dist/src/formats/graphml/index.d.ts +8 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -0
- package/dist/src/formats/graphml/index.js +8 -0
- package/dist/src/formats/graphml/index.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts +72 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -0
- package/dist/src/formats/graphml/tree.js +290 -0
- package/dist/src/formats/graphml/tree.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +125 -0
- package/dist/src/formats/json/dialect.d.ts.map +1 -0
- package/dist/src/formats/json/dialect.js +262 -0
- package/dist/src/formats/json/dialect.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts +89 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -0
- package/dist/src/formats/json/exporter.js +1358 -0
- package/dist/src/formats/json/exporter.js.map +1 -0
- package/dist/src/formats/json/importer.d.ts +108 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -0
- package/dist/src/formats/json/importer.js +1838 -0
- package/dist/src/formats/json/importer.js.map +1 -0
- package/dist/src/formats/json/index.d.ts +8 -0
- package/dist/src/formats/json/index.d.ts.map +1 -0
- package/dist/src/formats/json/index.js +8 -0
- package/dist/src/formats/json/index.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts +68 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
- package/dist/src/formats/neo4j/exporter.js +1055 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -0
- package/dist/src/formats/neo4j/header.d.ts +52 -0
- package/dist/src/formats/neo4j/header.d.ts.map +1 -0
- package/dist/src/formats/neo4j/header.js +131 -0
- package/dist/src/formats/neo4j/header.js.map +1 -0
- package/dist/src/formats/neo4j/importer.d.ts +73 -0
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
- package/dist/src/formats/neo4j/importer.js +932 -0
- package/dist/src/formats/neo4j/importer.js.map +1 -0
- package/dist/src/formats/neo4j/index.d.ts +79 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -0
- package/dist/src/formats/neo4j/index.js +83 -0
- package/dist/src/formats/neo4j/index.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts +58 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
- package/dist/src/formats/pajek/exporter.js +825 -0
- package/dist/src/formats/pajek/exporter.js.map +1 -0
- package/dist/src/formats/pajek/importer.d.ts +88 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -0
- package/dist/src/formats/pajek/importer.js +1047 -0
- package/dist/src/formats/pajek/importer.js.map +1 -0
- package/dist/src/formats/pajek/index.d.ts +7 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -0
- package/dist/src/formats/pajek/index.js +7 -0
- package/dist/src/formats/pajek/index.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +112 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
- package/dist/src/formats/pajek/syntax.js +269 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +207 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +481 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/sniff.d.ts +104 -0
- package/dist/src/sniff.d.ts.map +1 -0
- package/dist/src/sniff.js +357 -0
- package/dist/src/sniff.js.map +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +29 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +122 -7
- package/src/children.ts +335 -0
- package/src/common/attributes.ts +520 -0
- package/src/common/codes.ts +153 -0
- package/src/common/declared-types.ts +374 -0
- package/src/common/direction.ts +518 -0
- package/src/common/escape.ts +231 -0
- package/src/common/export.ts +817 -0
- package/src/common/format.ts +111 -0
- package/src/common/ids.ts +176 -0
- package/src/common/input.ts +378 -0
- package/src/common/lists.ts +196 -0
- package/src/common/options.ts +377 -0
- package/src/common/report.ts +352 -0
- package/src/common/temporal.ts +302 -0
- package/src/common/text.ts +294 -0
- package/src/common/weights.ts +202 -0
- package/src/common/writer.ts +123 -0
- package/src/common/xml.ts +1053 -0
- package/src/formats/csv/exporter.ts +894 -0
- package/src/formats/csv/header.ts +172 -0
- package/src/formats/csv/importer.ts +1104 -0
- package/src/formats/csv/index.ts +88 -0
- package/src/formats/csv/records.ts +813 -0
- package/src/formats/csv/values.ts +224 -0
- package/src/formats/dot/exporter.ts +1014 -0
- package/src/formats/dot/importer.ts +1549 -0
- package/src/formats/dot/index.ts +7 -0
- package/src/formats/dot/names.ts +40 -0
- package/src/formats/dot/tokenizer.ts +384 -0
- package/src/formats/gexf/exporter.ts +1696 -0
- package/src/formats/gexf/importer.ts +2333 -0
- package/src/formats/gexf/index.ts +142 -0
- package/src/formats/gexf/schema.ts +361 -0
- package/src/formats/gml/exporter.ts +1404 -0
- package/src/formats/gml/importer.ts +1591 -0
- package/src/formats/gml/index.ts +128 -0
- package/src/formats/gml/syntax.ts +545 -0
- package/src/formats/graphml/constants.ts +225 -0
- package/src/formats/graphml/exporter.ts +1458 -0
- package/src/formats/graphml/importer.ts +2027 -0
- package/src/formats/graphml/index.ts +8 -0
- package/src/formats/graphml/tree.ts +318 -0
- package/src/formats/json/dialect.ts +317 -0
- package/src/formats/json/exporter.ts +1616 -0
- package/src/formats/json/importer.ts +2271 -0
- package/src/formats/json/index.ts +8 -0
- package/src/formats/neo4j/exporter.ts +1287 -0
- package/src/formats/neo4j/header.ts +156 -0
- package/src/formats/neo4j/importer.ts +1220 -0
- package/src/formats/neo4j/index.ts +116 -0
- package/src/formats/pajek/exporter.ts +1000 -0
- package/src/formats/pajek/importer.ts +1311 -0
- package/src/formats/pajek/index.ts +7 -0
- package/src/formats/pajek/syntax.ts +307 -0
- package/src/index.ts +244 -0
- package/src/registry.ts +617 -0
- package/src/sniff.ts +397 -0
- package/src/types.ts +262 -0
package/dist/gexf.js
ADDED
|
@@ -0,0 +1,3466 @@
|
|
|
1
|
+
import { aJ as TIME_TEXT_ROLE, aK as TIME_TEXT_SUFFIX, aC as resolveImportOptions, I as ImportReportBuilder, az as reportSinkOptions, aA as reportUnusedOptions, t as textChunks, a7 as XML_SYNTAX_CODE, a as throwIfAborted, s as IdCoercer, l as DirectionResolver, Y as STRAY_TEXT_CODE, G as NO_GRAPH_CODE, d as COUNT_HINT_CODE, at as isWeightField, aL as declareAttribute, af as declareResolved, w as MISSING_ID_CODE, k as DUPLICATE_NODE_CODE, a3 as UNKNOWN_PARENT_CODE, M as MISSING_ENDPOINT_CODE, i as DUPLICATE_EDGE_ID_CODE, ax as parseWeightText, aM as parseDeclaredTemporal, ae as declareCompanion, aN as parseDeclaredValue, aO as losesPrecision, R as PRECISION_CODE, aP as parseTimeText, aQ as splitListText, a2 as UNKNOWN_ELEMENT_CODE, n as ID_MERGED_CODE, aR as timeTextCompanion, aS as parseDecimalText, a9 as capabilities, L as LOSS, au as joinText, ah as encodeChunks, aD as sanitizeIds, aw as pairFolding, ai as explicitWeights, aB as resolveExportOptions, aa as checkCapabilities, aT as formatTimeValue, ak as formatF32, al as formatF64, aU as mapDeclaredType, aV as joinListText, aW as formatTemporal, an as formatInteger, aq as isCanonicalIntegerText, x as MIXED_DIRECTION_CODE, g as DIRECTION_FORCED_CODE, h as DIRECTION_REFUSED_CODE, W as SINK_OPTION_CODE, J as OPTION_IGNORED_CODE, V as ROLE_TAKEN_CODE, C as COLUMN_RENAMED_CODE, b as BAD_OPTIONS_CODE, B as BAD_DEFAULT_CODE, a1 as UNKNOWN_ATTR_TYPE_CODE, r as INVALID_UTF8_CODE } from "./chunks/writer-DxSKC7TL.js";
|
|
2
|
+
import { GraphFormatError, INVALID_INDEX, MAX_COUNT } from "@graphty/graph-format";
|
|
3
|
+
import { t as tokenizeXml, X as XmlSyntaxError, l as localName, d as isWhitespace, x as xmlIllegalTextNotes, e as escapeXmlText, f as escapeXmlAttribute } from "./chunks/escape-DyI8JofU.js";
|
|
4
|
+
const GEXF_FORMAT = "gexf";
|
|
5
|
+
const GEXF_NAMESPACES = {
|
|
6
|
+
"1.2": { gexf: "http://www.gexf.net/1.2draft", viz: "http://www.gexf.net/1.2draft/viz" },
|
|
7
|
+
"1.3": { gexf: "http://gexf.net/1.3", viz: "http://gexf.net/1.3/viz" }
|
|
8
|
+
};
|
|
9
|
+
const TIME_FORMATS = /* @__PURE__ */ new Set(["integer", "double", "date", "dateTime"]);
|
|
10
|
+
const GRAPH_MODES = /* @__PURE__ */ new Set(["static", "dynamic", "slice"]);
|
|
11
|
+
const TIME_REPRESENTATIONS = /* @__PURE__ */ new Set(["interval", "timestamp"]);
|
|
12
|
+
const VIZ_NAMESPACE = "viz";
|
|
13
|
+
const XML_ORIGIN = { format: GEXF_FORMAT };
|
|
14
|
+
const VIZ_ORIGIN = { format: GEXF_FORMAT, namespace: VIZ_NAMESPACE };
|
|
15
|
+
const NODE_COLUMNS = Object.freeze({
|
|
16
|
+
label: "label",
|
|
17
|
+
parent: "parent",
|
|
18
|
+
parents: "parents",
|
|
19
|
+
start: "start",
|
|
20
|
+
end: "end",
|
|
21
|
+
timestamp: "timestamp",
|
|
22
|
+
spells: "spells",
|
|
23
|
+
timestamps: "timestamps",
|
|
24
|
+
open: "open",
|
|
25
|
+
position: "position",
|
|
26
|
+
color: "color",
|
|
27
|
+
size: "size",
|
|
28
|
+
shape: "shape",
|
|
29
|
+
shapeUri: "shape.uri"
|
|
30
|
+
});
|
|
31
|
+
const EDGE_COLUMNS = Object.freeze({
|
|
32
|
+
id: "id",
|
|
33
|
+
label: "label",
|
|
34
|
+
kind: "kind",
|
|
35
|
+
start: "start",
|
|
36
|
+
end: "end",
|
|
37
|
+
timestamp: "timestamp",
|
|
38
|
+
spells: "spells",
|
|
39
|
+
timestamps: "timestamps",
|
|
40
|
+
open: "open",
|
|
41
|
+
color: "color",
|
|
42
|
+
thickness: "thickness",
|
|
43
|
+
shape: "shape"
|
|
44
|
+
});
|
|
45
|
+
const RESERVED_NODE_NAMES = /* @__PURE__ */ new Set([
|
|
46
|
+
...Object.values(NODE_COLUMNS),
|
|
47
|
+
NODE_COLUMNS.start + TIME_TEXT_SUFFIX,
|
|
48
|
+
NODE_COLUMNS.end + TIME_TEXT_SUFFIX,
|
|
49
|
+
NODE_COLUMNS.timestamp + TIME_TEXT_SUFFIX
|
|
50
|
+
]);
|
|
51
|
+
const RESERVED_EDGE_NAMES = /* @__PURE__ */ new Set([
|
|
52
|
+
...Object.values(EDGE_COLUMNS),
|
|
53
|
+
EDGE_COLUMNS.start + TIME_TEXT_SUFFIX,
|
|
54
|
+
EDGE_COLUMNS.end + TIME_TEXT_SUFFIX,
|
|
55
|
+
EDGE_COLUMNS.timestamp + TIME_TEXT_SUFFIX
|
|
56
|
+
]);
|
|
57
|
+
const NODE_DECLS = {
|
|
58
|
+
label: { name: NODE_COLUMNS.label, dtype: "string", role: "label", nullable: true, origin: XML_ORIGIN },
|
|
59
|
+
parent: {
|
|
60
|
+
name: NODE_COLUMNS.parent,
|
|
61
|
+
dtype: "u32",
|
|
62
|
+
role: "parent",
|
|
63
|
+
refersTo: "node",
|
|
64
|
+
nullable: true,
|
|
65
|
+
origin: XML_ORIGIN
|
|
66
|
+
},
|
|
67
|
+
parents: {
|
|
68
|
+
name: NODE_COLUMNS.parents,
|
|
69
|
+
dtype: "list",
|
|
70
|
+
itemDtype: "u32",
|
|
71
|
+
role: "parents",
|
|
72
|
+
refersTo: "node",
|
|
73
|
+
nullable: true,
|
|
74
|
+
origin: XML_ORIGIN
|
|
75
|
+
},
|
|
76
|
+
start: { name: NODE_COLUMNS.start, dtype: "f64", role: "start", nullable: true, origin: XML_ORIGIN },
|
|
77
|
+
end: { name: NODE_COLUMNS.end, dtype: "f64", role: "end", nullable: true, origin: XML_ORIGIN },
|
|
78
|
+
timestamp: { name: NODE_COLUMNS.timestamp, dtype: "f64", role: "timestamp", nullable: true, origin: XML_ORIGIN },
|
|
79
|
+
spells: {
|
|
80
|
+
name: NODE_COLUMNS.spells,
|
|
81
|
+
dtype: "list",
|
|
82
|
+
itemDtype: "f64",
|
|
83
|
+
itemComponents: 2,
|
|
84
|
+
role: "spells",
|
|
85
|
+
nullable: true,
|
|
86
|
+
origin: XML_ORIGIN
|
|
87
|
+
},
|
|
88
|
+
timestamps: {
|
|
89
|
+
name: NODE_COLUMNS.timestamps,
|
|
90
|
+
dtype: "list",
|
|
91
|
+
itemDtype: "f64",
|
|
92
|
+
role: "timestamps",
|
|
93
|
+
nullable: true,
|
|
94
|
+
origin: XML_ORIGIN
|
|
95
|
+
},
|
|
96
|
+
open: { name: NODE_COLUMNS.open, dtype: "u8", role: "open", nullable: true, origin: XML_ORIGIN },
|
|
97
|
+
position: {
|
|
98
|
+
name: NODE_COLUMNS.position,
|
|
99
|
+
dtype: "f32",
|
|
100
|
+
components: 3,
|
|
101
|
+
role: "position",
|
|
102
|
+
mutable: true,
|
|
103
|
+
nullable: true,
|
|
104
|
+
origin: VIZ_ORIGIN,
|
|
105
|
+
extra: { units: "file" }
|
|
106
|
+
},
|
|
107
|
+
color: {
|
|
108
|
+
name: NODE_COLUMNS.color,
|
|
109
|
+
dtype: "f32",
|
|
110
|
+
components: 4,
|
|
111
|
+
role: "color",
|
|
112
|
+
nullable: true,
|
|
113
|
+
origin: VIZ_ORIGIN
|
|
114
|
+
},
|
|
115
|
+
size: { name: NODE_COLUMNS.size, dtype: "f32", role: "size", nullable: true, origin: VIZ_ORIGIN },
|
|
116
|
+
shape: { name: NODE_COLUMNS.shape, dtype: "dict", role: "shape", nullable: true, origin: VIZ_ORIGIN },
|
|
117
|
+
shapeUri: { name: NODE_COLUMNS.shapeUri, dtype: "string", nullable: true, origin: VIZ_ORIGIN }
|
|
118
|
+
};
|
|
119
|
+
const EDGE_DECLS = {
|
|
120
|
+
id: { name: EDGE_COLUMNS.id, dtype: "string", role: "id", unique: true, nullable: true, origin: XML_ORIGIN },
|
|
121
|
+
label: { name: EDGE_COLUMNS.label, dtype: "string", role: "label", nullable: true, origin: XML_ORIGIN },
|
|
122
|
+
kind: { name: EDGE_COLUMNS.kind, dtype: "dict", role: "kind", nullable: true, origin: XML_ORIGIN },
|
|
123
|
+
start: { name: EDGE_COLUMNS.start, dtype: "f64", role: "start", nullable: true, origin: XML_ORIGIN },
|
|
124
|
+
end: { name: EDGE_COLUMNS.end, dtype: "f64", role: "end", nullable: true, origin: XML_ORIGIN },
|
|
125
|
+
timestamp: { name: EDGE_COLUMNS.timestamp, dtype: "f64", role: "timestamp", nullable: true, origin: XML_ORIGIN },
|
|
126
|
+
spells: {
|
|
127
|
+
name: EDGE_COLUMNS.spells,
|
|
128
|
+
dtype: "list",
|
|
129
|
+
itemDtype: "f64",
|
|
130
|
+
itemComponents: 2,
|
|
131
|
+
role: "spells",
|
|
132
|
+
nullable: true,
|
|
133
|
+
origin: XML_ORIGIN
|
|
134
|
+
},
|
|
135
|
+
timestamps: {
|
|
136
|
+
name: EDGE_COLUMNS.timestamps,
|
|
137
|
+
dtype: "list",
|
|
138
|
+
itemDtype: "f64",
|
|
139
|
+
role: "timestamps",
|
|
140
|
+
nullable: true,
|
|
141
|
+
origin: XML_ORIGIN
|
|
142
|
+
},
|
|
143
|
+
open: { name: EDGE_COLUMNS.open, dtype: "u8", role: "open", nullable: true, origin: XML_ORIGIN },
|
|
144
|
+
color: {
|
|
145
|
+
name: EDGE_COLUMNS.color,
|
|
146
|
+
dtype: "f32",
|
|
147
|
+
components: 4,
|
|
148
|
+
role: "color",
|
|
149
|
+
nullable: true,
|
|
150
|
+
origin: VIZ_ORIGIN
|
|
151
|
+
},
|
|
152
|
+
thickness: { name: EDGE_COLUMNS.thickness, dtype: "f32", role: "thickness", nullable: true, origin: VIZ_ORIGIN },
|
|
153
|
+
shape: { name: EDGE_COLUMNS.shape, dtype: "dict", role: "shape", nullable: true, origin: VIZ_ORIGIN }
|
|
154
|
+
};
|
|
155
|
+
const OPEN_START = 1;
|
|
156
|
+
const OPEN_END = 2;
|
|
157
|
+
const TEMPORAL_TABLE_PREFIX = "temporal:";
|
|
158
|
+
const TEMPORAL_COLUMNS = Object.freeze({
|
|
159
|
+
element: "element",
|
|
160
|
+
start: "start",
|
|
161
|
+
end: "end",
|
|
162
|
+
value: "value",
|
|
163
|
+
open: "open",
|
|
164
|
+
startText: `start${TIME_TEXT_SUFFIX}`,
|
|
165
|
+
endText: `end${TIME_TEXT_SUFFIX}`,
|
|
166
|
+
valueText: `value${TIME_TEXT_SUFFIX}`
|
|
167
|
+
});
|
|
168
|
+
function temporalTableName(domain, column) {
|
|
169
|
+
return `${TEMPORAL_TABLE_PREFIX}${domain}:${column}`;
|
|
170
|
+
}
|
|
171
|
+
function parseTemporalTableName(name) {
|
|
172
|
+
if (!name.startsWith(TEMPORAL_TABLE_PREFIX)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const rest = name.slice(TEMPORAL_TABLE_PREFIX.length);
|
|
176
|
+
const colon = rest.indexOf(":");
|
|
177
|
+
if (colon < 0) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const domain = rest.slice(0, colon);
|
|
181
|
+
if (domain !== "node" && domain !== "edge") {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return { domain, column: rest.slice(colon + 1) };
|
|
185
|
+
}
|
|
186
|
+
function temporalTableDecls(domain, value, temporalValue) {
|
|
187
|
+
const decls = [
|
|
188
|
+
{ name: TEMPORAL_COLUMNS.element, dtype: "u32", refersTo: domain, nullable: false },
|
|
189
|
+
{ name: TEMPORAL_COLUMNS.start, dtype: "f64", role: "start", nullable: true },
|
|
190
|
+
{ name: TEMPORAL_COLUMNS.end, dtype: "f64", role: "end", nullable: true },
|
|
191
|
+
{ name: TEMPORAL_COLUMNS.value, ...value, nullable: true },
|
|
192
|
+
{
|
|
193
|
+
name: TEMPORAL_COLUMNS.startText,
|
|
194
|
+
dtype: "string",
|
|
195
|
+
role: TIME_TEXT_ROLE,
|
|
196
|
+
nullable: true,
|
|
197
|
+
extra: { for: TEMPORAL_COLUMNS.start }
|
|
198
|
+
},
|
|
199
|
+
{ name: TEMPORAL_COLUMNS.endText, dtype: "string", nullable: true, extra: { for: TEMPORAL_COLUMNS.end } },
|
|
200
|
+
{ name: TEMPORAL_COLUMNS.open, dtype: "u8", role: "open", nullable: true }
|
|
201
|
+
];
|
|
202
|
+
if (temporalValue) {
|
|
203
|
+
decls.push({
|
|
204
|
+
name: TEMPORAL_COLUMNS.valueText,
|
|
205
|
+
dtype: "string",
|
|
206
|
+
nullable: true,
|
|
207
|
+
extra: { for: TEMPORAL_COLUMNS.value }
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return decls;
|
|
211
|
+
}
|
|
212
|
+
const TYPES_1_2 = /* @__PURE__ */ new Set(["integer", "long", "double", "float", "boolean", "string", "anyURI"]);
|
|
213
|
+
const TYPES_1_3 = /* @__PURE__ */ new Set([
|
|
214
|
+
...TYPES_1_2,
|
|
215
|
+
"bigdecimal",
|
|
216
|
+
"biginteger",
|
|
217
|
+
"byte",
|
|
218
|
+
"char",
|
|
219
|
+
"short",
|
|
220
|
+
"date",
|
|
221
|
+
"dateTime"
|
|
222
|
+
]);
|
|
223
|
+
function isScalarType(type, version) {
|
|
224
|
+
return (version === "1.2" ? TYPES_1_2 : TYPES_1_3).has(type);
|
|
225
|
+
}
|
|
226
|
+
function isListType(type, version) {
|
|
227
|
+
return type.startsWith("list") && type.length > 4 && isScalarType(type.slice(4), "1.3");
|
|
228
|
+
}
|
|
229
|
+
function canonicalScalarType(dtype) {
|
|
230
|
+
switch (dtype) {
|
|
231
|
+
case "bool":
|
|
232
|
+
return "boolean";
|
|
233
|
+
case "i32":
|
|
234
|
+
case "u8":
|
|
235
|
+
return "integer";
|
|
236
|
+
case "u32":
|
|
237
|
+
return "long";
|
|
238
|
+
case "f32":
|
|
239
|
+
return "float";
|
|
240
|
+
case "f64":
|
|
241
|
+
return "double";
|
|
242
|
+
case "dict":
|
|
243
|
+
case "string":
|
|
244
|
+
case "json":
|
|
245
|
+
case "list":
|
|
246
|
+
return "string";
|
|
247
|
+
default: {
|
|
248
|
+
const name = dtype;
|
|
249
|
+
return name;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const NOT_GEXF_CODE = "E_NOT_GEXF";
|
|
254
|
+
const MISSING_NODES_CODE = "E_GEXF_MISSING_NODES";
|
|
255
|
+
const EDGE_TYPE_CODE = "E_GEXF_EDGE_TYPE";
|
|
256
|
+
const ATTRIBUTES_CLASS_CODE = "E_GEXF_ATTRIBUTES_CLASS";
|
|
257
|
+
const ATTRIBUTE_ID_CODE = "E_GEXF_ATTRIBUTE_ID";
|
|
258
|
+
const HEADER_VALUE_CODE = "W_GEXF_HEADER_VALUE";
|
|
259
|
+
const DUPLICATE_ATTRIBUTE_CODE = "W_GEXF_DUPLICATE_ATTRIBUTE";
|
|
260
|
+
const ATTRIBUTE_TYPE_CODE = "W_GEXF_ATTRIBUTE_TYPE";
|
|
261
|
+
const UNKNOWN_ATTRIBUTE_CODE = "W_GEXF_UNKNOWN_ATTRIBUTE";
|
|
262
|
+
const ATTVALUE_SHAPE_CODE = "W_GEXF_ATTVALUE_SHAPE";
|
|
263
|
+
const TIMED_STATIC_CODE = "W_GEXF_TIMED_VALUE_ON_STATIC";
|
|
264
|
+
const WEIGHT_IGNORED_CODE = "W_GEXF_WEIGHT_IGNORED";
|
|
265
|
+
const VIZ_SKIPPED_CODE = "W_GEXF_VIZ_SKIPPED";
|
|
266
|
+
const VIZ_DYNAMIC_CODE = "W_GEXF_VIZ_DYNAMIC_DROPPED";
|
|
267
|
+
const SPELL_OPEN_CODE = "W_GEXF_SPELL_OPEN_DROPPED";
|
|
268
|
+
const VIZ_VALUE_CODE = "W_GEXF_VIZ_VALUE";
|
|
269
|
+
const OPEN_BOUND_CONFLICT_CODE = "W_GEXF_OPEN_BOUND_CONFLICT";
|
|
270
|
+
const EDGE_TYPES = /* @__PURE__ */ new Set(["directed", "undirected", "mutual"]);
|
|
271
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
272
|
+
const NODE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
273
|
+
"id",
|
|
274
|
+
"label",
|
|
275
|
+
"pid",
|
|
276
|
+
"start",
|
|
277
|
+
"end",
|
|
278
|
+
"timestamp",
|
|
279
|
+
"startopen",
|
|
280
|
+
"endopen",
|
|
281
|
+
"timestamps",
|
|
282
|
+
"intervals"
|
|
283
|
+
]);
|
|
284
|
+
const EDGE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
285
|
+
"id",
|
|
286
|
+
"source",
|
|
287
|
+
"target",
|
|
288
|
+
"type",
|
|
289
|
+
"label",
|
|
290
|
+
"weight",
|
|
291
|
+
"kind",
|
|
292
|
+
"start",
|
|
293
|
+
"end",
|
|
294
|
+
"timestamp",
|
|
295
|
+
"startopen",
|
|
296
|
+
"endopen",
|
|
297
|
+
"timestamps",
|
|
298
|
+
"intervals"
|
|
299
|
+
]);
|
|
300
|
+
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
301
|
+
"ids",
|
|
302
|
+
"addMissingNodes",
|
|
303
|
+
"duplicateEdges",
|
|
304
|
+
"selfLoops",
|
|
305
|
+
"onMixedDirection",
|
|
306
|
+
"defaultDirected",
|
|
307
|
+
"weightFrom",
|
|
308
|
+
"weightDtype",
|
|
309
|
+
"long",
|
|
310
|
+
"errorLimit",
|
|
311
|
+
"signal",
|
|
312
|
+
"onProgress"
|
|
313
|
+
]);
|
|
314
|
+
class LazyColumn {
|
|
315
|
+
/**
|
|
316
|
+
* Create a lazily declared column.
|
|
317
|
+
* @param domain - node or edge
|
|
318
|
+
* @param decl - the declaration
|
|
319
|
+
*/
|
|
320
|
+
constructor(domain, decl) {
|
|
321
|
+
this.handle = INVALID_INDEX;
|
|
322
|
+
this.domain = domain;
|
|
323
|
+
this.decl = decl;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The handle, declaring the column on the first call (renamed or without its role when the
|
|
327
|
+
* sink already holds the name or the role, reported).
|
|
328
|
+
* @param sink - the sink
|
|
329
|
+
* @param report - the report
|
|
330
|
+
* @param where - the location of the first use
|
|
331
|
+
* @returns the handle
|
|
332
|
+
*/
|
|
333
|
+
get(sink, report, where) {
|
|
334
|
+
if (this.handle === INVALID_INDEX) {
|
|
335
|
+
this.handle = declareResolved(sink, this.domain, this.decl, report, where).handle;
|
|
336
|
+
}
|
|
337
|
+
return this.handle;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Write one cell, declaring the column first when needed.
|
|
341
|
+
* @param sink - the sink
|
|
342
|
+
* @param report - the report
|
|
343
|
+
* @param index - the row
|
|
344
|
+
* @param value - the value
|
|
345
|
+
* @param where - the location, for a declaration issue
|
|
346
|
+
*/
|
|
347
|
+
set(sink, report, index, value, where) {
|
|
348
|
+
const handle = this.get(sink, report, where);
|
|
349
|
+
if (this.domain === "node") {
|
|
350
|
+
sink.setNodeValue(handle, index, value);
|
|
351
|
+
} else {
|
|
352
|
+
sink.setEdgeValue(handle, index, value);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
class TemporalColumn {
|
|
357
|
+
/**
|
|
358
|
+
* Create the pair.
|
|
359
|
+
* @param domain - node or edge
|
|
360
|
+
* @param decl - the numeric column's declaration
|
|
361
|
+
*/
|
|
362
|
+
constructor(domain, decl) {
|
|
363
|
+
this.companion = INVALID_INDEX;
|
|
364
|
+
this.domain = domain;
|
|
365
|
+
this.name = decl.name;
|
|
366
|
+
this.column = new LazyColumn(domain, decl);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Write a parsed time value and, when its text must be kept, the companion cell.
|
|
370
|
+
* @param sink - the sink
|
|
371
|
+
* @param report - the report
|
|
372
|
+
* @param index - the row
|
|
373
|
+
* @param value - the parsed value
|
|
374
|
+
* @param where - the location, for a declaration issue
|
|
375
|
+
*/
|
|
376
|
+
set(sink, report, index, value, where) {
|
|
377
|
+
this.column.set(sink, report, index, value.value, where);
|
|
378
|
+
if (value.text === null) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (this.companion === INVALID_INDEX) {
|
|
382
|
+
this.companion = declareCompanion(sink, this.domain, timeTextCompanion(this.name), report);
|
|
383
|
+
}
|
|
384
|
+
if (this.domain === "node") {
|
|
385
|
+
sink.setNodeValue(this.companion, index, value.text);
|
|
386
|
+
} else {
|
|
387
|
+
sink.setEdgeValue(this.companion, index, value.text);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
class GexfReader {
|
|
392
|
+
/**
|
|
393
|
+
* Set up one import.
|
|
394
|
+
* @param sink - the sink
|
|
395
|
+
* @param report - the report
|
|
396
|
+
* @param options - the resolved common options
|
|
397
|
+
* @param viz - whether viz elements are imported
|
|
398
|
+
*/
|
|
399
|
+
constructor(sink, report, options, viz) {
|
|
400
|
+
this.ctx = [];
|
|
401
|
+
this.timeFormat = null;
|
|
402
|
+
this.defaultKind = "undirected";
|
|
403
|
+
this.meta = { sourceFormat: GEXF_FORMAT };
|
|
404
|
+
this.metaText = "";
|
|
405
|
+
this.metaField = null;
|
|
406
|
+
this.nodeAttrs = /* @__PURE__ */ new Map();
|
|
407
|
+
this.edgeAttrs = /* @__PURE__ */ new Map();
|
|
408
|
+
this.declaredNodeNames = /* @__PURE__ */ new Set();
|
|
409
|
+
this.declaredEdgeNames = /* @__PURE__ */ new Set();
|
|
410
|
+
this.weightAttr = null;
|
|
411
|
+
this.parentColumn = new LazyColumn("node", NODE_DECLS.parent);
|
|
412
|
+
this.parentsColumn = new LazyColumn("node", NODE_DECLS.parents);
|
|
413
|
+
this.positionColumn = null;
|
|
414
|
+
this.sizeColumn = new LazyColumn("node", NODE_DECLS.size);
|
|
415
|
+
this.shapeUriColumn = new LazyColumn("node", NODE_DECLS.shapeUri);
|
|
416
|
+
this.thicknessColumn = new LazyColumn("edge", EDGE_DECLS.thickness);
|
|
417
|
+
this.edgeIdColumn = new LazyColumn("edge", EDGE_DECLS.id);
|
|
418
|
+
this.edgeKindColumn = new LazyColumn("edge", EDGE_DECLS.kind);
|
|
419
|
+
this.edgeIds = /* @__PURE__ */ new Set();
|
|
420
|
+
this.parentRefs = [];
|
|
421
|
+
this.parentsRefs = [];
|
|
422
|
+
this.unknownAttributes = /* @__PURE__ */ new Set();
|
|
423
|
+
this.nodeStack = [];
|
|
424
|
+
this.edge = null;
|
|
425
|
+
this.edgeFrame = {
|
|
426
|
+
index: INVALID_INDEX,
|
|
427
|
+
mirror: INVALID_INDEX,
|
|
428
|
+
where: { line: 0, element: null }
|
|
429
|
+
};
|
|
430
|
+
this.valueWhere = { line: 0, element: null };
|
|
431
|
+
this.attributeGroup = null;
|
|
432
|
+
this.pendingAttribute = null;
|
|
433
|
+
this.attributeText = null;
|
|
434
|
+
this.attributeTextValue = "";
|
|
435
|
+
this.spellPairs = null;
|
|
436
|
+
this.spellsWhere = null;
|
|
437
|
+
this.parentIds = null;
|
|
438
|
+
this.nodesSeen = false;
|
|
439
|
+
this.graphSeen = false;
|
|
440
|
+
this.elementsSinceCheck = 0;
|
|
441
|
+
this.sink = sink;
|
|
442
|
+
this.report = report;
|
|
443
|
+
this.options = options;
|
|
444
|
+
this.viz = viz;
|
|
445
|
+
this.coercer = new IdCoercer(options.ids);
|
|
446
|
+
this.resolver = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
447
|
+
this.nodeColumns = {
|
|
448
|
+
label: new LazyColumn("node", NODE_DECLS.label),
|
|
449
|
+
start: new TemporalColumn("node", NODE_DECLS.start),
|
|
450
|
+
end: new TemporalColumn("node", NODE_DECLS.end),
|
|
451
|
+
timestamp: new TemporalColumn("node", NODE_DECLS.timestamp),
|
|
452
|
+
spells: new LazyColumn("node", NODE_DECLS.spells),
|
|
453
|
+
timestamps: new LazyColumn("node", NODE_DECLS.timestamps),
|
|
454
|
+
open: new LazyColumn("node", NODE_DECLS.open),
|
|
455
|
+
color: new LazyColumn("node", NODE_DECLS.color),
|
|
456
|
+
shape: new LazyColumn("node", NODE_DECLS.shape)
|
|
457
|
+
};
|
|
458
|
+
this.edgeColumns = {
|
|
459
|
+
label: new LazyColumn("edge", EDGE_DECLS.label),
|
|
460
|
+
start: new TemporalColumn("edge", EDGE_DECLS.start),
|
|
461
|
+
end: new TemporalColumn("edge", EDGE_DECLS.end),
|
|
462
|
+
timestamp: new TemporalColumn("edge", EDGE_DECLS.timestamp),
|
|
463
|
+
spells: new LazyColumn("edge", EDGE_DECLS.spells),
|
|
464
|
+
timestamps: new LazyColumn("edge", EDGE_DECLS.timestamps),
|
|
465
|
+
open: new LazyColumn("edge", EDGE_DECLS.open),
|
|
466
|
+
color: new LazyColumn("edge", EDGE_DECLS.color),
|
|
467
|
+
shape: new LazyColumn("edge", EDGE_DECLS.shape)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
// ---------------------------------------------------------------- events
|
|
471
|
+
start(name, attrs, line) {
|
|
472
|
+
const parent = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
|
|
473
|
+
const local = localName(name);
|
|
474
|
+
switch (parent) {
|
|
475
|
+
case null:
|
|
476
|
+
if (local !== "gexf") {
|
|
477
|
+
this.report.fail(NOT_GEXF_CODE, `the root element is <${name}>, not <gexf>`, { line });
|
|
478
|
+
}
|
|
479
|
+
this.readRoot(attrs);
|
|
480
|
+
this.ctx.push(
|
|
481
|
+
0
|
|
482
|
+
/* Gexf */
|
|
483
|
+
);
|
|
484
|
+
return;
|
|
485
|
+
case 0:
|
|
486
|
+
this.startInGexf(name, local, attrs, line);
|
|
487
|
+
return;
|
|
488
|
+
case 1:
|
|
489
|
+
this.startInMeta(name, local, line);
|
|
490
|
+
return;
|
|
491
|
+
case 3:
|
|
492
|
+
this.startInGraph(name, local, attrs, line);
|
|
493
|
+
return;
|
|
494
|
+
case 4:
|
|
495
|
+
this.startInAttributes(name, local, attrs, line);
|
|
496
|
+
return;
|
|
497
|
+
case 5:
|
|
498
|
+
this.startInAttribute(name, local, line);
|
|
499
|
+
return;
|
|
500
|
+
case 7:
|
|
501
|
+
this.startInNodes(name, local, attrs, line);
|
|
502
|
+
return;
|
|
503
|
+
case 8:
|
|
504
|
+
this.startInNode(name, local, attrs, line);
|
|
505
|
+
return;
|
|
506
|
+
case 9:
|
|
507
|
+
this.startInAttvalues(name, local, attrs, line);
|
|
508
|
+
return;
|
|
509
|
+
case 10:
|
|
510
|
+
this.startInParents(name, local, attrs, line);
|
|
511
|
+
return;
|
|
512
|
+
case 11:
|
|
513
|
+
this.startInSpells(name, local, attrs, line);
|
|
514
|
+
return;
|
|
515
|
+
case 12:
|
|
516
|
+
this.startInEdges(name, local, attrs, line);
|
|
517
|
+
return;
|
|
518
|
+
case 13:
|
|
519
|
+
this.startInEdge(name, local, attrs, line);
|
|
520
|
+
return;
|
|
521
|
+
case 2:
|
|
522
|
+
case 6:
|
|
523
|
+
case 14:
|
|
524
|
+
this.ctx.push(
|
|
525
|
+
14
|
|
526
|
+
/* Skip */
|
|
527
|
+
);
|
|
528
|
+
return;
|
|
529
|
+
default:
|
|
530
|
+
this.unknownElement(name, line);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* An element ends.
|
|
535
|
+
* @param _name - the element name
|
|
536
|
+
* @param _line - the line (unused: issues of an element cite its start line)
|
|
537
|
+
*/
|
|
538
|
+
end(_name, _line) {
|
|
539
|
+
const ctx = this.ctx.pop();
|
|
540
|
+
switch (ctx) {
|
|
541
|
+
case 1:
|
|
542
|
+
this.finishMeta();
|
|
543
|
+
return;
|
|
544
|
+
case 2:
|
|
545
|
+
this.finishMetaText();
|
|
546
|
+
return;
|
|
547
|
+
case 3:
|
|
548
|
+
this.resolveParents();
|
|
549
|
+
return;
|
|
550
|
+
case 4:
|
|
551
|
+
this.attributeGroup = null;
|
|
552
|
+
return;
|
|
553
|
+
case 5:
|
|
554
|
+
this.finishAttribute();
|
|
555
|
+
return;
|
|
556
|
+
case 6:
|
|
557
|
+
this.finishAttributeText();
|
|
558
|
+
return;
|
|
559
|
+
case 8:
|
|
560
|
+
this.nodeStack.pop();
|
|
561
|
+
return;
|
|
562
|
+
case 13:
|
|
563
|
+
this.edge = null;
|
|
564
|
+
return;
|
|
565
|
+
case 10:
|
|
566
|
+
this.finishParents();
|
|
567
|
+
return;
|
|
568
|
+
case 11:
|
|
569
|
+
this.finishSpells();
|
|
570
|
+
return;
|
|
571
|
+
case 0:
|
|
572
|
+
case 7:
|
|
573
|
+
case 12:
|
|
574
|
+
case 9:
|
|
575
|
+
case 14:
|
|
576
|
+
case void 0:
|
|
577
|
+
return;
|
|
578
|
+
default: {
|
|
579
|
+
const value = ctx;
|
|
580
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown reader state ${String(value)}`, {});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Character data: kept inside `<creator>` / `<description>` / `<keywords>` and `<default>` /
|
|
586
|
+
* `<options>`, ignored elsewhere (reported once when it is not whitespace).
|
|
587
|
+
* @param text - the text
|
|
588
|
+
* @param line - the line
|
|
589
|
+
*/
|
|
590
|
+
text(text, line) {
|
|
591
|
+
const ctx = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
|
|
592
|
+
switch (ctx) {
|
|
593
|
+
case 2:
|
|
594
|
+
this.metaText += text;
|
|
595
|
+
return;
|
|
596
|
+
case 6:
|
|
597
|
+
this.attributeTextValue += text;
|
|
598
|
+
return;
|
|
599
|
+
case 14:
|
|
600
|
+
return;
|
|
601
|
+
default:
|
|
602
|
+
if (!isWhitespace(text)) {
|
|
603
|
+
this.report.warnOnce(
|
|
604
|
+
"validation-error",
|
|
605
|
+
STRAY_TEXT_CODE,
|
|
606
|
+
"text where GEXF allows only elements was ignored",
|
|
607
|
+
{ line }
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Apply what the root and `<meta>` gave to the sink; called once the graph's header is read.
|
|
614
|
+
*/
|
|
615
|
+
finish() {
|
|
616
|
+
if (!this.graphSeen) {
|
|
617
|
+
this.report.fail(NO_GRAPH_CODE, "the document has no <graph> element");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// ---------------------------------------------------------------- root, meta, graph
|
|
621
|
+
/**
|
|
622
|
+
* Record the root's version.
|
|
623
|
+
* @param attrs - the `<gexf>` attributes
|
|
624
|
+
*/
|
|
625
|
+
readRoot(attrs) {
|
|
626
|
+
const version = attrs.get("version");
|
|
627
|
+
if (version !== void 0 && version.length > 0) {
|
|
628
|
+
this.meta.sourceVersion = version;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* A child of `<gexf>`.
|
|
633
|
+
* @param name - the element name
|
|
634
|
+
* @param local - its local name
|
|
635
|
+
* @param attrs - its attributes
|
|
636
|
+
* @param line - the line
|
|
637
|
+
*/
|
|
638
|
+
startInGexf(name, local, attrs, line) {
|
|
639
|
+
switch (local) {
|
|
640
|
+
case "meta": {
|
|
641
|
+
const modified = attrs.get("lastmodifieddate");
|
|
642
|
+
if (modified !== void 0 && modified.length > 0) {
|
|
643
|
+
this.meta.modified = modified;
|
|
644
|
+
}
|
|
645
|
+
this.ctx.push(
|
|
646
|
+
1
|
|
647
|
+
/* Meta */
|
|
648
|
+
);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
case "graph":
|
|
652
|
+
if (this.graphSeen) {
|
|
653
|
+
this.report.fail(NO_GRAPH_CODE, "a second <graph> element; a GEXF document has one graph", {
|
|
654
|
+
line
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
this.graphSeen = true;
|
|
658
|
+
this.readHeader(attrs, line);
|
|
659
|
+
this.ctx.push(
|
|
660
|
+
3
|
|
661
|
+
/* Graph */
|
|
662
|
+
);
|
|
663
|
+
return;
|
|
664
|
+
default:
|
|
665
|
+
this.unknownElement(name, line);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* A child of `<meta>`.
|
|
670
|
+
* @param name - the element name
|
|
671
|
+
* @param local - its local name
|
|
672
|
+
* @param line - the line
|
|
673
|
+
*/
|
|
674
|
+
startInMeta(name, local, line) {
|
|
675
|
+
if (local === "creator" || local === "description" || local === "keywords") {
|
|
676
|
+
this.metaField = local;
|
|
677
|
+
this.metaText = "";
|
|
678
|
+
this.ctx.push(
|
|
679
|
+
2
|
|
680
|
+
/* MetaText */
|
|
681
|
+
);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.unknownElement(name, line);
|
|
685
|
+
}
|
|
686
|
+
/** Close a `<creator>` / `<description>` / `<keywords>`. */
|
|
687
|
+
finishMetaText() {
|
|
688
|
+
const text = this.metaText;
|
|
689
|
+
switch (this.metaField) {
|
|
690
|
+
case "creator":
|
|
691
|
+
this.meta.creator = text;
|
|
692
|
+
break;
|
|
693
|
+
case "description":
|
|
694
|
+
this.meta.description = text;
|
|
695
|
+
break;
|
|
696
|
+
case "keywords":
|
|
697
|
+
this.meta.keywords = text.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
this.metaField = null;
|
|
701
|
+
this.metaText = "";
|
|
702
|
+
}
|
|
703
|
+
/** Close `<meta>`: nothing more to do (the patch is applied at `<graph>`). */
|
|
704
|
+
finishMeta() {
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Read `<graph defaultedgetype mode timeformat timerepresentation idtype start end timestamp>`
|
|
708
|
+
* into the meta and the importer's own state, apply the meta patch and set the sink's
|
|
709
|
+
* direction; an unknown value is a warning and the default applies.
|
|
710
|
+
* @param attrs - the `<graph>` attributes
|
|
711
|
+
* @param line - the line
|
|
712
|
+
*/
|
|
713
|
+
readHeader(attrs, line) {
|
|
714
|
+
const where = { line };
|
|
715
|
+
const patch = this.meta;
|
|
716
|
+
const edgeType2 = attrs.get("defaultedgetype");
|
|
717
|
+
if (edgeType2 === void 0) {
|
|
718
|
+
this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
|
|
719
|
+
} else if (EDGE_TYPES.has(edgeType2)) {
|
|
720
|
+
this.defaultKind = edgeType2;
|
|
721
|
+
} else {
|
|
722
|
+
this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
|
|
723
|
+
this.warnHeader("defaultedgetype", edgeType2, where);
|
|
724
|
+
}
|
|
725
|
+
const mode = attrs.get("mode") ?? attrs.get("type");
|
|
726
|
+
if (mode !== void 0) {
|
|
727
|
+
if (GRAPH_MODES.has(mode)) {
|
|
728
|
+
patch.mode = mode;
|
|
729
|
+
} else {
|
|
730
|
+
this.warnHeader("mode", mode, where);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const timeFormat = attrs.get("timeformat");
|
|
734
|
+
if (timeFormat !== void 0) {
|
|
735
|
+
if (TIME_FORMATS.has(timeFormat)) {
|
|
736
|
+
this.timeFormat = timeFormat;
|
|
737
|
+
patch.timeFormat = this.timeFormat;
|
|
738
|
+
} else {
|
|
739
|
+
this.warnHeader("timeformat", timeFormat, where);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const representation = attrs.get("timerepresentation");
|
|
743
|
+
if (representation !== void 0) {
|
|
744
|
+
if (TIME_REPRESENTATIONS.has(representation)) {
|
|
745
|
+
patch.timeRepresentation = representation;
|
|
746
|
+
} else {
|
|
747
|
+
this.warnHeader("timerepresentation", representation, where);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const idType = attrs.get("idtype");
|
|
751
|
+
if (idType !== void 0) {
|
|
752
|
+
if (idType === "string") {
|
|
753
|
+
patch.idType = "string";
|
|
754
|
+
} else if (idType === "integer" || idType === "long") {
|
|
755
|
+
patch.idType = "integer";
|
|
756
|
+
} else {
|
|
757
|
+
this.warnHeader("idtype", idType, where);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const extra = {};
|
|
761
|
+
for (const key of ["start", "end", "timestamp"]) {
|
|
762
|
+
const value = attrs.get(key);
|
|
763
|
+
if (value !== void 0) {
|
|
764
|
+
extra[key] = value;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (Object.keys(extra).length > 0) {
|
|
768
|
+
patch.extra = { gexf: extra };
|
|
769
|
+
}
|
|
770
|
+
this.sink.setMeta(patch);
|
|
771
|
+
this.resolver.setHeader(this.defaultKind !== "undirected", where);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Record a header value outside its set.
|
|
775
|
+
* @param name - the attribute name
|
|
776
|
+
* @param value - the value found
|
|
777
|
+
* @param where - the header line
|
|
778
|
+
*/
|
|
779
|
+
warnHeader(name, value, where) {
|
|
780
|
+
this.report.warning(
|
|
781
|
+
"validation-error",
|
|
782
|
+
HEADER_VALUE_CODE,
|
|
783
|
+
`<graph ${name}="${value}"> is not a known value; the default applies`,
|
|
784
|
+
{ ...where, element: name }
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* A child of `<graph>`.
|
|
789
|
+
* @param name - the element name
|
|
790
|
+
* @param local - its local name
|
|
791
|
+
* @param attrs - its attributes
|
|
792
|
+
* @param line - the line
|
|
793
|
+
*/
|
|
794
|
+
startInGraph(name, local, attrs, line) {
|
|
795
|
+
switch (local) {
|
|
796
|
+
case "attributes":
|
|
797
|
+
this.beginAttributes(attrs, line);
|
|
798
|
+
return;
|
|
799
|
+
case "nodes":
|
|
800
|
+
this.nodesSeen = true;
|
|
801
|
+
this.reserve(attrs, "node", line);
|
|
802
|
+
this.ctx.push(
|
|
803
|
+
7
|
|
804
|
+
/* Nodes */
|
|
805
|
+
);
|
|
806
|
+
return;
|
|
807
|
+
case "edges":
|
|
808
|
+
if (!this.nodesSeen) {
|
|
809
|
+
this.report.error(
|
|
810
|
+
"validation-error",
|
|
811
|
+
MISSING_NODES_CODE,
|
|
812
|
+
"the graph declares <edges> but no <nodes>",
|
|
813
|
+
{ line }
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
this.resolveParents();
|
|
817
|
+
this.reserve(attrs, "edge", line);
|
|
818
|
+
this.ctx.push(
|
|
819
|
+
12
|
|
820
|
+
/* Edges */
|
|
821
|
+
);
|
|
822
|
+
return;
|
|
823
|
+
default:
|
|
824
|
+
this.unknownElement(name, line);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Pass a section's `count` hint to the sink; a hint the sink cannot reserve is a warning.
|
|
829
|
+
* @param attrs - the `<nodes>` or `<edges>` attributes
|
|
830
|
+
* @param domain - which count it is
|
|
831
|
+
* @param line - the line
|
|
832
|
+
*/
|
|
833
|
+
reserve(attrs, domain, line) {
|
|
834
|
+
const text = attrs.get("count");
|
|
835
|
+
if (text === void 0) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const count = Number(text);
|
|
839
|
+
if (!Number.isInteger(count) || count < 0 || count > MAX_COUNT) {
|
|
840
|
+
this.report.warning(
|
|
841
|
+
"validation-error",
|
|
842
|
+
COUNT_HINT_CODE,
|
|
843
|
+
`<${domain}s count="${text}"> is not a count the sink can reserve; ignored`,
|
|
844
|
+
{ line, element: "count" }
|
|
845
|
+
);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (count === 0) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
if (domain === "node") {
|
|
853
|
+
this.sink.reserve(count, void 0);
|
|
854
|
+
} else {
|
|
855
|
+
this.sink.reserve(void 0, count);
|
|
856
|
+
}
|
|
857
|
+
} catch (err) {
|
|
858
|
+
if (!(err instanceof GraphFormatError)) {
|
|
859
|
+
throw err;
|
|
860
|
+
}
|
|
861
|
+
this.report.warning("validation-error", COUNT_HINT_CODE, `count hint ${text} refused: ${err.message}`, {
|
|
862
|
+
line,
|
|
863
|
+
element: "count"
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// ---------------------------------------------------------------- attribute declarations
|
|
868
|
+
/**
|
|
869
|
+
* Open an `<attributes class mode>` group.
|
|
870
|
+
* @param attrs - the group's attributes
|
|
871
|
+
* @param line - the line
|
|
872
|
+
*/
|
|
873
|
+
beginAttributes(attrs, line) {
|
|
874
|
+
const cls = attrs.get("class");
|
|
875
|
+
const where = { line };
|
|
876
|
+
let domain = null;
|
|
877
|
+
if (cls === "node" || cls === "edge") {
|
|
878
|
+
domain = cls;
|
|
879
|
+
} else {
|
|
880
|
+
this.report.error(
|
|
881
|
+
"validation-error",
|
|
882
|
+
ATTRIBUTES_CLASS_CODE,
|
|
883
|
+
cls === void 0 ? "<attributes> without a class" : `<attributes class="${cls}"> is neither node nor edge`,
|
|
884
|
+
where
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const mode = attrs.get("mode");
|
|
888
|
+
if (mode !== void 0 && mode !== "static" && mode !== "dynamic") {
|
|
889
|
+
this.warnHeader("mode", mode, where);
|
|
890
|
+
}
|
|
891
|
+
this.attributeGroup = { domain, dynamic: mode === "dynamic" };
|
|
892
|
+
this.ctx.push(
|
|
893
|
+
4
|
|
894
|
+
/* Attributes */
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* A child of `<attributes>`.
|
|
899
|
+
* @param name - the element name
|
|
900
|
+
* @param local - its local name
|
|
901
|
+
* @param attrs - its attributes
|
|
902
|
+
* @param line - the line
|
|
903
|
+
*/
|
|
904
|
+
startInAttributes(name, local, attrs, line) {
|
|
905
|
+
if (local !== "attribute") {
|
|
906
|
+
this.unknownElement(name, line);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const group = this.attributeGroup;
|
|
910
|
+
if (group === null || group.domain === null) {
|
|
911
|
+
this.ctx.push(
|
|
912
|
+
14
|
|
913
|
+
/* Skip */
|
|
914
|
+
);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
this.pendingAttribute = {
|
|
918
|
+
domain: group.domain,
|
|
919
|
+
dynamic: group.dynamic,
|
|
920
|
+
attrs,
|
|
921
|
+
line,
|
|
922
|
+
defaultText: null,
|
|
923
|
+
optionsText: null
|
|
924
|
+
};
|
|
925
|
+
this.ctx.push(
|
|
926
|
+
5
|
|
927
|
+
/* Attribute */
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* A child of `<attribute>`: `<default>` or `<options>`.
|
|
932
|
+
* @param name - the element name
|
|
933
|
+
* @param local - its local name
|
|
934
|
+
* @param line - the line
|
|
935
|
+
*/
|
|
936
|
+
startInAttribute(name, local, line) {
|
|
937
|
+
if (local === "default" || local === "options") {
|
|
938
|
+
this.attributeText = local;
|
|
939
|
+
this.attributeTextValue = "";
|
|
940
|
+
this.ctx.push(
|
|
941
|
+
6
|
|
942
|
+
/* AttributeText */
|
|
943
|
+
);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
this.unknownElement(name, line);
|
|
947
|
+
}
|
|
948
|
+
/** Close a `<default>` / `<options>`. */
|
|
949
|
+
finishAttributeText() {
|
|
950
|
+
const pending = this.pendingAttribute;
|
|
951
|
+
if (pending !== null) {
|
|
952
|
+
if (this.attributeText === "default") {
|
|
953
|
+
pending.defaultText = this.attributeTextValue;
|
|
954
|
+
} else if (this.attributeText === "options") {
|
|
955
|
+
pending.optionsText = this.attributeTextValue;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
this.attributeText = null;
|
|
959
|
+
this.attributeTextValue = "";
|
|
960
|
+
}
|
|
961
|
+
/** Close an `<attribute>`: declare it. */
|
|
962
|
+
finishAttribute() {
|
|
963
|
+
const pending = this.pendingAttribute;
|
|
964
|
+
this.pendingAttribute = null;
|
|
965
|
+
if (pending === null) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
this.declareOne(pending);
|
|
970
|
+
} catch (err) {
|
|
971
|
+
this.report.recordError(err, { line: pending.line, element: pending.attrs.get("id") ?? null });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Declare one `<attribute id title type>` with its `<default>` and `<options>` (design
|
|
976
|
+
* sections 5.1, 5.5, 5.6). The edge attribute titled `weightFrom` is the weight and gets no
|
|
977
|
+
* column; its declaration is recorded in meta.weightOrigin.
|
|
978
|
+
* @param pending - the attribute as read
|
|
979
|
+
*/
|
|
980
|
+
declareOne(pending) {
|
|
981
|
+
const { report, sink, options } = this;
|
|
982
|
+
const { domain, dynamic, attrs } = pending;
|
|
983
|
+
const id = attrs.get("id");
|
|
984
|
+
const where = { line: pending.line, element: id ?? null };
|
|
985
|
+
if (id === void 0) {
|
|
986
|
+
report.error("validation-error", ATTRIBUTE_ID_CODE, "<attribute> without an id", where);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const tracked = domain === "node" ? this.nodeAttrs : this.edgeAttrs;
|
|
990
|
+
if (tracked.has(id) || domain === "edge" && this.weightAttr !== null && this.weightAttr.id === id) {
|
|
991
|
+
report.warning(
|
|
992
|
+
"validation-error",
|
|
993
|
+
DUPLICATE_ATTRIBUTE_CODE,
|
|
994
|
+
`${domain} attribute "${id}" is declared twice; the first declaration is kept`,
|
|
995
|
+
where
|
|
996
|
+
);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const title = attrs.get("title") ?? null;
|
|
1000
|
+
const type = attrs.get("type") ?? null;
|
|
1001
|
+
if (type === null) {
|
|
1002
|
+
report.warning(
|
|
1003
|
+
"validation-error",
|
|
1004
|
+
ATTRIBUTE_TYPE_CODE,
|
|
1005
|
+
`attribute "${id}" declares no type; read as string`,
|
|
1006
|
+
where
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
const name = title !== null && title.length > 0 ? title : id;
|
|
1010
|
+
if (domain === "edge" && isWeightField(name, options.weightFrom)) {
|
|
1011
|
+
const declared2 = declareAttribute({ format: GEXF_FORMAT, id, title, type, long: options.long });
|
|
1012
|
+
for (const issue of declared2.issues) {
|
|
1013
|
+
report.warning(issue.category, issue.code, issue.message, where);
|
|
1014
|
+
}
|
|
1015
|
+
this.weightAttr = { id, spec: declared2.spec, table: INVALID_INDEX };
|
|
1016
|
+
sink.setMeta({ weightOrigin: { format: GEXF_FORMAT, id, title, type, namespace: null } });
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const declaredNames = domain === "node" ? this.declaredNodeNames : this.declaredEdgeNames;
|
|
1020
|
+
const reserved = domain === "node" ? RESERVED_NODE_NAMES : RESERVED_EDGE_NAMES;
|
|
1021
|
+
const taken = (candidate) => reserved.has(candidate) || declaredNames.has(candidate);
|
|
1022
|
+
const input = {
|
|
1023
|
+
format: GEXF_FORMAT,
|
|
1024
|
+
id,
|
|
1025
|
+
title,
|
|
1026
|
+
type,
|
|
1027
|
+
defaultText: pending.defaultText,
|
|
1028
|
+
optionsText: pending.optionsText,
|
|
1029
|
+
listSyntax: "gexf",
|
|
1030
|
+
dynamic,
|
|
1031
|
+
long: options.long,
|
|
1032
|
+
taken
|
|
1033
|
+
};
|
|
1034
|
+
const declared = declareAttribute(input);
|
|
1035
|
+
for (const issue of declared.issues) {
|
|
1036
|
+
report.warning(issue.category, issue.code, issue.message, where);
|
|
1037
|
+
}
|
|
1038
|
+
const resolved = declareResolved(sink, domain, declared.decl, report, where);
|
|
1039
|
+
declaredNames.add(resolved.decl.name);
|
|
1040
|
+
let companionDecl = null;
|
|
1041
|
+
if (declared.companion !== null) {
|
|
1042
|
+
companionDecl = {
|
|
1043
|
+
...declared.companion,
|
|
1044
|
+
name: `${resolved.decl.name}.text`,
|
|
1045
|
+
extra: { for: resolved.decl.name }
|
|
1046
|
+
};
|
|
1047
|
+
declaredNames.add(companionDecl.name);
|
|
1048
|
+
}
|
|
1049
|
+
const valueDecl = {
|
|
1050
|
+
dtype: resolved.decl.dtype,
|
|
1051
|
+
itemDtype: resolved.decl.itemDtype,
|
|
1052
|
+
options: resolved.decl.options,
|
|
1053
|
+
origin: resolved.decl.origin
|
|
1054
|
+
};
|
|
1055
|
+
tracked.set(id, {
|
|
1056
|
+
id,
|
|
1057
|
+
name: resolved.decl.name,
|
|
1058
|
+
spec: declared.spec,
|
|
1059
|
+
listSyntax: declared.listSyntax,
|
|
1060
|
+
handle: resolved.handle,
|
|
1061
|
+
companionDecl,
|
|
1062
|
+
companion: INVALID_INDEX,
|
|
1063
|
+
dynamic,
|
|
1064
|
+
valueDecl,
|
|
1065
|
+
table: INVALID_INDEX
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
// ---------------------------------------------------------------- nodes
|
|
1069
|
+
/**
|
|
1070
|
+
* A child of `<nodes>`.
|
|
1071
|
+
* @param name - the element name
|
|
1072
|
+
* @param local - its local name
|
|
1073
|
+
* @param attrs - its attributes
|
|
1074
|
+
* @param line - the line
|
|
1075
|
+
*/
|
|
1076
|
+
startInNodes(name, local, attrs, line) {
|
|
1077
|
+
if (local !== "node") {
|
|
1078
|
+
this.unknownElement(name, line);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
this.beginNode(attrs, line);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Push one `<node>`.
|
|
1085
|
+
* @param attrs - its attributes
|
|
1086
|
+
* @param line - the line
|
|
1087
|
+
*/
|
|
1088
|
+
beginNode(attrs, line) {
|
|
1089
|
+
const { report, sink } = this;
|
|
1090
|
+
this.checkAbort();
|
|
1091
|
+
const enclosing = this.nodeStack.length === 0 ? INVALID_INDEX : this.nodeStack[this.nodeStack.length - 1].index;
|
|
1092
|
+
const idText = attrs.get("id");
|
|
1093
|
+
const where = { line, element: idText ?? attrs.get("label") ?? null };
|
|
1094
|
+
this.checkAttributeNames(attrs, NODE_ATTRIBUTES, "node", where);
|
|
1095
|
+
if (idText === void 0) {
|
|
1096
|
+
report.error("missing-value", MISSING_ID_CODE, "<node> without an id", where);
|
|
1097
|
+
report.counts.skippedNodes++;
|
|
1098
|
+
this.nodeStack.push({ index: INVALID_INDEX, where });
|
|
1099
|
+
this.ctx.push(
|
|
1100
|
+
8
|
|
1101
|
+
/* Node */
|
|
1102
|
+
);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
let index;
|
|
1106
|
+
try {
|
|
1107
|
+
const id = this.coerceId(idText, where);
|
|
1108
|
+
const existing = sink.indexOf(id);
|
|
1109
|
+
if (existing !== INVALID_INDEX) {
|
|
1110
|
+
report.warning(
|
|
1111
|
+
"validation-error",
|
|
1112
|
+
DUPLICATE_NODE_CODE,
|
|
1113
|
+
`node "${idText}" is declared more than once; later attributes overwrite`,
|
|
1114
|
+
where
|
|
1115
|
+
);
|
|
1116
|
+
index = existing;
|
|
1117
|
+
} else {
|
|
1118
|
+
index = sink.addNode(id);
|
|
1119
|
+
report.counts.nodes++;
|
|
1120
|
+
}
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
report.recordError(err, where);
|
|
1123
|
+
report.counts.skippedNodes++;
|
|
1124
|
+
this.nodeStack.push({ index: INVALID_INDEX, where });
|
|
1125
|
+
this.ctx.push(
|
|
1126
|
+
8
|
|
1127
|
+
/* Node */
|
|
1128
|
+
);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
this.nodeStack.push({ index, where });
|
|
1132
|
+
this.ctx.push(
|
|
1133
|
+
8
|
|
1134
|
+
/* Node */
|
|
1135
|
+
);
|
|
1136
|
+
try {
|
|
1137
|
+
const label = attrs.get("label");
|
|
1138
|
+
if (label !== void 0) {
|
|
1139
|
+
this.nodeColumns.label.set(sink, report, index, label, where);
|
|
1140
|
+
}
|
|
1141
|
+
const pid = attrs.get("pid");
|
|
1142
|
+
if (pid !== void 0) {
|
|
1143
|
+
this.parentRefs.push({
|
|
1144
|
+
child: index,
|
|
1145
|
+
parentId: this.coerceId(pid, where),
|
|
1146
|
+
parentIndex: INVALID_INDEX,
|
|
1147
|
+
where
|
|
1148
|
+
});
|
|
1149
|
+
} else if (enclosing !== INVALID_INDEX) {
|
|
1150
|
+
this.parentRefs.push({ child: index, parentId: null, parentIndex: enclosing, where });
|
|
1151
|
+
}
|
|
1152
|
+
this.writeLifetime(attrs, index, this.nodeColumns, where);
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
report.recordError(err, where);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* A child of `<node>`.
|
|
1159
|
+
* @param name - the element name
|
|
1160
|
+
* @param local - its local name
|
|
1161
|
+
* @param attrs - its attributes
|
|
1162
|
+
* @param line - the line
|
|
1163
|
+
*/
|
|
1164
|
+
startInNode(name, local, attrs, line) {
|
|
1165
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1166
|
+
if (frame.index === INVALID_INDEX && local !== "nodes") {
|
|
1167
|
+
this.ctx.push(
|
|
1168
|
+
14
|
|
1169
|
+
/* Skip */
|
|
1170
|
+
);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
switch (local) {
|
|
1174
|
+
case "attvalues":
|
|
1175
|
+
this.ctx.push(
|
|
1176
|
+
9
|
|
1177
|
+
/* Attvalues */
|
|
1178
|
+
);
|
|
1179
|
+
return;
|
|
1180
|
+
case "parents":
|
|
1181
|
+
this.parentIds = [];
|
|
1182
|
+
this.ctx.push(
|
|
1183
|
+
10
|
|
1184
|
+
/* Parents */
|
|
1185
|
+
);
|
|
1186
|
+
return;
|
|
1187
|
+
case "spells":
|
|
1188
|
+
this.spellPairs = [];
|
|
1189
|
+
this.spellsWhere = frame.where;
|
|
1190
|
+
this.ctx.push(
|
|
1191
|
+
11
|
|
1192
|
+
/* Spells */
|
|
1193
|
+
);
|
|
1194
|
+
return;
|
|
1195
|
+
case "nodes":
|
|
1196
|
+
this.reserve(attrs, "node", line);
|
|
1197
|
+
this.ctx.push(
|
|
1198
|
+
7
|
|
1199
|
+
/* Nodes */
|
|
1200
|
+
);
|
|
1201
|
+
return;
|
|
1202
|
+
case "color":
|
|
1203
|
+
case "position":
|
|
1204
|
+
case "size":
|
|
1205
|
+
case "shape":
|
|
1206
|
+
this.writeViz(name, local, attrs, "node", frame.index, frame.where);
|
|
1207
|
+
this.ctx.push(
|
|
1208
|
+
14
|
|
1209
|
+
/* Skip */
|
|
1210
|
+
);
|
|
1211
|
+
return;
|
|
1212
|
+
default:
|
|
1213
|
+
this.unknownElement(name, line);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* A `<parent for>` inside `<parents>`.
|
|
1218
|
+
* @param name - the element name
|
|
1219
|
+
* @param local - its local name
|
|
1220
|
+
* @param attrs - its attributes
|
|
1221
|
+
* @param line - the line
|
|
1222
|
+
*/
|
|
1223
|
+
startInParents(name, local, attrs, line) {
|
|
1224
|
+
if (local !== "parent") {
|
|
1225
|
+
this.unknownElement(name, line);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
this.ctx.push(
|
|
1229
|
+
14
|
|
1230
|
+
/* Skip */
|
|
1231
|
+
);
|
|
1232
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1233
|
+
const text = attrs.get("for");
|
|
1234
|
+
if (text === void 0) {
|
|
1235
|
+
this.report.warning("missing-value", ATTVALUE_SHAPE_CODE, "<parent> without a for attribute", frame.where);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
try {
|
|
1239
|
+
this.parentIds?.push(this.coerceId(text, frame.where));
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
this.report.recordError(err, frame.where);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
/** Close `<parents>`: queue the list for resolution. */
|
|
1245
|
+
finishParents() {
|
|
1246
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1247
|
+
if (this.parentIds !== null && frame.index !== INVALID_INDEX) {
|
|
1248
|
+
this.parentsRefs.push({ child: frame.index, ids: this.parentIds, where: frame.where });
|
|
1249
|
+
}
|
|
1250
|
+
this.parentIds = null;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Resolve every `pid`, nested and `<parents>` reference read so far now that every node is
|
|
1254
|
+
* declared (design section 8.4); an unknown parent is a missing-value error.
|
|
1255
|
+
*/
|
|
1256
|
+
resolveParents() {
|
|
1257
|
+
const { sink, report } = this;
|
|
1258
|
+
const refs = this.parentRefs;
|
|
1259
|
+
const lists = this.parentsRefs;
|
|
1260
|
+
this.parentRefs = [];
|
|
1261
|
+
this.parentsRefs = [];
|
|
1262
|
+
for (const ref of refs) {
|
|
1263
|
+
let parent = ref.parentIndex;
|
|
1264
|
+
if (ref.parentId !== null) {
|
|
1265
|
+
parent = sink.indexOf(ref.parentId);
|
|
1266
|
+
if (parent === INVALID_INDEX) {
|
|
1267
|
+
report.error(
|
|
1268
|
+
"missing-value",
|
|
1269
|
+
UNKNOWN_PARENT_CODE,
|
|
1270
|
+
`pid "${String(ref.parentId)}" names a node the document does not declare`,
|
|
1271
|
+
ref.where
|
|
1272
|
+
);
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
this.parentColumn.set(sink, report, ref.child, parent, ref.where);
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
report.recordError(err, ref.where);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
for (const ref of lists) {
|
|
1283
|
+
const indices = [];
|
|
1284
|
+
for (const id of ref.ids) {
|
|
1285
|
+
const parent = sink.indexOf(id);
|
|
1286
|
+
if (parent === INVALID_INDEX) {
|
|
1287
|
+
report.error(
|
|
1288
|
+
"missing-value",
|
|
1289
|
+
UNKNOWN_PARENT_CODE,
|
|
1290
|
+
`<parent for="${String(id)}"> names a node the document does not declare`,
|
|
1291
|
+
ref.where
|
|
1292
|
+
);
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
indices.push(parent);
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
this.parentsColumn.set(sink, report, ref.child, indices, ref.where);
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
report.recordError(err, ref.where);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
// ---------------------------------------------------------------- edges
|
|
1305
|
+
/**
|
|
1306
|
+
* A child of `<edges>`.
|
|
1307
|
+
* @param name - the element name
|
|
1308
|
+
* @param local - its local name
|
|
1309
|
+
* @param attrs - its attributes
|
|
1310
|
+
* @param line - the line
|
|
1311
|
+
*/
|
|
1312
|
+
startInEdges(name, local, attrs, line) {
|
|
1313
|
+
if (local !== "edge") {
|
|
1314
|
+
this.unknownElement(name, line);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
this.beginEdge(attrs, line);
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Push one `<edge>`.
|
|
1321
|
+
* @param attrs - its attributes
|
|
1322
|
+
* @param line - the line
|
|
1323
|
+
*/
|
|
1324
|
+
beginEdge(attrs, line) {
|
|
1325
|
+
const { report, sink, options } = this;
|
|
1326
|
+
this.checkAbort();
|
|
1327
|
+
this.ctx.push(
|
|
1328
|
+
13
|
|
1329
|
+
/* Edge */
|
|
1330
|
+
);
|
|
1331
|
+
this.edge = null;
|
|
1332
|
+
const sourceText = attrs.get("source");
|
|
1333
|
+
const targetText = attrs.get("target");
|
|
1334
|
+
const idText = attrs.get("id");
|
|
1335
|
+
const { where } = this.edgeFrame;
|
|
1336
|
+
where.line = line;
|
|
1337
|
+
where.element = idText ?? edgeElement(sourceText, targetText);
|
|
1338
|
+
this.checkAttributeNames(attrs, EDGE_ATTRIBUTES, "edge", where);
|
|
1339
|
+
if (sourceText === void 0 || targetText === void 0) {
|
|
1340
|
+
report.error(
|
|
1341
|
+
"missing-value",
|
|
1342
|
+
MISSING_ENDPOINT_CODE,
|
|
1343
|
+
`<edge> without a ${sourceText === void 0 ? "source" : "target"}`,
|
|
1344
|
+
where
|
|
1345
|
+
);
|
|
1346
|
+
report.counts.skippedEdges++;
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
let kind = this.defaultKind;
|
|
1350
|
+
const type = attrs.get("type");
|
|
1351
|
+
if (type !== void 0) {
|
|
1352
|
+
if (!EDGE_TYPES.has(type)) {
|
|
1353
|
+
report.error(
|
|
1354
|
+
"validation-error",
|
|
1355
|
+
EDGE_TYPE_CODE,
|
|
1356
|
+
`edge type "${type}" is not directed, undirected or mutual`,
|
|
1357
|
+
where
|
|
1358
|
+
);
|
|
1359
|
+
report.counts.skippedEdges++;
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
kind = type;
|
|
1363
|
+
}
|
|
1364
|
+
if (idText !== void 0) {
|
|
1365
|
+
if (this.edgeIds.has(idText)) {
|
|
1366
|
+
report.error(
|
|
1367
|
+
"validation-error",
|
|
1368
|
+
DUPLICATE_EDGE_ID_CODE,
|
|
1369
|
+
`edge id "${idText}" is declared more than once; the edge is skipped`,
|
|
1370
|
+
where
|
|
1371
|
+
);
|
|
1372
|
+
report.counts.skippedEdges++;
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
this.edgeIds.add(idText);
|
|
1376
|
+
}
|
|
1377
|
+
let index;
|
|
1378
|
+
try {
|
|
1379
|
+
const source = this.coerceId(sourceText, where);
|
|
1380
|
+
const target = this.coerceId(targetText, where);
|
|
1381
|
+
if (!options.addMissingNodes) {
|
|
1382
|
+
if (sink.indexOf(source) === INVALID_INDEX) {
|
|
1383
|
+
throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${sourceText}"`, {
|
|
1384
|
+
id: source
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
if (sink.indexOf(target) === INVALID_INDEX) {
|
|
1388
|
+
throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${targetText}"`, {
|
|
1389
|
+
id: target
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const weight = this.xmlWeight(attrs, where);
|
|
1394
|
+
const before = sink.edgeCount;
|
|
1395
|
+
index = this.resolver.addEdge(source, target, kind, weight, where);
|
|
1396
|
+
report.counts.edges += sink.edgeCount - before;
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
report.recordError(err, where);
|
|
1399
|
+
report.counts.skippedEdges++;
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const frame = this.edgeFrame;
|
|
1403
|
+
frame.index = index;
|
|
1404
|
+
frame.mirror = this.resolver.lastMirror;
|
|
1405
|
+
this.edge = frame;
|
|
1406
|
+
try {
|
|
1407
|
+
if (idText !== void 0) {
|
|
1408
|
+
this.edgeIdColumn.set(sink, report, index, idText, where);
|
|
1409
|
+
}
|
|
1410
|
+
const label = attrs.get("label");
|
|
1411
|
+
if (label !== void 0) {
|
|
1412
|
+
this.edgeColumns.label.set(sink, report, index, label, where);
|
|
1413
|
+
}
|
|
1414
|
+
const edgeKind = attrs.get("kind");
|
|
1415
|
+
if (edgeKind !== void 0) {
|
|
1416
|
+
this.edgeKindColumn.set(sink, report, index, edgeKind, where);
|
|
1417
|
+
}
|
|
1418
|
+
this.writeLifetime(attrs, index, this.edgeColumns, where);
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
report.recordError(err, where);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* The weight argument of one edge from its `weight` XML attribute; undefined when absent (the
|
|
1425
|
+
* sink records a defaulted weight, design section 3.7).
|
|
1426
|
+
* @param attrs - the edge's attributes
|
|
1427
|
+
* @param where - the edge's location
|
|
1428
|
+
* @returns the weight, or undefined
|
|
1429
|
+
*/
|
|
1430
|
+
xmlWeight(attrs, where) {
|
|
1431
|
+
const text = attrs.get("weight");
|
|
1432
|
+
if (text === void 0) {
|
|
1433
|
+
return void 0;
|
|
1434
|
+
}
|
|
1435
|
+
if (this.options.weightFrom === null) {
|
|
1436
|
+
this.report.warnOnce(
|
|
1437
|
+
"coercion",
|
|
1438
|
+
WEIGHT_IGNORED_CODE,
|
|
1439
|
+
"the weight attribute is ignored because weightFrom is null; edges are unweighted",
|
|
1440
|
+
where
|
|
1441
|
+
);
|
|
1442
|
+
return void 0;
|
|
1443
|
+
}
|
|
1444
|
+
return parseWeightText(text);
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* A child of `<edge>`.
|
|
1448
|
+
* @param name - the element name
|
|
1449
|
+
* @param local - its local name
|
|
1450
|
+
* @param attrs - its attributes
|
|
1451
|
+
* @param line - the line
|
|
1452
|
+
*/
|
|
1453
|
+
startInEdge(name, local, attrs, line) {
|
|
1454
|
+
const { edge } = this;
|
|
1455
|
+
if (edge === null) {
|
|
1456
|
+
this.ctx.push(
|
|
1457
|
+
14
|
|
1458
|
+
/* Skip */
|
|
1459
|
+
);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
switch (local) {
|
|
1463
|
+
case "attvalues":
|
|
1464
|
+
this.ctx.push(
|
|
1465
|
+
9
|
|
1466
|
+
/* Attvalues */
|
|
1467
|
+
);
|
|
1468
|
+
return;
|
|
1469
|
+
case "spells":
|
|
1470
|
+
this.spellPairs = [];
|
|
1471
|
+
this.spellsWhere = edge.where;
|
|
1472
|
+
this.ctx.push(
|
|
1473
|
+
11
|
|
1474
|
+
/* Spells */
|
|
1475
|
+
);
|
|
1476
|
+
return;
|
|
1477
|
+
case "color":
|
|
1478
|
+
case "thickness":
|
|
1479
|
+
case "shape":
|
|
1480
|
+
this.writeViz(name, local, attrs, "edge", edge.index, edge.where);
|
|
1481
|
+
this.ctx.push(
|
|
1482
|
+
14
|
|
1483
|
+
/* Skip */
|
|
1484
|
+
);
|
|
1485
|
+
return;
|
|
1486
|
+
default:
|
|
1487
|
+
this.unknownElement(name, line);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
// ---------------------------------------------------------------- attribute values
|
|
1491
|
+
/**
|
|
1492
|
+
* An `<attvalue>` of the open node or edge; a value that does not parse is recorded and skipped
|
|
1493
|
+
* without losing the element or its other values.
|
|
1494
|
+
* @param name - the element name
|
|
1495
|
+
* @param local - its local name
|
|
1496
|
+
* @param attrs - its attributes
|
|
1497
|
+
* @param line - the line
|
|
1498
|
+
*/
|
|
1499
|
+
startInAttvalues(name, local, attrs, line) {
|
|
1500
|
+
if (local !== "attvalue") {
|
|
1501
|
+
this.unknownElement(name, line);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
this.ctx.push(
|
|
1505
|
+
14
|
|
1506
|
+
/* Skip */
|
|
1507
|
+
);
|
|
1508
|
+
const { report, edge } = this;
|
|
1509
|
+
const domain = edge === null ? "node" : "edge";
|
|
1510
|
+
let index;
|
|
1511
|
+
let element;
|
|
1512
|
+
if (edge !== null) {
|
|
1513
|
+
({ index } = edge);
|
|
1514
|
+
element = edge.where.element ?? null;
|
|
1515
|
+
} else {
|
|
1516
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1517
|
+
({ index } = frame);
|
|
1518
|
+
element = frame.where.element ?? null;
|
|
1519
|
+
}
|
|
1520
|
+
if (index === INVALID_INDEX) {
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const forId = attrs.get("for");
|
|
1524
|
+
const { valueWhere } = this;
|
|
1525
|
+
valueWhere.line = line;
|
|
1526
|
+
valueWhere.element = element;
|
|
1527
|
+
if (forId === void 0) {
|
|
1528
|
+
report.warning("missing-value", ATTVALUE_SHAPE_CODE, "<attvalue> without a for attribute", valueWhere);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
const value = attrs.get("value");
|
|
1532
|
+
if (value === void 0) {
|
|
1533
|
+
report.warning(
|
|
1534
|
+
"missing-value",
|
|
1535
|
+
ATTVALUE_SHAPE_CODE,
|
|
1536
|
+
`<attvalue for="${forId}"> without a value attribute`,
|
|
1537
|
+
valueWhere
|
|
1538
|
+
);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
try {
|
|
1542
|
+
if (domain === "edge" && this.weightAttr !== null && this.weightAttr.id === forId) {
|
|
1543
|
+
if (isTimed(attrs)) {
|
|
1544
|
+
this.writeTimedWeight(attrs, value, index, valueWhere);
|
|
1545
|
+
} else {
|
|
1546
|
+
this.applyStaticWeight(value, valueWhere);
|
|
1547
|
+
}
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const tracked = domain === "node" ? this.nodeAttrs : this.edgeAttrs;
|
|
1551
|
+
const attribute = tracked.get(forId);
|
|
1552
|
+
if (attribute === void 0) {
|
|
1553
|
+
const key = `${domain}:${forId}`;
|
|
1554
|
+
if (!this.unknownAttributes.has(key)) {
|
|
1555
|
+
this.unknownAttributes.add(key);
|
|
1556
|
+
report.warning(
|
|
1557
|
+
"missing-value",
|
|
1558
|
+
UNKNOWN_ATTRIBUTE_CODE,
|
|
1559
|
+
`<attvalue for="${forId}"> names an undeclared ${domain} attribute; its values are dropped`,
|
|
1560
|
+
{ ...valueWhere, element: forId }
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
if (isTimed(attrs)) {
|
|
1566
|
+
this.writeTimedValue(attribute, domain, attrs, value, index, valueWhere);
|
|
1567
|
+
} else {
|
|
1568
|
+
this.writeStaticValue(attribute, domain, value, index, valueWhere);
|
|
1569
|
+
}
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
report.recordError(err, { ...valueWhere, element: `${element ?? "?"}[${forId}]` });
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* A static value of the attribute titled `weightFrom` overrides the XML weight of the open
|
|
1576
|
+
* edge (and of its mirror half, design section 3.6).
|
|
1577
|
+
* @param text - the value text
|
|
1578
|
+
* @param where - the value's location
|
|
1579
|
+
*/
|
|
1580
|
+
applyStaticWeight(text, where) {
|
|
1581
|
+
const { edge, sink } = this;
|
|
1582
|
+
if (edge === null) {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const weight = parseWeightText(text);
|
|
1586
|
+
if (weight === void 0) {
|
|
1587
|
+
throw new GraphFormatError("E_INVALID_WEIGHT", "an edge weight cannot be blank", {
|
|
1588
|
+
value: text,
|
|
1589
|
+
line: where.line ?? null
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
sink.setEdgeWeight(edge.index, weight);
|
|
1593
|
+
if (edge.mirror !== INVALID_INDEX) {
|
|
1594
|
+
sink.setEdgeWeight(edge.mirror, weight);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Parse a value by its declaration and write the cell (and the temporal companion, declared
|
|
1599
|
+
* on the first value whose text is not canonical, design section 5.1).
|
|
1600
|
+
* @param attribute - the declaration
|
|
1601
|
+
* @param domain - node or edge
|
|
1602
|
+
* @param text - the value text
|
|
1603
|
+
* @param index - the element's index
|
|
1604
|
+
* @param where - the value's location
|
|
1605
|
+
*/
|
|
1606
|
+
writeStaticValue(attribute, domain, text, index, where) {
|
|
1607
|
+
const { spec } = attribute;
|
|
1608
|
+
if (spec.temporal !== null && !spec.list) {
|
|
1609
|
+
const parsed = parseDeclaredTemporal(text, spec);
|
|
1610
|
+
this.setCell(domain, attribute.handle, index, parsed.value);
|
|
1611
|
+
if (parsed.text !== null && attribute.companionDecl !== null) {
|
|
1612
|
+
if (attribute.companion === INVALID_INDEX) {
|
|
1613
|
+
attribute.companion = declareCompanion(this.sink, domain, attribute.companionDecl, this.report);
|
|
1614
|
+
}
|
|
1615
|
+
this.setCell(domain, attribute.companion, index, parsed.text);
|
|
1616
|
+
}
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const value = parseDeclaredValue(text, spec, attribute.listSyntax);
|
|
1620
|
+
if (losesPrecision(spec, text)) {
|
|
1621
|
+
this.report.warning(
|
|
1622
|
+
"precision",
|
|
1623
|
+
PRECISION_CODE,
|
|
1624
|
+
`long value ${text.trim()} of "${attribute.name}" exceeds 2^53 and was rounded`,
|
|
1625
|
+
where
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
this.setCell(domain, attribute.handle, index, value);
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Append a timed value to the attribute's temporal table (design section 5.10), creating the
|
|
1632
|
+
* table on first use.
|
|
1633
|
+
* @param attribute - the declaration
|
|
1634
|
+
* @param domain - node or edge
|
|
1635
|
+
* @param attrs - the `<attvalue>` attributes carrying the bounds
|
|
1636
|
+
* @param text - the value text
|
|
1637
|
+
* @param index - the element's index
|
|
1638
|
+
* @param where - the value's location
|
|
1639
|
+
*/
|
|
1640
|
+
writeTimedValue(attribute, domain, attrs, text, index, where) {
|
|
1641
|
+
const { sink, report } = this;
|
|
1642
|
+
if (!attribute.dynamic) {
|
|
1643
|
+
report.warnOnce(
|
|
1644
|
+
"validation-error",
|
|
1645
|
+
TIMED_STATIC_CODE,
|
|
1646
|
+
`attribute "${attribute.name}" is declared static but has timed values; they are stored as dynamic`,
|
|
1647
|
+
where
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
const { spec } = attribute;
|
|
1651
|
+
const temporalValue = spec.temporal !== null && !spec.list;
|
|
1652
|
+
if (attribute.table === INVALID_INDEX) {
|
|
1653
|
+
attribute.table = sink.addExtensionTable(
|
|
1654
|
+
temporalTableName(domain, attribute.name),
|
|
1655
|
+
temporalTableDecls(domain, attribute.valueDecl, temporalValue)
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
let value;
|
|
1659
|
+
let valueText = null;
|
|
1660
|
+
if (temporalValue) {
|
|
1661
|
+
const parsed = parseDeclaredTemporal(text, spec);
|
|
1662
|
+
({ value } = parsed);
|
|
1663
|
+
valueText = parsed.text;
|
|
1664
|
+
} else {
|
|
1665
|
+
value = parseDeclaredValue(text, spec, attribute.listSyntax);
|
|
1666
|
+
if (losesPrecision(spec, text)) {
|
|
1667
|
+
report.warning(
|
|
1668
|
+
"precision",
|
|
1669
|
+
PRECISION_CODE,
|
|
1670
|
+
`long value ${text.trim()} of "${attribute.name}" exceeds 2^53 and was rounded`,
|
|
1671
|
+
where
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const row = this.temporalRow(attrs, index, value, where);
|
|
1676
|
+
if (temporalValue) {
|
|
1677
|
+
row.push(valueText ?? void 0);
|
|
1678
|
+
}
|
|
1679
|
+
sink.addExtensionRow(attribute.table, row);
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Append a timed weight to the `temporal:edge:weight` table (design section 5.10).
|
|
1683
|
+
* @param attrs - the `<attvalue>` attributes carrying the bounds
|
|
1684
|
+
* @param text - the value text
|
|
1685
|
+
* @param index - the edge index
|
|
1686
|
+
* @param where - the value's location
|
|
1687
|
+
*/
|
|
1688
|
+
writeTimedWeight(attrs, text, index, where) {
|
|
1689
|
+
const { weightAttr } = this;
|
|
1690
|
+
if (weightAttr === null) {
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const { sink } = this;
|
|
1694
|
+
if (weightAttr.table === INVALID_INDEX) {
|
|
1695
|
+
weightAttr.table = sink.addExtensionTable(
|
|
1696
|
+
temporalTableName("edge", this.options.weightFrom ?? "weight"),
|
|
1697
|
+
temporalTableDecls(
|
|
1698
|
+
"edge",
|
|
1699
|
+
{
|
|
1700
|
+
dtype: "f64",
|
|
1701
|
+
origin: { format: GEXF_FORMAT, id: weightAttr.id, type: weightAttr.spec.declared }
|
|
1702
|
+
},
|
|
1703
|
+
false
|
|
1704
|
+
)
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
const value = parseWeightText(text);
|
|
1708
|
+
if (value === void 0) {
|
|
1709
|
+
throw new GraphFormatError("E_INVALID_WEIGHT", "a timed edge weight cannot be blank", {
|
|
1710
|
+
value: text,
|
|
1711
|
+
line: where.line ?? null
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
sink.addExtensionRow(weightAttr.table, this.temporalRow(attrs, index, value, where));
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* The row of a temporal table for one timed value: element, bounds, value, bound texts, open bits.
|
|
1718
|
+
* @param attrs - the attributes carrying `start` / `end` / `timestamp` / `startopen` / `endopen`
|
|
1719
|
+
* @param index - the element's index
|
|
1720
|
+
* @param value - the parsed value
|
|
1721
|
+
* @param where - the value's location
|
|
1722
|
+
* @returns the row, without the optional value text
|
|
1723
|
+
*/
|
|
1724
|
+
temporalRow(attrs, index, value, where) {
|
|
1725
|
+
const bounds = this.readBounds(attrs, where);
|
|
1726
|
+
return [
|
|
1727
|
+
index,
|
|
1728
|
+
bounds.start.value,
|
|
1729
|
+
bounds.end.value,
|
|
1730
|
+
value,
|
|
1731
|
+
bounds.start.text ?? void 0,
|
|
1732
|
+
bounds.end.text ?? void 0,
|
|
1733
|
+
bounds.open === 0 ? void 0 : bounds.open
|
|
1734
|
+
];
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* The interval of an element or timed value: `timestamp`, or `start` / `startopen` and `end` /
|
|
1738
|
+
* `endopen` (GEXF 1.2: the open form carries the bound's time and marks the bound open, design
|
|
1739
|
+
* section 5.1); missing bounds are unbounded.
|
|
1740
|
+
* @param attrs - the attributes
|
|
1741
|
+
* @param where - the location, for the conflict warning
|
|
1742
|
+
* @returns the bounds and the open bits
|
|
1743
|
+
*/
|
|
1744
|
+
readBounds(attrs, where) {
|
|
1745
|
+
let start = { value: -Infinity, text: null };
|
|
1746
|
+
let end = { value: Infinity, text: null };
|
|
1747
|
+
let open = 0;
|
|
1748
|
+
const timestamp = attrs.get("timestamp");
|
|
1749
|
+
if (timestamp !== void 0) {
|
|
1750
|
+
start = parseTimeText(timestamp, this.timeFormat);
|
|
1751
|
+
end = start;
|
|
1752
|
+
}
|
|
1753
|
+
const startText = attrs.get("start");
|
|
1754
|
+
const startOpen = attrs.get("startopen");
|
|
1755
|
+
if (startText !== void 0) {
|
|
1756
|
+
start = parseTimeText(startText, this.timeFormat);
|
|
1757
|
+
if (startOpen !== void 0) {
|
|
1758
|
+
this.warnOpenConflict("start", where);
|
|
1759
|
+
}
|
|
1760
|
+
} else if (startOpen !== void 0) {
|
|
1761
|
+
start = parseTimeText(startOpen, this.timeFormat);
|
|
1762
|
+
open |= OPEN_START;
|
|
1763
|
+
}
|
|
1764
|
+
const endText = attrs.get("end");
|
|
1765
|
+
const endOpen = attrs.get("endopen");
|
|
1766
|
+
if (endText !== void 0) {
|
|
1767
|
+
end = parseTimeText(endText, this.timeFormat);
|
|
1768
|
+
if (endOpen !== void 0) {
|
|
1769
|
+
this.warnOpenConflict("end", where);
|
|
1770
|
+
}
|
|
1771
|
+
} else if (endOpen !== void 0) {
|
|
1772
|
+
end = parseTimeText(endOpen, this.timeFormat);
|
|
1773
|
+
open |= OPEN_END;
|
|
1774
|
+
}
|
|
1775
|
+
return { start, end, open };
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Record a `start` next to a `startopen` (or `end` next to `endopen`), which the 1.2 primer forbids.
|
|
1779
|
+
* @param bound - which bound
|
|
1780
|
+
* @param where - the location
|
|
1781
|
+
*/
|
|
1782
|
+
warnOpenConflict(bound, where) {
|
|
1783
|
+
this.report.warnOnce(
|
|
1784
|
+
"validation-error",
|
|
1785
|
+
OPEN_BOUND_CONFLICT_CODE,
|
|
1786
|
+
`both ${bound} and ${bound}open are given; the closed bound is kept`,
|
|
1787
|
+
where
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
// ---------------------------------------------------------------- lifetimes
|
|
1791
|
+
/**
|
|
1792
|
+
* Write an element's lifetime: `start` / `end` / `timestamp` (with text companions), the 1.2
|
|
1793
|
+
* `startopen` / `endopen` bounds and bits, and the 1.3 `timestamps` / `intervals` attributes.
|
|
1794
|
+
* @param attrs - the node or edge attributes
|
|
1795
|
+
* @param index - its index
|
|
1796
|
+
* @param columns - the domain's columns
|
|
1797
|
+
* @param where - the element's location
|
|
1798
|
+
*/
|
|
1799
|
+
writeLifetime(attrs, index, columns, where) {
|
|
1800
|
+
const { sink, report } = this;
|
|
1801
|
+
if (attrs.has("start") || attrs.has("end") || attrs.has("timestamp") || attrs.has("startopen") || attrs.has("endopen")) {
|
|
1802
|
+
const bounds = this.readBounds(attrs, where);
|
|
1803
|
+
const timestamp = attrs.get("timestamp");
|
|
1804
|
+
if (timestamp !== void 0) {
|
|
1805
|
+
columns.timestamp.set(sink, report, index, bounds.start, where);
|
|
1806
|
+
}
|
|
1807
|
+
if (attrs.has("start") || attrs.has("startopen")) {
|
|
1808
|
+
columns.start.set(sink, report, index, bounds.start, where);
|
|
1809
|
+
}
|
|
1810
|
+
if (attrs.has("end") || attrs.has("endopen")) {
|
|
1811
|
+
columns.end.set(sink, report, index, bounds.end, where);
|
|
1812
|
+
}
|
|
1813
|
+
if (bounds.open !== 0) {
|
|
1814
|
+
columns.open.set(sink, report, index, bounds.open, where);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
const timestamps = attrs.get("timestamps");
|
|
1818
|
+
if (timestamps !== void 0) {
|
|
1819
|
+
const items = splitListText(stripAngles(timestamps), "brackets");
|
|
1820
|
+
columns.timestamps.set(
|
|
1821
|
+
sink,
|
|
1822
|
+
report,
|
|
1823
|
+
index,
|
|
1824
|
+
items.map((item) => parseTimeText(item, this.timeFormat).value),
|
|
1825
|
+
where
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const intervals = attrs.get("intervals");
|
|
1829
|
+
if (intervals !== void 0) {
|
|
1830
|
+
columns.spells.set(sink, report, index, this.parseIntervals(intervals, where), where);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Parse a 1.3 `intervals="<[s, e]; [s, e]>"` attribute into spell pairs.
|
|
1835
|
+
* @param text - the attribute text
|
|
1836
|
+
* @param where - the element's location, for the error
|
|
1837
|
+
* @returns the pairs
|
|
1838
|
+
*/
|
|
1839
|
+
parseIntervals(text, where) {
|
|
1840
|
+
const pairs = [];
|
|
1841
|
+
for (const part of stripAngles(text).split(";")) {
|
|
1842
|
+
if (part.trim().length === 0) {
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
const items = splitListText(part, "brackets");
|
|
1846
|
+
if (items.length !== 2) {
|
|
1847
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `interval "${part.trim()}" is not a [start, end] pair`, {
|
|
1848
|
+
value: part,
|
|
1849
|
+
line: where.line ?? null
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
pairs.push([
|
|
1853
|
+
parseTimeText(items[0], this.timeFormat).value,
|
|
1854
|
+
parseTimeText(items[1], this.timeFormat).value
|
|
1855
|
+
]);
|
|
1856
|
+
}
|
|
1857
|
+
return pairs;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* A `<spell>` inside `<spells>`.
|
|
1861
|
+
* @param name - the element name
|
|
1862
|
+
* @param local - its local name
|
|
1863
|
+
* @param attrs - its attributes
|
|
1864
|
+
* @param line - the line
|
|
1865
|
+
*/
|
|
1866
|
+
startInSpells(name, local, attrs, line) {
|
|
1867
|
+
if (local !== "spell") {
|
|
1868
|
+
this.unknownElement(name, line);
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
this.ctx.push(
|
|
1872
|
+
14
|
|
1873
|
+
/* Skip */
|
|
1874
|
+
);
|
|
1875
|
+
const where = this.spellsWhere ?? { line };
|
|
1876
|
+
try {
|
|
1877
|
+
const bounds = this.readBounds(attrs, where);
|
|
1878
|
+
if (bounds.open !== 0) {
|
|
1879
|
+
this.report.warnOnce(
|
|
1880
|
+
"unsupported",
|
|
1881
|
+
SPELL_OPEN_CODE,
|
|
1882
|
+
"startopen / endopen on a <spell> cannot be kept; the spell is stored closed",
|
|
1883
|
+
where
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
this.spellPairs?.push([bounds.start.value, bounds.end.value]);
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
this.report.recordError(err, where);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
/** Close `<spells>`: write the pairs into the open element's spells column. */
|
|
1892
|
+
finishSpells() {
|
|
1893
|
+
const pairs = this.spellPairs;
|
|
1894
|
+
const where = this.spellsWhere ?? void 0;
|
|
1895
|
+
this.spellPairs = null;
|
|
1896
|
+
this.spellsWhere = null;
|
|
1897
|
+
if (pairs === null) {
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
try {
|
|
1901
|
+
if (this.edge !== null) {
|
|
1902
|
+
this.edgeColumns.spells.set(this.sink, this.report, this.edge.index, pairs, where);
|
|
1903
|
+
} else {
|
|
1904
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1905
|
+
if (frame.index !== INVALID_INDEX) {
|
|
1906
|
+
this.nodeColumns.spells.set(this.sink, this.report, frame.index, pairs, where);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
this.report.recordError(err, where);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
// ---------------------------------------------------------------- viz
|
|
1914
|
+
/**
|
|
1915
|
+
* Write one viz element into its role column; a value that does not parse is a recorded
|
|
1916
|
+
* warning and the element is skipped.
|
|
1917
|
+
* @param name - the element name as written
|
|
1918
|
+
* @param local - its local name
|
|
1919
|
+
* @param attrs - its attributes
|
|
1920
|
+
* @param domain - node or edge
|
|
1921
|
+
* @param index - the element's index
|
|
1922
|
+
* @param where - the element's location
|
|
1923
|
+
*/
|
|
1924
|
+
writeViz(name, local, attrs, domain, index, where) {
|
|
1925
|
+
const { sink, report } = this;
|
|
1926
|
+
if (!this.viz) {
|
|
1927
|
+
report.warnOnce("unsupported", VIZ_SKIPPED_CODE, "viz elements are skipped (option viz: false)", where);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (isTimed(attrs)) {
|
|
1931
|
+
report.warnOnce(
|
|
1932
|
+
"unsupported",
|
|
1933
|
+
VIZ_DYNAMIC_CODE,
|
|
1934
|
+
"dynamic viz elements are not supported; the bounds are dropped and the last value kept",
|
|
1935
|
+
where
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
const columns = domain === "node" ? this.nodeColumns : this.edgeColumns;
|
|
1939
|
+
try {
|
|
1940
|
+
switch (local) {
|
|
1941
|
+
case "color":
|
|
1942
|
+
columns.color.set(sink, report, index, parseColor(attrs), where);
|
|
1943
|
+
break;
|
|
1944
|
+
case "position": {
|
|
1945
|
+
const x = vizNumber(attrs, "x");
|
|
1946
|
+
const y = vizNumber(attrs, "y");
|
|
1947
|
+
const z = attrs.has("z") ? vizNumber(attrs, "z") : 0;
|
|
1948
|
+
this.positionColumn ??= new LazyColumn("node", {
|
|
1949
|
+
...NODE_DECLS.position,
|
|
1950
|
+
extra: { ...NODE_DECLS.position.extra, sourceDims: attrs.has("z") ? 3 : 2 }
|
|
1951
|
+
});
|
|
1952
|
+
this.positionColumn.set(sink, report, index, [x, y, z], where);
|
|
1953
|
+
break;
|
|
1954
|
+
}
|
|
1955
|
+
case "size":
|
|
1956
|
+
this.sizeColumn.set(sink, report, index, vizNumber(attrs, "value"), where);
|
|
1957
|
+
break;
|
|
1958
|
+
case "thickness":
|
|
1959
|
+
this.thicknessColumn.set(sink, report, index, vizNumber(attrs, "value"), where);
|
|
1960
|
+
break;
|
|
1961
|
+
case "shape": {
|
|
1962
|
+
const value = attrs.get("value");
|
|
1963
|
+
if (value === void 0) {
|
|
1964
|
+
throw new GraphFormatError("E_COLUMN_TYPE", "<viz:shape> without a value", { element: name });
|
|
1965
|
+
}
|
|
1966
|
+
columns.shape.set(sink, report, index, value, where);
|
|
1967
|
+
const uri = attrs.get("uri");
|
|
1968
|
+
if (domain === "node" && uri !== void 0) {
|
|
1969
|
+
this.shapeUriColumn.set(sink, report, index, uri, where);
|
|
1970
|
+
}
|
|
1971
|
+
break;
|
|
1972
|
+
}
|
|
1973
|
+
default:
|
|
1974
|
+
break;
|
|
1975
|
+
}
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
if (!(err instanceof GraphFormatError) || err.code !== "E_COLUMN_TYPE") {
|
|
1978
|
+
throw err;
|
|
1979
|
+
}
|
|
1980
|
+
report.warning("validation-error", VIZ_VALUE_CODE, `<${name}> skipped: ${err.message}`, where);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
// ---------------------------------------------------------------- helpers
|
|
1984
|
+
/**
|
|
1985
|
+
* Report an element the schema does not define at this place and skip its subtree.
|
|
1986
|
+
* @param name - the element name
|
|
1987
|
+
* @param line - the line
|
|
1988
|
+
*/
|
|
1989
|
+
unknownElement(name, line) {
|
|
1990
|
+
this.report.warnOnce("unsupported", UNKNOWN_ELEMENT_CODE, `element <${name}> is not GEXF and was skipped`, {
|
|
1991
|
+
line,
|
|
1992
|
+
element: name
|
|
1993
|
+
});
|
|
1994
|
+
this.ctx.push(
|
|
1995
|
+
14
|
|
1996
|
+
/* Skip */
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Report XML attributes the importer does not read (once per attribute name and domain).
|
|
2001
|
+
* @param attrs - the element's attributes
|
|
2002
|
+
* @param known - the attribute names read
|
|
2003
|
+
* @param domain - node or edge
|
|
2004
|
+
* @param where - the element's location
|
|
2005
|
+
*/
|
|
2006
|
+
checkAttributeNames(attrs, known, domain, where) {
|
|
2007
|
+
for (const key of attrs.keys()) {
|
|
2008
|
+
if (!known.has(key)) {
|
|
2009
|
+
const seen = `${domain}@${key}`;
|
|
2010
|
+
if (!this.unknownAttributes.has(seen)) {
|
|
2011
|
+
this.unknownAttributes.add(seen);
|
|
2012
|
+
this.report.warning(
|
|
2013
|
+
"unsupported",
|
|
2014
|
+
UNKNOWN_ELEMENT_CODE,
|
|
2015
|
+
`attribute ${key} of <${domain}> is not GEXF and was ignored`,
|
|
2016
|
+
{ ...where, element: key }
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Write one cell of a domain.
|
|
2024
|
+
* @param domain - node or edge
|
|
2025
|
+
* @param handle - the column
|
|
2026
|
+
* @param index - the row
|
|
2027
|
+
* @param value - the value
|
|
2028
|
+
*/
|
|
2029
|
+
setCell(domain, handle, index, value) {
|
|
2030
|
+
if (domain === "node") {
|
|
2031
|
+
this.sink.setNodeValue(handle, index, value);
|
|
2032
|
+
} else {
|
|
2033
|
+
this.sink.setEdgeValue(handle, index, value);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Coerce an id text, recording a merge under ids "number".
|
|
2038
|
+
* @param text - the id text
|
|
2039
|
+
* @param where - the location, for the merge warning
|
|
2040
|
+
* @returns the id
|
|
2041
|
+
*/
|
|
2042
|
+
coerceId(text, where) {
|
|
2043
|
+
const id = this.coercer.text(text);
|
|
2044
|
+
const merge = this.coercer.lastMerge;
|
|
2045
|
+
if (merge !== null) {
|
|
2046
|
+
this.report.warning(
|
|
2047
|
+
"coercion",
|
|
2048
|
+
ID_MERGED_CODE,
|
|
2049
|
+
`id text "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids "number"`,
|
|
2050
|
+
where
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
return id;
|
|
2054
|
+
}
|
|
2055
|
+
/** Check the cancellation signal every ABORT_CHECK_INTERVAL elements. */
|
|
2056
|
+
checkAbort() {
|
|
2057
|
+
if (++this.elementsSinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
2058
|
+
this.elementsSinceCheck = 0;
|
|
2059
|
+
throwIfAborted(this.options.signal);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
function edgeElement(source, target) {
|
|
2064
|
+
return `${source ?? "?"}->${target ?? "?"}`;
|
|
2065
|
+
}
|
|
2066
|
+
function isTimed(attrs) {
|
|
2067
|
+
return attrs.has("start") || attrs.has("end") || attrs.has("timestamp") || attrs.has("startopen") || attrs.has("endopen");
|
|
2068
|
+
}
|
|
2069
|
+
function stripAngles(text) {
|
|
2070
|
+
let inner = text.trim();
|
|
2071
|
+
if (inner.startsWith("<")) {
|
|
2072
|
+
inner = inner.slice(1);
|
|
2073
|
+
}
|
|
2074
|
+
if (inner.endsWith(">")) {
|
|
2075
|
+
inner = inner.slice(0, -1);
|
|
2076
|
+
}
|
|
2077
|
+
return inner;
|
|
2078
|
+
}
|
|
2079
|
+
function vizNumber(attrs, name) {
|
|
2080
|
+
const text = attrs.get(name);
|
|
2081
|
+
if (text === void 0) {
|
|
2082
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `viz element without a ${name} attribute`, { attribute: name });
|
|
2083
|
+
}
|
|
2084
|
+
const value = parseDecimalText(text);
|
|
2085
|
+
if (Number.isNaN(value)) {
|
|
2086
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `viz ${name}="${text}" is not a number`, { attribute: name });
|
|
2087
|
+
}
|
|
2088
|
+
return value;
|
|
2089
|
+
}
|
|
2090
|
+
function parseColor(attrs) {
|
|
2091
|
+
let r;
|
|
2092
|
+
let g;
|
|
2093
|
+
let b;
|
|
2094
|
+
const hex = attrs.get("hex");
|
|
2095
|
+
if (hex !== void 0 && !attrs.has("r")) {
|
|
2096
|
+
const digits = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
2097
|
+
if (!/^[0-9a-fA-F]{6}$/.test(digits)) {
|
|
2098
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `viz hex="${hex}" is not a #RRGGBB colour`, {
|
|
2099
|
+
attribute: "hex"
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
r = Number.parseInt(digits.slice(0, 2), 16);
|
|
2103
|
+
g = Number.parseInt(digits.slice(2, 4), 16);
|
|
2104
|
+
b = Number.parseInt(digits.slice(4, 6), 16);
|
|
2105
|
+
} else {
|
|
2106
|
+
r = vizNumber(attrs, "r");
|
|
2107
|
+
g = vizNumber(attrs, "g");
|
|
2108
|
+
b = vizNumber(attrs, "b");
|
|
2109
|
+
}
|
|
2110
|
+
const a = attrs.has("a") ? vizNumber(attrs, "a") : 1;
|
|
2111
|
+
return [r / 255, g / 255, b / 255, a];
|
|
2112
|
+
}
|
|
2113
|
+
function sniffGexf(head) {
|
|
2114
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(head);
|
|
2115
|
+
if (/<(\w+:)?gexf[\s>]/.test(text)) {
|
|
2116
|
+
return 1;
|
|
2117
|
+
}
|
|
2118
|
+
return text.includes("gexf.net") ? 0.8 : 0;
|
|
2119
|
+
}
|
|
2120
|
+
function resolveGexfOptions(options) {
|
|
2121
|
+
const viz = options?.viz;
|
|
2122
|
+
if (viz === void 0) {
|
|
2123
|
+
return true;
|
|
2124
|
+
}
|
|
2125
|
+
if (typeof viz !== "boolean") {
|
|
2126
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option viz: ${typeof viz} is not a boolean`, {
|
|
2127
|
+
option: "viz",
|
|
2128
|
+
found: viz
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
return viz;
|
|
2132
|
+
}
|
|
2133
|
+
const gexfImporter = Object.freeze({
|
|
2134
|
+
format: GEXF_FORMAT,
|
|
2135
|
+
extensions: Object.freeze([".gexf"]),
|
|
2136
|
+
mimeTypes: Object.freeze(["application/gexf+xml", "application/xml", "text/xml"]),
|
|
2137
|
+
sniff: sniffGexf,
|
|
2138
|
+
/**
|
|
2139
|
+
* Read a GEXF document into the sink in one streaming pass.
|
|
2140
|
+
* @param input - the document as text, bytes, a stream or chunks
|
|
2141
|
+
* @param sink - the sink
|
|
2142
|
+
* @param options - format-specific and common options
|
|
2143
|
+
* @returns the report
|
|
2144
|
+
*/
|
|
2145
|
+
async import(input, sink, options) {
|
|
2146
|
+
const resolved = resolveImportOptions(options, {
|
|
2147
|
+
ids: "canonical",
|
|
2148
|
+
defaultDirected: false,
|
|
2149
|
+
weightFrom: "weight",
|
|
2150
|
+
addMissingNodes: false
|
|
2151
|
+
});
|
|
2152
|
+
const viz = resolveGexfOptions(options);
|
|
2153
|
+
const report = new ImportReportBuilder(GEXF_FORMAT, resolved.errorLimit);
|
|
2154
|
+
reportSinkOptions(sink, options, report, true);
|
|
2155
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
2156
|
+
const reader = new GexfReader(sink, report, resolved, viz);
|
|
2157
|
+
try {
|
|
2158
|
+
await tokenizeXml(textChunks(input, report, resolved), reader);
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
if (err instanceof XmlSyntaxError) {
|
|
2161
|
+
report.fail(XML_SYNTAX_CODE, err.message, { line: err.line });
|
|
2162
|
+
}
|
|
2163
|
+
throw err;
|
|
2164
|
+
}
|
|
2165
|
+
reader.finish();
|
|
2166
|
+
throwIfAborted(resolved.signal);
|
|
2167
|
+
return report.finish();
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
const GEXF_LOSS = Object.freeze({
|
|
2171
|
+
/** GEXF 1.2 has no parallel-edge `kind`; the kind column is dropped. */
|
|
2172
|
+
KIND_DROPPED: "W_GEXF_KIND_DROPPED",
|
|
2173
|
+
/** GEXF 1.2 has no timestamps; a timestamp becomes a closed interval [t, t]. */
|
|
2174
|
+
TIMESTAMP_AS_INTERVAL: "W_TIMESTAMP_AS_INTERVAL",
|
|
2175
|
+
/** GEXF 1.2 liststring items are separated by `|`; an item containing one cannot be split back. */
|
|
2176
|
+
LIST_SEPARATOR: "W_LIST_SEPARATOR",
|
|
2177
|
+
/** A role column of a shape GEXF cannot map (a string `start`, a 2-component color); written as a plain attribute. */
|
|
2178
|
+
ROLE_SHAPE: "W_ROLE_SHAPE",
|
|
2179
|
+
/** A temporal extension table without the element / start / end / value columns of design section 5.10. */
|
|
2180
|
+
TEMPORAL_TABLE_SHAPE: "W_TEMPORAL_TABLE_SHAPE",
|
|
2181
|
+
/** An attribute whose title the importer would rename on re-import (a reserved name). */
|
|
2182
|
+
ATTRIBUTE_RENAMED: "W_ATTRIBUTE_RENAMED",
|
|
2183
|
+
/** A cell, default or option the declared type cannot express (skipped). */
|
|
2184
|
+
VALUE_UNWRITABLE: "W_VALUE_UNWRITABLE",
|
|
2185
|
+
/** A declared type the target version lacks (1.2: date, dateTime, typed lists...); the canonical type is written. */
|
|
2186
|
+
DECLARED_TYPE: "W_DECLARED_TYPE",
|
|
2187
|
+
/** A node id whose text reads back as the other type under the canonical rule (design section 4.1): a non-integer number, a string of integer text. */
|
|
2188
|
+
ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
|
|
2189
|
+
/** A viz role column (position, color, size, thickness) that is not f32; the importer reads viz values as f32. */
|
|
2190
|
+
VIZ_DTYPE: "W_GEXF_VIZ_DTYPE",
|
|
2191
|
+
/** A plain `weight` edge column reads back as THE weight (the importer's weightFrom default). */
|
|
2192
|
+
WEIGHT_KEY_CLASH: LOSS.WEIGHT_KEY_CLASH,
|
|
2193
|
+
/** A dict column without declared options gains one from its dictionary on re-import. */
|
|
2194
|
+
OPTIONS_GAINED: LOSS.OPTIONS_GAINED,
|
|
2195
|
+
/** A string cell holding a character XML 1.0 forbids; export() throws E_COLUMN_TYPE. */
|
|
2196
|
+
XML_ILLEGAL_CHAR: LOSS.XML_ILLEGAL_CHAR
|
|
2197
|
+
});
|
|
2198
|
+
const DTYPES_KEPT = ["f32", "f64", "i32", "bool", "dict", "string"];
|
|
2199
|
+
const CAPABILITIES_1_3 = capabilities({
|
|
2200
|
+
mixedDirection: true,
|
|
2201
|
+
multiEdges: true,
|
|
2202
|
+
selfLoops: true,
|
|
2203
|
+
edgeIds: "optional",
|
|
2204
|
+
idCharset: "any",
|
|
2205
|
+
dtypes: DTYPES_KEPT,
|
|
2206
|
+
lists: true,
|
|
2207
|
+
defaults: true,
|
|
2208
|
+
options: true,
|
|
2209
|
+
hierarchy: true,
|
|
2210
|
+
temporal: "dynamic-values",
|
|
2211
|
+
positions: true,
|
|
2212
|
+
viz: true
|
|
2213
|
+
});
|
|
2214
|
+
const CAPABILITIES_1_2 = capabilities({
|
|
2215
|
+
...CAPABILITIES_1_3,
|
|
2216
|
+
multiEdges: false,
|
|
2217
|
+
edgeIds: "required"
|
|
2218
|
+
});
|
|
2219
|
+
const STRUCTURAL_ROLES = /* @__PURE__ */ new Set(["directed", "pair", "mutual", "weight", "timeText"]);
|
|
2220
|
+
const DEFAULT_WEIGHT_TITLE = "weight";
|
|
2221
|
+
const NUMERIC_DTYPES = /* @__PURE__ */ new Set(["f32", "f64", "i32", "u32", "u8"]);
|
|
2222
|
+
const TEXT_DTYPES = /* @__PURE__ */ new Set(["string", "dict"]);
|
|
2223
|
+
function resolveVersion(options) {
|
|
2224
|
+
const version = options?.version;
|
|
2225
|
+
if (version === void 0) {
|
|
2226
|
+
return "1.3";
|
|
2227
|
+
}
|
|
2228
|
+
if (version !== "1.2" && version !== "1.3") {
|
|
2229
|
+
throw new GraphFormatError(
|
|
2230
|
+
"E_UNSUPPORTED",
|
|
2231
|
+
`option version: ${JSON.stringify(version)} is not "1.2" or "1.3"`,
|
|
2232
|
+
{
|
|
2233
|
+
option: "version",
|
|
2234
|
+
found: version,
|
|
2235
|
+
supported: ["1.2", "1.3"]
|
|
2236
|
+
}
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
return version;
|
|
2240
|
+
}
|
|
2241
|
+
function planExport(snapshot, options) {
|
|
2242
|
+
const version = resolveVersion(options);
|
|
2243
|
+
const resolved = resolveExportOptions(options);
|
|
2244
|
+
const caps = version === "1.2" ? CAPABILITIES_1_2 : CAPABILITIES_1_3;
|
|
2245
|
+
const notes = [
|
|
2246
|
+
...checkCapabilities(snapshot, caps, resolved, {
|
|
2247
|
+
openIntervals: version === "1.2",
|
|
2248
|
+
temporalText: true,
|
|
2249
|
+
positionDtype: "f32",
|
|
2250
|
+
roles: MAPPED_ROLES,
|
|
2251
|
+
roleNames: ROLE_NAMES
|
|
2252
|
+
})
|
|
2253
|
+
];
|
|
2254
|
+
const note = (code, message, column = null, count = null) => {
|
|
2255
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
2256
|
+
};
|
|
2257
|
+
notes.push(...xmlIllegalTextNotes(snapshot));
|
|
2258
|
+
let typeChanges = 0;
|
|
2259
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
2260
|
+
const id = snapshot.ids.idOf(i);
|
|
2261
|
+
if (typeof id === "number" ? !Number.isSafeInteger(id) : isCanonicalIntegerText(id)) {
|
|
2262
|
+
typeChanges++;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
if (typeChanges > 0) {
|
|
2266
|
+
note(
|
|
2267
|
+
GEXF_LOSS.ID_TEXT_TYPE,
|
|
2268
|
+
`${typeChanges} node id(s) change type when read back under ids: "canonical" (string ids that are integer text, non-integer numbers); the file's idtype is not honoured by the importer`,
|
|
2269
|
+
null,
|
|
2270
|
+
typeChanges
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
const tables = collectTemporalTables(snapshot, version, note);
|
|
2274
|
+
const nodeRoles = collectRoles(snapshot.nodes, "node", note);
|
|
2275
|
+
const edgeRoles = collectRoles(snapshot.edges, "edge", note);
|
|
2276
|
+
const nodeAttrs = collectAttributes(snapshot, "node", nodeRoles, tables.get("node") ?? /* @__PURE__ */ new Map(), version, note);
|
|
2277
|
+
const edgeAttrs = collectAttributes(snapshot, "edge", edgeRoles, tables.get("edge") ?? /* @__PURE__ */ new Map(), version, note);
|
|
2278
|
+
const temporal = hasLifetime(nodeRoles) || hasLifetime(edgeRoles) || nodeAttrs.some((a) => a.dynamic) || edgeAttrs.some((a) => a.dynamic);
|
|
2279
|
+
if (version === "1.2") {
|
|
2280
|
+
for (const [domain, roles] of [
|
|
2281
|
+
["node", nodeRoles],
|
|
2282
|
+
["edge", edgeRoles]
|
|
2283
|
+
]) {
|
|
2284
|
+
if (roles.timestamp !== null || roles.timestamps !== null) {
|
|
2285
|
+
note(
|
|
2286
|
+
GEXF_LOSS.TIMESTAMP_AS_INTERVAL,
|
|
2287
|
+
`${domain} timestamps are written as closed intervals; GEXF 1.2 has no timestamp representation`,
|
|
2288
|
+
(roles.timestamp ?? roles.timestamps)?.meta.name ?? null,
|
|
2289
|
+
null
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
if (edgeRoles.kind !== null) {
|
|
2294
|
+
note(
|
|
2295
|
+
GEXF_LOSS.KIND_DROPPED,
|
|
2296
|
+
`edge column "${edgeRoles.kind.meta.name}" (kind) cannot be written; GEXF 1.2 has no edge kind`,
|
|
2297
|
+
edgeRoles.kind.meta.name,
|
|
2298
|
+
edgeRoles.kind.length - edgeRoles.kind.nullCount
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
const { meta } = snapshot;
|
|
2303
|
+
let { timeRepresentation } = meta;
|
|
2304
|
+
if (timeRepresentation === null && temporal) {
|
|
2305
|
+
const timestampOnly = (nodeRoles.timestamp !== null || edgeRoles.timestamp !== null) && nodeRoles.start === null && nodeRoles.end === null && edgeRoles.start === null && edgeRoles.end === null;
|
|
2306
|
+
timeRepresentation = timestampOnly ? "timestamp" : null;
|
|
2307
|
+
}
|
|
2308
|
+
return {
|
|
2309
|
+
version,
|
|
2310
|
+
options: resolved,
|
|
2311
|
+
notes,
|
|
2312
|
+
nodeRoles,
|
|
2313
|
+
edgeRoles,
|
|
2314
|
+
nodeAttrs,
|
|
2315
|
+
edgeAttrs,
|
|
2316
|
+
timeFormat: meta.timeFormat,
|
|
2317
|
+
temporal,
|
|
2318
|
+
timeRepresentation
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
function hasLifetime(roles) {
|
|
2322
|
+
return roles.start !== null || roles.end !== null || roles.timestamp !== null || roles.spells !== null || roles.timestamps !== null;
|
|
2323
|
+
}
|
|
2324
|
+
function isNumericScalar(column) {
|
|
2325
|
+
return NUMERIC_DTYPES.has(column.dtype) && column.meta.components === 1;
|
|
2326
|
+
}
|
|
2327
|
+
function isNumericVector(column, components) {
|
|
2328
|
+
return NUMERIC_DTYPES.has(column.dtype) && components.includes(column.meta.components);
|
|
2329
|
+
}
|
|
2330
|
+
function isText(column) {
|
|
2331
|
+
return TEXT_DTYPES.has(column.dtype);
|
|
2332
|
+
}
|
|
2333
|
+
function isNumericList(column, itemComponents) {
|
|
2334
|
+
return column.dtype === "list" && column.meta.itemDtype !== null && NUMERIC_DTYPES.has(column.meta.itemDtype) && (column.meta.itemComponents ?? 1) === itemComponents;
|
|
2335
|
+
}
|
|
2336
|
+
function roleShapeOk(role, column, domain) {
|
|
2337
|
+
switch (role) {
|
|
2338
|
+
case "label":
|
|
2339
|
+
case "shape":
|
|
2340
|
+
return isText(column);
|
|
2341
|
+
case "kind":
|
|
2342
|
+
return domain === "edge" && isText(column);
|
|
2343
|
+
case "id":
|
|
2344
|
+
return domain === "edge" && (isText(column) || isNumericScalar(column));
|
|
2345
|
+
case "start":
|
|
2346
|
+
case "end":
|
|
2347
|
+
case "timestamp":
|
|
2348
|
+
case "open":
|
|
2349
|
+
return isNumericScalar(column);
|
|
2350
|
+
case "size":
|
|
2351
|
+
return domain === "node" && isNumericScalar(column);
|
|
2352
|
+
case "thickness":
|
|
2353
|
+
return domain === "edge" && isNumericScalar(column);
|
|
2354
|
+
case "spells":
|
|
2355
|
+
return isNumericList(column, 2);
|
|
2356
|
+
case "timestamps":
|
|
2357
|
+
return isNumericList(column, 1);
|
|
2358
|
+
case "position":
|
|
2359
|
+
return domain === "node" && isNumericVector(column, [2, 3]);
|
|
2360
|
+
case "color":
|
|
2361
|
+
return isNumericVector(column, [3, 4]);
|
|
2362
|
+
case "parent":
|
|
2363
|
+
return domain === "node" && column.dtype === "u32" && column.meta.components === 1;
|
|
2364
|
+
case "parents":
|
|
2365
|
+
return domain === "node" && column.dtype === "list" && column.meta.itemDtype === "u32";
|
|
2366
|
+
default:
|
|
2367
|
+
return false;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
const VIZ_NUMERIC_ROLES = /* @__PURE__ */ new Set(["position", "color", "size", "thickness"]);
|
|
2371
|
+
const MAPPED_ROLES = /* @__PURE__ */ new Set([
|
|
2372
|
+
"label",
|
|
2373
|
+
"id",
|
|
2374
|
+
"kind",
|
|
2375
|
+
"start",
|
|
2376
|
+
"end",
|
|
2377
|
+
"timestamp",
|
|
2378
|
+
"spells",
|
|
2379
|
+
"timestamps",
|
|
2380
|
+
"open",
|
|
2381
|
+
"position",
|
|
2382
|
+
"color",
|
|
2383
|
+
"size",
|
|
2384
|
+
"shape",
|
|
2385
|
+
"thickness",
|
|
2386
|
+
"parent",
|
|
2387
|
+
"parents"
|
|
2388
|
+
]);
|
|
2389
|
+
const ROLE_NAMES = Object.freeze({
|
|
2390
|
+
label: NODE_COLUMNS.label,
|
|
2391
|
+
id: "id",
|
|
2392
|
+
kind: "kind",
|
|
2393
|
+
start: NODE_COLUMNS.start,
|
|
2394
|
+
end: NODE_COLUMNS.end,
|
|
2395
|
+
timestamp: NODE_COLUMNS.timestamp,
|
|
2396
|
+
spells: NODE_COLUMNS.spells,
|
|
2397
|
+
timestamps: NODE_COLUMNS.timestamps,
|
|
2398
|
+
open: NODE_COLUMNS.open,
|
|
2399
|
+
position: NODE_COLUMNS.position,
|
|
2400
|
+
color: NODE_COLUMNS.color,
|
|
2401
|
+
size: NODE_COLUMNS.size,
|
|
2402
|
+
shape: NODE_COLUMNS.shape,
|
|
2403
|
+
thickness: "thickness",
|
|
2404
|
+
parent: NODE_COLUMNS.parent,
|
|
2405
|
+
parents: NODE_COLUMNS.parents
|
|
2406
|
+
});
|
|
2407
|
+
function collectRoles(table, domain, note) {
|
|
2408
|
+
const companions = /* @__PURE__ */ new Map();
|
|
2409
|
+
const roles = {
|
|
2410
|
+
label: null,
|
|
2411
|
+
id: null,
|
|
2412
|
+
kind: null,
|
|
2413
|
+
start: null,
|
|
2414
|
+
end: null,
|
|
2415
|
+
timestamp: null,
|
|
2416
|
+
spells: null,
|
|
2417
|
+
timestamps: null,
|
|
2418
|
+
open: null,
|
|
2419
|
+
position: null,
|
|
2420
|
+
color: null,
|
|
2421
|
+
size: null,
|
|
2422
|
+
shape: null,
|
|
2423
|
+
shapeUri: null,
|
|
2424
|
+
thickness: null,
|
|
2425
|
+
parent: null,
|
|
2426
|
+
parents: null,
|
|
2427
|
+
companions
|
|
2428
|
+
};
|
|
2429
|
+
for (const column of table) {
|
|
2430
|
+
const { meta } = column;
|
|
2431
|
+
const forName = meta.extra.for;
|
|
2432
|
+
if ((meta.role === "timeText" || typeof forName === "string") && column.dtype === "string") {
|
|
2433
|
+
if (typeof forName === "string") {
|
|
2434
|
+
companions.set(forName, column);
|
|
2435
|
+
}
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
if (domain === "node" && meta.name === NODE_COLUMNS.shapeUri && meta.origin?.namespace === VIZ_NAMESPACE) {
|
|
2439
|
+
roles.shapeUri = column.dtype === "string" ? column : null;
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
const { role } = meta;
|
|
2443
|
+
if (role === null || !MAPPED_ROLES.has(role)) {
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (!roleShapeOk(role, column, domain)) {
|
|
2447
|
+
note(
|
|
2448
|
+
GEXF_LOSS.ROLE_SHAPE,
|
|
2449
|
+
`${domain} column "${meta.name}" (${role}) has a shape GEXF cannot map (${describeShape(column)}); written as a plain attribute`,
|
|
2450
|
+
meta.name,
|
|
2451
|
+
null
|
|
2452
|
+
);
|
|
2453
|
+
const set = column.length - column.nullCount;
|
|
2454
|
+
if (NUMERIC_DTYPES.has(column.dtype) && meta.components > 1) {
|
|
2455
|
+
note(
|
|
2456
|
+
LOSS.COMPONENTS,
|
|
2457
|
+
`${domain} column "${meta.name}" has ${meta.components} components; written as a list`,
|
|
2458
|
+
meta.name,
|
|
2459
|
+
set
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
if (column.dtype === "json") {
|
|
2463
|
+
note(
|
|
2464
|
+
LOSS.JSON,
|
|
2465
|
+
`${domain} column "${meta.name}" holds nested values; written as JSON text`,
|
|
2466
|
+
meta.name,
|
|
2467
|
+
set
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
if (column.dtype === "u32" || column.dtype === "u8") {
|
|
2471
|
+
note(
|
|
2472
|
+
LOSS.DTYPE,
|
|
2473
|
+
`${domain} column "${meta.name}" is ${column.dtype}; the format cannot keep that dtype`,
|
|
2474
|
+
meta.name,
|
|
2475
|
+
set
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
if (VIZ_NUMERIC_ROLES.has(role) && column.dtype !== "f32") {
|
|
2481
|
+
note(
|
|
2482
|
+
GEXF_LOSS.VIZ_DTYPE,
|
|
2483
|
+
`${domain} column "${meta.name}" (${role}) is ${column.dtype}; viz values read back as f32`,
|
|
2484
|
+
meta.name,
|
|
2485
|
+
column.length - column.nullCount
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
roles[role] = column;
|
|
2489
|
+
}
|
|
2490
|
+
return roles;
|
|
2491
|
+
}
|
|
2492
|
+
function describeShape(column) {
|
|
2493
|
+
if (column.dtype === "list") {
|
|
2494
|
+
return `list of ${column.meta.itemDtype ?? "?"} x ${column.meta.itemComponents ?? 1}`;
|
|
2495
|
+
}
|
|
2496
|
+
return `${column.dtype} x ${column.meta.components}`;
|
|
2497
|
+
}
|
|
2498
|
+
function collectTemporalTables(snapshot, version, note) {
|
|
2499
|
+
const out = /* @__PURE__ */ new Map();
|
|
2500
|
+
for (const [name, table] of snapshot.extensions) {
|
|
2501
|
+
const parsed = parseTemporalTableName(name);
|
|
2502
|
+
if (parsed === null) {
|
|
2503
|
+
continue;
|
|
2504
|
+
}
|
|
2505
|
+
const element = table.get(TEMPORAL_COLUMNS.element);
|
|
2506
|
+
const start = table.get(TEMPORAL_COLUMNS.start);
|
|
2507
|
+
const end = table.get(TEMPORAL_COLUMNS.end);
|
|
2508
|
+
const value = table.get(TEMPORAL_COLUMNS.value);
|
|
2509
|
+
if (element === null || element.dtype !== "u32" || start === null || !isNumericScalar(start) || end === null || !isNumericScalar(end) || value === null) {
|
|
2510
|
+
note(
|
|
2511
|
+
GEXF_LOSS.TEMPORAL_TABLE_SHAPE,
|
|
2512
|
+
`extension table "${name}" lacks the element / start / end / value columns and cannot be written`,
|
|
2513
|
+
name,
|
|
2514
|
+
table.rowCount
|
|
2515
|
+
);
|
|
2516
|
+
continue;
|
|
2517
|
+
}
|
|
2518
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2519
|
+
for (let r = 0; r < table.rowCount; r++) {
|
|
2520
|
+
if (!element.isSet(r)) {
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
const e = element.value(r);
|
|
2524
|
+
const list = rows.get(e);
|
|
2525
|
+
if (list === void 0) {
|
|
2526
|
+
rows.set(e, [r]);
|
|
2527
|
+
} else {
|
|
2528
|
+
list.push(r);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
const typed = attributeType(value, version);
|
|
2532
|
+
const open = optionalNumeric(table.get(TEMPORAL_COLUMNS.open));
|
|
2533
|
+
if (version === "1.3" && open !== null && open.length - open.nullCount > 0) {
|
|
2534
|
+
note(
|
|
2535
|
+
LOSS.OPEN_INTERVAL,
|
|
2536
|
+
`dynamic values of "${name}" carry open intervals, which GEXF 1.3 cannot write`,
|
|
2537
|
+
name,
|
|
2538
|
+
open.length - open.nullCount
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
const resolved = {
|
|
2542
|
+
element,
|
|
2543
|
+
start,
|
|
2544
|
+
end,
|
|
2545
|
+
value,
|
|
2546
|
+
startText: optionalText(table.get(TEMPORAL_COLUMNS.startText)),
|
|
2547
|
+
endText: optionalText(table.get(TEMPORAL_COLUMNS.endText)),
|
|
2548
|
+
valueText: isTemporalType(typed.type) ? optionalText(table.get(TEMPORAL_COLUMNS.valueText)) : null,
|
|
2549
|
+
open,
|
|
2550
|
+
rows,
|
|
2551
|
+
format: typed.format
|
|
2552
|
+
};
|
|
2553
|
+
let byDomain = out.get(parsed.domain);
|
|
2554
|
+
if (byDomain === void 0) {
|
|
2555
|
+
byDomain = /* @__PURE__ */ new Map();
|
|
2556
|
+
out.set(parsed.domain, byDomain);
|
|
2557
|
+
}
|
|
2558
|
+
byDomain.set(parsed.column, resolved);
|
|
2559
|
+
}
|
|
2560
|
+
return out;
|
|
2561
|
+
}
|
|
2562
|
+
function optionalText(column) {
|
|
2563
|
+
return column !== null && column.dtype === "string" ? column : null;
|
|
2564
|
+
}
|
|
2565
|
+
function optionalNumeric(column) {
|
|
2566
|
+
return column !== null && isNumericScalar(column) ? column : null;
|
|
2567
|
+
}
|
|
2568
|
+
function attributeType(column, version) {
|
|
2569
|
+
const { meta } = column;
|
|
2570
|
+
const originType = meta.origin?.type ?? null;
|
|
2571
|
+
if (meta.dtype === "list" || NUMERIC_DTYPES.has(meta.dtype) && meta.components > 1) {
|
|
2572
|
+
const itemDtype = meta.dtype === "list" ? meta.itemDtype ?? "string" : meta.dtype;
|
|
2573
|
+
let itemType = canonicalScalarType(itemDtype);
|
|
2574
|
+
if (originType !== null && meta.dtype === "list" && isListType(originType) && agrees(originType, "list", itemDtype)) {
|
|
2575
|
+
itemType = originType.slice(4);
|
|
2576
|
+
}
|
|
2577
|
+
if (version === "1.2") {
|
|
2578
|
+
const itemFormat = scalarFormatter(itemType, itemDtype);
|
|
2579
|
+
return {
|
|
2580
|
+
type: "liststring",
|
|
2581
|
+
list: true,
|
|
2582
|
+
itemType: "string",
|
|
2583
|
+
format: listFormatter(itemFormat, version),
|
|
2584
|
+
dropped: itemType === "string" ? null : `list${itemType}`
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
return {
|
|
2588
|
+
type: `list${itemType}`,
|
|
2589
|
+
list: true,
|
|
2590
|
+
itemType,
|
|
2591
|
+
format: listFormatter(scalarFormatter(itemType, itemDtype), version),
|
|
2592
|
+
dropped: null
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
let type = canonicalScalarType(meta.dtype);
|
|
2596
|
+
let dropped = null;
|
|
2597
|
+
const temporal = originType === null || meta.dtype !== "f64" ? null : gexfTemporalType(originType);
|
|
2598
|
+
if (temporal !== null) {
|
|
2599
|
+
if (isScalarType(temporal, version)) {
|
|
2600
|
+
type = temporal;
|
|
2601
|
+
} else {
|
|
2602
|
+
dropped = temporal;
|
|
2603
|
+
}
|
|
2604
|
+
} else if (originType !== null && agrees(originType, meta.dtype, null)) {
|
|
2605
|
+
if (isScalarType(originType, version)) {
|
|
2606
|
+
type = originType;
|
|
2607
|
+
} else if (isScalarType(originType, "1.3")) {
|
|
2608
|
+
dropped = originType;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
return { type, list: false, itemType: type, format: scalarFormatter(type, meta.dtype), dropped };
|
|
2612
|
+
}
|
|
2613
|
+
function gexfTemporalType(originType) {
|
|
2614
|
+
switch (originType.toLowerCase()) {
|
|
2615
|
+
case "date":
|
|
2616
|
+
return "date";
|
|
2617
|
+
case "datetime":
|
|
2618
|
+
case "localdatetime":
|
|
2619
|
+
return "dateTime";
|
|
2620
|
+
default:
|
|
2621
|
+
return null;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
function agrees(type, dtype, itemDtype) {
|
|
2625
|
+
const spec = mapDeclaredType("gexf", type, "f64");
|
|
2626
|
+
if (spec === null) {
|
|
2627
|
+
return false;
|
|
2628
|
+
}
|
|
2629
|
+
if (dtype === "list") {
|
|
2630
|
+
return spec.list && spec.itemDtype === itemDtype;
|
|
2631
|
+
}
|
|
2632
|
+
if (spec.list) {
|
|
2633
|
+
return false;
|
|
2634
|
+
}
|
|
2635
|
+
return spec.dtype === dtype || dtype === "dict" && spec.dtype === "string";
|
|
2636
|
+
}
|
|
2637
|
+
function scalarFormatter(type, dtype) {
|
|
2638
|
+
switch (type) {
|
|
2639
|
+
case "boolean":
|
|
2640
|
+
return (value) => {
|
|
2641
|
+
if (typeof value === "boolean") {
|
|
2642
|
+
return value ? "true" : "false";
|
|
2643
|
+
}
|
|
2644
|
+
if (typeof value !== "number") {
|
|
2645
|
+
return null;
|
|
2646
|
+
}
|
|
2647
|
+
return value !== 0 ? "true" : "false";
|
|
2648
|
+
};
|
|
2649
|
+
case "integer":
|
|
2650
|
+
case "long":
|
|
2651
|
+
case "byte":
|
|
2652
|
+
case "short":
|
|
2653
|
+
case "biginteger":
|
|
2654
|
+
return (value) => {
|
|
2655
|
+
if (typeof value === "number") {
|
|
2656
|
+
return Number.isInteger(value) ? formatInteger(value) : formatF64(value);
|
|
2657
|
+
}
|
|
2658
|
+
return typeof value === "string" ? value : null;
|
|
2659
|
+
};
|
|
2660
|
+
case "float":
|
|
2661
|
+
return (value) => typeof value === "number" ? formatF32(value) : null;
|
|
2662
|
+
case "double":
|
|
2663
|
+
case "bigdecimal":
|
|
2664
|
+
return (value) => {
|
|
2665
|
+
if (typeof value === "number") {
|
|
2666
|
+
return dtype === "f32" ? formatF32(value) : formatF64(value);
|
|
2667
|
+
}
|
|
2668
|
+
return typeof value === "string" ? value : null;
|
|
2669
|
+
};
|
|
2670
|
+
case "date":
|
|
2671
|
+
case "dateTime":
|
|
2672
|
+
return (value) => {
|
|
2673
|
+
if (typeof value === "number") {
|
|
2674
|
+
return formatTemporal(value, type);
|
|
2675
|
+
}
|
|
2676
|
+
return typeof value === "string" ? value : null;
|
|
2677
|
+
};
|
|
2678
|
+
default:
|
|
2679
|
+
return (value) => {
|
|
2680
|
+
switch (typeof value) {
|
|
2681
|
+
case "string":
|
|
2682
|
+
return value;
|
|
2683
|
+
case "number":
|
|
2684
|
+
return dtype === "f32" ? formatF32(value) : formatF64(value);
|
|
2685
|
+
case "boolean":
|
|
2686
|
+
return value ? "true" : "false";
|
|
2687
|
+
case "object":
|
|
2688
|
+
return value === null ? null : JSON.stringify(value);
|
|
2689
|
+
default:
|
|
2690
|
+
return null;
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
function listFormatter(item, version) {
|
|
2696
|
+
return (value) => {
|
|
2697
|
+
if (!Array.isArray(value) && !ArrayBuffer.isView(value)) {
|
|
2698
|
+
return null;
|
|
2699
|
+
}
|
|
2700
|
+
const items = [];
|
|
2701
|
+
for (const entry of Array.from(value)) {
|
|
2702
|
+
const text = item(entry);
|
|
2703
|
+
if (text === null) {
|
|
2704
|
+
return null;
|
|
2705
|
+
}
|
|
2706
|
+
items.push(text);
|
|
2707
|
+
}
|
|
2708
|
+
return joinListText(items, version === "1.2" ? "pipe" : "gexf");
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
function collectAttributes(snapshot, domain, roles, tables, version, note) {
|
|
2712
|
+
const table = domain === "node" ? snapshot.nodes : snapshot.edges;
|
|
2713
|
+
const reserved = domain === "node" ? RESERVED_NODE_NAMES : RESERVED_EDGE_NAMES;
|
|
2714
|
+
const roleColumns = /* @__PURE__ */ new Set();
|
|
2715
|
+
for (const value of Object.values(roles)) {
|
|
2716
|
+
if (value !== null && !(value instanceof Map)) {
|
|
2717
|
+
roleColumns.add(value);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
2721
|
+
const specs = [];
|
|
2722
|
+
const usedTables = /* @__PURE__ */ new Set();
|
|
2723
|
+
const declare = (column, name, origin, typed, dynamic, temporal) => {
|
|
2724
|
+
const id = uniqueId(origin.id ?? name, name, usedIds);
|
|
2725
|
+
const title = origin.title ?? name;
|
|
2726
|
+
if (typed.dropped !== null) {
|
|
2727
|
+
note(
|
|
2728
|
+
GEXF_LOSS.DECLARED_TYPE,
|
|
2729
|
+
`${domain} column "${name}" is declared ${typed.dropped}, which GEXF ${version} lacks; written as ${typed.type}`,
|
|
2730
|
+
name,
|
|
2731
|
+
null
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
if (domain === "edge" && column !== null && column.meta.role === null && title === DEFAULT_WEIGHT_TITLE) {
|
|
2735
|
+
note(
|
|
2736
|
+
GEXF_LOSS.WEIGHT_KEY_CLASH,
|
|
2737
|
+
`edge column "${name}" is written as an attribute titled "${title}", which the importer reads as THE weight (weightFrom); it reads back as the weight, not as a column`,
|
|
2738
|
+
name,
|
|
2739
|
+
column.length - column.nullCount
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
const reimportName = reserved.has(title) ? `${title}#${id}` : title;
|
|
2743
|
+
if (reimportName !== name) {
|
|
2744
|
+
note(
|
|
2745
|
+
GEXF_LOSS.ATTRIBUTE_RENAMED,
|
|
2746
|
+
`${domain} column "${name}" is written with title "${title}" and reads back as "${reimportName}"`,
|
|
2747
|
+
name,
|
|
2748
|
+
null
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
const { defaultText, optionsText } = column === null ? { defaultText: null, optionsText: null } : declaredTexts(column, domain, typed, version, note);
|
|
2752
|
+
const companion = column === null ? null : roles.companions.get(name) ?? null;
|
|
2753
|
+
if (companion !== null && !isTemporalType(typed.type)) {
|
|
2754
|
+
note(
|
|
2755
|
+
LOSS.TEMPORAL_TEXT,
|
|
2756
|
+
`${domain} column "${companion.meta.name}" (the lexical form of "${name}") cannot be written: "${name}" is written as ${typed.type}, not a GEXF temporal type`,
|
|
2757
|
+
companion.meta.name,
|
|
2758
|
+
companion.length - companion.nullCount
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
specs.push({
|
|
2762
|
+
id,
|
|
2763
|
+
title,
|
|
2764
|
+
type: typed.type,
|
|
2765
|
+
column,
|
|
2766
|
+
companion: isTemporalType(typed.type) ? companion : null,
|
|
2767
|
+
format: typed.format,
|
|
2768
|
+
defaultText,
|
|
2769
|
+
optionsText,
|
|
2770
|
+
dynamic,
|
|
2771
|
+
table: temporal
|
|
2772
|
+
});
|
|
2773
|
+
};
|
|
2774
|
+
for (const column of table) {
|
|
2775
|
+
const { meta } = column;
|
|
2776
|
+
if (roleColumns.has(column) || meta.role !== null && STRUCTURAL_ROLES.has(meta.role)) {
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
if (typeof meta.extra.for === "string" && column.dtype === "string") {
|
|
2780
|
+
continue;
|
|
2781
|
+
}
|
|
2782
|
+
if (domain === "node" && column === roles.shapeUri) {
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
const temporal = tables.get(meta.name) ?? null;
|
|
2786
|
+
if (temporal !== null) {
|
|
2787
|
+
usedTables.add(meta.name);
|
|
2788
|
+
}
|
|
2789
|
+
declare(
|
|
2790
|
+
column,
|
|
2791
|
+
meta.name,
|
|
2792
|
+
{ id: meta.origin?.id ?? null, title: meta.origin?.title ?? null },
|
|
2793
|
+
attributeType(column, version),
|
|
2794
|
+
meta.dynamic || temporal !== null,
|
|
2795
|
+
temporal
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
for (const [name, temporal] of tables) {
|
|
2799
|
+
if (usedTables.has(name)) {
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
const { origin } = temporal.value.meta;
|
|
2803
|
+
const { weightOrigin } = snapshot.meta;
|
|
2804
|
+
const weightId = domain === "edge" && weightOrigin !== null && (weightOrigin.title ?? weightOrigin.id) === name ? weightOrigin.id : null;
|
|
2805
|
+
declare(
|
|
2806
|
+
null,
|
|
2807
|
+
name,
|
|
2808
|
+
{ id: origin?.id ?? weightId, title: null },
|
|
2809
|
+
attributeType(temporal.value, version),
|
|
2810
|
+
true,
|
|
2811
|
+
temporal
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
return specs;
|
|
2815
|
+
}
|
|
2816
|
+
function declaredTexts(column, domain, typed, version, note) {
|
|
2817
|
+
const { meta } = column;
|
|
2818
|
+
const { name } = meta;
|
|
2819
|
+
let defaultText = null;
|
|
2820
|
+
let optionsText = null;
|
|
2821
|
+
if (meta.default !== void 0) {
|
|
2822
|
+
defaultText = typed.format(meta.default);
|
|
2823
|
+
if (defaultText === null) {
|
|
2824
|
+
note(
|
|
2825
|
+
GEXF_LOSS.VALUE_UNWRITABLE,
|
|
2826
|
+
`${domain} column "${name}": the default cannot be written as ${typed.type}`,
|
|
2827
|
+
name,
|
|
2828
|
+
null
|
|
2829
|
+
);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
const options = meta.options ?? (column.dtype === "dict" ? column.dictionary : null);
|
|
2833
|
+
if (meta.options === null && options !== null && options.length > 0) {
|
|
2834
|
+
note(
|
|
2835
|
+
GEXF_LOSS.OPTIONS_GAINED,
|
|
2836
|
+
`${domain} column "${name}" declares no options; its dictionary is written as <options> and reads back as declared options`,
|
|
2837
|
+
name,
|
|
2838
|
+
null
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
if (options !== null) {
|
|
2842
|
+
const itemFormat = typed.list ? scalarFormatter(typed.itemType, meta.itemDtype ?? "string") : typed.format;
|
|
2843
|
+
const texts = [];
|
|
2844
|
+
let ok = true;
|
|
2845
|
+
for (const option of options) {
|
|
2846
|
+
const text = itemFormat(option);
|
|
2847
|
+
if (text === null) {
|
|
2848
|
+
ok = false;
|
|
2849
|
+
break;
|
|
2850
|
+
}
|
|
2851
|
+
texts.push(text);
|
|
2852
|
+
}
|
|
2853
|
+
if (ok) {
|
|
2854
|
+
optionsText = joinListText(texts, version === "1.2" ? "pipe" : "gexf");
|
|
2855
|
+
} else {
|
|
2856
|
+
note(
|
|
2857
|
+
GEXF_LOSS.VALUE_UNWRITABLE,
|
|
2858
|
+
`${domain} column "${name}": the options cannot be written as ${typed.type}`,
|
|
2859
|
+
name,
|
|
2860
|
+
null
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
if (version === "1.2" && column.dtype === "list") {
|
|
2865
|
+
let count = 0;
|
|
2866
|
+
for (let r = 0; r < column.length; r++) {
|
|
2867
|
+
if (column.isSet(r) && listItems(column, r).some((item) => String(item).includes("|"))) {
|
|
2868
|
+
count++;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
if (count > 0) {
|
|
2872
|
+
note(
|
|
2873
|
+
GEXF_LOSS.LIST_SEPARATOR,
|
|
2874
|
+
`${domain} column "${name}": ${count} row(s) hold an item containing "|", the 1.2 list separator`,
|
|
2875
|
+
name,
|
|
2876
|
+
count
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
return { defaultText, optionsText };
|
|
2881
|
+
}
|
|
2882
|
+
function isTemporalType(type) {
|
|
2883
|
+
return type === "date" || type === "dateTime";
|
|
2884
|
+
}
|
|
2885
|
+
function uniqueId(preferred, name, used) {
|
|
2886
|
+
const candidates = [preferred, name];
|
|
2887
|
+
for (const candidate of candidates) {
|
|
2888
|
+
if (candidate.length > 0 && !used.has(candidate)) {
|
|
2889
|
+
used.add(candidate);
|
|
2890
|
+
return candidate;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
for (let n = 2; ; n++) {
|
|
2894
|
+
const candidate = `${name}#${n}`;
|
|
2895
|
+
if (!used.has(candidate)) {
|
|
2896
|
+
used.add(candidate);
|
|
2897
|
+
return candidate;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
function timeText(column, companion, row, timeFormat) {
|
|
2902
|
+
if (companion !== null && companion.isSet(row)) {
|
|
2903
|
+
return companion.value(row);
|
|
2904
|
+
}
|
|
2905
|
+
return formatTimeValue(column.value(row), timeFormat);
|
|
2906
|
+
}
|
|
2907
|
+
function lifetimeAttrs(roles, row, plan) {
|
|
2908
|
+
let out = "";
|
|
2909
|
+
const { timeFormat, version } = plan;
|
|
2910
|
+
let bits = 0;
|
|
2911
|
+
if (version === "1.2" && roles.open !== null && roles.open.isSet(row)) {
|
|
2912
|
+
bits = roles.open.value(row);
|
|
2913
|
+
}
|
|
2914
|
+
const write = (name, column, open) => {
|
|
2915
|
+
if (column === null || !column.isSet(row)) {
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
const value = column.value(row);
|
|
2919
|
+
if (!Number.isFinite(value)) {
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
const text = timeText(column, roles.companions.get(column.meta.name) ?? null, row, timeFormat);
|
|
2923
|
+
out += ` ${open ? `${name}open` : name}="${escapeXmlAttribute(text)}"`;
|
|
2924
|
+
};
|
|
2925
|
+
if (roles.timestamp !== null && roles.timestamp.isSet(row)) {
|
|
2926
|
+
if (version === "1.3" && plan.timeRepresentation === "timestamp") {
|
|
2927
|
+
write("timestamp", roles.timestamp, false);
|
|
2928
|
+
} else {
|
|
2929
|
+
write("start", roles.timestamp, false);
|
|
2930
|
+
write("end", roles.timestamp, false);
|
|
2931
|
+
}
|
|
2932
|
+
} else {
|
|
2933
|
+
write("start", roles.start, (bits & OPEN_START) !== 0);
|
|
2934
|
+
write("end", roles.end, (bits & OPEN_END) !== 0);
|
|
2935
|
+
}
|
|
2936
|
+
if (version === "1.3" && roles.timestamps !== null && roles.timestamps.isSet(row)) {
|
|
2937
|
+
const items = listItems(roles.timestamps, row).map((t) => formatTimeValue(t, timeFormat));
|
|
2938
|
+
out += ` timestamps="${escapeXmlAttribute(`<[${items.join(", ")}]>`)}"`;
|
|
2939
|
+
}
|
|
2940
|
+
return out;
|
|
2941
|
+
}
|
|
2942
|
+
function spellsElement(roles, row, plan, indent) {
|
|
2943
|
+
const pairs = [];
|
|
2944
|
+
if (roles.spells !== null && roles.spells.isSet(row)) {
|
|
2945
|
+
for (const pair of listItems(roles.spells, row)) {
|
|
2946
|
+
const [s, e] = Array.from(pair);
|
|
2947
|
+
pairs.push([s, e]);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
if (plan.version === "1.2" && roles.timestamps !== null && roles.timestamps.isSet(row)) {
|
|
2951
|
+
for (const t of listItems(roles.timestamps, row)) {
|
|
2952
|
+
pairs.push([t, t]);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
if (pairs.length === 0) {
|
|
2956
|
+
return "";
|
|
2957
|
+
}
|
|
2958
|
+
let out = `${indent}<spells>
|
|
2959
|
+
`;
|
|
2960
|
+
for (const [s, e] of pairs) {
|
|
2961
|
+
let attrs = "";
|
|
2962
|
+
if (Number.isFinite(s)) {
|
|
2963
|
+
attrs += ` start="${escapeXmlAttribute(formatTimeValue(s, plan.timeFormat))}"`;
|
|
2964
|
+
}
|
|
2965
|
+
if (Number.isFinite(e)) {
|
|
2966
|
+
attrs += ` end="${escapeXmlAttribute(formatTimeValue(e, plan.timeFormat))}"`;
|
|
2967
|
+
}
|
|
2968
|
+
out += `${indent} <spell${attrs}/>
|
|
2969
|
+
`;
|
|
2970
|
+
}
|
|
2971
|
+
return `${out}${indent}</spells>
|
|
2972
|
+
`;
|
|
2973
|
+
}
|
|
2974
|
+
function attvaluesElement(attrs, row, plan, indent) {
|
|
2975
|
+
let out = "";
|
|
2976
|
+
for (const spec of attrs) {
|
|
2977
|
+
const { column } = spec;
|
|
2978
|
+
if (column !== null && column.isSet(row)) {
|
|
2979
|
+
let text;
|
|
2980
|
+
if (spec.companion !== null && spec.companion.isSet(row)) {
|
|
2981
|
+
text = spec.companion.value(row);
|
|
2982
|
+
} else {
|
|
2983
|
+
text = spec.format(cellValue(column, row));
|
|
2984
|
+
}
|
|
2985
|
+
if (text !== null) {
|
|
2986
|
+
out += `${indent} <attvalue for="${escapeXmlAttribute(spec.id)}" value="${escapeXmlAttribute(text)}"/>
|
|
2987
|
+
`;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
const { table } = spec;
|
|
2991
|
+
if (table === null) {
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
const rows = table.rows.get(row);
|
|
2995
|
+
if (rows === void 0) {
|
|
2996
|
+
continue;
|
|
2997
|
+
}
|
|
2998
|
+
for (const r of rows) {
|
|
2999
|
+
if (!table.value.isSet(r)) {
|
|
3000
|
+
continue;
|
|
3001
|
+
}
|
|
3002
|
+
let text;
|
|
3003
|
+
if (table.valueText !== null && table.valueText.isSet(r)) {
|
|
3004
|
+
text = table.valueText.value(r);
|
|
3005
|
+
} else {
|
|
3006
|
+
text = table.format(cellValue(table.value, r));
|
|
3007
|
+
}
|
|
3008
|
+
if (text === null) {
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
out += `${indent} <attvalue for="${escapeXmlAttribute(spec.id)}" value="${escapeXmlAttribute(text)}"${timedAttrs(table, r, plan)}/>
|
|
3012
|
+
`;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
return out.length === 0 ? "" : `${indent}<attvalues>
|
|
3016
|
+
${out}${indent}</attvalues>
|
|
3017
|
+
`;
|
|
3018
|
+
}
|
|
3019
|
+
function timedAttrs(table, r, plan) {
|
|
3020
|
+
const start = table.start.isSet(r) ? table.start.value(r) : -Infinity;
|
|
3021
|
+
const end = table.end.isSet(r) ? table.end.value(r) : Infinity;
|
|
3022
|
+
let out = "";
|
|
3023
|
+
if (plan.version === "1.3" && plan.timeRepresentation === "timestamp" && start === end && Number.isFinite(start)) {
|
|
3024
|
+
return ` timestamp="${escapeXmlAttribute(timeText(table.start, table.startText, r, plan.timeFormat))}"`;
|
|
3025
|
+
}
|
|
3026
|
+
let bits = 0;
|
|
3027
|
+
if (plan.version === "1.2" && table.open !== null && table.open.isSet(r)) {
|
|
3028
|
+
bits = table.open.value(r);
|
|
3029
|
+
}
|
|
3030
|
+
if (Number.isFinite(start)) {
|
|
3031
|
+
const name = (bits & OPEN_START) !== 0 ? "startopen" : "start";
|
|
3032
|
+
out += ` ${name}="${escapeXmlAttribute(timeText(table.start, table.startText, r, plan.timeFormat))}"`;
|
|
3033
|
+
}
|
|
3034
|
+
if (Number.isFinite(end)) {
|
|
3035
|
+
const name = (bits & OPEN_END) !== 0 ? "endopen" : "end";
|
|
3036
|
+
out += ` ${name}="${escapeXmlAttribute(timeText(table.end, table.endText, r, plan.timeFormat))}"`;
|
|
3037
|
+
}
|
|
3038
|
+
return out;
|
|
3039
|
+
}
|
|
3040
|
+
function cellValue(column, row) {
|
|
3041
|
+
switch (column.dtype) {
|
|
3042
|
+
case "list":
|
|
3043
|
+
return column.sliceOf(row);
|
|
3044
|
+
case "json":
|
|
3045
|
+
return column.values[row];
|
|
3046
|
+
default: {
|
|
3047
|
+
const value = column.value(row);
|
|
3048
|
+
return ArrayBuffer.isView(value) ? Array.from(value) : value;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
function positionWritesZ(position) {
|
|
3053
|
+
if (position === null || position.meta.components < 3) {
|
|
3054
|
+
return false;
|
|
3055
|
+
}
|
|
3056
|
+
if (position.meta.extra.sourceDims !== 2) {
|
|
3057
|
+
return true;
|
|
3058
|
+
}
|
|
3059
|
+
const { components } = position.meta;
|
|
3060
|
+
let data;
|
|
3061
|
+
switch (position.dtype) {
|
|
3062
|
+
case "f32":
|
|
3063
|
+
case "f64":
|
|
3064
|
+
case "i32":
|
|
3065
|
+
case "u32":
|
|
3066
|
+
case "u8":
|
|
3067
|
+
({ data } = position);
|
|
3068
|
+
break;
|
|
3069
|
+
default:
|
|
3070
|
+
return true;
|
|
3071
|
+
}
|
|
3072
|
+
for (let i = 0; i < position.length; i++) {
|
|
3073
|
+
if (position.isSet(i) && data[i * components + 2] !== 0) {
|
|
3074
|
+
return true;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
return false;
|
|
3078
|
+
}
|
|
3079
|
+
function listItems(column, row) {
|
|
3080
|
+
return column.dtype === "list" ? column.sliceOf(row) : [];
|
|
3081
|
+
}
|
|
3082
|
+
function vizElements(roles, row, indent, writeZ) {
|
|
3083
|
+
let out = "";
|
|
3084
|
+
const { color } = roles;
|
|
3085
|
+
if (color !== null && color.isSet(row)) {
|
|
3086
|
+
const lanes = Array.from(color.value(row));
|
|
3087
|
+
const scale = color.dtype === "u8" ? 1 : 255;
|
|
3088
|
+
const channel = (v) => String(Math.max(0, Math.min(255, Math.round(v * scale))));
|
|
3089
|
+
let attrs = ` r="${channel(lanes[0])}" g="${channel(lanes[1])}" b="${channel(lanes[2])}"`;
|
|
3090
|
+
if (lanes.length > 3) {
|
|
3091
|
+
const a = color.dtype === "u8" ? lanes[3] / 255 : lanes[3];
|
|
3092
|
+
if (a !== 1) {
|
|
3093
|
+
attrs += ` a="${formatF32(Math.fround(a))}"`;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
out += `${indent}<viz:color${attrs}/>
|
|
3097
|
+
`;
|
|
3098
|
+
}
|
|
3099
|
+
const { position } = roles;
|
|
3100
|
+
if (position !== null && position.isSet(row)) {
|
|
3101
|
+
const lanes = Array.from(position.value(row));
|
|
3102
|
+
const fmt = position.dtype === "f32" ? formatF32 : formatF64;
|
|
3103
|
+
let attrs = ` x="${fmt(lanes[0])}" y="${fmt(lanes[1])}"`;
|
|
3104
|
+
if (lanes.length > 2 && writeZ) {
|
|
3105
|
+
attrs += ` z="${fmt(lanes[2])}"`;
|
|
3106
|
+
}
|
|
3107
|
+
out += `${indent}<viz:position${attrs}/>
|
|
3108
|
+
`;
|
|
3109
|
+
}
|
|
3110
|
+
for (const [name, column] of [
|
|
3111
|
+
["size", roles.size],
|
|
3112
|
+
["thickness", roles.thickness]
|
|
3113
|
+
]) {
|
|
3114
|
+
if (column !== null && column.isSet(row)) {
|
|
3115
|
+
const value = column.value(row);
|
|
3116
|
+
const text = column.dtype === "f32" ? formatF32(value) : formatF64(value);
|
|
3117
|
+
out += `${indent}<viz:${name} value="${escapeXmlAttribute(text)}"/>
|
|
3118
|
+
`;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
const { shape } = roles;
|
|
3122
|
+
if (shape !== null && shape.isSet(row)) {
|
|
3123
|
+
let attrs = ` value="${escapeXmlAttribute(String(shape.value(row)))}"`;
|
|
3124
|
+
if (roles.shapeUri !== null && roles.shapeUri.isSet(row)) {
|
|
3125
|
+
attrs += ` uri="${escapeXmlAttribute(roles.shapeUri.value(row))}"`;
|
|
3126
|
+
}
|
|
3127
|
+
out += `${indent}<viz:shape${attrs}/>
|
|
3128
|
+
`;
|
|
3129
|
+
}
|
|
3130
|
+
return out;
|
|
3131
|
+
}
|
|
3132
|
+
function attributesGroups(cls, specs, indent) {
|
|
3133
|
+
let out = "";
|
|
3134
|
+
for (const mode of ["static", "dynamic"]) {
|
|
3135
|
+
const group = specs.filter((s) => s.dynamic === (mode === "dynamic"));
|
|
3136
|
+
if (group.length === 0) {
|
|
3137
|
+
continue;
|
|
3138
|
+
}
|
|
3139
|
+
out += `${indent}<attributes class="${cls}" mode="${mode}">
|
|
3140
|
+
`;
|
|
3141
|
+
for (const spec of group) {
|
|
3142
|
+
const head = `${indent} <attribute id="${escapeXmlAttribute(spec.id)}" title="${escapeXmlAttribute(spec.title)}" type="${spec.type}"`;
|
|
3143
|
+
if (spec.defaultText === null && spec.optionsText === null) {
|
|
3144
|
+
out += `${head}/>
|
|
3145
|
+
`;
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
out += `${head}>
|
|
3149
|
+
`;
|
|
3150
|
+
if (spec.defaultText !== null) {
|
|
3151
|
+
out += `${indent} <default>${escapeXmlText(spec.defaultText)}</default>
|
|
3152
|
+
`;
|
|
3153
|
+
}
|
|
3154
|
+
if (spec.optionsText !== null) {
|
|
3155
|
+
out += `${indent} <options>${escapeXmlText(spec.optionsText)}</options>
|
|
3156
|
+
`;
|
|
3157
|
+
}
|
|
3158
|
+
out += `${indent} </attribute>
|
|
3159
|
+
`;
|
|
3160
|
+
}
|
|
3161
|
+
out += `${indent}</attributes>
|
|
3162
|
+
`;
|
|
3163
|
+
}
|
|
3164
|
+
return out;
|
|
3165
|
+
}
|
|
3166
|
+
function edgeType(snapshot, e, folding) {
|
|
3167
|
+
if (!snapshot.directed) {
|
|
3168
|
+
return "undirected";
|
|
3169
|
+
}
|
|
3170
|
+
if (folding.folded(e)) {
|
|
3171
|
+
return null;
|
|
3172
|
+
}
|
|
3173
|
+
if (!folding.sourceDirected(e)) {
|
|
3174
|
+
return "undirected";
|
|
3175
|
+
}
|
|
3176
|
+
if (folding.mateOf(e) !== INVALID_INDEX || folding.isMutual(e)) {
|
|
3177
|
+
return "mutual";
|
|
3178
|
+
}
|
|
3179
|
+
return "directed";
|
|
3180
|
+
}
|
|
3181
|
+
function* writeGexf(snapshot, options) {
|
|
3182
|
+
const plan = planExport(snapshot, options);
|
|
3183
|
+
const { version } = plan;
|
|
3184
|
+
const ids = sanitizeIds(snapshot, "any", plan.options.sanitizeIds);
|
|
3185
|
+
const idText = (index) => escapeXmlAttribute(String(ids.idAt(index)));
|
|
3186
|
+
const ns = GEXF_NAMESPACES[version];
|
|
3187
|
+
const { meta } = snapshot;
|
|
3188
|
+
yield '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
3189
|
+
yield `<gexf xmlns="${ns.gexf}" xmlns:viz="${ns.viz}" version="${version}">
|
|
3190
|
+
`;
|
|
3191
|
+
yield metaElement(meta);
|
|
3192
|
+
yield graphStart(snapshot, plan);
|
|
3193
|
+
yield attributesGroups("node", plan.nodeAttrs, " ");
|
|
3194
|
+
yield attributesGroups("edge", plan.edgeAttrs, " ");
|
|
3195
|
+
const { nodeRoles } = plan;
|
|
3196
|
+
const writeZ = positionWritesZ(nodeRoles.position);
|
|
3197
|
+
yield ` <nodes count="${snapshot.nodeCount}">
|
|
3198
|
+
`;
|
|
3199
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
3200
|
+
let attrs = ` id="${idText(i)}"`;
|
|
3201
|
+
if (nodeRoles.label !== null && nodeRoles.label.isSet(i)) {
|
|
3202
|
+
attrs += ` label="${escapeXmlAttribute(String(nodeRoles.label.value(i)))}"`;
|
|
3203
|
+
}
|
|
3204
|
+
if (nodeRoles.parent !== null && nodeRoles.parent.isSet(i)) {
|
|
3205
|
+
attrs += ` pid="${idText(nodeRoles.parent.value(i))}"`;
|
|
3206
|
+
}
|
|
3207
|
+
attrs += lifetimeAttrs(nodeRoles, i, plan);
|
|
3208
|
+
let body = attvaluesElement(plan.nodeAttrs, i, plan, " ");
|
|
3209
|
+
if (nodeRoles.parents !== null && nodeRoles.parents.isSet(i)) {
|
|
3210
|
+
const parents = listItems(nodeRoles.parents, i);
|
|
3211
|
+
if (parents.length > 0) {
|
|
3212
|
+
body += " <parents>\n";
|
|
3213
|
+
for (const p of parents) {
|
|
3214
|
+
body += ` <parent for="${idText(p)}"/>
|
|
3215
|
+
`;
|
|
3216
|
+
}
|
|
3217
|
+
body += " </parents>\n";
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
body += spellsElement(nodeRoles, i, plan, " ");
|
|
3221
|
+
body += vizElements(nodeRoles, i, " ", writeZ);
|
|
3222
|
+
yield body.length === 0 ? ` <node${attrs}/>
|
|
3223
|
+
` : ` <node${attrs}>
|
|
3224
|
+
${body} </node>
|
|
3225
|
+
`;
|
|
3226
|
+
}
|
|
3227
|
+
yield " </nodes>\n";
|
|
3228
|
+
const { edgeRoles } = plan;
|
|
3229
|
+
const edgeList = snapshot.edgeList();
|
|
3230
|
+
const folding = pairFolding(snapshot, { foldMutual: true });
|
|
3231
|
+
const weights = explicitWeights(snapshot);
|
|
3232
|
+
const defaultType = snapshot.directed ? "directed" : "undirected";
|
|
3233
|
+
let written = 0;
|
|
3234
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
3235
|
+
if (edgeType(snapshot, e, folding) !== null) {
|
|
3236
|
+
written++;
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
yield ` <edges count="${written}">
|
|
3240
|
+
`;
|
|
3241
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
3242
|
+
const type = edgeType(snapshot, e, folding);
|
|
3243
|
+
if (type === null) {
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3246
|
+
let attrs = "";
|
|
3247
|
+
if (edgeRoles.id !== null && edgeRoles.id.isSet(e)) {
|
|
3248
|
+
attrs += ` id="${escapeXmlAttribute(String(edgeRoles.id.value(e)))}"`;
|
|
3249
|
+
} else if (version === "1.2") {
|
|
3250
|
+
attrs += ` id="e${e}"`;
|
|
3251
|
+
}
|
|
3252
|
+
attrs += ` source="${idText(edgeList.src[e])}" target="${idText(edgeList.dst[e])}"`;
|
|
3253
|
+
if (type !== defaultType) {
|
|
3254
|
+
attrs += ` type="${type}"`;
|
|
3255
|
+
}
|
|
3256
|
+
const weight = weights.text(e);
|
|
3257
|
+
if (weight !== null) {
|
|
3258
|
+
attrs += ` weight="${weight}"`;
|
|
3259
|
+
}
|
|
3260
|
+
if (edgeRoles.label !== null && edgeRoles.label.isSet(e)) {
|
|
3261
|
+
attrs += ` label="${escapeXmlAttribute(String(edgeRoles.label.value(e)))}"`;
|
|
3262
|
+
}
|
|
3263
|
+
if (version === "1.3" && edgeRoles.kind !== null && edgeRoles.kind.isSet(e)) {
|
|
3264
|
+
attrs += ` kind="${escapeXmlAttribute(String(edgeRoles.kind.value(e)))}"`;
|
|
3265
|
+
}
|
|
3266
|
+
attrs += lifetimeAttrs(edgeRoles, e, plan);
|
|
3267
|
+
let body = attvaluesElement(plan.edgeAttrs, e, plan, " ");
|
|
3268
|
+
body += spellsElement(edgeRoles, e, plan, " ");
|
|
3269
|
+
body += vizElements(edgeRoles, e, " ", false);
|
|
3270
|
+
yield body.length === 0 ? ` <edge${attrs}/>
|
|
3271
|
+
` : ` <edge${attrs}>
|
|
3272
|
+
${body} </edge>
|
|
3273
|
+
`;
|
|
3274
|
+
}
|
|
3275
|
+
yield " </edges>\n";
|
|
3276
|
+
yield " </graph>\n";
|
|
3277
|
+
yield "</gexf>\n";
|
|
3278
|
+
}
|
|
3279
|
+
function metaElement(meta) {
|
|
3280
|
+
let body = "";
|
|
3281
|
+
if (meta.creator !== null) {
|
|
3282
|
+
body += ` <creator>${escapeXmlText(meta.creator)}</creator>
|
|
3283
|
+
`;
|
|
3284
|
+
}
|
|
3285
|
+
if (meta.description !== null) {
|
|
3286
|
+
body += ` <description>${escapeXmlText(meta.description)}</description>
|
|
3287
|
+
`;
|
|
3288
|
+
}
|
|
3289
|
+
if (meta.keywords.length > 0) {
|
|
3290
|
+
body += ` <keywords>${escapeXmlText(meta.keywords.join(", "))}</keywords>
|
|
3291
|
+
`;
|
|
3292
|
+
}
|
|
3293
|
+
const modified = meta.modified === null ? "" : ` lastmodifieddate="${escapeXmlAttribute(meta.modified)}"`;
|
|
3294
|
+
if (body.length === 0 && modified.length === 0) {
|
|
3295
|
+
return "";
|
|
3296
|
+
}
|
|
3297
|
+
return body.length === 0 ? ` <meta${modified}/>
|
|
3298
|
+
` : ` <meta${modified}>
|
|
3299
|
+
${body} </meta>
|
|
3300
|
+
`;
|
|
3301
|
+
}
|
|
3302
|
+
function graphStart(snapshot, plan) {
|
|
3303
|
+
const { meta } = snapshot;
|
|
3304
|
+
let attrs = ` defaultedgetype="${snapshot.directed ? "directed" : "undirected"}"`;
|
|
3305
|
+
let mode = meta.mode ?? "static";
|
|
3306
|
+
if (plan.temporal && mode === "static") {
|
|
3307
|
+
mode = "dynamic";
|
|
3308
|
+
}
|
|
3309
|
+
if (plan.version === "1.2" && mode === "slice") {
|
|
3310
|
+
mode = "dynamic";
|
|
3311
|
+
}
|
|
3312
|
+
attrs += ` mode="${mode}"`;
|
|
3313
|
+
let { idType } = meta;
|
|
3314
|
+
if (idType === null) {
|
|
3315
|
+
const { kind } = snapshot.ids;
|
|
3316
|
+
if (kind === "string") {
|
|
3317
|
+
idType = "string";
|
|
3318
|
+
} else if (kind !== "mixed") {
|
|
3319
|
+
idType = "integer";
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
if (idType === "integer" || idType === "string") {
|
|
3323
|
+
attrs += ` idtype="${idType}"`;
|
|
3324
|
+
}
|
|
3325
|
+
if (plan.timeFormat !== null) {
|
|
3326
|
+
attrs += ` timeformat="${plan.timeFormat}"`;
|
|
3327
|
+
} else if (plan.temporal) {
|
|
3328
|
+
attrs += ' timeformat="double"';
|
|
3329
|
+
}
|
|
3330
|
+
if (plan.version === "1.3" && plan.timeRepresentation !== null) {
|
|
3331
|
+
attrs += ` timerepresentation="${plan.timeRepresentation}"`;
|
|
3332
|
+
}
|
|
3333
|
+
const gexfExtra = meta.extra.gexf;
|
|
3334
|
+
if (typeof gexfExtra === "object" && gexfExtra !== null) {
|
|
3335
|
+
for (const key of ["start", "end", "timestamp"]) {
|
|
3336
|
+
const value = gexfExtra[key];
|
|
3337
|
+
if (typeof value === "string") {
|
|
3338
|
+
attrs += ` ${key}="${escapeXmlAttribute(value)}"`;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
return ` <graph${attrs}>
|
|
3343
|
+
`;
|
|
3344
|
+
}
|
|
3345
|
+
const gexfExporter = Object.freeze({
|
|
3346
|
+
format: GEXF_FORMAT,
|
|
3347
|
+
capabilities: CAPABILITIES_1_3,
|
|
3348
|
+
/**
|
|
3349
|
+
* Pre-flight: what export() would lose.
|
|
3350
|
+
* @param snapshot - the snapshot
|
|
3351
|
+
* @param options - format-specific and common options
|
|
3352
|
+
* @returns the loss notes, empty when the export is exact
|
|
3353
|
+
*/
|
|
3354
|
+
check(snapshot, options) {
|
|
3355
|
+
return Object.freeze(planExport(snapshot, options).notes);
|
|
3356
|
+
},
|
|
3357
|
+
/**
|
|
3358
|
+
* Write the snapshot as UTF-8 chunks.
|
|
3359
|
+
* @param snapshot - the snapshot
|
|
3360
|
+
* @param options - format-specific and common options
|
|
3361
|
+
* @returns the chunks
|
|
3362
|
+
*/
|
|
3363
|
+
export(snapshot, options) {
|
|
3364
|
+
return encodeChunks(writeGexf(snapshot, options));
|
|
3365
|
+
},
|
|
3366
|
+
/**
|
|
3367
|
+
* Write the snapshot as one string.
|
|
3368
|
+
* @param snapshot - the snapshot
|
|
3369
|
+
* @param options - format-specific and common options
|
|
3370
|
+
* @returns the document
|
|
3371
|
+
*/
|
|
3372
|
+
async exportToString(snapshot, options) {
|
|
3373
|
+
return joinText(writeGexf(snapshot, options));
|
|
3374
|
+
}
|
|
3375
|
+
});
|
|
3376
|
+
const GEXF_1_2_CAPABILITIES = CAPABILITIES_1_2;
|
|
3377
|
+
const GEXF_ISSUE = Object.freeze({
|
|
3378
|
+
/** The XML is not well-formed (fatal). */
|
|
3379
|
+
XML_SYNTAX: XML_SYNTAX_CODE,
|
|
3380
|
+
/** The input holds invalid UTF-8 (fatal). */
|
|
3381
|
+
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
3382
|
+
/** The root element is not `<gexf>` (fatal). */
|
|
3383
|
+
NOT_GEXF: NOT_GEXF_CODE,
|
|
3384
|
+
/** The document has no `<graph>` (fatal). */
|
|
3385
|
+
NO_GRAPH: NO_GRAPH_CODE,
|
|
3386
|
+
/** `<edges>` without `<nodes>`. */
|
|
3387
|
+
MISSING_NODES: MISSING_NODES_CODE,
|
|
3388
|
+
/** A node without an id. */
|
|
3389
|
+
MISSING_ID: MISSING_ID_CODE,
|
|
3390
|
+
/** An edge without a source or target. */
|
|
3391
|
+
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
3392
|
+
/** An edge `type` outside directed / undirected / mutual. */
|
|
3393
|
+
EDGE_TYPE: EDGE_TYPE_CODE,
|
|
3394
|
+
/** A `pid` / `<parent for>` naming an unknown node. */
|
|
3395
|
+
UNKNOWN_PARENT: UNKNOWN_PARENT_CODE,
|
|
3396
|
+
/** An `<attributes class>` outside node / edge. */
|
|
3397
|
+
ATTRIBUTES_CLASS: ATTRIBUTES_CLASS_CODE,
|
|
3398
|
+
/** An `<attribute>` without an id. */
|
|
3399
|
+
ATTRIBUTE_ID: ATTRIBUTE_ID_CODE,
|
|
3400
|
+
/** A `<graph>` header attribute with an unknown value. */
|
|
3401
|
+
HEADER_VALUE: HEADER_VALUE_CODE,
|
|
3402
|
+
/** A `count` hint the sink cannot reserve (ignored). */
|
|
3403
|
+
COUNT_HINT: COUNT_HINT_CODE,
|
|
3404
|
+
/** A node id declared twice. */
|
|
3405
|
+
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
3406
|
+
/** An edge id declared twice (the second edge is skipped). */
|
|
3407
|
+
DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
|
|
3408
|
+
/** An attribute id declared twice in one class. */
|
|
3409
|
+
DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
|
|
3410
|
+
/** An attribute type the importer maps to string. */
|
|
3411
|
+
ATTRIBUTE_TYPE: ATTRIBUTE_TYPE_CODE,
|
|
3412
|
+
/** A declared type the format does not define (kept as string). */
|
|
3413
|
+
UNKNOWN_ATTR_TYPE: UNKNOWN_ATTR_TYPE_CODE,
|
|
3414
|
+
/** A default that does not parse as the declared type. */
|
|
3415
|
+
BAD_DEFAULT: BAD_DEFAULT_CODE,
|
|
3416
|
+
/** Options that do not parse as the declared type. */
|
|
3417
|
+
BAD_OPTIONS: BAD_OPTIONS_CODE,
|
|
3418
|
+
/** A column renamed `<name>#<id>` because the name was taken (design section 5.6). */
|
|
3419
|
+
COLUMN_RENAMED: COLUMN_RENAMED_CODE,
|
|
3420
|
+
/** A column declared without its role because the table already holds it. */
|
|
3421
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
3422
|
+
/** An `<attvalue for>` naming an undeclared attribute. */
|
|
3423
|
+
UNKNOWN_ATTRIBUTE: UNKNOWN_ATTRIBUTE_CODE,
|
|
3424
|
+
/** An `<attvalue>` without a value. */
|
|
3425
|
+
ATTVALUE_SHAPE: ATTVALUE_SHAPE_CODE,
|
|
3426
|
+
/** A timed value on an attribute of a static group. */
|
|
3427
|
+
TIMED_VALUE_ON_STATIC: TIMED_STATIC_CODE,
|
|
3428
|
+
/** A long value beyond 2^53 rounded. */
|
|
3429
|
+
PRECISION: PRECISION_CODE,
|
|
3430
|
+
/** Two id texts merged into one number under ids "number". */
|
|
3431
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
3432
|
+
/** The XML weight attribute ignored under weightFrom null. */
|
|
3433
|
+
WEIGHT_IGNORED: WEIGHT_IGNORED_CODE,
|
|
3434
|
+
/** viz elements skipped under viz: false. */
|
|
3435
|
+
VIZ_SKIPPED: VIZ_SKIPPED_CODE,
|
|
3436
|
+
/** A 1.2 dynamic viz element whose bounds were dropped. */
|
|
3437
|
+
VIZ_DYNAMIC_DROPPED: VIZ_DYNAMIC_CODE,
|
|
3438
|
+
/** A 1.2 startopen / endopen spell stored closed. */
|
|
3439
|
+
SPELL_OPEN_DROPPED: SPELL_OPEN_CODE,
|
|
3440
|
+
/** A viz value that could not be read. */
|
|
3441
|
+
VIZ_VALUE: VIZ_VALUE_CODE,
|
|
3442
|
+
/** Both `start` and `startopen` (or `end` and `endopen`) on one element. */
|
|
3443
|
+
OPEN_BOUND_CONFLICT: OPEN_BOUND_CONFLICT_CODE,
|
|
3444
|
+
/** An element or attribute GEXF does not define was skipped. */
|
|
3445
|
+
UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
|
|
3446
|
+
/** Text where GEXF allows only elements was ignored. */
|
|
3447
|
+
STRAY_TEXT: STRAY_TEXT_CODE,
|
|
3448
|
+
/** A common option the importer has no use for was given. */
|
|
3449
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
3450
|
+
/** A builder-policy option the sink does not honour. */
|
|
3451
|
+
SINK_OPTION: SINK_OPTION_CODE,
|
|
3452
|
+
/** The sink refused the file's direction. */
|
|
3453
|
+
DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
|
|
3454
|
+
/** Edges forced to the policy's direction. */
|
|
3455
|
+
DIRECTION_FORCED: DIRECTION_FORCED_CODE,
|
|
3456
|
+
/** A mixed file under onMixedDirection "error" (fatal). */
|
|
3457
|
+
MIXED_DIRECTION: MIXED_DIRECTION_CODE
|
|
3458
|
+
});
|
|
3459
|
+
export {
|
|
3460
|
+
GEXF_1_2_CAPABILITIES,
|
|
3461
|
+
GEXF_ISSUE,
|
|
3462
|
+
GEXF_LOSS,
|
|
3463
|
+
gexfExporter,
|
|
3464
|
+
gexfImporter
|
|
3465
|
+
};
|
|
3466
|
+
//# sourceMappingURL=gexf.js.map
|