@graphty/graph-io 0.0.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -28
- package/dist/chunks/children-CL3Cy0ez.js +238 -0
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
- package/dist/chunks/escape-DyI8JofU.js +938 -0
- package/dist/chunks/escape-DyI8JofU.js.map +1 -0
- package/dist/chunks/importer-CQnJuWJw.js +2987 -0
- package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
- package/dist/chunks/importer-CpCpfbxr.js +2015 -0
- package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
- package/dist/chunks/importer-DbnGYr3_.js +2342 -0
- package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
- package/dist/chunks/importer-GozH8DkN.js +3050 -0
- package/dist/chunks/importer-GozH8DkN.js.map +1 -0
- package/dist/chunks/records-CGpxszm1.js +605 -0
- package/dist/chunks/records-CGpxszm1.js.map +1 -0
- package/dist/chunks/text-CajMdVFy.js +189 -0
- package/dist/chunks/text-CajMdVFy.js.map +1 -0
- package/dist/chunks/writer-DxSKC7TL.js +2842 -0
- package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1702 -0
- package/dist/csv.js.map +1 -0
- package/dist/dot.d.ts +1 -0
- package/dist/dot.js +8 -0
- package/dist/dot.js.map +1 -0
- package/dist/gexf.d.ts +1 -0
- package/dist/gexf.js +3466 -0
- package/dist/gexf.js.map +1 -0
- package/dist/gml.d.ts +1 -0
- package/dist/gml.js +2647 -0
- package/dist/gml.js.map +1 -0
- package/dist/graph-io.d.ts +1 -0
- package/dist/graph-io.js +790 -0
- package/dist/graph-io.js.map +1 -0
- package/dist/graphml.d.ts +1 -0
- package/dist/graphml.js +8 -0
- package/dist/graphml.js.map +1 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +11 -0
- package/dist/json.js.map +1 -0
- package/dist/neo4j.d.ts +1 -0
- package/dist/neo4j.js +2046 -0
- package/dist/neo4j.js.map +1 -0
- package/dist/pajek.d.ts +1 -0
- package/dist/pajek.js +8 -0
- package/dist/pajek.js.map +1 -0
- package/dist/src/children.d.ts +134 -0
- package/dist/src/children.d.ts.map +1 -0
- package/dist/src/children.js +274 -0
- package/dist/src/children.js.map +1 -0
- package/dist/src/common/attributes.d.ts +229 -0
- package/dist/src/common/attributes.d.ts.map +1 -0
- package/dist/src/common/attributes.js +368 -0
- package/dist/src/common/attributes.js.map +1 -0
- package/dist/src/common/codes.d.ts +105 -0
- package/dist/src/common/codes.d.ts.map +1 -0
- package/dist/src/common/codes.js +107 -0
- package/dist/src/common/codes.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +84 -0
- package/dist/src/common/declared-types.d.ts.map +1 -0
- package/dist/src/common/declared-types.js +326 -0
- package/dist/src/common/declared-types.js.map +1 -0
- package/dist/src/common/direction.d.ts +206 -0
- package/dist/src/common/direction.d.ts.map +1 -0
- package/dist/src/common/direction.js +370 -0
- package/dist/src/common/direction.js.map +1 -0
- package/dist/src/common/escape.d.ts +92 -0
- package/dist/src/common/escape.d.ts.map +1 -0
- package/dist/src/common/escape.js +212 -0
- package/dist/src/common/escape.js.map +1 -0
- package/dist/src/common/export.d.ts +249 -0
- package/dist/src/common/export.d.ts.map +1 -0
- package/dist/src/common/export.js +594 -0
- package/dist/src/common/export.js.map +1 -0
- package/dist/src/common/format.d.ts +59 -0
- package/dist/src/common/format.d.ts.map +1 -0
- package/dist/src/common/format.js +106 -0
- package/dist/src/common/format.js.map +1 -0
- package/dist/src/common/ids.d.ts +83 -0
- package/dist/src/common/ids.d.ts.map +1 -0
- package/dist/src/common/ids.js +158 -0
- package/dist/src/common/ids.js.map +1 -0
- package/dist/src/common/input.d.ts +100 -0
- package/dist/src/common/input.d.ts.map +1 -0
- package/dist/src/common/input.js +335 -0
- package/dist/src/common/input.js.map +1 -0
- package/dist/src/common/lists.d.ts +34 -0
- package/dist/src/common/lists.d.ts.map +1 -0
- package/dist/src/common/lists.js +185 -0
- package/dist/src/common/lists.js.map +1 -0
- package/dist/src/common/options.d.ts +108 -0
- package/dist/src/common/options.d.ts.map +1 -0
- package/dist/src/common/options.js +265 -0
- package/dist/src/common/options.js.map +1 -0
- package/dist/src/common/report.d.ts +187 -0
- package/dist/src/common/report.d.ts.map +1 -0
- package/dist/src/common/report.js +274 -0
- package/dist/src/common/report.js.map +1 -0
- package/dist/src/common/temporal.d.ts +71 -0
- package/dist/src/common/temporal.d.ts.map +1 -0
- package/dist/src/common/temporal.js +266 -0
- package/dist/src/common/temporal.js.map +1 -0
- package/dist/src/common/text.d.ts +104 -0
- package/dist/src/common/text.d.ts.map +1 -0
- package/dist/src/common/text.js +255 -0
- package/dist/src/common/text.js.map +1 -0
- package/dist/src/common/weights.d.ts +77 -0
- package/dist/src/common/weights.d.ts.map +1 -0
- package/dist/src/common/weights.js +156 -0
- package/dist/src/common/weights.js.map +1 -0
- package/dist/src/common/writer.d.ts +51 -0
- package/dist/src/common/writer.d.ts.map +1 -0
- package/dist/src/common/writer.js +108 -0
- package/dist/src/common/writer.js.map +1 -0
- package/dist/src/common/xml.d.ts +245 -0
- package/dist/src/common/xml.d.ts.map +1 -0
- package/dist/src/common/xml.js +942 -0
- package/dist/src/common/xml.js.map +1 -0
- package/dist/src/formats/csv/exporter.d.ts +70 -0
- package/dist/src/formats/csv/exporter.d.ts.map +1 -0
- package/dist/src/formats/csv/exporter.js +682 -0
- package/dist/src/formats/csv/exporter.js.map +1 -0
- package/dist/src/formats/csv/header.d.ts +66 -0
- package/dist/src/formats/csv/header.d.ts.map +1 -0
- package/dist/src/formats/csv/header.js +152 -0
- package/dist/src/formats/csv/header.js.map +1 -0
- package/dist/src/formats/csv/importer.d.ts +82 -0
- package/dist/src/formats/csv/importer.d.ts.map +1 -0
- package/dist/src/formats/csv/importer.js +849 -0
- package/dist/src/formats/csv/importer.js.map +1 -0
- package/dist/src/formats/csv/index.d.ts +60 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -0
- package/dist/src/formats/csv/index.js +63 -0
- package/dist/src/formats/csv/index.js.map +1 -0
- package/dist/src/formats/csv/records.d.ts +188 -0
- package/dist/src/formats/csv/records.d.ts.map +1 -0
- package/dist/src/formats/csv/records.js +702 -0
- package/dist/src/formats/csv/records.js.map +1 -0
- package/dist/src/formats/csv/values.d.ts +105 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -0
- package/dist/src/formats/csv/values.js +192 -0
- package/dist/src/formats/csv/values.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +52 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -0
- package/dist/src/formats/dot/exporter.js +836 -0
- package/dist/src/formats/dot/exporter.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts +102 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -0
- package/dist/src/formats/dot/importer.js +1291 -0
- package/dist/src/formats/dot/importer.js.map +1 -0
- package/dist/src/formats/dot/index.d.ts +7 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -0
- package/dist/src/formats/dot/index.js +7 -0
- package/dist/src/formats/dot/index.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +29 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -0
- package/dist/src/formats/dot/names.js +28 -0
- package/dist/src/formats/dot/names.js.map +1 -0
- package/dist/src/formats/dot/tokenizer.d.ts +114 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
- package/dist/src/formats/dot/tokenizer.js +341 -0
- package/dist/src/formats/dot/tokenizer.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts +56 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
- package/dist/src/formats/gexf/exporter.js +1395 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +73 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -0
- package/dist/src/formats/gexf/importer.js +1880 -0
- package/dist/src/formats/gexf/importer.js.map +1 -0
- package/dist/src/formats/gexf/index.d.ts +96 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -0
- package/dist/src/formats/gexf/index.js +97 -0
- package/dist/src/formats/gexf/index.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts +135 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -0
- package/dist/src/formats/gexf/schema.js +323 -0
- package/dist/src/formats/gexf/schema.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +69 -0
- package/dist/src/formats/gml/exporter.d.ts.map +1 -0
- package/dist/src/formats/gml/exporter.js +1093 -0
- package/dist/src/formats/gml/exporter.js.map +1 -0
- package/dist/src/formats/gml/importer.d.ts +66 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -0
- package/dist/src/formats/gml/importer.js +1331 -0
- package/dist/src/formats/gml/importer.js.map +1 -0
- package/dist/src/formats/gml/index.d.ts +85 -0
- package/dist/src/formats/gml/index.d.ts.map +1 -0
- package/dist/src/formats/gml/index.js +88 -0
- package/dist/src/formats/gml/index.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +186 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -0
- package/dist/src/formats/gml/syntax.js +467 -0
- package/dist/src/formats/gml/syntax.js.map +1 -0
- package/dist/src/formats/graphml/constants.d.ts +169 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -0
- package/dist/src/formats/graphml/constants.js +165 -0
- package/dist/src/formats/graphml/constants.js.map +1 -0
- package/dist/src/formats/graphml/exporter.d.ts +34 -0
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
- package/dist/src/formats/graphml/exporter.js +1176 -0
- package/dist/src/formats/graphml/exporter.js.map +1 -0
- package/dist/src/formats/graphml/importer.d.ts +31 -0
- package/dist/src/formats/graphml/importer.d.ts.map +1 -0
- package/dist/src/formats/graphml/importer.js +1607 -0
- package/dist/src/formats/graphml/importer.js.map +1 -0
- package/dist/src/formats/graphml/index.d.ts +8 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -0
- package/dist/src/formats/graphml/index.js +8 -0
- package/dist/src/formats/graphml/index.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts +72 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -0
- package/dist/src/formats/graphml/tree.js +290 -0
- package/dist/src/formats/graphml/tree.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +125 -0
- package/dist/src/formats/json/dialect.d.ts.map +1 -0
- package/dist/src/formats/json/dialect.js +262 -0
- package/dist/src/formats/json/dialect.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts +89 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -0
- package/dist/src/formats/json/exporter.js +1358 -0
- package/dist/src/formats/json/exporter.js.map +1 -0
- package/dist/src/formats/json/importer.d.ts +108 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -0
- package/dist/src/formats/json/importer.js +1838 -0
- package/dist/src/formats/json/importer.js.map +1 -0
- package/dist/src/formats/json/index.d.ts +8 -0
- package/dist/src/formats/json/index.d.ts.map +1 -0
- package/dist/src/formats/json/index.js +8 -0
- package/dist/src/formats/json/index.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts +68 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
- package/dist/src/formats/neo4j/exporter.js +1055 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -0
- package/dist/src/formats/neo4j/header.d.ts +52 -0
- package/dist/src/formats/neo4j/header.d.ts.map +1 -0
- package/dist/src/formats/neo4j/header.js +131 -0
- package/dist/src/formats/neo4j/header.js.map +1 -0
- package/dist/src/formats/neo4j/importer.d.ts +73 -0
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
- package/dist/src/formats/neo4j/importer.js +932 -0
- package/dist/src/formats/neo4j/importer.js.map +1 -0
- package/dist/src/formats/neo4j/index.d.ts +79 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -0
- package/dist/src/formats/neo4j/index.js +83 -0
- package/dist/src/formats/neo4j/index.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts +58 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
- package/dist/src/formats/pajek/exporter.js +825 -0
- package/dist/src/formats/pajek/exporter.js.map +1 -0
- package/dist/src/formats/pajek/importer.d.ts +88 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -0
- package/dist/src/formats/pajek/importer.js +1047 -0
- package/dist/src/formats/pajek/importer.js.map +1 -0
- package/dist/src/formats/pajek/index.d.ts +7 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -0
- package/dist/src/formats/pajek/index.js +7 -0
- package/dist/src/formats/pajek/index.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +112 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
- package/dist/src/formats/pajek/syntax.js +269 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +207 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +481 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/sniff.d.ts +104 -0
- package/dist/src/sniff.d.ts.map +1 -0
- package/dist/src/sniff.js +357 -0
- package/dist/src/sniff.js.map +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +29 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +122 -7
- package/src/children.ts +335 -0
- package/src/common/attributes.ts +520 -0
- package/src/common/codes.ts +153 -0
- package/src/common/declared-types.ts +374 -0
- package/src/common/direction.ts +518 -0
- package/src/common/escape.ts +231 -0
- package/src/common/export.ts +817 -0
- package/src/common/format.ts +111 -0
- package/src/common/ids.ts +176 -0
- package/src/common/input.ts +378 -0
- package/src/common/lists.ts +196 -0
- package/src/common/options.ts +377 -0
- package/src/common/report.ts +352 -0
- package/src/common/temporal.ts +302 -0
- package/src/common/text.ts +294 -0
- package/src/common/weights.ts +202 -0
- package/src/common/writer.ts +123 -0
- package/src/common/xml.ts +1053 -0
- package/src/formats/csv/exporter.ts +894 -0
- package/src/formats/csv/header.ts +172 -0
- package/src/formats/csv/importer.ts +1104 -0
- package/src/formats/csv/index.ts +88 -0
- package/src/formats/csv/records.ts +813 -0
- package/src/formats/csv/values.ts +224 -0
- package/src/formats/dot/exporter.ts +1014 -0
- package/src/formats/dot/importer.ts +1549 -0
- package/src/formats/dot/index.ts +7 -0
- package/src/formats/dot/names.ts +40 -0
- package/src/formats/dot/tokenizer.ts +384 -0
- package/src/formats/gexf/exporter.ts +1696 -0
- package/src/formats/gexf/importer.ts +2333 -0
- package/src/formats/gexf/index.ts +142 -0
- package/src/formats/gexf/schema.ts +361 -0
- package/src/formats/gml/exporter.ts +1404 -0
- package/src/formats/gml/importer.ts +1591 -0
- package/src/formats/gml/index.ts +128 -0
- package/src/formats/gml/syntax.ts +545 -0
- package/src/formats/graphml/constants.ts +225 -0
- package/src/formats/graphml/exporter.ts +1458 -0
- package/src/formats/graphml/importer.ts +2027 -0
- package/src/formats/graphml/index.ts +8 -0
- package/src/formats/graphml/tree.ts +318 -0
- package/src/formats/json/dialect.ts +317 -0
- package/src/formats/json/exporter.ts +1616 -0
- package/src/formats/json/importer.ts +2271 -0
- package/src/formats/json/index.ts +8 -0
- package/src/formats/neo4j/exporter.ts +1287 -0
- package/src/formats/neo4j/header.ts +156 -0
- package/src/formats/neo4j/importer.ts +1220 -0
- package/src/formats/neo4j/index.ts +116 -0
- package/src/formats/pajek/exporter.ts +1000 -0
- package/src/formats/pajek/importer.ts +1311 -0
- package/src/formats/pajek/index.ts +7 -0
- package/src/formats/pajek/syntax.ts +307 -0
- package/src/index.ts +244 -0
- package/src/registry.ts +617 -0
- package/src/sniff.ts +397 -0
- package/src/types.ts +262 -0
|
@@ -0,0 +1,2333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GEXF importer (design sections 8.4, 8.6, 5.1, 5.6, 5.10; research note 07 section 2.1):
|
|
3
|
+
* GEXF 1.1, 1.2 and 1.3 documents read in one SAX-style pass over the shared streaming XML
|
|
4
|
+
* tokenizer (src/common/xml.ts) and pushed into the sink one scalar at a time, so a document of
|
|
5
|
+
* any size is never held in memory. Declared attributes become typed columns as they are read
|
|
6
|
+
* (defaults, options, list types, temporal types with their text companions declared on the first
|
|
7
|
+
* value whose text is not canonical); the viz namespace becomes the position / color / size /
|
|
8
|
+
* shape / thickness role columns with origin.namespace "viz"; `pid`, nested `<nodes>` and
|
|
9
|
+
* `<parents>` become the parent / parents columns, resolved once every node is declared so a
|
|
10
|
+
* forward reference is legal; element lifetimes (`start` / `end` / `timestamp`, `<spells>`, 1.3
|
|
11
|
+
* `timestamps` / `intervals`, 1.2 `startopen` / `endopen` holding the open bound's time) become
|
|
12
|
+
* the temporal role columns; dynamic attribute values go into the temporal extension tables of
|
|
13
|
+
* design section 5.10; the graph header's `mode`, `timeformat`, `timerepresentation` and `idtype`
|
|
14
|
+
* land in the graph meta (`idtype` is informational: Gephi writes `idtype="string"` for every
|
|
15
|
+
* file, so ids follow the `ids` rule, canonical by default).
|
|
16
|
+
*
|
|
17
|
+
* Direction follows design section 8.4: `defaultedgetype` (undirected when absent, per the spec and
|
|
18
|
+
* the `defaultDirected` option) sets the sink's direction once, and every per-edge `type` override,
|
|
19
|
+
* `mutual` included, goes through the common DirectionResolver and the `onMixedDirection` policy.
|
|
20
|
+
* The weight is the `weight` XML attribute, overridden by a static value of the edge attribute
|
|
21
|
+
* titled `weightFrom` ("weight" by default), applied with `setEdgeWeight` when that value arrives;
|
|
22
|
+
* timed values of that attribute form the `temporal:edge:weight` table (design section 5.10). Ids
|
|
23
|
+
* are coerced with the "canonical" rule of design section 4.1 by default. Edges may not reference
|
|
24
|
+
* undeclared nodes unless `addMissingNodes` is set (the GEXF default is false, design section 8.4).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
type ColumnDecl,
|
|
29
|
+
type ColumnHandle,
|
|
30
|
+
type ExtensionHandle,
|
|
31
|
+
GraphFormatError,
|
|
32
|
+
type GraphMetaPatch,
|
|
33
|
+
type GraphSink,
|
|
34
|
+
INVALID_INDEX,
|
|
35
|
+
MAX_COUNT,
|
|
36
|
+
type NodeId,
|
|
37
|
+
} from "@graphty/graph-format";
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
type AttributeDeclarationInput,
|
|
41
|
+
declareAttribute,
|
|
42
|
+
declareCompanion,
|
|
43
|
+
type DeclaredAttribute,
|
|
44
|
+
declareResolved,
|
|
45
|
+
losesPrecision,
|
|
46
|
+
parseDeclaredTemporal,
|
|
47
|
+
parseDeclaredValue,
|
|
48
|
+
PRECISION_CODE,
|
|
49
|
+
} from "../../common/attributes.js";
|
|
50
|
+
import {
|
|
51
|
+
COUNT_HINT_CODE,
|
|
52
|
+
DUPLICATE_EDGE_ID_CODE,
|
|
53
|
+
DUPLICATE_NODE_CODE,
|
|
54
|
+
ID_MERGED_CODE,
|
|
55
|
+
MISSING_ENDPOINT_CODE,
|
|
56
|
+
MISSING_ID_CODE,
|
|
57
|
+
NO_GRAPH_CODE,
|
|
58
|
+
STRAY_TEXT_CODE,
|
|
59
|
+
UNKNOWN_ELEMENT_CODE,
|
|
60
|
+
UNKNOWN_PARENT_CODE,
|
|
61
|
+
XML_SYNTAX_CODE,
|
|
62
|
+
} from "../../common/codes.js";
|
|
63
|
+
import { type DeclaredTypeSpec, parseDecimalText } from "../../common/declared-types.js";
|
|
64
|
+
import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
|
|
65
|
+
import { IdCoercer } from "../../common/ids.js";
|
|
66
|
+
import { textChunks, throwIfAborted } from "../../common/input.js";
|
|
67
|
+
import { type ListSyntax, splitListText } from "../../common/lists.js";
|
|
68
|
+
import {
|
|
69
|
+
reportSinkOptions,
|
|
70
|
+
reportUnusedOptions,
|
|
71
|
+
type ResolvedImportOptions,
|
|
72
|
+
resolveImportOptions,
|
|
73
|
+
} from "../../common/options.js";
|
|
74
|
+
import { ImportReportBuilder, type IssueLocation } from "../../common/report.js";
|
|
75
|
+
import { parseTimeText, type TemporalValue, type TimeFormat, timeTextCompanion } from "../../common/temporal.js";
|
|
76
|
+
import { isWeightField, parseWeightText } from "../../common/weights.js";
|
|
77
|
+
import { isWhitespace, localName, tokenizeXml, type XmlHandler, XmlSyntaxError } from "../../common/xml.js";
|
|
78
|
+
import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
|
|
79
|
+
import {
|
|
80
|
+
EDGE_DECLS,
|
|
81
|
+
GEXF_FORMAT,
|
|
82
|
+
type GexfEdgeType,
|
|
83
|
+
GRAPH_MODES,
|
|
84
|
+
NODE_DECLS,
|
|
85
|
+
OPEN_END,
|
|
86
|
+
OPEN_START,
|
|
87
|
+
RESERVED_EDGE_NAMES,
|
|
88
|
+
RESERVED_NODE_NAMES,
|
|
89
|
+
temporalTableDecls,
|
|
90
|
+
temporalTableName,
|
|
91
|
+
TIME_FORMATS,
|
|
92
|
+
TIME_REPRESENTATIONS,
|
|
93
|
+
} from "./schema.js";
|
|
94
|
+
|
|
95
|
+
/** The format-specific options of the GEXF importer. */
|
|
96
|
+
export interface GexfImportOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Whether the viz namespace elements (color, position, size, shape, thickness) are imported as
|
|
99
|
+
* role columns (default true); false ignores them and records one `unsupported` warning.
|
|
100
|
+
*/
|
|
101
|
+
viz?: boolean | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Issue code: the document is not a GEXF document (no `<gexf>` root). */
|
|
105
|
+
export const NOT_GEXF_CODE = "E_NOT_GEXF";
|
|
106
|
+
/** Issue code: the graph declares edges but no `<nodes>` section. */
|
|
107
|
+
export const MISSING_NODES_CODE = "E_GEXF_MISSING_NODES";
|
|
108
|
+
/** Issue code: an edge `type` outside directed / undirected / mutual. */
|
|
109
|
+
export const EDGE_TYPE_CODE = "E_GEXF_EDGE_TYPE";
|
|
110
|
+
/** Issue code: an `<attributes>` group without a class, or with an unknown one. */
|
|
111
|
+
export const ATTRIBUTES_CLASS_CODE = "E_GEXF_ATTRIBUTES_CLASS";
|
|
112
|
+
/** Issue code: an `<attribute>` without an id. */
|
|
113
|
+
export const ATTRIBUTE_ID_CODE = "E_GEXF_ATTRIBUTE_ID";
|
|
114
|
+
/** Issue code: a graph header value (`defaultedgetype`, `mode`, `timeformat`, ...) outside its set. */
|
|
115
|
+
export const HEADER_VALUE_CODE = "W_GEXF_HEADER_VALUE";
|
|
116
|
+
/** Issue code: a second `<attribute>` with the same id in the same class (the first is kept). */
|
|
117
|
+
export const DUPLICATE_ATTRIBUTE_CODE = "W_GEXF_DUPLICATE_ATTRIBUTE";
|
|
118
|
+
/** Issue code: an `<attribute>` without a type (read as string). */
|
|
119
|
+
export const ATTRIBUTE_TYPE_CODE = "W_GEXF_ATTRIBUTE_TYPE";
|
|
120
|
+
/** Issue code: an `<attvalue>` naming an attribute the document never declares (once per id). */
|
|
121
|
+
export const UNKNOWN_ATTRIBUTE_CODE = "W_GEXF_UNKNOWN_ATTRIBUTE";
|
|
122
|
+
/** Issue code: an `<attvalue>` without a `for` or a `value`. */
|
|
123
|
+
export const ATTVALUE_SHAPE_CODE = "W_GEXF_ATTVALUE_SHAPE";
|
|
124
|
+
/** Issue code: a timed value on an attribute declared in a static group (stored as dynamic anyway). */
|
|
125
|
+
export const TIMED_STATIC_CODE = "W_GEXF_TIMED_VALUE_ON_STATIC";
|
|
126
|
+
/** Issue code: the `weight` XML attribute is present but `weightFrom` is null. */
|
|
127
|
+
export const WEIGHT_IGNORED_CODE = "W_GEXF_WEIGHT_IGNORED";
|
|
128
|
+
/** Issue code: viz elements skipped under `viz: false`. */
|
|
129
|
+
export const VIZ_SKIPPED_CODE = "W_GEXF_VIZ_SKIPPED";
|
|
130
|
+
/** Issue code: a 1.2 dynamic viz element (its bounds are dropped, the value kept). */
|
|
131
|
+
export const VIZ_DYNAMIC_CODE = "W_GEXF_VIZ_DYNAMIC_DROPPED";
|
|
132
|
+
/** Issue code: `startopen` / `endopen` on a `<spell>` (the spells column has no open bits). */
|
|
133
|
+
export const SPELL_OPEN_CODE = "W_GEXF_SPELL_OPEN_DROPPED";
|
|
134
|
+
/** Issue code: a viz element with a value that does not parse (the element is skipped). */
|
|
135
|
+
export const VIZ_VALUE_CODE = "W_GEXF_VIZ_VALUE";
|
|
136
|
+
/** Issue code: both `start` and `startopen` (or `end` and `endopen`) on one element; the closed bound wins. */
|
|
137
|
+
export const OPEN_BOUND_CONFLICT_CODE = "W_GEXF_OPEN_BOUND_CONFLICT";
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
DUPLICATE_NODE_CODE,
|
|
141
|
+
ID_MERGED_CODE,
|
|
142
|
+
MISSING_ENDPOINT_CODE,
|
|
143
|
+
MISSING_ID_CODE,
|
|
144
|
+
NO_GRAPH_CODE,
|
|
145
|
+
UNKNOWN_PARENT_CODE,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const EDGE_TYPES: ReadonlySet<string> = new Set(["directed", "undirected", "mutual"]);
|
|
149
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
150
|
+
|
|
151
|
+
/** The XML attributes the importer reads on `<node>`; any other one is reported once. */
|
|
152
|
+
const NODE_ATTRIBUTES: ReadonlySet<string> = new Set([
|
|
153
|
+
"id",
|
|
154
|
+
"label",
|
|
155
|
+
"pid",
|
|
156
|
+
"start",
|
|
157
|
+
"end",
|
|
158
|
+
"timestamp",
|
|
159
|
+
"startopen",
|
|
160
|
+
"endopen",
|
|
161
|
+
"timestamps",
|
|
162
|
+
"intervals",
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
/** The XML attributes the importer reads on `<edge>`; any other one is reported once. */
|
|
166
|
+
const EDGE_ATTRIBUTES: ReadonlySet<string> = new Set([
|
|
167
|
+
"id",
|
|
168
|
+
"source",
|
|
169
|
+
"target",
|
|
170
|
+
"type",
|
|
171
|
+
"label",
|
|
172
|
+
"weight",
|
|
173
|
+
"kind",
|
|
174
|
+
"start",
|
|
175
|
+
"end",
|
|
176
|
+
"timestamp",
|
|
177
|
+
"startopen",
|
|
178
|
+
"endopen",
|
|
179
|
+
"timestamps",
|
|
180
|
+
"intervals",
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
/** The common options the GEXF importer reads (the rest is reported by reportUnusedOptions). */
|
|
184
|
+
const USED_OPTIONS: ReadonlySet<keyof CommonImportOptions> = new Set<keyof CommonImportOptions>([
|
|
185
|
+
"ids",
|
|
186
|
+
"addMissingNodes",
|
|
187
|
+
"duplicateEdges",
|
|
188
|
+
"selfLoops",
|
|
189
|
+
"onMixedDirection",
|
|
190
|
+
"defaultDirected",
|
|
191
|
+
"weightFrom",
|
|
192
|
+
"weightDtype",
|
|
193
|
+
"long",
|
|
194
|
+
"errorLimit",
|
|
195
|
+
"signal",
|
|
196
|
+
"onProgress",
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
/** A GraphMetaPatch under construction (its fields are readonly once handed to the sink). */
|
|
200
|
+
type MetaPatchDraft = { -readonly [K in keyof GraphMetaPatch]: GraphMetaPatch[K] };
|
|
201
|
+
|
|
202
|
+
/** A declared attribute as the importer tracks it: its column and how values are parsed and stored. */
|
|
203
|
+
interface TrackedAttribute {
|
|
204
|
+
/** The attribute id as written. */
|
|
205
|
+
readonly id: string;
|
|
206
|
+
/** The column name. */
|
|
207
|
+
readonly name: string;
|
|
208
|
+
/** How values are parsed. */
|
|
209
|
+
readonly spec: DeclaredTypeSpec;
|
|
210
|
+
/** The list syntax of values. */
|
|
211
|
+
readonly listSyntax: ListSyntax;
|
|
212
|
+
/** The column handle. */
|
|
213
|
+
readonly handle: ColumnHandle;
|
|
214
|
+
/** The companion text column's declaration of a temporal attribute, or null. */
|
|
215
|
+
readonly companionDecl: ColumnDecl | null;
|
|
216
|
+
/** The companion's handle once a value needed it, or INVALID_INDEX. */
|
|
217
|
+
companion: ColumnHandle;
|
|
218
|
+
/** Whether the attribute was declared in a dynamic group. */
|
|
219
|
+
readonly dynamic: boolean;
|
|
220
|
+
/** The value column fields of the temporal table. */
|
|
221
|
+
readonly valueDecl: Pick<ColumnDecl, "dtype" | "itemDtype" | "options" | "origin">;
|
|
222
|
+
/** The temporal table, created on the first timed value. */
|
|
223
|
+
table: ExtensionHandle;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** A pending containment reference resolved after every node is declared. */
|
|
227
|
+
interface ParentRef {
|
|
228
|
+
/** The child's node index. */
|
|
229
|
+
readonly child: number;
|
|
230
|
+
/** The parent's id (from `pid` / `<parent for>`), or null when the index is known. */
|
|
231
|
+
readonly parentId: NodeId | null;
|
|
232
|
+
/** The parent's index (nested `<nodes>`), or INVALID_INDEX. */
|
|
233
|
+
readonly parentIndex: number;
|
|
234
|
+
/** Where the reference was read. */
|
|
235
|
+
readonly where: IssueLocation;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A column declared on first use, through the design section 5.6 rule. */
|
|
239
|
+
class LazyColumn {
|
|
240
|
+
private handle: ColumnHandle = INVALID_INDEX as ColumnHandle;
|
|
241
|
+
|
|
242
|
+
private readonly domain: "node" | "edge";
|
|
243
|
+
|
|
244
|
+
private readonly decl: ColumnDecl;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Create a lazily declared column.
|
|
248
|
+
* @param domain - node or edge
|
|
249
|
+
* @param decl - the declaration
|
|
250
|
+
*/
|
|
251
|
+
constructor(domain: "node" | "edge", decl: ColumnDecl) {
|
|
252
|
+
this.domain = domain;
|
|
253
|
+
this.decl = decl;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The handle, declaring the column on the first call (renamed or without its role when the
|
|
258
|
+
* sink already holds the name or the role, reported).
|
|
259
|
+
* @param sink - the sink
|
|
260
|
+
* @param report - the report
|
|
261
|
+
* @param where - the location of the first use
|
|
262
|
+
* @returns the handle
|
|
263
|
+
*/
|
|
264
|
+
get(sink: GraphSink, report: ImportReportBuilder, where?: IssueLocation): ColumnHandle {
|
|
265
|
+
if (this.handle === INVALID_INDEX) {
|
|
266
|
+
this.handle = declareResolved(sink, this.domain, this.decl, report, where).handle;
|
|
267
|
+
}
|
|
268
|
+
return this.handle;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Write one cell, declaring the column first when needed.
|
|
273
|
+
* @param sink - the sink
|
|
274
|
+
* @param report - the report
|
|
275
|
+
* @param index - the row
|
|
276
|
+
* @param value - the value
|
|
277
|
+
* @param where - the location, for a declaration issue
|
|
278
|
+
*/
|
|
279
|
+
set(sink: GraphSink, report: ImportReportBuilder, index: number, value: unknown, where?: IssueLocation): void {
|
|
280
|
+
const handle = this.get(sink, report, where);
|
|
281
|
+
if (this.domain === "node") {
|
|
282
|
+
sink.setNodeValue(handle, index, value);
|
|
283
|
+
} else {
|
|
284
|
+
sink.setEdgeValue(handle, index, value);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** A temporal role column with its lazily declared text companion. */
|
|
290
|
+
class TemporalColumn {
|
|
291
|
+
private readonly column: LazyColumn;
|
|
292
|
+
|
|
293
|
+
private companion: ColumnHandle = INVALID_INDEX as ColumnHandle;
|
|
294
|
+
|
|
295
|
+
private readonly domain: "node" | "edge";
|
|
296
|
+
|
|
297
|
+
private readonly name: string;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Create the pair.
|
|
301
|
+
* @param domain - node or edge
|
|
302
|
+
* @param decl - the numeric column's declaration
|
|
303
|
+
*/
|
|
304
|
+
constructor(domain: "node" | "edge", decl: ColumnDecl) {
|
|
305
|
+
this.domain = domain;
|
|
306
|
+
this.name = decl.name;
|
|
307
|
+
this.column = new LazyColumn(domain, decl);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Write a parsed time value and, when its text must be kept, the companion cell.
|
|
312
|
+
* @param sink - the sink
|
|
313
|
+
* @param report - the report
|
|
314
|
+
* @param index - the row
|
|
315
|
+
* @param value - the parsed value
|
|
316
|
+
* @param where - the location, for a declaration issue
|
|
317
|
+
*/
|
|
318
|
+
set(
|
|
319
|
+
sink: GraphSink,
|
|
320
|
+
report: ImportReportBuilder,
|
|
321
|
+
index: number,
|
|
322
|
+
value: TemporalValue,
|
|
323
|
+
where?: IssueLocation,
|
|
324
|
+
): void {
|
|
325
|
+
this.column.set(sink, report, index, value.value, where);
|
|
326
|
+
if (value.text === null) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (this.companion === INVALID_INDEX) {
|
|
330
|
+
this.companion = declareCompanion(sink, this.domain, timeTextCompanion(this.name), report);
|
|
331
|
+
}
|
|
332
|
+
if (this.domain === "node") {
|
|
333
|
+
sink.setNodeValue(this.companion, index, value.text);
|
|
334
|
+
} else {
|
|
335
|
+
sink.setEdgeValue(this.companion, index, value.text);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The lazily declared columns of one domain. */
|
|
341
|
+
interface DomainColumns {
|
|
342
|
+
readonly label: LazyColumn;
|
|
343
|
+
readonly start: TemporalColumn;
|
|
344
|
+
readonly end: TemporalColumn;
|
|
345
|
+
readonly timestamp: TemporalColumn;
|
|
346
|
+
readonly spells: LazyColumn;
|
|
347
|
+
readonly timestamps: LazyColumn;
|
|
348
|
+
readonly open: LazyColumn;
|
|
349
|
+
readonly color: LazyColumn;
|
|
350
|
+
readonly shape: LazyColumn;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Where the reader is in the document. */
|
|
354
|
+
const enum Ctx {
|
|
355
|
+
Gexf,
|
|
356
|
+
Meta,
|
|
357
|
+
MetaText,
|
|
358
|
+
Graph,
|
|
359
|
+
Attributes,
|
|
360
|
+
Attribute,
|
|
361
|
+
AttributeText,
|
|
362
|
+
Nodes,
|
|
363
|
+
Node,
|
|
364
|
+
Attvalues,
|
|
365
|
+
Parents,
|
|
366
|
+
Spells,
|
|
367
|
+
Edges,
|
|
368
|
+
Edge,
|
|
369
|
+
Skip,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** An open `<node>`: its index (INVALID_INDEX when the node was skipped) and location. */
|
|
373
|
+
interface NodeFrame {
|
|
374
|
+
readonly index: number;
|
|
375
|
+
readonly where: IssueLocation;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The open `<edge>`: its primary index, the mirror of an expanded pair (or INVALID_INDEX) and
|
|
380
|
+
* location. One frame is reused for every edge (the hot loop allocates nothing per edge but the
|
|
381
|
+
* tokenizer's attribute map): the report copies a location's fields into each issue, and nothing
|
|
382
|
+
* keeps a reference to an edge's frame or location past the edge's end tag.
|
|
383
|
+
*/
|
|
384
|
+
interface EdgeFrame {
|
|
385
|
+
index: number;
|
|
386
|
+
mirror: number;
|
|
387
|
+
readonly where: MutableLocation;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** A location whose fields are overwritten per element (see EdgeFrame). */
|
|
391
|
+
interface MutableLocation {
|
|
392
|
+
line: number;
|
|
393
|
+
element: string | null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** An `<attribute>` being read: its start-tag fields and the texts of `<default>` / `<options>`. */
|
|
397
|
+
interface PendingAttribute {
|
|
398
|
+
readonly domain: "node" | "edge";
|
|
399
|
+
readonly dynamic: boolean;
|
|
400
|
+
readonly attrs: ReadonlyMap<string, string>;
|
|
401
|
+
readonly line: number;
|
|
402
|
+
defaultText: string | null;
|
|
403
|
+
optionsText: string | null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The event handler that turns the tokenizer's events into sink calls: one import call.
|
|
408
|
+
*/
|
|
409
|
+
class GexfReader implements XmlHandler {
|
|
410
|
+
private readonly sink: GraphSink;
|
|
411
|
+
|
|
412
|
+
private readonly report: ImportReportBuilder;
|
|
413
|
+
|
|
414
|
+
private readonly options: ResolvedImportOptions;
|
|
415
|
+
|
|
416
|
+
private readonly viz: boolean;
|
|
417
|
+
|
|
418
|
+
private readonly coercer: IdCoercer;
|
|
419
|
+
|
|
420
|
+
private readonly resolver: DirectionResolver;
|
|
421
|
+
|
|
422
|
+
private readonly ctx: Ctx[] = [];
|
|
423
|
+
|
|
424
|
+
private timeFormat: TimeFormat | null = null;
|
|
425
|
+
|
|
426
|
+
private defaultKind: GexfEdgeType = "undirected";
|
|
427
|
+
|
|
428
|
+
private readonly meta: MetaPatchDraft = { sourceFormat: GEXF_FORMAT };
|
|
429
|
+
|
|
430
|
+
private metaText = "";
|
|
431
|
+
|
|
432
|
+
private metaField: "creator" | "description" | "keywords" | null = null;
|
|
433
|
+
|
|
434
|
+
private readonly nodeAttrs = new Map<string, TrackedAttribute>();
|
|
435
|
+
|
|
436
|
+
private readonly edgeAttrs = new Map<string, TrackedAttribute>();
|
|
437
|
+
|
|
438
|
+
private readonly declaredNodeNames = new Set<string>();
|
|
439
|
+
|
|
440
|
+
private readonly declaredEdgeNames = new Set<string>();
|
|
441
|
+
|
|
442
|
+
/** The edge attribute that is THE weight (its title equals weightFrom), or null. */
|
|
443
|
+
private weightAttr: { readonly id: string; readonly spec: DeclaredTypeSpec; table: ExtensionHandle } | null = null;
|
|
444
|
+
|
|
445
|
+
private readonly nodeColumns: DomainColumns;
|
|
446
|
+
|
|
447
|
+
private readonly edgeColumns: DomainColumns;
|
|
448
|
+
|
|
449
|
+
private readonly parentColumn = new LazyColumn("node", NODE_DECLS.parent);
|
|
450
|
+
|
|
451
|
+
private readonly parentsColumn = new LazyColumn("node", NODE_DECLS.parents);
|
|
452
|
+
|
|
453
|
+
private positionColumn: LazyColumn | null = null;
|
|
454
|
+
|
|
455
|
+
private readonly sizeColumn = new LazyColumn("node", NODE_DECLS.size);
|
|
456
|
+
|
|
457
|
+
private readonly shapeUriColumn = new LazyColumn("node", NODE_DECLS.shapeUri);
|
|
458
|
+
|
|
459
|
+
private readonly thicknessColumn = new LazyColumn("edge", EDGE_DECLS.thickness);
|
|
460
|
+
|
|
461
|
+
private readonly edgeIdColumn = new LazyColumn("edge", EDGE_DECLS.id);
|
|
462
|
+
|
|
463
|
+
private readonly edgeKindColumn = new LazyColumn("edge", EDGE_DECLS.kind);
|
|
464
|
+
|
|
465
|
+
private readonly edgeIds = new Set<string>();
|
|
466
|
+
|
|
467
|
+
private parentRefs: ParentRef[] = [];
|
|
468
|
+
|
|
469
|
+
private parentsRefs: { readonly child: number; readonly ids: readonly NodeId[]; readonly where: IssueLocation }[] =
|
|
470
|
+
[];
|
|
471
|
+
|
|
472
|
+
private readonly unknownAttributes = new Set<string>();
|
|
473
|
+
|
|
474
|
+
private readonly nodeStack: NodeFrame[] = [];
|
|
475
|
+
|
|
476
|
+
private edge: EdgeFrame | null = null;
|
|
477
|
+
|
|
478
|
+
/** The one edge frame, reused for every `<edge>` (see EdgeFrame). */
|
|
479
|
+
private readonly edgeFrame: EdgeFrame = {
|
|
480
|
+
index: INVALID_INDEX,
|
|
481
|
+
mirror: INVALID_INDEX,
|
|
482
|
+
where: { line: 0, element: null },
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
/** The one `<attvalue>` location, reused per value (its fields are copied into every issue). */
|
|
486
|
+
private readonly valueWhere: MutableLocation = { line: 0, element: null };
|
|
487
|
+
|
|
488
|
+
private attributeGroup: { readonly domain: "node" | "edge" | null; readonly dynamic: boolean } | null = null;
|
|
489
|
+
|
|
490
|
+
private pendingAttribute: PendingAttribute | null = null;
|
|
491
|
+
|
|
492
|
+
private attributeText: "default" | "options" | null = null;
|
|
493
|
+
|
|
494
|
+
private attributeTextValue = "";
|
|
495
|
+
|
|
496
|
+
private spellPairs: number[][] | null = null;
|
|
497
|
+
|
|
498
|
+
private spellsWhere: IssueLocation | null = null;
|
|
499
|
+
|
|
500
|
+
private parentIds: NodeId[] | null = null;
|
|
501
|
+
|
|
502
|
+
private nodesSeen = false;
|
|
503
|
+
|
|
504
|
+
private graphSeen = false;
|
|
505
|
+
|
|
506
|
+
private elementsSinceCheck = 0;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Set up one import.
|
|
510
|
+
* @param sink - the sink
|
|
511
|
+
* @param report - the report
|
|
512
|
+
* @param options - the resolved common options
|
|
513
|
+
* @param viz - whether viz elements are imported
|
|
514
|
+
*/
|
|
515
|
+
constructor(sink: GraphSink, report: ImportReportBuilder, options: ResolvedImportOptions, viz: boolean) {
|
|
516
|
+
this.sink = sink;
|
|
517
|
+
this.report = report;
|
|
518
|
+
this.options = options;
|
|
519
|
+
this.viz = viz;
|
|
520
|
+
this.coercer = new IdCoercer(options.ids);
|
|
521
|
+
this.resolver = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
522
|
+
this.nodeColumns = {
|
|
523
|
+
label: new LazyColumn("node", NODE_DECLS.label),
|
|
524
|
+
start: new TemporalColumn("node", NODE_DECLS.start),
|
|
525
|
+
end: new TemporalColumn("node", NODE_DECLS.end),
|
|
526
|
+
timestamp: new TemporalColumn("node", NODE_DECLS.timestamp),
|
|
527
|
+
spells: new LazyColumn("node", NODE_DECLS.spells),
|
|
528
|
+
timestamps: new LazyColumn("node", NODE_DECLS.timestamps),
|
|
529
|
+
open: new LazyColumn("node", NODE_DECLS.open),
|
|
530
|
+
color: new LazyColumn("node", NODE_DECLS.color),
|
|
531
|
+
shape: new LazyColumn("node", NODE_DECLS.shape),
|
|
532
|
+
};
|
|
533
|
+
this.edgeColumns = {
|
|
534
|
+
label: new LazyColumn("edge", EDGE_DECLS.label),
|
|
535
|
+
start: new TemporalColumn("edge", EDGE_DECLS.start),
|
|
536
|
+
end: new TemporalColumn("edge", EDGE_DECLS.end),
|
|
537
|
+
timestamp: new TemporalColumn("edge", EDGE_DECLS.timestamp),
|
|
538
|
+
spells: new LazyColumn("edge", EDGE_DECLS.spells),
|
|
539
|
+
timestamps: new LazyColumn("edge", EDGE_DECLS.timestamps),
|
|
540
|
+
open: new LazyColumn("edge", EDGE_DECLS.open),
|
|
541
|
+
color: new LazyColumn("edge", EDGE_DECLS.color),
|
|
542
|
+
shape: new LazyColumn("edge", EDGE_DECLS.shape),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ---------------------------------------------------------------- events
|
|
547
|
+
|
|
548
|
+
start(name: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
549
|
+
const parent = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
|
|
550
|
+
const local = localName(name);
|
|
551
|
+
switch (parent) {
|
|
552
|
+
case null:
|
|
553
|
+
if (local !== "gexf") {
|
|
554
|
+
this.report.fail(NOT_GEXF_CODE, `the root element is <${name}>, not <gexf>`, { line });
|
|
555
|
+
}
|
|
556
|
+
this.readRoot(attrs);
|
|
557
|
+
this.ctx.push(Ctx.Gexf);
|
|
558
|
+
return;
|
|
559
|
+
case Ctx.Gexf:
|
|
560
|
+
this.startInGexf(name, local, attrs, line);
|
|
561
|
+
return;
|
|
562
|
+
case Ctx.Meta:
|
|
563
|
+
this.startInMeta(name, local, line);
|
|
564
|
+
return;
|
|
565
|
+
case Ctx.Graph:
|
|
566
|
+
this.startInGraph(name, local, attrs, line);
|
|
567
|
+
return;
|
|
568
|
+
case Ctx.Attributes:
|
|
569
|
+
this.startInAttributes(name, local, attrs, line);
|
|
570
|
+
return;
|
|
571
|
+
case Ctx.Attribute:
|
|
572
|
+
this.startInAttribute(name, local, line);
|
|
573
|
+
return;
|
|
574
|
+
case Ctx.Nodes:
|
|
575
|
+
this.startInNodes(name, local, attrs, line);
|
|
576
|
+
return;
|
|
577
|
+
case Ctx.Node:
|
|
578
|
+
this.startInNode(name, local, attrs, line);
|
|
579
|
+
return;
|
|
580
|
+
case Ctx.Attvalues:
|
|
581
|
+
this.startInAttvalues(name, local, attrs, line);
|
|
582
|
+
return;
|
|
583
|
+
case Ctx.Parents:
|
|
584
|
+
this.startInParents(name, local, attrs, line);
|
|
585
|
+
return;
|
|
586
|
+
case Ctx.Spells:
|
|
587
|
+
this.startInSpells(name, local, attrs, line);
|
|
588
|
+
return;
|
|
589
|
+
case Ctx.Edges:
|
|
590
|
+
this.startInEdges(name, local, attrs, line);
|
|
591
|
+
return;
|
|
592
|
+
case Ctx.Edge:
|
|
593
|
+
this.startInEdge(name, local, attrs, line);
|
|
594
|
+
return;
|
|
595
|
+
case Ctx.MetaText:
|
|
596
|
+
case Ctx.AttributeText:
|
|
597
|
+
case Ctx.Skip:
|
|
598
|
+
this.ctx.push(Ctx.Skip);
|
|
599
|
+
return;
|
|
600
|
+
default:
|
|
601
|
+
this.unknownElement(name, line);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* An element ends.
|
|
607
|
+
* @param _name - the element name
|
|
608
|
+
* @param _line - the line (unused: issues of an element cite its start line)
|
|
609
|
+
*/
|
|
610
|
+
end(_name: string, _line: number): void {
|
|
611
|
+
const ctx = this.ctx.pop();
|
|
612
|
+
switch (ctx) {
|
|
613
|
+
case Ctx.Meta:
|
|
614
|
+
this.finishMeta();
|
|
615
|
+
return;
|
|
616
|
+
case Ctx.MetaText:
|
|
617
|
+
this.finishMetaText();
|
|
618
|
+
return;
|
|
619
|
+
case Ctx.Graph:
|
|
620
|
+
this.resolveParents();
|
|
621
|
+
return;
|
|
622
|
+
case Ctx.Attributes:
|
|
623
|
+
this.attributeGroup = null;
|
|
624
|
+
return;
|
|
625
|
+
case Ctx.Attribute:
|
|
626
|
+
this.finishAttribute();
|
|
627
|
+
return;
|
|
628
|
+
case Ctx.AttributeText:
|
|
629
|
+
this.finishAttributeText();
|
|
630
|
+
return;
|
|
631
|
+
case Ctx.Node:
|
|
632
|
+
this.nodeStack.pop();
|
|
633
|
+
return;
|
|
634
|
+
case Ctx.Edge:
|
|
635
|
+
this.edge = null;
|
|
636
|
+
return;
|
|
637
|
+
case Ctx.Parents:
|
|
638
|
+
this.finishParents();
|
|
639
|
+
return;
|
|
640
|
+
case Ctx.Spells:
|
|
641
|
+
this.finishSpells();
|
|
642
|
+
return;
|
|
643
|
+
case Ctx.Gexf:
|
|
644
|
+
case Ctx.Nodes:
|
|
645
|
+
case Ctx.Edges:
|
|
646
|
+
case Ctx.Attvalues:
|
|
647
|
+
case Ctx.Skip:
|
|
648
|
+
case undefined:
|
|
649
|
+
return;
|
|
650
|
+
default: {
|
|
651
|
+
const value: never = ctx;
|
|
652
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown reader state ${String(value)}`, {});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Character data: kept inside `<creator>` / `<description>` / `<keywords>` and `<default>` /
|
|
659
|
+
* `<options>`, ignored elsewhere (reported once when it is not whitespace).
|
|
660
|
+
* @param text - the text
|
|
661
|
+
* @param line - the line
|
|
662
|
+
*/
|
|
663
|
+
text(text: string, line: number): void {
|
|
664
|
+
const ctx = this.ctx.length === 0 ? null : this.ctx[this.ctx.length - 1];
|
|
665
|
+
switch (ctx) {
|
|
666
|
+
case Ctx.MetaText:
|
|
667
|
+
this.metaText += text;
|
|
668
|
+
return;
|
|
669
|
+
case Ctx.AttributeText:
|
|
670
|
+
this.attributeTextValue += text;
|
|
671
|
+
return;
|
|
672
|
+
case Ctx.Skip:
|
|
673
|
+
return;
|
|
674
|
+
default:
|
|
675
|
+
if (!isWhitespace(text)) {
|
|
676
|
+
this.report.warnOnce(
|
|
677
|
+
"validation-error",
|
|
678
|
+
STRAY_TEXT_CODE,
|
|
679
|
+
"text where GEXF allows only elements was ignored",
|
|
680
|
+
{ line },
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Apply what the root and `<meta>` gave to the sink; called once the graph's header is read.
|
|
688
|
+
*/
|
|
689
|
+
finish(): void {
|
|
690
|
+
if (!this.graphSeen) {
|
|
691
|
+
this.report.fail(NO_GRAPH_CODE, "the document has no <graph> element");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ---------------------------------------------------------------- root, meta, graph
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Record the root's version.
|
|
699
|
+
* @param attrs - the `<gexf>` attributes
|
|
700
|
+
*/
|
|
701
|
+
private readRoot(attrs: ReadonlyMap<string, string>): void {
|
|
702
|
+
const version = attrs.get("version");
|
|
703
|
+
if (version !== undefined && version.length > 0) {
|
|
704
|
+
this.meta.sourceVersion = version;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* A child of `<gexf>`.
|
|
710
|
+
* @param name - the element name
|
|
711
|
+
* @param local - its local name
|
|
712
|
+
* @param attrs - its attributes
|
|
713
|
+
* @param line - the line
|
|
714
|
+
*/
|
|
715
|
+
private startInGexf(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
716
|
+
switch (local) {
|
|
717
|
+
case "meta": {
|
|
718
|
+
const modified = attrs.get("lastmodifieddate");
|
|
719
|
+
if (modified !== undefined && modified.length > 0) {
|
|
720
|
+
this.meta.modified = modified;
|
|
721
|
+
}
|
|
722
|
+
this.ctx.push(Ctx.Meta);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
case "graph":
|
|
726
|
+
if (this.graphSeen) {
|
|
727
|
+
this.report.fail(NO_GRAPH_CODE, "a second <graph> element; a GEXF document has one graph", {
|
|
728
|
+
line,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
this.graphSeen = true;
|
|
732
|
+
this.readHeader(attrs, line);
|
|
733
|
+
this.ctx.push(Ctx.Graph);
|
|
734
|
+
return;
|
|
735
|
+
default:
|
|
736
|
+
this.unknownElement(name, line);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* A child of `<meta>`.
|
|
742
|
+
* @param name - the element name
|
|
743
|
+
* @param local - its local name
|
|
744
|
+
* @param line - the line
|
|
745
|
+
*/
|
|
746
|
+
private startInMeta(name: string, local: string, line: number): void {
|
|
747
|
+
if (local === "creator" || local === "description" || local === "keywords") {
|
|
748
|
+
this.metaField = local;
|
|
749
|
+
this.metaText = "";
|
|
750
|
+
this.ctx.push(Ctx.MetaText);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
this.unknownElement(name, line);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Close a `<creator>` / `<description>` / `<keywords>`. */
|
|
757
|
+
private finishMetaText(): void {
|
|
758
|
+
const text = this.metaText;
|
|
759
|
+
switch (this.metaField) {
|
|
760
|
+
case "creator":
|
|
761
|
+
this.meta.creator = text;
|
|
762
|
+
break;
|
|
763
|
+
case "description":
|
|
764
|
+
this.meta.description = text;
|
|
765
|
+
break;
|
|
766
|
+
case "keywords":
|
|
767
|
+
this.meta.keywords = text
|
|
768
|
+
.split(",")
|
|
769
|
+
.map((k) => k.trim())
|
|
770
|
+
.filter((k) => k.length > 0);
|
|
771
|
+
break;
|
|
772
|
+
default:
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
this.metaField = null;
|
|
776
|
+
this.metaText = "";
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** Close `<meta>`: nothing more to do (the patch is applied at `<graph>`). */
|
|
780
|
+
private finishMeta(): void {
|
|
781
|
+
// the meta patch is applied when the graph header is read
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Read `<graph defaultedgetype mode timeformat timerepresentation idtype start end timestamp>`
|
|
786
|
+
* into the meta and the importer's own state, apply the meta patch and set the sink's
|
|
787
|
+
* direction; an unknown value is a warning and the default applies.
|
|
788
|
+
* @param attrs - the `<graph>` attributes
|
|
789
|
+
* @param line - the line
|
|
790
|
+
*/
|
|
791
|
+
private readHeader(attrs: ReadonlyMap<string, string>, line: number): void {
|
|
792
|
+
const where = { line };
|
|
793
|
+
const patch = this.meta;
|
|
794
|
+
const edgeType = attrs.get("defaultedgetype");
|
|
795
|
+
if (edgeType === undefined) {
|
|
796
|
+
this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
|
|
797
|
+
} else if (EDGE_TYPES.has(edgeType)) {
|
|
798
|
+
this.defaultKind = edgeType as GexfEdgeType;
|
|
799
|
+
} else {
|
|
800
|
+
this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
|
|
801
|
+
this.warnHeader("defaultedgetype", edgeType, where);
|
|
802
|
+
}
|
|
803
|
+
const mode = attrs.get("mode") ?? attrs.get("type");
|
|
804
|
+
if (mode !== undefined) {
|
|
805
|
+
if (GRAPH_MODES.has(mode)) {
|
|
806
|
+
patch.mode = mode as "static" | "dynamic" | "slice";
|
|
807
|
+
} else {
|
|
808
|
+
this.warnHeader("mode", mode, where);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const timeFormat = attrs.get("timeformat");
|
|
812
|
+
if (timeFormat !== undefined) {
|
|
813
|
+
if (TIME_FORMATS.has(timeFormat)) {
|
|
814
|
+
this.timeFormat = timeFormat as TimeFormat;
|
|
815
|
+
patch.timeFormat = this.timeFormat;
|
|
816
|
+
} else {
|
|
817
|
+
this.warnHeader("timeformat", timeFormat, where);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const representation = attrs.get("timerepresentation");
|
|
821
|
+
if (representation !== undefined) {
|
|
822
|
+
if (TIME_REPRESENTATIONS.has(representation)) {
|
|
823
|
+
patch.timeRepresentation = representation as "interval" | "timestamp";
|
|
824
|
+
} else {
|
|
825
|
+
this.warnHeader("timerepresentation", representation, where);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const idType = attrs.get("idtype");
|
|
829
|
+
if (idType !== undefined) {
|
|
830
|
+
if (idType === "string") {
|
|
831
|
+
// informational only: Gephi writes idtype="string" unconditionally, so honouring it
|
|
832
|
+
// would turn every Gephi export's integer ids into text; the canonical rule of
|
|
833
|
+
// design section 4.1 applies and the exporter's check() reports W_ID_TEXT_TYPE
|
|
834
|
+
patch.idType = "string";
|
|
835
|
+
} else if (idType === "integer" || idType === "long") {
|
|
836
|
+
patch.idType = "integer";
|
|
837
|
+
} else {
|
|
838
|
+
this.warnHeader("idtype", idType, where);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const extra: Record<string, unknown> = {};
|
|
842
|
+
for (const key of ["start", "end", "timestamp"]) {
|
|
843
|
+
const value = attrs.get(key);
|
|
844
|
+
if (value !== undefined) {
|
|
845
|
+
extra[key] = value;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (Object.keys(extra).length > 0) {
|
|
849
|
+
patch.extra = { gexf: extra };
|
|
850
|
+
}
|
|
851
|
+
this.sink.setMeta(patch);
|
|
852
|
+
this.resolver.setHeader(this.defaultKind !== "undirected", where);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Record a header value outside its set.
|
|
857
|
+
* @param name - the attribute name
|
|
858
|
+
* @param value - the value found
|
|
859
|
+
* @param where - the header line
|
|
860
|
+
*/
|
|
861
|
+
private warnHeader(name: string, value: string, where: IssueLocation): void {
|
|
862
|
+
this.report.warning(
|
|
863
|
+
"validation-error",
|
|
864
|
+
HEADER_VALUE_CODE,
|
|
865
|
+
`<graph ${name}="${value}"> is not a known value; the default applies`,
|
|
866
|
+
{ ...where, element: name },
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* A child of `<graph>`.
|
|
872
|
+
* @param name - the element name
|
|
873
|
+
* @param local - its local name
|
|
874
|
+
* @param attrs - its attributes
|
|
875
|
+
* @param line - the line
|
|
876
|
+
*/
|
|
877
|
+
private startInGraph(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
878
|
+
switch (local) {
|
|
879
|
+
case "attributes":
|
|
880
|
+
this.beginAttributes(attrs, line);
|
|
881
|
+
return;
|
|
882
|
+
case "nodes":
|
|
883
|
+
this.nodesSeen = true;
|
|
884
|
+
this.reserve(attrs, "node", line);
|
|
885
|
+
this.ctx.push(Ctx.Nodes);
|
|
886
|
+
return;
|
|
887
|
+
case "edges":
|
|
888
|
+
if (!this.nodesSeen) {
|
|
889
|
+
this.report.error(
|
|
890
|
+
"validation-error",
|
|
891
|
+
MISSING_NODES_CODE,
|
|
892
|
+
"the graph declares <edges> but no <nodes>",
|
|
893
|
+
{ line },
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
this.resolveParents();
|
|
897
|
+
this.reserve(attrs, "edge", line);
|
|
898
|
+
this.ctx.push(Ctx.Edges);
|
|
899
|
+
return;
|
|
900
|
+
default:
|
|
901
|
+
this.unknownElement(name, line);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Pass a section's `count` hint to the sink; a hint the sink cannot reserve is a warning.
|
|
907
|
+
* @param attrs - the `<nodes>` or `<edges>` attributes
|
|
908
|
+
* @param domain - which count it is
|
|
909
|
+
* @param line - the line
|
|
910
|
+
*/
|
|
911
|
+
private reserve(attrs: ReadonlyMap<string, string>, domain: "node" | "edge", line: number): void {
|
|
912
|
+
const text = attrs.get("count");
|
|
913
|
+
if (text === undefined) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const count = Number(text);
|
|
917
|
+
if (!Number.isInteger(count) || count < 0 || count > MAX_COUNT) {
|
|
918
|
+
this.report.warning(
|
|
919
|
+
"validation-error",
|
|
920
|
+
COUNT_HINT_CODE,
|
|
921
|
+
`<${domain}s count="${text}"> is not a count the sink can reserve; ignored`,
|
|
922
|
+
{ line, element: "count" },
|
|
923
|
+
);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (count === 0) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
if (domain === "node") {
|
|
931
|
+
this.sink.reserve(count, undefined);
|
|
932
|
+
} else {
|
|
933
|
+
this.sink.reserve(undefined, count);
|
|
934
|
+
}
|
|
935
|
+
} catch (err) {
|
|
936
|
+
if (!(err instanceof GraphFormatError)) {
|
|
937
|
+
throw err;
|
|
938
|
+
}
|
|
939
|
+
this.report.warning("validation-error", COUNT_HINT_CODE, `count hint ${text} refused: ${err.message}`, {
|
|
940
|
+
line,
|
|
941
|
+
element: "count",
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ---------------------------------------------------------------- attribute declarations
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Open an `<attributes class mode>` group.
|
|
950
|
+
* @param attrs - the group's attributes
|
|
951
|
+
* @param line - the line
|
|
952
|
+
*/
|
|
953
|
+
private beginAttributes(attrs: ReadonlyMap<string, string>, line: number): void {
|
|
954
|
+
const cls = attrs.get("class");
|
|
955
|
+
const where = { line };
|
|
956
|
+
let domain: "node" | "edge" | null = null;
|
|
957
|
+
if (cls === "node" || cls === "edge") {
|
|
958
|
+
domain = cls;
|
|
959
|
+
} else {
|
|
960
|
+
this.report.error(
|
|
961
|
+
"validation-error",
|
|
962
|
+
ATTRIBUTES_CLASS_CODE,
|
|
963
|
+
cls === undefined
|
|
964
|
+
? "<attributes> without a class"
|
|
965
|
+
: `<attributes class="${cls}"> is neither node nor edge`,
|
|
966
|
+
where,
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
const mode = attrs.get("mode");
|
|
970
|
+
if (mode !== undefined && mode !== "static" && mode !== "dynamic") {
|
|
971
|
+
this.warnHeader("mode", mode, where);
|
|
972
|
+
}
|
|
973
|
+
this.attributeGroup = { domain, dynamic: mode === "dynamic" };
|
|
974
|
+
this.ctx.push(Ctx.Attributes);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* A child of `<attributes>`.
|
|
979
|
+
* @param name - the element name
|
|
980
|
+
* @param local - its local name
|
|
981
|
+
* @param attrs - its attributes
|
|
982
|
+
* @param line - the line
|
|
983
|
+
*/
|
|
984
|
+
private startInAttributes(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
985
|
+
if (local !== "attribute") {
|
|
986
|
+
this.unknownElement(name, line);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const group = this.attributeGroup;
|
|
990
|
+
if (group === null || group.domain === null) {
|
|
991
|
+
// the group's class was refused; its attributes are dropped with it
|
|
992
|
+
this.ctx.push(Ctx.Skip);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
this.pendingAttribute = {
|
|
996
|
+
domain: group.domain,
|
|
997
|
+
dynamic: group.dynamic,
|
|
998
|
+
attrs,
|
|
999
|
+
line,
|
|
1000
|
+
defaultText: null,
|
|
1001
|
+
optionsText: null,
|
|
1002
|
+
};
|
|
1003
|
+
this.ctx.push(Ctx.Attribute);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* A child of `<attribute>`: `<default>` or `<options>`.
|
|
1008
|
+
* @param name - the element name
|
|
1009
|
+
* @param local - its local name
|
|
1010
|
+
* @param line - the line
|
|
1011
|
+
*/
|
|
1012
|
+
private startInAttribute(name: string, local: string, line: number): void {
|
|
1013
|
+
if (local === "default" || local === "options") {
|
|
1014
|
+
this.attributeText = local;
|
|
1015
|
+
this.attributeTextValue = "";
|
|
1016
|
+
this.ctx.push(Ctx.AttributeText);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
this.unknownElement(name, line);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/** Close a `<default>` / `<options>`. */
|
|
1023
|
+
private finishAttributeText(): void {
|
|
1024
|
+
const pending = this.pendingAttribute;
|
|
1025
|
+
if (pending !== null) {
|
|
1026
|
+
if (this.attributeText === "default") {
|
|
1027
|
+
pending.defaultText = this.attributeTextValue;
|
|
1028
|
+
} else if (this.attributeText === "options") {
|
|
1029
|
+
pending.optionsText = this.attributeTextValue;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
this.attributeText = null;
|
|
1033
|
+
this.attributeTextValue = "";
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** Close an `<attribute>`: declare it. */
|
|
1037
|
+
private finishAttribute(): void {
|
|
1038
|
+
const pending = this.pendingAttribute;
|
|
1039
|
+
this.pendingAttribute = null;
|
|
1040
|
+
if (pending === null) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
this.declareOne(pending);
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
this.report.recordError(err, { line: pending.line, element: pending.attrs.get("id") ?? null });
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Declare one `<attribute id title type>` with its `<default>` and `<options>` (design
|
|
1052
|
+
* sections 5.1, 5.5, 5.6). The edge attribute titled `weightFrom` is the weight and gets no
|
|
1053
|
+
* column; its declaration is recorded in meta.weightOrigin.
|
|
1054
|
+
* @param pending - the attribute as read
|
|
1055
|
+
*/
|
|
1056
|
+
private declareOne(pending: PendingAttribute): void {
|
|
1057
|
+
const { report, sink, options } = this;
|
|
1058
|
+
const { domain, dynamic, attrs } = pending;
|
|
1059
|
+
const id = attrs.get("id");
|
|
1060
|
+
const where = { line: pending.line, element: id ?? null };
|
|
1061
|
+
if (id === undefined) {
|
|
1062
|
+
report.error("validation-error", ATTRIBUTE_ID_CODE, "<attribute> without an id", where);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const tracked = domain === "node" ? this.nodeAttrs : this.edgeAttrs;
|
|
1066
|
+
if (tracked.has(id) || (domain === "edge" && this.weightAttr !== null && this.weightAttr.id === id)) {
|
|
1067
|
+
report.warning(
|
|
1068
|
+
"validation-error",
|
|
1069
|
+
DUPLICATE_ATTRIBUTE_CODE,
|
|
1070
|
+
`${domain} attribute "${id}" is declared twice; the first declaration is kept`,
|
|
1071
|
+
where,
|
|
1072
|
+
);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const title = attrs.get("title") ?? null;
|
|
1076
|
+
const type = attrs.get("type") ?? null;
|
|
1077
|
+
if (type === null) {
|
|
1078
|
+
report.warning(
|
|
1079
|
+
"validation-error",
|
|
1080
|
+
ATTRIBUTE_TYPE_CODE,
|
|
1081
|
+
`attribute "${id}" declares no type; read as string`,
|
|
1082
|
+
where,
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const name = title !== null && title.length > 0 ? title : id;
|
|
1086
|
+
if (domain === "edge" && isWeightField(name, options.weightFrom)) {
|
|
1087
|
+
const declared = declareAttribute({ format: GEXF_FORMAT, id, title, type, long: options.long });
|
|
1088
|
+
for (const issue of declared.issues) {
|
|
1089
|
+
report.warning(issue.category, issue.code, issue.message, where);
|
|
1090
|
+
}
|
|
1091
|
+
this.weightAttr = { id, spec: declared.spec, table: INVALID_INDEX as ExtensionHandle };
|
|
1092
|
+
sink.setMeta({ weightOrigin: { format: GEXF_FORMAT, id, title, type, namespace: null } });
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const declaredNames = domain === "node" ? this.declaredNodeNames : this.declaredEdgeNames;
|
|
1096
|
+
const reserved = domain === "node" ? RESERVED_NODE_NAMES : RESERVED_EDGE_NAMES;
|
|
1097
|
+
const taken = (candidate: string): boolean => reserved.has(candidate) || declaredNames.has(candidate);
|
|
1098
|
+
const input: AttributeDeclarationInput = {
|
|
1099
|
+
format: GEXF_FORMAT,
|
|
1100
|
+
id,
|
|
1101
|
+
title,
|
|
1102
|
+
type,
|
|
1103
|
+
defaultText: pending.defaultText,
|
|
1104
|
+
optionsText: pending.optionsText,
|
|
1105
|
+
listSyntax: "gexf",
|
|
1106
|
+
dynamic,
|
|
1107
|
+
long: options.long,
|
|
1108
|
+
taken,
|
|
1109
|
+
};
|
|
1110
|
+
const declared: DeclaredAttribute = declareAttribute(input);
|
|
1111
|
+
for (const issue of declared.issues) {
|
|
1112
|
+
report.warning(issue.category, issue.code, issue.message, where);
|
|
1113
|
+
}
|
|
1114
|
+
const resolved = declareResolved(sink, domain, declared.decl, report, where);
|
|
1115
|
+
declaredNames.add(resolved.decl.name);
|
|
1116
|
+
let companionDecl: ColumnDecl | null = null;
|
|
1117
|
+
if (declared.companion !== null) {
|
|
1118
|
+
companionDecl = {
|
|
1119
|
+
...declared.companion,
|
|
1120
|
+
name: `${resolved.decl.name}.text`,
|
|
1121
|
+
extra: { for: resolved.decl.name },
|
|
1122
|
+
};
|
|
1123
|
+
declaredNames.add(companionDecl.name);
|
|
1124
|
+
}
|
|
1125
|
+
const valueDecl: Pick<ColumnDecl, "dtype" | "itemDtype" | "options" | "origin"> = {
|
|
1126
|
+
dtype: resolved.decl.dtype,
|
|
1127
|
+
itemDtype: resolved.decl.itemDtype,
|
|
1128
|
+
options: resolved.decl.options,
|
|
1129
|
+
origin: resolved.decl.origin,
|
|
1130
|
+
};
|
|
1131
|
+
tracked.set(id, {
|
|
1132
|
+
id,
|
|
1133
|
+
name: resolved.decl.name,
|
|
1134
|
+
spec: declared.spec,
|
|
1135
|
+
listSyntax: declared.listSyntax,
|
|
1136
|
+
handle: resolved.handle,
|
|
1137
|
+
companionDecl,
|
|
1138
|
+
companion: INVALID_INDEX as ColumnHandle,
|
|
1139
|
+
dynamic,
|
|
1140
|
+
valueDecl,
|
|
1141
|
+
table: INVALID_INDEX as ExtensionHandle,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// ---------------------------------------------------------------- nodes
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* A child of `<nodes>`.
|
|
1149
|
+
* @param name - the element name
|
|
1150
|
+
* @param local - its local name
|
|
1151
|
+
* @param attrs - its attributes
|
|
1152
|
+
* @param line - the line
|
|
1153
|
+
*/
|
|
1154
|
+
private startInNodes(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1155
|
+
if (local !== "node") {
|
|
1156
|
+
this.unknownElement(name, line);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
this.beginNode(attrs, line);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Push one `<node>`.
|
|
1164
|
+
* @param attrs - its attributes
|
|
1165
|
+
* @param line - the line
|
|
1166
|
+
*/
|
|
1167
|
+
private beginNode(attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1168
|
+
const { report, sink } = this;
|
|
1169
|
+
this.checkAbort();
|
|
1170
|
+
const enclosing = this.nodeStack.length === 0 ? INVALID_INDEX : this.nodeStack[this.nodeStack.length - 1].index;
|
|
1171
|
+
const idText = attrs.get("id");
|
|
1172
|
+
const where: IssueLocation = { line, element: idText ?? attrs.get("label") ?? null };
|
|
1173
|
+
this.checkAttributeNames(attrs, NODE_ATTRIBUTES, "node", where);
|
|
1174
|
+
if (idText === undefined) {
|
|
1175
|
+
report.error("missing-value", MISSING_ID_CODE, "<node> without an id", where);
|
|
1176
|
+
report.counts.skippedNodes++;
|
|
1177
|
+
this.nodeStack.push({ index: INVALID_INDEX, where });
|
|
1178
|
+
this.ctx.push(Ctx.Node);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
let index: number;
|
|
1182
|
+
try {
|
|
1183
|
+
const id = this.coerceId(idText, where);
|
|
1184
|
+
const existing = sink.indexOf(id);
|
|
1185
|
+
if (existing !== INVALID_INDEX) {
|
|
1186
|
+
report.warning(
|
|
1187
|
+
"validation-error",
|
|
1188
|
+
DUPLICATE_NODE_CODE,
|
|
1189
|
+
`node "${idText}" is declared more than once; later attributes overwrite`,
|
|
1190
|
+
where,
|
|
1191
|
+
);
|
|
1192
|
+
index = existing;
|
|
1193
|
+
} else {
|
|
1194
|
+
index = sink.addNode(id);
|
|
1195
|
+
report.counts.nodes++;
|
|
1196
|
+
}
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
report.recordError(err, where);
|
|
1199
|
+
report.counts.skippedNodes++;
|
|
1200
|
+
this.nodeStack.push({ index: INVALID_INDEX, where });
|
|
1201
|
+
this.ctx.push(Ctx.Node);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
this.nodeStack.push({ index, where });
|
|
1205
|
+
this.ctx.push(Ctx.Node);
|
|
1206
|
+
try {
|
|
1207
|
+
const label = attrs.get("label");
|
|
1208
|
+
if (label !== undefined) {
|
|
1209
|
+
this.nodeColumns.label.set(sink, report, index, label, where);
|
|
1210
|
+
}
|
|
1211
|
+
const pid = attrs.get("pid");
|
|
1212
|
+
if (pid !== undefined) {
|
|
1213
|
+
this.parentRefs.push({
|
|
1214
|
+
child: index,
|
|
1215
|
+
parentId: this.coerceId(pid, where),
|
|
1216
|
+
parentIndex: INVALID_INDEX,
|
|
1217
|
+
where,
|
|
1218
|
+
});
|
|
1219
|
+
} else if (enclosing !== INVALID_INDEX) {
|
|
1220
|
+
this.parentRefs.push({ child: index, parentId: null, parentIndex: enclosing, where });
|
|
1221
|
+
}
|
|
1222
|
+
this.writeLifetime(attrs, index, this.nodeColumns, where);
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
report.recordError(err, where);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* A child of `<node>`.
|
|
1230
|
+
* @param name - the element name
|
|
1231
|
+
* @param local - its local name
|
|
1232
|
+
* @param attrs - its attributes
|
|
1233
|
+
* @param line - the line
|
|
1234
|
+
*/
|
|
1235
|
+
private startInNode(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1236
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1237
|
+
if (frame.index === INVALID_INDEX && local !== "nodes") {
|
|
1238
|
+
// the node was skipped; its children are too
|
|
1239
|
+
this.ctx.push(Ctx.Skip);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
switch (local) {
|
|
1243
|
+
case "attvalues":
|
|
1244
|
+
this.ctx.push(Ctx.Attvalues);
|
|
1245
|
+
return;
|
|
1246
|
+
case "parents":
|
|
1247
|
+
this.parentIds = [];
|
|
1248
|
+
this.ctx.push(Ctx.Parents);
|
|
1249
|
+
return;
|
|
1250
|
+
case "spells":
|
|
1251
|
+
this.spellPairs = [];
|
|
1252
|
+
this.spellsWhere = frame.where;
|
|
1253
|
+
this.ctx.push(Ctx.Spells);
|
|
1254
|
+
return;
|
|
1255
|
+
case "nodes":
|
|
1256
|
+
this.reserve(attrs, "node", line);
|
|
1257
|
+
this.ctx.push(Ctx.Nodes);
|
|
1258
|
+
return;
|
|
1259
|
+
case "color":
|
|
1260
|
+
case "position":
|
|
1261
|
+
case "size":
|
|
1262
|
+
case "shape":
|
|
1263
|
+
this.writeViz(name, local, attrs, "node", frame.index, frame.where);
|
|
1264
|
+
this.ctx.push(Ctx.Skip);
|
|
1265
|
+
return;
|
|
1266
|
+
default:
|
|
1267
|
+
this.unknownElement(name, line);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* A `<parent for>` inside `<parents>`.
|
|
1273
|
+
* @param name - the element name
|
|
1274
|
+
* @param local - its local name
|
|
1275
|
+
* @param attrs - its attributes
|
|
1276
|
+
* @param line - the line
|
|
1277
|
+
*/
|
|
1278
|
+
private startInParents(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1279
|
+
if (local !== "parent") {
|
|
1280
|
+
this.unknownElement(name, line);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
this.ctx.push(Ctx.Skip);
|
|
1284
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1285
|
+
const text = attrs.get("for");
|
|
1286
|
+
if (text === undefined) {
|
|
1287
|
+
this.report.warning("missing-value", ATTVALUE_SHAPE_CODE, "<parent> without a for attribute", frame.where);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
try {
|
|
1291
|
+
this.parentIds?.push(this.coerceId(text, frame.where));
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
this.report.recordError(err, frame.where);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/** Close `<parents>`: queue the list for resolution. */
|
|
1298
|
+
private finishParents(): void {
|
|
1299
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1300
|
+
if (this.parentIds !== null && frame.index !== INVALID_INDEX) {
|
|
1301
|
+
this.parentsRefs.push({ child: frame.index, ids: this.parentIds, where: frame.where });
|
|
1302
|
+
}
|
|
1303
|
+
this.parentIds = null;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Resolve every `pid`, nested and `<parents>` reference read so far now that every node is
|
|
1308
|
+
* declared (design section 8.4); an unknown parent is a missing-value error.
|
|
1309
|
+
*/
|
|
1310
|
+
private resolveParents(): void {
|
|
1311
|
+
const { sink, report } = this;
|
|
1312
|
+
const refs = this.parentRefs;
|
|
1313
|
+
const lists = this.parentsRefs;
|
|
1314
|
+
this.parentRefs = [];
|
|
1315
|
+
this.parentsRefs = [];
|
|
1316
|
+
for (const ref of refs) {
|
|
1317
|
+
let parent = ref.parentIndex;
|
|
1318
|
+
if (ref.parentId !== null) {
|
|
1319
|
+
parent = sink.indexOf(ref.parentId);
|
|
1320
|
+
if (parent === INVALID_INDEX) {
|
|
1321
|
+
report.error(
|
|
1322
|
+
"missing-value",
|
|
1323
|
+
UNKNOWN_PARENT_CODE,
|
|
1324
|
+
`pid "${String(ref.parentId)}" names a node the document does not declare`,
|
|
1325
|
+
ref.where,
|
|
1326
|
+
);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
try {
|
|
1331
|
+
this.parentColumn.set(sink, report, ref.child, parent, ref.where);
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
report.recordError(err, ref.where);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
for (const ref of lists) {
|
|
1337
|
+
const indices: number[] = [];
|
|
1338
|
+
for (const id of ref.ids) {
|
|
1339
|
+
const parent = sink.indexOf(id);
|
|
1340
|
+
if (parent === INVALID_INDEX) {
|
|
1341
|
+
report.error(
|
|
1342
|
+
"missing-value",
|
|
1343
|
+
UNKNOWN_PARENT_CODE,
|
|
1344
|
+
`<parent for="${String(id)}"> names a node the document does not declare`,
|
|
1345
|
+
ref.where,
|
|
1346
|
+
);
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
indices.push(parent);
|
|
1350
|
+
}
|
|
1351
|
+
try {
|
|
1352
|
+
this.parentsColumn.set(sink, report, ref.child, indices, ref.where);
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
report.recordError(err, ref.where);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ---------------------------------------------------------------- edges
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* A child of `<edges>`.
|
|
1363
|
+
* @param name - the element name
|
|
1364
|
+
* @param local - its local name
|
|
1365
|
+
* @param attrs - its attributes
|
|
1366
|
+
* @param line - the line
|
|
1367
|
+
*/
|
|
1368
|
+
private startInEdges(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1369
|
+
if (local !== "edge") {
|
|
1370
|
+
this.unknownElement(name, line);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
this.beginEdge(attrs, line);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Push one `<edge>`.
|
|
1378
|
+
* @param attrs - its attributes
|
|
1379
|
+
* @param line - the line
|
|
1380
|
+
*/
|
|
1381
|
+
private beginEdge(attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1382
|
+
const { report, sink, options } = this;
|
|
1383
|
+
this.checkAbort();
|
|
1384
|
+
this.ctx.push(Ctx.Edge);
|
|
1385
|
+
this.edge = null;
|
|
1386
|
+
const sourceText = attrs.get("source");
|
|
1387
|
+
const targetText = attrs.get("target");
|
|
1388
|
+
const idText = attrs.get("id");
|
|
1389
|
+
const { where } = this.edgeFrame;
|
|
1390
|
+
where.line = line;
|
|
1391
|
+
where.element = idText ?? edgeElement(sourceText, targetText);
|
|
1392
|
+
this.checkAttributeNames(attrs, EDGE_ATTRIBUTES, "edge", where);
|
|
1393
|
+
if (sourceText === undefined || targetText === undefined) {
|
|
1394
|
+
report.error(
|
|
1395
|
+
"missing-value",
|
|
1396
|
+
MISSING_ENDPOINT_CODE,
|
|
1397
|
+
`<edge> without a ${sourceText === undefined ? "source" : "target"}`,
|
|
1398
|
+
where,
|
|
1399
|
+
);
|
|
1400
|
+
report.counts.skippedEdges++;
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
let kind: EdgeKind = this.defaultKind;
|
|
1404
|
+
const type = attrs.get("type");
|
|
1405
|
+
if (type !== undefined) {
|
|
1406
|
+
if (!EDGE_TYPES.has(type)) {
|
|
1407
|
+
report.error(
|
|
1408
|
+
"validation-error",
|
|
1409
|
+
EDGE_TYPE_CODE,
|
|
1410
|
+
`edge type "${type}" is not directed, undirected or mutual`,
|
|
1411
|
+
where,
|
|
1412
|
+
);
|
|
1413
|
+
report.counts.skippedEdges++;
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
kind = type as EdgeKind;
|
|
1417
|
+
}
|
|
1418
|
+
if (idText !== undefined) {
|
|
1419
|
+
if (this.edgeIds.has(idText)) {
|
|
1420
|
+
report.error(
|
|
1421
|
+
"validation-error",
|
|
1422
|
+
DUPLICATE_EDGE_ID_CODE,
|
|
1423
|
+
`edge id "${idText}" is declared more than once; the edge is skipped`,
|
|
1424
|
+
where,
|
|
1425
|
+
);
|
|
1426
|
+
report.counts.skippedEdges++;
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
this.edgeIds.add(idText);
|
|
1430
|
+
}
|
|
1431
|
+
let index: number;
|
|
1432
|
+
try {
|
|
1433
|
+
const source = this.coerceId(sourceText, where);
|
|
1434
|
+
const target = this.coerceId(targetText, where);
|
|
1435
|
+
if (!options.addMissingNodes) {
|
|
1436
|
+
if (sink.indexOf(source) === INVALID_INDEX) {
|
|
1437
|
+
throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${sourceText}"`, {
|
|
1438
|
+
id: source,
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
if (sink.indexOf(target) === INVALID_INDEX) {
|
|
1442
|
+
throw new GraphFormatError("E_UNKNOWN_NODE", `edge references undeclared node "${targetText}"`, {
|
|
1443
|
+
id: target,
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
const weight = this.xmlWeight(attrs, where);
|
|
1448
|
+
const before = sink.edgeCount;
|
|
1449
|
+
index = this.resolver.addEdge(source, target, kind, weight, where);
|
|
1450
|
+
report.counts.edges += sink.edgeCount - before;
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
report.recordError(err, where);
|
|
1453
|
+
report.counts.skippedEdges++;
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const frame = this.edgeFrame;
|
|
1457
|
+
frame.index = index;
|
|
1458
|
+
frame.mirror = this.resolver.lastMirror;
|
|
1459
|
+
this.edge = frame;
|
|
1460
|
+
try {
|
|
1461
|
+
if (idText !== undefined) {
|
|
1462
|
+
this.edgeIdColumn.set(sink, report, index, idText, where);
|
|
1463
|
+
}
|
|
1464
|
+
const label = attrs.get("label");
|
|
1465
|
+
if (label !== undefined) {
|
|
1466
|
+
this.edgeColumns.label.set(sink, report, index, label, where);
|
|
1467
|
+
}
|
|
1468
|
+
const edgeKind = attrs.get("kind");
|
|
1469
|
+
if (edgeKind !== undefined) {
|
|
1470
|
+
this.edgeKindColumn.set(sink, report, index, edgeKind, where);
|
|
1471
|
+
}
|
|
1472
|
+
this.writeLifetime(attrs, index, this.edgeColumns, where);
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
report.recordError(err, where);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* The weight argument of one edge from its `weight` XML attribute; undefined when absent (the
|
|
1480
|
+
* sink records a defaulted weight, design section 3.7).
|
|
1481
|
+
* @param attrs - the edge's attributes
|
|
1482
|
+
* @param where - the edge's location
|
|
1483
|
+
* @returns the weight, or undefined
|
|
1484
|
+
*/
|
|
1485
|
+
private xmlWeight(attrs: ReadonlyMap<string, string>, where: IssueLocation): number | undefined {
|
|
1486
|
+
const text = attrs.get("weight");
|
|
1487
|
+
if (text === undefined) {
|
|
1488
|
+
return undefined;
|
|
1489
|
+
}
|
|
1490
|
+
if (this.options.weightFrom === null) {
|
|
1491
|
+
this.report.warnOnce(
|
|
1492
|
+
"coercion",
|
|
1493
|
+
WEIGHT_IGNORED_CODE,
|
|
1494
|
+
"the weight attribute is ignored because weightFrom is null; edges are unweighted",
|
|
1495
|
+
where,
|
|
1496
|
+
);
|
|
1497
|
+
return undefined;
|
|
1498
|
+
}
|
|
1499
|
+
return parseWeightText(text);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* A child of `<edge>`.
|
|
1504
|
+
* @param name - the element name
|
|
1505
|
+
* @param local - its local name
|
|
1506
|
+
* @param attrs - its attributes
|
|
1507
|
+
* @param line - the line
|
|
1508
|
+
*/
|
|
1509
|
+
private startInEdge(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1510
|
+
const { edge } = this;
|
|
1511
|
+
if (edge === null) {
|
|
1512
|
+
this.ctx.push(Ctx.Skip);
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
switch (local) {
|
|
1516
|
+
case "attvalues":
|
|
1517
|
+
this.ctx.push(Ctx.Attvalues);
|
|
1518
|
+
return;
|
|
1519
|
+
case "spells":
|
|
1520
|
+
this.spellPairs = [];
|
|
1521
|
+
this.spellsWhere = edge.where;
|
|
1522
|
+
this.ctx.push(Ctx.Spells);
|
|
1523
|
+
return;
|
|
1524
|
+
case "color":
|
|
1525
|
+
case "thickness":
|
|
1526
|
+
case "shape":
|
|
1527
|
+
this.writeViz(name, local, attrs, "edge", edge.index, edge.where);
|
|
1528
|
+
this.ctx.push(Ctx.Skip);
|
|
1529
|
+
return;
|
|
1530
|
+
default:
|
|
1531
|
+
this.unknownElement(name, line);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// ---------------------------------------------------------------- attribute values
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* An `<attvalue>` of the open node or edge; a value that does not parse is recorded and skipped
|
|
1539
|
+
* without losing the element or its other values.
|
|
1540
|
+
* @param name - the element name
|
|
1541
|
+
* @param local - its local name
|
|
1542
|
+
* @param attrs - its attributes
|
|
1543
|
+
* @param line - the line
|
|
1544
|
+
*/
|
|
1545
|
+
private startInAttvalues(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1546
|
+
if (local !== "attvalue") {
|
|
1547
|
+
this.unknownElement(name, line);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
this.ctx.push(Ctx.Skip);
|
|
1551
|
+
const { report, edge } = this;
|
|
1552
|
+
const domain: "node" | "edge" = edge === null ? "node" : "edge";
|
|
1553
|
+
let index: number;
|
|
1554
|
+
let element: string | null;
|
|
1555
|
+
if (edge !== null) {
|
|
1556
|
+
({ index } = edge);
|
|
1557
|
+
element = edge.where.element ?? null;
|
|
1558
|
+
} else {
|
|
1559
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1560
|
+
({ index } = frame);
|
|
1561
|
+
element = frame.where.element ?? null;
|
|
1562
|
+
}
|
|
1563
|
+
if (index === INVALID_INDEX) {
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const forId = attrs.get("for");
|
|
1567
|
+
const { valueWhere } = this;
|
|
1568
|
+
valueWhere.line = line;
|
|
1569
|
+
valueWhere.element = element;
|
|
1570
|
+
if (forId === undefined) {
|
|
1571
|
+
report.warning("missing-value", ATTVALUE_SHAPE_CODE, "<attvalue> without a for attribute", valueWhere);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const value = attrs.get("value");
|
|
1575
|
+
if (value === undefined) {
|
|
1576
|
+
report.warning(
|
|
1577
|
+
"missing-value",
|
|
1578
|
+
ATTVALUE_SHAPE_CODE,
|
|
1579
|
+
`<attvalue for="${forId}"> without a value attribute`,
|
|
1580
|
+
valueWhere,
|
|
1581
|
+
);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
if (domain === "edge" && this.weightAttr !== null && this.weightAttr.id === forId) {
|
|
1586
|
+
if (isTimed(attrs)) {
|
|
1587
|
+
this.writeTimedWeight(attrs, value, index, valueWhere);
|
|
1588
|
+
} else {
|
|
1589
|
+
this.applyStaticWeight(value, valueWhere);
|
|
1590
|
+
}
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const tracked = domain === "node" ? this.nodeAttrs : this.edgeAttrs;
|
|
1594
|
+
const attribute = tracked.get(forId);
|
|
1595
|
+
if (attribute === undefined) {
|
|
1596
|
+
const key = `${domain}:${forId}`;
|
|
1597
|
+
if (!this.unknownAttributes.has(key)) {
|
|
1598
|
+
this.unknownAttributes.add(key);
|
|
1599
|
+
report.warning(
|
|
1600
|
+
"missing-value",
|
|
1601
|
+
UNKNOWN_ATTRIBUTE_CODE,
|
|
1602
|
+
`<attvalue for="${forId}"> names an undeclared ${domain} attribute; its values are dropped`,
|
|
1603
|
+
{ ...valueWhere, element: forId },
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (isTimed(attrs)) {
|
|
1609
|
+
this.writeTimedValue(attribute, domain, attrs, value, index, valueWhere);
|
|
1610
|
+
} else {
|
|
1611
|
+
this.writeStaticValue(attribute, domain, value, index, valueWhere);
|
|
1612
|
+
}
|
|
1613
|
+
} catch (err) {
|
|
1614
|
+
report.recordError(err, { ...valueWhere, element: `${element ?? "?"}[${forId}]` });
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* A static value of the attribute titled `weightFrom` overrides the XML weight of the open
|
|
1620
|
+
* edge (and of its mirror half, design section 3.6).
|
|
1621
|
+
* @param text - the value text
|
|
1622
|
+
* @param where - the value's location
|
|
1623
|
+
*/
|
|
1624
|
+
private applyStaticWeight(text: string, where: IssueLocation): void {
|
|
1625
|
+
const { edge, sink } = this;
|
|
1626
|
+
if (edge === null) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const weight = parseWeightText(text);
|
|
1630
|
+
if (weight === undefined) {
|
|
1631
|
+
throw new GraphFormatError("E_INVALID_WEIGHT", "an edge weight cannot be blank", {
|
|
1632
|
+
value: text,
|
|
1633
|
+
line: where.line ?? null,
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
sink.setEdgeWeight(edge.index, weight);
|
|
1637
|
+
if (edge.mirror !== INVALID_INDEX) {
|
|
1638
|
+
sink.setEdgeWeight(edge.mirror, weight);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* Parse a value by its declaration and write the cell (and the temporal companion, declared
|
|
1644
|
+
* on the first value whose text is not canonical, design section 5.1).
|
|
1645
|
+
* @param attribute - the declaration
|
|
1646
|
+
* @param domain - node or edge
|
|
1647
|
+
* @param text - the value text
|
|
1648
|
+
* @param index - the element's index
|
|
1649
|
+
* @param where - the value's location
|
|
1650
|
+
*/
|
|
1651
|
+
private writeStaticValue(
|
|
1652
|
+
attribute: TrackedAttribute,
|
|
1653
|
+
domain: "node" | "edge",
|
|
1654
|
+
text: string,
|
|
1655
|
+
index: number,
|
|
1656
|
+
where: IssueLocation,
|
|
1657
|
+
): void {
|
|
1658
|
+
const { spec } = attribute;
|
|
1659
|
+
if (spec.temporal !== null && !spec.list) {
|
|
1660
|
+
const parsed = parseDeclaredTemporal(text, spec);
|
|
1661
|
+
this.setCell(domain, attribute.handle, index, parsed.value);
|
|
1662
|
+
if (parsed.text !== null && attribute.companionDecl !== null) {
|
|
1663
|
+
if (attribute.companion === INVALID_INDEX) {
|
|
1664
|
+
attribute.companion = declareCompanion(this.sink, domain, attribute.companionDecl, this.report);
|
|
1665
|
+
}
|
|
1666
|
+
this.setCell(domain, attribute.companion, index, parsed.text);
|
|
1667
|
+
}
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
const value = parseDeclaredValue(text, spec, attribute.listSyntax);
|
|
1671
|
+
if (losesPrecision(spec, text)) {
|
|
1672
|
+
this.report.warning(
|
|
1673
|
+
"precision",
|
|
1674
|
+
PRECISION_CODE,
|
|
1675
|
+
`long value ${text.trim()} of "${attribute.name}" exceeds 2^53 and was rounded`,
|
|
1676
|
+
where,
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
this.setCell(domain, attribute.handle, index, value);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Append a timed value to the attribute's temporal table (design section 5.10), creating the
|
|
1684
|
+
* table on first use.
|
|
1685
|
+
* @param attribute - the declaration
|
|
1686
|
+
* @param domain - node or edge
|
|
1687
|
+
* @param attrs - the `<attvalue>` attributes carrying the bounds
|
|
1688
|
+
* @param text - the value text
|
|
1689
|
+
* @param index - the element's index
|
|
1690
|
+
* @param where - the value's location
|
|
1691
|
+
*/
|
|
1692
|
+
private writeTimedValue(
|
|
1693
|
+
attribute: TrackedAttribute,
|
|
1694
|
+
domain: "node" | "edge",
|
|
1695
|
+
attrs: ReadonlyMap<string, string>,
|
|
1696
|
+
text: string,
|
|
1697
|
+
index: number,
|
|
1698
|
+
where: IssueLocation,
|
|
1699
|
+
): void {
|
|
1700
|
+
const { sink, report } = this;
|
|
1701
|
+
if (!attribute.dynamic) {
|
|
1702
|
+
report.warnOnce(
|
|
1703
|
+
"validation-error",
|
|
1704
|
+
TIMED_STATIC_CODE,
|
|
1705
|
+
`attribute "${attribute.name}" is declared static but has timed values; they are stored as dynamic`,
|
|
1706
|
+
where,
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
const { spec } = attribute;
|
|
1710
|
+
const temporalValue = spec.temporal !== null && !spec.list;
|
|
1711
|
+
if (attribute.table === INVALID_INDEX) {
|
|
1712
|
+
attribute.table = sink.addExtensionTable(
|
|
1713
|
+
temporalTableName(domain, attribute.name),
|
|
1714
|
+
temporalTableDecls(domain, attribute.valueDecl, temporalValue),
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
let value: unknown;
|
|
1718
|
+
let valueText: string | null = null;
|
|
1719
|
+
if (temporalValue) {
|
|
1720
|
+
const parsed = parseDeclaredTemporal(text, spec);
|
|
1721
|
+
({ value } = parsed);
|
|
1722
|
+
valueText = parsed.text;
|
|
1723
|
+
} else {
|
|
1724
|
+
value = parseDeclaredValue(text, spec, attribute.listSyntax);
|
|
1725
|
+
if (losesPrecision(spec, text)) {
|
|
1726
|
+
report.warning(
|
|
1727
|
+
"precision",
|
|
1728
|
+
PRECISION_CODE,
|
|
1729
|
+
`long value ${text.trim()} of "${attribute.name}" exceeds 2^53 and was rounded`,
|
|
1730
|
+
where,
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const row = this.temporalRow(attrs, index, value, where);
|
|
1735
|
+
if (temporalValue) {
|
|
1736
|
+
row.push(valueText ?? undefined);
|
|
1737
|
+
}
|
|
1738
|
+
sink.addExtensionRow(attribute.table, row);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* Append a timed weight to the `temporal:edge:weight` table (design section 5.10).
|
|
1743
|
+
* @param attrs - the `<attvalue>` attributes carrying the bounds
|
|
1744
|
+
* @param text - the value text
|
|
1745
|
+
* @param index - the edge index
|
|
1746
|
+
* @param where - the value's location
|
|
1747
|
+
*/
|
|
1748
|
+
private writeTimedWeight(
|
|
1749
|
+
attrs: ReadonlyMap<string, string>,
|
|
1750
|
+
text: string,
|
|
1751
|
+
index: number,
|
|
1752
|
+
where: IssueLocation,
|
|
1753
|
+
): void {
|
|
1754
|
+
const { weightAttr } = this;
|
|
1755
|
+
if (weightAttr === null) {
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
const { sink } = this;
|
|
1759
|
+
if (weightAttr.table === INVALID_INDEX) {
|
|
1760
|
+
weightAttr.table = sink.addExtensionTable(
|
|
1761
|
+
temporalTableName("edge", this.options.weightFrom ?? "weight"),
|
|
1762
|
+
temporalTableDecls(
|
|
1763
|
+
"edge",
|
|
1764
|
+
{
|
|
1765
|
+
dtype: "f64",
|
|
1766
|
+
origin: { format: GEXF_FORMAT, id: weightAttr.id, type: weightAttr.spec.declared },
|
|
1767
|
+
},
|
|
1768
|
+
false,
|
|
1769
|
+
),
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
const value = parseWeightText(text);
|
|
1773
|
+
if (value === undefined) {
|
|
1774
|
+
throw new GraphFormatError("E_INVALID_WEIGHT", "a timed edge weight cannot be blank", {
|
|
1775
|
+
value: text,
|
|
1776
|
+
line: where.line ?? null,
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
sink.addExtensionRow(weightAttr.table, this.temporalRow(attrs, index, value, where));
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* The row of a temporal table for one timed value: element, bounds, value, bound texts, open bits.
|
|
1784
|
+
* @param attrs - the attributes carrying `start` / `end` / `timestamp` / `startopen` / `endopen`
|
|
1785
|
+
* @param index - the element's index
|
|
1786
|
+
* @param value - the parsed value
|
|
1787
|
+
* @param where - the value's location
|
|
1788
|
+
* @returns the row, without the optional value text
|
|
1789
|
+
*/
|
|
1790
|
+
private temporalRow(
|
|
1791
|
+
attrs: ReadonlyMap<string, string>,
|
|
1792
|
+
index: number,
|
|
1793
|
+
value: unknown,
|
|
1794
|
+
where: IssueLocation,
|
|
1795
|
+
): unknown[] {
|
|
1796
|
+
const bounds = this.readBounds(attrs, where);
|
|
1797
|
+
return [
|
|
1798
|
+
index,
|
|
1799
|
+
bounds.start.value,
|
|
1800
|
+
bounds.end.value,
|
|
1801
|
+
value,
|
|
1802
|
+
bounds.start.text ?? undefined,
|
|
1803
|
+
bounds.end.text ?? undefined,
|
|
1804
|
+
bounds.open === 0 ? undefined : bounds.open,
|
|
1805
|
+
];
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* The interval of an element or timed value: `timestamp`, or `start` / `startopen` and `end` /
|
|
1810
|
+
* `endopen` (GEXF 1.2: the open form carries the bound's time and marks the bound open, design
|
|
1811
|
+
* section 5.1); missing bounds are unbounded.
|
|
1812
|
+
* @param attrs - the attributes
|
|
1813
|
+
* @param where - the location, for the conflict warning
|
|
1814
|
+
* @returns the bounds and the open bits
|
|
1815
|
+
*/
|
|
1816
|
+
private readBounds(
|
|
1817
|
+
attrs: ReadonlyMap<string, string>,
|
|
1818
|
+
where: IssueLocation,
|
|
1819
|
+
): { start: TemporalValue; end: TemporalValue; open: number } {
|
|
1820
|
+
let start: TemporalValue = { value: -Infinity, text: null };
|
|
1821
|
+
let end: TemporalValue = { value: Infinity, text: null };
|
|
1822
|
+
let open = 0;
|
|
1823
|
+
const timestamp = attrs.get("timestamp");
|
|
1824
|
+
if (timestamp !== undefined) {
|
|
1825
|
+
start = parseTimeText(timestamp, this.timeFormat);
|
|
1826
|
+
end = start;
|
|
1827
|
+
}
|
|
1828
|
+
const startText = attrs.get("start");
|
|
1829
|
+
const startOpen = attrs.get("startopen");
|
|
1830
|
+
if (startText !== undefined) {
|
|
1831
|
+
start = parseTimeText(startText, this.timeFormat);
|
|
1832
|
+
if (startOpen !== undefined) {
|
|
1833
|
+
this.warnOpenConflict("start", where);
|
|
1834
|
+
}
|
|
1835
|
+
} else if (startOpen !== undefined) {
|
|
1836
|
+
start = parseTimeText(startOpen, this.timeFormat);
|
|
1837
|
+
open |= OPEN_START;
|
|
1838
|
+
}
|
|
1839
|
+
const endText = attrs.get("end");
|
|
1840
|
+
const endOpen = attrs.get("endopen");
|
|
1841
|
+
if (endText !== undefined) {
|
|
1842
|
+
end = parseTimeText(endText, this.timeFormat);
|
|
1843
|
+
if (endOpen !== undefined) {
|
|
1844
|
+
this.warnOpenConflict("end", where);
|
|
1845
|
+
}
|
|
1846
|
+
} else if (endOpen !== undefined) {
|
|
1847
|
+
end = parseTimeText(endOpen, this.timeFormat);
|
|
1848
|
+
open |= OPEN_END;
|
|
1849
|
+
}
|
|
1850
|
+
return { start, end, open };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Record a `start` next to a `startopen` (or `end` next to `endopen`), which the 1.2 primer forbids.
|
|
1855
|
+
* @param bound - which bound
|
|
1856
|
+
* @param where - the location
|
|
1857
|
+
*/
|
|
1858
|
+
private warnOpenConflict(bound: "start" | "end", where: IssueLocation): void {
|
|
1859
|
+
this.report.warnOnce(
|
|
1860
|
+
"validation-error",
|
|
1861
|
+
OPEN_BOUND_CONFLICT_CODE,
|
|
1862
|
+
`both ${bound} and ${bound}open are given; the closed bound is kept`,
|
|
1863
|
+
where,
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// ---------------------------------------------------------------- lifetimes
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Write an element's lifetime: `start` / `end` / `timestamp` (with text companions), the 1.2
|
|
1871
|
+
* `startopen` / `endopen` bounds and bits, and the 1.3 `timestamps` / `intervals` attributes.
|
|
1872
|
+
* @param attrs - the node or edge attributes
|
|
1873
|
+
* @param index - its index
|
|
1874
|
+
* @param columns - the domain's columns
|
|
1875
|
+
* @param where - the element's location
|
|
1876
|
+
*/
|
|
1877
|
+
private writeLifetime(
|
|
1878
|
+
attrs: ReadonlyMap<string, string>,
|
|
1879
|
+
index: number,
|
|
1880
|
+
columns: DomainColumns,
|
|
1881
|
+
where: IssueLocation,
|
|
1882
|
+
): void {
|
|
1883
|
+
const { sink, report } = this;
|
|
1884
|
+
if (
|
|
1885
|
+
attrs.has("start") ||
|
|
1886
|
+
attrs.has("end") ||
|
|
1887
|
+
attrs.has("timestamp") ||
|
|
1888
|
+
attrs.has("startopen") ||
|
|
1889
|
+
attrs.has("endopen")
|
|
1890
|
+
) {
|
|
1891
|
+
const bounds = this.readBounds(attrs, where);
|
|
1892
|
+
const timestamp = attrs.get("timestamp");
|
|
1893
|
+
if (timestamp !== undefined) {
|
|
1894
|
+
columns.timestamp.set(sink, report, index, bounds.start, where);
|
|
1895
|
+
}
|
|
1896
|
+
if (attrs.has("start") || attrs.has("startopen")) {
|
|
1897
|
+
columns.start.set(sink, report, index, bounds.start, where);
|
|
1898
|
+
}
|
|
1899
|
+
if (attrs.has("end") || attrs.has("endopen")) {
|
|
1900
|
+
columns.end.set(sink, report, index, bounds.end, where);
|
|
1901
|
+
}
|
|
1902
|
+
if (bounds.open !== 0) {
|
|
1903
|
+
columns.open.set(sink, report, index, bounds.open, where);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
const timestamps = attrs.get("timestamps");
|
|
1907
|
+
if (timestamps !== undefined) {
|
|
1908
|
+
const items = splitListText(stripAngles(timestamps), "brackets");
|
|
1909
|
+
columns.timestamps.set(
|
|
1910
|
+
sink,
|
|
1911
|
+
report,
|
|
1912
|
+
index,
|
|
1913
|
+
items.map((item) => parseTimeText(item, this.timeFormat).value),
|
|
1914
|
+
where,
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
const intervals = attrs.get("intervals");
|
|
1918
|
+
if (intervals !== undefined) {
|
|
1919
|
+
columns.spells.set(sink, report, index, this.parseIntervals(intervals, where), where);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Parse a 1.3 `intervals="<[s, e]; [s, e]>"` attribute into spell pairs.
|
|
1925
|
+
* @param text - the attribute text
|
|
1926
|
+
* @param where - the element's location, for the error
|
|
1927
|
+
* @returns the pairs
|
|
1928
|
+
*/
|
|
1929
|
+
private parseIntervals(text: string, where: IssueLocation): number[][] {
|
|
1930
|
+
const pairs: number[][] = [];
|
|
1931
|
+
for (const part of stripAngles(text).split(";")) {
|
|
1932
|
+
if (part.trim().length === 0) {
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
const items = splitListText(part, "brackets");
|
|
1936
|
+
if (items.length !== 2) {
|
|
1937
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `interval "${part.trim()}" is not a [start, end] pair`, {
|
|
1938
|
+
value: part,
|
|
1939
|
+
line: where.line ?? null,
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
pairs.push([
|
|
1943
|
+
parseTimeText(items[0], this.timeFormat).value,
|
|
1944
|
+
parseTimeText(items[1], this.timeFormat).value,
|
|
1945
|
+
]);
|
|
1946
|
+
}
|
|
1947
|
+
return pairs;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/**
|
|
1951
|
+
* A `<spell>` inside `<spells>`.
|
|
1952
|
+
* @param name - the element name
|
|
1953
|
+
* @param local - its local name
|
|
1954
|
+
* @param attrs - its attributes
|
|
1955
|
+
* @param line - the line
|
|
1956
|
+
*/
|
|
1957
|
+
private startInSpells(name: string, local: string, attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1958
|
+
if (local !== "spell") {
|
|
1959
|
+
this.unknownElement(name, line);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
this.ctx.push(Ctx.Skip);
|
|
1963
|
+
const where = this.spellsWhere ?? { line };
|
|
1964
|
+
try {
|
|
1965
|
+
const bounds = this.readBounds(attrs, where);
|
|
1966
|
+
if (bounds.open !== 0) {
|
|
1967
|
+
this.report.warnOnce(
|
|
1968
|
+
"unsupported",
|
|
1969
|
+
SPELL_OPEN_CODE,
|
|
1970
|
+
"startopen / endopen on a <spell> cannot be kept; the spell is stored closed",
|
|
1971
|
+
where,
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
this.spellPairs?.push([bounds.start.value, bounds.end.value]);
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
this.report.recordError(err, where);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
/** Close `<spells>`: write the pairs into the open element's spells column. */
|
|
1981
|
+
private finishSpells(): void {
|
|
1982
|
+
const pairs = this.spellPairs;
|
|
1983
|
+
const where = this.spellsWhere ?? undefined;
|
|
1984
|
+
this.spellPairs = null;
|
|
1985
|
+
this.spellsWhere = null;
|
|
1986
|
+
if (pairs === null) {
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
try {
|
|
1990
|
+
if (this.edge !== null) {
|
|
1991
|
+
this.edgeColumns.spells.set(this.sink, this.report, this.edge.index, pairs, where);
|
|
1992
|
+
} else {
|
|
1993
|
+
const frame = this.nodeStack[this.nodeStack.length - 1];
|
|
1994
|
+
if (frame.index !== INVALID_INDEX) {
|
|
1995
|
+
this.nodeColumns.spells.set(this.sink, this.report, frame.index, pairs, where);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
this.report.recordError(err, where);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// ---------------------------------------------------------------- viz
|
|
2004
|
+
|
|
2005
|
+
/**
|
|
2006
|
+
* Write one viz element into its role column; a value that does not parse is a recorded
|
|
2007
|
+
* warning and the element is skipped.
|
|
2008
|
+
* @param name - the element name as written
|
|
2009
|
+
* @param local - its local name
|
|
2010
|
+
* @param attrs - its attributes
|
|
2011
|
+
* @param domain - node or edge
|
|
2012
|
+
* @param index - the element's index
|
|
2013
|
+
* @param where - the element's location
|
|
2014
|
+
*/
|
|
2015
|
+
private writeViz(
|
|
2016
|
+
name: string,
|
|
2017
|
+
local: string,
|
|
2018
|
+
attrs: ReadonlyMap<string, string>,
|
|
2019
|
+
domain: "node" | "edge",
|
|
2020
|
+
index: number,
|
|
2021
|
+
where: IssueLocation,
|
|
2022
|
+
): void {
|
|
2023
|
+
const { sink, report } = this;
|
|
2024
|
+
if (!this.viz) {
|
|
2025
|
+
report.warnOnce("unsupported", VIZ_SKIPPED_CODE, "viz elements are skipped (option viz: false)", where);
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
if (isTimed(attrs)) {
|
|
2029
|
+
report.warnOnce(
|
|
2030
|
+
"unsupported",
|
|
2031
|
+
VIZ_DYNAMIC_CODE,
|
|
2032
|
+
"dynamic viz elements are not supported; the bounds are dropped and the last value kept",
|
|
2033
|
+
where,
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
const columns = domain === "node" ? this.nodeColumns : this.edgeColumns;
|
|
2037
|
+
try {
|
|
2038
|
+
switch (local) {
|
|
2039
|
+
case "color":
|
|
2040
|
+
columns.color.set(sink, report, index, parseColor(attrs), where);
|
|
2041
|
+
break;
|
|
2042
|
+
case "position": {
|
|
2043
|
+
const x = vizNumber(attrs, "x");
|
|
2044
|
+
const y = vizNumber(attrs, "y");
|
|
2045
|
+
const z = attrs.has("z") ? vizNumber(attrs, "z") : 0;
|
|
2046
|
+
this.positionColumn ??= new LazyColumn("node", {
|
|
2047
|
+
...NODE_DECLS.position,
|
|
2048
|
+
extra: { ...NODE_DECLS.position.extra, sourceDims: attrs.has("z") ? 3 : 2 },
|
|
2049
|
+
});
|
|
2050
|
+
this.positionColumn.set(sink, report, index, [x, y, z], where);
|
|
2051
|
+
break;
|
|
2052
|
+
}
|
|
2053
|
+
case "size":
|
|
2054
|
+
this.sizeColumn.set(sink, report, index, vizNumber(attrs, "value"), where);
|
|
2055
|
+
break;
|
|
2056
|
+
case "thickness":
|
|
2057
|
+
this.thicknessColumn.set(sink, report, index, vizNumber(attrs, "value"), where);
|
|
2058
|
+
break;
|
|
2059
|
+
case "shape": {
|
|
2060
|
+
const value = attrs.get("value");
|
|
2061
|
+
if (value === undefined) {
|
|
2062
|
+
throw new GraphFormatError("E_COLUMN_TYPE", "<viz:shape> without a value", { element: name });
|
|
2063
|
+
}
|
|
2064
|
+
columns.shape.set(sink, report, index, value, where);
|
|
2065
|
+
const uri = attrs.get("uri");
|
|
2066
|
+
if (domain === "node" && uri !== undefined) {
|
|
2067
|
+
this.shapeUriColumn.set(sink, report, index, uri, where);
|
|
2068
|
+
}
|
|
2069
|
+
break;
|
|
2070
|
+
}
|
|
2071
|
+
default:
|
|
2072
|
+
break;
|
|
2073
|
+
}
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
if (!(err instanceof GraphFormatError) || err.code !== "E_COLUMN_TYPE") {
|
|
2076
|
+
throw err;
|
|
2077
|
+
}
|
|
2078
|
+
report.warning("validation-error", VIZ_VALUE_CODE, `<${name}> skipped: ${err.message}`, where);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// ---------------------------------------------------------------- helpers
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* Report an element the schema does not define at this place and skip its subtree.
|
|
2086
|
+
* @param name - the element name
|
|
2087
|
+
* @param line - the line
|
|
2088
|
+
*/
|
|
2089
|
+
private unknownElement(name: string, line: number): void {
|
|
2090
|
+
this.report.warnOnce("unsupported", UNKNOWN_ELEMENT_CODE, `element <${name}> is not GEXF and was skipped`, {
|
|
2091
|
+
line,
|
|
2092
|
+
element: name,
|
|
2093
|
+
});
|
|
2094
|
+
this.ctx.push(Ctx.Skip);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Report XML attributes the importer does not read (once per attribute name and domain).
|
|
2099
|
+
* @param attrs - the element's attributes
|
|
2100
|
+
* @param known - the attribute names read
|
|
2101
|
+
* @param domain - node or edge
|
|
2102
|
+
* @param where - the element's location
|
|
2103
|
+
*/
|
|
2104
|
+
private checkAttributeNames(
|
|
2105
|
+
attrs: ReadonlyMap<string, string>,
|
|
2106
|
+
known: ReadonlySet<string>,
|
|
2107
|
+
domain: "node" | "edge",
|
|
2108
|
+
where: IssueLocation,
|
|
2109
|
+
): void {
|
|
2110
|
+
for (const key of attrs.keys()) {
|
|
2111
|
+
if (!known.has(key)) {
|
|
2112
|
+
const seen = `${domain}@${key}`;
|
|
2113
|
+
if (!this.unknownAttributes.has(seen)) {
|
|
2114
|
+
this.unknownAttributes.add(seen);
|
|
2115
|
+
this.report.warning(
|
|
2116
|
+
"unsupported",
|
|
2117
|
+
UNKNOWN_ELEMENT_CODE,
|
|
2118
|
+
`attribute ${key} of <${domain}> is not GEXF and was ignored`,
|
|
2119
|
+
{ ...where, element: key },
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
/**
|
|
2127
|
+
* Write one cell of a domain.
|
|
2128
|
+
* @param domain - node or edge
|
|
2129
|
+
* @param handle - the column
|
|
2130
|
+
* @param index - the row
|
|
2131
|
+
* @param value - the value
|
|
2132
|
+
*/
|
|
2133
|
+
private setCell(domain: "node" | "edge", handle: ColumnHandle, index: number, value: unknown): void {
|
|
2134
|
+
if (domain === "node") {
|
|
2135
|
+
this.sink.setNodeValue(handle, index, value);
|
|
2136
|
+
} else {
|
|
2137
|
+
this.sink.setEdgeValue(handle, index, value);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
/**
|
|
2142
|
+
* Coerce an id text, recording a merge under ids "number".
|
|
2143
|
+
* @param text - the id text
|
|
2144
|
+
* @param where - the location, for the merge warning
|
|
2145
|
+
* @returns the id
|
|
2146
|
+
*/
|
|
2147
|
+
private coerceId(text: string, where: IssueLocation): NodeId {
|
|
2148
|
+
const id = this.coercer.text(text);
|
|
2149
|
+
const merge = this.coercer.lastMerge;
|
|
2150
|
+
if (merge !== null) {
|
|
2151
|
+
this.report.warning(
|
|
2152
|
+
"coercion",
|
|
2153
|
+
ID_MERGED_CODE,
|
|
2154
|
+
`id text "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids "number"`,
|
|
2155
|
+
where,
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
return id;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
/** Check the cancellation signal every ABORT_CHECK_INTERVAL elements. */
|
|
2162
|
+
private checkAbort(): void {
|
|
2163
|
+
if (++this.elementsSinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
2164
|
+
this.elementsSinceCheck = 0;
|
|
2165
|
+
throwIfAborted(this.options.signal);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/**
|
|
2171
|
+
* The element text of an edge without an id, for issues.
|
|
2172
|
+
* @param source - the source text, or undefined
|
|
2173
|
+
* @param target - the target text, or undefined
|
|
2174
|
+
* @returns `source->target`
|
|
2175
|
+
*/
|
|
2176
|
+
function edgeElement(source: string | undefined, target: string | undefined): string {
|
|
2177
|
+
return `${source ?? "?"}->${target ?? "?"}`;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
/**
|
|
2181
|
+
* Whether an element carries time bounds.
|
|
2182
|
+
* @param attrs - the element's attributes
|
|
2183
|
+
* @returns true when start, end, timestamp or an open bound is present
|
|
2184
|
+
*/
|
|
2185
|
+
function isTimed(attrs: ReadonlyMap<string, string>): boolean {
|
|
2186
|
+
return (
|
|
2187
|
+
attrs.has("start") ||
|
|
2188
|
+
attrs.has("end") ||
|
|
2189
|
+
attrs.has("timestamp") ||
|
|
2190
|
+
attrs.has("startopen") ||
|
|
2191
|
+
attrs.has("endopen")
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Remove the `<` `>` wrapper of a 1.3 `timestamps` / `intervals` attribute value.
|
|
2197
|
+
* @param text - the attribute text
|
|
2198
|
+
* @returns the inner text
|
|
2199
|
+
*/
|
|
2200
|
+
function stripAngles(text: string): string {
|
|
2201
|
+
let inner = text.trim();
|
|
2202
|
+
if (inner.startsWith("<")) {
|
|
2203
|
+
inner = inner.slice(1);
|
|
2204
|
+
}
|
|
2205
|
+
if (inner.endsWith(">")) {
|
|
2206
|
+
inner = inner.slice(0, -1);
|
|
2207
|
+
}
|
|
2208
|
+
return inner;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/**
|
|
2212
|
+
* A numeric viz attribute.
|
|
2213
|
+
* @param attrs - the element's attributes
|
|
2214
|
+
* @param name - the attribute name
|
|
2215
|
+
* @returns the number; E_COLUMN_TYPE when absent or not a number
|
|
2216
|
+
*/
|
|
2217
|
+
function vizNumber(attrs: ReadonlyMap<string, string>, name: string): number {
|
|
2218
|
+
const text = attrs.get(name);
|
|
2219
|
+
if (text === undefined) {
|
|
2220
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `viz element without a ${name} attribute`, { attribute: name });
|
|
2221
|
+
}
|
|
2222
|
+
const value = parseDecimalText(text);
|
|
2223
|
+
if (Number.isNaN(value)) {
|
|
2224
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `viz ${name}="${text}" is not a number`, { attribute: name });
|
|
2225
|
+
}
|
|
2226
|
+
return value;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
/**
|
|
2230
|
+
* Parse `<viz:color r g b a hex>` into rgba in 0..1 (design section 5.5: role color, f32 x4).
|
|
2231
|
+
* @param attrs - the element's attributes
|
|
2232
|
+
* @returns [r, g, b, a]
|
|
2233
|
+
*/
|
|
2234
|
+
function parseColor(attrs: ReadonlyMap<string, string>): number[] {
|
|
2235
|
+
let r: number;
|
|
2236
|
+
let g: number;
|
|
2237
|
+
let b: number;
|
|
2238
|
+
const hex = attrs.get("hex");
|
|
2239
|
+
if (hex !== undefined && !attrs.has("r")) {
|
|
2240
|
+
const digits = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
2241
|
+
if (!/^[0-9a-fA-F]{6}$/.test(digits)) {
|
|
2242
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `viz hex="${hex}" is not a #RRGGBB colour`, {
|
|
2243
|
+
attribute: "hex",
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
r = Number.parseInt(digits.slice(0, 2), 16);
|
|
2247
|
+
g = Number.parseInt(digits.slice(2, 4), 16);
|
|
2248
|
+
b = Number.parseInt(digits.slice(4, 6), 16);
|
|
2249
|
+
} else {
|
|
2250
|
+
r = vizNumber(attrs, "r");
|
|
2251
|
+
g = vizNumber(attrs, "g");
|
|
2252
|
+
b = vizNumber(attrs, "b");
|
|
2253
|
+
}
|
|
2254
|
+
const a = attrs.has("a") ? vizNumber(attrs, "a") : 1;
|
|
2255
|
+
return [r / 255, g / 255, b / 255, a];
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
/**
|
|
2259
|
+
* Confidence that a document head is GEXF.
|
|
2260
|
+
* @param head - the first bytes
|
|
2261
|
+
* @returns 1 for a `<gexf` tag, 0.8 for a gexf.net namespace, 0 otherwise
|
|
2262
|
+
*/
|
|
2263
|
+
function sniffGexf(head: Uint8Array): number {
|
|
2264
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(head);
|
|
2265
|
+
if (/<(\w+:)?gexf[\s>]/.test(text)) {
|
|
2266
|
+
return 1;
|
|
2267
|
+
}
|
|
2268
|
+
return text.includes("gexf.net") ? 0.8 : 0;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
/**
|
|
2272
|
+
* Validate the format-specific options.
|
|
2273
|
+
* @param options - the caller's options
|
|
2274
|
+
* @returns whether viz elements are imported
|
|
2275
|
+
*/
|
|
2276
|
+
function resolveGexfOptions(options: (GexfImportOptions & CommonImportOptions) | undefined): boolean {
|
|
2277
|
+
const viz = options?.viz;
|
|
2278
|
+
if (viz === undefined) {
|
|
2279
|
+
return true;
|
|
2280
|
+
}
|
|
2281
|
+
if (typeof viz !== "boolean") {
|
|
2282
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option viz: ${typeof viz} is not a boolean`, {
|
|
2283
|
+
option: "viz",
|
|
2284
|
+
found: viz,
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
return viz;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
/** The GEXF importer (design section 8.4). */
|
|
2291
|
+
export const gexfImporter: GraphImporter<GexfImportOptions> = Object.freeze({
|
|
2292
|
+
format: GEXF_FORMAT,
|
|
2293
|
+
extensions: Object.freeze([".gexf"]),
|
|
2294
|
+
mimeTypes: Object.freeze(["application/gexf+xml", "application/xml", "text/xml"]),
|
|
2295
|
+
sniff: sniffGexf,
|
|
2296
|
+
/**
|
|
2297
|
+
* Read a GEXF document into the sink in one streaming pass.
|
|
2298
|
+
* @param input - the document as text, bytes, a stream or chunks
|
|
2299
|
+
* @param sink - the sink
|
|
2300
|
+
* @param options - format-specific and common options
|
|
2301
|
+
* @returns the report
|
|
2302
|
+
*/
|
|
2303
|
+
async import(
|
|
2304
|
+
input: ImportInput,
|
|
2305
|
+
sink: GraphSink,
|
|
2306
|
+
options?: GexfImportOptions & CommonImportOptions,
|
|
2307
|
+
): Promise<ImportReport> {
|
|
2308
|
+
const resolved = resolveImportOptions(options, {
|
|
2309
|
+
ids: "canonical",
|
|
2310
|
+
defaultDirected: false,
|
|
2311
|
+
weightFrom: "weight",
|
|
2312
|
+
addMissingNodes: false,
|
|
2313
|
+
});
|
|
2314
|
+
const viz = resolveGexfOptions(options);
|
|
2315
|
+
const report = new ImportReportBuilder(GEXF_FORMAT, resolved.errorLimit);
|
|
2316
|
+
reportSinkOptions(sink, options, report, true);
|
|
2317
|
+
// nodeIdFrom is among the options reportUnusedOptions() reports: GEXF ids are mandatory
|
|
2318
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
2319
|
+
const reader = new GexfReader(sink, report, resolved, viz);
|
|
2320
|
+
try {
|
|
2321
|
+
await tokenizeXml(textChunks(input, report, resolved), reader);
|
|
2322
|
+
} catch (err) {
|
|
2323
|
+
if (err instanceof XmlSyntaxError) {
|
|
2324
|
+
report.fail(XML_SYNTAX_CODE, err.message, { line: err.line });
|
|
2325
|
+
}
|
|
2326
|
+
throw err;
|
|
2327
|
+
}
|
|
2328
|
+
reader.finish();
|
|
2329
|
+
// an abort raised during the last few elements (after the last periodic check) still rejects
|
|
2330
|
+
throwIfAborted(resolved.signal);
|
|
2331
|
+
return report.finish();
|
|
2332
|
+
},
|
|
2333
|
+
});
|