@graphty/graph-io 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -28
- package/dist/chunks/children-CL3Cy0ez.js +238 -0
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
- package/dist/chunks/escape-DyI8JofU.js +938 -0
- package/dist/chunks/escape-DyI8JofU.js.map +1 -0
- package/dist/chunks/importer-CQnJuWJw.js +2987 -0
- package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
- package/dist/chunks/importer-CpCpfbxr.js +2015 -0
- package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
- package/dist/chunks/importer-DbnGYr3_.js +2342 -0
- package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
- package/dist/chunks/importer-GozH8DkN.js +3050 -0
- package/dist/chunks/importer-GozH8DkN.js.map +1 -0
- package/dist/chunks/records-CGpxszm1.js +605 -0
- package/dist/chunks/records-CGpxszm1.js.map +1 -0
- package/dist/chunks/text-CajMdVFy.js +189 -0
- package/dist/chunks/text-CajMdVFy.js.map +1 -0
- package/dist/chunks/writer-DxSKC7TL.js +2842 -0
- package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1702 -0
- package/dist/csv.js.map +1 -0
- package/dist/dot.d.ts +1 -0
- package/dist/dot.js +8 -0
- package/dist/dot.js.map +1 -0
- package/dist/gexf.d.ts +1 -0
- package/dist/gexf.js +3466 -0
- package/dist/gexf.js.map +1 -0
- package/dist/gml.d.ts +1 -0
- package/dist/gml.js +2647 -0
- package/dist/gml.js.map +1 -0
- package/dist/graph-io.d.ts +1 -0
- package/dist/graph-io.js +790 -0
- package/dist/graph-io.js.map +1 -0
- package/dist/graphml.d.ts +1 -0
- package/dist/graphml.js +8 -0
- package/dist/graphml.js.map +1 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +11 -0
- package/dist/json.js.map +1 -0
- package/dist/neo4j.d.ts +1 -0
- package/dist/neo4j.js +2046 -0
- package/dist/neo4j.js.map +1 -0
- package/dist/pajek.d.ts +1 -0
- package/dist/pajek.js +8 -0
- package/dist/pajek.js.map +1 -0
- package/dist/src/children.d.ts +134 -0
- package/dist/src/children.d.ts.map +1 -0
- package/dist/src/children.js +274 -0
- package/dist/src/children.js.map +1 -0
- package/dist/src/common/attributes.d.ts +229 -0
- package/dist/src/common/attributes.d.ts.map +1 -0
- package/dist/src/common/attributes.js +368 -0
- package/dist/src/common/attributes.js.map +1 -0
- package/dist/src/common/codes.d.ts +105 -0
- package/dist/src/common/codes.d.ts.map +1 -0
- package/dist/src/common/codes.js +107 -0
- package/dist/src/common/codes.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +84 -0
- package/dist/src/common/declared-types.d.ts.map +1 -0
- package/dist/src/common/declared-types.js +326 -0
- package/dist/src/common/declared-types.js.map +1 -0
- package/dist/src/common/direction.d.ts +206 -0
- package/dist/src/common/direction.d.ts.map +1 -0
- package/dist/src/common/direction.js +370 -0
- package/dist/src/common/direction.js.map +1 -0
- package/dist/src/common/escape.d.ts +92 -0
- package/dist/src/common/escape.d.ts.map +1 -0
- package/dist/src/common/escape.js +212 -0
- package/dist/src/common/escape.js.map +1 -0
- package/dist/src/common/export.d.ts +249 -0
- package/dist/src/common/export.d.ts.map +1 -0
- package/dist/src/common/export.js +594 -0
- package/dist/src/common/export.js.map +1 -0
- package/dist/src/common/format.d.ts +59 -0
- package/dist/src/common/format.d.ts.map +1 -0
- package/dist/src/common/format.js +106 -0
- package/dist/src/common/format.js.map +1 -0
- package/dist/src/common/ids.d.ts +83 -0
- package/dist/src/common/ids.d.ts.map +1 -0
- package/dist/src/common/ids.js +158 -0
- package/dist/src/common/ids.js.map +1 -0
- package/dist/src/common/input.d.ts +100 -0
- package/dist/src/common/input.d.ts.map +1 -0
- package/dist/src/common/input.js +335 -0
- package/dist/src/common/input.js.map +1 -0
- package/dist/src/common/lists.d.ts +34 -0
- package/dist/src/common/lists.d.ts.map +1 -0
- package/dist/src/common/lists.js +185 -0
- package/dist/src/common/lists.js.map +1 -0
- package/dist/src/common/options.d.ts +108 -0
- package/dist/src/common/options.d.ts.map +1 -0
- package/dist/src/common/options.js +265 -0
- package/dist/src/common/options.js.map +1 -0
- package/dist/src/common/report.d.ts +187 -0
- package/dist/src/common/report.d.ts.map +1 -0
- package/dist/src/common/report.js +274 -0
- package/dist/src/common/report.js.map +1 -0
- package/dist/src/common/temporal.d.ts +71 -0
- package/dist/src/common/temporal.d.ts.map +1 -0
- package/dist/src/common/temporal.js +266 -0
- package/dist/src/common/temporal.js.map +1 -0
- package/dist/src/common/text.d.ts +104 -0
- package/dist/src/common/text.d.ts.map +1 -0
- package/dist/src/common/text.js +255 -0
- package/dist/src/common/text.js.map +1 -0
- package/dist/src/common/weights.d.ts +77 -0
- package/dist/src/common/weights.d.ts.map +1 -0
- package/dist/src/common/weights.js +156 -0
- package/dist/src/common/weights.js.map +1 -0
- package/dist/src/common/writer.d.ts +51 -0
- package/dist/src/common/writer.d.ts.map +1 -0
- package/dist/src/common/writer.js +108 -0
- package/dist/src/common/writer.js.map +1 -0
- package/dist/src/common/xml.d.ts +245 -0
- package/dist/src/common/xml.d.ts.map +1 -0
- package/dist/src/common/xml.js +942 -0
- package/dist/src/common/xml.js.map +1 -0
- package/dist/src/formats/csv/exporter.d.ts +70 -0
- package/dist/src/formats/csv/exporter.d.ts.map +1 -0
- package/dist/src/formats/csv/exporter.js +682 -0
- package/dist/src/formats/csv/exporter.js.map +1 -0
- package/dist/src/formats/csv/header.d.ts +66 -0
- package/dist/src/formats/csv/header.d.ts.map +1 -0
- package/dist/src/formats/csv/header.js +152 -0
- package/dist/src/formats/csv/header.js.map +1 -0
- package/dist/src/formats/csv/importer.d.ts +82 -0
- package/dist/src/formats/csv/importer.d.ts.map +1 -0
- package/dist/src/formats/csv/importer.js +849 -0
- package/dist/src/formats/csv/importer.js.map +1 -0
- package/dist/src/formats/csv/index.d.ts +60 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -0
- package/dist/src/formats/csv/index.js +63 -0
- package/dist/src/formats/csv/index.js.map +1 -0
- package/dist/src/formats/csv/records.d.ts +188 -0
- package/dist/src/formats/csv/records.d.ts.map +1 -0
- package/dist/src/formats/csv/records.js +702 -0
- package/dist/src/formats/csv/records.js.map +1 -0
- package/dist/src/formats/csv/values.d.ts +105 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -0
- package/dist/src/formats/csv/values.js +192 -0
- package/dist/src/formats/csv/values.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +52 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -0
- package/dist/src/formats/dot/exporter.js +836 -0
- package/dist/src/formats/dot/exporter.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts +102 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -0
- package/dist/src/formats/dot/importer.js +1291 -0
- package/dist/src/formats/dot/importer.js.map +1 -0
- package/dist/src/formats/dot/index.d.ts +7 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -0
- package/dist/src/formats/dot/index.js +7 -0
- package/dist/src/formats/dot/index.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +29 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -0
- package/dist/src/formats/dot/names.js +28 -0
- package/dist/src/formats/dot/names.js.map +1 -0
- package/dist/src/formats/dot/tokenizer.d.ts +114 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
- package/dist/src/formats/dot/tokenizer.js +341 -0
- package/dist/src/formats/dot/tokenizer.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts +56 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
- package/dist/src/formats/gexf/exporter.js +1395 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +73 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -0
- package/dist/src/formats/gexf/importer.js +1880 -0
- package/dist/src/formats/gexf/importer.js.map +1 -0
- package/dist/src/formats/gexf/index.d.ts +96 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -0
- package/dist/src/formats/gexf/index.js +97 -0
- package/dist/src/formats/gexf/index.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts +135 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -0
- package/dist/src/formats/gexf/schema.js +323 -0
- package/dist/src/formats/gexf/schema.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +69 -0
- package/dist/src/formats/gml/exporter.d.ts.map +1 -0
- package/dist/src/formats/gml/exporter.js +1093 -0
- package/dist/src/formats/gml/exporter.js.map +1 -0
- package/dist/src/formats/gml/importer.d.ts +66 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -0
- package/dist/src/formats/gml/importer.js +1331 -0
- package/dist/src/formats/gml/importer.js.map +1 -0
- package/dist/src/formats/gml/index.d.ts +85 -0
- package/dist/src/formats/gml/index.d.ts.map +1 -0
- package/dist/src/formats/gml/index.js +88 -0
- package/dist/src/formats/gml/index.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +186 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -0
- package/dist/src/formats/gml/syntax.js +467 -0
- package/dist/src/formats/gml/syntax.js.map +1 -0
- package/dist/src/formats/graphml/constants.d.ts +169 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -0
- package/dist/src/formats/graphml/constants.js +165 -0
- package/dist/src/formats/graphml/constants.js.map +1 -0
- package/dist/src/formats/graphml/exporter.d.ts +34 -0
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
- package/dist/src/formats/graphml/exporter.js +1176 -0
- package/dist/src/formats/graphml/exporter.js.map +1 -0
- package/dist/src/formats/graphml/importer.d.ts +31 -0
- package/dist/src/formats/graphml/importer.d.ts.map +1 -0
- package/dist/src/formats/graphml/importer.js +1607 -0
- package/dist/src/formats/graphml/importer.js.map +1 -0
- package/dist/src/formats/graphml/index.d.ts +8 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -0
- package/dist/src/formats/graphml/index.js +8 -0
- package/dist/src/formats/graphml/index.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts +72 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -0
- package/dist/src/formats/graphml/tree.js +290 -0
- package/dist/src/formats/graphml/tree.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +125 -0
- package/dist/src/formats/json/dialect.d.ts.map +1 -0
- package/dist/src/formats/json/dialect.js +262 -0
- package/dist/src/formats/json/dialect.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts +89 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -0
- package/dist/src/formats/json/exporter.js +1358 -0
- package/dist/src/formats/json/exporter.js.map +1 -0
- package/dist/src/formats/json/importer.d.ts +108 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -0
- package/dist/src/formats/json/importer.js +1838 -0
- package/dist/src/formats/json/importer.js.map +1 -0
- package/dist/src/formats/json/index.d.ts +8 -0
- package/dist/src/formats/json/index.d.ts.map +1 -0
- package/dist/src/formats/json/index.js +8 -0
- package/dist/src/formats/json/index.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts +68 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
- package/dist/src/formats/neo4j/exporter.js +1055 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -0
- package/dist/src/formats/neo4j/header.d.ts +52 -0
- package/dist/src/formats/neo4j/header.d.ts.map +1 -0
- package/dist/src/formats/neo4j/header.js +131 -0
- package/dist/src/formats/neo4j/header.js.map +1 -0
- package/dist/src/formats/neo4j/importer.d.ts +73 -0
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
- package/dist/src/formats/neo4j/importer.js +932 -0
- package/dist/src/formats/neo4j/importer.js.map +1 -0
- package/dist/src/formats/neo4j/index.d.ts +79 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -0
- package/dist/src/formats/neo4j/index.js +83 -0
- package/dist/src/formats/neo4j/index.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts +58 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
- package/dist/src/formats/pajek/exporter.js +825 -0
- package/dist/src/formats/pajek/exporter.js.map +1 -0
- package/dist/src/formats/pajek/importer.d.ts +88 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -0
- package/dist/src/formats/pajek/importer.js +1047 -0
- package/dist/src/formats/pajek/importer.js.map +1 -0
- package/dist/src/formats/pajek/index.d.ts +7 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -0
- package/dist/src/formats/pajek/index.js +7 -0
- package/dist/src/formats/pajek/index.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +112 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
- package/dist/src/formats/pajek/syntax.js +269 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +207 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +481 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/sniff.d.ts +104 -0
- package/dist/src/sniff.d.ts.map +1 -0
- package/dist/src/sniff.js +357 -0
- package/dist/src/sniff.js.map +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +29 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +122 -7
- package/src/children.ts +335 -0
- package/src/common/attributes.ts +520 -0
- package/src/common/codes.ts +153 -0
- package/src/common/declared-types.ts +374 -0
- package/src/common/direction.ts +518 -0
- package/src/common/escape.ts +231 -0
- package/src/common/export.ts +817 -0
- package/src/common/format.ts +111 -0
- package/src/common/ids.ts +176 -0
- package/src/common/input.ts +378 -0
- package/src/common/lists.ts +196 -0
- package/src/common/options.ts +377 -0
- package/src/common/report.ts +352 -0
- package/src/common/temporal.ts +302 -0
- package/src/common/text.ts +294 -0
- package/src/common/weights.ts +202 -0
- package/src/common/writer.ts +123 -0
- package/src/common/xml.ts +1053 -0
- package/src/formats/csv/exporter.ts +894 -0
- package/src/formats/csv/header.ts +172 -0
- package/src/formats/csv/importer.ts +1104 -0
- package/src/formats/csv/index.ts +88 -0
- package/src/formats/csv/records.ts +813 -0
- package/src/formats/csv/values.ts +224 -0
- package/src/formats/dot/exporter.ts +1014 -0
- package/src/formats/dot/importer.ts +1549 -0
- package/src/formats/dot/index.ts +7 -0
- package/src/formats/dot/names.ts +40 -0
- package/src/formats/dot/tokenizer.ts +384 -0
- package/src/formats/gexf/exporter.ts +1696 -0
- package/src/formats/gexf/importer.ts +2333 -0
- package/src/formats/gexf/index.ts +142 -0
- package/src/formats/gexf/schema.ts +361 -0
- package/src/formats/gml/exporter.ts +1404 -0
- package/src/formats/gml/importer.ts +1591 -0
- package/src/formats/gml/index.ts +128 -0
- package/src/formats/gml/syntax.ts +545 -0
- package/src/formats/graphml/constants.ts +225 -0
- package/src/formats/graphml/exporter.ts +1458 -0
- package/src/formats/graphml/importer.ts +2027 -0
- package/src/formats/graphml/index.ts +8 -0
- package/src/formats/graphml/tree.ts +318 -0
- package/src/formats/json/dialect.ts +317 -0
- package/src/formats/json/exporter.ts +1616 -0
- package/src/formats/json/importer.ts +2271 -0
- package/src/formats/json/index.ts +8 -0
- package/src/formats/neo4j/exporter.ts +1287 -0
- package/src/formats/neo4j/header.ts +156 -0
- package/src/formats/neo4j/importer.ts +1220 -0
- package/src/formats/neo4j/index.ts +116 -0
- package/src/formats/pajek/exporter.ts +1000 -0
- package/src/formats/pajek/importer.ts +1311 -0
- package/src/formats/pajek/index.ts +7 -0
- package/src/formats/pajek/syntax.ts +307 -0
- package/src/index.ts +244 -0
- package/src/registry.ts +617 -0
- package/src/sniff.ts +397 -0
- package/src/types.ts +262 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@graphty/graph-io/gml` subpath (design section 8.2): the GML importer and exporter, their
|
|
3
|
+
* option types, and their issue and loss-note codes grouped in two tables.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
COLUMN_RENAMED_CODE,
|
|
8
|
+
DIRECTION_FORCED_CODE,
|
|
9
|
+
DIRECTION_REFUSED_CODE,
|
|
10
|
+
ID_MERGED_CODE,
|
|
11
|
+
INVALID_UTF8_CODE,
|
|
12
|
+
MIXED_DIRECTION_CODE,
|
|
13
|
+
OPTION_IGNORED_CODE,
|
|
14
|
+
SINK_OPTION_CODE,
|
|
15
|
+
SYNTAX_CODE,
|
|
16
|
+
} from "../../common/codes.js";
|
|
17
|
+
import {
|
|
18
|
+
GRAPHICS_CONFLICT_CODE,
|
|
19
|
+
GRAPHICS_OVERRIDDEN_CODE,
|
|
20
|
+
INVALID_KEY_CODE,
|
|
21
|
+
JSON_ARRAY_CODE,
|
|
22
|
+
KEY_MANGLED_CODE,
|
|
23
|
+
NESTED_ARRAY_CODE,
|
|
24
|
+
POSITION_COMPONENTS_CODE,
|
|
25
|
+
RECORD_BOOLEAN_CODE,
|
|
26
|
+
RECORD_NULL_CODE,
|
|
27
|
+
RECORD_NUMBER_TYPE_CODE,
|
|
28
|
+
RESERVED_KEY_CODE,
|
|
29
|
+
} from "./exporter.js";
|
|
30
|
+
import {
|
|
31
|
+
DUPLICATE_NODE_CODE,
|
|
32
|
+
ELEMENT_TYPE_CODE,
|
|
33
|
+
FLAG_TYPE_CODE,
|
|
34
|
+
FLAG_VALUE_CODE,
|
|
35
|
+
ID_DROPPED_CODE,
|
|
36
|
+
ID_TYPE_CODE,
|
|
37
|
+
MISSING_ENDPOINT_CODE,
|
|
38
|
+
MISSING_ID_CODE,
|
|
39
|
+
MISSING_LABEL_CODE,
|
|
40
|
+
NO_GRAPH_CODE,
|
|
41
|
+
PRECISION_CODE,
|
|
42
|
+
REPEATED_KEY_CODE,
|
|
43
|
+
ROLE_TAKEN_CODE,
|
|
44
|
+
SECOND_GRAPH_CODE,
|
|
45
|
+
} from "./importer.js";
|
|
46
|
+
|
|
47
|
+
export { gmlExporter, type GmlExportOptions } from "./exporter.js";
|
|
48
|
+
export { gmlImporter, type GmlImportOptions } from "./importer.js";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The issue codes the GML importer records (design section 8.6), by name: the codes shared with
|
|
52
|
+
* the other importers (src/common/codes.ts) and the GML-specific ones. A key is the code without
|
|
53
|
+
* its severity and format prefixes.
|
|
54
|
+
*/
|
|
55
|
+
export const GML_ISSUE = Object.freeze({
|
|
56
|
+
/** A grammar violation: an untokenisable bare token, an unclosed string or `[`, a stray `]`, a key without a value (fatal). */
|
|
57
|
+
SYNTAX: SYNTAX_CODE,
|
|
58
|
+
/** The input holds invalid UTF-8 (fatal). */
|
|
59
|
+
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
60
|
+
/** No `graph [` block (fatal). */
|
|
61
|
+
NO_GRAPH: NO_GRAPH_CODE,
|
|
62
|
+
/** More than one `graph` block (fatal). */
|
|
63
|
+
SECOND_GRAPH: SECOND_GRAPH_CODE,
|
|
64
|
+
/** A node without an `id`. */
|
|
65
|
+
MISSING_ID: MISSING_ID_CODE,
|
|
66
|
+
/** A node without a `label` under nodeIdFrom "label". */
|
|
67
|
+
MISSING_LABEL: MISSING_LABEL_CODE,
|
|
68
|
+
/** An edge without `source` or `target`. */
|
|
69
|
+
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
70
|
+
/** A node id, source or target that is not an integer. */
|
|
71
|
+
ID_TYPE: ID_TYPE_CODE,
|
|
72
|
+
/** A node id declared twice (later keys overwrite). */
|
|
73
|
+
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
74
|
+
/** A structural key repeated in one element. */
|
|
75
|
+
REPEATED_KEY: REPEATED_KEY_CODE,
|
|
76
|
+
/** A `node` / `edge` key whose value is not a record. */
|
|
77
|
+
ELEMENT_TYPE: ELEMENT_TYPE_CODE,
|
|
78
|
+
/** A `directed` / `multigraph` flag that is not an integer. */
|
|
79
|
+
FLAG_TYPE: FLAG_TYPE_CODE,
|
|
80
|
+
/** A `directed` / `multigraph` flag outside 0 / 1, or repeated. */
|
|
81
|
+
FLAG_VALUE: FLAG_VALUE_CODE,
|
|
82
|
+
/** An integer beyond 2^53 rounded to f64. */
|
|
83
|
+
PRECISION: PRECISION_CODE,
|
|
84
|
+
/** A column renamed `<name>#<key>` because the sink held the name with another shape. */
|
|
85
|
+
COLUMN_RENAMED: COLUMN_RENAMED_CODE,
|
|
86
|
+
/** A column declared without its role because the sink already holds it. */
|
|
87
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
88
|
+
/** Two id texts merged into one number under ids "number". */
|
|
89
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
90
|
+
/** A common option the importer has no use for was given. */
|
|
91
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
92
|
+
/** A builder-policy option the sink does not honour. */
|
|
93
|
+
SINK_OPTION: SINK_OPTION_CODE,
|
|
94
|
+
/** The sink refused the file's direction. */
|
|
95
|
+
DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
|
|
96
|
+
/** Edges forced to the policy's direction. */
|
|
97
|
+
DIRECTION_FORCED: DIRECTION_FORCED_CODE,
|
|
98
|
+
/** A mixed file under onMixedDirection "error" (fatal). */
|
|
99
|
+
MIXED_DIRECTION: MIXED_DIRECTION_CODE,
|
|
100
|
+
/** Under nodeIdFrom "label" / "index" the integer ids are not kept (a loss note). */
|
|
101
|
+
ID_DROPPED: ID_DROPPED_CODE,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/** The loss-note codes the GML exporter's check() reports beyond the shared LOSS table, by name. */
|
|
105
|
+
export const GML_LOSS = Object.freeze({
|
|
106
|
+
/** A json column holds numbers; GML records cannot keep int versus real (design section 8.5). */
|
|
107
|
+
RECORD_NUMBER_TYPE: RECORD_NUMBER_TYPE_CODE,
|
|
108
|
+
/** A json column holds booleans, written 1 / 0. */
|
|
109
|
+
RECORD_BOOLEAN: RECORD_BOOLEAN_CODE,
|
|
110
|
+
/** A json column holds nulls, omitted. */
|
|
111
|
+
RECORD_NULL: RECORD_NULL_CODE,
|
|
112
|
+
/** An array inside an array; export() throws. */
|
|
113
|
+
NESTED_ARRAY: NESTED_ARRAY_CODE,
|
|
114
|
+
/** A json row that is an array, written as repeated keys. */
|
|
115
|
+
JSON_ARRAY: JSON_ARRAY_CODE,
|
|
116
|
+
/** A column name or record key outside the GML key grammar. */
|
|
117
|
+
INVALID_KEY: INVALID_KEY_CODE,
|
|
118
|
+
/** A column named like a structural key. */
|
|
119
|
+
RESERVED_KEY: RESERVED_KEY_CODE,
|
|
120
|
+
/** A key rewritten under sanitizeKeys "mangle". */
|
|
121
|
+
KEY_MANGLED: KEY_MANGLED_CODE,
|
|
122
|
+
/** A position column with more than three components. */
|
|
123
|
+
POSITION_COMPONENTS: POSITION_COMPONENTS_CODE,
|
|
124
|
+
/** A graphics record whose x / y / z the position column overrides. */
|
|
125
|
+
GRAPHICS_OVERRIDDEN: GRAPHICS_OVERRIDDEN_CODE,
|
|
126
|
+
/** A graphics record that cannot hold the position. */
|
|
127
|
+
GRAPHICS_CONFLICT: GRAPHICS_CONFLICT_CODE,
|
|
128
|
+
});
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GML lexical layer shared by the importer and the exporter (research note 07 section 2.2,
|
|
3
|
+
* NetworkX readwrite/gml.py as the reference reader): the token list a whole GML text lexes into,
|
|
4
|
+
* the structural validation that guarantees every later walk sees balanced `key value` pairs, the
|
|
5
|
+
* value grammar (int, real, quoted string with `&#NN;` character references, `[ ... ]` record),
|
|
6
|
+
* the NetworkX list conventions (`_networkx_list_start`, `"[]"`) and the key charset.
|
|
7
|
+
*
|
|
8
|
+
* Tokens are kept in typed arrays (kind, start, end, line, matching bracket) rather than objects:
|
|
9
|
+
* the token list is the one intermediate structure of the GML importer, and it costs 17 bytes per
|
|
10
|
+
* token instead of an object per node.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { SYNTAX_CODE } from "../../common/codes.js";
|
|
14
|
+
import { decodeGmlString } from "../../common/escape.js";
|
|
15
|
+
|
|
16
|
+
/** A bare word: a key, or `INF` / `NAN` at a value position. */
|
|
17
|
+
export const TOKEN_WORD = 0;
|
|
18
|
+
/** An integer literal. */
|
|
19
|
+
export const TOKEN_INT = 1;
|
|
20
|
+
/** A real literal, including the non-finite spellings. */
|
|
21
|
+
export const TOKEN_REAL = 2;
|
|
22
|
+
/** A double-quoted string; start / end delimit the body between the quotes. */
|
|
23
|
+
export const TOKEN_STRING = 3;
|
|
24
|
+
/** `[`. */
|
|
25
|
+
export const TOKEN_OPEN = 4;
|
|
26
|
+
/** `]`. */
|
|
27
|
+
export const TOKEN_CLOSE = 5;
|
|
28
|
+
|
|
29
|
+
/** The kind of one token. */
|
|
30
|
+
type TokenKind = 0 | 1 | 2 | 3 | 4 | 5;
|
|
31
|
+
|
|
32
|
+
/** The NetworkX marker that starts a one-element list written as repeated keys. */
|
|
33
|
+
export const LIST_START_MARKER = "_networkx_list_start";
|
|
34
|
+
|
|
35
|
+
/** The string NetworkX writes for an empty list. */
|
|
36
|
+
export const EMPTY_LIST_TEXT = "[]";
|
|
37
|
+
|
|
38
|
+
/** The string NetworkX writes for an empty tuple, read as an empty list too. */
|
|
39
|
+
export const EMPTY_TUPLE_TEXT = "()";
|
|
40
|
+
|
|
41
|
+
/** The node key the exporter writes a mangled node's original id into (design section 8.5). */
|
|
42
|
+
export const ORIGINAL_ID_KEY = "graphty_originalId";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Issue code of every grammar violation (shared with the other text formats; the message carries
|
|
46
|
+
* the detail): a bare token that is neither a key nor a number, an unclosed string, an unclosed
|
|
47
|
+
* `[`, a `]` with no open `[`, a value without a key or a key without a value.
|
|
48
|
+
*/
|
|
49
|
+
export const SYNTAX_STRUCTURE_CODE = SYNTAX_CODE;
|
|
50
|
+
/** The code of an untokenisable bare token (E_SYNTAX). */
|
|
51
|
+
export const SYNTAX_TOKEN_CODE = SYNTAX_CODE;
|
|
52
|
+
/** The code of an unclosed string (E_SYNTAX). */
|
|
53
|
+
export const SYNTAX_STRING_CODE = SYNTAX_CODE;
|
|
54
|
+
/** The code of an unclosed `[` (E_SYNTAX). */
|
|
55
|
+
export const SYNTAX_BRACKET_CODE = SYNTAX_CODE;
|
|
56
|
+
|
|
57
|
+
const KEY_TEXT = /^[A-Za-z_][0-9A-Za-z_]*$/;
|
|
58
|
+
const STRICT_KEY_TEXT = /^[A-Za-z][0-9A-Za-z_]*$/;
|
|
59
|
+
const INT_TEXT = /^[+-]?[0-9]+$/;
|
|
60
|
+
const REAL_TEXT = /^[+-]?(?:(?:[0-9]+\.[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+)$/;
|
|
61
|
+
const NON_FINITE_TEXT = /^[+-]?(?:inf|infinity|nan)$/i;
|
|
62
|
+
const NON_FINITE_WORD = /^(?:inf|infinity|nan)$/i;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A lexical or structural error of a GML text, with the 1-based line it was found on. The importer
|
|
66
|
+
* turns it into a fatal parse-error issue.
|
|
67
|
+
*/
|
|
68
|
+
export class GmlSyntaxError extends Error {
|
|
69
|
+
/** The issue code. */
|
|
70
|
+
readonly code: string;
|
|
71
|
+
|
|
72
|
+
/** The 1-based line. */
|
|
73
|
+
readonly line: number;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a syntax error.
|
|
77
|
+
* @param code - the issue code
|
|
78
|
+
* @param message - a plain-ASCII message
|
|
79
|
+
* @param line - the 1-based line
|
|
80
|
+
*/
|
|
81
|
+
constructor(code: string, message: string, line: number) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = "GmlSyntaxError";
|
|
84
|
+
this.code = code;
|
|
85
|
+
this.line = line;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The tokens of one GML text, structurally validated: every `[` has its matching `]` recorded and
|
|
91
|
+
* every key is followed by a value, so walkers need no error paths.
|
|
92
|
+
*/
|
|
93
|
+
export class GmlTokens {
|
|
94
|
+
/** The source text the offsets index into. */
|
|
95
|
+
readonly text: string;
|
|
96
|
+
|
|
97
|
+
/** The kind of every token. */
|
|
98
|
+
kind: Uint8Array;
|
|
99
|
+
|
|
100
|
+
/** The start offset of every token (a string's body start). */
|
|
101
|
+
start: Uint32Array;
|
|
102
|
+
|
|
103
|
+
/** The end offset (exclusive) of every token (a string's body end). */
|
|
104
|
+
end: Uint32Array;
|
|
105
|
+
|
|
106
|
+
/** The 1-based line of every token. */
|
|
107
|
+
line: Uint32Array;
|
|
108
|
+
|
|
109
|
+
/** For an open bracket, the index of its matching close bracket; 0 elsewhere. */
|
|
110
|
+
match: Uint32Array;
|
|
111
|
+
|
|
112
|
+
/** The number of tokens. */
|
|
113
|
+
count = 0;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create an empty token list over a text.
|
|
117
|
+
* @param text - the source text
|
|
118
|
+
* @param capacity - the initial capacity
|
|
119
|
+
*/
|
|
120
|
+
constructor(text: string, capacity = 1024) {
|
|
121
|
+
this.text = text;
|
|
122
|
+
this.kind = new Uint8Array(capacity);
|
|
123
|
+
this.start = new Uint32Array(capacity);
|
|
124
|
+
this.end = new Uint32Array(capacity);
|
|
125
|
+
this.line = new Uint32Array(capacity);
|
|
126
|
+
this.match = new Uint32Array(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Append a token.
|
|
131
|
+
* @param kind - the kind
|
|
132
|
+
* @param start - the start offset
|
|
133
|
+
* @param end - the end offset
|
|
134
|
+
* @param line - the 1-based line
|
|
135
|
+
*/
|
|
136
|
+
push(kind: TokenKind, start: number, end: number, line: number): void {
|
|
137
|
+
if (this.count === this.kind.length) {
|
|
138
|
+
this.grow();
|
|
139
|
+
}
|
|
140
|
+
const i = this.count++;
|
|
141
|
+
this.kind[i] = kind;
|
|
142
|
+
this.start[i] = start;
|
|
143
|
+
this.end[i] = end;
|
|
144
|
+
this.line[i] = line;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The raw text of a token (a string's body without the quotes, entities undecoded).
|
|
149
|
+
* @param i - the token index
|
|
150
|
+
* @returns the text
|
|
151
|
+
*/
|
|
152
|
+
textOf(i: number): string {
|
|
153
|
+
return this.text.slice(this.start[i], this.end[i]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The decoded value of a string token.
|
|
158
|
+
* @param i - the token index
|
|
159
|
+
* @returns the body with character references decoded
|
|
160
|
+
*/
|
|
161
|
+
stringOf(i: number): string {
|
|
162
|
+
return decodeGmlString(this.textOf(i));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Whether a token is a `[` or `]`.
|
|
167
|
+
* @param i - the token index
|
|
168
|
+
* @returns true for a bracket
|
|
169
|
+
*/
|
|
170
|
+
isBracket(i: number): boolean {
|
|
171
|
+
return this.kind[i] >= TOKEN_OPEN;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The index of the pair following the pair whose key is at `key`.
|
|
176
|
+
* @param key - the index of a key token
|
|
177
|
+
* @returns the index after the pair's value (after the matching `]` for a record)
|
|
178
|
+
*/
|
|
179
|
+
nextPair(key: number): number {
|
|
180
|
+
const value = key + 1;
|
|
181
|
+
return this.kind[value] === TOKEN_OPEN ? this.match[value] + 1 : value + 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Double every array. */
|
|
185
|
+
private grow(): void {
|
|
186
|
+
const capacity = this.kind.length * 2;
|
|
187
|
+
const kind = new Uint8Array(capacity);
|
|
188
|
+
kind.set(this.kind);
|
|
189
|
+
this.kind = kind;
|
|
190
|
+
const start = new Uint32Array(capacity);
|
|
191
|
+
start.set(this.start);
|
|
192
|
+
this.start = start;
|
|
193
|
+
const end = new Uint32Array(capacity);
|
|
194
|
+
end.set(this.end);
|
|
195
|
+
this.end = end;
|
|
196
|
+
const line = new Uint32Array(capacity);
|
|
197
|
+
line.set(this.line);
|
|
198
|
+
this.line = line;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Lex and structurally validate a whole GML text.
|
|
204
|
+
*
|
|
205
|
+
* Lexical rules (NetworkX): whitespace separates tokens; `#` outside a string starts a comment
|
|
206
|
+
* that runs to the end of the line; a string is delimited by double quotes and must close on its
|
|
207
|
+
* line (there is no escape mechanism; `&#NN;` references are decoded later); `[` and `]` are
|
|
208
|
+
* single-character tokens; every other run of characters is a key (`[A-Za-z_][0-9A-Za-z_]*`),
|
|
209
|
+
* an integer (`[+-]?[0-9]+`), a real (with a decimal point or an exponent, or `INF` / `NAN` in
|
|
210
|
+
* any case with an optional sign) or an error.
|
|
211
|
+
*
|
|
212
|
+
* Structural rules: the text is a sequence of `key value` pairs where a value is a number, a
|
|
213
|
+
* string, `INF` / `NAN`, or a bracketed sequence of pairs; every `[` has a `]`.
|
|
214
|
+
* @param text - the GML text
|
|
215
|
+
* @returns the validated tokens; GmlSyntaxError with the line of the first problem
|
|
216
|
+
*/
|
|
217
|
+
export function tokenizeGml(text: string): GmlTokens {
|
|
218
|
+
const tokens = new GmlTokens(text, Math.max(1024, text.length >>> 3));
|
|
219
|
+
const n = text.length;
|
|
220
|
+
let i = 0;
|
|
221
|
+
let line = 1;
|
|
222
|
+
while (i < n) {
|
|
223
|
+
const c = text.charCodeAt(i);
|
|
224
|
+
if (c === 10) {
|
|
225
|
+
line++;
|
|
226
|
+
i++;
|
|
227
|
+
} else if (c === 13) {
|
|
228
|
+
line++;
|
|
229
|
+
i++;
|
|
230
|
+
if (i < n && text.charCodeAt(i) === 10) {
|
|
231
|
+
i++;
|
|
232
|
+
}
|
|
233
|
+
} else if (c === 32 || c === 9 || c === 11 || c === 12) {
|
|
234
|
+
i++;
|
|
235
|
+
} else if (c === 35) {
|
|
236
|
+
i = endOfLine(text, i);
|
|
237
|
+
} else if (c === 91) {
|
|
238
|
+
tokens.push(TOKEN_OPEN, i, i + 1, line);
|
|
239
|
+
i++;
|
|
240
|
+
} else if (c === 93) {
|
|
241
|
+
tokens.push(TOKEN_CLOSE, i, i + 1, line);
|
|
242
|
+
i++;
|
|
243
|
+
} else if (c === 34) {
|
|
244
|
+
const close = text.indexOf('"', i + 1);
|
|
245
|
+
const eol = endOfLine(text, i + 1);
|
|
246
|
+
if (close < 0 || close > eol) {
|
|
247
|
+
throw new GmlSyntaxError(SYNTAX_STRING_CODE, `unclosed string at line ${line}`, line);
|
|
248
|
+
}
|
|
249
|
+
tokens.push(TOKEN_STRING, i + 1, close, line);
|
|
250
|
+
i = close + 1;
|
|
251
|
+
} else {
|
|
252
|
+
let j = i + 1;
|
|
253
|
+
while (j < n && !isDelimiter(text.charCodeAt(j))) {
|
|
254
|
+
j++;
|
|
255
|
+
}
|
|
256
|
+
tokens.push(classifyBare(text, i, j, line), i, j, line);
|
|
257
|
+
i = j;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
validateStructure(tokens);
|
|
261
|
+
return tokens;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The offset of the line terminator at or after `from`, or the text length.
|
|
266
|
+
* @param text - the text
|
|
267
|
+
* @param from - the offset to search from
|
|
268
|
+
* @returns the offset of the next `\n` or `\r`
|
|
269
|
+
*/
|
|
270
|
+
function endOfLine(text: string, from: number): number {
|
|
271
|
+
const n = text.length;
|
|
272
|
+
for (let i = from; i < n; i++) {
|
|
273
|
+
const c = text.charCodeAt(i);
|
|
274
|
+
if (c === 10 || c === 13) {
|
|
275
|
+
return i;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return n;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Whether a character ends a bare token.
|
|
283
|
+
* @param c - the char code
|
|
284
|
+
* @returns true for whitespace, brackets, a quote or `#`
|
|
285
|
+
*/
|
|
286
|
+
function isDelimiter(c: number): boolean {
|
|
287
|
+
return (
|
|
288
|
+
c === 32 ||
|
|
289
|
+
c === 9 ||
|
|
290
|
+
c === 10 ||
|
|
291
|
+
c === 13 ||
|
|
292
|
+
c === 11 ||
|
|
293
|
+
c === 12 ||
|
|
294
|
+
c === 91 ||
|
|
295
|
+
c === 93 ||
|
|
296
|
+
c === 34 ||
|
|
297
|
+
c === 35
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Classify a bare token.
|
|
303
|
+
* @param text - the text
|
|
304
|
+
* @param start - the token start
|
|
305
|
+
* @param end - the token end
|
|
306
|
+
* @param line - the token line, for the error
|
|
307
|
+
* @returns WORD, INT or REAL; GmlSyntaxError for anything else
|
|
308
|
+
*/
|
|
309
|
+
function classifyBare(text: string, start: number, end: number, line: number): TokenKind {
|
|
310
|
+
const token = text.slice(start, end);
|
|
311
|
+
if (KEY_TEXT.test(token)) {
|
|
312
|
+
return TOKEN_WORD;
|
|
313
|
+
}
|
|
314
|
+
if (INT_TEXT.test(token)) {
|
|
315
|
+
return TOKEN_INT;
|
|
316
|
+
}
|
|
317
|
+
if (REAL_TEXT.test(token) || NON_FINITE_TEXT.test(token)) {
|
|
318
|
+
return TOKEN_REAL;
|
|
319
|
+
}
|
|
320
|
+
const shown = token.length > 20 ? `${token.slice(0, 20)}...` : token;
|
|
321
|
+
throw new GmlSyntaxError(SYNTAX_TOKEN_CODE, `cannot tokenize "${shown}" at line ${line}`, line);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Check the `key value` structure and record the matching bracket of every `[`.
|
|
326
|
+
* @param tokens - the lexed tokens
|
|
327
|
+
*/
|
|
328
|
+
function validateStructure(tokens: GmlTokens): void {
|
|
329
|
+
const { count, kind } = tokens;
|
|
330
|
+
const match = new Uint32Array(count);
|
|
331
|
+
const stack: number[] = [];
|
|
332
|
+
let i = 0;
|
|
333
|
+
while (i < count) {
|
|
334
|
+
const k = kind[i];
|
|
335
|
+
if (k === TOKEN_CLOSE) {
|
|
336
|
+
const open = stack.pop();
|
|
337
|
+
if (open === undefined) {
|
|
338
|
+
throw new GmlSyntaxError(
|
|
339
|
+
SYNTAX_STRUCTURE_CODE,
|
|
340
|
+
`unexpected "]" at line ${tokens.line[i]}: no open "["`,
|
|
341
|
+
tokens.line[i],
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
match[open] = i;
|
|
345
|
+
i++;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (k !== TOKEN_WORD) {
|
|
349
|
+
throw new GmlSyntaxError(
|
|
350
|
+
SYNTAX_STRUCTURE_CODE,
|
|
351
|
+
`expected a key at line ${tokens.line[i]}, found ${describeToken(tokens, i)}`,
|
|
352
|
+
tokens.line[i],
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const value = i + 1;
|
|
356
|
+
if (value >= count) {
|
|
357
|
+
throw new GmlSyntaxError(
|
|
358
|
+
SYNTAX_STRUCTURE_CODE,
|
|
359
|
+
`key "${tokens.textOf(i)}" at line ${tokens.line[i]} has no value`,
|
|
360
|
+
tokens.line[i],
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
const vk = kind[value];
|
|
364
|
+
if (vk === TOKEN_OPEN) {
|
|
365
|
+
stack.push(value);
|
|
366
|
+
} else if (vk === TOKEN_CLOSE || (vk === TOKEN_WORD && !NON_FINITE_WORD.test(tokens.textOf(value)))) {
|
|
367
|
+
throw new GmlSyntaxError(
|
|
368
|
+
SYNTAX_STRUCTURE_CODE,
|
|
369
|
+
`expected a value for key "${tokens.textOf(i)}" at line ${tokens.line[i]}, found ${describeToken(tokens, value)}`,
|
|
370
|
+
tokens.line[value],
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
i = value + 1;
|
|
374
|
+
}
|
|
375
|
+
if (stack.length > 0) {
|
|
376
|
+
const open = stack[stack.length - 1];
|
|
377
|
+
throw new GmlSyntaxError(
|
|
378
|
+
SYNTAX_BRACKET_CODE,
|
|
379
|
+
`unexpected end of input: ${stack.length} bracket(s) still open, the last opened at line ${tokens.line[open]}`,
|
|
380
|
+
tokens.line[open],
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
tokens.match = match;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* A short description of a token for messages.
|
|
388
|
+
* @param tokens - the tokens
|
|
389
|
+
* @param i - the token index
|
|
390
|
+
* @returns the quoted text or the bracket
|
|
391
|
+
*/
|
|
392
|
+
function describeToken(tokens: GmlTokens, i: number): string {
|
|
393
|
+
switch (tokens.kind[i]) {
|
|
394
|
+
case TOKEN_OPEN:
|
|
395
|
+
return '"["';
|
|
396
|
+
case TOKEN_CLOSE:
|
|
397
|
+
return '"]"';
|
|
398
|
+
case TOKEN_STRING:
|
|
399
|
+
return `the string "${tokens.textOf(i)}"`;
|
|
400
|
+
default:
|
|
401
|
+
return `"${tokens.textOf(i)}"`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The numeric value of an INT or REAL token (or a WORD that is `INF` / `NAN` at a value position).
|
|
407
|
+
* @param text - the token text
|
|
408
|
+
* @returns the number; Infinity / -Infinity / NaN for the non-finite spellings
|
|
409
|
+
*/
|
|
410
|
+
export function numberOfText(text: string): number {
|
|
411
|
+
if (NON_FINITE_TEXT.test(text)) {
|
|
412
|
+
const lower = text.toLowerCase();
|
|
413
|
+
if (lower.endsWith("nan")) {
|
|
414
|
+
return NaN;
|
|
415
|
+
}
|
|
416
|
+
return lower.startsWith("-") ? -Infinity : Infinity;
|
|
417
|
+
}
|
|
418
|
+
return Number(text);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Whether a value token is one of the non-finite bare words (`INF`, `NAN`), which lex as words.
|
|
423
|
+
* @param tokens - the tokens
|
|
424
|
+
* @param i - the value token index
|
|
425
|
+
* @returns true when the word is a real
|
|
426
|
+
*/
|
|
427
|
+
export function isNonFiniteWord(tokens: GmlTokens, i: number): boolean {
|
|
428
|
+
return tokens.kind[i] === TOKEN_WORD && NON_FINITE_WORD.test(tokens.textOf(i));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Whether a text is a GML key the strict grammar accepts (`[A-Za-z][0-9A-Za-z_]*`, what NetworkX
|
|
433
|
+
* writes and reads).
|
|
434
|
+
* @param text - the candidate key
|
|
435
|
+
* @returns true when it can be written as a key
|
|
436
|
+
*/
|
|
437
|
+
export function isGmlKey(text: string): boolean {
|
|
438
|
+
return STRICT_KEY_TEXT.test(text);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Rewrite a text as a GML key: every character outside `[0-9A-Za-z_]` becomes `_` and a leading
|
|
443
|
+
* non-letter is prefixed with `x` (the exporter's `sanitizeKeys: "mangle"`).
|
|
444
|
+
* @param text - the text
|
|
445
|
+
* @returns a valid key
|
|
446
|
+
*/
|
|
447
|
+
export function mangleGmlKey(text: string): string {
|
|
448
|
+
const body = text.replace(/[^0-9A-Za-z_]/g, "_");
|
|
449
|
+
return /^[A-Za-z]/.test(body) ? body : `x${body}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** One open record while parseRecord() walks the tokens without recursion. */
|
|
453
|
+
interface RecordFrame {
|
|
454
|
+
readonly record: Record<string, unknown>;
|
|
455
|
+
listKeys: Set<string> | null;
|
|
456
|
+
/** The index of the record's `]`. */
|
|
457
|
+
readonly close: number;
|
|
458
|
+
/** The next key token to read. */
|
|
459
|
+
next: number;
|
|
460
|
+
/** The key the record is stored under in its parent, or null for the root. */
|
|
461
|
+
readonly key: string | null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Parse a bracketed record into a JSON object (design section 8.5: nested GML records map to
|
|
466
|
+
* `json`). Values follow the value grammar; repeated keys become arrays with the NetworkX
|
|
467
|
+
* conventions: the `_networkx_list_start` marker starts a list so a one-element list survives,
|
|
468
|
+
* `"[]"` and `"()"` are empty lists, and a key seen twice without the marker becomes a
|
|
469
|
+
* two-element array. Nested records are walked with an explicit stack, so the nesting depth is
|
|
470
|
+
* bounded by memory, not by the call stack.
|
|
471
|
+
* @param tokens - the validated tokens
|
|
472
|
+
* @param open - the index of the record's `[`
|
|
473
|
+
* @returns a null-prototype object
|
|
474
|
+
*/
|
|
475
|
+
export function parseRecord(tokens: GmlTokens, open: number): Record<string, unknown> {
|
|
476
|
+
const root = Object.create(null) as Record<string, unknown>;
|
|
477
|
+
const stack: RecordFrame[] = [
|
|
478
|
+
{ record: root, listKeys: null, close: tokens.match[open], next: open + 1, key: null },
|
|
479
|
+
];
|
|
480
|
+
while (stack.length > 0) {
|
|
481
|
+
const frame = stack[stack.length - 1];
|
|
482
|
+
if (frame.next >= frame.close) {
|
|
483
|
+
stack.pop();
|
|
484
|
+
if (frame.key !== null) {
|
|
485
|
+
storeValue(stack[stack.length - 1], frame.key, frame.record);
|
|
486
|
+
}
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const p = frame.next;
|
|
490
|
+
frame.next = tokens.nextPair(p);
|
|
491
|
+
const key = tokens.textOf(p);
|
|
492
|
+
const v = p + 1;
|
|
493
|
+
if (tokens.kind[v] === TOKEN_STRING) {
|
|
494
|
+
const text = tokens.stringOf(v);
|
|
495
|
+
if (text === LIST_START_MARKER) {
|
|
496
|
+
frame.record[key] = [];
|
|
497
|
+
frame.listKeys ??= new Set();
|
|
498
|
+
frame.listKeys.add(key);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
storeValue(frame, key, text === EMPTY_LIST_TEXT || text === EMPTY_TUPLE_TEXT ? [] : text);
|
|
502
|
+
} else if (tokens.kind[v] === TOKEN_OPEN) {
|
|
503
|
+
stack.push({
|
|
504
|
+
record: Object.create(null) as Record<string, unknown>,
|
|
505
|
+
listKeys: null,
|
|
506
|
+
close: tokens.match[v],
|
|
507
|
+
next: v + 1,
|
|
508
|
+
key,
|
|
509
|
+
});
|
|
510
|
+
} else {
|
|
511
|
+
storeValue(frame, key, numberOfText(tokens.textOf(v)));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return root;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Store one value under a key: appended to the key's list when the key is a list, paired with the
|
|
519
|
+
* earlier value when the key repeats, set otherwise.
|
|
520
|
+
* @param frame - the open record
|
|
521
|
+
* @param key - the key
|
|
522
|
+
* @param value - the value
|
|
523
|
+
*/
|
|
524
|
+
function storeValue(frame: RecordFrame, key: string, value: unknown): void {
|
|
525
|
+
const { record } = frame;
|
|
526
|
+
if (frame.listKeys !== null && frame.listKeys.has(key)) {
|
|
527
|
+
(record[key] as unknown[]).push(value);
|
|
528
|
+
} else if (key in record) {
|
|
529
|
+
record[key] = [record[key], value];
|
|
530
|
+
frame.listKeys ??= new Set();
|
|
531
|
+
frame.listKeys.add(key);
|
|
532
|
+
} else {
|
|
533
|
+
record[key] = value;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* The JSON value of a non-string value token: a number, or a record.
|
|
539
|
+
* @param tokens - the tokens
|
|
540
|
+
* @param v - the value token index (INT, REAL, non-finite WORD or OPEN)
|
|
541
|
+
* @returns the value
|
|
542
|
+
*/
|
|
543
|
+
export function scalarOrRecord(tokens: GmlTokens, v: number): unknown {
|
|
544
|
+
return tokens.kind[v] === TOKEN_OPEN ? parseRecord(tokens, v) : numberOfText(tokens.textOf(v));
|
|
545
|
+
}
|