@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,2015 @@
|
|
|
1
|
+
import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
|
|
2
|
+
import { L as LOSS, au as joinText, ah as encodeChunks, aB as resolveExportOptions, aD as sanitizeIds, aa as checkCapabilities, aw as pairFolding, a9 as capabilities, ai as explicitWeights, al as formatF64, aj as formatDecimal, ak as formatF32, an as formatInteger, a8 as canonicalId, W as SINK_OPTION_CODE, J as OPTION_IGNORED_CODE, V as ROLE_TAKEN_CODE, S as RENAMED_CODE, n as ID_MERGED_CODE, k as DUPLICATE_NODE_CODE, Z as SYNTAX_CODE, r as INVALID_UTF8_CODE, aC as resolveImportOptions, I as ImportReportBuilder, az as reportSinkOptions, aA as reportUnusedOptions, v as LineReader, a as throwIfAborted, l as DirectionResolver, s as IdCoercer, ac as coerceIdText, af as declareResolved, at as isWeightField, ax as parseWeightText } from "./writer-DxSKC7TL.js";
|
|
3
|
+
import { i as isPajekLabel, q as quotePajekLabel } from "./escape-DyI8JofU.js";
|
|
4
|
+
import { W as WIDENING_UNSUPPORTED_CODE, a as isNumericText, T as TextCellWriter } from "./text-CajMdVFy.js";
|
|
5
|
+
const LABEL_COLUMN = "label";
|
|
6
|
+
const POSITION_COLUMN = "position";
|
|
7
|
+
const SHAPE_COLUMN = "shape";
|
|
8
|
+
const RELATION_COLUMN = "relation";
|
|
9
|
+
const SPELLS_COLUMN = "spells";
|
|
10
|
+
const VALUE_COLUMN = "value";
|
|
11
|
+
const VALUE_FIELD = "value";
|
|
12
|
+
const SHAPES = /* @__PURE__ */ new Set(["ellipse", "box", "diamond", "triangle", "cross", "empty"]);
|
|
13
|
+
const ORIGINAL_ID_KEY = "graphty_originalId";
|
|
14
|
+
const SECTION_KINDS = /* @__PURE__ */ new Map([
|
|
15
|
+
["network", "network"],
|
|
16
|
+
["vertices", "vertices"],
|
|
17
|
+
["arcs", "arcs"],
|
|
18
|
+
["edges", "edges"],
|
|
19
|
+
["arcslist", "arcslist"],
|
|
20
|
+
["edgeslist", "edgeslist"],
|
|
21
|
+
["matrix", "matrix"]
|
|
22
|
+
]);
|
|
23
|
+
const INTEGER_TEXT = /^[+-]?[0-9]+$/;
|
|
24
|
+
const TIME_POINT = /^(\*|[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)(?:-(\*|[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?))?$/;
|
|
25
|
+
function tokenize(line) {
|
|
26
|
+
const tokens = [];
|
|
27
|
+
let current = "";
|
|
28
|
+
let started = false;
|
|
29
|
+
let quoted = false;
|
|
30
|
+
for (let i = 0; i < line.length; i++) {
|
|
31
|
+
const c = line.charCodeAt(i);
|
|
32
|
+
if (c === 34) {
|
|
33
|
+
quoted = !quoted;
|
|
34
|
+
started = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!quoted && (c === 32 || c === 9 || c === 13 || c === 12 || c === 11)) {
|
|
38
|
+
if (started) {
|
|
39
|
+
tokens.push(current);
|
|
40
|
+
current = "";
|
|
41
|
+
started = false;
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
current += line[i];
|
|
46
|
+
started = true;
|
|
47
|
+
}
|
|
48
|
+
if (quoted) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (started) {
|
|
52
|
+
tokens.push(current);
|
|
53
|
+
}
|
|
54
|
+
return tokens;
|
|
55
|
+
}
|
|
56
|
+
function isCommentOrBlank(line) {
|
|
57
|
+
for (let i = 0; i < line.length; i++) {
|
|
58
|
+
const c = line.charCodeAt(i);
|
|
59
|
+
if (c === 32 || c === 9 || c === 13 || c === 12 || c === 11) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
return c === 37;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
function isSectionLine(line) {
|
|
67
|
+
for (let i = 0; i < line.length; i++) {
|
|
68
|
+
const c = line.charCodeAt(i);
|
|
69
|
+
if (c === 32 || c === 9 || c === 13 || c === 12 || c === 11) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
return c === 42;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function parseSectionHeader(line) {
|
|
77
|
+
const tokens = tokenize(line.trimStart().slice(1));
|
|
78
|
+
if (tokens === null || tokens.length === 0 || tokens[0].length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const keyword = tokens[0];
|
|
82
|
+
const kind = SECTION_KINDS.get(keyword.toLowerCase()) ?? "unsupported";
|
|
83
|
+
let count = null;
|
|
84
|
+
let secondCount = null;
|
|
85
|
+
let relation = null;
|
|
86
|
+
let name = null;
|
|
87
|
+
const extra = [];
|
|
88
|
+
let i = 1;
|
|
89
|
+
switch (kind) {
|
|
90
|
+
case "vertices":
|
|
91
|
+
if (i < tokens.length && INTEGER_TEXT.test(tokens[i])) {
|
|
92
|
+
count = Number(tokens[i]);
|
|
93
|
+
i++;
|
|
94
|
+
if (i < tokens.length && INTEGER_TEXT.test(tokens[i])) {
|
|
95
|
+
secondCount = Number(tokens[i]);
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case "arcs":
|
|
101
|
+
case "edges":
|
|
102
|
+
case "arcslist":
|
|
103
|
+
case "edgeslist":
|
|
104
|
+
case "matrix":
|
|
105
|
+
if (i < tokens.length && tokens[i].startsWith(":") && INTEGER_TEXT.test(tokens[i].slice(1))) {
|
|
106
|
+
relation = Number(tokens[i].slice(1));
|
|
107
|
+
i++;
|
|
108
|
+
if (i < tokens.length) {
|
|
109
|
+
name = tokens[i];
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
case "network":
|
|
115
|
+
if (i < tokens.length) {
|
|
116
|
+
name = tokens.slice(i).join(" ");
|
|
117
|
+
i = tokens.length;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
case "unsupported":
|
|
121
|
+
i = tokens.length;
|
|
122
|
+
break;
|
|
123
|
+
default: {
|
|
124
|
+
const unknown = kind;
|
|
125
|
+
throw new Error(`unknown section kind ${unknown}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (; i < tokens.length; i++) {
|
|
129
|
+
extra.push(tokens[i]);
|
|
130
|
+
}
|
|
131
|
+
return { kind, keyword, count, secondCount, relation, name, extra };
|
|
132
|
+
}
|
|
133
|
+
function isVertexNumber(token) {
|
|
134
|
+
if (token.length === 0) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
for (let i = 0; i < token.length; i++) {
|
|
138
|
+
const c = token.charCodeAt(i);
|
|
139
|
+
if (c < 48 || c > 57) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
function isIntervalToken(token) {
|
|
146
|
+
return token.length >= 2 && token.startsWith("[") && token.endsWith("]");
|
|
147
|
+
}
|
|
148
|
+
function parseIntervals(token) {
|
|
149
|
+
const body = token.slice(1, -1);
|
|
150
|
+
if (body.trim().length === 0) {
|
|
151
|
+
throw new Error(`empty time interval ${token}`);
|
|
152
|
+
}
|
|
153
|
+
const spells = [];
|
|
154
|
+
for (const part of body.split(",")) {
|
|
155
|
+
const match = TIME_POINT.exec(part.trim());
|
|
156
|
+
if (match === null) {
|
|
157
|
+
throw new Error(`malformed time interval ${token}: "${part}" is not a-b, a-* or a`);
|
|
158
|
+
}
|
|
159
|
+
const start = match[1] === "*" ? -Infinity : Number(match[1]);
|
|
160
|
+
const endText = match[2];
|
|
161
|
+
let end;
|
|
162
|
+
if (endText === void 0) {
|
|
163
|
+
end = start;
|
|
164
|
+
} else if (endText === "*") {
|
|
165
|
+
end = Infinity;
|
|
166
|
+
} else {
|
|
167
|
+
end = Number(endText);
|
|
168
|
+
}
|
|
169
|
+
if (start > end) {
|
|
170
|
+
throw new Error(`malformed time interval ${token}: ${match[1]} is after ${endText ?? ""}`);
|
|
171
|
+
}
|
|
172
|
+
spells.push([start, end]);
|
|
173
|
+
}
|
|
174
|
+
return spells;
|
|
175
|
+
}
|
|
176
|
+
function formatIntervals(spells) {
|
|
177
|
+
const parts = [];
|
|
178
|
+
for (const [start, end] of spells) {
|
|
179
|
+
const s = start === -Infinity ? "*" : String(start);
|
|
180
|
+
if (start === end) {
|
|
181
|
+
parts.push(s);
|
|
182
|
+
} else {
|
|
183
|
+
parts.push(`${s}-${end === Infinity ? "*" : String(end)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return `[${parts.join(",")}]`;
|
|
187
|
+
}
|
|
188
|
+
function isParameterKey(text) {
|
|
189
|
+
if (text.length === 0 || /[\s"]/.test(text) || text.startsWith("[") || text.startsWith("*")) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (SHAPES.has(text)) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return !/^[+-]?(\.[0-9]|[0-9])/.test(text);
|
|
196
|
+
}
|
|
197
|
+
const PAJEK_LOSS = Object.freeze({
|
|
198
|
+
/** A label or text value holds a double quote or a line break; export() will throw E_UNSUPPORTED. */
|
|
199
|
+
TEXT: "E_PAJEK_TEXT",
|
|
200
|
+
/** A column whose name cannot be a parameter key (whitespace, a quote, numeric, a shape keyword); skipped. */
|
|
201
|
+
KEY_DROPPED: "W_PAJEK_KEY_DROPPED",
|
|
202
|
+
/** A label role column that is not text; its values are written as text and re-import as string. */
|
|
203
|
+
LABEL_AS_TEXT: "W_PAJEK_LABEL_AS_TEXT",
|
|
204
|
+
/** A non-finite f64 value; written as Infinity / NaN text, which re-imports as string. */
|
|
205
|
+
NONFINITE_AS_TEXT: "W_PAJEK_NONFINITE_AS_TEXT",
|
|
206
|
+
/** A mutual pair; written as one undirected edge, the mark lost. */
|
|
207
|
+
MUTUAL_AS_UNDIRECTED: LOSS.MUTUAL_AS_UNDIRECTED,
|
|
208
|
+
/** A start / end / timestamp role column; Pajek intervals are written from the spells role only. */
|
|
209
|
+
TEMPORAL_DROPPED: LOSS.TEMPORAL,
|
|
210
|
+
/** A position column with a stride other than 2 or 3. */
|
|
211
|
+
POSITION_STRIDE: "W_PAJEK_POSITION_STRIDE",
|
|
212
|
+
/** meta.extra.pajek.firstMode is not a count within 0..N; the two-mode header is not written. */
|
|
213
|
+
FIRST_MODE_DROPPED: "W_PAJEK_FIRST_MODE_DROPPED",
|
|
214
|
+
/** A role column Pajek has no slot for (kind, ...) written as a plain parameter; the role is lost. */
|
|
215
|
+
ROLE_DROPPED: LOSS.ROLE,
|
|
216
|
+
/** A `shape` column with a value outside the shape keywords is written as a parameter (a string on re-import). */
|
|
217
|
+
SHAPE_AS_PARAMETER: "W_PAJEK_SHAPE_AS_PARAMETER",
|
|
218
|
+
/** A vertex line with coordinates, a shape or parameters needs a label: the id text is written and reads back as a label. */
|
|
219
|
+
LABEL_GAINED: "W_PAJEK_LABEL_GAINED",
|
|
220
|
+
/** A role-less node column named `label` reads back with the label role (its values become the vertex labels). */
|
|
221
|
+
ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
|
|
222
|
+
/** Under sanitizeIds "mangle": an original id whose text reads back as the other type under ids "canonical". */
|
|
223
|
+
ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE
|
|
224
|
+
});
|
|
225
|
+
const SLOT_ROLES = /* @__PURE__ */ new Set(["label"]);
|
|
226
|
+
const ROLE_NAMES = Object.freeze({ label: "label" });
|
|
227
|
+
const STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
228
|
+
"directed",
|
|
229
|
+
"pair",
|
|
230
|
+
"mutual",
|
|
231
|
+
"weight",
|
|
232
|
+
"timeText",
|
|
233
|
+
"originalId"
|
|
234
|
+
]);
|
|
235
|
+
const CHECKED_ROLES = /* @__PURE__ */ new Set([
|
|
236
|
+
"color",
|
|
237
|
+
"size",
|
|
238
|
+
"shape",
|
|
239
|
+
"thickness",
|
|
240
|
+
"parent",
|
|
241
|
+
"parents",
|
|
242
|
+
"open"
|
|
243
|
+
]);
|
|
244
|
+
const DROPPED_TEMPORAL_ROLES = /* @__PURE__ */ new Set(["start", "end", "timestamp", "timestamps"]);
|
|
245
|
+
const CAPABILITIES = capabilities({
|
|
246
|
+
mixedDirection: true,
|
|
247
|
+
multiEdges: true,
|
|
248
|
+
selfLoops: true,
|
|
249
|
+
edgeIds: "none",
|
|
250
|
+
idCharset: "dense-1-based",
|
|
251
|
+
dtypes: ["f64", "i32", "bool", "string"],
|
|
252
|
+
temporal: "spells",
|
|
253
|
+
positions: true
|
|
254
|
+
});
|
|
255
|
+
function plan(table, domain, notes) {
|
|
256
|
+
const planned = [];
|
|
257
|
+
const rows = (column) => column.length - column.nullCount;
|
|
258
|
+
for (const column of table) {
|
|
259
|
+
const { meta } = column;
|
|
260
|
+
const { role, name, dtype } = meta;
|
|
261
|
+
if (role === "label" && domain === "node") {
|
|
262
|
+
if (dtype !== "string" && dtype !== "dict" && notes !== null) {
|
|
263
|
+
notes.push(
|
|
264
|
+
note(
|
|
265
|
+
PAJEK_LOSS.LABEL_AS_TEXT,
|
|
266
|
+
`node column "${name}" (label) is ${dtype}; labels are text and re-import as string`,
|
|
267
|
+
name,
|
|
268
|
+
rows(column)
|
|
269
|
+
)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
planned.push({ column, slot: "label" });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (role === "position") {
|
|
276
|
+
if (domain === "node" && (meta.components === 2 || meta.components === 3)) {
|
|
277
|
+
planned.push({ column, slot: "position" });
|
|
278
|
+
} else if (notes !== null && domain === "node") {
|
|
279
|
+
notes.push(
|
|
280
|
+
note(
|
|
281
|
+
PAJEK_LOSS.POSITION_STRIDE,
|
|
282
|
+
`node column "${name}" (position) has ${meta.components} components; Pajek coordinates are x y [z]`,
|
|
283
|
+
name,
|
|
284
|
+
rows(column)
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
} else if (notes !== null) {
|
|
288
|
+
notes.push(
|
|
289
|
+
note(LOSS.POSITIONS, `edge column "${name}" (position) cannot be written`, name, rows(column))
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (role === "spells") {
|
|
295
|
+
if (dtype === "list" && (meta.itemDtype === "f64" || meta.itemDtype === "i32" || meta.itemDtype === "f32")) {
|
|
296
|
+
planned.push({ column, slot: "spells" });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (notes !== null) {
|
|
300
|
+
notes.push(
|
|
301
|
+
note(
|
|
302
|
+
LOSS.SPELLS,
|
|
303
|
+
`${domain} column "${name}" (spells) is not a list of number pairs; it cannot be written`,
|
|
304
|
+
name,
|
|
305
|
+
rows(column)
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (role !== null && DROPPED_TEMPORAL_ROLES.has(role)) {
|
|
312
|
+
if (notes !== null) {
|
|
313
|
+
notes.push(
|
|
314
|
+
note(
|
|
315
|
+
PAJEK_LOSS.TEMPORAL_DROPPED,
|
|
316
|
+
`${domain} column "${name}" (${role}) cannot be written; Pajek intervals come from the spells role`,
|
|
317
|
+
name,
|
|
318
|
+
rows(column)
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (role !== null && (STRUCTURAL_ROLES.has(role) || CHECKED_ROLES.has(role))) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (domain === "edge" && role === "id") {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (dtype === "list" || dtype === "json") {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (domain === "node" && name === SHAPE_COLUMN && (dtype === "dict" || dtype === "string")) {
|
|
334
|
+
if (allShapeKeywords(column)) {
|
|
335
|
+
planned.push({ column, slot: "shape" });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (notes !== null && dtype === "dict") {
|
|
339
|
+
notes.push(
|
|
340
|
+
note(
|
|
341
|
+
PAJEK_LOSS.SHAPE_AS_PARAMETER,
|
|
342
|
+
`node column "${name}" holds values outside the Pajek shape keywords; it is written as a parameter and reads back as string`,
|
|
343
|
+
name,
|
|
344
|
+
rows(column)
|
|
345
|
+
)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
planned.push({ column, slot: "param" });
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (domain === "edge" && name === RELATION_COLUMN && (dtype === "dict" || dtype === "string")) {
|
|
352
|
+
planned.push({ column, slot: "relation" });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (!isParameterKey(name)) {
|
|
356
|
+
if (notes !== null) {
|
|
357
|
+
notes.push(
|
|
358
|
+
note(
|
|
359
|
+
PAJEK_LOSS.KEY_DROPPED,
|
|
360
|
+
`${domain} column "${name}" cannot be a Pajek parameter key; it is not written`,
|
|
361
|
+
name,
|
|
362
|
+
rows(column)
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
planned.push({ column, slot: "param" });
|
|
369
|
+
}
|
|
370
|
+
return planned;
|
|
371
|
+
}
|
|
372
|
+
function labelNotes(snapshot, nodePlan, notes) {
|
|
373
|
+
let labels = null;
|
|
374
|
+
let carried = 0;
|
|
375
|
+
for (const { column, slot } of nodePlan) {
|
|
376
|
+
if (slot === "label") {
|
|
377
|
+
labels = column;
|
|
378
|
+
} else if (slot !== "skip" && slot !== "relation") {
|
|
379
|
+
carried++;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const plain = [...snapshot.nodes].find((c) => c.meta.role === null && c.meta.name === LABEL_COLUMN);
|
|
383
|
+
if (plain !== void 0) {
|
|
384
|
+
notes.push(
|
|
385
|
+
note(
|
|
386
|
+
PAJEK_LOSS.ROLE_ASSUMED,
|
|
387
|
+
`node column "${LABEL_COLUMN}" has no role; written as a parameter, it reads back as the vertex label (role label)`,
|
|
388
|
+
LABEL_COLUMN,
|
|
389
|
+
plain.length - plain.nullCount
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (carried === 0) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
let gained = 0;
|
|
397
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
398
|
+
if (labels === null || !labels.isSet(i)) {
|
|
399
|
+
gained++;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (gained > 0) {
|
|
403
|
+
notes.push(
|
|
404
|
+
note(
|
|
405
|
+
PAJEK_LOSS.LABEL_GAINED,
|
|
406
|
+
`${gained} vertex line(s) carry coordinates, a shape or parameters and need a label; the id text is written there and reads back as a label`,
|
|
407
|
+
labels === null ? LABEL_COLUMN : labels.meta.name,
|
|
408
|
+
gained
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function allShapeKeywords(column) {
|
|
414
|
+
for (let r = 0; r < column.length; r++) {
|
|
415
|
+
if (column.isSet(r) && !SHAPES.has(column.value(r))) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
function note(code, message, column = null, count = null) {
|
|
422
|
+
return Object.freeze({ code, message, column, count });
|
|
423
|
+
}
|
|
424
|
+
function numberText(value, dtype) {
|
|
425
|
+
switch (dtype) {
|
|
426
|
+
case "i32":
|
|
427
|
+
case "u32":
|
|
428
|
+
case "u8":
|
|
429
|
+
return formatInteger(value);
|
|
430
|
+
case "f32":
|
|
431
|
+
return formatF32(value);
|
|
432
|
+
case "f64":
|
|
433
|
+
return formatDecimal(value, "f64");
|
|
434
|
+
default: {
|
|
435
|
+
const name = dtype;
|
|
436
|
+
throw new Error(`unknown numeric dtype ${name}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function cellText(column, row) {
|
|
441
|
+
switch (column.dtype) {
|
|
442
|
+
case "f32":
|
|
443
|
+
case "f64":
|
|
444
|
+
case "i32":
|
|
445
|
+
case "u32":
|
|
446
|
+
case "u8": {
|
|
447
|
+
const value = column.value(row);
|
|
448
|
+
if (typeof value === "number") {
|
|
449
|
+
return numberText(value, column.dtype);
|
|
450
|
+
}
|
|
451
|
+
const parts = [];
|
|
452
|
+
const vector = value;
|
|
453
|
+
for (let k = 0; k < vector.length; k++) {
|
|
454
|
+
parts.push(numberText(vector[k], column.dtype));
|
|
455
|
+
}
|
|
456
|
+
return parts.join(" ");
|
|
457
|
+
}
|
|
458
|
+
case "bool":
|
|
459
|
+
return column.value(row) === true ? "true" : "false";
|
|
460
|
+
case "dict":
|
|
461
|
+
case "string":
|
|
462
|
+
return column.value(row);
|
|
463
|
+
case "list":
|
|
464
|
+
case "json":
|
|
465
|
+
return JSON.stringify(column.value(row));
|
|
466
|
+
default: {
|
|
467
|
+
const name = column.dtype;
|
|
468
|
+
throw new Error(`unknown dtype ${name}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function checkCells(planned, domain, notes) {
|
|
473
|
+
for (const { column, slot } of planned) {
|
|
474
|
+
if (slot === "position" || slot === "spells") {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const { name } = column.meta;
|
|
478
|
+
let badText = 0;
|
|
479
|
+
let nonFinite = 0;
|
|
480
|
+
for (let r = 0; r < column.length; r++) {
|
|
481
|
+
if (!column.isSet(r)) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (column.dtype === "f64" || column.dtype === "f32") {
|
|
485
|
+
const value = column.value(r);
|
|
486
|
+
if (typeof value === "number") {
|
|
487
|
+
if (!Number.isFinite(value)) {
|
|
488
|
+
nonFinite++;
|
|
489
|
+
}
|
|
490
|
+
} else if (value !== void 0 && !Array.from(value).every((v) => Number.isFinite(v))) {
|
|
491
|
+
nonFinite++;
|
|
492
|
+
}
|
|
493
|
+
} else if (column.dtype === "string" || column.dtype === "dict") {
|
|
494
|
+
if (!isPajekLabel(column.value(r))) {
|
|
495
|
+
badText++;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (badText > 0) {
|
|
500
|
+
notes.push(
|
|
501
|
+
note(
|
|
502
|
+
PAJEK_LOSS.TEXT,
|
|
503
|
+
`${badText} value(s) of ${domain} column "${name}" hold a double quote or a line break; Pajek cannot write them`,
|
|
504
|
+
name,
|
|
505
|
+
badText
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
if (nonFinite > 0) {
|
|
510
|
+
notes.push(
|
|
511
|
+
note(
|
|
512
|
+
PAJEK_LOSS.NONFINITE_AS_TEXT,
|
|
513
|
+
`${nonFinite} non-finite value(s) of ${domain} column "${name}" are written as text`,
|
|
514
|
+
name,
|
|
515
|
+
nonFinite
|
|
516
|
+
)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function firstModeOf(snapshot) {
|
|
522
|
+
const { pajek } = snapshot.meta.extra;
|
|
523
|
+
if (typeof pajek !== "object" || pajek === null) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
const value = pajek.firstMode;
|
|
527
|
+
if (value === void 0) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= snapshot.nodeCount) {
|
|
531
|
+
return value;
|
|
532
|
+
}
|
|
533
|
+
return NaN;
|
|
534
|
+
}
|
|
535
|
+
function labelOf(labels, ids, i, needed) {
|
|
536
|
+
if (labels !== null && labels.isSet(i)) {
|
|
537
|
+
return cellText(labels, i);
|
|
538
|
+
}
|
|
539
|
+
if (needed || ids.isChanged(i)) {
|
|
540
|
+
return idText(ids.originalAt(i));
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
function idText(id) {
|
|
545
|
+
return typeof id === "number" ? formatF64(id) : id;
|
|
546
|
+
}
|
|
547
|
+
function spellsText(column, row) {
|
|
548
|
+
if (column.dtype !== "list" || !column.isSet(row)) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const items = column.sliceOf(row);
|
|
552
|
+
if (items.length === 0) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
const pairs = [];
|
|
556
|
+
for (const item of items) {
|
|
557
|
+
const pair = item;
|
|
558
|
+
pairs.push([pair[0], pair[1]]);
|
|
559
|
+
}
|
|
560
|
+
return formatIntervals(pairs);
|
|
561
|
+
}
|
|
562
|
+
function coordinatesText(column, i, writeZ) {
|
|
563
|
+
if (!column.isSet(i)) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const dtype = column.dtype;
|
|
567
|
+
const vector = column.value(i);
|
|
568
|
+
const x = numberText(vector[0], dtype);
|
|
569
|
+
const y = numberText(vector[1], dtype);
|
|
570
|
+
if (column.meta.components === 3 && writeZ) {
|
|
571
|
+
return `${x} ${y} ${numberText(vector[2], dtype)}`;
|
|
572
|
+
}
|
|
573
|
+
return `${x} ${y}`;
|
|
574
|
+
}
|
|
575
|
+
function needsZ(column) {
|
|
576
|
+
if (column.meta.components !== 3) {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
if (column.meta.extra.sourceDims !== 2) {
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
for (let i = 0; i < column.length; i++) {
|
|
583
|
+
if (column.isSet(i)) {
|
|
584
|
+
const vector = column.value(i);
|
|
585
|
+
if (vector[2] !== 0) {
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
function* writeParts(snapshot, options) {
|
|
593
|
+
const ids = sanitizeIds(snapshot, CAPABILITIES.idCharset, options.mangle ? "mangle" : "error");
|
|
594
|
+
const nodePlan = withoutReservedKey(plan(snapshot.nodes, "node", null), options.mangle);
|
|
595
|
+
const edgePlan = plan(snapshot.edges, "edge", null);
|
|
596
|
+
const preflight = [];
|
|
597
|
+
checkCells(nodePlan, "node", preflight);
|
|
598
|
+
checkCells(edgePlan, "edge", preflight);
|
|
599
|
+
checkIdTexts(snapshot.nodeCount, ids, options.mangle, preflight);
|
|
600
|
+
for (const n of preflight) {
|
|
601
|
+
if (n.code === PAJEK_LOSS.TEXT) {
|
|
602
|
+
throw new GraphFormatError("E_UNSUPPORTED", n.message, {
|
|
603
|
+
reason: "pajek label",
|
|
604
|
+
column: n.column,
|
|
605
|
+
count: n.count
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (options.networkHeader && snapshot.meta.name !== null) {
|
|
610
|
+
yield `*Network ${snapshot.meta.name}
|
|
611
|
+
`;
|
|
612
|
+
}
|
|
613
|
+
const firstMode = firstModeOf(snapshot);
|
|
614
|
+
yield firstMode !== null && !Number.isNaN(firstMode) ? `*Vertices ${snapshot.nodeCount} ${firstMode}
|
|
615
|
+
` : `*Vertices ${snapshot.nodeCount}
|
|
616
|
+
`;
|
|
617
|
+
yield* writeVertices(snapshot, ids, nodePlan, options.mangle);
|
|
618
|
+
yield* writeLines(snapshot, edgePlan);
|
|
619
|
+
}
|
|
620
|
+
function withoutReservedKey(nodePlan, mangle) {
|
|
621
|
+
if (!mangle) {
|
|
622
|
+
return nodePlan;
|
|
623
|
+
}
|
|
624
|
+
return nodePlan.filter((p) => !(p.slot === "param" && p.column.meta.name === ORIGINAL_ID_KEY));
|
|
625
|
+
}
|
|
626
|
+
function checkIdTexts(nodeCount, ids, mangle, notes) {
|
|
627
|
+
let badText = 0;
|
|
628
|
+
let typeChanged = 0;
|
|
629
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
630
|
+
if (!ids.isChanged(i)) {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const original = ids.originalAt(i);
|
|
634
|
+
const text = idText(original);
|
|
635
|
+
if (!isPajekLabel(text)) {
|
|
636
|
+
badText++;
|
|
637
|
+
} else if (mangle && typeof canonicalId(text) !== typeof original) {
|
|
638
|
+
typeChanged++;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (badText > 0) {
|
|
642
|
+
notes.push(
|
|
643
|
+
note(
|
|
644
|
+
PAJEK_LOSS.TEXT,
|
|
645
|
+
`${badText} node id(s) hold a double quote or a line break; Pajek cannot write them as labels or parameters`,
|
|
646
|
+
null,
|
|
647
|
+
badText
|
|
648
|
+
)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (typeChanged > 0) {
|
|
652
|
+
notes.push(
|
|
653
|
+
note(
|
|
654
|
+
PAJEK_LOSS.ID_TEXT_TYPE,
|
|
655
|
+
`${typeChanged} original id(s) read back from ${ORIGINAL_ID_KEY} as the other type under ids: "canonical" (a string "1" becomes 1, a number 1.5 becomes "1.5")`,
|
|
656
|
+
null,
|
|
657
|
+
typeChanged
|
|
658
|
+
)
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function* writeVertices(snapshot, ids, nodePlan, mangle) {
|
|
663
|
+
let labels = null;
|
|
664
|
+
let position = null;
|
|
665
|
+
let shape = null;
|
|
666
|
+
let nodeSpells = null;
|
|
667
|
+
const nodeParams = [];
|
|
668
|
+
for (const { column, slot } of nodePlan) {
|
|
669
|
+
switch (slot) {
|
|
670
|
+
case "label":
|
|
671
|
+
labels = column;
|
|
672
|
+
break;
|
|
673
|
+
case "position":
|
|
674
|
+
position = column;
|
|
675
|
+
break;
|
|
676
|
+
case "shape":
|
|
677
|
+
shape = column;
|
|
678
|
+
break;
|
|
679
|
+
case "spells":
|
|
680
|
+
nodeSpells = column;
|
|
681
|
+
break;
|
|
682
|
+
case "param":
|
|
683
|
+
nodeParams.push(column);
|
|
684
|
+
break;
|
|
685
|
+
case "relation":
|
|
686
|
+
case "skip":
|
|
687
|
+
break;
|
|
688
|
+
default: {
|
|
689
|
+
const name = slot;
|
|
690
|
+
throw new Error(`unknown slot ${name}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const writeZ = position !== null && needsZ(position);
|
|
695
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
696
|
+
const parts = [];
|
|
697
|
+
if (position !== null) {
|
|
698
|
+
const coordinates = coordinatesText(position, i, writeZ);
|
|
699
|
+
if (coordinates !== null) {
|
|
700
|
+
parts.push(coordinates);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (shape !== null && shape.isSet(i)) {
|
|
704
|
+
const keyword = cellText(shape, i);
|
|
705
|
+
if (SHAPES.has(keyword)) {
|
|
706
|
+
parts.push(keyword);
|
|
707
|
+
} else {
|
|
708
|
+
parts.push(`${SHAPE_COLUMN} ${quotePajekLabel(keyword)}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
for (const column of nodeParams) {
|
|
712
|
+
if (column.isSet(i)) {
|
|
713
|
+
parts.push(`${column.meta.name} ${quotePajekLabel(cellText(column, i))}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (mangle && ids.isChanged(i)) {
|
|
717
|
+
parts.push(`${ORIGINAL_ID_KEY} ${quotePajekLabel(idText(ids.originalAt(i)))}`);
|
|
718
|
+
}
|
|
719
|
+
if (nodeSpells !== null) {
|
|
720
|
+
const spells = spellsText(nodeSpells, i);
|
|
721
|
+
if (spells !== null) {
|
|
722
|
+
parts.push(spells);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const label = labelOf(labels, ids, i, parts.length > 0);
|
|
726
|
+
let line = String(i + 1);
|
|
727
|
+
if (label !== null) {
|
|
728
|
+
line += ` ${quotePajekLabel(label)}`;
|
|
729
|
+
}
|
|
730
|
+
if (parts.length > 0) {
|
|
731
|
+
line += ` ${parts.join(" ")}`;
|
|
732
|
+
}
|
|
733
|
+
yield `${line}
|
|
734
|
+
`;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function* writeLines(snapshot, edgePlan) {
|
|
738
|
+
const folding = pairFolding(snapshot, { foldMutual: true });
|
|
739
|
+
const weights = explicitWeights(snapshot);
|
|
740
|
+
const { src, dst } = snapshot.edgeList();
|
|
741
|
+
let relation = null;
|
|
742
|
+
let edgeSpells = null;
|
|
743
|
+
const edgeParams = [];
|
|
744
|
+
for (const { column, slot } of edgePlan) {
|
|
745
|
+
switch (slot) {
|
|
746
|
+
case "relation":
|
|
747
|
+
relation = column;
|
|
748
|
+
break;
|
|
749
|
+
case "spells":
|
|
750
|
+
edgeSpells = column;
|
|
751
|
+
break;
|
|
752
|
+
case "param":
|
|
753
|
+
edgeParams.push(column);
|
|
754
|
+
break;
|
|
755
|
+
case "label":
|
|
756
|
+
case "position":
|
|
757
|
+
case "shape":
|
|
758
|
+
case "skip":
|
|
759
|
+
break;
|
|
760
|
+
default: {
|
|
761
|
+
const name = slot;
|
|
762
|
+
throw new Error(`unknown slot ${name}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const relationNumbers = /* @__PURE__ */ new Map();
|
|
767
|
+
let currentKind = null;
|
|
768
|
+
let currentRelation = null;
|
|
769
|
+
let sectionOpen = false;
|
|
770
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
771
|
+
if (folding.folded(e)) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
const directed = snapshot.directed && folding.sourceDirected(e) && !folding.isMutual(e);
|
|
775
|
+
const kind = directed ? "arcs" : "edges";
|
|
776
|
+
const relationName = relation !== null && relation.isSet(e) ? cellText(relation, e) : null;
|
|
777
|
+
if (!sectionOpen || kind !== currentKind || relationName !== currentRelation) {
|
|
778
|
+
currentKind = kind;
|
|
779
|
+
currentRelation = relationName;
|
|
780
|
+
sectionOpen = true;
|
|
781
|
+
const keyword = kind === "arcs" ? "*Arcs" : "*Edges";
|
|
782
|
+
if (relationName === null) {
|
|
783
|
+
yield `${keyword}
|
|
784
|
+
`;
|
|
785
|
+
} else {
|
|
786
|
+
let number = relationNumbers.get(relationName);
|
|
787
|
+
if (number === void 0) {
|
|
788
|
+
number = relationNumbers.size + 1;
|
|
789
|
+
relationNumbers.set(relationName, number);
|
|
790
|
+
}
|
|
791
|
+
yield `${keyword} :${number} ${quotePajekLabel(relationName)}
|
|
792
|
+
`;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
let line = `${src[e] + 1} ${dst[e] + 1}`;
|
|
796
|
+
const weight = weights.text(e);
|
|
797
|
+
if (weight !== null) {
|
|
798
|
+
line += ` ${weight}`;
|
|
799
|
+
}
|
|
800
|
+
for (const column of edgeParams) {
|
|
801
|
+
if (column.isSet(e)) {
|
|
802
|
+
line += ` ${column.meta.name} ${quotePajekLabel(cellText(column, e))}`;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (edgeSpells !== null) {
|
|
806
|
+
const spells = spellsText(edgeSpells, e);
|
|
807
|
+
if (spells !== null) {
|
|
808
|
+
line += ` ${spells}`;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
yield `${line}
|
|
812
|
+
`;
|
|
813
|
+
}
|
|
814
|
+
if (!sectionOpen) {
|
|
815
|
+
yield snapshot.directed ? "*Arcs\n" : "*Edges\n";
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function resolvePajekOptions(options) {
|
|
819
|
+
const common = resolveExportOptions(options);
|
|
820
|
+
const value = options?.networkHeader;
|
|
821
|
+
if (value !== void 0 && typeof value !== "boolean") {
|
|
822
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option networkHeader: not a boolean", {
|
|
823
|
+
option: "networkHeader",
|
|
824
|
+
found: typeof value
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
return { networkHeader: value ?? false, mangle: common.sanitizeIds === "mangle" };
|
|
828
|
+
}
|
|
829
|
+
const pajekExporter = Object.freeze({
|
|
830
|
+
format: "pajek",
|
|
831
|
+
capabilities: CAPABILITIES,
|
|
832
|
+
/**
|
|
833
|
+
* Pre-flight: the generic capability gaps plus the Pajek-specific ones.
|
|
834
|
+
* @param snapshot - the snapshot
|
|
835
|
+
* @param options - export options
|
|
836
|
+
* @returns the loss notes, empty when the export is exact
|
|
837
|
+
*/
|
|
838
|
+
check(snapshot, options) {
|
|
839
|
+
const resolved = resolveExportOptions(options);
|
|
840
|
+
const { mangle } = resolvePajekOptions(options);
|
|
841
|
+
const notes = [];
|
|
842
|
+
const planned = plan(snapshot.nodes, "node", notes);
|
|
843
|
+
const nodePlan = withoutReservedKey(planned, mangle);
|
|
844
|
+
const edgePlan = plan(snapshot.edges, "edge", notes);
|
|
845
|
+
const ids = sanitizeIds(snapshot, CAPABILITIES.idCharset, mangle ? "mangle" : "error");
|
|
846
|
+
checkIdTexts(snapshot.nodeCount, ids, mangle, notes);
|
|
847
|
+
if (nodePlan.length !== planned.length) {
|
|
848
|
+
const reserved = planned.find((p) => p.slot === "param" && p.column.meta.name === ORIGINAL_ID_KEY);
|
|
849
|
+
if (reserved !== void 0) {
|
|
850
|
+
notes.push(
|
|
851
|
+
note(
|
|
852
|
+
PAJEK_LOSS.KEY_DROPPED,
|
|
853
|
+
`node column "${ORIGINAL_ID_KEY}" is the parameter the exporter writes the original ids under (sanitizeIds "mangle"); it is not written`,
|
|
854
|
+
ORIGINAL_ID_KEY,
|
|
855
|
+
reserved.column.length - reserved.column.nullCount
|
|
856
|
+
)
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const owned = /* @__PURE__ */ new Set();
|
|
861
|
+
for (const { column, slot } of [...nodePlan, ...edgePlan]) {
|
|
862
|
+
if (slot === "shape" || slot === "relation") {
|
|
863
|
+
owned.add(`${column.meta.domain}:${column.meta.name}`);
|
|
864
|
+
} else if (slot === "param" && column.meta.domain === "node" && column.meta.name === SHAPE_COLUMN) {
|
|
865
|
+
owned.add(`${column.meta.domain}:${column.meta.name}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
for (const table of [snapshot.nodes, snapshot.edges]) {
|
|
869
|
+
for (const column of table) {
|
|
870
|
+
if (column.meta.role === "spells") {
|
|
871
|
+
owned.add(`${column.meta.domain}:${column.meta.name}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
for (const n of checkCapabilities(snapshot, CAPABILITIES, resolved, {
|
|
876
|
+
roles: SLOT_ROLES,
|
|
877
|
+
roleNames: ROLE_NAMES
|
|
878
|
+
})) {
|
|
879
|
+
if (n.column !== null && (n.code === LOSS.LIST || n.code === LOSS.DTYPE)) {
|
|
880
|
+
const domain = n.message.startsWith("node column") ? "node" : "edge";
|
|
881
|
+
if (owned.has(`${domain}:${n.column}`)) {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (n.code === LOSS.COLUMN_NAME_CHANGED && n.message.startsWith("edge column")) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
notes.push(n);
|
|
889
|
+
}
|
|
890
|
+
labelNotes(snapshot, nodePlan, notes);
|
|
891
|
+
for (const column of snapshot.edges) {
|
|
892
|
+
if (column.meta.role === "label") {
|
|
893
|
+
notes.push(
|
|
894
|
+
note(
|
|
895
|
+
LOSS.ROLE,
|
|
896
|
+
`edge column "${column.meta.name}" (label) is written as a plain parameter; Pajek labels vertices only and the role is lost`,
|
|
897
|
+
column.meta.name,
|
|
898
|
+
column.length - column.nullCount
|
|
899
|
+
)
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
checkCells(nodePlan, "node", notes);
|
|
904
|
+
checkCells(edgePlan, "edge", notes);
|
|
905
|
+
const folding = pairFolding(snapshot, { foldMutual: true });
|
|
906
|
+
if (folding.mutualCount > 0) {
|
|
907
|
+
notes.push(
|
|
908
|
+
note(
|
|
909
|
+
PAJEK_LOSS.MUTUAL_AS_UNDIRECTED,
|
|
910
|
+
`${folding.mutualCount} mutual pair(s) are written as undirected edges; the mutual mark is lost`,
|
|
911
|
+
snapshot.edges.byRole("mutual")?.meta.name ?? null,
|
|
912
|
+
folding.mutualCount
|
|
913
|
+
)
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
if (Number.isNaN(firstModeOf(snapshot))) {
|
|
917
|
+
notes.push(
|
|
918
|
+
note(
|
|
919
|
+
PAJEK_LOSS.FIRST_MODE_DROPPED,
|
|
920
|
+
"meta.extra.pajek.firstMode is not a count within 0..nodeCount; the two-mode header is not written"
|
|
921
|
+
)
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
return notes;
|
|
925
|
+
},
|
|
926
|
+
/**
|
|
927
|
+
* Write the snapshot as UTF-8 chunks.
|
|
928
|
+
* @param snapshot - the snapshot
|
|
929
|
+
* @param options - export options
|
|
930
|
+
* @returns the chunks
|
|
931
|
+
*/
|
|
932
|
+
export(snapshot, options) {
|
|
933
|
+
const resolved = resolvePajekOptions(options);
|
|
934
|
+
return encodeChunks(writeParts(snapshot, resolved));
|
|
935
|
+
},
|
|
936
|
+
/**
|
|
937
|
+
* Write the snapshot as one string.
|
|
938
|
+
* @param snapshot - the snapshot
|
|
939
|
+
* @param options - export options
|
|
940
|
+
* @returns the document
|
|
941
|
+
*/
|
|
942
|
+
exportToString(snapshot, options) {
|
|
943
|
+
const resolved = resolvePajekOptions(options);
|
|
944
|
+
return joinText(writeParts(snapshot, resolved));
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
const PAJEK_ISSUE = Object.freeze({
|
|
948
|
+
/** The input holds invalid UTF-8 (fatal). */
|
|
949
|
+
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
950
|
+
/** Fatal: no `*Vertices` section (an empty file, or not a Pajek network). */
|
|
951
|
+
NO_VERTICES: "E_PAJEK_NO_VERTICES",
|
|
952
|
+
/** Fatal: `*Vertices` without a vertex count, or one the sink cannot hold. */
|
|
953
|
+
VERTICES_COUNT: "E_PAJEK_VERTICES_COUNT",
|
|
954
|
+
/** Aborts: a second `*Vertices` or `*Network` section; only one network per file is read. */
|
|
955
|
+
MULTIPLE_NETWORKS: "E_PAJEK_MULTIPLE_NETWORKS",
|
|
956
|
+
/** A data line before the first section header. */
|
|
957
|
+
OUTSIDE_SECTION: "E_PAJEK_OUTSIDE_SECTION",
|
|
958
|
+
/** A section header the importer cannot parse. */
|
|
959
|
+
SYNTAX: SYNTAX_CODE,
|
|
960
|
+
/** A double quote not closed before the end of the line. */
|
|
961
|
+
UNTERMINATED_QUOTE: "E_PAJEK_UNTERMINATED_QUOTE",
|
|
962
|
+
/** A vertex line the grammar does not accept. */
|
|
963
|
+
VERTEX_LINE: "E_PAJEK_VERTEX_LINE",
|
|
964
|
+
/** A vertex number outside the declared range. */
|
|
965
|
+
VERTEX_RANGE: "E_PAJEK_VERTEX_RANGE",
|
|
966
|
+
/** Fewer vertex lines than `*Vertices` declares (vertices without a line have no label). */
|
|
967
|
+
VERTEX_COUNT: "E_PAJEK_VERTEX_COUNT",
|
|
968
|
+
/** A second line for the same vertex; the later values overwrite. */
|
|
969
|
+
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
970
|
+
/** A line (arc, edge, list or matrix row) the grammar does not accept. */
|
|
971
|
+
LINE: "E_PAJEK_LINE",
|
|
972
|
+
/** A line endpoint outside the declared vertex range (the core's code, forwarded). */
|
|
973
|
+
UNKNOWN_NODE: "E_UNKNOWN_NODE",
|
|
974
|
+
/** A malformed time interval token. */
|
|
975
|
+
INTERVAL: "E_PAJEK_INTERVAL",
|
|
976
|
+
/** A `*Matrix` section with the wrong number of rows. */
|
|
977
|
+
MATRIX_ROWS: "E_PAJEK_MATRIX_ROWS",
|
|
978
|
+
/** A project-file section (`*Partition`, `*Vector`, ...) the importer does not read. */
|
|
979
|
+
UNSUPPORTED_SECTION: "E_PAJEK_UNSUPPORTED_SECTION",
|
|
980
|
+
/** Tokens after a section header the grammar does not account for. */
|
|
981
|
+
HEADER_EXTRA: "W_PAJEK_HEADER_EXTRA",
|
|
982
|
+
/** The file declares vertices but no line section. */
|
|
983
|
+
NO_LINES: "W_PAJEK_NO_LINES",
|
|
984
|
+
/** Vertex numbering starts at 0 rather than 1. */
|
|
985
|
+
ZERO_BASED: "W_PAJEK_ZERO_BASED",
|
|
986
|
+
/** Vertex lines mix two and three coordinates. */
|
|
987
|
+
COORD_DIMS: "W_PAJEK_COORD_DIMS",
|
|
988
|
+
/** Two vertices share a label under nodeIdFrom "label" and became one node. */
|
|
989
|
+
LABEL_MERGED: "W_PAJEK_LABEL_MERGED",
|
|
990
|
+
/** Two distinct label texts became one numeric id under ids "number". */
|
|
991
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
992
|
+
/** A parameter column of `2.0`-style text kept at the value-inferred dtype (the sink cannot widen). */
|
|
993
|
+
WIDENING_UNSUPPORTED: WIDENING_UNSUPPORTED_CODE,
|
|
994
|
+
/** A structural column (label, position, shape, spells, relation) renamed `<name>#<id>` because the name was taken (design section 5.6). */
|
|
995
|
+
COLUMN_RENAMED: RENAMED_CODE,
|
|
996
|
+
/** A structural column declared without its role because the sink already holds it. */
|
|
997
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
998
|
+
/** Two vertices carry the same `graphty_originalId` under restoreMangledIds and became one node. */
|
|
999
|
+
ORIGINAL_ID_MERGED: "W_PAJEK_ORIGINAL_ID_MERGED",
|
|
1000
|
+
/** A vertex line's `graphty_originalId` came after a later vertex's line had created it under its number. */
|
|
1001
|
+
ORIGINAL_ID_UNRESTORED: "W_PAJEK_ORIGINAL_ID_UNRESTORED",
|
|
1002
|
+
/** A common option the importer has no use for (weightFrom naming a parameter and restoreMangledIds are honoured; long, hyperedges are not). */
|
|
1003
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
1004
|
+
/** A builder-policy option the caller passed that the caller's sink does not use (the shared W_SINK_OPTION). */
|
|
1005
|
+
SINK_OPTION: SINK_OPTION_CODE
|
|
1006
|
+
});
|
|
1007
|
+
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
1008
|
+
"ids",
|
|
1009
|
+
"nodeIdFrom",
|
|
1010
|
+
"restoreMangledIds",
|
|
1011
|
+
"addMissingNodes",
|
|
1012
|
+
"duplicateEdges",
|
|
1013
|
+
"selfLoops",
|
|
1014
|
+
"onMixedDirection",
|
|
1015
|
+
"defaultDirected",
|
|
1016
|
+
"weightFrom",
|
|
1017
|
+
"weightDtype",
|
|
1018
|
+
"errorLimit",
|
|
1019
|
+
"signal",
|
|
1020
|
+
"onProgress"
|
|
1021
|
+
]);
|
|
1022
|
+
const DEFAULTS = { ids: "canonical", defaultDirected: true, weightFrom: VALUE_FIELD };
|
|
1023
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
1024
|
+
const FIRST_VERTEX_VALUES = /* @__PURE__ */ new Set([0, 1, "auto"]);
|
|
1025
|
+
const LABEL_DECL = {
|
|
1026
|
+
name: LABEL_COLUMN,
|
|
1027
|
+
dtype: "string",
|
|
1028
|
+
nullable: true,
|
|
1029
|
+
role: "label",
|
|
1030
|
+
origin: { format: "pajek", id: "label" }
|
|
1031
|
+
};
|
|
1032
|
+
const SHAPE_DECL = {
|
|
1033
|
+
name: SHAPE_COLUMN,
|
|
1034
|
+
dtype: "dict",
|
|
1035
|
+
nullable: true,
|
|
1036
|
+
origin: { format: "pajek", id: "shape" }
|
|
1037
|
+
};
|
|
1038
|
+
const RELATION_DECL = {
|
|
1039
|
+
name: RELATION_COLUMN,
|
|
1040
|
+
dtype: "dict",
|
|
1041
|
+
nullable: true,
|
|
1042
|
+
origin: { format: "pajek", id: "relation" }
|
|
1043
|
+
};
|
|
1044
|
+
function spellsDecl() {
|
|
1045
|
+
return {
|
|
1046
|
+
name: SPELLS_COLUMN,
|
|
1047
|
+
dtype: "list",
|
|
1048
|
+
itemDtype: "f64",
|
|
1049
|
+
itemComponents: 2,
|
|
1050
|
+
nullable: true,
|
|
1051
|
+
role: "spells",
|
|
1052
|
+
origin: { format: "pajek", id: "interval" }
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function positionDecl(sourceDims) {
|
|
1056
|
+
return {
|
|
1057
|
+
name: POSITION_COLUMN,
|
|
1058
|
+
dtype: "f32",
|
|
1059
|
+
components: 3,
|
|
1060
|
+
nullable: true,
|
|
1061
|
+
role: "position",
|
|
1062
|
+
origin: { format: "pajek", id: "coordinates" },
|
|
1063
|
+
extra: { sourceDims, units: "file" }
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
class LineError extends Error {
|
|
1067
|
+
/**
|
|
1068
|
+
* Create a line error.
|
|
1069
|
+
* @param category - the issue category
|
|
1070
|
+
* @param code - the issue code
|
|
1071
|
+
* @param message - a plain-ASCII message
|
|
1072
|
+
*/
|
|
1073
|
+
constructor(category, code, message) {
|
|
1074
|
+
super(message);
|
|
1075
|
+
this.name = "PajekLineError";
|
|
1076
|
+
this.category = category;
|
|
1077
|
+
this.code = code;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
class PajekParser {
|
|
1081
|
+
/**
|
|
1082
|
+
* Create the parser for one import call.
|
|
1083
|
+
* @param sink - the sink
|
|
1084
|
+
* @param report - the report
|
|
1085
|
+
* @param options - the resolved common options
|
|
1086
|
+
* @param firstVertex - the firstVertex format option
|
|
1087
|
+
*/
|
|
1088
|
+
constructor(sink, report, options, firstVertex) {
|
|
1089
|
+
this.section = "none";
|
|
1090
|
+
this.kind = "directed";
|
|
1091
|
+
this.relation = null;
|
|
1092
|
+
this.vertexCount = -1;
|
|
1093
|
+
this.firstMode = null;
|
|
1094
|
+
this.networkName = null;
|
|
1095
|
+
this.base = 1;
|
|
1096
|
+
this.baseKnown = false;
|
|
1097
|
+
this.nodesCreated = false;
|
|
1098
|
+
this.verticesDone = false;
|
|
1099
|
+
this.sawLineSection = false;
|
|
1100
|
+
this.indexOfPos = new Uint32Array(0);
|
|
1101
|
+
this.idOfPos = null;
|
|
1102
|
+
this.seen = new Uint8Array(0);
|
|
1103
|
+
this.seenCount = 0;
|
|
1104
|
+
this.filledBelow = 0;
|
|
1105
|
+
this.matrixRows = 0;
|
|
1106
|
+
this.coordDims = 0;
|
|
1107
|
+
this.labelHandle = INVALID_INDEX;
|
|
1108
|
+
this.positionHandle = INVALID_INDEX;
|
|
1109
|
+
this.shapeHandle = INVALID_INDEX;
|
|
1110
|
+
this.nodeSpellsHandle = INVALID_INDEX;
|
|
1111
|
+
this.relationHandle = INVALID_INDEX;
|
|
1112
|
+
this.edgeSpellsHandle = INVALID_INDEX;
|
|
1113
|
+
this.nodeParams = /* @__PURE__ */ new Map();
|
|
1114
|
+
this.edgeParams = /* @__PURE__ */ new Map();
|
|
1115
|
+
this.sink = sink;
|
|
1116
|
+
this.report = report;
|
|
1117
|
+
this.options = options;
|
|
1118
|
+
this.firstVertex = firstVertex;
|
|
1119
|
+
this.resolver = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
1120
|
+
this.coercer = new IdCoercer(options.ids);
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Handle one line; every problem of the line is recorded as an issue and the line skipped.
|
|
1124
|
+
* @param text - the line without its terminator
|
|
1125
|
+
* @param line - the 1-based line number
|
|
1126
|
+
*/
|
|
1127
|
+
line(text, line) {
|
|
1128
|
+
if (isCommentOrBlank(text)) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
try {
|
|
1132
|
+
if (isSectionLine(text)) {
|
|
1133
|
+
this.header(text, line);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
switch (this.section) {
|
|
1137
|
+
case "none":
|
|
1138
|
+
throw new LineError(
|
|
1139
|
+
"parse-error",
|
|
1140
|
+
PAJEK_ISSUE.OUTSIDE_SECTION,
|
|
1141
|
+
"data before the first section header; a Pajek network starts with *Vertices"
|
|
1142
|
+
);
|
|
1143
|
+
case "vertices":
|
|
1144
|
+
this.vertexLine(text, line);
|
|
1145
|
+
return;
|
|
1146
|
+
case "lines":
|
|
1147
|
+
this.edgeLine(text, line);
|
|
1148
|
+
return;
|
|
1149
|
+
case "list":
|
|
1150
|
+
this.listLine(text, line);
|
|
1151
|
+
return;
|
|
1152
|
+
case "matrix":
|
|
1153
|
+
this.matrixLine(text, line);
|
|
1154
|
+
return;
|
|
1155
|
+
case "skip":
|
|
1156
|
+
return;
|
|
1157
|
+
default: {
|
|
1158
|
+
const name = this.section;
|
|
1159
|
+
throw new Error(`unknown section state ${name}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
if (err instanceof LineError) {
|
|
1164
|
+
this.report.error(err.category, err.code, err.message, { line });
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
this.report.recordError(err, { line });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* End of input: the vertex set is completed, a network without line sections gets the default
|
|
1172
|
+
* direction, and the metadata is written.
|
|
1173
|
+
*/
|
|
1174
|
+
finish() {
|
|
1175
|
+
if (this.vertexCount < 0) {
|
|
1176
|
+
this.report.fail(PAJEK_ISSUE.NO_VERTICES, "not a Pajek network: no *Vertices section found");
|
|
1177
|
+
}
|
|
1178
|
+
this.endSection();
|
|
1179
|
+
this.finishVertices();
|
|
1180
|
+
if (!this.sawLineSection) {
|
|
1181
|
+
this.report.warning(
|
|
1182
|
+
"validation-error",
|
|
1183
|
+
PAJEK_ISSUE.NO_LINES,
|
|
1184
|
+
"the file declares vertices but no *Arcs, *Edges, *Arcslist, *Edgeslist or *Matrix section"
|
|
1185
|
+
);
|
|
1186
|
+
this.resolver.setHeader(this.options.defaultDirected);
|
|
1187
|
+
}
|
|
1188
|
+
if (this.coercer.mergeCount > 0) {
|
|
1189
|
+
this.report.warning(
|
|
1190
|
+
"coercion",
|
|
1191
|
+
PAJEK_ISSUE.ID_MERGED,
|
|
1192
|
+
`${this.coercer.mergeCount} label(s) merged into an id another label already produced under ids "number"`
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
const meta = {
|
|
1196
|
+
sourceFormat: "pajek",
|
|
1197
|
+
...this.networkName === null ? {} : { name: this.networkName },
|
|
1198
|
+
...this.firstMode === null ? {} : { extra: { pajek: { firstMode: this.firstMode } } }
|
|
1199
|
+
};
|
|
1200
|
+
this.sink.setMeta(meta);
|
|
1201
|
+
}
|
|
1202
|
+
// ============================================================ headers
|
|
1203
|
+
/**
|
|
1204
|
+
* A section header line.
|
|
1205
|
+
* @param text - the line
|
|
1206
|
+
* @param line - the line number
|
|
1207
|
+
*/
|
|
1208
|
+
header(text, line) {
|
|
1209
|
+
const skipping = this.section === "skip";
|
|
1210
|
+
this.endSection();
|
|
1211
|
+
const h = parseSectionHeader(text);
|
|
1212
|
+
if (h === null) {
|
|
1213
|
+
this.section = "skip";
|
|
1214
|
+
throw new LineError("parse-error", PAJEK_ISSUE.SYNTAX, `cannot read the section header "${text}"`);
|
|
1215
|
+
}
|
|
1216
|
+
if (h.extra.length > 0) {
|
|
1217
|
+
this.report.warning(
|
|
1218
|
+
"validation-error",
|
|
1219
|
+
PAJEK_ISSUE.HEADER_EXTRA,
|
|
1220
|
+
`ignored ${h.extra.length} unexpected token(s) after *${h.keyword}: ${h.extra.join(" ")}`,
|
|
1221
|
+
{ line }
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
switch (h.kind) {
|
|
1225
|
+
case "network":
|
|
1226
|
+
if (this.networkName !== null || this.vertexCount >= 0) {
|
|
1227
|
+
this.refuseSecondNetwork(h.keyword, line);
|
|
1228
|
+
}
|
|
1229
|
+
this.networkName = h.name ?? "";
|
|
1230
|
+
this.section = "none";
|
|
1231
|
+
return;
|
|
1232
|
+
case "vertices":
|
|
1233
|
+
if (skipping && this.vertexCount >= 0) {
|
|
1234
|
+
this.section = "skip";
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
this.verticesHeader(h, line);
|
|
1238
|
+
return;
|
|
1239
|
+
case "arcs":
|
|
1240
|
+
case "edges":
|
|
1241
|
+
this.lineSection(h, line, "lines", h.kind === "arcs" ? "directed" : "undirected");
|
|
1242
|
+
return;
|
|
1243
|
+
case "arcslist":
|
|
1244
|
+
case "edgeslist":
|
|
1245
|
+
this.lineSection(h, line, "list", h.kind === "arcslist" ? "directed" : "undirected");
|
|
1246
|
+
return;
|
|
1247
|
+
case "matrix":
|
|
1248
|
+
this.lineSection(h, line, "matrix", "directed");
|
|
1249
|
+
this.matrixRows = 0;
|
|
1250
|
+
return;
|
|
1251
|
+
case "unsupported":
|
|
1252
|
+
this.section = "skip";
|
|
1253
|
+
this.report.error(
|
|
1254
|
+
"unsupported",
|
|
1255
|
+
PAJEK_ISSUE.UNSUPPORTED_SECTION,
|
|
1256
|
+
`the *${h.keyword} section is not supported; its lines are skipped`,
|
|
1257
|
+
{ line }
|
|
1258
|
+
);
|
|
1259
|
+
return;
|
|
1260
|
+
default: {
|
|
1261
|
+
const name = h.kind;
|
|
1262
|
+
throw new Error(`unknown section kind ${name}`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* `*Vertices N [N1]`.
|
|
1268
|
+
* @param h - the header
|
|
1269
|
+
* @param line - the line number
|
|
1270
|
+
*/
|
|
1271
|
+
verticesHeader(h, line) {
|
|
1272
|
+
if (this.vertexCount >= 0) {
|
|
1273
|
+
this.refuseSecondNetwork(h.keyword, line);
|
|
1274
|
+
}
|
|
1275
|
+
if (h.count === null) {
|
|
1276
|
+
this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, "*Vertices needs a vertex count", { line });
|
|
1277
|
+
}
|
|
1278
|
+
if (!Number.isSafeInteger(h.count)) {
|
|
1279
|
+
this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count}: not a count`, { line });
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
this.sink.reserve(h.count);
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1285
|
+
this.report.fail(
|
|
1286
|
+
PAJEK_ISSUE.VERTICES_COUNT,
|
|
1287
|
+
`*Vertices ${h.count}: the sink cannot hold that many (${reason})`,
|
|
1288
|
+
{
|
|
1289
|
+
line
|
|
1290
|
+
}
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
this.vertexCount = h.count;
|
|
1294
|
+
if (h.secondCount !== null) {
|
|
1295
|
+
this.firstMode = h.secondCount;
|
|
1296
|
+
}
|
|
1297
|
+
this.section = "vertices";
|
|
1298
|
+
this.indexOfPos = new Uint32Array(h.count);
|
|
1299
|
+
this.seen = new Uint8Array(h.count);
|
|
1300
|
+
this.filledBelow = 0;
|
|
1301
|
+
if (this.options.nodeIdFrom === "label" || this.restoresIds()) {
|
|
1302
|
+
this.idOfPos = new Array(h.count);
|
|
1303
|
+
}
|
|
1304
|
+
if (this.firstVertex !== "auto") {
|
|
1305
|
+
this.setBase(this.firstVertex, line);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* A line section header: the vertex set is completed, the first one sets the sink's direction
|
|
1310
|
+
* (rule 1 of design section 8.4), later ones only switch the kind.
|
|
1311
|
+
* @param h - the header
|
|
1312
|
+
* @param line - the line number
|
|
1313
|
+
* @param section - the parser state for the section
|
|
1314
|
+
* @param kind - the direction of its lines
|
|
1315
|
+
*/
|
|
1316
|
+
lineSection(h, line, section, kind) {
|
|
1317
|
+
if (this.vertexCount < 0) {
|
|
1318
|
+
this.report.fail(
|
|
1319
|
+
PAJEK_ISSUE.NO_VERTICES,
|
|
1320
|
+
`*${h.keyword} before *Vertices; a Pajek network starts with *Vertices`,
|
|
1321
|
+
{ line }
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
this.finishVertices();
|
|
1325
|
+
if (!this.sawLineSection) {
|
|
1326
|
+
this.sawLineSection = true;
|
|
1327
|
+
this.resolver.setHeader(kind === "directed", { line });
|
|
1328
|
+
}
|
|
1329
|
+
this.section = section;
|
|
1330
|
+
this.kind = kind;
|
|
1331
|
+
this.relation = h.relation === null ? null : h.name ?? String(h.relation);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Refuse a second network: an issue, then ImportError with the report so far.
|
|
1335
|
+
* @param keyword - the header keyword
|
|
1336
|
+
* @param line - the line number
|
|
1337
|
+
*/
|
|
1338
|
+
refuseSecondNetwork(keyword, line) {
|
|
1339
|
+
const message = `*${keyword} starts a second network; the importer reads one network per file`;
|
|
1340
|
+
this.report.error("unsupported", PAJEK_ISSUE.MULTIPLE_NETWORKS, message, { line });
|
|
1341
|
+
throw this.report.abort(message, { code: PAJEK_ISSUE.MULTIPLE_NETWORKS, line });
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Checks that run when a section ends.
|
|
1345
|
+
*/
|
|
1346
|
+
endSection() {
|
|
1347
|
+
if (this.section === "matrix" && this.matrixRows !== this.vertexCount) {
|
|
1348
|
+
this.report.error(
|
|
1349
|
+
"validation-error",
|
|
1350
|
+
PAJEK_ISSUE.MATRIX_ROWS,
|
|
1351
|
+
`*Matrix has ${this.matrixRows} row(s); *Vertices declares ${this.vertexCount}`
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
this.section = "none";
|
|
1355
|
+
}
|
|
1356
|
+
// ============================================================ vertices
|
|
1357
|
+
/**
|
|
1358
|
+
* Fix the vertex numbering base and, under nodeIdFrom "id" / "index", create the declared
|
|
1359
|
+
* vertices in number order so the id map is identity.
|
|
1360
|
+
* @param base - 0 or 1
|
|
1361
|
+
* @param line - the line number
|
|
1362
|
+
*/
|
|
1363
|
+
setBase(base, line) {
|
|
1364
|
+
this.base = base;
|
|
1365
|
+
this.baseKnown = true;
|
|
1366
|
+
if (base === 0) {
|
|
1367
|
+
this.report.warning(
|
|
1368
|
+
"coercion",
|
|
1369
|
+
PAJEK_ISSUE.ZERO_BASED,
|
|
1370
|
+
"vertex numbering starts at 0; Pajek numbers vertices from 1",
|
|
1371
|
+
{ line }
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Push vertices base..base+N-1 in order (nodeIdFrom "id" / "index"), so the id map is identity;
|
|
1377
|
+
* called on the first vertex line (once it is known that the lines carry no `graphty_originalId`
|
|
1378
|
+
* parameter to restore) or when the vertex section ends without one.
|
|
1379
|
+
*/
|
|
1380
|
+
createNodes() {
|
|
1381
|
+
if (this.nodesCreated || this.idOfPos !== null) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
this.nodesCreated = true;
|
|
1385
|
+
const n = this.vertexCount;
|
|
1386
|
+
const { sink } = this;
|
|
1387
|
+
for (let pos = 0; pos < n; pos++) {
|
|
1388
|
+
this.indexOfPos[pos] = sink.addNode(this.idOfNumber(pos + this.base));
|
|
1389
|
+
}
|
|
1390
|
+
this.report.counts.nodes += n;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* The id of a vertex number under nodeIdFrom "id" (the number coerced by the ids rule) or
|
|
1394
|
+
* "index" (the position).
|
|
1395
|
+
* @param k - the vertex number
|
|
1396
|
+
* @returns the id
|
|
1397
|
+
*/
|
|
1398
|
+
idOfNumber(k) {
|
|
1399
|
+
if (this.options.nodeIdFrom === "index") {
|
|
1400
|
+
return k - this.base;
|
|
1401
|
+
}
|
|
1402
|
+
switch (this.options.ids) {
|
|
1403
|
+
case "canonical":
|
|
1404
|
+
case "number":
|
|
1405
|
+
return k;
|
|
1406
|
+
case "keep":
|
|
1407
|
+
case "string":
|
|
1408
|
+
return String(k);
|
|
1409
|
+
default:
|
|
1410
|
+
return coerceIdText(String(k), this.options.ids);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* The id of a vertex position, for addEdge.
|
|
1415
|
+
* @param pos - the vertex number minus the base
|
|
1416
|
+
* @returns the id
|
|
1417
|
+
*/
|
|
1418
|
+
idAt(pos) {
|
|
1419
|
+
if (this.idOfPos !== null) {
|
|
1420
|
+
return this.idOfPos[pos];
|
|
1421
|
+
}
|
|
1422
|
+
return this.idOfNumber(pos + this.base);
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Complete the vertex set once the vertex section is over: vertices without a line are
|
|
1426
|
+
* created (under "label" or a restored `graphty_originalId` with their number as the id), and
|
|
1427
|
+
* a partial list is reported.
|
|
1428
|
+
*/
|
|
1429
|
+
finishVertices() {
|
|
1430
|
+
if (this.verticesDone) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
this.verticesDone = true;
|
|
1434
|
+
if (!this.baseKnown) {
|
|
1435
|
+
this.setBase(1, 0);
|
|
1436
|
+
}
|
|
1437
|
+
const n = this.vertexCount;
|
|
1438
|
+
this.createNodes();
|
|
1439
|
+
if (this.idOfPos !== null) {
|
|
1440
|
+
for (let pos = 0; pos < n; pos++) {
|
|
1441
|
+
if (this.idOfPos[pos] === void 0) {
|
|
1442
|
+
this.numberedNode(this.idOfPos, pos);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
const lines = this.seenCount + this.report.counts.skippedNodes;
|
|
1447
|
+
if (this.seenCount > 0 && lines < n) {
|
|
1448
|
+
this.report.error(
|
|
1449
|
+
"validation-error",
|
|
1450
|
+
PAJEK_ISSUE.VERTEX_COUNT,
|
|
1451
|
+
`*Vertices declares ${n} vertices but ${lines} vertex line(s) were read; the others have no label`
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* A vertex line: `number [label] [x y [z]] [shape] [key value ...] [interval]`.
|
|
1457
|
+
* @param text - the line
|
|
1458
|
+
* @param line - the line number
|
|
1459
|
+
*/
|
|
1460
|
+
vertexLine(text, line) {
|
|
1461
|
+
const tokens = this.tokens(text, "node");
|
|
1462
|
+
if (!isVertexNumber(tokens[0])) {
|
|
1463
|
+
this.report.counts.skippedNodes++;
|
|
1464
|
+
throw new LineError(
|
|
1465
|
+
"parse-error",
|
|
1466
|
+
PAJEK_ISSUE.VERTEX_LINE,
|
|
1467
|
+
`a vertex line starts with a vertex number, not "${tokens[0]}"`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
const k = Number(tokens[0]);
|
|
1471
|
+
if (!this.baseKnown) {
|
|
1472
|
+
this.setBase(k === 0 ? 0 : 1, line);
|
|
1473
|
+
}
|
|
1474
|
+
const pos = k - this.base;
|
|
1475
|
+
if (pos < 0 || pos >= this.vertexCount) {
|
|
1476
|
+
this.report.counts.skippedNodes++;
|
|
1477
|
+
throw new LineError(
|
|
1478
|
+
"validation-error",
|
|
1479
|
+
PAJEK_ISSUE.VERTEX_RANGE,
|
|
1480
|
+
`vertex ${k} is outside ${this.base}..${this.base + this.vertexCount - 1}`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
let i = 1;
|
|
1484
|
+
const label = tokens.length > 1 ? tokens[1] : null;
|
|
1485
|
+
if (label !== null) {
|
|
1486
|
+
i = 2;
|
|
1487
|
+
}
|
|
1488
|
+
let x = 0;
|
|
1489
|
+
let y = 0;
|
|
1490
|
+
let z = 0;
|
|
1491
|
+
let dims = 0;
|
|
1492
|
+
if (i + 1 < tokens.length && isNumericText(tokens[i]) && isNumericText(tokens[i + 1])) {
|
|
1493
|
+
x = Number(tokens[i]);
|
|
1494
|
+
y = Number(tokens[i + 1]);
|
|
1495
|
+
i += 2;
|
|
1496
|
+
dims = 2;
|
|
1497
|
+
if (i < tokens.length && isNumericText(tokens[i])) {
|
|
1498
|
+
z = Number(tokens[i]);
|
|
1499
|
+
i++;
|
|
1500
|
+
dims = 3;
|
|
1501
|
+
}
|
|
1502
|
+
} else if (i < tokens.length && isNumericText(tokens[i])) {
|
|
1503
|
+
this.report.counts.skippedNodes++;
|
|
1504
|
+
throw new LineError(
|
|
1505
|
+
"parse-error",
|
|
1506
|
+
PAJEK_ISSUE.VERTEX_LINE,
|
|
1507
|
+
`vertex ${k}: a single coordinate "${tokens[i]}"; coordinates are x y [z]`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
let shape = null;
|
|
1511
|
+
if (i < tokens.length && SHAPES.has(tokens[i])) {
|
|
1512
|
+
shape = tokens[i];
|
|
1513
|
+
i++;
|
|
1514
|
+
}
|
|
1515
|
+
const extras = this.extras(tokens, i, "node");
|
|
1516
|
+
const restored = this.restoredId(extras);
|
|
1517
|
+
this.createNodes();
|
|
1518
|
+
const repeated = this.seen[pos] !== 0;
|
|
1519
|
+
if (repeated) {
|
|
1520
|
+
this.report.warning(
|
|
1521
|
+
"merged",
|
|
1522
|
+
PAJEK_ISSUE.DUPLICATE_NODE,
|
|
1523
|
+
`vertex ${k} has more than one line; the later values overwrite`,
|
|
1524
|
+
{ line, element: String(k) }
|
|
1525
|
+
);
|
|
1526
|
+
} else {
|
|
1527
|
+
this.seen[pos] = 1;
|
|
1528
|
+
this.seenCount++;
|
|
1529
|
+
}
|
|
1530
|
+
let index;
|
|
1531
|
+
if (this.idOfPos !== null && !repeated && this.options.nodeIdFrom === "label") {
|
|
1532
|
+
index = this.deferredNode(this.idOfPos, pos, k, label, line, "label");
|
|
1533
|
+
} else if (this.idOfPos !== null && this.idOfPos[pos] === void 0) {
|
|
1534
|
+
for (let p = this.filledBelow; p < pos; p++) {
|
|
1535
|
+
if (this.idOfPos[p] === void 0) {
|
|
1536
|
+
this.numberedNode(this.idOfPos, p);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if (pos > this.filledBelow) {
|
|
1540
|
+
this.filledBelow = pos;
|
|
1541
|
+
}
|
|
1542
|
+
index = this.deferredNode(this.idOfPos, pos, k, restored, line, "originalId");
|
|
1543
|
+
} else {
|
|
1544
|
+
index = this.indexOfPos[pos];
|
|
1545
|
+
if (restored !== null && !repeated) {
|
|
1546
|
+
this.report.warning(
|
|
1547
|
+
"coercion",
|
|
1548
|
+
PAJEK_ISSUE.ORIGINAL_ID_UNRESTORED,
|
|
1549
|
+
`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`,
|
|
1550
|
+
{ line, element: String(k) }
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const { sink } = this;
|
|
1555
|
+
if (label !== null) {
|
|
1556
|
+
if (this.labelHandle === INVALID_INDEX) {
|
|
1557
|
+
this.labelHandle = declareResolved(sink, "node", LABEL_DECL, this.report, { line }).handle;
|
|
1558
|
+
}
|
|
1559
|
+
sink.setNodeValue(this.labelHandle, index, label);
|
|
1560
|
+
}
|
|
1561
|
+
if (dims > 0) {
|
|
1562
|
+
if (this.positionHandle === INVALID_INDEX) {
|
|
1563
|
+
this.coordDims = dims;
|
|
1564
|
+
this.positionHandle = declareResolved(sink, "node", positionDecl(dims), this.report, { line }).handle;
|
|
1565
|
+
} else if (dims !== this.coordDims) {
|
|
1566
|
+
this.report.warnOnce(
|
|
1567
|
+
"validation-error",
|
|
1568
|
+
PAJEK_ISSUE.COORD_DIMS,
|
|
1569
|
+
`vertex lines mix ${this.coordDims} and ${dims} coordinates; the position column records sourceDims ${this.coordDims}`,
|
|
1570
|
+
{ line, element: String(k) }
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
sink.setNodeValue(this.positionHandle, index, [x, y, z]);
|
|
1574
|
+
}
|
|
1575
|
+
if (shape !== null) {
|
|
1576
|
+
if (this.shapeHandle === INVALID_INDEX && this.nodeParams.has(SHAPE_COLUMN)) {
|
|
1577
|
+
this.writeParameter("node", SHAPE_COLUMN, index, shape);
|
|
1578
|
+
} else {
|
|
1579
|
+
if (this.shapeHandle === INVALID_INDEX) {
|
|
1580
|
+
this.shapeHandle = declareResolved(sink, "node", SHAPE_DECL, this.report, { line }).handle;
|
|
1581
|
+
}
|
|
1582
|
+
sink.setNodeValue(this.shapeHandle, index, shape);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
for (let p = 0; p < extras.keys.length; p++) {
|
|
1586
|
+
this.writeParameter("node", extras.keys[p], index, extras.values[p]);
|
|
1587
|
+
}
|
|
1588
|
+
if (extras.spells !== null) {
|
|
1589
|
+
if (this.nodeSpellsHandle === INVALID_INDEX) {
|
|
1590
|
+
this.nodeSpellsHandle = declareResolved(sink, "node", spellsDecl(), this.report, { line }).handle;
|
|
1591
|
+
}
|
|
1592
|
+
sink.setNodeValue(this.nodeSpellsHandle, index, extras.spells);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Whether a `graphty_originalId` parameter is restored as the vertex id: restoreMangledIds
|
|
1597
|
+
* under nodeIdFrom "id" (under "label" / "index" the parameter stays an ordinary column).
|
|
1598
|
+
* @returns true when the vertex lines may carry restorable ids
|
|
1599
|
+
*/
|
|
1600
|
+
restoresIds() {
|
|
1601
|
+
return this.options.restoreMangledIds && this.options.nodeIdFrom === "id";
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Push a vertex whose id is its number (restore mode, a position whose line has not come or
|
|
1605
|
+
* carries no original id; or the end of the vertex section under "label").
|
|
1606
|
+
* @param ids - the id per vertex position
|
|
1607
|
+
* @param pos - the vertex position
|
|
1608
|
+
*/
|
|
1609
|
+
numberedNode(ids, pos) {
|
|
1610
|
+
const id = this.idOfNumber(pos + this.base);
|
|
1611
|
+
ids[pos] = id;
|
|
1612
|
+
this.indexOfPos[pos] = this.sink.addNode(id);
|
|
1613
|
+
this.report.counts.nodes++;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* The `graphty_originalId` parameter of a vertex line, taken out of the extras when it is to be
|
|
1617
|
+
* restored as the id (restoreMangledIds, under nodeIdFrom "id"); otherwise null and the
|
|
1618
|
+
* parameter stays an ordinary column.
|
|
1619
|
+
* @param extras - the parsed extras
|
|
1620
|
+
* @returns the original id text, or null
|
|
1621
|
+
*/
|
|
1622
|
+
restoredId(extras) {
|
|
1623
|
+
if (!this.restoresIds()) {
|
|
1624
|
+
return null;
|
|
1625
|
+
}
|
|
1626
|
+
const at = extras.keys.indexOf(ORIGINAL_ID_KEY);
|
|
1627
|
+
if (at < 0) {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
const [text] = extras.values.splice(at, 1);
|
|
1631
|
+
extras.keys.splice(at, 1);
|
|
1632
|
+
return text;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Push the node of a vertex line whose id comes from the line itself: its label under
|
|
1636
|
+
* nodeIdFrom "label", or its restored `graphty_originalId` (the number when the line carries
|
|
1637
|
+
* none), reporting a merge when the id was seen before.
|
|
1638
|
+
* @param ids - the id per vertex position
|
|
1639
|
+
* @param pos - the vertex position
|
|
1640
|
+
* @param k - the vertex number
|
|
1641
|
+
* @param text - the label or original id text, or null
|
|
1642
|
+
* @param line - the line number
|
|
1643
|
+
* @param from - which text the id comes from
|
|
1644
|
+
* @returns the node index
|
|
1645
|
+
*/
|
|
1646
|
+
deferredNode(ids, pos, k, text, line, from) {
|
|
1647
|
+
const id = text === null ? this.idOfNumber(k) : this.coercer.text(text);
|
|
1648
|
+
const before = this.sink.indexOf(id);
|
|
1649
|
+
const index = this.sink.addNode(id);
|
|
1650
|
+
ids[pos] = id;
|
|
1651
|
+
this.indexOfPos[pos] = index;
|
|
1652
|
+
this.report.counts.nodes++;
|
|
1653
|
+
if (before !== INVALID_INDEX) {
|
|
1654
|
+
this.report.warning(
|
|
1655
|
+
"merged",
|
|
1656
|
+
from === "label" ? PAJEK_ISSUE.LABEL_MERGED : PAJEK_ISSUE.ORIGINAL_ID_MERGED,
|
|
1657
|
+
from === "label" ? `vertex ${k} has the label ${JSON.stringify(String(id))} of an earlier vertex; both are one node` : `vertex ${k} has the ${ORIGINAL_ID_KEY} ${JSON.stringify(String(id))} of an earlier vertex; both are one node`,
|
|
1658
|
+
{ line, element: String(k) }
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
return index;
|
|
1662
|
+
}
|
|
1663
|
+
// ============================================================ lines
|
|
1664
|
+
/**
|
|
1665
|
+
* An arc or edge line: `u v [value] [key value ...] [interval]`.
|
|
1666
|
+
* @param text - the line
|
|
1667
|
+
* @param line - the line number
|
|
1668
|
+
*/
|
|
1669
|
+
edgeLine(text, line) {
|
|
1670
|
+
const tokens = this.tokens(text, "edge");
|
|
1671
|
+
if (tokens.length < 2 || !isVertexNumber(tokens[0]) || !isVertexNumber(tokens[1])) {
|
|
1672
|
+
this.report.counts.skippedEdges++;
|
|
1673
|
+
throw new LineError(
|
|
1674
|
+
"parse-error",
|
|
1675
|
+
PAJEK_ISSUE.LINE,
|
|
1676
|
+
`a line starts with two vertex numbers, not "${tokens.slice(0, 2).join(" ")}"`
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
const u = this.endpoint(Number(tokens[0]));
|
|
1680
|
+
const v = this.endpoint(Number(tokens[1]));
|
|
1681
|
+
let i = 2;
|
|
1682
|
+
let valueText = null;
|
|
1683
|
+
if (i < tokens.length && isNumericText(tokens[i])) {
|
|
1684
|
+
valueText = tokens[i];
|
|
1685
|
+
i++;
|
|
1686
|
+
}
|
|
1687
|
+
const extras = this.extras(tokens, i, "edge");
|
|
1688
|
+
const { weightFrom } = this.options;
|
|
1689
|
+
let weight;
|
|
1690
|
+
let valueIsWeight = false;
|
|
1691
|
+
if (isWeightField(VALUE_FIELD, weightFrom)) {
|
|
1692
|
+
valueIsWeight = true;
|
|
1693
|
+
if (valueText !== null) {
|
|
1694
|
+
weight = this.weightOf(valueText);
|
|
1695
|
+
}
|
|
1696
|
+
} else if (weightFrom !== null) {
|
|
1697
|
+
const at = extras.keys.indexOf(weightFrom);
|
|
1698
|
+
if (at >= 0) {
|
|
1699
|
+
weight = this.weightOf(extras.values[at]);
|
|
1700
|
+
extras.keys.splice(at, 1);
|
|
1701
|
+
extras.values.splice(at, 1);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
const e = this.pushEdge(u, v, weight, line);
|
|
1705
|
+
if (valueText !== null && !valueIsWeight) {
|
|
1706
|
+
this.writeParameter("edge", VALUE_COLUMN, e, valueText);
|
|
1707
|
+
}
|
|
1708
|
+
this.writeEdgeExtras(e, extras);
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2 ...`.
|
|
1712
|
+
* @param text - the line
|
|
1713
|
+
* @param line - the line number
|
|
1714
|
+
*/
|
|
1715
|
+
listLine(text, line) {
|
|
1716
|
+
const tokens = this.tokens(text, "edge");
|
|
1717
|
+
for (const token of tokens) {
|
|
1718
|
+
if (!isVertexNumber(token)) {
|
|
1719
|
+
this.report.counts.skippedEdges++;
|
|
1720
|
+
throw new LineError(
|
|
1721
|
+
"parse-error",
|
|
1722
|
+
PAJEK_ISSUE.LINE,
|
|
1723
|
+
`an adjacency list holds vertex numbers only, not "${token}"`
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const u = this.endpoint(Number(tokens[0]));
|
|
1728
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
1729
|
+
const v = this.endpoint(Number(tokens[i]));
|
|
1730
|
+
const e = this.pushEdge(u, v, void 0, line);
|
|
1731
|
+
this.writeRelation(e);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* A `*Matrix` row: N values, every non-zero entry an arc from the row's vertex with that value.
|
|
1736
|
+
* @param text - the line
|
|
1737
|
+
* @param line - the line number
|
|
1738
|
+
*/
|
|
1739
|
+
matrixLine(text, line) {
|
|
1740
|
+
const tokens = this.tokens(text, "edge");
|
|
1741
|
+
const row = this.matrixRows;
|
|
1742
|
+
this.matrixRows++;
|
|
1743
|
+
if (row >= this.vertexCount) {
|
|
1744
|
+
throw new LineError(
|
|
1745
|
+
"validation-error",
|
|
1746
|
+
PAJEK_ISSUE.MATRIX_ROWS,
|
|
1747
|
+
`*Matrix row ${row + 1} is beyond the ${this.vertexCount} declared vertices`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
if (tokens.length !== this.vertexCount) {
|
|
1751
|
+
throw new LineError(
|
|
1752
|
+
"parse-error",
|
|
1753
|
+
PAJEK_ISSUE.LINE,
|
|
1754
|
+
`*Matrix row ${row + 1} has ${tokens.length} value(s); *Vertices declares ${this.vertexCount}`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
for (const token of tokens) {
|
|
1758
|
+
if (!isNumericText(token)) {
|
|
1759
|
+
throw new LineError(
|
|
1760
|
+
"parse-error",
|
|
1761
|
+
PAJEK_ISSUE.LINE,
|
|
1762
|
+
`*Matrix row ${row + 1}: "${token}" is not a number`
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
for (let j = 0; j < tokens.length; j++) {
|
|
1767
|
+
const value = Number(tokens[j]);
|
|
1768
|
+
if (value === 0) {
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
const e = this.pushEdge(row, j, value, line);
|
|
1772
|
+
this.writeRelation(e);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Push one line through the direction resolver, counting the logical edges it produced.
|
|
1777
|
+
* @param u - the source position
|
|
1778
|
+
* @param v - the target position
|
|
1779
|
+
* @param weight - the weight, or undefined for none
|
|
1780
|
+
* @param line - the line number
|
|
1781
|
+
* @returns the primary logical edge index
|
|
1782
|
+
*/
|
|
1783
|
+
pushEdge(u, v, weight, line) {
|
|
1784
|
+
const { sink } = this;
|
|
1785
|
+
const before = sink.edgeCount;
|
|
1786
|
+
const e = this.resolver.addEdge(this.idAt(u), this.idAt(v), this.kind, weight, { line });
|
|
1787
|
+
this.report.counts.edges += sink.edgeCount - before;
|
|
1788
|
+
return e;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Write the relation, parameters and spells of a line on its primary half.
|
|
1792
|
+
* @param e - the edge index
|
|
1793
|
+
* @param extras - the parsed extras
|
|
1794
|
+
*/
|
|
1795
|
+
writeEdgeExtras(e, extras) {
|
|
1796
|
+
const { sink } = this;
|
|
1797
|
+
this.writeRelation(e);
|
|
1798
|
+
for (let p = 0; p < extras.keys.length; p++) {
|
|
1799
|
+
this.writeParameter("edge", extras.keys[p], e, extras.values[p]);
|
|
1800
|
+
}
|
|
1801
|
+
if (extras.spells !== null) {
|
|
1802
|
+
if (this.edgeSpellsHandle === INVALID_INDEX) {
|
|
1803
|
+
this.edgeSpellsHandle = declareResolved(sink, "edge", spellsDecl(), this.report).handle;
|
|
1804
|
+
}
|
|
1805
|
+
sink.setEdgeValue(this.edgeSpellsHandle, e, extras.spells);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Write the current section's relation name on an edge, when the section has one.
|
|
1810
|
+
* @param e - the edge index
|
|
1811
|
+
*/
|
|
1812
|
+
writeRelation(e) {
|
|
1813
|
+
if (this.relation === null) {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (this.relationHandle === INVALID_INDEX) {
|
|
1817
|
+
this.relationHandle = declareResolved(this.sink, "edge", RELATION_DECL, this.report).handle;
|
|
1818
|
+
}
|
|
1819
|
+
this.sink.setEdgeValue(this.relationHandle, e, this.relation);
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* A line endpoint: the vertex number checked against the declared range; a reference outside
|
|
1823
|
+
* it is a missing-value error and the line is skipped (Pajek's `*Vertices N` bounds the id space,
|
|
1824
|
+
* so addMissingNodes does not apply).
|
|
1825
|
+
* @param k - the vertex number
|
|
1826
|
+
* @returns the vertex position
|
|
1827
|
+
*/
|
|
1828
|
+
endpoint(k) {
|
|
1829
|
+
const pos = k - this.base;
|
|
1830
|
+
if (pos < 0 || pos >= this.vertexCount) {
|
|
1831
|
+
this.report.counts.skippedEdges++;
|
|
1832
|
+
throw new LineError(
|
|
1833
|
+
"missing-value",
|
|
1834
|
+
PAJEK_ISSUE.UNKNOWN_NODE,
|
|
1835
|
+
`vertex ${k} is outside ${this.base}..${this.base + this.vertexCount - 1}`
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
return pos;
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* The weight of a line value text.
|
|
1842
|
+
* @param text - the text
|
|
1843
|
+
* @returns the weight; E_INVALID_WEIGHT is recorded by the caller's catch
|
|
1844
|
+
*/
|
|
1845
|
+
weightOf(text) {
|
|
1846
|
+
try {
|
|
1847
|
+
return parseWeightText(text);
|
|
1848
|
+
} catch (err) {
|
|
1849
|
+
this.report.counts.skippedEdges++;
|
|
1850
|
+
throw err;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
// ============================================================ shared
|
|
1854
|
+
/**
|
|
1855
|
+
* Tokenize a data line.
|
|
1856
|
+
* @param text - the line
|
|
1857
|
+
* @param domain - what is skipped on an unterminated quote
|
|
1858
|
+
* @returns the tokens (at least one: blank lines never reach here)
|
|
1859
|
+
*/
|
|
1860
|
+
tokens(text, domain) {
|
|
1861
|
+
const tokens = tokenize(text);
|
|
1862
|
+
if (tokens === null) {
|
|
1863
|
+
if (domain === "node") {
|
|
1864
|
+
this.report.counts.skippedNodes++;
|
|
1865
|
+
} else {
|
|
1866
|
+
this.report.counts.skippedEdges++;
|
|
1867
|
+
}
|
|
1868
|
+
throw new LineError("parse-error", PAJEK_ISSUE.UNTERMINATED_QUOTE, "a double quote is not closed");
|
|
1869
|
+
}
|
|
1870
|
+
return tokens;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* The `key value` parameters and the interval token at the end of a row, validated before
|
|
1874
|
+
* anything is written.
|
|
1875
|
+
* @param tokens - the row's tokens
|
|
1876
|
+
* @param start - the first token to read
|
|
1877
|
+
* @param domain - what is skipped when the row is malformed
|
|
1878
|
+
* @returns the parsed extras
|
|
1879
|
+
*/
|
|
1880
|
+
extras(tokens, start, domain) {
|
|
1881
|
+
const extras = { keys: [], values: [], spells: null };
|
|
1882
|
+
for (let i = start; i < tokens.length; ) {
|
|
1883
|
+
const token = tokens[i];
|
|
1884
|
+
if (isIntervalToken(token)) {
|
|
1885
|
+
try {
|
|
1886
|
+
extras.spells = parseIntervals(token);
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
this.skip(domain);
|
|
1889
|
+
throw new LineError(
|
|
1890
|
+
"validation-error",
|
|
1891
|
+
PAJEK_ISSUE.INTERVAL,
|
|
1892
|
+
err instanceof Error ? err.message : String(err)
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
i++;
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
if (i + 1 >= tokens.length) {
|
|
1899
|
+
this.skip(domain);
|
|
1900
|
+
throw new LineError(
|
|
1901
|
+
"parse-error",
|
|
1902
|
+
domain === "node" ? PAJEK_ISSUE.VERTEX_LINE : PAJEK_ISSUE.LINE,
|
|
1903
|
+
`parameter "${token}" has no value`
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
extras.keys.push(token);
|
|
1907
|
+
extras.values.push(tokens[i + 1]);
|
|
1908
|
+
i += 2;
|
|
1909
|
+
}
|
|
1910
|
+
return extras;
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Count a skipped element.
|
|
1914
|
+
* @param domain - node or edge
|
|
1915
|
+
*/
|
|
1916
|
+
skip(domain) {
|
|
1917
|
+
if (domain === "node") {
|
|
1918
|
+
this.report.counts.skippedNodes++;
|
|
1919
|
+
} else {
|
|
1920
|
+
this.report.counts.skippedEdges++;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Write one `key value` parameter cell through the column's text writer (the 5.1 grammar per
|
|
1925
|
+
* column, the lexical form kept). A `shape` parameter next to the shape keyword column goes
|
|
1926
|
+
* into that column (a dict takes any text), and a shape keyword next to a `shape` parameter
|
|
1927
|
+
* column into that one, so a file mixing the two spellings reads as one column.
|
|
1928
|
+
* @param domain - node or edge
|
|
1929
|
+
* @param key - the parameter key (the column name)
|
|
1930
|
+
* @param row - the node or edge index
|
|
1931
|
+
* @param text - the value text
|
|
1932
|
+
*/
|
|
1933
|
+
writeParameter(domain, key, row, text) {
|
|
1934
|
+
if (domain === "node" && key === SHAPE_COLUMN && this.shapeHandle !== INVALID_INDEX) {
|
|
1935
|
+
this.sink.setNodeValue(this.shapeHandle, row, text);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
const writers = domain === "node" ? this.nodeParams : this.edgeParams;
|
|
1939
|
+
let writer = writers.get(key);
|
|
1940
|
+
if (writer === void 0) {
|
|
1941
|
+
writer = new TextCellWriter(key, domain, this.sink, this.report);
|
|
1942
|
+
writers.set(key, writer);
|
|
1943
|
+
}
|
|
1944
|
+
writer.write(row, text);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
function firstVertexOption(value) {
|
|
1948
|
+
if (value === void 0) {
|
|
1949
|
+
return "auto";
|
|
1950
|
+
}
|
|
1951
|
+
if (FIRST_VERTEX_VALUES.has(value)) {
|
|
1952
|
+
return value;
|
|
1953
|
+
}
|
|
1954
|
+
throw new GraphFormatError(
|
|
1955
|
+
"E_UNSUPPORTED",
|
|
1956
|
+
`option firstVertex: ${JSON.stringify(value) ?? typeof value} is not 0, 1 or "auto"`,
|
|
1957
|
+
{
|
|
1958
|
+
option: "firstVertex",
|
|
1959
|
+
found: value
|
|
1960
|
+
}
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
const HEAD_PATTERN = /^\s*\*(vertices|network)\b/i;
|
|
1964
|
+
const pajekImporter = Object.freeze({
|
|
1965
|
+
format: "pajek",
|
|
1966
|
+
extensions: Object.freeze([".net", ".paj"]),
|
|
1967
|
+
mimeTypes: Object.freeze(["text/x-pajek", "text/plain"]),
|
|
1968
|
+
/**
|
|
1969
|
+
* Confidence that the input is a Pajek network: it starts with `*Vertices` (or `*Network`).
|
|
1970
|
+
* @param head - the first bytes
|
|
1971
|
+
* @returns 0.9 for a `*Vertices` start, 0.8 for `*Network`, 0 otherwise
|
|
1972
|
+
*/
|
|
1973
|
+
sniff(head) {
|
|
1974
|
+
const text = new TextDecoder("utf-8").decode(head.subarray(0, Math.min(head.byteLength, 512)));
|
|
1975
|
+
const match = HEAD_PATTERN.exec(text.startsWith(String.fromCharCode(65279)) ? text.slice(1) : text);
|
|
1976
|
+
if (match === null) {
|
|
1977
|
+
return 0;
|
|
1978
|
+
}
|
|
1979
|
+
return match[1].toLowerCase() === "vertices" ? 0.9 : 0.8;
|
|
1980
|
+
},
|
|
1981
|
+
/**
|
|
1982
|
+
* Read a Pajek network into the sink.
|
|
1983
|
+
* @param input - the text, bytes or stream
|
|
1984
|
+
* @param sink - the sink
|
|
1985
|
+
* @param options - common and Pajek options
|
|
1986
|
+
* @returns the report
|
|
1987
|
+
*/
|
|
1988
|
+
async import(input, sink, options) {
|
|
1989
|
+
const resolved = resolveImportOptions(options, DEFAULTS);
|
|
1990
|
+
const firstVertex = firstVertexOption(options?.firstVertex);
|
|
1991
|
+
const report = new ImportReportBuilder("pajek", resolved.errorLimit);
|
|
1992
|
+
reportSinkOptions(sink, options, report);
|
|
1993
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
1994
|
+
const parser = new PajekParser(sink, report, resolved, firstVertex);
|
|
1995
|
+
const reader = new LineReader(input, report, resolved);
|
|
1996
|
+
let sinceCheck = 0;
|
|
1997
|
+
for await (const text of reader) {
|
|
1998
|
+
parser.line(text, reader.line);
|
|
1999
|
+
if (++sinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
2000
|
+
sinceCheck = 0;
|
|
2001
|
+
throwIfAborted(resolved.signal);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
parser.finish();
|
|
2005
|
+
throwIfAborted(resolved.signal);
|
|
2006
|
+
return report.finish();
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
export {
|
|
2010
|
+
PAJEK_ISSUE as P,
|
|
2011
|
+
PAJEK_LOSS as a,
|
|
2012
|
+
pajekImporter as b,
|
|
2013
|
+
pajekExporter as p
|
|
2014
|
+
};
|
|
2015
|
+
//# sourceMappingURL=importer-CpCpfbxr.js.map
|