@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,1047 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Pajek NET importer (design sections 8.4 and 8.6; research note 07 section 2.5): a
|
|
3
|
+
* line-oriented streaming reader over the common LineReader that pushes scalars into the sink as
|
|
4
|
+
* each line is read. `*Vertices N` declares vertices 1..N (0..N-1 for the zero-based files some
|
|
5
|
+
* scripts write, detected from the first vertex line), so the id map of a well-formed file is
|
|
6
|
+
* `identity` with offset 1; vertex lines add the label, the coordinates (the position role,
|
|
7
|
+
* units "file"), the shape keyword and the `key value` parameters (a `graphty_originalId`
|
|
8
|
+
* parameter, written by the exporter under `sanitizeIds: "mangle"`, is restored as the vertex's
|
|
9
|
+
* id under `restoreMangledIds`); `*Arcs` sections are directed
|
|
10
|
+
* and `*Edges` sections undirected, resolved through the common DirectionResolver under the
|
|
11
|
+
* mixed-direction policy; the third column of a line is the weight unless `weightFrom` says
|
|
12
|
+
* otherwise; `*Arcslist` / `*Edgeslist` adjacency lists and `*Matrix` rows are read too; time
|
|
13
|
+
* interval tokens `[1-5,7-*]` become the spells role. A second network in one file is refused
|
|
14
|
+
* with an issue; project-file sections (`*Partition`, `*Vector`, ...) are reported as unsupported.
|
|
15
|
+
*/
|
|
16
|
+
import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
|
|
17
|
+
import { declareResolved, RENAMED_CODE, ROLE_TAKEN_CODE } from "../../common/attributes.js";
|
|
18
|
+
import { DUPLICATE_NODE_CODE, INVALID_UTF8_CODE, OPTION_IGNORED_CODE, SYNTAX_CODE } from "../../common/codes.js";
|
|
19
|
+
import { DirectionResolver } from "../../common/direction.js";
|
|
20
|
+
import { coerceIdText, ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
|
|
21
|
+
import { LineReader, throwIfAborted } from "../../common/input.js";
|
|
22
|
+
import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTION_CODE, } from "../../common/options.js";
|
|
23
|
+
import { ImportReportBuilder } from "../../common/report.js";
|
|
24
|
+
import { isNumericText, TextCellWriter, WIDENING_UNSUPPORTED_CODE } from "../../common/text.js";
|
|
25
|
+
import { isWeightField, parseWeightText } from "../../common/weights.js";
|
|
26
|
+
import { isCommentOrBlank, isIntervalToken, isSectionLine, isVertexNumber, LABEL_COLUMN, ORIGINAL_ID_KEY, parseIntervals, parseSectionHeader, POSITION_COLUMN, RELATION_COLUMN, SHAPE_COLUMN, SHAPES, SPELLS_COLUMN, tokenize, VALUE_COLUMN, VALUE_FIELD, } from "./syntax.js";
|
|
27
|
+
/** Issue codes of the Pajek importer. */
|
|
28
|
+
export const PAJEK_ISSUE = Object.freeze({
|
|
29
|
+
/** The input holds invalid UTF-8 (fatal). */
|
|
30
|
+
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
31
|
+
/** Fatal: no `*Vertices` section (an empty file, or not a Pajek network). */
|
|
32
|
+
NO_VERTICES: "E_PAJEK_NO_VERTICES",
|
|
33
|
+
/** Fatal: `*Vertices` without a vertex count, or one the sink cannot hold. */
|
|
34
|
+
VERTICES_COUNT: "E_PAJEK_VERTICES_COUNT",
|
|
35
|
+
/** Aborts: a second `*Vertices` or `*Network` section; only one network per file is read. */
|
|
36
|
+
MULTIPLE_NETWORKS: "E_PAJEK_MULTIPLE_NETWORKS",
|
|
37
|
+
/** A data line before the first section header. */
|
|
38
|
+
OUTSIDE_SECTION: "E_PAJEK_OUTSIDE_SECTION",
|
|
39
|
+
/** A section header the importer cannot parse. */
|
|
40
|
+
SYNTAX: SYNTAX_CODE,
|
|
41
|
+
/** A double quote not closed before the end of the line. */
|
|
42
|
+
UNTERMINATED_QUOTE: "E_PAJEK_UNTERMINATED_QUOTE",
|
|
43
|
+
/** A vertex line the grammar does not accept. */
|
|
44
|
+
VERTEX_LINE: "E_PAJEK_VERTEX_LINE",
|
|
45
|
+
/** A vertex number outside the declared range. */
|
|
46
|
+
VERTEX_RANGE: "E_PAJEK_VERTEX_RANGE",
|
|
47
|
+
/** Fewer vertex lines than `*Vertices` declares (vertices without a line have no label). */
|
|
48
|
+
VERTEX_COUNT: "E_PAJEK_VERTEX_COUNT",
|
|
49
|
+
/** A second line for the same vertex; the later values overwrite. */
|
|
50
|
+
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
51
|
+
/** A line (arc, edge, list or matrix row) the grammar does not accept. */
|
|
52
|
+
LINE: "E_PAJEK_LINE",
|
|
53
|
+
/** A line endpoint outside the declared vertex range (the core's code, forwarded). */
|
|
54
|
+
UNKNOWN_NODE: "E_UNKNOWN_NODE",
|
|
55
|
+
/** A malformed time interval token. */
|
|
56
|
+
INTERVAL: "E_PAJEK_INTERVAL",
|
|
57
|
+
/** A `*Matrix` section with the wrong number of rows. */
|
|
58
|
+
MATRIX_ROWS: "E_PAJEK_MATRIX_ROWS",
|
|
59
|
+
/** A project-file section (`*Partition`, `*Vector`, ...) the importer does not read. */
|
|
60
|
+
UNSUPPORTED_SECTION: "E_PAJEK_UNSUPPORTED_SECTION",
|
|
61
|
+
/** Tokens after a section header the grammar does not account for. */
|
|
62
|
+
HEADER_EXTRA: "W_PAJEK_HEADER_EXTRA",
|
|
63
|
+
/** The file declares vertices but no line section. */
|
|
64
|
+
NO_LINES: "W_PAJEK_NO_LINES",
|
|
65
|
+
/** Vertex numbering starts at 0 rather than 1. */
|
|
66
|
+
ZERO_BASED: "W_PAJEK_ZERO_BASED",
|
|
67
|
+
/** Vertex lines mix two and three coordinates. */
|
|
68
|
+
COORD_DIMS: "W_PAJEK_COORD_DIMS",
|
|
69
|
+
/** Two vertices share a label under nodeIdFrom "label" and became one node. */
|
|
70
|
+
LABEL_MERGED: "W_PAJEK_LABEL_MERGED",
|
|
71
|
+
/** Two distinct label texts became one numeric id under ids "number". */
|
|
72
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
73
|
+
/** A parameter column of `2.0`-style text kept at the value-inferred dtype (the sink cannot widen). */
|
|
74
|
+
WIDENING_UNSUPPORTED: WIDENING_UNSUPPORTED_CODE,
|
|
75
|
+
/** A structural column (label, position, shape, spells, relation) renamed `<name>#<id>` because the name was taken (design section 5.6). */
|
|
76
|
+
COLUMN_RENAMED: RENAMED_CODE,
|
|
77
|
+
/** A structural column declared without its role because the sink already holds it. */
|
|
78
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
79
|
+
/** Two vertices carry the same `graphty_originalId` under restoreMangledIds and became one node. */
|
|
80
|
+
ORIGINAL_ID_MERGED: "W_PAJEK_ORIGINAL_ID_MERGED",
|
|
81
|
+
/** A vertex line's `graphty_originalId` came after a later vertex's line had created it under its number. */
|
|
82
|
+
ORIGINAL_ID_UNRESTORED: "W_PAJEK_ORIGINAL_ID_UNRESTORED",
|
|
83
|
+
/** A common option the importer has no use for (weightFrom naming a parameter and restoreMangledIds are honoured; long, hyperedges are not). */
|
|
84
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
85
|
+
/** A builder-policy option the caller passed that the caller's sink does not use (the shared W_SINK_OPTION). */
|
|
86
|
+
SINK_OPTION: SINK_OPTION_CODE,
|
|
87
|
+
});
|
|
88
|
+
/** The common options the Pajek importer reads (the rest is reported by reportUnusedOptions). */
|
|
89
|
+
const USED_OPTIONS = new Set([
|
|
90
|
+
"ids",
|
|
91
|
+
"nodeIdFrom",
|
|
92
|
+
"restoreMangledIds",
|
|
93
|
+
"addMissingNodes",
|
|
94
|
+
"duplicateEdges",
|
|
95
|
+
"selfLoops",
|
|
96
|
+
"onMixedDirection",
|
|
97
|
+
"defaultDirected",
|
|
98
|
+
"weightFrom",
|
|
99
|
+
"weightDtype",
|
|
100
|
+
"errorLimit",
|
|
101
|
+
"signal",
|
|
102
|
+
"onProgress",
|
|
103
|
+
]);
|
|
104
|
+
const DEFAULTS = { ids: "canonical", defaultDirected: true, weightFrom: VALUE_FIELD };
|
|
105
|
+
/** Lines between two checks of the cancellation signal (a whole string input is one chunk). */
|
|
106
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
107
|
+
const FIRST_VERTEX_VALUES = new Set([0, 1, "auto"]);
|
|
108
|
+
const LABEL_DECL = {
|
|
109
|
+
name: LABEL_COLUMN,
|
|
110
|
+
dtype: "string",
|
|
111
|
+
nullable: true,
|
|
112
|
+
role: "label",
|
|
113
|
+
origin: { format: "pajek", id: "label" },
|
|
114
|
+
};
|
|
115
|
+
const SHAPE_DECL = {
|
|
116
|
+
name: SHAPE_COLUMN,
|
|
117
|
+
dtype: "dict",
|
|
118
|
+
nullable: true,
|
|
119
|
+
origin: { format: "pajek", id: "shape" },
|
|
120
|
+
};
|
|
121
|
+
const RELATION_DECL = {
|
|
122
|
+
name: RELATION_COLUMN,
|
|
123
|
+
dtype: "dict",
|
|
124
|
+
nullable: true,
|
|
125
|
+
origin: { format: "pajek", id: "relation" },
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* The spells column declaration of a domain (design section 5.1: a list of f64 pairs).
|
|
129
|
+
* @returns a fresh declaration
|
|
130
|
+
*/
|
|
131
|
+
function spellsDecl() {
|
|
132
|
+
return {
|
|
133
|
+
name: SPELLS_COLUMN,
|
|
134
|
+
dtype: "list",
|
|
135
|
+
itemDtype: "f64",
|
|
136
|
+
itemComponents: 2,
|
|
137
|
+
nullable: true,
|
|
138
|
+
role: "spells",
|
|
139
|
+
origin: { format: "pajek", id: "interval" },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The position column declaration (design section 5.2: f32 x 3, role position, units "file").
|
|
144
|
+
* @param sourceDims - 2 or 3, the coordinate count of the first vertex line with coordinates
|
|
145
|
+
* @returns a fresh declaration
|
|
146
|
+
*/
|
|
147
|
+
function positionDecl(sourceDims) {
|
|
148
|
+
return {
|
|
149
|
+
name: POSITION_COLUMN,
|
|
150
|
+
dtype: "f32",
|
|
151
|
+
components: 3,
|
|
152
|
+
nullable: true,
|
|
153
|
+
role: "position",
|
|
154
|
+
origin: { format: "pajek", id: "coordinates" },
|
|
155
|
+
extra: { sourceDims, units: "file" },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** A problem with one line, recorded as an issue by the per-line catch. */
|
|
159
|
+
class LineError extends Error {
|
|
160
|
+
/**
|
|
161
|
+
* Create a line error.
|
|
162
|
+
* @param category - the issue category
|
|
163
|
+
* @param code - the issue code
|
|
164
|
+
* @param message - a plain-ASCII message
|
|
165
|
+
*/
|
|
166
|
+
constructor(category, code, message) {
|
|
167
|
+
super(message);
|
|
168
|
+
this.name = "PajekLineError";
|
|
169
|
+
this.category = category;
|
|
170
|
+
this.code = code;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* One import call's state machine: the sections, the vertex range, the columns declared so far
|
|
175
|
+
* and the direction resolver.
|
|
176
|
+
*/
|
|
177
|
+
class PajekParser {
|
|
178
|
+
/**
|
|
179
|
+
* Create the parser for one import call.
|
|
180
|
+
* @param sink - the sink
|
|
181
|
+
* @param report - the report
|
|
182
|
+
* @param options - the resolved common options
|
|
183
|
+
* @param firstVertex - the firstVertex format option
|
|
184
|
+
*/
|
|
185
|
+
constructor(sink, report, options, firstVertex) {
|
|
186
|
+
this.section = "none";
|
|
187
|
+
/** The direction of the current line section. */
|
|
188
|
+
this.kind = "directed";
|
|
189
|
+
/** The relation name of the current line section, or null. */
|
|
190
|
+
this.relation = null;
|
|
191
|
+
/** `*Vertices N`; -1 before the header. */
|
|
192
|
+
this.vertexCount = -1;
|
|
193
|
+
/** The first-mode count of a two-mode network, or null. */
|
|
194
|
+
this.firstMode = null;
|
|
195
|
+
this.networkName = null;
|
|
196
|
+
this.base = 1;
|
|
197
|
+
this.baseKnown = false;
|
|
198
|
+
this.nodesCreated = false;
|
|
199
|
+
this.verticesDone = false;
|
|
200
|
+
this.sawLineSection = false;
|
|
201
|
+
/** Node index per vertex position (vertex number minus base). */
|
|
202
|
+
this.indexOfPos = new Uint32Array(0);
|
|
203
|
+
/**
|
|
204
|
+
* The node id per vertex position when the vertex lines decide the ids (nodeIdFrom "label",
|
|
205
|
+
* or a restorable `graphty_originalId` under restoreMangledIds); null when the ids are the
|
|
206
|
+
* vertex numbers and every vertex is created up front.
|
|
207
|
+
*/
|
|
208
|
+
this.idOfPos = null;
|
|
209
|
+
/** Which positions had a vertex line. */
|
|
210
|
+
this.seen = new Uint8Array(0);
|
|
211
|
+
this.seenCount = 0;
|
|
212
|
+
/**
|
|
213
|
+
* Every position below this one already has an id in `idOfPos`: the high-water mark of the
|
|
214
|
+
* gap-fill in `vertexLine`. Without it the gap-fill restarts at zero on every vertex line, which
|
|
215
|
+
* is quadratic in the vertex count for a file whose lines are in ascending order -- which is the
|
|
216
|
+
* order Pajek itself writes.
|
|
217
|
+
*/
|
|
218
|
+
this.filledBelow = 0;
|
|
219
|
+
this.matrixRows = 0;
|
|
220
|
+
this.coordDims = 0;
|
|
221
|
+
this.labelHandle = INVALID_INDEX;
|
|
222
|
+
this.positionHandle = INVALID_INDEX;
|
|
223
|
+
this.shapeHandle = INVALID_INDEX;
|
|
224
|
+
this.nodeSpellsHandle = INVALID_INDEX;
|
|
225
|
+
this.relationHandle = INVALID_INDEX;
|
|
226
|
+
this.edgeSpellsHandle = INVALID_INDEX;
|
|
227
|
+
this.nodeParams = new Map();
|
|
228
|
+
this.edgeParams = new Map();
|
|
229
|
+
this.sink = sink;
|
|
230
|
+
this.report = report;
|
|
231
|
+
this.options = options;
|
|
232
|
+
this.firstVertex = firstVertex;
|
|
233
|
+
this.resolver = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
234
|
+
this.coercer = new IdCoercer(options.ids);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Handle one line; every problem of the line is recorded as an issue and the line skipped.
|
|
238
|
+
* @param text - the line without its terminator
|
|
239
|
+
* @param line - the 1-based line number
|
|
240
|
+
*/
|
|
241
|
+
line(text, line) {
|
|
242
|
+
if (isCommentOrBlank(text)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
if (isSectionLine(text)) {
|
|
247
|
+
this.header(text, line);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
switch (this.section) {
|
|
251
|
+
case "none":
|
|
252
|
+
throw new LineError("parse-error", PAJEK_ISSUE.OUTSIDE_SECTION, "data before the first section header; a Pajek network starts with *Vertices");
|
|
253
|
+
case "vertices":
|
|
254
|
+
this.vertexLine(text, line);
|
|
255
|
+
return;
|
|
256
|
+
case "lines":
|
|
257
|
+
this.edgeLine(text, line);
|
|
258
|
+
return;
|
|
259
|
+
case "list":
|
|
260
|
+
this.listLine(text, line);
|
|
261
|
+
return;
|
|
262
|
+
case "matrix":
|
|
263
|
+
this.matrixLine(text, line);
|
|
264
|
+
return;
|
|
265
|
+
case "skip":
|
|
266
|
+
return;
|
|
267
|
+
default: {
|
|
268
|
+
const name = this.section;
|
|
269
|
+
throw new Error(`unknown section state ${name}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (err instanceof LineError) {
|
|
275
|
+
this.report.error(err.category, err.code, err.message, { line });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
this.report.recordError(err, { line });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* End of input: the vertex set is completed, a network without line sections gets the default
|
|
283
|
+
* direction, and the metadata is written.
|
|
284
|
+
*/
|
|
285
|
+
finish() {
|
|
286
|
+
if (this.vertexCount < 0) {
|
|
287
|
+
this.report.fail(PAJEK_ISSUE.NO_VERTICES, "not a Pajek network: no *Vertices section found");
|
|
288
|
+
}
|
|
289
|
+
this.endSection();
|
|
290
|
+
this.finishVertices();
|
|
291
|
+
if (!this.sawLineSection) {
|
|
292
|
+
this.report.warning("validation-error", PAJEK_ISSUE.NO_LINES, "the file declares vertices but no *Arcs, *Edges, *Arcslist, *Edgeslist or *Matrix section");
|
|
293
|
+
this.resolver.setHeader(this.options.defaultDirected);
|
|
294
|
+
}
|
|
295
|
+
if (this.coercer.mergeCount > 0) {
|
|
296
|
+
this.report.warning("coercion", PAJEK_ISSUE.ID_MERGED, `${this.coercer.mergeCount} label(s) merged into an id another label already produced under ids "number"`);
|
|
297
|
+
}
|
|
298
|
+
const meta = {
|
|
299
|
+
sourceFormat: "pajek",
|
|
300
|
+
...(this.networkName === null ? {} : { name: this.networkName }),
|
|
301
|
+
...(this.firstMode === null ? {} : { extra: { pajek: { firstMode: this.firstMode } } }),
|
|
302
|
+
};
|
|
303
|
+
this.sink.setMeta(meta);
|
|
304
|
+
}
|
|
305
|
+
// ============================================================ headers
|
|
306
|
+
/**
|
|
307
|
+
* A section header line.
|
|
308
|
+
* @param text - the line
|
|
309
|
+
* @param line - the line number
|
|
310
|
+
*/
|
|
311
|
+
header(text, line) {
|
|
312
|
+
const skipping = this.section === "skip";
|
|
313
|
+
this.endSection();
|
|
314
|
+
const h = parseSectionHeader(text);
|
|
315
|
+
if (h === null) {
|
|
316
|
+
this.section = "skip";
|
|
317
|
+
throw new LineError("parse-error", PAJEK_ISSUE.SYNTAX, `cannot read the section header "${text}"`);
|
|
318
|
+
}
|
|
319
|
+
if (h.extra.length > 0) {
|
|
320
|
+
this.report.warning("validation-error", PAJEK_ISSUE.HEADER_EXTRA, `ignored ${h.extra.length} unexpected token(s) after *${h.keyword}: ${h.extra.join(" ")}`, { line });
|
|
321
|
+
}
|
|
322
|
+
switch (h.kind) {
|
|
323
|
+
case "network":
|
|
324
|
+
if (this.networkName !== null || this.vertexCount >= 0) {
|
|
325
|
+
this.refuseSecondNetwork(h.keyword, line);
|
|
326
|
+
}
|
|
327
|
+
this.networkName = h.name ?? "";
|
|
328
|
+
this.section = "none";
|
|
329
|
+
return;
|
|
330
|
+
case "vertices":
|
|
331
|
+
if (skipping && this.vertexCount >= 0) {
|
|
332
|
+
// the `*Vertices N` line of a skipped project-file section (*Partition, *Vector)
|
|
333
|
+
this.section = "skip";
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.verticesHeader(h, line);
|
|
337
|
+
return;
|
|
338
|
+
case "arcs":
|
|
339
|
+
case "edges":
|
|
340
|
+
this.lineSection(h, line, "lines", h.kind === "arcs" ? "directed" : "undirected");
|
|
341
|
+
return;
|
|
342
|
+
case "arcslist":
|
|
343
|
+
case "edgeslist":
|
|
344
|
+
this.lineSection(h, line, "list", h.kind === "arcslist" ? "directed" : "undirected");
|
|
345
|
+
return;
|
|
346
|
+
case "matrix":
|
|
347
|
+
this.lineSection(h, line, "matrix", "directed");
|
|
348
|
+
this.matrixRows = 0;
|
|
349
|
+
return;
|
|
350
|
+
case "unsupported":
|
|
351
|
+
this.section = "skip";
|
|
352
|
+
this.report.error("unsupported", PAJEK_ISSUE.UNSUPPORTED_SECTION, `the *${h.keyword} section is not supported; its lines are skipped`, { line });
|
|
353
|
+
return;
|
|
354
|
+
default: {
|
|
355
|
+
const name = h.kind;
|
|
356
|
+
throw new Error(`unknown section kind ${name}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* `*Vertices N [N1]`.
|
|
362
|
+
* @param h - the header
|
|
363
|
+
* @param line - the line number
|
|
364
|
+
*/
|
|
365
|
+
verticesHeader(h, line) {
|
|
366
|
+
if (this.vertexCount >= 0) {
|
|
367
|
+
this.refuseSecondNetwork(h.keyword, line);
|
|
368
|
+
}
|
|
369
|
+
if (h.count === null) {
|
|
370
|
+
this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, "*Vertices needs a vertex count", { line });
|
|
371
|
+
}
|
|
372
|
+
if (!Number.isSafeInteger(h.count)) {
|
|
373
|
+
this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count}: not a count`, { line });
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
// the declared count must be reservable (the sink is the authority on what it can hold,
|
|
377
|
+
// E_TOO_LARGE beyond MAX_COUNT) before any vertex is materialised
|
|
378
|
+
this.sink.reserve(h.count);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
382
|
+
this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count}: the sink cannot hold that many (${reason})`, {
|
|
383
|
+
line,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
this.vertexCount = h.count;
|
|
387
|
+
if (h.secondCount !== null) {
|
|
388
|
+
this.firstMode = h.secondCount;
|
|
389
|
+
}
|
|
390
|
+
this.section = "vertices";
|
|
391
|
+
this.indexOfPos = new Uint32Array(h.count);
|
|
392
|
+
this.seen = new Uint8Array(h.count);
|
|
393
|
+
this.filledBelow = 0;
|
|
394
|
+
if (this.options.nodeIdFrom === "label" || this.restoresIds()) {
|
|
395
|
+
this.idOfPos = new Array(h.count);
|
|
396
|
+
}
|
|
397
|
+
if (this.firstVertex !== "auto") {
|
|
398
|
+
this.setBase(this.firstVertex, line);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* A line section header: the vertex set is completed, the first one sets the sink's direction
|
|
403
|
+
* (rule 1 of design section 8.4), later ones only switch the kind.
|
|
404
|
+
* @param h - the header
|
|
405
|
+
* @param line - the line number
|
|
406
|
+
* @param section - the parser state for the section
|
|
407
|
+
* @param kind - the direction of its lines
|
|
408
|
+
*/
|
|
409
|
+
lineSection(h, line, section, kind) {
|
|
410
|
+
if (this.vertexCount < 0) {
|
|
411
|
+
this.report.fail(PAJEK_ISSUE.NO_VERTICES, `*${h.keyword} before *Vertices; a Pajek network starts with *Vertices`, { line });
|
|
412
|
+
}
|
|
413
|
+
this.finishVertices();
|
|
414
|
+
if (!this.sawLineSection) {
|
|
415
|
+
this.sawLineSection = true;
|
|
416
|
+
this.resolver.setHeader(kind === "directed", { line });
|
|
417
|
+
}
|
|
418
|
+
this.section = section;
|
|
419
|
+
this.kind = kind;
|
|
420
|
+
this.relation = h.relation === null ? null : (h.name ?? String(h.relation));
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Refuse a second network: an issue, then ImportError with the report so far.
|
|
424
|
+
* @param keyword - the header keyword
|
|
425
|
+
* @param line - the line number
|
|
426
|
+
*/
|
|
427
|
+
refuseSecondNetwork(keyword, line) {
|
|
428
|
+
const message = `*${keyword} starts a second network; the importer reads one network per file`;
|
|
429
|
+
this.report.error("unsupported", PAJEK_ISSUE.MULTIPLE_NETWORKS, message, { line });
|
|
430
|
+
throw this.report.abort(message, { code: PAJEK_ISSUE.MULTIPLE_NETWORKS, line });
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Checks that run when a section ends.
|
|
434
|
+
*/
|
|
435
|
+
endSection() {
|
|
436
|
+
if (this.section === "matrix" && this.matrixRows !== this.vertexCount) {
|
|
437
|
+
this.report.error("validation-error", PAJEK_ISSUE.MATRIX_ROWS, `*Matrix has ${this.matrixRows} row(s); *Vertices declares ${this.vertexCount}`);
|
|
438
|
+
}
|
|
439
|
+
this.section = "none";
|
|
440
|
+
}
|
|
441
|
+
// ============================================================ vertices
|
|
442
|
+
/**
|
|
443
|
+
* Fix the vertex numbering base and, under nodeIdFrom "id" / "index", create the declared
|
|
444
|
+
* vertices in number order so the id map is identity.
|
|
445
|
+
* @param base - 0 or 1
|
|
446
|
+
* @param line - the line number
|
|
447
|
+
*/
|
|
448
|
+
setBase(base, line) {
|
|
449
|
+
this.base = base;
|
|
450
|
+
this.baseKnown = true;
|
|
451
|
+
if (base === 0) {
|
|
452
|
+
this.report.warning("coercion", PAJEK_ISSUE.ZERO_BASED, "vertex numbering starts at 0; Pajek numbers vertices from 1", { line });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Push vertices base..base+N-1 in order (nodeIdFrom "id" / "index"), so the id map is identity;
|
|
457
|
+
* called on the first vertex line (once it is known that the lines carry no `graphty_originalId`
|
|
458
|
+
* parameter to restore) or when the vertex section ends without one.
|
|
459
|
+
*/
|
|
460
|
+
createNodes() {
|
|
461
|
+
if (this.nodesCreated || this.idOfPos !== null) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.nodesCreated = true;
|
|
465
|
+
const n = this.vertexCount;
|
|
466
|
+
const { sink } = this;
|
|
467
|
+
for (let pos = 0; pos < n; pos++) {
|
|
468
|
+
this.indexOfPos[pos] = sink.addNode(this.idOfNumber(pos + this.base));
|
|
469
|
+
}
|
|
470
|
+
this.report.counts.nodes += n;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* The id of a vertex number under nodeIdFrom "id" (the number coerced by the ids rule) or
|
|
474
|
+
* "index" (the position).
|
|
475
|
+
* @param k - the vertex number
|
|
476
|
+
* @returns the id
|
|
477
|
+
*/
|
|
478
|
+
idOfNumber(k) {
|
|
479
|
+
if (this.options.nodeIdFrom === "index") {
|
|
480
|
+
return k - this.base;
|
|
481
|
+
}
|
|
482
|
+
switch (this.options.ids) {
|
|
483
|
+
case "canonical":
|
|
484
|
+
case "number":
|
|
485
|
+
return k;
|
|
486
|
+
case "keep":
|
|
487
|
+
case "string":
|
|
488
|
+
return String(k);
|
|
489
|
+
default:
|
|
490
|
+
return coerceIdText(String(k), this.options.ids);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* The id of a vertex position, for addEdge.
|
|
495
|
+
* @param pos - the vertex number minus the base
|
|
496
|
+
* @returns the id
|
|
497
|
+
*/
|
|
498
|
+
idAt(pos) {
|
|
499
|
+
if (this.idOfPos !== null) {
|
|
500
|
+
return this.idOfPos[pos];
|
|
501
|
+
}
|
|
502
|
+
return this.idOfNumber(pos + this.base);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Complete the vertex set once the vertex section is over: vertices without a line are
|
|
506
|
+
* created (under "label" or a restored `graphty_originalId` with their number as the id), and
|
|
507
|
+
* a partial list is reported.
|
|
508
|
+
*/
|
|
509
|
+
finishVertices() {
|
|
510
|
+
if (this.verticesDone) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.verticesDone = true;
|
|
514
|
+
if (!this.baseKnown) {
|
|
515
|
+
this.setBase(1, 0);
|
|
516
|
+
}
|
|
517
|
+
const n = this.vertexCount;
|
|
518
|
+
this.createNodes();
|
|
519
|
+
if (this.idOfPos !== null) {
|
|
520
|
+
for (let pos = 0; pos < n; pos++) {
|
|
521
|
+
if (this.idOfPos[pos] === undefined) {
|
|
522
|
+
this.numberedNode(this.idOfPos, pos);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// a partial list: fewer lines than declared, counting the lines skipped after an error
|
|
527
|
+
const lines = this.seenCount + this.report.counts.skippedNodes;
|
|
528
|
+
if (this.seenCount > 0 && lines < n) {
|
|
529
|
+
this.report.error("validation-error", PAJEK_ISSUE.VERTEX_COUNT, `*Vertices declares ${n} vertices but ${lines} vertex line(s) were read; the others have no label`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* A vertex line: `number [label] [x y [z]] [shape] [key value ...] [interval]`.
|
|
534
|
+
* @param text - the line
|
|
535
|
+
* @param line - the line number
|
|
536
|
+
*/
|
|
537
|
+
vertexLine(text, line) {
|
|
538
|
+
const tokens = this.tokens(text, "node");
|
|
539
|
+
if (!isVertexNumber(tokens[0])) {
|
|
540
|
+
this.report.counts.skippedNodes++;
|
|
541
|
+
throw new LineError("parse-error", PAJEK_ISSUE.VERTEX_LINE, `a vertex line starts with a vertex number, not "${tokens[0]}"`);
|
|
542
|
+
}
|
|
543
|
+
const k = Number(tokens[0]);
|
|
544
|
+
if (!this.baseKnown) {
|
|
545
|
+
this.setBase(k === 0 ? 0 : 1, line);
|
|
546
|
+
}
|
|
547
|
+
const pos = k - this.base;
|
|
548
|
+
if (pos < 0 || pos >= this.vertexCount) {
|
|
549
|
+
this.report.counts.skippedNodes++;
|
|
550
|
+
throw new LineError("validation-error", PAJEK_ISSUE.VERTEX_RANGE, `vertex ${k} is outside ${this.base}..${this.base + this.vertexCount - 1}`);
|
|
551
|
+
}
|
|
552
|
+
// parse everything before writing anything
|
|
553
|
+
let i = 1;
|
|
554
|
+
const label = tokens.length > 1 ? tokens[1] : null;
|
|
555
|
+
if (label !== null) {
|
|
556
|
+
i = 2;
|
|
557
|
+
}
|
|
558
|
+
let x = 0;
|
|
559
|
+
let y = 0;
|
|
560
|
+
let z = 0;
|
|
561
|
+
let dims = 0;
|
|
562
|
+
if (i + 1 < tokens.length && isNumericText(tokens[i]) && isNumericText(tokens[i + 1])) {
|
|
563
|
+
x = Number(tokens[i]);
|
|
564
|
+
y = Number(tokens[i + 1]);
|
|
565
|
+
i += 2;
|
|
566
|
+
dims = 2;
|
|
567
|
+
if (i < tokens.length && isNumericText(tokens[i])) {
|
|
568
|
+
z = Number(tokens[i]);
|
|
569
|
+
i++;
|
|
570
|
+
dims = 3;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else if (i < tokens.length && isNumericText(tokens[i])) {
|
|
574
|
+
this.report.counts.skippedNodes++;
|
|
575
|
+
throw new LineError("parse-error", PAJEK_ISSUE.VERTEX_LINE, `vertex ${k}: a single coordinate "${tokens[i]}"; coordinates are x y [z]`);
|
|
576
|
+
}
|
|
577
|
+
let shape = null;
|
|
578
|
+
if (i < tokens.length && SHAPES.has(tokens[i])) {
|
|
579
|
+
shape = tokens[i];
|
|
580
|
+
i++;
|
|
581
|
+
}
|
|
582
|
+
const extras = this.extras(tokens, i, "node");
|
|
583
|
+
const restored = this.restoredId(extras);
|
|
584
|
+
this.createNodes();
|
|
585
|
+
// write
|
|
586
|
+
const repeated = this.seen[pos] !== 0;
|
|
587
|
+
if (repeated) {
|
|
588
|
+
this.report.warning("merged", PAJEK_ISSUE.DUPLICATE_NODE, `vertex ${k} has more than one line; the later values overwrite`, { line, element: String(k) });
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
this.seen[pos] = 1;
|
|
592
|
+
this.seenCount++;
|
|
593
|
+
}
|
|
594
|
+
let index;
|
|
595
|
+
if (this.idOfPos !== null && !repeated && this.options.nodeIdFrom === "label") {
|
|
596
|
+
index = this.deferredNode(this.idOfPos, pos, k, label, line, "label");
|
|
597
|
+
}
|
|
598
|
+
else if (this.idOfPos !== null && this.idOfPos[pos] === undefined) {
|
|
599
|
+
// restore mode: the earlier positions without a line yet get their numbers first, so the
|
|
600
|
+
// index order is the vertex number order whatever the line order (the identity id map
|
|
601
|
+
// of an ordered file); then this vertex with its original id, or its number
|
|
602
|
+
for (let p = this.filledBelow; p < pos; p++) {
|
|
603
|
+
if (this.idOfPos[p] === undefined) {
|
|
604
|
+
this.numberedNode(this.idOfPos, p);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// after the loop on purpose: a throw part-way leaves the mark at its smaller, still
|
|
608
|
+
// correct value and the next line re-scans the range
|
|
609
|
+
if (pos > this.filledBelow) {
|
|
610
|
+
this.filledBelow = pos;
|
|
611
|
+
}
|
|
612
|
+
index = this.deferredNode(this.idOfPos, pos, k, restored, line, "originalId");
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
index = this.indexOfPos[pos];
|
|
616
|
+
if (restored !== null && !repeated) {
|
|
617
|
+
// an out-of-order line: the vertex already exists under its number
|
|
618
|
+
this.report.warning("coercion", PAJEK_ISSUE.ORIGINAL_ID_UNRESTORED, `vertex ${k} carries ${ORIGINAL_ID_KEY} ${JSON.stringify(restored)} but a later vertex's line came first and created it under its number; the original id is not restored`, { line, element: String(k) });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const { sink } = this;
|
|
622
|
+
if (label !== null) {
|
|
623
|
+
if (this.labelHandle === INVALID_INDEX) {
|
|
624
|
+
this.labelHandle = declareResolved(sink, "node", LABEL_DECL, this.report, { line }).handle;
|
|
625
|
+
}
|
|
626
|
+
sink.setNodeValue(this.labelHandle, index, label);
|
|
627
|
+
}
|
|
628
|
+
if (dims > 0) {
|
|
629
|
+
if (this.positionHandle === INVALID_INDEX) {
|
|
630
|
+
this.coordDims = dims;
|
|
631
|
+
this.positionHandle = declareResolved(sink, "node", positionDecl(dims), this.report, { line }).handle;
|
|
632
|
+
}
|
|
633
|
+
else if (dims !== this.coordDims) {
|
|
634
|
+
this.report.warnOnce("validation-error", PAJEK_ISSUE.COORD_DIMS, `vertex lines mix ${this.coordDims} and ${dims} coordinates; the position column records sourceDims ${this.coordDims}`, { line, element: String(k) });
|
|
635
|
+
}
|
|
636
|
+
sink.setNodeValue(this.positionHandle, index, [x, y, z]);
|
|
637
|
+
}
|
|
638
|
+
if (shape !== null) {
|
|
639
|
+
if (this.shapeHandle === INVALID_INDEX && this.nodeParams.has(SHAPE_COLUMN)) {
|
|
640
|
+
// an earlier `shape "<text>"` parameter owns the column: the keyword is written there
|
|
641
|
+
this.writeParameter("node", SHAPE_COLUMN, index, shape);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
if (this.shapeHandle === INVALID_INDEX) {
|
|
645
|
+
this.shapeHandle = declareResolved(sink, "node", SHAPE_DECL, this.report, { line }).handle;
|
|
646
|
+
}
|
|
647
|
+
sink.setNodeValue(this.shapeHandle, index, shape);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
for (let p = 0; p < extras.keys.length; p++) {
|
|
651
|
+
this.writeParameter("node", extras.keys[p], index, extras.values[p]);
|
|
652
|
+
}
|
|
653
|
+
if (extras.spells !== null) {
|
|
654
|
+
if (this.nodeSpellsHandle === INVALID_INDEX) {
|
|
655
|
+
this.nodeSpellsHandle = declareResolved(sink, "node", spellsDecl(), this.report, { line }).handle;
|
|
656
|
+
}
|
|
657
|
+
sink.setNodeValue(this.nodeSpellsHandle, index, extras.spells);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Whether a `graphty_originalId` parameter is restored as the vertex id: restoreMangledIds
|
|
662
|
+
* under nodeIdFrom "id" (under "label" / "index" the parameter stays an ordinary column).
|
|
663
|
+
* @returns true when the vertex lines may carry restorable ids
|
|
664
|
+
*/
|
|
665
|
+
restoresIds() {
|
|
666
|
+
return this.options.restoreMangledIds && this.options.nodeIdFrom === "id";
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Push a vertex whose id is its number (restore mode, a position whose line has not come or
|
|
670
|
+
* carries no original id; or the end of the vertex section under "label").
|
|
671
|
+
* @param ids - the id per vertex position
|
|
672
|
+
* @param pos - the vertex position
|
|
673
|
+
*/
|
|
674
|
+
numberedNode(ids, pos) {
|
|
675
|
+
const id = this.idOfNumber(pos + this.base);
|
|
676
|
+
ids[pos] = id;
|
|
677
|
+
this.indexOfPos[pos] = this.sink.addNode(id);
|
|
678
|
+
this.report.counts.nodes++;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* The `graphty_originalId` parameter of a vertex line, taken out of the extras when it is to be
|
|
682
|
+
* restored as the id (restoreMangledIds, under nodeIdFrom "id"); otherwise null and the
|
|
683
|
+
* parameter stays an ordinary column.
|
|
684
|
+
* @param extras - the parsed extras
|
|
685
|
+
* @returns the original id text, or null
|
|
686
|
+
*/
|
|
687
|
+
restoredId(extras) {
|
|
688
|
+
if (!this.restoresIds()) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const at = extras.keys.indexOf(ORIGINAL_ID_KEY);
|
|
692
|
+
if (at < 0) {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
const [text] = extras.values.splice(at, 1);
|
|
696
|
+
extras.keys.splice(at, 1);
|
|
697
|
+
return text;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Push the node of a vertex line whose id comes from the line itself: its label under
|
|
701
|
+
* nodeIdFrom "label", or its restored `graphty_originalId` (the number when the line carries
|
|
702
|
+
* none), reporting a merge when the id was seen before.
|
|
703
|
+
* @param ids - the id per vertex position
|
|
704
|
+
* @param pos - the vertex position
|
|
705
|
+
* @param k - the vertex number
|
|
706
|
+
* @param text - the label or original id text, or null
|
|
707
|
+
* @param line - the line number
|
|
708
|
+
* @param from - which text the id comes from
|
|
709
|
+
* @returns the node index
|
|
710
|
+
*/
|
|
711
|
+
deferredNode(ids, pos, k, text, line, from) {
|
|
712
|
+
const id = text === null ? this.idOfNumber(k) : this.coercer.text(text);
|
|
713
|
+
const before = this.sink.indexOf(id);
|
|
714
|
+
const index = this.sink.addNode(id);
|
|
715
|
+
ids[pos] = id;
|
|
716
|
+
this.indexOfPos[pos] = index;
|
|
717
|
+
this.report.counts.nodes++;
|
|
718
|
+
if (before !== INVALID_INDEX) {
|
|
719
|
+
this.report.warning("merged", from === "label" ? PAJEK_ISSUE.LABEL_MERGED : PAJEK_ISSUE.ORIGINAL_ID_MERGED, from === "label"
|
|
720
|
+
? `vertex ${k} has the label ${JSON.stringify(String(id))} of an earlier vertex; both are one node`
|
|
721
|
+
: `vertex ${k} has the ${ORIGINAL_ID_KEY} ${JSON.stringify(String(id))} of an earlier vertex; both are one node`, { line, element: String(k) });
|
|
722
|
+
}
|
|
723
|
+
return index;
|
|
724
|
+
}
|
|
725
|
+
// ============================================================ lines
|
|
726
|
+
/**
|
|
727
|
+
* An arc or edge line: `u v [value] [key value ...] [interval]`.
|
|
728
|
+
* @param text - the line
|
|
729
|
+
* @param line - the line number
|
|
730
|
+
*/
|
|
731
|
+
edgeLine(text, line) {
|
|
732
|
+
const tokens = this.tokens(text, "edge");
|
|
733
|
+
if (tokens.length < 2 || !isVertexNumber(tokens[0]) || !isVertexNumber(tokens[1])) {
|
|
734
|
+
this.report.counts.skippedEdges++;
|
|
735
|
+
throw new LineError("parse-error", PAJEK_ISSUE.LINE, `a line starts with two vertex numbers, not "${tokens.slice(0, 2).join(" ")}"`);
|
|
736
|
+
}
|
|
737
|
+
const u = this.endpoint(Number(tokens[0]));
|
|
738
|
+
const v = this.endpoint(Number(tokens[1]));
|
|
739
|
+
let i = 2;
|
|
740
|
+
let valueText = null;
|
|
741
|
+
if (i < tokens.length && isNumericText(tokens[i])) {
|
|
742
|
+
valueText = tokens[i];
|
|
743
|
+
i++;
|
|
744
|
+
}
|
|
745
|
+
const extras = this.extras(tokens, i, "edge");
|
|
746
|
+
const { weightFrom } = this.options;
|
|
747
|
+
let weight;
|
|
748
|
+
let valueIsWeight = false;
|
|
749
|
+
if (isWeightField(VALUE_FIELD, weightFrom)) {
|
|
750
|
+
valueIsWeight = true;
|
|
751
|
+
if (valueText !== null) {
|
|
752
|
+
weight = this.weightOf(valueText);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else if (weightFrom !== null) {
|
|
756
|
+
const at = extras.keys.indexOf(weightFrom);
|
|
757
|
+
if (at >= 0) {
|
|
758
|
+
weight = this.weightOf(extras.values[at]);
|
|
759
|
+
extras.keys.splice(at, 1);
|
|
760
|
+
extras.values.splice(at, 1);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const e = this.pushEdge(u, v, weight, line);
|
|
764
|
+
if (valueText !== null && !valueIsWeight) {
|
|
765
|
+
// inferred like every parameter, so a re-import of the exporter's `value` parameter agrees
|
|
766
|
+
this.writeParameter("edge", VALUE_COLUMN, e, valueText);
|
|
767
|
+
}
|
|
768
|
+
this.writeEdgeExtras(e, extras);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2 ...`.
|
|
772
|
+
* @param text - the line
|
|
773
|
+
* @param line - the line number
|
|
774
|
+
*/
|
|
775
|
+
listLine(text, line) {
|
|
776
|
+
const tokens = this.tokens(text, "edge");
|
|
777
|
+
for (const token of tokens) {
|
|
778
|
+
if (!isVertexNumber(token)) {
|
|
779
|
+
this.report.counts.skippedEdges++;
|
|
780
|
+
throw new LineError("parse-error", PAJEK_ISSUE.LINE, `an adjacency list holds vertex numbers only, not "${token}"`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const u = this.endpoint(Number(tokens[0]));
|
|
784
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
785
|
+
const v = this.endpoint(Number(tokens[i]));
|
|
786
|
+
const e = this.pushEdge(u, v, undefined, line);
|
|
787
|
+
this.writeRelation(e);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* A `*Matrix` row: N values, every non-zero entry an arc from the row's vertex with that value.
|
|
792
|
+
* @param text - the line
|
|
793
|
+
* @param line - the line number
|
|
794
|
+
*/
|
|
795
|
+
matrixLine(text, line) {
|
|
796
|
+
const tokens = this.tokens(text, "edge");
|
|
797
|
+
const row = this.matrixRows;
|
|
798
|
+
this.matrixRows++;
|
|
799
|
+
if (row >= this.vertexCount) {
|
|
800
|
+
throw new LineError("validation-error", PAJEK_ISSUE.MATRIX_ROWS, `*Matrix row ${row + 1} is beyond the ${this.vertexCount} declared vertices`);
|
|
801
|
+
}
|
|
802
|
+
if (tokens.length !== this.vertexCount) {
|
|
803
|
+
throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*Matrix row ${row + 1} has ${tokens.length} value(s); *Vertices declares ${this.vertexCount}`);
|
|
804
|
+
}
|
|
805
|
+
for (const token of tokens) {
|
|
806
|
+
if (!isNumericText(token)) {
|
|
807
|
+
throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*Matrix row ${row + 1}: "${token}" is not a number`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
for (let j = 0; j < tokens.length; j++) {
|
|
811
|
+
const value = Number(tokens[j]);
|
|
812
|
+
if (value === 0) {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const e = this.pushEdge(row, j, value, line);
|
|
816
|
+
this.writeRelation(e);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Push one line through the direction resolver, counting the logical edges it produced.
|
|
821
|
+
* @param u - the source position
|
|
822
|
+
* @param v - the target position
|
|
823
|
+
* @param weight - the weight, or undefined for none
|
|
824
|
+
* @param line - the line number
|
|
825
|
+
* @returns the primary logical edge index
|
|
826
|
+
*/
|
|
827
|
+
pushEdge(u, v, weight, line) {
|
|
828
|
+
const { sink } = this;
|
|
829
|
+
const before = sink.edgeCount;
|
|
830
|
+
const e = this.resolver.addEdge(this.idAt(u), this.idAt(v), this.kind, weight, { line });
|
|
831
|
+
this.report.counts.edges += sink.edgeCount - before;
|
|
832
|
+
return e;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Write the relation, parameters and spells of a line on its primary half.
|
|
836
|
+
* @param e - the edge index
|
|
837
|
+
* @param extras - the parsed extras
|
|
838
|
+
*/
|
|
839
|
+
writeEdgeExtras(e, extras) {
|
|
840
|
+
const { sink } = this;
|
|
841
|
+
this.writeRelation(e);
|
|
842
|
+
for (let p = 0; p < extras.keys.length; p++) {
|
|
843
|
+
this.writeParameter("edge", extras.keys[p], e, extras.values[p]);
|
|
844
|
+
}
|
|
845
|
+
if (extras.spells !== null) {
|
|
846
|
+
if (this.edgeSpellsHandle === INVALID_INDEX) {
|
|
847
|
+
this.edgeSpellsHandle = declareResolved(sink, "edge", spellsDecl(), this.report).handle;
|
|
848
|
+
}
|
|
849
|
+
sink.setEdgeValue(this.edgeSpellsHandle, e, extras.spells);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Write the current section's relation name on an edge, when the section has one.
|
|
854
|
+
* @param e - the edge index
|
|
855
|
+
*/
|
|
856
|
+
writeRelation(e) {
|
|
857
|
+
if (this.relation === null) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (this.relationHandle === INVALID_INDEX) {
|
|
861
|
+
this.relationHandle = declareResolved(this.sink, "edge", RELATION_DECL, this.report).handle;
|
|
862
|
+
}
|
|
863
|
+
this.sink.setEdgeValue(this.relationHandle, e, this.relation);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* A line endpoint: the vertex number checked against the declared range; a reference outside
|
|
867
|
+
* it is a missing-value error and the line is skipped (Pajek's `*Vertices N` bounds the id space,
|
|
868
|
+
* so addMissingNodes does not apply).
|
|
869
|
+
* @param k - the vertex number
|
|
870
|
+
* @returns the vertex position
|
|
871
|
+
*/
|
|
872
|
+
endpoint(k) {
|
|
873
|
+
const pos = k - this.base;
|
|
874
|
+
if (pos < 0 || pos >= this.vertexCount) {
|
|
875
|
+
this.report.counts.skippedEdges++;
|
|
876
|
+
throw new LineError("missing-value", PAJEK_ISSUE.UNKNOWN_NODE, `vertex ${k} is outside ${this.base}..${this.base + this.vertexCount - 1}`);
|
|
877
|
+
}
|
|
878
|
+
return pos;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* The weight of a line value text.
|
|
882
|
+
* @param text - the text
|
|
883
|
+
* @returns the weight; E_INVALID_WEIGHT is recorded by the caller's catch
|
|
884
|
+
*/
|
|
885
|
+
weightOf(text) {
|
|
886
|
+
try {
|
|
887
|
+
return parseWeightText(text);
|
|
888
|
+
}
|
|
889
|
+
catch (err) {
|
|
890
|
+
this.report.counts.skippedEdges++;
|
|
891
|
+
throw err;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// ============================================================ shared
|
|
895
|
+
/**
|
|
896
|
+
* Tokenize a data line.
|
|
897
|
+
* @param text - the line
|
|
898
|
+
* @param domain - what is skipped on an unterminated quote
|
|
899
|
+
* @returns the tokens (at least one: blank lines never reach here)
|
|
900
|
+
*/
|
|
901
|
+
tokens(text, domain) {
|
|
902
|
+
const tokens = tokenize(text);
|
|
903
|
+
if (tokens === null) {
|
|
904
|
+
if (domain === "node") {
|
|
905
|
+
this.report.counts.skippedNodes++;
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
this.report.counts.skippedEdges++;
|
|
909
|
+
}
|
|
910
|
+
throw new LineError("parse-error", PAJEK_ISSUE.UNTERMINATED_QUOTE, "a double quote is not closed");
|
|
911
|
+
}
|
|
912
|
+
return tokens;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* The `key value` parameters and the interval token at the end of a row, validated before
|
|
916
|
+
* anything is written.
|
|
917
|
+
* @param tokens - the row's tokens
|
|
918
|
+
* @param start - the first token to read
|
|
919
|
+
* @param domain - what is skipped when the row is malformed
|
|
920
|
+
* @returns the parsed extras
|
|
921
|
+
*/
|
|
922
|
+
extras(tokens, start, domain) {
|
|
923
|
+
const extras = { keys: [], values: [], spells: null };
|
|
924
|
+
for (let i = start; i < tokens.length;) {
|
|
925
|
+
const token = tokens[i];
|
|
926
|
+
if (isIntervalToken(token)) {
|
|
927
|
+
try {
|
|
928
|
+
extras.spells = parseIntervals(token);
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
this.skip(domain);
|
|
932
|
+
throw new LineError("validation-error", PAJEK_ISSUE.INTERVAL, err instanceof Error ? err.message : String(err));
|
|
933
|
+
}
|
|
934
|
+
i++;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (i + 1 >= tokens.length) {
|
|
938
|
+
this.skip(domain);
|
|
939
|
+
throw new LineError("parse-error", domain === "node" ? PAJEK_ISSUE.VERTEX_LINE : PAJEK_ISSUE.LINE, `parameter "${token}" has no value`);
|
|
940
|
+
}
|
|
941
|
+
extras.keys.push(token);
|
|
942
|
+
extras.values.push(tokens[i + 1]);
|
|
943
|
+
i += 2;
|
|
944
|
+
}
|
|
945
|
+
return extras;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Count a skipped element.
|
|
949
|
+
* @param domain - node or edge
|
|
950
|
+
*/
|
|
951
|
+
skip(domain) {
|
|
952
|
+
if (domain === "node") {
|
|
953
|
+
this.report.counts.skippedNodes++;
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
this.report.counts.skippedEdges++;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Write one `key value` parameter cell through the column's text writer (the 5.1 grammar per
|
|
961
|
+
* column, the lexical form kept). A `shape` parameter next to the shape keyword column goes
|
|
962
|
+
* into that column (a dict takes any text), and a shape keyword next to a `shape` parameter
|
|
963
|
+
* column into that one, so a file mixing the two spellings reads as one column.
|
|
964
|
+
* @param domain - node or edge
|
|
965
|
+
* @param key - the parameter key (the column name)
|
|
966
|
+
* @param row - the node or edge index
|
|
967
|
+
* @param text - the value text
|
|
968
|
+
*/
|
|
969
|
+
writeParameter(domain, key, row, text) {
|
|
970
|
+
if (domain === "node" && key === SHAPE_COLUMN && this.shapeHandle !== INVALID_INDEX) {
|
|
971
|
+
this.sink.setNodeValue(this.shapeHandle, row, text);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const writers = domain === "node" ? this.nodeParams : this.edgeParams;
|
|
975
|
+
let writer = writers.get(key);
|
|
976
|
+
if (writer === undefined) {
|
|
977
|
+
writer = new TextCellWriter(key, domain, this.sink, this.report);
|
|
978
|
+
writers.set(key, writer);
|
|
979
|
+
}
|
|
980
|
+
writer.write(row, text);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Resolve the firstVertex option.
|
|
985
|
+
* @param value - the caller's value
|
|
986
|
+
* @returns 0, 1 or "auto"; E_UNSUPPORTED for anything else
|
|
987
|
+
*/
|
|
988
|
+
function firstVertexOption(value) {
|
|
989
|
+
if (value === undefined) {
|
|
990
|
+
return "auto";
|
|
991
|
+
}
|
|
992
|
+
if (FIRST_VERTEX_VALUES.has(value)) {
|
|
993
|
+
return value;
|
|
994
|
+
}
|
|
995
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option firstVertex: ${JSON.stringify(value) ?? typeof value} is not 0, 1 or "auto"`, {
|
|
996
|
+
option: "firstVertex",
|
|
997
|
+
found: value,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
const HEAD_PATTERN = /^\s*\*(vertices|network)\b/i;
|
|
1001
|
+
/** The Pajek NET importer. */
|
|
1002
|
+
export const pajekImporter = Object.freeze({
|
|
1003
|
+
format: "pajek",
|
|
1004
|
+
extensions: Object.freeze([".net", ".paj"]),
|
|
1005
|
+
mimeTypes: Object.freeze(["text/x-pajek", "text/plain"]),
|
|
1006
|
+
/**
|
|
1007
|
+
* Confidence that the input is a Pajek network: it starts with `*Vertices` (or `*Network`).
|
|
1008
|
+
* @param head - the first bytes
|
|
1009
|
+
* @returns 0.9 for a `*Vertices` start, 0.8 for `*Network`, 0 otherwise
|
|
1010
|
+
*/
|
|
1011
|
+
sniff(head) {
|
|
1012
|
+
const text = new TextDecoder("utf-8").decode(head.subarray(0, Math.min(head.byteLength, 512)));
|
|
1013
|
+
const match = HEAD_PATTERN.exec(text.startsWith(String.fromCharCode(0xfeff)) ? text.slice(1) : text);
|
|
1014
|
+
if (match === null) {
|
|
1015
|
+
return 0;
|
|
1016
|
+
}
|
|
1017
|
+
return match[1].toLowerCase() === "vertices" ? 0.9 : 0.8;
|
|
1018
|
+
},
|
|
1019
|
+
/**
|
|
1020
|
+
* Read a Pajek network into the sink.
|
|
1021
|
+
* @param input - the text, bytes or stream
|
|
1022
|
+
* @param sink - the sink
|
|
1023
|
+
* @param options - common and Pajek options
|
|
1024
|
+
* @returns the report
|
|
1025
|
+
*/
|
|
1026
|
+
async import(input, sink, options) {
|
|
1027
|
+
const resolved = resolveImportOptions(options, DEFAULTS);
|
|
1028
|
+
const firstVertex = firstVertexOption(options?.firstVertex);
|
|
1029
|
+
const report = new ImportReportBuilder("pajek", resolved.errorLimit);
|
|
1030
|
+
reportSinkOptions(sink, options, report);
|
|
1031
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
1032
|
+
const parser = new PajekParser(sink, report, resolved, firstVertex);
|
|
1033
|
+
const reader = new LineReader(input, report, resolved);
|
|
1034
|
+
let sinceCheck = 0;
|
|
1035
|
+
for await (const text of reader) {
|
|
1036
|
+
parser.line(text, reader.line);
|
|
1037
|
+
if (++sinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
1038
|
+
sinceCheck = 0;
|
|
1039
|
+
throwIfAborted(resolved.signal);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
parser.finish();
|
|
1043
|
+
throwIfAborted(resolved.signal);
|
|
1044
|
+
return report.finish();
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
//# sourceMappingURL=importer.js.map
|