@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,1591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GML importer (design sections 8.4 and 8.5, research note 07 section 2.2): reads a whole GML
|
|
3
|
+
* text (GML awaits the whole text, 8.4), lexes it once into a typed token list, derives the column
|
|
4
|
+
* schema of every node, edge and graph key in a first walk (GML types are lexical per value, so
|
|
5
|
+
* the column dtype is the union of the value classes seen: `int` -> i32, `real` -> f64, `string`
|
|
6
|
+
* -> string, a nested `[ ]` record -> json, a repeated key -> list; the class is recorded in
|
|
7
|
+
* `origin.type` per column as 8.5 requires), then pushes scalars into the sink in a second walk:
|
|
8
|
+
* every node block first, then every edge block, so node indices follow the file's node order
|
|
9
|
+
* and forward edge references resolve.
|
|
10
|
+
*
|
|
11
|
+
* Conventions honoured: NetworkX's `_networkx_list_start` marker and `"[]"` empty lists, `&#NN;`
|
|
12
|
+
* character references in strings, `Creator` / `Version` top-level metadata, `directed 0|1` and
|
|
13
|
+
* `multigraph 0|1` graph flags, comments, keys and values on one line or split across lines, and
|
|
14
|
+
* a `graphics [ x y z ]` node record mapped to the `position` role (note 07 section 9) with the
|
|
15
|
+
* remaining graphics keys kept as a json column.
|
|
16
|
+
*
|
|
17
|
+
* Node ids are the integer `id` keys (the GML spec type; a non-integer id is a validation error,
|
|
18
|
+
* never a silent string) under `nodeIdFrom: "id"`, the `label` under `"label"`, and the node's
|
|
19
|
+
* ordinal under `"index"`; `source` / `target` always resolve through the integer ids. A node's
|
|
20
|
+
* `graphty_originalId` key written by the exporter's `sanitizeIds: "mangle"` restores the
|
|
21
|
+
* original id under `restoreMangledIds` (the default).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
type ColumnDecl,
|
|
26
|
+
type ColumnHandle,
|
|
27
|
+
type ColumnRole,
|
|
28
|
+
type Dtype,
|
|
29
|
+
GraphFormatError,
|
|
30
|
+
type GraphSink,
|
|
31
|
+
INVALID_INDEX,
|
|
32
|
+
type NodeId,
|
|
33
|
+
type ScalarDtype,
|
|
34
|
+
} from "@graphty/graph-format";
|
|
35
|
+
|
|
36
|
+
import { declareResolved, DictHeuristic, uniqueColumnName } from "../../common/attributes.js";
|
|
37
|
+
import {
|
|
38
|
+
COLUMN_RENAMED_CODE,
|
|
39
|
+
DUPLICATE_NODE_CODE as SHARED_DUPLICATE_NODE_CODE,
|
|
40
|
+
MISSING_ENDPOINT_CODE as SHARED_MISSING_ENDPOINT_CODE,
|
|
41
|
+
MISSING_ID_CODE as SHARED_MISSING_ID_CODE,
|
|
42
|
+
NO_GRAPH_CODE as SHARED_NO_GRAPH_CODE,
|
|
43
|
+
PRECISION_CODE as SHARED_PRECISION_CODE,
|
|
44
|
+
ROLE_TAKEN_CODE as SHARED_ROLE_TAKEN_CODE,
|
|
45
|
+
} from "../../common/codes.js";
|
|
46
|
+
import { DirectionResolver } from "../../common/direction.js";
|
|
47
|
+
import { IdCoercer } from "../../common/ids.js";
|
|
48
|
+
import { readText, throwIfAborted } from "../../common/input.js";
|
|
49
|
+
import {
|
|
50
|
+
type ImportFormatDefaults,
|
|
51
|
+
reportSinkOptions,
|
|
52
|
+
reportUnusedOptions,
|
|
53
|
+
type ResolvedImportOptions,
|
|
54
|
+
resolveImportOptions,
|
|
55
|
+
} from "../../common/options.js";
|
|
56
|
+
import { ImportReportBuilder } from "../../common/report.js";
|
|
57
|
+
import { parseWeightText, weightFromValue } from "../../common/weights.js";
|
|
58
|
+
import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
|
|
59
|
+
import {
|
|
60
|
+
EMPTY_LIST_TEXT,
|
|
61
|
+
EMPTY_TUPLE_TEXT,
|
|
62
|
+
GmlSyntaxError,
|
|
63
|
+
type GmlTokens,
|
|
64
|
+
LIST_START_MARKER,
|
|
65
|
+
numberOfText,
|
|
66
|
+
ORIGINAL_ID_KEY,
|
|
67
|
+
parseRecord,
|
|
68
|
+
scalarOrRecord,
|
|
69
|
+
TOKEN_INT,
|
|
70
|
+
TOKEN_OPEN,
|
|
71
|
+
TOKEN_REAL,
|
|
72
|
+
TOKEN_STRING,
|
|
73
|
+
TOKEN_WORD,
|
|
74
|
+
tokenizeGml,
|
|
75
|
+
} from "./syntax.js";
|
|
76
|
+
|
|
77
|
+
/** The format-specific options of the GML importer. */
|
|
78
|
+
export interface GmlImportOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Map a node's `graphics [ x y z ]` record to a `position` column with the position role
|
|
81
|
+
* (default true); false keeps the whole record in the `graphics` json column.
|
|
82
|
+
*/
|
|
83
|
+
positions?: boolean | undefined;
|
|
84
|
+
/** Apply the dictionary heuristic of design section 5.4 to string columns (default true). */
|
|
85
|
+
dictionaries?: boolean | undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Issue code: the text holds no `graph [ ... ]` block. */
|
|
89
|
+
export const NO_GRAPH_CODE = SHARED_NO_GRAPH_CODE;
|
|
90
|
+
/** Issue code: the text holds more than one `graph [ ... ]` block (fatal; one network per file). */
|
|
91
|
+
export const SECOND_GRAPH_CODE = "E_GML_SECOND_GRAPH";
|
|
92
|
+
/** Issue code: a node block has no `id` key. */
|
|
93
|
+
export const MISSING_ID_CODE = SHARED_MISSING_ID_CODE;
|
|
94
|
+
/** Issue code: a node block has no `label` key under `nodeIdFrom: "label"`. */
|
|
95
|
+
export const MISSING_LABEL_CODE = "E_GML_MISSING_LABEL";
|
|
96
|
+
/** Issue code: an edge block has no `source` or no `target` key. */
|
|
97
|
+
export const MISSING_ENDPOINT_CODE = SHARED_MISSING_ENDPOINT_CODE;
|
|
98
|
+
/** Issue code: an `id`, `source` or `target` value is not an integer. */
|
|
99
|
+
export const ID_TYPE_CODE = "E_GML_ID_TYPE";
|
|
100
|
+
/** Issue code: a node id (or label under `nodeIdFrom: "label"`) is declared twice; later keys overwrite. */
|
|
101
|
+
export const DUPLICATE_NODE_CODE = SHARED_DUPLICATE_NODE_CODE;
|
|
102
|
+
/** Issue code: a structural key (`id`, `source`, `target`) appears twice in one block. */
|
|
103
|
+
export const REPEATED_KEY_CODE = "E_GML_REPEATED_KEY";
|
|
104
|
+
/** Issue code: a `node` or `edge` key whose value is not a `[ ... ]` block. */
|
|
105
|
+
export const ELEMENT_TYPE_CODE = "E_GML_ELEMENT_TYPE";
|
|
106
|
+
/** Issue code: a `directed` or `multigraph` flag that is not an integer. */
|
|
107
|
+
export const FLAG_TYPE_CODE = "E_GML_FLAG_TYPE";
|
|
108
|
+
/** Issue code: a `directed` / `multigraph` flag that is not 0 or 1 (read as its truth value), or one repeated. */
|
|
109
|
+
export const FLAG_VALUE_CODE = "W_GML_FLAG_VALUE";
|
|
110
|
+
/** Issue code: an integer beyond 2^53 stored as the nearest f64 (design section 5.1). */
|
|
111
|
+
export const PRECISION_CODE = SHARED_PRECISION_CODE;
|
|
112
|
+
/** Issue code: the sink already holds a column of the name with another declaration; renamed `<name>#<key>`. */
|
|
113
|
+
export const COLUMN_RENAMED_CODE_GML = COLUMN_RENAMED_CODE;
|
|
114
|
+
/** Issue code: the sink already holds a column with the role; the column is declared without it. */
|
|
115
|
+
export const ROLE_TAKEN_CODE = SHARED_ROLE_TAKEN_CODE;
|
|
116
|
+
/** Loss code: under `nodeIdFrom` "label" / "index" the integer `id` keys are not kept. */
|
|
117
|
+
export const ID_DROPPED_CODE = "W_GML_ID_DROPPED";
|
|
118
|
+
|
|
119
|
+
const FORMAT_DEFAULTS: ImportFormatDefaults = { ids: "canonical", defaultDirected: false, weightFrom: "value" };
|
|
120
|
+
|
|
121
|
+
const KIND_INT = 1;
|
|
122
|
+
const KIND_REAL = 2;
|
|
123
|
+
const KIND_STRING = 4;
|
|
124
|
+
const KIND_RECORD = 8;
|
|
125
|
+
|
|
126
|
+
const I32_MIN = -2147483648;
|
|
127
|
+
const I32_MAX = 2147483647;
|
|
128
|
+
|
|
129
|
+
/** How many elements are pushed between abort-signal checks. */
|
|
130
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
131
|
+
|
|
132
|
+
/** The common options the GML importer reads (the rest is reported by reportUnusedOptions). */
|
|
133
|
+
const USED_OPTIONS: ReadonlySet<keyof CommonImportOptions> = new Set<keyof CommonImportOptions>([
|
|
134
|
+
"ids",
|
|
135
|
+
"nodeIdFrom",
|
|
136
|
+
"addMissingNodes",
|
|
137
|
+
"duplicateEdges",
|
|
138
|
+
"selfLoops",
|
|
139
|
+
"onMixedDirection",
|
|
140
|
+
"defaultDirected",
|
|
141
|
+
"weightFrom",
|
|
142
|
+
"weightDtype",
|
|
143
|
+
"restoreMangledIds",
|
|
144
|
+
"errorLimit",
|
|
145
|
+
"signal",
|
|
146
|
+
"onProgress",
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
/** The element sequence number of the graph block's own keys. */
|
|
150
|
+
const GRAPH_SEQ = -1;
|
|
151
|
+
/** The element sequence number of the top-level keys. */
|
|
152
|
+
const TOP_SEQ = -2;
|
|
153
|
+
|
|
154
|
+
/** The GML value class a column stores. */
|
|
155
|
+
type GmlKind = "int" | "real" | "string" | "record";
|
|
156
|
+
|
|
157
|
+
/** What the first walk learns about one key of one table. */
|
|
158
|
+
interface KeySchema {
|
|
159
|
+
/** The key as written. */
|
|
160
|
+
readonly key: string;
|
|
161
|
+
/** The union of value classes seen (KIND_* bits). */
|
|
162
|
+
kinds: number;
|
|
163
|
+
/** Whether the key repeats within one element, or uses a list marker. */
|
|
164
|
+
list: boolean;
|
|
165
|
+
/** The element sequence the key was last seen in. */
|
|
166
|
+
lastSeq: number;
|
|
167
|
+
/** Whether an integer outside the i32 range was seen. */
|
|
168
|
+
wide: boolean;
|
|
169
|
+
/** Whether an integer beyond 2^53 was seen. */
|
|
170
|
+
unsafe: boolean;
|
|
171
|
+
/** The dictionary heuristic while every value is a string, else null. */
|
|
172
|
+
dict: DictHeuristic | null;
|
|
173
|
+
/** Node `graphics` only: some row has numeric x and y. */
|
|
174
|
+
position: boolean;
|
|
175
|
+
/** Node `graphics` only: some row keeps keys besides the position. */
|
|
176
|
+
rest: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The column a key maps to in the second walk. */
|
|
180
|
+
interface ColumnPlan {
|
|
181
|
+
/** The key as written. */
|
|
182
|
+
readonly key: string;
|
|
183
|
+
/** The column name (renamed `<name>#<key>` when the sink held the name with another shape). */
|
|
184
|
+
name: string;
|
|
185
|
+
/** The column dtype. */
|
|
186
|
+
readonly dtype: Dtype;
|
|
187
|
+
/** The list item dtype, or null. */
|
|
188
|
+
readonly itemDtype: ScalarDtype | null;
|
|
189
|
+
/** How scalar values (or list items) are converted. */
|
|
190
|
+
readonly kind: GmlKind;
|
|
191
|
+
/** Whether the column is a list. */
|
|
192
|
+
readonly list: boolean;
|
|
193
|
+
/** The role, or null. */
|
|
194
|
+
readonly role: ColumnRole | null;
|
|
195
|
+
/** Whether the values come from the top level of the file (graph table only). */
|
|
196
|
+
readonly top: boolean;
|
|
197
|
+
/** Node `graphics` with the position mapping: the position column plan, or null. */
|
|
198
|
+
readonly position: ColumnPlan | null;
|
|
199
|
+
/** Node `graphics` with the position mapping: the json column of the remaining keys, or null. */
|
|
200
|
+
readonly rest: ColumnPlan | null;
|
|
201
|
+
/** The handle once declared. */
|
|
202
|
+
handle: ColumnHandle;
|
|
203
|
+
/** Whether the declaration was attempted (a failed one reuses the sink's column or is null). */
|
|
204
|
+
declared: boolean;
|
|
205
|
+
/** The list items of the current element. */
|
|
206
|
+
items: unknown[] | null;
|
|
207
|
+
/** The element sequence the items belong to. */
|
|
208
|
+
seq: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A table's plans by key. */
|
|
212
|
+
type PlanMap = Map<string, ColumnPlan>;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Whether a key gets a role in its table (design section 5.6 applied to GML keys).
|
|
216
|
+
* @param domain - the table
|
|
217
|
+
* @param key - the key
|
|
218
|
+
* @returns the role, or null
|
|
219
|
+
*/
|
|
220
|
+
function roleOf(domain: "node" | "edge" | "graph", key: string): ColumnRole | null {
|
|
221
|
+
if (key === "label" && domain !== "graph") {
|
|
222
|
+
return "label";
|
|
223
|
+
}
|
|
224
|
+
if (domain === "edge") {
|
|
225
|
+
if (key === "id") {
|
|
226
|
+
return "id";
|
|
227
|
+
}
|
|
228
|
+
if (key === "key") {
|
|
229
|
+
return "key";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** One import call. */
|
|
236
|
+
class GmlImport {
|
|
237
|
+
private readonly sink: GraphSink;
|
|
238
|
+
|
|
239
|
+
private readonly report: ImportReportBuilder;
|
|
240
|
+
|
|
241
|
+
private readonly options: ResolvedImportOptions;
|
|
242
|
+
|
|
243
|
+
private readonly positions: boolean;
|
|
244
|
+
|
|
245
|
+
private readonly dictionaries: boolean;
|
|
246
|
+
|
|
247
|
+
private readonly coercer: IdCoercer;
|
|
248
|
+
|
|
249
|
+
private tokens: GmlTokens | null = null;
|
|
250
|
+
|
|
251
|
+
private resolver: DirectionResolver | null = null;
|
|
252
|
+
|
|
253
|
+
private readonly nodeSchema = new Map<string, KeySchema>();
|
|
254
|
+
|
|
255
|
+
private readonly edgeSchema = new Map<string, KeySchema>();
|
|
256
|
+
|
|
257
|
+
private readonly graphSchema = new Map<string, KeySchema>();
|
|
258
|
+
|
|
259
|
+
private readonly topSchema = new Map<string, KeySchema>();
|
|
260
|
+
|
|
261
|
+
private readonly nodePlans: PlanMap = new Map();
|
|
262
|
+
|
|
263
|
+
private readonly edgePlans: PlanMap = new Map();
|
|
264
|
+
|
|
265
|
+
private readonly graphPlans: PlanMap = new Map();
|
|
266
|
+
|
|
267
|
+
private readonly topPlans: PlanMap = new Map();
|
|
268
|
+
|
|
269
|
+
private readonly graphNames = new Set<string>();
|
|
270
|
+
|
|
271
|
+
private graphOpen = -1;
|
|
272
|
+
|
|
273
|
+
private directedToken = -1;
|
|
274
|
+
|
|
275
|
+
private multigraphToken = -1;
|
|
276
|
+
|
|
277
|
+
private creatorToken = -1;
|
|
278
|
+
|
|
279
|
+
private versionToken = -1;
|
|
280
|
+
|
|
281
|
+
private nodeCount = 0;
|
|
282
|
+
|
|
283
|
+
private edgeCount = 0;
|
|
284
|
+
|
|
285
|
+
private weightKinds = 0;
|
|
286
|
+
|
|
287
|
+
private hasOriginalId = false;
|
|
288
|
+
|
|
289
|
+
private headerDirected = false;
|
|
290
|
+
|
|
291
|
+
private seq = 0;
|
|
292
|
+
|
|
293
|
+
private nodeOrdinal = 0;
|
|
294
|
+
|
|
295
|
+
private elementsSinceCheck = 0;
|
|
296
|
+
|
|
297
|
+
/** The sink id every file id maps to when they differ (label / index / restored ids), else null. */
|
|
298
|
+
private idMap: Map<number, NodeId> | null = null;
|
|
299
|
+
|
|
300
|
+
/** Node indices declared by a node block of this import, for duplicate detection. */
|
|
301
|
+
private declared = new Uint32Array(64);
|
|
302
|
+
|
|
303
|
+
/** The list plans touched by the current node or edge. */
|
|
304
|
+
private readonly touched: ColumnPlan[] = [];
|
|
305
|
+
|
|
306
|
+
/** The list plans touched by the graph table's keys. */
|
|
307
|
+
private readonly graphTouched: ColumnPlan[] = [];
|
|
308
|
+
|
|
309
|
+
/** The sequence number of the current node or edge, which its list items belong to. */
|
|
310
|
+
private seqNow = 0;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Create one import.
|
|
314
|
+
* @param sink - the sink
|
|
315
|
+
* @param report - the report
|
|
316
|
+
* @param options - the resolved common options
|
|
317
|
+
* @param format - the format-specific options
|
|
318
|
+
*/
|
|
319
|
+
constructor(
|
|
320
|
+
sink: GraphSink,
|
|
321
|
+
report: ImportReportBuilder,
|
|
322
|
+
options: ResolvedImportOptions,
|
|
323
|
+
format: GmlImportOptions | undefined,
|
|
324
|
+
) {
|
|
325
|
+
this.sink = sink;
|
|
326
|
+
this.report = report;
|
|
327
|
+
this.options = options;
|
|
328
|
+
this.positions = format?.positions ?? true;
|
|
329
|
+
this.dictionaries = format?.dictionaries ?? true;
|
|
330
|
+
this.coercer = new IdCoercer(options.ids);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Run the import over a text.
|
|
335
|
+
* @param text - the whole GML text
|
|
336
|
+
*/
|
|
337
|
+
run(text: string): void {
|
|
338
|
+
const { report } = this;
|
|
339
|
+
try {
|
|
340
|
+
this.tokens = tokenizeGml(text);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
if (err instanceof GmlSyntaxError) {
|
|
343
|
+
report.fail(err.code, err.message, { line: err.line });
|
|
344
|
+
}
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
this.scan();
|
|
348
|
+
this.push();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ============================================================ pass 1: the schema
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Walk the top level: find the graph block, the metadata keys and the top-level attributes,
|
|
355
|
+
* and derive every table's schema.
|
|
356
|
+
*/
|
|
357
|
+
private scan(): void {
|
|
358
|
+
const t = this.requireTokens();
|
|
359
|
+
for (let p = 0; p < t.count; p = t.nextPair(p)) {
|
|
360
|
+
const key = t.textOf(p);
|
|
361
|
+
const v = p + 1;
|
|
362
|
+
const record = t.kind[v] === TOKEN_OPEN;
|
|
363
|
+
if (key === "graph" && record) {
|
|
364
|
+
if (this.graphOpen >= 0) {
|
|
365
|
+
this.report.fail(SECOND_GRAPH_CODE, "the input contains more than one graph", {
|
|
366
|
+
line: t.line[p],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
this.graphOpen = v;
|
|
370
|
+
this.scanGraph(v);
|
|
371
|
+
} else if (key === "Creator" && !record && this.creatorToken < 0) {
|
|
372
|
+
this.creatorToken = v;
|
|
373
|
+
} else if (key === "Version" && !record && this.versionToken < 0) {
|
|
374
|
+
this.versionToken = v;
|
|
375
|
+
} else {
|
|
376
|
+
this.observe(this.topSchema, "graph", key, v, TOP_SEQ);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (this.graphOpen < 0) {
|
|
380
|
+
this.report.fail(NO_GRAPH_CODE, "the input contains no graph [ ... ] block", { line: null });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Walk the graph block for the schema.
|
|
386
|
+
* @param open - the index of the block's `[`
|
|
387
|
+
*/
|
|
388
|
+
private scanGraph(open: number): void {
|
|
389
|
+
const t = this.requireTokens();
|
|
390
|
+
const close = t.match[open];
|
|
391
|
+
for (let p = open + 1; p < close; p = t.nextPair(p)) {
|
|
392
|
+
const key = t.textOf(p);
|
|
393
|
+
const v = p + 1;
|
|
394
|
+
switch (key) {
|
|
395
|
+
case "node":
|
|
396
|
+
if (t.kind[v] === TOKEN_OPEN) {
|
|
397
|
+
this.nodeCount++;
|
|
398
|
+
this.scanElement("node", v);
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
case "edge":
|
|
402
|
+
if (t.kind[v] === TOKEN_OPEN) {
|
|
403
|
+
this.edgeCount++;
|
|
404
|
+
this.scanElement("edge", v);
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
case "directed":
|
|
408
|
+
if (this.directedToken < 0) {
|
|
409
|
+
this.directedToken = v;
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
case "multigraph":
|
|
413
|
+
if (this.multigraphToken < 0) {
|
|
414
|
+
this.multigraphToken = v;
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
default:
|
|
418
|
+
this.observe(this.graphSchema, "graph", key, v, GRAPH_SEQ);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Walk one node or edge block for the schema.
|
|
425
|
+
* @param domain - node or edge
|
|
426
|
+
* @param open - the block's `[`
|
|
427
|
+
*/
|
|
428
|
+
private scanElement(domain: "node" | "edge", open: number): void {
|
|
429
|
+
const t = this.requireTokens();
|
|
430
|
+
const close = t.match[open];
|
|
431
|
+
const seq = this.seq++;
|
|
432
|
+
const schema = domain === "node" ? this.nodeSchema : this.edgeSchema;
|
|
433
|
+
const { weightFrom, restoreMangledIds } = this.options;
|
|
434
|
+
for (let p = open + 1; p < close; p = t.nextPair(p)) {
|
|
435
|
+
const key = t.textOf(p);
|
|
436
|
+
const v = p + 1;
|
|
437
|
+
if (domain === "node") {
|
|
438
|
+
if (key === "id") {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (key === ORIGINAL_ID_KEY && restoreMangledIds) {
|
|
442
|
+
this.hasOriginalId = true;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
if (key === "source" || key === "target") {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (key === weightFrom) {
|
|
450
|
+
this.weightKinds |= kindBit(t, v);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
this.observe(schema, domain, key, v, seq);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Record one value of one key in a table's schema.
|
|
460
|
+
* @param schema - the table's schema
|
|
461
|
+
* @param domain - the table
|
|
462
|
+
* @param key - the key
|
|
463
|
+
* @param v - the value token
|
|
464
|
+
* @param seq - the element sequence number
|
|
465
|
+
*/
|
|
466
|
+
private observe(
|
|
467
|
+
schema: Map<string, KeySchema>,
|
|
468
|
+
domain: "node" | "edge" | "graph",
|
|
469
|
+
key: string,
|
|
470
|
+
v: number,
|
|
471
|
+
seq: number,
|
|
472
|
+
): void {
|
|
473
|
+
const t = this.requireTokens();
|
|
474
|
+
let entry = schema.get(key);
|
|
475
|
+
if (entry === undefined) {
|
|
476
|
+
entry = {
|
|
477
|
+
key,
|
|
478
|
+
kinds: 0,
|
|
479
|
+
list: false,
|
|
480
|
+
lastSeq: seq,
|
|
481
|
+
wide: false,
|
|
482
|
+
unsafe: false,
|
|
483
|
+
dict: this.dictionaries && roleOf(domain, key) === null ? new DictHeuristic() : null,
|
|
484
|
+
position: false,
|
|
485
|
+
rest: false,
|
|
486
|
+
};
|
|
487
|
+
schema.set(key, entry);
|
|
488
|
+
} else if (entry.lastSeq === seq) {
|
|
489
|
+
entry.list = true;
|
|
490
|
+
} else {
|
|
491
|
+
entry.lastSeq = seq;
|
|
492
|
+
}
|
|
493
|
+
switch (t.kind[v]) {
|
|
494
|
+
case TOKEN_STRING: {
|
|
495
|
+
const raw = t.textOf(v);
|
|
496
|
+
if (raw === LIST_START_MARKER || raw === EMPTY_LIST_TEXT || raw === EMPTY_TUPLE_TEXT) {
|
|
497
|
+
entry.list = true;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
entry.kinds |= KIND_STRING;
|
|
501
|
+
if (entry.dict !== null && !entry.dict.decided) {
|
|
502
|
+
entry.dict.observe(raw);
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
case TOKEN_INT: {
|
|
507
|
+
entry.kinds |= KIND_INT;
|
|
508
|
+
const n = Number(t.textOf(v));
|
|
509
|
+
if (n < I32_MIN || n > I32_MAX) {
|
|
510
|
+
entry.wide = true;
|
|
511
|
+
if (!Number.isSafeInteger(n)) {
|
|
512
|
+
entry.unsafe = true;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
case TOKEN_OPEN:
|
|
518
|
+
entry.kinds |= KIND_RECORD;
|
|
519
|
+
if (domain === "node" && key === "graphics" && this.positions) {
|
|
520
|
+
this.inspectGraphics(entry, v);
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
523
|
+
default:
|
|
524
|
+
entry.kinds |= KIND_REAL;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Note whether a node's graphics record carries a position (numeric x and y) and whether
|
|
530
|
+
* anything besides the position remains.
|
|
531
|
+
* @param entry - the graphics schema entry
|
|
532
|
+
* @param open - the record's `[`
|
|
533
|
+
*/
|
|
534
|
+
private inspectGraphics(entry: KeySchema, open: number): void {
|
|
535
|
+
const t = this.requireTokens();
|
|
536
|
+
const close = t.match[open];
|
|
537
|
+
let x = false;
|
|
538
|
+
let y = false;
|
|
539
|
+
let other = false;
|
|
540
|
+
for (let p = open + 1; p < close; p = t.nextPair(p)) {
|
|
541
|
+
const key = t.textOf(p);
|
|
542
|
+
const numeric = t.kind[p + 1] === TOKEN_INT || t.kind[p + 1] === TOKEN_REAL;
|
|
543
|
+
if (key === "x" && numeric && !x) {
|
|
544
|
+
x = true;
|
|
545
|
+
} else if (key === "y" && numeric && !y) {
|
|
546
|
+
y = true;
|
|
547
|
+
} else if (!(key === "z" && numeric)) {
|
|
548
|
+
other = true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const positioned = x && y;
|
|
552
|
+
entry.position ||= positioned;
|
|
553
|
+
entry.rest ||= positioned ? other : true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ============================================================ pass 2: the push
|
|
557
|
+
|
|
558
|
+
/** Declare what the schema needs, set the header and push every element. */
|
|
559
|
+
private push(): void {
|
|
560
|
+
const { sink, report, options } = this;
|
|
561
|
+
const t = this.requireTokens();
|
|
562
|
+
sink.reserve(this.nodeCount, this.edgeCount);
|
|
563
|
+
this.buildPlans();
|
|
564
|
+
this.setMetadata();
|
|
565
|
+
this.headerDirected = this.readFlag(this.directedToken, "directed") ?? options.defaultDirected;
|
|
566
|
+
const resolver = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
567
|
+
resolver.setHeader(this.headerDirected, {
|
|
568
|
+
line: this.directedToken >= 0 ? t.line[this.directedToken] : t.line[this.graphOpen],
|
|
569
|
+
});
|
|
570
|
+
this.resolver = resolver;
|
|
571
|
+
if (options.nodeIdFrom !== "id" || this.hasOriginalId) {
|
|
572
|
+
this.idMap = new Map();
|
|
573
|
+
}
|
|
574
|
+
if (options.nodeIdFrom !== "id") {
|
|
575
|
+
report.loss(
|
|
576
|
+
ID_DROPPED_CODE,
|
|
577
|
+
`node ids taken from the ${options.nodeIdFrom}; the integer id keys are not kept as a column`,
|
|
578
|
+
null,
|
|
579
|
+
this.nodeCount,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
this.pushTopLevel();
|
|
583
|
+
this.pushNodes();
|
|
584
|
+
this.pushEdges();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Turn every schema entry into a column plan. */
|
|
588
|
+
private buildPlans(): void {
|
|
589
|
+
for (const [key, entry] of this.nodeSchema) {
|
|
590
|
+
this.nodePlans.set(key, this.planOf("node", entry));
|
|
591
|
+
}
|
|
592
|
+
for (const [key, entry] of this.edgeSchema) {
|
|
593
|
+
this.edgePlans.set(key, this.planOf("edge", entry));
|
|
594
|
+
}
|
|
595
|
+
for (const [key, entry] of this.graphSchema) {
|
|
596
|
+
this.graphPlans.set(key, this.planOf("graph", entry));
|
|
597
|
+
}
|
|
598
|
+
for (const [key, entry] of this.topSchema) {
|
|
599
|
+
this.topPlans.set(key, this.planOf("graph", entry, true));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* The plan of one schema entry.
|
|
605
|
+
* @param domain - the table
|
|
606
|
+
* @param entry - the schema entry
|
|
607
|
+
* @param top - whether the key is a top-level one (graph table)
|
|
608
|
+
* @returns the plan
|
|
609
|
+
*/
|
|
610
|
+
private planOf(domain: "node" | "edge" | "graph", entry: KeySchema, top = false): ColumnPlan {
|
|
611
|
+
const kind = kindOf(entry);
|
|
612
|
+
let scalar: ScalarDtype = dtypeOfKind(kind, entry);
|
|
613
|
+
if (
|
|
614
|
+
scalar === "string" &&
|
|
615
|
+
!entry.list &&
|
|
616
|
+
entry.dict !== null &&
|
|
617
|
+
entry.kinds === KIND_STRING &&
|
|
618
|
+
entry.dict.decide() === "dict"
|
|
619
|
+
) {
|
|
620
|
+
scalar = "dict";
|
|
621
|
+
}
|
|
622
|
+
if (entry.unsafe) {
|
|
623
|
+
this.report.warnOnce(
|
|
624
|
+
"precision",
|
|
625
|
+
PRECISION_CODE,
|
|
626
|
+
`${domain} key "${entry.key}" holds integers beyond 2^53; they are stored as the nearest f64`,
|
|
627
|
+
{ element: entry.key },
|
|
628
|
+
`${PRECISION_CODE}:${domain}:${entry.key}`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const name = domain === "graph" ? this.uniqueGraphName(entry.key) : entry.key;
|
|
632
|
+
let itemDtype: ScalarDtype | null = null;
|
|
633
|
+
if (entry.list) {
|
|
634
|
+
itemDtype = scalar === "dict" ? "string" : scalar;
|
|
635
|
+
}
|
|
636
|
+
const plan: ColumnPlan = {
|
|
637
|
+
key: entry.key,
|
|
638
|
+
name,
|
|
639
|
+
dtype: entry.list ? "list" : scalar,
|
|
640
|
+
itemDtype,
|
|
641
|
+
kind,
|
|
642
|
+
list: entry.list,
|
|
643
|
+
role: roleOf(domain, entry.key),
|
|
644
|
+
top,
|
|
645
|
+
position: null,
|
|
646
|
+
rest: null,
|
|
647
|
+
handle: INVALID_INDEX as ColumnHandle,
|
|
648
|
+
declared: false,
|
|
649
|
+
items: null,
|
|
650
|
+
seq: Number.NEGATIVE_INFINITY,
|
|
651
|
+
};
|
|
652
|
+
if (
|
|
653
|
+
domain === "node" &&
|
|
654
|
+
entry.key === "graphics" &&
|
|
655
|
+
entry.position &&
|
|
656
|
+
entry.kinds === KIND_RECORD &&
|
|
657
|
+
!entry.list
|
|
658
|
+
) {
|
|
659
|
+
const taken = (candidate: string): boolean =>
|
|
660
|
+
this.nodeSchema.has(candidate) || this.sink.nodeColumn(candidate) !== INVALID_INDEX;
|
|
661
|
+
const position: ColumnPlan = {
|
|
662
|
+
...plan,
|
|
663
|
+
name: uniqueColumnName("position", "graphics", taken),
|
|
664
|
+
dtype: "f64",
|
|
665
|
+
itemDtype: null,
|
|
666
|
+
kind: "real",
|
|
667
|
+
role: "position",
|
|
668
|
+
};
|
|
669
|
+
const rest: ColumnPlan | null = entry.rest ? { ...plan } : null;
|
|
670
|
+
return { ...plan, position, rest };
|
|
671
|
+
}
|
|
672
|
+
return plan;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* A graph column name not yet used by this import (top-level and in-graph keys share the table).
|
|
677
|
+
* @param key - the key
|
|
678
|
+
* @returns the name
|
|
679
|
+
*/
|
|
680
|
+
private uniqueGraphName(key: string): string {
|
|
681
|
+
const name = uniqueColumnName(key, key, (candidate) => this.graphNames.has(candidate));
|
|
682
|
+
this.graphNames.add(name);
|
|
683
|
+
return name;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Set the graph metadata: source format, creator, version, multigraph flag, weight origin. */
|
|
687
|
+
private setMetadata(): void {
|
|
688
|
+
const t = this.requireTokens();
|
|
689
|
+
const { sink, options } = this;
|
|
690
|
+
const textOf = (v: number): string => (t.kind[v] === TOKEN_STRING ? t.stringOf(v) : t.textOf(v));
|
|
691
|
+
const multigraph = this.readFlag(this.multigraphToken, "multigraph");
|
|
692
|
+
sink.setMeta({
|
|
693
|
+
sourceFormat: "gml",
|
|
694
|
+
creator: this.creatorToken >= 0 ? textOf(this.creatorToken) : undefined,
|
|
695
|
+
sourceVersion: this.versionToken >= 0 ? textOf(this.versionToken) : undefined,
|
|
696
|
+
declaredMultigraph: multigraph ?? undefined,
|
|
697
|
+
weightOrigin:
|
|
698
|
+
this.weightKinds !== 0 && options.weightFrom !== null
|
|
699
|
+
? {
|
|
700
|
+
format: "gml",
|
|
701
|
+
id: options.weightFrom,
|
|
702
|
+
title: null,
|
|
703
|
+
type: this.weightKinds === KIND_INT ? "int" : "real",
|
|
704
|
+
namespace: null,
|
|
705
|
+
}
|
|
706
|
+
: undefined,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Read a `directed` / `multigraph` flag token.
|
|
712
|
+
* @param v - the value token, or -1 when absent
|
|
713
|
+
* @param name - the flag name, for issues
|
|
714
|
+
* @returns the flag, or null when absent or not an integer
|
|
715
|
+
*/
|
|
716
|
+
private readFlag(v: number, name: string): boolean | null {
|
|
717
|
+
if (v < 0) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
const t = this.requireTokens();
|
|
721
|
+
if (t.kind[v] !== TOKEN_INT) {
|
|
722
|
+
this.report.error(
|
|
723
|
+
"validation-error",
|
|
724
|
+
FLAG_TYPE_CODE,
|
|
725
|
+
`graph flag "${name}" must be the integer 0 or 1, found ${describeValue(t, v)}`,
|
|
726
|
+
{ line: t.line[v], element: name },
|
|
727
|
+
);
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
const n = Number(t.textOf(v));
|
|
731
|
+
if (n !== 0 && n !== 1) {
|
|
732
|
+
this.report.warning(
|
|
733
|
+
"validation-error",
|
|
734
|
+
FLAG_VALUE_CODE,
|
|
735
|
+
`graph flag "${name}" is ${n}; read as ${n !== 0 ? "1" : "0"}`,
|
|
736
|
+
{ line: t.line[v], element: name },
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
return n !== 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Push the top-level attributes (every top-level key but graph, Creator and Version). */
|
|
743
|
+
private pushTopLevel(): void {
|
|
744
|
+
const t = this.requireTokens();
|
|
745
|
+
for (let p = 0; p < t.count; p = t.nextPair(p)) {
|
|
746
|
+
const v = p + 1;
|
|
747
|
+
if (v === this.graphOpen || v === this.creatorToken || v === this.versionToken) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const plan = this.topPlans.get(t.textOf(p));
|
|
751
|
+
if (plan !== undefined) {
|
|
752
|
+
this.writeGraph(plan, v, TOP_SEQ);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
this.flushGraphLists();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Push every node block and the graph block's own attributes, in file order. */
|
|
759
|
+
private pushNodes(): void {
|
|
760
|
+
const t = this.requireTokens();
|
|
761
|
+
const close = t.match[this.graphOpen];
|
|
762
|
+
for (let p = this.graphOpen + 1; p < close; p = t.nextPair(p)) {
|
|
763
|
+
const key = t.textOf(p);
|
|
764
|
+
const v = p + 1;
|
|
765
|
+
switch (key) {
|
|
766
|
+
case "node":
|
|
767
|
+
if (t.kind[v] === TOKEN_OPEN) {
|
|
768
|
+
this.importNode(v);
|
|
769
|
+
} else {
|
|
770
|
+
this.badElement("node", p);
|
|
771
|
+
this.report.counts.skippedNodes++;
|
|
772
|
+
}
|
|
773
|
+
break;
|
|
774
|
+
case "edge":
|
|
775
|
+
break;
|
|
776
|
+
case "directed":
|
|
777
|
+
case "multigraph":
|
|
778
|
+
if (v !== this.directedToken && v !== this.multigraphToken) {
|
|
779
|
+
this.report.warning(
|
|
780
|
+
"validation-error",
|
|
781
|
+
FLAG_VALUE_CODE,
|
|
782
|
+
`graph flag "${key}" repeated; the first value stands`,
|
|
783
|
+
{ line: t.line[p], element: key },
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
break;
|
|
787
|
+
default: {
|
|
788
|
+
const plan = this.graphPlans.get(key);
|
|
789
|
+
if (plan !== undefined) {
|
|
790
|
+
this.writeGraph(plan, v, GRAPH_SEQ);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
this.flushGraphLists();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** Push every edge block, in file order. */
|
|
799
|
+
private pushEdges(): void {
|
|
800
|
+
const t = this.requireTokens();
|
|
801
|
+
const close = t.match[this.graphOpen];
|
|
802
|
+
let ordinal = 0;
|
|
803
|
+
for (let p = this.graphOpen + 1; p < close; p = t.nextPair(p)) {
|
|
804
|
+
if (t.textOf(p) !== "edge") {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const v = p + 1;
|
|
808
|
+
if (t.kind[v] === TOKEN_OPEN) {
|
|
809
|
+
this.importEdge(v, ordinal);
|
|
810
|
+
} else {
|
|
811
|
+
this.badElement("edge", p);
|
|
812
|
+
this.report.counts.skippedEdges++;
|
|
813
|
+
}
|
|
814
|
+
ordinal++;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Record a `node` / `edge` key whose value is not a block.
|
|
820
|
+
* @param what - node or edge
|
|
821
|
+
* @param p - the key token
|
|
822
|
+
*/
|
|
823
|
+
private badElement(what: string, p: number): void {
|
|
824
|
+
const t = this.requireTokens();
|
|
825
|
+
this.report.error(
|
|
826
|
+
"validation-error",
|
|
827
|
+
ELEMENT_TYPE_CODE,
|
|
828
|
+
`${what} must be a [ ... ] block, found ${describeValue(t, p + 1)}`,
|
|
829
|
+
{ line: t.line[p], element: what },
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Push one node block.
|
|
835
|
+
* @param open - the block's `[`
|
|
836
|
+
*/
|
|
837
|
+
private importNode(open: number): void {
|
|
838
|
+
const t = this.requireTokens();
|
|
839
|
+
const { sink, report, options } = this;
|
|
840
|
+
const close = t.match[open];
|
|
841
|
+
const line = t.line[open];
|
|
842
|
+
const ordinal = this.nodeOrdinal++;
|
|
843
|
+
this.checkAbort();
|
|
844
|
+
let index: number;
|
|
845
|
+
let sinkId: NodeId;
|
|
846
|
+
try {
|
|
847
|
+
let idTok = -1;
|
|
848
|
+
let labelTok = -1;
|
|
849
|
+
let originalTok = -1;
|
|
850
|
+
for (let p = open + 1; p < close; p = t.nextPair(p)) {
|
|
851
|
+
const key = t.textOf(p);
|
|
852
|
+
if (key === "id") {
|
|
853
|
+
idTok = this.structuralToken(idTok, p, "id");
|
|
854
|
+
} else if (key === "label") {
|
|
855
|
+
labelTok = labelTok < 0 ? p + 1 : labelTok;
|
|
856
|
+
} else if (key === ORIGINAL_ID_KEY && options.restoreMangledIds) {
|
|
857
|
+
originalTok = originalTok < 0 ? p + 1 : originalTok;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (idTok < 0) {
|
|
861
|
+
throw new GraphFormatError("E_INVALID_ID", "node has no id", { reason: "missing id" });
|
|
862
|
+
}
|
|
863
|
+
const fileId = this.integerOf(idTok, "id");
|
|
864
|
+
if (originalTok >= 0) {
|
|
865
|
+
sinkId = this.restoredId(originalTok);
|
|
866
|
+
} else {
|
|
867
|
+
switch (options.nodeIdFrom) {
|
|
868
|
+
case "label":
|
|
869
|
+
if (labelTok < 0) {
|
|
870
|
+
throw new GraphFormatError("E_INVALID_ID", "node has no label", {
|
|
871
|
+
reason: "missing label",
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
sinkId = this.coerceValueToken(labelTok);
|
|
875
|
+
break;
|
|
876
|
+
case "index":
|
|
877
|
+
sinkId = ordinal;
|
|
878
|
+
break;
|
|
879
|
+
default:
|
|
880
|
+
sinkId = this.coerceInteger(idTok, fileId);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (this.idMap !== null) {
|
|
884
|
+
this.idMap.set(fileId, sinkId);
|
|
885
|
+
}
|
|
886
|
+
index = sink.addNode(sinkId);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
this.recordNodeError(err, line);
|
|
889
|
+
report.counts.skippedNodes++;
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (this.isDeclared(index)) {
|
|
893
|
+
report.warning(
|
|
894
|
+
"merged",
|
|
895
|
+
DUPLICATE_NODE_CODE,
|
|
896
|
+
`node ${JSON.stringify(sinkId)} is declared twice; later keys overwrite`,
|
|
897
|
+
{ line, element: String(sinkId) },
|
|
898
|
+
);
|
|
899
|
+
} else {
|
|
900
|
+
this.markDeclared(index);
|
|
901
|
+
report.counts.nodes++;
|
|
902
|
+
}
|
|
903
|
+
this.beginElement(this.seq++);
|
|
904
|
+
try {
|
|
905
|
+
for (let p = open + 1; p < close; p = t.nextPair(p)) {
|
|
906
|
+
const key = t.textOf(p);
|
|
907
|
+
if (key === "id" || (key === ORIGINAL_ID_KEY && options.restoreMangledIds)) {
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const plan = this.nodePlans.get(key);
|
|
911
|
+
if (plan !== undefined) {
|
|
912
|
+
this.writeNode(plan, index, p + 1);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
this.flushLists("node", index);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
this.touched.length = 0;
|
|
918
|
+
report.recordError(err, { line, element: String(sinkId) });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Push one edge block.
|
|
924
|
+
* @param open - the block's `[`
|
|
925
|
+
* @param ordinal - the edge's ordinal among edge blocks, for messages
|
|
926
|
+
*/
|
|
927
|
+
private importEdge(open: number, ordinal: number): void {
|
|
928
|
+
const t = this.requireTokens();
|
|
929
|
+
const { sink, report, options } = this;
|
|
930
|
+
const close = t.match[open];
|
|
931
|
+
const line = t.line[open];
|
|
932
|
+
const element = `edge #${ordinal}`;
|
|
933
|
+
this.checkAbort();
|
|
934
|
+
let edge: number;
|
|
935
|
+
try {
|
|
936
|
+
let sourceTok = -1;
|
|
937
|
+
let targetTok = -1;
|
|
938
|
+
let weightTok = -1;
|
|
939
|
+
for (let p = open + 1; p < close; p = t.nextPair(p)) {
|
|
940
|
+
const key = t.textOf(p);
|
|
941
|
+
if (key === "source") {
|
|
942
|
+
sourceTok = this.structuralToken(sourceTok, p, "source");
|
|
943
|
+
} else if (key === "target") {
|
|
944
|
+
targetTok = this.structuralToken(targetTok, p, "target");
|
|
945
|
+
} else if (key === options.weightFrom) {
|
|
946
|
+
weightTok = p + 1;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (sourceTok < 0 || targetTok < 0) {
|
|
950
|
+
throw new GraphFormatError("E_UNKNOWN_NODE", `edge has no ${sourceTok < 0 ? "source" : "target"}`, {
|
|
951
|
+
reason: "missing endpoint",
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
const source = this.endpoint(sourceTok, "source");
|
|
955
|
+
const target = this.endpoint(targetTok, "target");
|
|
956
|
+
const weight = weightTok >= 0 ? this.weightOf(weightTok) : undefined;
|
|
957
|
+
const before = sink.edgeCount;
|
|
958
|
+
const sourceNew = sink.indexOf(source) === INVALID_INDEX;
|
|
959
|
+
const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
|
|
960
|
+
edge = this.requireResolver().addEdge(
|
|
961
|
+
source,
|
|
962
|
+
target,
|
|
963
|
+
this.headerDirected ? "directed" : "undirected",
|
|
964
|
+
weight,
|
|
965
|
+
{ line, element },
|
|
966
|
+
);
|
|
967
|
+
report.counts.edges += sink.edgeCount - before;
|
|
968
|
+
// endpoints the file never declares (addMissingNodes) are nodes of the sink too
|
|
969
|
+
report.counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
|
|
970
|
+
} catch (err) {
|
|
971
|
+
this.recordEdgeError(err, line, element);
|
|
972
|
+
report.counts.skippedEdges++;
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
this.beginElement(this.seq++);
|
|
976
|
+
try {
|
|
977
|
+
for (let p = open + 1; p < close; p = t.nextPair(p)) {
|
|
978
|
+
const key = t.textOf(p);
|
|
979
|
+
if (key === "source" || key === "target" || key === options.weightFrom) {
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
const plan = this.edgePlans.get(key);
|
|
983
|
+
if (plan !== undefined) {
|
|
984
|
+
this.writeEdge(plan, edge, p + 1);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
this.flushLists("edge", edge);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
this.touched.length = 0;
|
|
990
|
+
report.recordError(err, { line, element });
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ============================================================ ids and weights
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Note a structural key token, refusing a repeat.
|
|
998
|
+
* @param previous - the token seen before, or -1
|
|
999
|
+
* @param p - the key token
|
|
1000
|
+
* @param key - the key name
|
|
1001
|
+
* @returns the value token
|
|
1002
|
+
*/
|
|
1003
|
+
private structuralToken(previous: number, p: number, key: string): number {
|
|
1004
|
+
if (previous >= 0) {
|
|
1005
|
+
throw new GraphFormatError("E_INVALID_ID", `key "${key}" appears twice in one block`, {
|
|
1006
|
+
reason: "duplicate key",
|
|
1007
|
+
key,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
return p + 1;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* The integer value of an id / source / target token (the GML grammar's integer ids; a string
|
|
1015
|
+
* id is refused, as the malformed corpus pins, although NetworkX would accept it).
|
|
1016
|
+
* @param v - the value token
|
|
1017
|
+
* @param key - the key name, for the error
|
|
1018
|
+
* @returns the number
|
|
1019
|
+
*/
|
|
1020
|
+
private integerOf(v: number, key: string): number {
|
|
1021
|
+
const t = this.requireTokens();
|
|
1022
|
+
if (t.kind[v] !== TOKEN_INT) {
|
|
1023
|
+
throw new GraphFormatError("E_INVALID_ID", `${key} must be an integer, found ${describeValue(t, v)}`, {
|
|
1024
|
+
reason: "not an integer",
|
|
1025
|
+
key,
|
|
1026
|
+
value: t.kind[v] === TOKEN_OPEN ? "[...]" : t.textOf(v),
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
return Number(t.textOf(v));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Coerce an integer id token under the `ids` rule: a safe integer passes as a typed number
|
|
1034
|
+
* (so `01` is 1 and `-0` is 0), a larger one goes through the text rule (a string under
|
|
1035
|
+
* "canonical"), and "string" keeps the text as written.
|
|
1036
|
+
* @param v - the value token
|
|
1037
|
+
* @param n - its numeric value
|
|
1038
|
+
* @returns the id
|
|
1039
|
+
*/
|
|
1040
|
+
private coerceInteger(v: number, n: number): NodeId {
|
|
1041
|
+
if (Number.isSafeInteger(n) && this.coercer.mode !== "string") {
|
|
1042
|
+
return this.coercer.value(n);
|
|
1043
|
+
}
|
|
1044
|
+
return this.coercer.text(this.requireTokens().textOf(v));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Coerce a label token under the `ids` rule: a string by its text, a number as a typed value.
|
|
1049
|
+
* @param v - the value token
|
|
1050
|
+
* @returns the id
|
|
1051
|
+
*/
|
|
1052
|
+
private coerceValueToken(v: number): NodeId {
|
|
1053
|
+
const t = this.requireTokens();
|
|
1054
|
+
switch (t.kind[v]) {
|
|
1055
|
+
case TOKEN_STRING:
|
|
1056
|
+
return this.coercer.text(t.stringOf(v));
|
|
1057
|
+
case TOKEN_INT:
|
|
1058
|
+
return this.coerceInteger(v, Number(t.textOf(v)));
|
|
1059
|
+
case TOKEN_OPEN:
|
|
1060
|
+
throw new GraphFormatError("E_INVALID_ID", "label must be a string or a number, found a record", {
|
|
1061
|
+
reason: "record label",
|
|
1062
|
+
});
|
|
1063
|
+
default:
|
|
1064
|
+
return this.coercer.value(numberOfText(t.textOf(v)));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* The id a `graphty_originalId` key restores: typed, never coerced (the exporter wrote it exactly).
|
|
1070
|
+
* @param v - the value token
|
|
1071
|
+
* @returns the original id
|
|
1072
|
+
*/
|
|
1073
|
+
private restoredId(v: number): NodeId {
|
|
1074
|
+
const t = this.requireTokens();
|
|
1075
|
+
switch (t.kind[v]) {
|
|
1076
|
+
case TOKEN_STRING:
|
|
1077
|
+
return t.stringOf(v);
|
|
1078
|
+
case TOKEN_OPEN:
|
|
1079
|
+
throw new GraphFormatError("E_INVALID_ID", `${ORIGINAL_ID_KEY} must be a string or a number`, {
|
|
1080
|
+
reason: "record original id",
|
|
1081
|
+
});
|
|
1082
|
+
default:
|
|
1083
|
+
return numberOfText(t.textOf(v));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Resolve a source / target token to a sink id.
|
|
1089
|
+
* @param v - the value token
|
|
1090
|
+
* @param key - source or target
|
|
1091
|
+
* @returns the id
|
|
1092
|
+
*/
|
|
1093
|
+
private endpoint(v: number, key: string): NodeId {
|
|
1094
|
+
const n = this.integerOf(v, key);
|
|
1095
|
+
if (this.idMap !== null) {
|
|
1096
|
+
const mapped = this.idMap.get(n);
|
|
1097
|
+
if (mapped !== undefined) {
|
|
1098
|
+
return mapped;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return this.coerceInteger(v, n);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* The weight of an edge from its weight token.
|
|
1106
|
+
* @param v - the value token
|
|
1107
|
+
* @returns the weight; E_INVALID_WEIGHT for a record or non-numeric text
|
|
1108
|
+
*/
|
|
1109
|
+
private weightOf(v: number): number | undefined {
|
|
1110
|
+
const t = this.requireTokens();
|
|
1111
|
+
switch (t.kind[v]) {
|
|
1112
|
+
case TOKEN_STRING:
|
|
1113
|
+
return parseWeightText(t.stringOf(v));
|
|
1114
|
+
case TOKEN_OPEN:
|
|
1115
|
+
throw new GraphFormatError("E_INVALID_WEIGHT", "edge weight is a record", { value: "[...]" });
|
|
1116
|
+
default:
|
|
1117
|
+
return weightFromValue(numberOfText(t.textOf(v)));
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Record a node-level error under the GML code it implies.
|
|
1123
|
+
* @param err - the thrown value
|
|
1124
|
+
* @param line - the block's line
|
|
1125
|
+
*/
|
|
1126
|
+
private recordNodeError(err: unknown, line: number): void {
|
|
1127
|
+
const { report } = this;
|
|
1128
|
+
if (err instanceof GraphFormatError && err.code === "E_INVALID_ID") {
|
|
1129
|
+
const { reason } = err.details;
|
|
1130
|
+
switch (reason) {
|
|
1131
|
+
case "missing id":
|
|
1132
|
+
report.error("missing-value", MISSING_ID_CODE, err.message, { line, element: "node" });
|
|
1133
|
+
return;
|
|
1134
|
+
case "missing label":
|
|
1135
|
+
report.error("missing-value", MISSING_LABEL_CODE, err.message, { line, element: "node" });
|
|
1136
|
+
return;
|
|
1137
|
+
case "duplicate key":
|
|
1138
|
+
report.error("validation-error", REPEATED_KEY_CODE, err.message, { line, element: "node" });
|
|
1139
|
+
return;
|
|
1140
|
+
case "not an integer":
|
|
1141
|
+
report.error("validation-error", ID_TYPE_CODE, err.message, { line, element: "node" });
|
|
1142
|
+
return;
|
|
1143
|
+
default:
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
report.recordError(err, { line, element: "node" });
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Record an edge-level error under the GML code it implies.
|
|
1152
|
+
* @param err - the thrown value
|
|
1153
|
+
* @param line - the block's line
|
|
1154
|
+
* @param element - the edge description
|
|
1155
|
+
*/
|
|
1156
|
+
private recordEdgeError(err: unknown, line: number, element: string): void {
|
|
1157
|
+
const { report } = this;
|
|
1158
|
+
if (err instanceof GraphFormatError) {
|
|
1159
|
+
const { reason } = err.details;
|
|
1160
|
+
if (err.code === "E_UNKNOWN_NODE" && reason === "missing endpoint") {
|
|
1161
|
+
report.error("missing-value", MISSING_ENDPOINT_CODE, err.message, { line, element });
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (err.code === "E_INVALID_ID" && reason === "duplicate key") {
|
|
1165
|
+
report.error("validation-error", REPEATED_KEY_CODE, err.message, { line, element });
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (err.code === "E_INVALID_ID" && reason === "not an integer") {
|
|
1169
|
+
report.error("validation-error", ID_TYPE_CODE, err.message, { line, element });
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
report.recordError(err, { line, element });
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// ============================================================ values
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Write one node value.
|
|
1180
|
+
* @param plan - the key's plan
|
|
1181
|
+
* @param index - the node index
|
|
1182
|
+
* @param v - the value token
|
|
1183
|
+
*/
|
|
1184
|
+
private writeNode(plan: ColumnPlan, index: number, v: number): void {
|
|
1185
|
+
if (plan.position !== null) {
|
|
1186
|
+
this.writeGraphics(plan, index, v);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (plan.list) {
|
|
1190
|
+
this.collectItem(plan, v, this.seqNow, this.touched);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
this.sink.setNodeValue(this.handleOf(plan, "node"), index, this.scalarOf(plan, v));
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Write one edge value.
|
|
1198
|
+
* @param plan - the key's plan
|
|
1199
|
+
* @param edge - the edge index
|
|
1200
|
+
* @param v - the value token
|
|
1201
|
+
*/
|
|
1202
|
+
private writeEdge(plan: ColumnPlan, edge: number, v: number): void {
|
|
1203
|
+
if (plan.list) {
|
|
1204
|
+
this.collectItem(plan, v, this.seqNow, this.touched);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
this.sink.setEdgeValue(this.handleOf(plan, "edge"), edge, this.scalarOf(plan, v));
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Write one graph value.
|
|
1212
|
+
* @param plan - the key's plan
|
|
1213
|
+
* @param v - the value token
|
|
1214
|
+
* @param seq - TOP_SEQ or GRAPH_SEQ
|
|
1215
|
+
*/
|
|
1216
|
+
private writeGraph(plan: ColumnPlan, v: number, seq: number): void {
|
|
1217
|
+
if (plan.list) {
|
|
1218
|
+
this.collectItem(plan, v, seq, this.graphTouched);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
this.sink.setGraphValue(plan.name, this.scalarOf(plan, v), graphDecl(plan));
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Write a node's graphics record: the position column from x / y / z, the remaining keys to
|
|
1226
|
+
* the json column.
|
|
1227
|
+
* @param plan - the graphics plan
|
|
1228
|
+
* @param index - the node index
|
|
1229
|
+
* @param v - the value token (a record)
|
|
1230
|
+
*/
|
|
1231
|
+
private writeGraphics(plan: ColumnPlan, index: number, v: number): void {
|
|
1232
|
+
const t = this.requireTokens();
|
|
1233
|
+
const { sink } = this;
|
|
1234
|
+
const record = parseRecord(t, v);
|
|
1235
|
+
const { x, y, z } = record;
|
|
1236
|
+
const positionPlan = plan.position;
|
|
1237
|
+
if (positionPlan !== null && typeof x === "number" && typeof y === "number") {
|
|
1238
|
+
sink.setNodeValue(this.handleOf(positionPlan, "node"), index, [x, y, typeof z === "number" ? z : 0]);
|
|
1239
|
+
delete record.x;
|
|
1240
|
+
delete record.y;
|
|
1241
|
+
if (typeof z === "number") {
|
|
1242
|
+
delete record.z;
|
|
1243
|
+
}
|
|
1244
|
+
if (plan.rest !== null && Object.keys(record).length > 0) {
|
|
1245
|
+
sink.setNodeValue(this.handleOf(plan.rest, "node"), index, record);
|
|
1246
|
+
}
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (plan.rest !== null) {
|
|
1250
|
+
sink.setNodeValue(this.handleOf(plan.rest, "node"), index, record);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Collect one list item.
|
|
1256
|
+
* @param plan - the list plan
|
|
1257
|
+
* @param v - the value token
|
|
1258
|
+
* @param seq - the sequence number of the element the item belongs to
|
|
1259
|
+
* @param touched - the plans touched by that element
|
|
1260
|
+
*/
|
|
1261
|
+
private collectItem(plan: ColumnPlan, v: number, seq: number, touched: ColumnPlan[]): void {
|
|
1262
|
+
const t = this.requireTokens();
|
|
1263
|
+
if (plan.seq !== seq) {
|
|
1264
|
+
plan.seq = seq;
|
|
1265
|
+
plan.items = [];
|
|
1266
|
+
touched.push(plan);
|
|
1267
|
+
}
|
|
1268
|
+
if (t.kind[v] === TOKEN_STRING) {
|
|
1269
|
+
const raw = t.textOf(v);
|
|
1270
|
+
if (raw === LIST_START_MARKER || raw === EMPTY_LIST_TEXT || raw === EMPTY_TUPLE_TEXT) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
(plan.items as unknown[]).push(this.scalarOf(plan, v));
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Start a node or edge: later list items belong to it.
|
|
1279
|
+
* @param seq - the element's sequence number
|
|
1280
|
+
*/
|
|
1281
|
+
private beginElement(seq: number): void {
|
|
1282
|
+
this.seqNow = seq;
|
|
1283
|
+
this.touched.length = 0;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Write the lists collected for the current node or edge.
|
|
1288
|
+
* @param domain - node or edge
|
|
1289
|
+
* @param row - the row
|
|
1290
|
+
*/
|
|
1291
|
+
private flushLists(domain: "node" | "edge", row: number): void {
|
|
1292
|
+
const { sink, touched } = this;
|
|
1293
|
+
for (const plan of touched) {
|
|
1294
|
+
const handle = this.handleOf(plan, domain);
|
|
1295
|
+
if (domain === "node") {
|
|
1296
|
+
sink.setNodeValue(handle, row, plan.items);
|
|
1297
|
+
} else {
|
|
1298
|
+
sink.setEdgeValue(handle, row, plan.items);
|
|
1299
|
+
}
|
|
1300
|
+
plan.items = null;
|
|
1301
|
+
}
|
|
1302
|
+
touched.length = 0;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/** Write the lists collected for the graph table. */
|
|
1306
|
+
private flushGraphLists(): void {
|
|
1307
|
+
const { sink, graphTouched } = this;
|
|
1308
|
+
for (const plan of graphTouched) {
|
|
1309
|
+
sink.setGraphValue(plan.name, plan.items, graphDecl(plan));
|
|
1310
|
+
plan.items = null;
|
|
1311
|
+
}
|
|
1312
|
+
graphTouched.length = 0;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* The JS value of a scalar value token (or list item) under a plan's storage class.
|
|
1317
|
+
* @param plan - the plan
|
|
1318
|
+
* @param v - the value token
|
|
1319
|
+
* @returns the value to push
|
|
1320
|
+
*/
|
|
1321
|
+
private scalarOf(plan: ColumnPlan, v: number): unknown {
|
|
1322
|
+
const t = this.requireTokens();
|
|
1323
|
+
const kind = t.kind[v];
|
|
1324
|
+
switch (plan.kind) {
|
|
1325
|
+
case "int":
|
|
1326
|
+
return Number(t.textOf(v));
|
|
1327
|
+
case "real":
|
|
1328
|
+
return numberOfText(t.textOf(v));
|
|
1329
|
+
case "string":
|
|
1330
|
+
return kind === TOKEN_STRING ? t.stringOf(v) : t.textOf(v);
|
|
1331
|
+
default:
|
|
1332
|
+
return kind === TOKEN_STRING ? t.stringOf(v) : scalarOrRecord(t, v);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* The handle of a plan's column, declaring it on first use. A declaration the sink refuses
|
|
1338
|
+
* because a column of the name exists with another declaration falls back to that column
|
|
1339
|
+
* (reported once); a taken role is dropped (reported once).
|
|
1340
|
+
* @param plan - the plan
|
|
1341
|
+
* @param domain - node or edge
|
|
1342
|
+
* @returns the handle
|
|
1343
|
+
*/
|
|
1344
|
+
private handleOf(plan: ColumnPlan, domain: "node" | "edge"): ColumnHandle {
|
|
1345
|
+
if (plan.declared) {
|
|
1346
|
+
return plan.handle;
|
|
1347
|
+
}
|
|
1348
|
+
plan.declared = true;
|
|
1349
|
+
const { sink, report } = this;
|
|
1350
|
+
const decl: ColumnDecl = {
|
|
1351
|
+
name: plan.name,
|
|
1352
|
+
dtype: plan.dtype,
|
|
1353
|
+
nullable: true,
|
|
1354
|
+
origin: { format: "gml", id: plan.key, title: null, type: originTypeOf(plan), namespace: null },
|
|
1355
|
+
};
|
|
1356
|
+
if (plan.list) {
|
|
1357
|
+
decl.itemDtype = plan.itemDtype ?? "string";
|
|
1358
|
+
}
|
|
1359
|
+
if (plan.role === "position") {
|
|
1360
|
+
decl.components = 3;
|
|
1361
|
+
}
|
|
1362
|
+
if (plan.role !== null) {
|
|
1363
|
+
decl.role = plan.role;
|
|
1364
|
+
}
|
|
1365
|
+
const resolved = declareResolved(sink, domain, decl, report, { element: plan.name });
|
|
1366
|
+
plan.handle = resolved.handle;
|
|
1367
|
+
if (resolved.renamed) {
|
|
1368
|
+
plan.name = resolved.decl.name;
|
|
1369
|
+
}
|
|
1370
|
+
return plan.handle;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// ============================================================ bookkeeping
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Whether a node index was declared by a node block of this import.
|
|
1377
|
+
* @param index - the node index
|
|
1378
|
+
* @returns true when declared
|
|
1379
|
+
*/
|
|
1380
|
+
private isDeclared(index: number): boolean {
|
|
1381
|
+
const word = index >>> 5;
|
|
1382
|
+
return word < this.declared.length && ((this.declared[word] >>> (index & 31)) & 1) === 1;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* Mark a node index as declared.
|
|
1387
|
+
* @param index - the node index
|
|
1388
|
+
*/
|
|
1389
|
+
private markDeclared(index: number): void {
|
|
1390
|
+
const word = index >>> 5;
|
|
1391
|
+
if (word >= this.declared.length) {
|
|
1392
|
+
const grown = new Uint32Array(Math.max(word + 1, this.declared.length * 2));
|
|
1393
|
+
grown.set(this.declared);
|
|
1394
|
+
this.declared = grown;
|
|
1395
|
+
}
|
|
1396
|
+
this.declared[word] |= 1 << (index & 31);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/** Check the abort signal every ABORT_CHECK_INTERVAL elements. */
|
|
1400
|
+
private checkAbort(): void {
|
|
1401
|
+
if (++this.elementsSinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
1402
|
+
this.elementsSinceCheck = 0;
|
|
1403
|
+
throwIfAborted(this.options.signal);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* The tokens, which exist after run() lexed the text.
|
|
1409
|
+
* @returns the tokens
|
|
1410
|
+
*/
|
|
1411
|
+
private requireTokens(): GmlTokens {
|
|
1412
|
+
if (this.tokens === null) {
|
|
1413
|
+
throw new GraphFormatError("E_UNSUPPORTED", "the GML text was not lexed", { reason: "no tokens" });
|
|
1414
|
+
}
|
|
1415
|
+
return this.tokens;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* The direction resolver, which exists once the header was set.
|
|
1420
|
+
* @returns the resolver
|
|
1421
|
+
*/
|
|
1422
|
+
private requireResolver(): DirectionResolver {
|
|
1423
|
+
if (this.resolver === null) {
|
|
1424
|
+
throw new GraphFormatError("E_DIRECTED", "the header was not set", { reason: "no resolver" });
|
|
1425
|
+
}
|
|
1426
|
+
return this.resolver;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* The KIND_* bit of a value token.
|
|
1432
|
+
* @param t - the tokens
|
|
1433
|
+
* @param v - the value token
|
|
1434
|
+
* @returns the bit
|
|
1435
|
+
*/
|
|
1436
|
+
function kindBit(t: GmlTokens, v: number): number {
|
|
1437
|
+
switch (t.kind[v]) {
|
|
1438
|
+
case TOKEN_INT:
|
|
1439
|
+
return KIND_INT;
|
|
1440
|
+
case TOKEN_STRING:
|
|
1441
|
+
return KIND_STRING;
|
|
1442
|
+
case TOKEN_OPEN:
|
|
1443
|
+
return KIND_RECORD;
|
|
1444
|
+
default:
|
|
1445
|
+
return KIND_REAL;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* The storage class of a schema entry: the widest value class seen.
|
|
1451
|
+
* @param entry - the entry
|
|
1452
|
+
* @returns the class
|
|
1453
|
+
*/
|
|
1454
|
+
function kindOf(entry: KeySchema): GmlKind {
|
|
1455
|
+
if ((entry.kinds & KIND_RECORD) !== 0) {
|
|
1456
|
+
return "record";
|
|
1457
|
+
}
|
|
1458
|
+
if ((entry.kinds & KIND_STRING) !== 0) {
|
|
1459
|
+
return "string";
|
|
1460
|
+
}
|
|
1461
|
+
if ((entry.kinds & KIND_REAL) !== 0) {
|
|
1462
|
+
return "real";
|
|
1463
|
+
}
|
|
1464
|
+
return "int";
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* The scalar dtype of a storage class.
|
|
1469
|
+
* @param kind - the class
|
|
1470
|
+
* @param entry - the entry (a wide int keeps origin int but stores f64)
|
|
1471
|
+
* @returns the dtype
|
|
1472
|
+
*/
|
|
1473
|
+
function dtypeOfKind(kind: GmlKind, entry: KeySchema): ScalarDtype {
|
|
1474
|
+
switch (kind) {
|
|
1475
|
+
case "int":
|
|
1476
|
+
return entry.wide ? "f64" : "i32";
|
|
1477
|
+
case "real":
|
|
1478
|
+
return "f64";
|
|
1479
|
+
case "string":
|
|
1480
|
+
return "string";
|
|
1481
|
+
default:
|
|
1482
|
+
return "json";
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* The origin.type text of a plan: the GML value class of its values (`int` for wide integers
|
|
1488
|
+
* stored as f64 too, so the exporter writes them without a decimal point).
|
|
1489
|
+
* @param plan - the plan
|
|
1490
|
+
* @returns the type text
|
|
1491
|
+
*/
|
|
1492
|
+
function originTypeOf(plan: ColumnPlan): string {
|
|
1493
|
+
return plan.kind;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* The declaration patch of a graph column.
|
|
1498
|
+
* @param plan - the plan
|
|
1499
|
+
* @returns the decl for setGraphValue
|
|
1500
|
+
*/
|
|
1501
|
+
function graphDecl(plan: ColumnPlan): ColumnDecl {
|
|
1502
|
+
const decl: ColumnDecl = {
|
|
1503
|
+
name: plan.name,
|
|
1504
|
+
dtype: plan.dtype,
|
|
1505
|
+
nullable: true,
|
|
1506
|
+
origin: { format: "gml", id: plan.key, title: null, type: originTypeOf(plan), namespace: null },
|
|
1507
|
+
};
|
|
1508
|
+
if (plan.list) {
|
|
1509
|
+
decl.itemDtype = plan.itemDtype ?? "string";
|
|
1510
|
+
}
|
|
1511
|
+
if (plan.top) {
|
|
1512
|
+
decl.extra = { gmlTopLevel: true };
|
|
1513
|
+
}
|
|
1514
|
+
return decl;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* A short description of a value token for messages.
|
|
1519
|
+
* @param t - the tokens
|
|
1520
|
+
* @param v - the value token
|
|
1521
|
+
* @returns the description
|
|
1522
|
+
*/
|
|
1523
|
+
function describeValue(t: GmlTokens, v: number): string {
|
|
1524
|
+
switch (t.kind[v]) {
|
|
1525
|
+
case TOKEN_OPEN:
|
|
1526
|
+
return "a record";
|
|
1527
|
+
case TOKEN_STRING:
|
|
1528
|
+
return `the string "${t.textOf(v)}"`;
|
|
1529
|
+
case TOKEN_REAL:
|
|
1530
|
+
return `the real ${t.textOf(v)}`;
|
|
1531
|
+
case TOKEN_WORD:
|
|
1532
|
+
return `the word ${t.textOf(v)}`;
|
|
1533
|
+
default:
|
|
1534
|
+
return t.textOf(v);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const SNIFF_GRAPH_START = /^(?:\s|#[^\n]*\n)*graph\s*\[/;
|
|
1539
|
+
const SNIFF_GRAPH_ANYWHERE = /(?:^|\s)graph\s*\[/;
|
|
1540
|
+
const SNIFF_KEY_START = /^(?:\s|#[^\n]*\n)*[A-Za-z_][0-9A-Za-z_]*\s+(?:"|[+-]?[0-9.]|\[)/;
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Confidence that a head of bytes is GML: a `graph [` opener, or key-value text with a `graph [`
|
|
1544
|
+
* somewhere in the head.
|
|
1545
|
+
* @param head - the first bytes of the input
|
|
1546
|
+
* @returns 0.95 for a leading `graph [`, 0.85 for key-value text containing one, 0.4 for key-value text, 0 otherwise
|
|
1547
|
+
*/
|
|
1548
|
+
function sniffGml(head: Uint8Array): number {
|
|
1549
|
+
let text = new TextDecoder("utf-8", { fatal: false }).decode(head);
|
|
1550
|
+
if (text.charCodeAt(0) === 0xfeff) {
|
|
1551
|
+
text = text.slice(1);
|
|
1552
|
+
}
|
|
1553
|
+
if (SNIFF_GRAPH_START.test(text)) {
|
|
1554
|
+
return 0.95;
|
|
1555
|
+
}
|
|
1556
|
+
if (SNIFF_KEY_START.test(text)) {
|
|
1557
|
+
return SNIFF_GRAPH_ANYWHERE.test(text) ? 0.85 : 0.4;
|
|
1558
|
+
}
|
|
1559
|
+
return 0;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/** The GML importer plugin. */
|
|
1563
|
+
export const gmlImporter: GraphImporter<GmlImportOptions> = Object.freeze({
|
|
1564
|
+
format: "gml",
|
|
1565
|
+
extensions: Object.freeze([".gml"]),
|
|
1566
|
+
mimeTypes: Object.freeze(["text/x-gml", "text/plain"]),
|
|
1567
|
+
sniff: sniffGml,
|
|
1568
|
+
/**
|
|
1569
|
+
* Read a GML text into a sink.
|
|
1570
|
+
* @param input - the text, bytes or stream
|
|
1571
|
+
* @param sink - the sink
|
|
1572
|
+
* @param options - format-specific and common options
|
|
1573
|
+
* @returns the report
|
|
1574
|
+
*/
|
|
1575
|
+
async import(
|
|
1576
|
+
input: ImportInput,
|
|
1577
|
+
sink: GraphSink,
|
|
1578
|
+
options?: GmlImportOptions & CommonImportOptions,
|
|
1579
|
+
): Promise<ImportReport> {
|
|
1580
|
+
const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
1581
|
+
const report = new ImportReportBuilder("gml", resolved.errorLimit);
|
|
1582
|
+
reportSinkOptions(sink, options, report);
|
|
1583
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
1584
|
+
const text = await readText(input, report, resolved);
|
|
1585
|
+
throwIfAborted(resolved.signal);
|
|
1586
|
+
new GmlImport(sink, report, resolved, options).run(text);
|
|
1587
|
+
// an abort raised during the last few elements (after the last periodic check) still rejects
|
|
1588
|
+
throwIfAborted(resolved.signal);
|
|
1589
|
+
return report.finish();
|
|
1590
|
+
},
|
|
1591
|
+
});
|