@graphty/graph-io 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -28
- package/dist/chunks/children-CL3Cy0ez.js +238 -0
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
- package/dist/chunks/escape-DyI8JofU.js +938 -0
- package/dist/chunks/escape-DyI8JofU.js.map +1 -0
- package/dist/chunks/importer-CQnJuWJw.js +2987 -0
- package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
- package/dist/chunks/importer-CpCpfbxr.js +2015 -0
- package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
- package/dist/chunks/importer-DbnGYr3_.js +2342 -0
- package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
- package/dist/chunks/importer-GozH8DkN.js +3050 -0
- package/dist/chunks/importer-GozH8DkN.js.map +1 -0
- package/dist/chunks/records-CGpxszm1.js +605 -0
- package/dist/chunks/records-CGpxszm1.js.map +1 -0
- package/dist/chunks/text-CajMdVFy.js +189 -0
- package/dist/chunks/text-CajMdVFy.js.map +1 -0
- package/dist/chunks/writer-DxSKC7TL.js +2842 -0
- package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1702 -0
- package/dist/csv.js.map +1 -0
- package/dist/dot.d.ts +1 -0
- package/dist/dot.js +8 -0
- package/dist/dot.js.map +1 -0
- package/dist/gexf.d.ts +1 -0
- package/dist/gexf.js +3466 -0
- package/dist/gexf.js.map +1 -0
- package/dist/gml.d.ts +1 -0
- package/dist/gml.js +2647 -0
- package/dist/gml.js.map +1 -0
- package/dist/graph-io.d.ts +1 -0
- package/dist/graph-io.js +790 -0
- package/dist/graph-io.js.map +1 -0
- package/dist/graphml.d.ts +1 -0
- package/dist/graphml.js +8 -0
- package/dist/graphml.js.map +1 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +11 -0
- package/dist/json.js.map +1 -0
- package/dist/neo4j.d.ts +1 -0
- package/dist/neo4j.js +2046 -0
- package/dist/neo4j.js.map +1 -0
- package/dist/pajek.d.ts +1 -0
- package/dist/pajek.js +8 -0
- package/dist/pajek.js.map +1 -0
- package/dist/src/children.d.ts +134 -0
- package/dist/src/children.d.ts.map +1 -0
- package/dist/src/children.js +274 -0
- package/dist/src/children.js.map +1 -0
- package/dist/src/common/attributes.d.ts +229 -0
- package/dist/src/common/attributes.d.ts.map +1 -0
- package/dist/src/common/attributes.js +368 -0
- package/dist/src/common/attributes.js.map +1 -0
- package/dist/src/common/codes.d.ts +105 -0
- package/dist/src/common/codes.d.ts.map +1 -0
- package/dist/src/common/codes.js +107 -0
- package/dist/src/common/codes.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +84 -0
- package/dist/src/common/declared-types.d.ts.map +1 -0
- package/dist/src/common/declared-types.js +326 -0
- package/dist/src/common/declared-types.js.map +1 -0
- package/dist/src/common/direction.d.ts +206 -0
- package/dist/src/common/direction.d.ts.map +1 -0
- package/dist/src/common/direction.js +370 -0
- package/dist/src/common/direction.js.map +1 -0
- package/dist/src/common/escape.d.ts +92 -0
- package/dist/src/common/escape.d.ts.map +1 -0
- package/dist/src/common/escape.js +212 -0
- package/dist/src/common/escape.js.map +1 -0
- package/dist/src/common/export.d.ts +249 -0
- package/dist/src/common/export.d.ts.map +1 -0
- package/dist/src/common/export.js +594 -0
- package/dist/src/common/export.js.map +1 -0
- package/dist/src/common/format.d.ts +59 -0
- package/dist/src/common/format.d.ts.map +1 -0
- package/dist/src/common/format.js +106 -0
- package/dist/src/common/format.js.map +1 -0
- package/dist/src/common/ids.d.ts +83 -0
- package/dist/src/common/ids.d.ts.map +1 -0
- package/dist/src/common/ids.js +158 -0
- package/dist/src/common/ids.js.map +1 -0
- package/dist/src/common/input.d.ts +100 -0
- package/dist/src/common/input.d.ts.map +1 -0
- package/dist/src/common/input.js +335 -0
- package/dist/src/common/input.js.map +1 -0
- package/dist/src/common/lists.d.ts +34 -0
- package/dist/src/common/lists.d.ts.map +1 -0
- package/dist/src/common/lists.js +185 -0
- package/dist/src/common/lists.js.map +1 -0
- package/dist/src/common/options.d.ts +108 -0
- package/dist/src/common/options.d.ts.map +1 -0
- package/dist/src/common/options.js +265 -0
- package/dist/src/common/options.js.map +1 -0
- package/dist/src/common/report.d.ts +187 -0
- package/dist/src/common/report.d.ts.map +1 -0
- package/dist/src/common/report.js +274 -0
- package/dist/src/common/report.js.map +1 -0
- package/dist/src/common/temporal.d.ts +71 -0
- package/dist/src/common/temporal.d.ts.map +1 -0
- package/dist/src/common/temporal.js +266 -0
- package/dist/src/common/temporal.js.map +1 -0
- package/dist/src/common/text.d.ts +104 -0
- package/dist/src/common/text.d.ts.map +1 -0
- package/dist/src/common/text.js +255 -0
- package/dist/src/common/text.js.map +1 -0
- package/dist/src/common/weights.d.ts +77 -0
- package/dist/src/common/weights.d.ts.map +1 -0
- package/dist/src/common/weights.js +156 -0
- package/dist/src/common/weights.js.map +1 -0
- package/dist/src/common/writer.d.ts +51 -0
- package/dist/src/common/writer.d.ts.map +1 -0
- package/dist/src/common/writer.js +108 -0
- package/dist/src/common/writer.js.map +1 -0
- package/dist/src/common/xml.d.ts +245 -0
- package/dist/src/common/xml.d.ts.map +1 -0
- package/dist/src/common/xml.js +942 -0
- package/dist/src/common/xml.js.map +1 -0
- package/dist/src/formats/csv/exporter.d.ts +70 -0
- package/dist/src/formats/csv/exporter.d.ts.map +1 -0
- package/dist/src/formats/csv/exporter.js +682 -0
- package/dist/src/formats/csv/exporter.js.map +1 -0
- package/dist/src/formats/csv/header.d.ts +66 -0
- package/dist/src/formats/csv/header.d.ts.map +1 -0
- package/dist/src/formats/csv/header.js +152 -0
- package/dist/src/formats/csv/header.js.map +1 -0
- package/dist/src/formats/csv/importer.d.ts +82 -0
- package/dist/src/formats/csv/importer.d.ts.map +1 -0
- package/dist/src/formats/csv/importer.js +849 -0
- package/dist/src/formats/csv/importer.js.map +1 -0
- package/dist/src/formats/csv/index.d.ts +60 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -0
- package/dist/src/formats/csv/index.js +63 -0
- package/dist/src/formats/csv/index.js.map +1 -0
- package/dist/src/formats/csv/records.d.ts +188 -0
- package/dist/src/formats/csv/records.d.ts.map +1 -0
- package/dist/src/formats/csv/records.js +702 -0
- package/dist/src/formats/csv/records.js.map +1 -0
- package/dist/src/formats/csv/values.d.ts +105 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -0
- package/dist/src/formats/csv/values.js +192 -0
- package/dist/src/formats/csv/values.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +52 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -0
- package/dist/src/formats/dot/exporter.js +836 -0
- package/dist/src/formats/dot/exporter.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts +102 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -0
- package/dist/src/formats/dot/importer.js +1291 -0
- package/dist/src/formats/dot/importer.js.map +1 -0
- package/dist/src/formats/dot/index.d.ts +7 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -0
- package/dist/src/formats/dot/index.js +7 -0
- package/dist/src/formats/dot/index.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +29 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -0
- package/dist/src/formats/dot/names.js +28 -0
- package/dist/src/formats/dot/names.js.map +1 -0
- package/dist/src/formats/dot/tokenizer.d.ts +114 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
- package/dist/src/formats/dot/tokenizer.js +341 -0
- package/dist/src/formats/dot/tokenizer.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts +56 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
- package/dist/src/formats/gexf/exporter.js +1395 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +73 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -0
- package/dist/src/formats/gexf/importer.js +1880 -0
- package/dist/src/formats/gexf/importer.js.map +1 -0
- package/dist/src/formats/gexf/index.d.ts +96 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -0
- package/dist/src/formats/gexf/index.js +97 -0
- package/dist/src/formats/gexf/index.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts +135 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -0
- package/dist/src/formats/gexf/schema.js +323 -0
- package/dist/src/formats/gexf/schema.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +69 -0
- package/dist/src/formats/gml/exporter.d.ts.map +1 -0
- package/dist/src/formats/gml/exporter.js +1093 -0
- package/dist/src/formats/gml/exporter.js.map +1 -0
- package/dist/src/formats/gml/importer.d.ts +66 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -0
- package/dist/src/formats/gml/importer.js +1331 -0
- package/dist/src/formats/gml/importer.js.map +1 -0
- package/dist/src/formats/gml/index.d.ts +85 -0
- package/dist/src/formats/gml/index.d.ts.map +1 -0
- package/dist/src/formats/gml/index.js +88 -0
- package/dist/src/formats/gml/index.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +186 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -0
- package/dist/src/formats/gml/syntax.js +467 -0
- package/dist/src/formats/gml/syntax.js.map +1 -0
- package/dist/src/formats/graphml/constants.d.ts +169 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -0
- package/dist/src/formats/graphml/constants.js +165 -0
- package/dist/src/formats/graphml/constants.js.map +1 -0
- package/dist/src/formats/graphml/exporter.d.ts +34 -0
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
- package/dist/src/formats/graphml/exporter.js +1176 -0
- package/dist/src/formats/graphml/exporter.js.map +1 -0
- package/dist/src/formats/graphml/importer.d.ts +31 -0
- package/dist/src/formats/graphml/importer.d.ts.map +1 -0
- package/dist/src/formats/graphml/importer.js +1607 -0
- package/dist/src/formats/graphml/importer.js.map +1 -0
- package/dist/src/formats/graphml/index.d.ts +8 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -0
- package/dist/src/formats/graphml/index.js +8 -0
- package/dist/src/formats/graphml/index.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts +72 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -0
- package/dist/src/formats/graphml/tree.js +290 -0
- package/dist/src/formats/graphml/tree.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +125 -0
- package/dist/src/formats/json/dialect.d.ts.map +1 -0
- package/dist/src/formats/json/dialect.js +262 -0
- package/dist/src/formats/json/dialect.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts +89 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -0
- package/dist/src/formats/json/exporter.js +1358 -0
- package/dist/src/formats/json/exporter.js.map +1 -0
- package/dist/src/formats/json/importer.d.ts +108 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -0
- package/dist/src/formats/json/importer.js +1838 -0
- package/dist/src/formats/json/importer.js.map +1 -0
- package/dist/src/formats/json/index.d.ts +8 -0
- package/dist/src/formats/json/index.d.ts.map +1 -0
- package/dist/src/formats/json/index.js +8 -0
- package/dist/src/formats/json/index.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts +68 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
- package/dist/src/formats/neo4j/exporter.js +1055 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -0
- package/dist/src/formats/neo4j/header.d.ts +52 -0
- package/dist/src/formats/neo4j/header.d.ts.map +1 -0
- package/dist/src/formats/neo4j/header.js +131 -0
- package/dist/src/formats/neo4j/header.js.map +1 -0
- package/dist/src/formats/neo4j/importer.d.ts +73 -0
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
- package/dist/src/formats/neo4j/importer.js +932 -0
- package/dist/src/formats/neo4j/importer.js.map +1 -0
- package/dist/src/formats/neo4j/index.d.ts +79 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -0
- package/dist/src/formats/neo4j/index.js +83 -0
- package/dist/src/formats/neo4j/index.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts +58 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
- package/dist/src/formats/pajek/exporter.js +825 -0
- package/dist/src/formats/pajek/exporter.js.map +1 -0
- package/dist/src/formats/pajek/importer.d.ts +88 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -0
- package/dist/src/formats/pajek/importer.js +1047 -0
- package/dist/src/formats/pajek/importer.js.map +1 -0
- package/dist/src/formats/pajek/index.d.ts +7 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -0
- package/dist/src/formats/pajek/index.js +7 -0
- package/dist/src/formats/pajek/index.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +112 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
- package/dist/src/formats/pajek/syntax.js +269 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +207 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +481 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/sniff.d.ts +104 -0
- package/dist/src/sniff.d.ts.map +1 -0
- package/dist/src/sniff.js +357 -0
- package/dist/src/sniff.js.map +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +29 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +122 -7
- package/src/children.ts +335 -0
- package/src/common/attributes.ts +520 -0
- package/src/common/codes.ts +153 -0
- package/src/common/declared-types.ts +374 -0
- package/src/common/direction.ts +518 -0
- package/src/common/escape.ts +231 -0
- package/src/common/export.ts +817 -0
- package/src/common/format.ts +111 -0
- package/src/common/ids.ts +176 -0
- package/src/common/input.ts +378 -0
- package/src/common/lists.ts +196 -0
- package/src/common/options.ts +377 -0
- package/src/common/report.ts +352 -0
- package/src/common/temporal.ts +302 -0
- package/src/common/text.ts +294 -0
- package/src/common/weights.ts +202 -0
- package/src/common/writer.ts +123 -0
- package/src/common/xml.ts +1053 -0
- package/src/formats/csv/exporter.ts +894 -0
- package/src/formats/csv/header.ts +172 -0
- package/src/formats/csv/importer.ts +1104 -0
- package/src/formats/csv/index.ts +88 -0
- package/src/formats/csv/records.ts +813 -0
- package/src/formats/csv/values.ts +224 -0
- package/src/formats/dot/exporter.ts +1014 -0
- package/src/formats/dot/importer.ts +1549 -0
- package/src/formats/dot/index.ts +7 -0
- package/src/formats/dot/names.ts +40 -0
- package/src/formats/dot/tokenizer.ts +384 -0
- package/src/formats/gexf/exporter.ts +1696 -0
- package/src/formats/gexf/importer.ts +2333 -0
- package/src/formats/gexf/index.ts +142 -0
- package/src/formats/gexf/schema.ts +361 -0
- package/src/formats/gml/exporter.ts +1404 -0
- package/src/formats/gml/importer.ts +1591 -0
- package/src/formats/gml/index.ts +128 -0
- package/src/formats/gml/syntax.ts +545 -0
- package/src/formats/graphml/constants.ts +225 -0
- package/src/formats/graphml/exporter.ts +1458 -0
- package/src/formats/graphml/importer.ts +2027 -0
- package/src/formats/graphml/index.ts +8 -0
- package/src/formats/graphml/tree.ts +318 -0
- package/src/formats/json/dialect.ts +317 -0
- package/src/formats/json/exporter.ts +1616 -0
- package/src/formats/json/importer.ts +2271 -0
- package/src/formats/json/index.ts +8 -0
- package/src/formats/neo4j/exporter.ts +1287 -0
- package/src/formats/neo4j/header.ts +156 -0
- package/src/formats/neo4j/importer.ts +1220 -0
- package/src/formats/neo4j/index.ts +116 -0
- package/src/formats/pajek/exporter.ts +1000 -0
- package/src/formats/pajek/importer.ts +1311 -0
- package/src/formats/pajek/index.ts +7 -0
- package/src/formats/pajek/syntax.ts +307 -0
- package/src/index.ts +244 -0
- package/src/registry.ts +617 -0
- package/src/sniff.ts +397 -0
- package/src/types.ts +262 -0
|
@@ -0,0 +1,2342 @@
|
|
|
1
|
+
import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
|
|
2
|
+
import { c as childrenCsr } from "./children-CL3Cy0ez.js";
|
|
3
|
+
import { L as LOSS, au as joinText, ah as encodeChunks, aB as resolveExportOptions, aa as checkCapabilities, a9 as capabilities, aG as countMixedEdges, ai as explicitWeights, aw as pairFolding, aD as sanitizeIds, ak as formatF32, al as formatF64, aj as formatDecimal, an as formatInteger, a8 as canonicalId, x as MIXED_DIRECTION_CODE, g as DIRECTION_FORCED_CODE, h as DIRECTION_REFUSED_CODE, W as SINK_OPTION_CODE, n as ID_MERGED_CODE, J as OPTION_IGNORED_CODE, C as COLUMN_RENAMED_CODE, V as ROLE_TAKEN_CODE, y as MULTIPLE_GRAPHS_CODE, r as INVALID_UTF8_CODE, m as EMPTY_INPUT_CODE, Z as SYNTAX_CODE, aC as resolveImportOptions, I as ImportReportBuilder, aA as reportUnusedOptions, az as reportSinkOptions, ay as readText, a as throwIfAborted, s as IdCoercer, l as DirectionResolver, ax as parseWeightText, af as declareResolved } from "./writer-DxSKC7TL.js";
|
|
4
|
+
import { b as quoteDotId, c as isWritableDotText } from "./escape-DyI8JofU.js";
|
|
5
|
+
import { i as inferTextDtype, W as WIDENING_UNSUPPORTED_CODE, p as parseTextCell, T as TextCellWriter } from "./text-CajMdVFy.js";
|
|
6
|
+
const DOT_FORMAT = "dot";
|
|
7
|
+
const DOT_ORIGIN = Object.freeze({ format: DOT_FORMAT });
|
|
8
|
+
const CLUSTER_COLUMN = "graphty.cluster";
|
|
9
|
+
const PARENT_COLUMN = "graphty.parent";
|
|
10
|
+
const SOURCE_PORT_COLUMN = "graphty.sourcePort";
|
|
11
|
+
const TARGET_PORT_COLUMN = "graphty.targetPort";
|
|
12
|
+
const LABEL_ATTRIBUTE = "label";
|
|
13
|
+
const POS_ATTRIBUTE = "pos";
|
|
14
|
+
const KEY_ATTRIBUTE = "key";
|
|
15
|
+
const PIN_ATTRIBUTE = "pin";
|
|
16
|
+
const DOT_META_KEY = "dot";
|
|
17
|
+
const DOT_LOSS = Object.freeze({
|
|
18
|
+
/** An id, name or text with a backslash before a quote or at its end cannot be written as a DOT quoted string; export() throws. */
|
|
19
|
+
TRAILING_BACKSLASH: "E_DOT_TRAILING_BACKSLASH",
|
|
20
|
+
/** A non-finite f32 / f64 cell has no numeric DOT spelling and reads back as text. */
|
|
21
|
+
NON_FINITE: "W_DOT_NON_FINITE",
|
|
22
|
+
/** Text cells that look like numbers or booleans read back as such (DOT attribute values are untyped). */
|
|
23
|
+
TEXT_INFERRED: LOSS.TEXT_INFERRED,
|
|
24
|
+
/** A plain column named like an attribute the exporter writes for a role (weight, key, pos) is not written. */
|
|
25
|
+
ATTRIBUTE_CLASH: "W_DOT_ATTRIBUTE_CLASH",
|
|
26
|
+
/** A mutual pair is written as two directed edges. */
|
|
27
|
+
MUTUAL_EXPANDED: LOSS.MUTUAL_EXPANDED,
|
|
28
|
+
/** A parents (multi-parent) column cannot be written; DOT clusters nest. */
|
|
29
|
+
PARENTS_DROPPED: LOSS.PARENTS,
|
|
30
|
+
/** A position column that is not a node column of 2 or 3 components is not written. */
|
|
31
|
+
POSITION_SHAPE: "W_DOT_POSITION_SHAPE",
|
|
32
|
+
/** An id whose text reads back as the other type under ids: "canonical" (1.5 as text, "1" as 1). */
|
|
33
|
+
ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
|
|
34
|
+
/** A declared column whose every row is unset is not written (DOT writes cells, never declarations). */
|
|
35
|
+
EMPTY_COLUMN_DROPPED: LOSS.EMPTY_COLUMN,
|
|
36
|
+
/** A role-less column named `label` reads back with the label role. */
|
|
37
|
+
ROLE_ASSUMED: LOSS.ROLE_ASSUMED
|
|
38
|
+
});
|
|
39
|
+
const KEPT_ROLES = /* @__PURE__ */ new Set(["label", "id", "sourcePort", "targetPort", "parent"]);
|
|
40
|
+
const ROLE_NAMES = Object.freeze({
|
|
41
|
+
label: LABEL_ATTRIBUTE,
|
|
42
|
+
id: KEY_ATTRIBUTE,
|
|
43
|
+
sourcePort: SOURCE_PORT_COLUMN,
|
|
44
|
+
targetPort: TARGET_PORT_COLUMN,
|
|
45
|
+
parent: PARENT_COLUMN,
|
|
46
|
+
position: POS_ATTRIBUTE
|
|
47
|
+
});
|
|
48
|
+
const CAPABILITIES = capabilities({
|
|
49
|
+
mixedDirection: false,
|
|
50
|
+
multiEdges: true,
|
|
51
|
+
selfLoops: true,
|
|
52
|
+
edgeIds: "optional",
|
|
53
|
+
idCharset: "any",
|
|
54
|
+
dtypes: ["bool", "i32", "f64", "string"],
|
|
55
|
+
components: false,
|
|
56
|
+
lists: false,
|
|
57
|
+
json: false,
|
|
58
|
+
defaults: false,
|
|
59
|
+
options: false,
|
|
60
|
+
hierarchy: true,
|
|
61
|
+
temporal: "none",
|
|
62
|
+
graphAttributes: true,
|
|
63
|
+
positions: true,
|
|
64
|
+
viz: false
|
|
65
|
+
});
|
|
66
|
+
const DEFAULT_INDENT = " ";
|
|
67
|
+
const CLUSTER_PREFIX$1 = "cluster";
|
|
68
|
+
const COMPASS_POINTS = /* @__PURE__ */ new Set(["n", "ne", "e", "se", "s", "sw", "w", "nw", "c", "_"]);
|
|
69
|
+
const SKIPPED_NODE_ROLES = /* @__PURE__ */ new Set([
|
|
70
|
+
"parent",
|
|
71
|
+
"parents",
|
|
72
|
+
"position",
|
|
73
|
+
"color",
|
|
74
|
+
"size",
|
|
75
|
+
"shape",
|
|
76
|
+
"thickness",
|
|
77
|
+
"start",
|
|
78
|
+
"end",
|
|
79
|
+
"timestamp",
|
|
80
|
+
"timestamps",
|
|
81
|
+
"spells",
|
|
82
|
+
"open",
|
|
83
|
+
"timeText",
|
|
84
|
+
"directed",
|
|
85
|
+
"pair",
|
|
86
|
+
"mutual",
|
|
87
|
+
"weight"
|
|
88
|
+
]);
|
|
89
|
+
const SKIPPED_EDGE_ROLES = /* @__PURE__ */ new Set([
|
|
90
|
+
"id",
|
|
91
|
+
"weight",
|
|
92
|
+
"directed",
|
|
93
|
+
"pair",
|
|
94
|
+
"mutual",
|
|
95
|
+
"sourcePort",
|
|
96
|
+
"targetPort",
|
|
97
|
+
"position",
|
|
98
|
+
"color",
|
|
99
|
+
"size",
|
|
100
|
+
"shape",
|
|
101
|
+
"thickness",
|
|
102
|
+
"start",
|
|
103
|
+
"end",
|
|
104
|
+
"timestamp",
|
|
105
|
+
"timestamps",
|
|
106
|
+
"spells",
|
|
107
|
+
"open",
|
|
108
|
+
"timeText",
|
|
109
|
+
"parent",
|
|
110
|
+
"parents"
|
|
111
|
+
]);
|
|
112
|
+
const SKIPPED_GRAPH_ROLES = /* @__PURE__ */ new Set(["position", "color", "size", "shape", "thickness"]);
|
|
113
|
+
const TEXT_ROLES = /* @__PURE__ */ new Set(["label", "id", "sourcePort", "targetPort"]);
|
|
114
|
+
const dotExporter = Object.freeze({
|
|
115
|
+
format: DOT_FORMAT,
|
|
116
|
+
capabilities: CAPABILITIES,
|
|
117
|
+
/**
|
|
118
|
+
* Pre-flight: every loss the DOT text would incur, without writing anything.
|
|
119
|
+
* @param snapshot - the snapshot to check
|
|
120
|
+
* @param options - format-specific and common options
|
|
121
|
+
* @returns the notes, empty when the export is exact
|
|
122
|
+
*/
|
|
123
|
+
check(snapshot, options) {
|
|
124
|
+
const resolved = resolveExportOptions(options);
|
|
125
|
+
const notes = checkCapabilities(snapshot, CAPABILITIES, resolved, {
|
|
126
|
+
positionDtype: "f32",
|
|
127
|
+
roles: KEPT_ROLES,
|
|
128
|
+
roleNames: ROLE_NAMES
|
|
129
|
+
});
|
|
130
|
+
const plan = new ExportPlan(snapshot, resolved, options);
|
|
131
|
+
return [...notes, ...plan.notes()];
|
|
132
|
+
},
|
|
133
|
+
/**
|
|
134
|
+
* Write the snapshot as UTF-8 chunks.
|
|
135
|
+
* @param snapshot - the snapshot to write
|
|
136
|
+
* @param options - format-specific and common options
|
|
137
|
+
* @returns the encoded chunks
|
|
138
|
+
*/
|
|
139
|
+
export(snapshot, options) {
|
|
140
|
+
return encodeChunks(writeDot(snapshot, options));
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Write the snapshot as one string.
|
|
144
|
+
* @param snapshot - the snapshot to write
|
|
145
|
+
* @param options - format-specific and common options
|
|
146
|
+
* @returns the whole document
|
|
147
|
+
*/
|
|
148
|
+
exportToString(snapshot, options) {
|
|
149
|
+
return joinText(writeDot(snapshot, options));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
function* writeDot(snapshot, options) {
|
|
153
|
+
const resolved = resolveExportOptions(options);
|
|
154
|
+
const plan = new ExportPlan(snapshot, resolved, options);
|
|
155
|
+
plan.refuse();
|
|
156
|
+
yield* plan.write();
|
|
157
|
+
}
|
|
158
|
+
function endsWithBackslash(text) {
|
|
159
|
+
return !isWritableDotText(text);
|
|
160
|
+
}
|
|
161
|
+
function isHtmlString(text) {
|
|
162
|
+
if (text.length < 2 || !text.startsWith("<") || !text.endsWith(">")) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
let depth = 0;
|
|
166
|
+
for (let i = 0; i < text.length; i++) {
|
|
167
|
+
const c = text.charCodeAt(i);
|
|
168
|
+
if (c === 60) {
|
|
169
|
+
depth++;
|
|
170
|
+
} else if (c === 62) {
|
|
171
|
+
depth--;
|
|
172
|
+
if (depth === 0 && i !== text.length - 1) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return depth === 0;
|
|
178
|
+
}
|
|
179
|
+
function writeId(text) {
|
|
180
|
+
return isHtmlString(text) ? text : quoteDotId(text);
|
|
181
|
+
}
|
|
182
|
+
function writePort(port) {
|
|
183
|
+
const colon = port.lastIndexOf(":");
|
|
184
|
+
if (colon > 0 && COMPASS_POINTS.has(port.slice(colon + 1))) {
|
|
185
|
+
return `:${quoteDotId(port.slice(0, colon))}:${port.slice(colon + 1)}`;
|
|
186
|
+
}
|
|
187
|
+
return `:${quoteDotId(port)}`;
|
|
188
|
+
}
|
|
189
|
+
function isTextualDtype(column) {
|
|
190
|
+
return column.dtype !== "list" && column.dtype !== "json";
|
|
191
|
+
}
|
|
192
|
+
class ExportPlan {
|
|
193
|
+
/**
|
|
194
|
+
* Plan one export.
|
|
195
|
+
* @param snapshot - the snapshot
|
|
196
|
+
* @param resolved - the resolved common options
|
|
197
|
+
* @param options - the caller's options (format-specific fields read here)
|
|
198
|
+
*/
|
|
199
|
+
constructor(snapshot, resolved, options) {
|
|
200
|
+
this.extraNotes = [];
|
|
201
|
+
this.snapshot = snapshot;
|
|
202
|
+
this.resolved = resolved;
|
|
203
|
+
this.indent = options?.indent ?? DEFAULT_INDENT;
|
|
204
|
+
this.name = options?.name === void 0 ? snapshot.meta.name : options.name;
|
|
205
|
+
this.strict = options?.strict ?? isStrictMeta(snapshot);
|
|
206
|
+
this.mixed = countMixedEdges(snapshot);
|
|
207
|
+
if (this.mixed > 0 && resolved.onMixedDirection !== "error") {
|
|
208
|
+
this.directed = resolved.onMixedDirection === "directed";
|
|
209
|
+
} else {
|
|
210
|
+
this.directed = snapshot.directed;
|
|
211
|
+
}
|
|
212
|
+
const { nodes, edges, graph } = snapshot;
|
|
213
|
+
this.position = nodes.byRole("position");
|
|
214
|
+
this.positionDims = positionDims(this.position);
|
|
215
|
+
this.parent = nodes.byRole("parent");
|
|
216
|
+
const cluster = nodes.get(CLUSTER_COLUMN);
|
|
217
|
+
this.cluster = cluster !== null && cluster.dtype === "bool" ? cluster : null;
|
|
218
|
+
this.edgeId = edges.byRole("id");
|
|
219
|
+
this.weights = explicitWeights(snapshot);
|
|
220
|
+
this.folding = pairFolding(snapshot);
|
|
221
|
+
this.sourcePort = edges.byRole("sourcePort");
|
|
222
|
+
this.targetPort = edges.byRole("targetPort");
|
|
223
|
+
this.nodeColumns = [...nodes].filter((c) => this.writesNodeColumn(c));
|
|
224
|
+
this.edgeColumns = [...edges].filter((c) => this.writesEdgeColumn(c));
|
|
225
|
+
this.graphColumns = [...graph].filter(
|
|
226
|
+
(c) => isTextualDtype(c) && (c.meta.role === null || !SKIPPED_GRAPH_ROLES.has(c.meta.role))
|
|
227
|
+
);
|
|
228
|
+
this.children = childrenCsr(snapshot, {
|
|
229
|
+
column: this.parent !== null && this.parent.dtype === "u32" ? this.parent : null
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Whether a node column is written as node attributes.
|
|
234
|
+
* @param column - the column
|
|
235
|
+
* @returns true when written
|
|
236
|
+
*/
|
|
237
|
+
writesNodeColumn(column) {
|
|
238
|
+
const { name, role } = column.meta;
|
|
239
|
+
if (!isTextualDtype(column) || name === CLUSTER_COLUMN) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (role !== null && SKIPPED_NODE_ROLES.has(role)) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
if (name === POS_ATTRIBUTE && role !== "position") {
|
|
246
|
+
this.clash("node", name, "the position role is written as pos");
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Whether an edge column is written as edge attributes.
|
|
253
|
+
* @param column - the column
|
|
254
|
+
* @returns true when written
|
|
255
|
+
*/
|
|
256
|
+
writesEdgeColumn(column) {
|
|
257
|
+
const { name, role } = column.meta;
|
|
258
|
+
if (!isTextualDtype(column)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (role !== null && SKIPPED_EDGE_ROLES.has(role)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
if (name === KEY_ATTRIBUTE && this.edgeId !== null) {
|
|
265
|
+
this.clash("edge", name, "the edge id role is written as key");
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
if (name === "weight") {
|
|
269
|
+
this.clash("edge", name, "the weight is written as weight");
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Record a column not written because its name is an attribute the exporter writes for a role.
|
|
276
|
+
* @param domain - the table
|
|
277
|
+
* @param name - the column name
|
|
278
|
+
* @param reason - why
|
|
279
|
+
*/
|
|
280
|
+
clash(domain, name, reason) {
|
|
281
|
+
this.extraNotes.push(
|
|
282
|
+
Object.freeze({
|
|
283
|
+
code: DOT_LOSS.ATTRIBUTE_CLASH,
|
|
284
|
+
message: `${domain} column "${name}" has no role and is not written: ${reason}`,
|
|
285
|
+
column: name,
|
|
286
|
+
count: null
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The format-specific loss notes (the generic ones come from checkCapabilities).
|
|
292
|
+
* @returns the notes
|
|
293
|
+
*/
|
|
294
|
+
notes() {
|
|
295
|
+
const notes = [...this.extraNotes];
|
|
296
|
+
const note = (code, message, column = null, count = null) => {
|
|
297
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
298
|
+
};
|
|
299
|
+
const { snapshot } = this;
|
|
300
|
+
const unwritable = this.countUnwritable();
|
|
301
|
+
if (unwritable > 0) {
|
|
302
|
+
note(
|
|
303
|
+
DOT_LOSS.TRAILING_BACKSLASH,
|
|
304
|
+
`${unwritable} id(s), name(s) or text value(s) hold a backslash before a quote or at the end, which a DOT quoted string cannot carry; export() will throw`,
|
|
305
|
+
null,
|
|
306
|
+
unwritable
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
for (const [label, columns] of [
|
|
310
|
+
["node", this.nodeColumns],
|
|
311
|
+
["edge", this.edgeColumns],
|
|
312
|
+
["graph", this.graphColumns]
|
|
313
|
+
]) {
|
|
314
|
+
for (const column of columns) {
|
|
315
|
+
if (label !== "graph" && column.nullCount === column.length) {
|
|
316
|
+
note(
|
|
317
|
+
DOT_LOSS.EMPTY_COLUMN_DROPPED,
|
|
318
|
+
`${label} column "${column.meta.name}" has no set cell and is not written: DOT writes cells, never declarations`,
|
|
319
|
+
column.meta.name,
|
|
320
|
+
0
|
|
321
|
+
);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (label !== "graph" && column.meta.role === null && column.meta.name === LABEL_ATTRIBUTE && (column.dtype === "string" || column.dtype === "dict")) {
|
|
325
|
+
note(
|
|
326
|
+
DOT_LOSS.ROLE_ASSUMED,
|
|
327
|
+
`${label} column "${column.meta.name}" reads back with the label role`,
|
|
328
|
+
column.meta.name,
|
|
329
|
+
column.length - column.nullCount
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (column.dtype === "f32" || column.dtype === "f64") {
|
|
333
|
+
const bad = countNonFinite(column);
|
|
334
|
+
if (bad > 0) {
|
|
335
|
+
note(
|
|
336
|
+
DOT_LOSS.NON_FINITE,
|
|
337
|
+
`${label} column "${column.meta.name}" holds ${bad} non-finite value(s) with no numeric DOT spelling; they read back as text`,
|
|
338
|
+
column.meta.name,
|
|
339
|
+
bad
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if ((column.dtype === "string" || column.dtype === "dict") && (column.meta.role === null || !TEXT_ROLES.has(column.meta.role))) {
|
|
344
|
+
const typed = countTypedLookingText(column);
|
|
345
|
+
if (typed > 0) {
|
|
346
|
+
note(
|
|
347
|
+
DOT_LOSS.TEXT_INFERRED,
|
|
348
|
+
`${label} column "${column.meta.name}" holds ${typed} text value(s) that look like numbers or booleans; DOT values are untyped and they read back as such`,
|
|
349
|
+
column.meta.name,
|
|
350
|
+
typed
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const mutual = this.folding.mutualCount;
|
|
357
|
+
if (mutual > 0) {
|
|
358
|
+
note(
|
|
359
|
+
DOT_LOSS.MUTUAL_EXPANDED,
|
|
360
|
+
`${mutual} mutual pair(s) are written as two directed edges each; the mutual mark is lost`,
|
|
361
|
+
null,
|
|
362
|
+
mutual
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const parents = snapshot.nodes.byRole("parents");
|
|
366
|
+
if (parents !== null) {
|
|
367
|
+
note(
|
|
368
|
+
DOT_LOSS.PARENTS_DROPPED,
|
|
369
|
+
`node column "${parents.meta.name}" (parents) cannot be written: a DOT node is in one cluster`,
|
|
370
|
+
parents.meta.name,
|
|
371
|
+
parents.length - parents.nullCount
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (this.position !== null && this.position.meta.components !== 2 && this.position.meta.components !== 3) {
|
|
375
|
+
note(
|
|
376
|
+
DOT_LOSS.POSITION_SHAPE,
|
|
377
|
+
`node column "${this.position.meta.name}" (position) has ${this.position.meta.components} components and is not written; pos takes 2 or 3`,
|
|
378
|
+
this.position.meta.name,
|
|
379
|
+
this.position.length - this.position.nullCount
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
for (const [label, table] of [
|
|
383
|
+
["edge", snapshot.edges],
|
|
384
|
+
["graph", snapshot.graph]
|
|
385
|
+
]) {
|
|
386
|
+
const column = table.byRole("position");
|
|
387
|
+
if (column !== null) {
|
|
388
|
+
note(
|
|
389
|
+
DOT_LOSS.POSITION_SHAPE,
|
|
390
|
+
`${label} column "${column.meta.name}" (position) is not written; only node positions map to pos`,
|
|
391
|
+
column.meta.name,
|
|
392
|
+
column.length - column.nullCount
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const textIds = countTextIds(snapshot);
|
|
397
|
+
if (textIds > 0) {
|
|
398
|
+
note(
|
|
399
|
+
DOT_LOSS.ID_TEXT_TYPE,
|
|
400
|
+
`${textIds} node id(s) read back as the other type under ids: "canonical" (a string "1" becomes 1, a number 1.5 becomes "1.5")`,
|
|
401
|
+
null,
|
|
402
|
+
textIds
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return notes;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Throw for the conditions check() reports as errors: mixed direction under "error", and an
|
|
409
|
+
* unwritable id / name / text.
|
|
410
|
+
*/
|
|
411
|
+
refuse() {
|
|
412
|
+
if (this.mixed > 0 && this.resolved.onMixedDirection === "error") {
|
|
413
|
+
throw new GraphFormatError(
|
|
414
|
+
"E_DIRECTED",
|
|
415
|
+
`${this.mixed} undirected edge(s) in a directed graph; DOT has no mixed direction and onMixedDirection is "error"`,
|
|
416
|
+
{ reason: LOSS.MIXED_DIRECTION_ERROR, count: this.mixed }
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const first = this.firstUnwritable();
|
|
420
|
+
if (first !== null) {
|
|
421
|
+
throw new GraphFormatError(
|
|
422
|
+
first.kind === "id" ? "E_INVALID_ID" : "E_COLUMN_TYPE",
|
|
423
|
+
`${first.kind} ${JSON.stringify(first.text)} ends in a backslash, which a DOT quoted string cannot carry`,
|
|
424
|
+
{ reason: "trailing backslash", kind: first.kind, value: first.text }
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
sanitizeIds(this.snapshot, CAPABILITIES.idCharset, this.resolved.sanitizeIds);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* How many ids, names and text values end in a backslash.
|
|
431
|
+
* @returns the count
|
|
432
|
+
*/
|
|
433
|
+
countUnwritable() {
|
|
434
|
+
let count = 0;
|
|
435
|
+
this.forEachText((text) => {
|
|
436
|
+
if (endsWithBackslash(text)) {
|
|
437
|
+
count++;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return count;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* The first id, name or text value ending in a backslash.
|
|
444
|
+
* @returns what it is and its text, or null
|
|
445
|
+
*/
|
|
446
|
+
firstUnwritable() {
|
|
447
|
+
let found = null;
|
|
448
|
+
this.forEachText((text, kind) => {
|
|
449
|
+
if (found === null && endsWithBackslash(text)) {
|
|
450
|
+
found = { kind, text };
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
return found;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Visit every text the document writes as a quoted string: the graph name, node ids, column
|
|
457
|
+
* names and string / dict cells.
|
|
458
|
+
* @param visit - the visitor
|
|
459
|
+
*/
|
|
460
|
+
forEachText(visit) {
|
|
461
|
+
const { snapshot } = this;
|
|
462
|
+
if (this.name !== null) {
|
|
463
|
+
visit(this.name, "graph name");
|
|
464
|
+
}
|
|
465
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
466
|
+
const id = snapshot.ids.idOf(i);
|
|
467
|
+
if (typeof id === "string") {
|
|
468
|
+
visit(id, "id");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
for (const columns of [this.nodeColumns, this.edgeColumns, this.graphColumns]) {
|
|
472
|
+
for (const column of columns) {
|
|
473
|
+
visit(column.meta.name, "attribute name");
|
|
474
|
+
if (column.dtype === "string" || column.dtype === "dict") {
|
|
475
|
+
for (let r = 0; r < column.length; r++) {
|
|
476
|
+
if (column.isSet(r)) {
|
|
477
|
+
visit(column.value(r), "value");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (const column of [this.edgeId, this.sourcePort, this.targetPort]) {
|
|
484
|
+
if (column !== null && (column.dtype === "string" || column.dtype === "dict")) {
|
|
485
|
+
for (let r = 0; r < column.length; r++) {
|
|
486
|
+
if (column.isSet(r)) {
|
|
487
|
+
visit(column.value(r), "value");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* The document, one statement per part.
|
|
495
|
+
* @yields the parts
|
|
496
|
+
* @returns nothing
|
|
497
|
+
*/
|
|
498
|
+
*write() {
|
|
499
|
+
const { snapshot, indent } = this;
|
|
500
|
+
const keyword = this.directed ? "digraph" : "graph";
|
|
501
|
+
const name = this.name === null ? "" : ` ${quoteDotId(this.name)}`;
|
|
502
|
+
yield `${this.strict ? "strict " : ""}${keyword}${name} {
|
|
503
|
+
`;
|
|
504
|
+
const graphAttributes = this.attributesOf(this.graphColumns, 0);
|
|
505
|
+
if (graphAttributes.length > 0) {
|
|
506
|
+
yield `${indent}graph [${graphAttributes.join(", ")}];
|
|
507
|
+
`;
|
|
508
|
+
}
|
|
509
|
+
const emitted = new Uint8Array(snapshot.nodeCount);
|
|
510
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
511
|
+
yield* this.writeNode(i, 1, emitted);
|
|
512
|
+
}
|
|
513
|
+
yield* this.writeEdges();
|
|
514
|
+
yield "}\n";
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Write one node (and, for a container, its cluster block with the members nested).
|
|
518
|
+
* @param i - the node index
|
|
519
|
+
* @param depth - the nesting depth
|
|
520
|
+
* @param emitted - which nodes were written already
|
|
521
|
+
* @yields the statements
|
|
522
|
+
* @returns nothing
|
|
523
|
+
*/
|
|
524
|
+
*writeNode(i, depth, emitted) {
|
|
525
|
+
if (emitted[i] === 1) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
emitted[i] = 1;
|
|
529
|
+
const pad = this.indent.repeat(depth);
|
|
530
|
+
const idText = this.idText(i);
|
|
531
|
+
const kids = this.children.childrenOf(i);
|
|
532
|
+
const marked = this.cluster !== null && this.cluster.isSet(i) && this.cluster.value(i) === true;
|
|
533
|
+
if (kids.length === 0 && !marked) {
|
|
534
|
+
yield `${pad}${this.nodeStatement(i, idText)};
|
|
535
|
+
`;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (!marked) {
|
|
539
|
+
yield `${pad}${this.nodeStatement(i, idText)};
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
542
|
+
const inner = `${pad}${this.indent}`;
|
|
543
|
+
yield `${pad}subgraph ${writeId(idText)} {
|
|
544
|
+
`;
|
|
545
|
+
if (!idText.startsWith(CLUSTER_PREFIX$1)) {
|
|
546
|
+
yield `${inner}cluster=true;
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
if (marked) {
|
|
550
|
+
const attributes = this.nodeAttributes(i);
|
|
551
|
+
if (attributes.length > 0) {
|
|
552
|
+
yield `${inner}graph [${attributes.join(", ")}];
|
|
553
|
+
`;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const child of kids) {
|
|
557
|
+
if (emitted[child] === 1) {
|
|
558
|
+
yield `${inner}${writeId(this.idText(child))};
|
|
559
|
+
`;
|
|
560
|
+
} else {
|
|
561
|
+
yield* this.writeNode(child, depth + 1, emitted);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
yield `${pad}}
|
|
565
|
+
`;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* A node statement without its terminator.
|
|
569
|
+
* @param i - the node index
|
|
570
|
+
* @param idText - the id text
|
|
571
|
+
* @returns `id` or `id [attrs]`
|
|
572
|
+
*/
|
|
573
|
+
nodeStatement(i, idText) {
|
|
574
|
+
const attributes = this.nodeAttributes(i);
|
|
575
|
+
return attributes.length === 0 ? writeId(idText) : `${writeId(idText)} [${attributes.join(", ")}]`;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The attributes of a node: its set cells plus `pos` from the position column.
|
|
579
|
+
* @param i - the node index
|
|
580
|
+
* @returns the attribute texts
|
|
581
|
+
*/
|
|
582
|
+
nodeAttributes(i) {
|
|
583
|
+
const attributes = this.attributesOf(this.nodeColumns, i);
|
|
584
|
+
const pos = this.positionText(i);
|
|
585
|
+
if (pos !== null) {
|
|
586
|
+
attributes.push(`${POS_ATTRIBUTE}=${quoteDotId(pos)}`);
|
|
587
|
+
}
|
|
588
|
+
return attributes;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* The `pos` text of a node, or null when it has none.
|
|
592
|
+
* @param i - the node index
|
|
593
|
+
* @returns `x,y` or `x,y,z`
|
|
594
|
+
*/
|
|
595
|
+
positionText(i) {
|
|
596
|
+
const column = this.position;
|
|
597
|
+
if (column === null || !column.isSet(i)) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
if (column.dtype !== "f32" && column.dtype !== "f64" && column.dtype !== "i32") {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
const { components } = column.meta;
|
|
604
|
+
if (components !== 2 && components !== 3) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const format = column.dtype === "f32" ? formatF32 : formatF64;
|
|
608
|
+
const base = i * components;
|
|
609
|
+
const { data } = column;
|
|
610
|
+
const parts = [format(data[base]), format(data[base + 1])];
|
|
611
|
+
if (this.positionDims === 3) {
|
|
612
|
+
parts.push(format(components === 3 ? data[base + 2] : 0));
|
|
613
|
+
}
|
|
614
|
+
return parts.join(",");
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The `name=value` attributes of one row over a column list (set cells only).
|
|
618
|
+
* @param columns - the columns
|
|
619
|
+
* @param row - the row
|
|
620
|
+
* @returns the attribute texts
|
|
621
|
+
*/
|
|
622
|
+
attributesOf(columns, row) {
|
|
623
|
+
const out = [];
|
|
624
|
+
for (const column of columns) {
|
|
625
|
+
if (!column.isSet(row)) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const text = cellText(column, row);
|
|
629
|
+
if (text !== null) {
|
|
630
|
+
out.push(`${quoteDotId(column.meta.name)}=${writeId(text)}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return out;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Every logical edge in index order, mirror halves of expanded undirected pairs folded away.
|
|
637
|
+
* @yields the edge statements
|
|
638
|
+
* @returns nothing
|
|
639
|
+
*/
|
|
640
|
+
*writeEdges() {
|
|
641
|
+
const { snapshot, indent } = this;
|
|
642
|
+
const list = snapshot.edgeList();
|
|
643
|
+
const op = this.directed ? "->" : "--";
|
|
644
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
645
|
+
if (this.folding.folded(e)) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const attributes = this.attributesOf(this.edgeColumns, e);
|
|
649
|
+
const weight = this.weights.text(e);
|
|
650
|
+
if (weight !== null) {
|
|
651
|
+
attributes.unshift(`weight=${quoteDotId(weight)}`);
|
|
652
|
+
}
|
|
653
|
+
const key = this.keyText(e);
|
|
654
|
+
if (key !== null) {
|
|
655
|
+
attributes.unshift(`${KEY_ATTRIBUTE}=${writeId(key)}`);
|
|
656
|
+
}
|
|
657
|
+
const source = `${writeId(this.idText(list.src[e]))}${this.portText(this.sourcePort, e)}`;
|
|
658
|
+
const target = `${writeId(this.idText(list.dst[e]))}${this.portText(this.targetPort, e)}`;
|
|
659
|
+
const tail = attributes.length === 0 ? "" : ` [${attributes.join(", ")}]`;
|
|
660
|
+
yield `${indent}${source} ${op} ${target}${tail};
|
|
661
|
+
`;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* The edge id of an edge as `key` text, or null.
|
|
666
|
+
* @param e - the edge index
|
|
667
|
+
* @returns the text
|
|
668
|
+
*/
|
|
669
|
+
keyText(e) {
|
|
670
|
+
const column = this.edgeId;
|
|
671
|
+
if (column === null || !column.isSet(e)) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
if (column.dtype === "f32" || column.dtype === "f64") {
|
|
675
|
+
return formatF64(column.data[e]);
|
|
676
|
+
}
|
|
677
|
+
return cellText(column, e);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* The port suffix of an endpoint.
|
|
681
|
+
* @param column - the port column, or null
|
|
682
|
+
* @param e - the edge index
|
|
683
|
+
* @returns `:port` or ""
|
|
684
|
+
*/
|
|
685
|
+
portText(column, e) {
|
|
686
|
+
if (column === null || !column.isSet(e)) {
|
|
687
|
+
return "";
|
|
688
|
+
}
|
|
689
|
+
const text = cellText(column, e);
|
|
690
|
+
return text === null || text.length === 0 ? "" : writePort(text);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* The id text of a node.
|
|
694
|
+
* @param i - the node index
|
|
695
|
+
* @returns String(id)
|
|
696
|
+
*/
|
|
697
|
+
idText(i) {
|
|
698
|
+
return idToText(this.snapshot.ids.idOf(i));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function idToText(id) {
|
|
702
|
+
return typeof id === "string" ? id : formatF64(id);
|
|
703
|
+
}
|
|
704
|
+
function isStrictMeta(snapshot) {
|
|
705
|
+
const dot = snapshot.meta.extra[DOT_META_KEY];
|
|
706
|
+
return typeof dot === "object" && dot !== null && dot.strict === true;
|
|
707
|
+
}
|
|
708
|
+
function positionDims(column) {
|
|
709
|
+
if (column === null) {
|
|
710
|
+
return 2;
|
|
711
|
+
}
|
|
712
|
+
const dims = column.meta.extra.sourceDims;
|
|
713
|
+
if (dims === 2 || dims === 3) {
|
|
714
|
+
return dims;
|
|
715
|
+
}
|
|
716
|
+
return column.meta.components === 2 ? 2 : 3;
|
|
717
|
+
}
|
|
718
|
+
function cellText(column, row) {
|
|
719
|
+
switch (column.dtype) {
|
|
720
|
+
case "bool":
|
|
721
|
+
return column.value(row) === true ? "true" : "false";
|
|
722
|
+
case "i32":
|
|
723
|
+
case "u32":
|
|
724
|
+
case "u8":
|
|
725
|
+
return numericText(column, row, formatInteger);
|
|
726
|
+
case "f32":
|
|
727
|
+
return numericText(column, row, (v) => formatDecimal(v, "f32"));
|
|
728
|
+
case "f64":
|
|
729
|
+
return numericText(column, row, (v) => formatDecimal(v, "f64"));
|
|
730
|
+
case "dict":
|
|
731
|
+
case "string":
|
|
732
|
+
return column.value(row) ?? "";
|
|
733
|
+
case "list":
|
|
734
|
+
case "json":
|
|
735
|
+
return null;
|
|
736
|
+
default: {
|
|
737
|
+
const name = column.dtype;
|
|
738
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function numericText(column, row, format) {
|
|
743
|
+
const { components } = column.meta;
|
|
744
|
+
if (components === 1) {
|
|
745
|
+
return format(column.data[row]);
|
|
746
|
+
}
|
|
747
|
+
const parts = [];
|
|
748
|
+
for (let k = 0; k < components; k++) {
|
|
749
|
+
parts.push(format(column.data[row * components + k]));
|
|
750
|
+
}
|
|
751
|
+
return parts.join(",");
|
|
752
|
+
}
|
|
753
|
+
function countNonFinite(column) {
|
|
754
|
+
if (column.dtype !== "f32" && column.dtype !== "f64") {
|
|
755
|
+
return 0;
|
|
756
|
+
}
|
|
757
|
+
const { components } = column.meta;
|
|
758
|
+
let count = 0;
|
|
759
|
+
for (let r = 0; r < column.length; r++) {
|
|
760
|
+
if (!column.isSet(r)) {
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
for (let k = 0; k < components; k++) {
|
|
764
|
+
if (!Number.isFinite(column.data[r * components + k])) {
|
|
765
|
+
count++;
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return count;
|
|
771
|
+
}
|
|
772
|
+
function countTypedLookingText(column) {
|
|
773
|
+
if (column.dtype !== "string" && column.dtype !== "dict") {
|
|
774
|
+
return 0;
|
|
775
|
+
}
|
|
776
|
+
let count = 0;
|
|
777
|
+
for (let r = 0; r < column.length; r++) {
|
|
778
|
+
if (column.isSet(r) && inferTextDtype(column.value(r) ?? "") !== "string") {
|
|
779
|
+
count++;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return count;
|
|
783
|
+
}
|
|
784
|
+
function countTextIds(snapshot) {
|
|
785
|
+
const { ids } = snapshot;
|
|
786
|
+
if (ids.kind === "identity" || ids.kind === "dense") {
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
let count = 0;
|
|
790
|
+
for (let i = 0; i < ids.size; i++) {
|
|
791
|
+
const id = ids.idOf(i);
|
|
792
|
+
if (canonicalId(idToText(id)) !== id) {
|
|
793
|
+
count++;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return count;
|
|
797
|
+
}
|
|
798
|
+
class DotSyntaxError extends Error {
|
|
799
|
+
/**
|
|
800
|
+
* Create a syntax error.
|
|
801
|
+
* @param message - a plain-ASCII message
|
|
802
|
+
* @param line - the 1-based line
|
|
803
|
+
*/
|
|
804
|
+
constructor(message, line) {
|
|
805
|
+
super(message);
|
|
806
|
+
this.name = "DotSyntaxError";
|
|
807
|
+
this.line = line;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
const EOF_TOKEN_TEXT = "";
|
|
811
|
+
function isIdentifierStart(c) {
|
|
812
|
+
return c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c >= 128;
|
|
813
|
+
}
|
|
814
|
+
function isIdentifierPart(c) {
|
|
815
|
+
return isIdentifierStart(c) || isDigit(c);
|
|
816
|
+
}
|
|
817
|
+
function isDigit(c) {
|
|
818
|
+
return c >= 48 && c <= 57;
|
|
819
|
+
}
|
|
820
|
+
class DotTokenizer {
|
|
821
|
+
/**
|
|
822
|
+
* Create a lexer over a whole document.
|
|
823
|
+
* @param text - the DOT text (BOM already removed)
|
|
824
|
+
* @param onAmbiguity - called for a badly delimited numeral (`1e3`, which Graphviz splits into
|
|
825
|
+
* `1` and `e3` with a warning), with the numeral text and its line
|
|
826
|
+
*/
|
|
827
|
+
constructor(text, onAmbiguity = null) {
|
|
828
|
+
this.pos = 0;
|
|
829
|
+
this.line = 1;
|
|
830
|
+
this.lookahead = [];
|
|
831
|
+
this.text = text;
|
|
832
|
+
this.onAmbiguity = onAmbiguity;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Look at a token without consuming it.
|
|
836
|
+
* @param n - how far ahead (0 = the next token)
|
|
837
|
+
* @returns the token; an "eof" token past the end
|
|
838
|
+
*/
|
|
839
|
+
peek(n = 0) {
|
|
840
|
+
while (this.lookahead.length <= n) {
|
|
841
|
+
this.lookahead.push(this.lex());
|
|
842
|
+
}
|
|
843
|
+
return this.lookahead[n];
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Consume the next token.
|
|
847
|
+
* @returns the token; an "eof" token past the end
|
|
848
|
+
*/
|
|
849
|
+
next() {
|
|
850
|
+
const ahead = this.lookahead.shift();
|
|
851
|
+
return ahead ?? this.lex();
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Produce the next token from the text.
|
|
855
|
+
* @returns the token
|
|
856
|
+
*/
|
|
857
|
+
lex() {
|
|
858
|
+
this.skipTrivia();
|
|
859
|
+
const { text } = this;
|
|
860
|
+
if (this.pos >= text.length) {
|
|
861
|
+
return { kind: "eof", text: EOF_TOKEN_TEXT, quoted: false, html: false, line: this.line };
|
|
862
|
+
}
|
|
863
|
+
const start = this.pos;
|
|
864
|
+
const { line } = this;
|
|
865
|
+
const c = text.charCodeAt(start);
|
|
866
|
+
switch (c) {
|
|
867
|
+
case 123:
|
|
868
|
+
// {
|
|
869
|
+
case 125:
|
|
870
|
+
// }
|
|
871
|
+
case 91:
|
|
872
|
+
// [
|
|
873
|
+
case 93:
|
|
874
|
+
// ]
|
|
875
|
+
case 59:
|
|
876
|
+
// ;
|
|
877
|
+
case 44:
|
|
878
|
+
// ,
|
|
879
|
+
case 61:
|
|
880
|
+
// =
|
|
881
|
+
case 58:
|
|
882
|
+
// :
|
|
883
|
+
case 43:
|
|
884
|
+
this.pos++;
|
|
885
|
+
return { kind: "punct", text: text[start], quoted: false, html: false, line };
|
|
886
|
+
case 45: {
|
|
887
|
+
const d = text.charCodeAt(start + 1);
|
|
888
|
+
if (d === 62 || d === 45) {
|
|
889
|
+
this.pos += 2;
|
|
890
|
+
return { kind: "punct", text: text.slice(start, start + 2), quoted: false, html: false, line };
|
|
891
|
+
}
|
|
892
|
+
if (isDigit(d) || d === 46 && isDigit(text.charCodeAt(start + 2))) {
|
|
893
|
+
return this.numeral(start, line);
|
|
894
|
+
}
|
|
895
|
+
throw new DotSyntaxError("unexpected '-': not an edge operator or a numeral", line);
|
|
896
|
+
}
|
|
897
|
+
case 34:
|
|
898
|
+
return this.quoted(start, line);
|
|
899
|
+
case 60:
|
|
900
|
+
return this.html(start, line);
|
|
901
|
+
}
|
|
902
|
+
if (isIdentifierStart(c)) {
|
|
903
|
+
let end = start + 1;
|
|
904
|
+
while (end < text.length && isIdentifierPart(text.charCodeAt(end))) {
|
|
905
|
+
end++;
|
|
906
|
+
}
|
|
907
|
+
this.pos = end;
|
|
908
|
+
return { kind: "id", text: text.slice(start, end), quoted: false, html: false, line };
|
|
909
|
+
}
|
|
910
|
+
if (isDigit(c) || c === 46 && isDigit(text.charCodeAt(start + 1))) {
|
|
911
|
+
return this.numeral(start, line);
|
|
912
|
+
}
|
|
913
|
+
throw new DotSyntaxError(`unexpected character ${JSON.stringify(text[start])}`, line);
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Lex a numeral `-?(.[0-9]+ | [0-9]+(.[0-9]*)?)` starting at `start`.
|
|
917
|
+
* @param start - the offset of the first character (a sign, a digit or a dot)
|
|
918
|
+
* @param line - the line
|
|
919
|
+
* @returns the id token holding the numeral text
|
|
920
|
+
*/
|
|
921
|
+
numeral(start, line) {
|
|
922
|
+
const { text } = this;
|
|
923
|
+
let end = start;
|
|
924
|
+
if (text.charCodeAt(end) === 45) {
|
|
925
|
+
end++;
|
|
926
|
+
}
|
|
927
|
+
while (end < text.length && isDigit(text.charCodeAt(end))) {
|
|
928
|
+
end++;
|
|
929
|
+
}
|
|
930
|
+
if (text.charCodeAt(end) === 46) {
|
|
931
|
+
end++;
|
|
932
|
+
while (end < text.length && isDigit(text.charCodeAt(end))) {
|
|
933
|
+
end++;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
this.pos = end;
|
|
937
|
+
const numeral = text.slice(start, end);
|
|
938
|
+
if (end < text.length && isIdentifierStart(text.charCodeAt(end)) && this.onAmbiguity !== null) {
|
|
939
|
+
this.onAmbiguity(numeral, line);
|
|
940
|
+
}
|
|
941
|
+
return { kind: "id", text: numeral, quoted: false, html: false, line };
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Lex a double-quoted string starting at the opening quote: `\"` is the only escape, a
|
|
945
|
+
* backslash followed by a line break is a continuation (both removed), every other backslash is
|
|
946
|
+
* kept as written, and raw line breaks are allowed inside.
|
|
947
|
+
* @param start - the offset of the opening quote
|
|
948
|
+
* @param line - the line of the opening quote
|
|
949
|
+
* @returns the id token holding the decoded text
|
|
950
|
+
*/
|
|
951
|
+
quoted(start, line) {
|
|
952
|
+
const { text } = this;
|
|
953
|
+
let out = "";
|
|
954
|
+
let segment = start + 1;
|
|
955
|
+
let i = start + 1;
|
|
956
|
+
for (; ; ) {
|
|
957
|
+
if (i >= text.length) {
|
|
958
|
+
throw new DotSyntaxError("unterminated quoted string (missing closing quote)", line);
|
|
959
|
+
}
|
|
960
|
+
const c = text.charCodeAt(i);
|
|
961
|
+
if (c === 34) {
|
|
962
|
+
out += text.slice(segment, i);
|
|
963
|
+
this.pos = i + 1;
|
|
964
|
+
return { kind: "id", text: out, quoted: true, html: false, line };
|
|
965
|
+
}
|
|
966
|
+
if (c === 92) {
|
|
967
|
+
const d = text.charCodeAt(i + 1);
|
|
968
|
+
if (d === 34) {
|
|
969
|
+
out += `${text.slice(segment, i)}"`;
|
|
970
|
+
i += 2;
|
|
971
|
+
segment = i;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (d === 92) {
|
|
975
|
+
i += 2;
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (d === 10 || d === 13) {
|
|
979
|
+
out += text.slice(segment, i);
|
|
980
|
+
i += d === 13 && text.charCodeAt(i + 2) === 10 ? 3 : 2;
|
|
981
|
+
this.line++;
|
|
982
|
+
segment = i;
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
i++;
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
if (c === 10 || c === 13) {
|
|
989
|
+
if (c === 13 && text.charCodeAt(i + 1) === 10) {
|
|
990
|
+
i++;
|
|
991
|
+
}
|
|
992
|
+
this.line++;
|
|
993
|
+
}
|
|
994
|
+
i++;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Lex an HTML string starting at the opening angle bracket: the text up to the matching
|
|
999
|
+
* closing bracket, brackets balanced, kept verbatim including the outer pair.
|
|
1000
|
+
* @param start - the offset of the opening bracket
|
|
1001
|
+
* @param line - the line of the opening bracket
|
|
1002
|
+
* @returns the id token holding the whole `<...>` text
|
|
1003
|
+
*/
|
|
1004
|
+
html(start, line) {
|
|
1005
|
+
const { text } = this;
|
|
1006
|
+
let depth = 0;
|
|
1007
|
+
for (let i = start; i < text.length; i++) {
|
|
1008
|
+
const c = text.charCodeAt(i);
|
|
1009
|
+
if (c === 60) {
|
|
1010
|
+
depth++;
|
|
1011
|
+
} else if (c === 62) {
|
|
1012
|
+
depth--;
|
|
1013
|
+
if (depth === 0) {
|
|
1014
|
+
this.pos = i + 1;
|
|
1015
|
+
return { kind: "id", text: text.slice(start, i + 1), quoted: false, html: true, line };
|
|
1016
|
+
}
|
|
1017
|
+
} else if (c === 10 || c === 13 && text.charCodeAt(i + 1) !== 10) {
|
|
1018
|
+
this.line++;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
throw new DotSyntaxError("unterminated HTML string (missing closing '>')", line);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Skip whitespace and comments, counting lines.
|
|
1025
|
+
*/
|
|
1026
|
+
skipTrivia() {
|
|
1027
|
+
const { text } = this;
|
|
1028
|
+
for (; ; ) {
|
|
1029
|
+
if (this.pos >= text.length) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const c = text.charCodeAt(this.pos);
|
|
1033
|
+
if (c === 10) {
|
|
1034
|
+
this.line++;
|
|
1035
|
+
this.pos++;
|
|
1036
|
+
this.skipPreprocessorLine();
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (c === 13) {
|
|
1040
|
+
this.line++;
|
|
1041
|
+
this.pos += text.charCodeAt(this.pos + 1) === 10 ? 2 : 1;
|
|
1042
|
+
this.skipPreprocessorLine();
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (c === 32 || c === 9 || c === 11 || c === 12) {
|
|
1046
|
+
this.pos++;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (c === 47) {
|
|
1050
|
+
const d = text.charCodeAt(this.pos + 1);
|
|
1051
|
+
if (d === 47) {
|
|
1052
|
+
this.skipToLineEnd();
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (d === 42) {
|
|
1056
|
+
this.skipBlockComment();
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (c === 35 && this.pos === 0) {
|
|
1061
|
+
this.skipToLineEnd();
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Discard a `#` line when one starts at the current position (the start of a line).
|
|
1069
|
+
*/
|
|
1070
|
+
skipPreprocessorLine() {
|
|
1071
|
+
if (this.text.charCodeAt(this.pos) === 35) {
|
|
1072
|
+
this.skipToLineEnd();
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Advance to the next line break without consuming it.
|
|
1077
|
+
*/
|
|
1078
|
+
skipToLineEnd() {
|
|
1079
|
+
const { text } = this;
|
|
1080
|
+
while (this.pos < text.length) {
|
|
1081
|
+
const c = text.charCodeAt(this.pos);
|
|
1082
|
+
if (c === 10 || c === 13) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
this.pos++;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Skip a block comment starting at the current `/*`.
|
|
1090
|
+
*/
|
|
1091
|
+
skipBlockComment() {
|
|
1092
|
+
const { text } = this;
|
|
1093
|
+
const startLine = this.line;
|
|
1094
|
+
let i = this.pos + 2;
|
|
1095
|
+
for (; i < text.length; i++) {
|
|
1096
|
+
const c = text.charCodeAt(i);
|
|
1097
|
+
if (c === 42 && text.charCodeAt(i + 1) === 47) {
|
|
1098
|
+
this.pos = i + 2;
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (c === 10 || c === 13 && text.charCodeAt(i + 1) !== 10) {
|
|
1102
|
+
this.line++;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
throw new DotSyntaxError("unterminated block comment", startLine);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const DOT_ISSUE = Object.freeze({
|
|
1109
|
+
/** A grammar violation; fatal. */
|
|
1110
|
+
SYNTAX: SYNTAX_CODE,
|
|
1111
|
+
/** The input holds no graph at all (empty or only comments); fatal. */
|
|
1112
|
+
EMPTY_INPUT: EMPTY_INPUT_CODE,
|
|
1113
|
+
/** The input holds invalid UTF-8 (fatal). */
|
|
1114
|
+
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
1115
|
+
/** Subgraphs or braces nested deeper than the parser's limit; fatal. */
|
|
1116
|
+
NESTING: "E_DOT_NESTING",
|
|
1117
|
+
/** An edge operator contradicting the graph keyword (warning under "operator" / "header"). */
|
|
1118
|
+
EDGE_OPERATOR: "W_DOT_EDGE_OPERATOR",
|
|
1119
|
+
/** A second graph in the same input; only the first is read. */
|
|
1120
|
+
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
1121
|
+
/** A badly delimited numeral (`1e3`) split into two tokens, as Graphviz does with a warning. */
|
|
1122
|
+
NUMERAL_AMBIGUITY: "W_DOT_NUMERAL_AMBIGUITY",
|
|
1123
|
+
/** Attributes of a subgraph that is not a cluster (rank=same and the like) cannot be represented. */
|
|
1124
|
+
SUBGRAPH_ATTRIBUTES_DROPPED: "W_DOT_SUBGRAPH_ATTRIBUTES_DROPPED",
|
|
1125
|
+
/** A port on a node statement has no meaning and was dropped. */
|
|
1126
|
+
NODE_PORT_DROPPED: "W_DOT_NODE_PORT_DROPPED",
|
|
1127
|
+
/** A plain node and a cluster share a name and were merged into one container node. */
|
|
1128
|
+
CLUSTER_NODE_MERGED: "W_DOT_CLUSTER_NODE_MERGED",
|
|
1129
|
+
/** A node mentioned in two unrelated clusters keeps the first. */
|
|
1130
|
+
CLUSTER_CONFLICT: "W_DOT_CLUSTER_CONFLICT",
|
|
1131
|
+
/** A node `pos` that is not a point; the value was dropped. */
|
|
1132
|
+
BAD_POS: "W_DOT_BAD_POS",
|
|
1133
|
+
/** A parallel edge merged into an earlier one under `strict`. */
|
|
1134
|
+
STRICT_MERGED: "W_DOT_STRICT_MERGED",
|
|
1135
|
+
/** An edge merged into an earlier one with the same endpoints and `key`. */
|
|
1136
|
+
KEY_MERGED: "W_DOT_KEY_MERGED",
|
|
1137
|
+
/** A role (label, id, position, ...) was already taken in the caller's sink; the column was declared without it. */
|
|
1138
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
1139
|
+
/** A column of another shape exists in the caller's sink under a name the importer declares; renamed `<name>#<id>`. */
|
|
1140
|
+
COLUMN_RENAMED: COLUMN_RENAMED_CODE,
|
|
1141
|
+
/** A common option the format has no use for was given a non-default value. */
|
|
1142
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
1143
|
+
/** Two distinct id texts merged under ids: "number". */
|
|
1144
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
1145
|
+
/** A builder-policy option the sink does not honour. */
|
|
1146
|
+
SINK_OPTION: SINK_OPTION_CODE,
|
|
1147
|
+
/** The sink refused the file's direction. */
|
|
1148
|
+
DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
|
|
1149
|
+
/** Edges forced to the policy's direction. */
|
|
1150
|
+
DIRECTION_FORCED: DIRECTION_FORCED_CODE,
|
|
1151
|
+
/** A mixed file under onMixedDirection "error" (fatal). */
|
|
1152
|
+
MIXED_DIRECTION: MIXED_DIRECTION_CODE,
|
|
1153
|
+
/** A text column the sink could not widen to the dtype its cells imply. */
|
|
1154
|
+
WIDENING_UNSUPPORTED: WIDENING_UNSUPPORTED_CODE
|
|
1155
|
+
});
|
|
1156
|
+
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
1157
|
+
"ids",
|
|
1158
|
+
"addMissingNodes",
|
|
1159
|
+
"duplicateEdges",
|
|
1160
|
+
"selfLoops",
|
|
1161
|
+
"onMixedDirection",
|
|
1162
|
+
"weightFrom",
|
|
1163
|
+
"weightDtype",
|
|
1164
|
+
"errorLimit",
|
|
1165
|
+
"signal",
|
|
1166
|
+
"onProgress"
|
|
1167
|
+
]);
|
|
1168
|
+
const MAX_NESTING = 1024;
|
|
1169
|
+
const EXTENSIONS = Object.freeze([".dot", ".gv"]);
|
|
1170
|
+
const MIME_TYPES = Object.freeze(["text/vnd.graphviz"]);
|
|
1171
|
+
const CLUSTER_PREFIX = "cluster";
|
|
1172
|
+
const CLUSTER_ATTRIBUTE = "cluster";
|
|
1173
|
+
const STATEMENTS_PER_ABORT_CHECK = 64;
|
|
1174
|
+
const MAX_ANCESTOR_WALK = 4096;
|
|
1175
|
+
const DOT_HEADER = /^\s*(strict\s+)?(di)?graph\b/i;
|
|
1176
|
+
const TRUE_TEXTS = /* @__PURE__ */ new Set(["true", "yes", "1"]);
|
|
1177
|
+
const POINT_TEXT = /^\s*([-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)\s*,\s*([-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)(?:\s*,\s*([-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?\s*(!?)\s*$/;
|
|
1178
|
+
const dotImporter = Object.freeze({
|
|
1179
|
+
format: DOT_FORMAT,
|
|
1180
|
+
extensions: EXTENSIONS,
|
|
1181
|
+
mimeTypes: MIME_TYPES,
|
|
1182
|
+
/**
|
|
1183
|
+
* Confidence that the head of an input is DOT: the `[strict] graph | digraph` header after
|
|
1184
|
+
* optional comments.
|
|
1185
|
+
* @param head - the first bytes of the input
|
|
1186
|
+
* @returns 0.95 for a header followed by `{`, 0.8 for a header alone, 0 otherwise
|
|
1187
|
+
*/
|
|
1188
|
+
sniff(head) {
|
|
1189
|
+
const text = stripLeadingComments(new TextDecoder("utf-8").decode(head));
|
|
1190
|
+
const match = DOT_HEADER.exec(text);
|
|
1191
|
+
if (match === null) {
|
|
1192
|
+
return 0;
|
|
1193
|
+
}
|
|
1194
|
+
return text.includes("{") ? 0.95 : 0.8;
|
|
1195
|
+
},
|
|
1196
|
+
/**
|
|
1197
|
+
* Read a DOT document into the sink.
|
|
1198
|
+
* @param input - the text, bytes or stream
|
|
1199
|
+
* @param sink - the sink to push into
|
|
1200
|
+
* @param options - format-specific and common options
|
|
1201
|
+
* @returns the import report; ImportError (E_IMPORT) on a syntax error or beyond the error limit
|
|
1202
|
+
*/
|
|
1203
|
+
async import(input, sink, options) {
|
|
1204
|
+
const resolved = resolveImportOptions(options, {
|
|
1205
|
+
ids: "canonical",
|
|
1206
|
+
defaultDirected: true,
|
|
1207
|
+
weightFrom: "weight"
|
|
1208
|
+
});
|
|
1209
|
+
const mismatch = mismatchOption(options?.mismatchedEdgeOperator);
|
|
1210
|
+
const report = new ImportReportBuilder(DOT_FORMAT, resolved.errorLimit);
|
|
1211
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
1212
|
+
reportSinkOptions(sink, options, report);
|
|
1213
|
+
const text = await readText(input, report, resolved);
|
|
1214
|
+
const parser = new DotParser(text, sink, report, resolved, mismatch);
|
|
1215
|
+
try {
|
|
1216
|
+
parser.parse();
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
if (err instanceof DotSyntaxError) {
|
|
1219
|
+
report.fail(SYNTAX_CODE, err.message, { line: err.line }, { line: err.line });
|
|
1220
|
+
}
|
|
1221
|
+
throw err;
|
|
1222
|
+
}
|
|
1223
|
+
throwIfAborted(resolved.signal);
|
|
1224
|
+
return report.finish();
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
function mismatchOption(value) {
|
|
1228
|
+
if (value === void 0) {
|
|
1229
|
+
return "operator";
|
|
1230
|
+
}
|
|
1231
|
+
if (value === "operator" || value === "header" || value === "error") {
|
|
1232
|
+
return value;
|
|
1233
|
+
}
|
|
1234
|
+
throw new GraphFormatError(
|
|
1235
|
+
"E_UNSUPPORTED",
|
|
1236
|
+
`option mismatchedEdgeOperator: ${JSON.stringify(value)} is not one of "operator", "header", "error"`,
|
|
1237
|
+
{ option: "mismatchedEdgeOperator", found: value, supported: ["operator", "header", "error"] }
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
function stripLeadingComments(text) {
|
|
1241
|
+
let rest = text;
|
|
1242
|
+
for (; ; ) {
|
|
1243
|
+
const trimmed = rest.replace(/^\s+/, "");
|
|
1244
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("#")) {
|
|
1245
|
+
const nl = trimmed.search(/[\r\n]/);
|
|
1246
|
+
if (nl < 0) {
|
|
1247
|
+
return "";
|
|
1248
|
+
}
|
|
1249
|
+
rest = trimmed.slice(nl);
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
if (trimmed.startsWith("/*")) {
|
|
1253
|
+
const end = trimmed.indexOf("*/");
|
|
1254
|
+
if (end < 0) {
|
|
1255
|
+
return "";
|
|
1256
|
+
}
|
|
1257
|
+
rest = trimmed.slice(end + 2);
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
return trimmed;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function isKeyword(token, keyword) {
|
|
1264
|
+
return token.kind === "id" && !token.quoted && !token.html && token.text.toLowerCase() === keyword;
|
|
1265
|
+
}
|
|
1266
|
+
function isPunct(token, text) {
|
|
1267
|
+
return token.kind === "punct" && token.text === text;
|
|
1268
|
+
}
|
|
1269
|
+
function isEdgeOp(token) {
|
|
1270
|
+
return token.kind === "punct" && (token.text === "->" || token.text === "--");
|
|
1271
|
+
}
|
|
1272
|
+
function describeToken(token) {
|
|
1273
|
+
if (token.kind === "eof") {
|
|
1274
|
+
return "end of input";
|
|
1275
|
+
}
|
|
1276
|
+
return JSON.stringify(token.text.length > 40 ? `${token.text.slice(0, 40)}...` : token.text);
|
|
1277
|
+
}
|
|
1278
|
+
function isClusterName(name) {
|
|
1279
|
+
return name !== null && name.startsWith(CLUSTER_PREFIX);
|
|
1280
|
+
}
|
|
1281
|
+
class DotParser {
|
|
1282
|
+
/**
|
|
1283
|
+
* Create a parser over one document.
|
|
1284
|
+
* @param text - the DOT text
|
|
1285
|
+
* @param sink - the sink
|
|
1286
|
+
* @param report - the report
|
|
1287
|
+
* @param options - the resolved common options
|
|
1288
|
+
* @param mismatch - the resolved mismatchedEdgeOperator option
|
|
1289
|
+
*/
|
|
1290
|
+
constructor(text, sink, report, options, mismatch) {
|
|
1291
|
+
this.directed = true;
|
|
1292
|
+
this.strict = false;
|
|
1293
|
+
this.statements = 0;
|
|
1294
|
+
this.idOf = /* @__PURE__ */ new Map();
|
|
1295
|
+
this.containers = /* @__PURE__ */ new Set();
|
|
1296
|
+
this.parentOf = /* @__PURE__ */ new Map();
|
|
1297
|
+
this.conflictWarned = /* @__PURE__ */ new Set();
|
|
1298
|
+
this.strictEdges = /* @__PURE__ */ new Map();
|
|
1299
|
+
this.keyedEdges = /* @__PURE__ */ new Map();
|
|
1300
|
+
this.nodeLabelHandle = INVALID_INDEX;
|
|
1301
|
+
this.edgeLabelHandle = INVALID_INDEX;
|
|
1302
|
+
this.positionHandle = INVALID_INDEX;
|
|
1303
|
+
this.clusterHandle = INVALID_INDEX;
|
|
1304
|
+
this.parentHandle = INVALID_INDEX;
|
|
1305
|
+
this.keyHandle = INVALID_INDEX;
|
|
1306
|
+
this.sourcePortHandle = INVALID_INDEX;
|
|
1307
|
+
this.targetPortHandle = INVALID_INDEX;
|
|
1308
|
+
this.depth = 0;
|
|
1309
|
+
this.nodeWriters = /* @__PURE__ */ new Map();
|
|
1310
|
+
this.edgeWriters = /* @__PURE__ */ new Map();
|
|
1311
|
+
this.lexer = new DotTokenizer(text, (numeral, line) => {
|
|
1312
|
+
report.warning(
|
|
1313
|
+
"validation-error",
|
|
1314
|
+
DOT_ISSUE.NUMERAL_AMBIGUITY,
|
|
1315
|
+
`badly delimited number ${JSON.stringify(numeral)} splits into two tokens (Graphviz warns the same)`,
|
|
1316
|
+
{ line, element: numeral }
|
|
1317
|
+
);
|
|
1318
|
+
});
|
|
1319
|
+
this.sink = sink;
|
|
1320
|
+
this.report = report;
|
|
1321
|
+
this.options = options;
|
|
1322
|
+
this.mismatch = mismatch;
|
|
1323
|
+
this.ids = new IdCoercer(options.ids);
|
|
1324
|
+
this.resolver = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Parse the whole document: `[strict] (graph | digraph) [ID] { stmt_list }`.
|
|
1328
|
+
*/
|
|
1329
|
+
parse() {
|
|
1330
|
+
const { lexer } = this;
|
|
1331
|
+
let token = lexer.next();
|
|
1332
|
+
if (token.kind === "eof") {
|
|
1333
|
+
this.report.fail(EMPTY_INPUT_CODE, "the input holds no graph (empty or only comments)", {
|
|
1334
|
+
line: token.line
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
if (isKeyword(token, "strict")) {
|
|
1338
|
+
this.strict = true;
|
|
1339
|
+
token = lexer.next();
|
|
1340
|
+
}
|
|
1341
|
+
if (isKeyword(token, "digraph")) {
|
|
1342
|
+
this.directed = true;
|
|
1343
|
+
} else if (isKeyword(token, "graph")) {
|
|
1344
|
+
this.directed = false;
|
|
1345
|
+
} else {
|
|
1346
|
+
throw new DotSyntaxError(`expected "graph" or "digraph", found ${describeToken(token)}`, token.line);
|
|
1347
|
+
}
|
|
1348
|
+
const headerLine = token.line;
|
|
1349
|
+
token = lexer.next();
|
|
1350
|
+
let name = null;
|
|
1351
|
+
if (token.kind === "id") {
|
|
1352
|
+
name = token.text;
|
|
1353
|
+
token = lexer.next();
|
|
1354
|
+
}
|
|
1355
|
+
if (!isPunct(token, "{")) {
|
|
1356
|
+
throw new DotSyntaxError(`expected "{" after the graph header, found ${describeToken(token)}`, token.line);
|
|
1357
|
+
}
|
|
1358
|
+
this.resolver.setHeader(this.directed, { line: headerLine });
|
|
1359
|
+
this.sink.setMeta({
|
|
1360
|
+
name,
|
|
1361
|
+
sourceFormat: DOT_FORMAT,
|
|
1362
|
+
...this.strict ? { extra: { dot: { strict: true } } } : {}
|
|
1363
|
+
});
|
|
1364
|
+
const root = this.newScope(null, headerLine, true, null);
|
|
1365
|
+
this.statementList(root);
|
|
1366
|
+
const trailing = lexer.next();
|
|
1367
|
+
if (trailing.kind !== "eof") {
|
|
1368
|
+
this.report.warning(
|
|
1369
|
+
"unsupported",
|
|
1370
|
+
MULTIPLE_GRAPHS_CODE,
|
|
1371
|
+
`content after the closing brace of the graph (${describeToken(trailing)}) was not read; one graph per input`,
|
|
1372
|
+
{ line: trailing.line }
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Create a scope.
|
|
1378
|
+
* @param parent - the enclosing scope, or null for the root
|
|
1379
|
+
* @param line - the line the scope opens on
|
|
1380
|
+
* @param root - whether this is the root graph
|
|
1381
|
+
* @param name - the subgraph name, or null
|
|
1382
|
+
* @returns the scope
|
|
1383
|
+
*/
|
|
1384
|
+
newScope(parent, line, root, name) {
|
|
1385
|
+
return {
|
|
1386
|
+
root,
|
|
1387
|
+
name,
|
|
1388
|
+
line,
|
|
1389
|
+
nodeDefaults: new Map(parent?.nodeDefaults),
|
|
1390
|
+
edgeDefaults: new Map(parent?.edgeDefaults),
|
|
1391
|
+
members: [],
|
|
1392
|
+
memberSet: /* @__PURE__ */ new Set(),
|
|
1393
|
+
containers: [],
|
|
1394
|
+
attributes: /* @__PURE__ */ new Map(),
|
|
1395
|
+
cluster: false,
|
|
1396
|
+
container: INVALID_INDEX
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Parse `stmt_list }` for a scope whose `{` was consumed.
|
|
1401
|
+
* @param scope - the scope
|
|
1402
|
+
*/
|
|
1403
|
+
statementList(scope) {
|
|
1404
|
+
const { lexer } = this;
|
|
1405
|
+
for (; ; ) {
|
|
1406
|
+
const token = lexer.peek();
|
|
1407
|
+
if (isPunct(token, "}")) {
|
|
1408
|
+
lexer.next();
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (token.kind === "eof") {
|
|
1412
|
+
throw new DotSyntaxError(
|
|
1413
|
+
scope.root ? 'unexpected end of input: missing "}" closing the graph' : `unexpected end of input: missing "}" closing the subgraph opened on line ${scope.line}`,
|
|
1414
|
+
token.line
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
if (isPunct(token, ";")) {
|
|
1418
|
+
lexer.next();
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
this.statement(scope);
|
|
1422
|
+
if (++this.statements % STATEMENTS_PER_ABORT_CHECK === 0) {
|
|
1423
|
+
throwIfAborted(this.options.signal);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Parse one statement of a scope.
|
|
1429
|
+
* @param scope - the scope
|
|
1430
|
+
*/
|
|
1431
|
+
statement(scope) {
|
|
1432
|
+
const { lexer } = this;
|
|
1433
|
+
const token = lexer.peek();
|
|
1434
|
+
if (token.kind === "punct") {
|
|
1435
|
+
if (token.text === "{") {
|
|
1436
|
+
const group = this.subgraph(scope);
|
|
1437
|
+
this.maybeEdgeStatement(scope, group, token.line);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
throw new DotSyntaxError(`unexpected ${describeToken(token)} at the start of a statement`, token.line);
|
|
1441
|
+
}
|
|
1442
|
+
if (!token.quoted && !token.html) {
|
|
1443
|
+
const keyword = token.text.toLowerCase();
|
|
1444
|
+
if (keyword === "node" || keyword === "edge" || keyword === "graph") {
|
|
1445
|
+
lexer.next();
|
|
1446
|
+
this.attributeStatement(scope, keyword, token.line);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (keyword === "subgraph") {
|
|
1450
|
+
const group = this.subgraph(scope);
|
|
1451
|
+
this.maybeEdgeStatement(scope, group, token.line);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (keyword === "digraph" || keyword === "strict") {
|
|
1455
|
+
throw new DotSyntaxError(`unexpected keyword ${describeToken(token)} inside a graph`, token.line);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const id = this.identifier();
|
|
1459
|
+
if (isPunct(lexer.peek(), "=")) {
|
|
1460
|
+
lexer.next();
|
|
1461
|
+
const value = this.identifier();
|
|
1462
|
+
this.scopeAttribute(scope, { name: id.text, value: value.text, line: id.line });
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const port = this.port();
|
|
1466
|
+
if (isEdgeOp(lexer.peek())) {
|
|
1467
|
+
const endpoint = this.mentionEndpoint(scope, id, port);
|
|
1468
|
+
this.edgeStatement(scope, endpoint === null ? [] : [endpoint], id.line);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
this.nodeStatement(scope, id, port);
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Read an ID, concatenating quoted strings joined by `+`.
|
|
1475
|
+
* @returns the id token (the concatenation keeps the first token's line and quoted flag)
|
|
1476
|
+
*/
|
|
1477
|
+
identifier() {
|
|
1478
|
+
const { lexer } = this;
|
|
1479
|
+
const token = lexer.next();
|
|
1480
|
+
if (token.kind !== "id") {
|
|
1481
|
+
throw new DotSyntaxError(`expected an identifier, found ${describeToken(token)}`, token.line);
|
|
1482
|
+
}
|
|
1483
|
+
if (!token.quoted || !isPunct(lexer.peek(), "+")) {
|
|
1484
|
+
return token;
|
|
1485
|
+
}
|
|
1486
|
+
let { text } = token;
|
|
1487
|
+
while (isPunct(lexer.peek(), "+")) {
|
|
1488
|
+
const plus = lexer.next();
|
|
1489
|
+
const more = lexer.next();
|
|
1490
|
+
if (more.kind !== "id" || !more.quoted) {
|
|
1491
|
+
throw new DotSyntaxError(
|
|
1492
|
+
`expected a quoted string after "+", found ${describeToken(more)}`,
|
|
1493
|
+
more.kind === "eof" ? plus.line : more.line
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
text += more.text;
|
|
1497
|
+
}
|
|
1498
|
+
return { kind: "id", text, quoted: true, html: false, line: token.line };
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Read an optional port after a node id: `: ID [ : compass ]` or `: compass`.
|
|
1502
|
+
* @returns the port text (`f0`, `f0:n`, `n`), or null when there is none
|
|
1503
|
+
*/
|
|
1504
|
+
port() {
|
|
1505
|
+
const { lexer } = this;
|
|
1506
|
+
if (!isPunct(lexer.peek(), ":")) {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
lexer.next();
|
|
1510
|
+
let { text } = this.identifier();
|
|
1511
|
+
if (isPunct(lexer.peek(), ":")) {
|
|
1512
|
+
lexer.next();
|
|
1513
|
+
text += `:${this.identifier().text}`;
|
|
1514
|
+
}
|
|
1515
|
+
return text;
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Parse `[subgraph [ID]] { stmt_list }` and return its member nodes for use as an edge endpoint.
|
|
1519
|
+
* @param parent - the enclosing scope
|
|
1520
|
+
* @returns the endpoints (every node mentioned in the subgraph, in first-mention order)
|
|
1521
|
+
*/
|
|
1522
|
+
subgraph(parent) {
|
|
1523
|
+
const { lexer } = this;
|
|
1524
|
+
let token = lexer.next();
|
|
1525
|
+
let name = null;
|
|
1526
|
+
if (isKeyword(token, "subgraph")) {
|
|
1527
|
+
token = lexer.next();
|
|
1528
|
+
if (token.kind === "id") {
|
|
1529
|
+
name = token.text;
|
|
1530
|
+
token = lexer.next();
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (!isPunct(token, "{")) {
|
|
1534
|
+
throw new DotSyntaxError(`expected "{" to open a subgraph, found ${describeToken(token)}`, token.line);
|
|
1535
|
+
}
|
|
1536
|
+
if (++this.depth > MAX_NESTING) {
|
|
1537
|
+
this.report.fail(
|
|
1538
|
+
DOT_ISSUE.NESTING,
|
|
1539
|
+
`subgraphs nested deeper than ${MAX_NESTING} levels (the parser recurses per level)`,
|
|
1540
|
+
{ line: token.line }
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
const scope = this.newScope(parent, token.line, false, name);
|
|
1544
|
+
if (isClusterName(name)) {
|
|
1545
|
+
scope.cluster = true;
|
|
1546
|
+
this.ensureContainer(scope);
|
|
1547
|
+
}
|
|
1548
|
+
this.statementList(scope);
|
|
1549
|
+
this.depth--;
|
|
1550
|
+
this.closeScope(scope, parent);
|
|
1551
|
+
return scope.members.map((index) => ({ id: this.idOf.get(index) ?? index, index, port: null }));
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Finish a subgraph: apply its attributes to the container node when it is a cluster (or report
|
|
1555
|
+
* them dropped), assign parents to its members, and propagate members and containers upward.
|
|
1556
|
+
* @param scope - the closed scope
|
|
1557
|
+
* @param parent - the enclosing scope
|
|
1558
|
+
*/
|
|
1559
|
+
closeScope(scope, parent) {
|
|
1560
|
+
const { container } = scope;
|
|
1561
|
+
if (container !== INVALID_INDEX) {
|
|
1562
|
+
for (const attribute of scope.attributes.values()) {
|
|
1563
|
+
if (attribute.name !== CLUSTER_ATTRIBUTE) {
|
|
1564
|
+
this.setNodeAttribute(container, attribute.name, attribute.value, attribute.line);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
for (const nested of scope.containers) {
|
|
1568
|
+
if (nested !== container && !this.parentOf.has(nested)) {
|
|
1569
|
+
this.setParent(nested, container);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
for (const member of scope.members) {
|
|
1573
|
+
if (member === container) {
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
const existing = this.parentOf.get(member);
|
|
1577
|
+
if (existing === void 0) {
|
|
1578
|
+
this.setParent(member, container);
|
|
1579
|
+
} else if (existing !== container && !this.isAncestor(container, existing)) {
|
|
1580
|
+
this.clusterConflict(member, existing, container, scope.line);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
parent.containers.push(container);
|
|
1584
|
+
} else {
|
|
1585
|
+
if (scope.attributes.size > 0) {
|
|
1586
|
+
const names = [...scope.attributes.keys()].join(", ");
|
|
1587
|
+
this.report.warning(
|
|
1588
|
+
"unsupported",
|
|
1589
|
+
DOT_ISSUE.SUBGRAPH_ATTRIBUTES_DROPPED,
|
|
1590
|
+
`subgraph ${scope.name === null ? "(anonymous)" : JSON.stringify(scope.name)} is not a cluster; its attributes (${names}) cannot be represented and were dropped`,
|
|
1591
|
+
{ line: scope.line, element: scope.name }
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
for (const nested of scope.containers) {
|
|
1595
|
+
parent.containers.push(nested);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (!parent.root) {
|
|
1599
|
+
for (const member of scope.members) {
|
|
1600
|
+
if (!parent.memberSet.has(member)) {
|
|
1601
|
+
parent.memberSet.add(member);
|
|
1602
|
+
parent.members.push(member);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Whether `candidate` is an ancestor of `node` through the parents assigned so far.
|
|
1609
|
+
* @param candidate - the possible ancestor
|
|
1610
|
+
* @param node - the node whose chain is walked
|
|
1611
|
+
* @returns true when candidate is reached
|
|
1612
|
+
*/
|
|
1613
|
+
isAncestor(candidate, node) {
|
|
1614
|
+
let current = node;
|
|
1615
|
+
for (let steps = 0; current !== void 0 && steps < MAX_ANCESTOR_WALK; steps++) {
|
|
1616
|
+
if (current === candidate) {
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
current = this.parentOf.get(current);
|
|
1620
|
+
}
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Record a node mentioned in two unrelated clusters (once per node).
|
|
1625
|
+
* @param member - the node
|
|
1626
|
+
* @param existing - its parent
|
|
1627
|
+
* @param container - the cluster it was also mentioned in
|
|
1628
|
+
* @param line - the line of the losing cluster
|
|
1629
|
+
*/
|
|
1630
|
+
clusterConflict(member, existing, container, line) {
|
|
1631
|
+
if (this.conflictWarned.has(member)) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
this.conflictWarned.add(member);
|
|
1635
|
+
const id = this.idOf.get(member) ?? member;
|
|
1636
|
+
this.report.warning(
|
|
1637
|
+
"coercion",
|
|
1638
|
+
DOT_ISSUE.CLUSTER_CONFLICT,
|
|
1639
|
+
`node ${JSON.stringify(id)} is in cluster ${JSON.stringify(this.idOf.get(existing) ?? existing)} and in cluster ${JSON.stringify(this.idOf.get(container) ?? container)}; the first is kept`,
|
|
1640
|
+
{ line, element: String(id) }
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Assign a parent.
|
|
1645
|
+
* @param node - the node index
|
|
1646
|
+
* @param container - the container index
|
|
1647
|
+
*/
|
|
1648
|
+
setParent(node, container) {
|
|
1649
|
+
if (node === container) {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
try {
|
|
1653
|
+
this.sink.setNodeValue(this.parentColumn(), node, container);
|
|
1654
|
+
this.parentOf.set(node, container);
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
this.report.recordError(err, { element: String(this.idOf.get(node) ?? node) });
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Create (or adopt) the container node of a cluster scope.
|
|
1661
|
+
* @param scope - the cluster scope
|
|
1662
|
+
*/
|
|
1663
|
+
ensureContainer(scope) {
|
|
1664
|
+
if (scope.container !== INVALID_INDEX || scope.name === null) {
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
const id = this.coerceId(scope.name, scope.line);
|
|
1668
|
+
if (id === null) {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
let index = this.sink.indexOf(id);
|
|
1672
|
+
try {
|
|
1673
|
+
if (index === INVALID_INDEX) {
|
|
1674
|
+
index = this.sink.addNode(id);
|
|
1675
|
+
this.report.counts.nodes++;
|
|
1676
|
+
} else if (!this.containers.has(index)) {
|
|
1677
|
+
this.report.warning(
|
|
1678
|
+
"coercion",
|
|
1679
|
+
DOT_ISSUE.CLUSTER_NODE_MERGED,
|
|
1680
|
+
`cluster ${JSON.stringify(scope.name)} and the node of the same name were merged into one container node`,
|
|
1681
|
+
{ line: scope.line, element: scope.name }
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
this.idOf.set(index, id);
|
|
1685
|
+
if (!this.containers.has(index)) {
|
|
1686
|
+
this.containers.add(index);
|
|
1687
|
+
this.sink.setNodeValue(this.clusterColumn(), index, true);
|
|
1688
|
+
}
|
|
1689
|
+
scope.container = index;
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
this.report.recordError(err, { line: scope.line, element: scope.name });
|
|
1692
|
+
if (index === INVALID_INDEX) {
|
|
1693
|
+
this.report.counts.skippedNodes++;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Apply an `ID = ID` or `graph [..]` attribute: the graph table at the root, a buffered
|
|
1699
|
+
* subgraph attribute otherwise (`cluster=true` turns the subgraph into a cluster).
|
|
1700
|
+
* @param scope - the scope
|
|
1701
|
+
* @param attribute - the attribute
|
|
1702
|
+
*/
|
|
1703
|
+
scopeAttribute(scope, attribute) {
|
|
1704
|
+
if (scope.root) {
|
|
1705
|
+
this.setGraphAttribute(attribute);
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
scope.attributes.set(attribute.name, attribute);
|
|
1709
|
+
if (attribute.name === CLUSTER_ATTRIBUTE && TRUE_TEXTS.has(attribute.value.trim().toLowerCase())) {
|
|
1710
|
+
scope.cluster = true;
|
|
1711
|
+
this.ensureContainer(scope);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Parse `node|edge|graph attr_list` after the keyword.
|
|
1716
|
+
* @param scope - the scope
|
|
1717
|
+
* @param keyword - which defaults are set
|
|
1718
|
+
* @param line - the keyword's line
|
|
1719
|
+
*/
|
|
1720
|
+
attributeStatement(scope, keyword, line) {
|
|
1721
|
+
if (!isPunct(this.lexer.peek(), "[")) {
|
|
1722
|
+
const found = this.lexer.peek();
|
|
1723
|
+
throw new DotSyntaxError(`expected "[" after "${keyword}", found ${describeToken(found)}`, found.line);
|
|
1724
|
+
}
|
|
1725
|
+
const attributes = this.attributeList();
|
|
1726
|
+
switch (keyword) {
|
|
1727
|
+
case "node":
|
|
1728
|
+
for (const a of attributes) {
|
|
1729
|
+
scope.nodeDefaults.set(a.name, a.value);
|
|
1730
|
+
}
|
|
1731
|
+
break;
|
|
1732
|
+
case "edge":
|
|
1733
|
+
for (const a of attributes) {
|
|
1734
|
+
scope.edgeDefaults.set(a.name, a.value);
|
|
1735
|
+
}
|
|
1736
|
+
break;
|
|
1737
|
+
case "graph":
|
|
1738
|
+
for (const a of attributes) {
|
|
1739
|
+
this.scopeAttribute(scope, a);
|
|
1740
|
+
}
|
|
1741
|
+
break;
|
|
1742
|
+
default:
|
|
1743
|
+
throw new DotSyntaxError(`unknown attribute statement "${keyword}"`, line);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Parse one or more `[ a_list ]` groups.
|
|
1748
|
+
* @returns the attributes in order (a repeated name keeps its last value at application time)
|
|
1749
|
+
*/
|
|
1750
|
+
attributeList() {
|
|
1751
|
+
const { lexer } = this;
|
|
1752
|
+
const out = [];
|
|
1753
|
+
while (isPunct(lexer.peek(), "[")) {
|
|
1754
|
+
lexer.next();
|
|
1755
|
+
for (; ; ) {
|
|
1756
|
+
const token = lexer.peek();
|
|
1757
|
+
if (isPunct(token, "]")) {
|
|
1758
|
+
lexer.next();
|
|
1759
|
+
break;
|
|
1760
|
+
}
|
|
1761
|
+
if (isPunct(token, ";") || isPunct(token, ",")) {
|
|
1762
|
+
lexer.next();
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
const name = this.identifier();
|
|
1766
|
+
const eq = lexer.next();
|
|
1767
|
+
if (!isPunct(eq, "=")) {
|
|
1768
|
+
throw new DotSyntaxError(
|
|
1769
|
+
`expected "=" after attribute name ${JSON.stringify(name.text)}, found ${describeToken(eq)}`,
|
|
1770
|
+
eq.line
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
const value = this.identifier();
|
|
1774
|
+
out.push({ name: name.text, value: value.text, line: name.line });
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return out;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Parse a node statement after its id and port: an optional attribute list, then apply.
|
|
1781
|
+
* @param scope - the scope
|
|
1782
|
+
* @param id - the id token
|
|
1783
|
+
* @param port - the port, if any (dropped with a warning)
|
|
1784
|
+
*/
|
|
1785
|
+
nodeStatement(scope, id, port) {
|
|
1786
|
+
const attributes = this.attributeList();
|
|
1787
|
+
const endpoint = this.mentionEndpoint(scope, id, null);
|
|
1788
|
+
if (endpoint === null) {
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (port !== null) {
|
|
1792
|
+
this.report.warning(
|
|
1793
|
+
"unsupported",
|
|
1794
|
+
DOT_ISSUE.NODE_PORT_DROPPED,
|
|
1795
|
+
`port ${JSON.stringify(port)} on the node statement of ${JSON.stringify(id.text)} has no meaning and was dropped`,
|
|
1796
|
+
{ line: id.line, element: id.text }
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
for (const attribute of attributes) {
|
|
1800
|
+
this.setNodeAttribute(endpoint.index, attribute.name, attribute.value, attribute.line);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* After a subgraph statement: continue as an edge statement when an edge operator follows.
|
|
1805
|
+
* @param scope - the scope
|
|
1806
|
+
* @param group - the subgraph's members
|
|
1807
|
+
* @param line - the statement's line
|
|
1808
|
+
*/
|
|
1809
|
+
maybeEdgeStatement(scope, group, line) {
|
|
1810
|
+
if (isEdgeOp(this.lexer.peek())) {
|
|
1811
|
+
this.edgeStatement(scope, group, line);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Parse `edgeRHS [attr_list]` after the first endpoint group and push the edges.
|
|
1816
|
+
* @param scope - the scope
|
|
1817
|
+
* @param first - the first group (empty when its node could not be created)
|
|
1818
|
+
* @param line - the statement's line
|
|
1819
|
+
*/
|
|
1820
|
+
edgeStatement(scope, first, line) {
|
|
1821
|
+
const { lexer } = this;
|
|
1822
|
+
const groups = [first];
|
|
1823
|
+
const kinds = [];
|
|
1824
|
+
while (isEdgeOp(lexer.peek())) {
|
|
1825
|
+
const op = lexer.next();
|
|
1826
|
+
kinds.push(this.edgeKind(op));
|
|
1827
|
+
const next = lexer.peek();
|
|
1828
|
+
if (isPunct(next, "{") || isKeyword(next, "subgraph")) {
|
|
1829
|
+
groups.push(this.subgraph(scope));
|
|
1830
|
+
} else {
|
|
1831
|
+
const id = this.identifier();
|
|
1832
|
+
const port = this.port();
|
|
1833
|
+
const endpoint = this.mentionEndpoint(scope, id, port);
|
|
1834
|
+
groups.push(endpoint === null ? [] : [endpoint]);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const attributes = this.attributeList();
|
|
1838
|
+
for (let k = 0; k < kinds.length; k++) {
|
|
1839
|
+
for (const source of groups[k]) {
|
|
1840
|
+
for (const target of groups[k + 1]) {
|
|
1841
|
+
this.pushEdge(scope, source, target, kinds[k], attributes, line);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* The direction of an edge from its operator, checked against the graph keyword.
|
|
1848
|
+
* @param op - the operator token
|
|
1849
|
+
* @returns the edge kind
|
|
1850
|
+
*/
|
|
1851
|
+
edgeKind(op) {
|
|
1852
|
+
const operatorDirected = op.text === "->";
|
|
1853
|
+
if (operatorDirected === this.directed) {
|
|
1854
|
+
return operatorDirected ? "directed" : "undirected";
|
|
1855
|
+
}
|
|
1856
|
+
const message = `edge operator "${op.text}" in a ${this.directed ? "digraph" : "graph"}`;
|
|
1857
|
+
switch (this.mismatch) {
|
|
1858
|
+
case "error":
|
|
1859
|
+
throw new DotSyntaxError(`${message} (mismatchedEdgeOperator: "error")`, op.line);
|
|
1860
|
+
case "header":
|
|
1861
|
+
this.report.warnOnce(
|
|
1862
|
+
"coercion",
|
|
1863
|
+
DOT_ISSUE.EDGE_OPERATOR,
|
|
1864
|
+
`${message}; read with the graph's direction (mismatchedEdgeOperator: "header")`,
|
|
1865
|
+
{ line: op.line }
|
|
1866
|
+
);
|
|
1867
|
+
return this.directed ? "directed" : "undirected";
|
|
1868
|
+
case "operator":
|
|
1869
|
+
this.report.warnOnce(
|
|
1870
|
+
"coercion",
|
|
1871
|
+
DOT_ISSUE.EDGE_OPERATOR,
|
|
1872
|
+
`${message}; read with the operator's direction and resolved per onMixedDirection`,
|
|
1873
|
+
{ line: op.line }
|
|
1874
|
+
);
|
|
1875
|
+
return operatorDirected ? "directed" : "undirected";
|
|
1876
|
+
default: {
|
|
1877
|
+
const name = this.mismatch;
|
|
1878
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown mismatchedEdgeOperator ${name}`, { found: name });
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Coerce an id text, recording a merge under ids: "number".
|
|
1884
|
+
* @param text - the id text
|
|
1885
|
+
* @param line - the line, for issues
|
|
1886
|
+
* @returns the id, or null when the text was rejected (recorded, node skipped)
|
|
1887
|
+
*/
|
|
1888
|
+
coerceId(text, line) {
|
|
1889
|
+
try {
|
|
1890
|
+
const id = this.ids.text(text);
|
|
1891
|
+
const merge = this.ids.lastMerge;
|
|
1892
|
+
if (merge !== null) {
|
|
1893
|
+
this.report.warning(
|
|
1894
|
+
"coercion",
|
|
1895
|
+
DOT_ISSUE.ID_MERGED,
|
|
1896
|
+
`id text ${JSON.stringify(merge.text)} merged with ${JSON.stringify(merge.previousText)} as ${merge.id} under ids: "number"`,
|
|
1897
|
+
{ line, element: text }
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
return id;
|
|
1901
|
+
} catch (err) {
|
|
1902
|
+
this.report.recordError(err, { line, element: text });
|
|
1903
|
+
this.report.counts.skippedNodes++;
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Mention a node: create it on first mention (applying the scope's node defaults), record it as
|
|
1909
|
+
* a member of the scope, and return it as an endpoint.
|
|
1910
|
+
* @param scope - the scope
|
|
1911
|
+
* @param id - the id token
|
|
1912
|
+
* @param port - the endpoint's port, or null
|
|
1913
|
+
* @returns the endpoint, or null when the node could not be created (recorded)
|
|
1914
|
+
*/
|
|
1915
|
+
mentionEndpoint(scope, id, port) {
|
|
1916
|
+
const nodeId = this.coerceId(id.text, id.line);
|
|
1917
|
+
if (nodeId === null) {
|
|
1918
|
+
return null;
|
|
1919
|
+
}
|
|
1920
|
+
const { sink } = this;
|
|
1921
|
+
let index = sink.indexOf(nodeId);
|
|
1922
|
+
if (index === INVALID_INDEX) {
|
|
1923
|
+
try {
|
|
1924
|
+
index = sink.addNode(nodeId);
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
this.report.recordError(err, { line: id.line, element: id.text });
|
|
1927
|
+
this.report.counts.skippedNodes++;
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
this.report.counts.nodes++;
|
|
1931
|
+
this.idOf.set(index, nodeId);
|
|
1932
|
+
for (const [name, value] of scope.nodeDefaults) {
|
|
1933
|
+
this.setNodeAttribute(index, name, value, id.line);
|
|
1934
|
+
}
|
|
1935
|
+
} else if (!this.idOf.has(index)) {
|
|
1936
|
+
this.idOf.set(index, nodeId);
|
|
1937
|
+
}
|
|
1938
|
+
if (!scope.root && !scope.memberSet.has(index)) {
|
|
1939
|
+
scope.memberSet.add(index);
|
|
1940
|
+
scope.members.push(index);
|
|
1941
|
+
}
|
|
1942
|
+
return { id: nodeId, index, port };
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Push one edge with its effective attributes (scope defaults overridden by the statement's).
|
|
1946
|
+
* @param scope - the scope
|
|
1947
|
+
* @param source - the source endpoint
|
|
1948
|
+
* @param target - the target endpoint
|
|
1949
|
+
* @param kind - the edge's direction
|
|
1950
|
+
* @param attributes - the statement's attributes
|
|
1951
|
+
* @param line - the statement's line
|
|
1952
|
+
*/
|
|
1953
|
+
pushEdge(scope, source, target, kind, attributes, line) {
|
|
1954
|
+
const effective = new Map(scope.edgeDefaults);
|
|
1955
|
+
for (const a of attributes) {
|
|
1956
|
+
effective.set(a.name, a.value);
|
|
1957
|
+
}
|
|
1958
|
+
const element = `${String(source.id)} ${kind === "directed" ? "->" : "--"} ${String(target.id)}`;
|
|
1959
|
+
const where = { line, element };
|
|
1960
|
+
const { weightFrom } = this.options;
|
|
1961
|
+
let weight;
|
|
1962
|
+
const weightText = weightFrom === null ? void 0 : effective.get(weightFrom);
|
|
1963
|
+
if (weightText !== void 0) {
|
|
1964
|
+
try {
|
|
1965
|
+
weight = parseWeightText(weightText);
|
|
1966
|
+
} catch (err) {
|
|
1967
|
+
this.report.recordError(err, where);
|
|
1968
|
+
this.report.counts.skippedEdges++;
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
const key = effective.get(KEY_ATTRIBUTE);
|
|
1973
|
+
const dedupeKey = this.dedupeKey(source.index, target.index, kind, key);
|
|
1974
|
+
if (dedupeKey !== null) {
|
|
1975
|
+
const existing = (this.strict ? this.strictEdges : this.keyedEdges).get(dedupeKey);
|
|
1976
|
+
if (existing !== void 0) {
|
|
1977
|
+
this.mergeEdge(existing, weight, effective, where, key);
|
|
1978
|
+
this.setPorts(existing, source, target, where);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
const { sink } = this;
|
|
1983
|
+
const before = sink.edgeCount;
|
|
1984
|
+
let e;
|
|
1985
|
+
try {
|
|
1986
|
+
e = this.resolver.addEdge(source.id, target.id, kind, weight, where);
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
this.report.recordError(err, where);
|
|
1989
|
+
this.report.counts.skippedEdges++;
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
this.report.counts.edges += sink.edgeCount - before;
|
|
1993
|
+
if (dedupeKey !== null) {
|
|
1994
|
+
(this.strict ? this.strictEdges : this.keyedEdges).set(dedupeKey, e);
|
|
1995
|
+
}
|
|
1996
|
+
for (const [name, value] of effective) {
|
|
1997
|
+
if (name !== weightFrom) {
|
|
1998
|
+
this.setEdgeAttribute(e, name, value, line, element);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
this.setPorts(e, source, target, where);
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Write the endpoints' ports of an edge, when they have any.
|
|
2005
|
+
* @param e - the edge index
|
|
2006
|
+
* @param source - the source endpoint
|
|
2007
|
+
* @param target - the target endpoint
|
|
2008
|
+
* @param where - the line and element
|
|
2009
|
+
*/
|
|
2010
|
+
setPorts(e, source, target, where) {
|
|
2011
|
+
if (source.port !== null) {
|
|
2012
|
+
this.setPort(e, "source", source.port, where);
|
|
2013
|
+
}
|
|
2014
|
+
if (target.port !== null) {
|
|
2015
|
+
this.setPort(e, "target", target.port, where);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* The key under which an edge is merged with an earlier one: every (source, target) pair under
|
|
2020
|
+
* `strict` (unordered for an undirected edge), else the (source, target, key) triple of an edge
|
|
2021
|
+
* carrying a `key` attribute (cgraph's edge identity).
|
|
2022
|
+
* @param source - the source index
|
|
2023
|
+
* @param target - the target index
|
|
2024
|
+
* @param kind - the edge's direction
|
|
2025
|
+
* @param key - the `key` attribute, or undefined
|
|
2026
|
+
* @returns the dedupe key, or null when the edge is never merged
|
|
2027
|
+
*/
|
|
2028
|
+
dedupeKey(source, target, kind, key) {
|
|
2029
|
+
const ordered = kind === "undirected" && target < source ? `${target}>${source}` : `${source}>${target}`;
|
|
2030
|
+
if (this.strict) {
|
|
2031
|
+
return ordered;
|
|
2032
|
+
}
|
|
2033
|
+
return key === void 0 ? null : `${ordered}#${key}`;
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Merge a repeated edge into the earlier one: its attributes overwrite, an explicit weight too.
|
|
2037
|
+
* @param e - the existing edge index
|
|
2038
|
+
* @param weight - the repeated edge's weight, or undefined
|
|
2039
|
+
* @param effective - the repeated edge's effective attributes
|
|
2040
|
+
* @param where - the line and element
|
|
2041
|
+
* @param key - the `key` attribute when the merge is by key
|
|
2042
|
+
*/
|
|
2043
|
+
mergeEdge(e, weight, effective, where, key) {
|
|
2044
|
+
const byKey = !this.strict && key !== void 0;
|
|
2045
|
+
this.report.warning(
|
|
2046
|
+
"merged",
|
|
2047
|
+
byKey ? DOT_ISSUE.KEY_MERGED : DOT_ISSUE.STRICT_MERGED,
|
|
2048
|
+
byKey ? `edge ${where.element} with key ${JSON.stringify(key)} repeats an earlier edge; attributes merged` : `parallel edge ${where.element} merged into the earlier one (strict graph)`,
|
|
2049
|
+
where
|
|
2050
|
+
);
|
|
2051
|
+
try {
|
|
2052
|
+
if (weight !== void 0) {
|
|
2053
|
+
this.sink.setEdgeWeight(e, weight);
|
|
2054
|
+
}
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
this.report.recordError(err, where);
|
|
2057
|
+
}
|
|
2058
|
+
for (const [name, value] of effective) {
|
|
2059
|
+
if (name !== this.options.weightFrom) {
|
|
2060
|
+
this.setEdgeAttribute(e, name, value, where.line, where.element);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Write one node attribute: `label` to the label column, `pos` to the position column,
|
|
2066
|
+
* anything else as an inferred cell.
|
|
2067
|
+
* @param index - the node index
|
|
2068
|
+
* @param name - the attribute name
|
|
2069
|
+
* @param value - the value text
|
|
2070
|
+
* @param line - the line, for issues
|
|
2071
|
+
*/
|
|
2072
|
+
setNodeAttribute(index, name, value, line) {
|
|
2073
|
+
const element = String(this.idOf.get(index) ?? index);
|
|
2074
|
+
try {
|
|
2075
|
+
if (name === LABEL_ATTRIBUTE) {
|
|
2076
|
+
this.sink.setNodeValue(this.nodeLabel(), index, value);
|
|
2077
|
+
} else if (name === POS_ATTRIBUTE) {
|
|
2078
|
+
this.setPosition(index, value, line, element);
|
|
2079
|
+
} else {
|
|
2080
|
+
this.textWriter("node", name).write(index, value);
|
|
2081
|
+
}
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
this.report.recordError(err, { line, element });
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Write one edge attribute: `label` to the label column, `key` to the edge id column, anything
|
|
2088
|
+
* else as an inferred cell.
|
|
2089
|
+
* @param e - the edge index
|
|
2090
|
+
* @param name - the attribute name
|
|
2091
|
+
* @param value - the value text
|
|
2092
|
+
* @param line - the line, for issues
|
|
2093
|
+
* @param element - the edge description, for issues
|
|
2094
|
+
*/
|
|
2095
|
+
setEdgeAttribute(e, name, value, line, element) {
|
|
2096
|
+
try {
|
|
2097
|
+
if (name === LABEL_ATTRIBUTE) {
|
|
2098
|
+
this.setEdgeText(this.edgeLabel(), e, value);
|
|
2099
|
+
} else if (name === KEY_ATTRIBUTE) {
|
|
2100
|
+
this.setEdgeText(this.keyColumn(), e, value);
|
|
2101
|
+
} else {
|
|
2102
|
+
this.textWriter("edge", name).write(e, value);
|
|
2103
|
+
}
|
|
2104
|
+
} catch (err) {
|
|
2105
|
+
this.report.recordError(err, { line, element });
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Write a graph attribute (the root's `ID = ID` and `graph [..]`).
|
|
2110
|
+
* @param attribute - the attribute
|
|
2111
|
+
*/
|
|
2112
|
+
setGraphAttribute(attribute) {
|
|
2113
|
+
try {
|
|
2114
|
+
if (attribute.name === LABEL_ATTRIBUTE) {
|
|
2115
|
+
this.sink.setGraphValue(attribute.name, attribute.value, { dtype: "string", origin: DOT_ORIGIN });
|
|
2116
|
+
} else {
|
|
2117
|
+
this.sink.setGraphValue(attribute.name, parseTextCell(attribute.value), { origin: DOT_ORIGIN });
|
|
2118
|
+
}
|
|
2119
|
+
} catch (err) {
|
|
2120
|
+
this.report.recordError(err, { line: attribute.line, element: attribute.name });
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Write a node's `pos`: `x,y[,z][!]` into the position column, the `!` as `pin` = true.
|
|
2125
|
+
* @param index - the node index
|
|
2126
|
+
* @param text - the pos text
|
|
2127
|
+
* @param line - the line
|
|
2128
|
+
* @param element - the node id text
|
|
2129
|
+
*/
|
|
2130
|
+
setPosition(index, text, line, element) {
|
|
2131
|
+
const match = POINT_TEXT.exec(text);
|
|
2132
|
+
if (match === null) {
|
|
2133
|
+
this.report.warning(
|
|
2134
|
+
"validation-error",
|
|
2135
|
+
DOT_ISSUE.BAD_POS,
|
|
2136
|
+
`pos ${JSON.stringify(text)} is not a point "x,y[,z][!]"; dropped`,
|
|
2137
|
+
{ line, element }
|
|
2138
|
+
);
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
const x = Number(match[1]);
|
|
2142
|
+
const y = Number(match[2]);
|
|
2143
|
+
const z = match[3] === void 0 ? 0 : Number(match[3]);
|
|
2144
|
+
const dims = match[3] === void 0 ? 2 : 3;
|
|
2145
|
+
this.sink.setNodeValue(this.positionColumn(dims), index, [x, y, z]);
|
|
2146
|
+
if (match[4] === "!") {
|
|
2147
|
+
this.sink.setNodeValue(PIN_ATTRIBUTE, index, true);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Write an endpoint's port into the source / target port column.
|
|
2152
|
+
* @param e - the edge index
|
|
2153
|
+
* @param side - which endpoint
|
|
2154
|
+
* @param port - the port text
|
|
2155
|
+
* @param where - the line and element
|
|
2156
|
+
*/
|
|
2157
|
+
setPort(e, side, port, where) {
|
|
2158
|
+
try {
|
|
2159
|
+
this.setEdgeText(side === "source" ? this.sourcePortColumn() : this.targetPortColumn(), e, port);
|
|
2160
|
+
} catch (err) {
|
|
2161
|
+
this.report.recordError(err, where);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Write a text cell into one of the importer's edge text columns.
|
|
2166
|
+
* @param handle - the column
|
|
2167
|
+
* @param e - the edge index
|
|
2168
|
+
* @param text - the text
|
|
2169
|
+
*/
|
|
2170
|
+
setEdgeText(handle, e, text) {
|
|
2171
|
+
this.sink.setEdgeValue(handle, e, text);
|
|
2172
|
+
}
|
|
2173
|
+
// ============================================================ lazily declared columns
|
|
2174
|
+
/**
|
|
2175
|
+
* The node label column (string, role label).
|
|
2176
|
+
* @returns the handle
|
|
2177
|
+
*/
|
|
2178
|
+
nodeLabel() {
|
|
2179
|
+
if (this.nodeLabelHandle === INVALID_INDEX) {
|
|
2180
|
+
this.nodeLabelHandle = this.declare("node", {
|
|
2181
|
+
name: LABEL_ATTRIBUTE,
|
|
2182
|
+
dtype: "string",
|
|
2183
|
+
nullable: true,
|
|
2184
|
+
role: "label",
|
|
2185
|
+
origin: DOT_ORIGIN
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
return this.nodeLabelHandle;
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* The edge label column (string, role label).
|
|
2192
|
+
* @returns the handle
|
|
2193
|
+
*/
|
|
2194
|
+
edgeLabel() {
|
|
2195
|
+
if (this.edgeLabelHandle === INVALID_INDEX) {
|
|
2196
|
+
this.edgeLabelHandle = this.declare("edge", {
|
|
2197
|
+
name: LABEL_ATTRIBUTE,
|
|
2198
|
+
dtype: "string",
|
|
2199
|
+
nullable: true,
|
|
2200
|
+
role: "label",
|
|
2201
|
+
origin: DOT_ORIGIN
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
return this.edgeLabelHandle;
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* The position column (f32 x3, role position, design section 5.2), declared on the first `pos`.
|
|
2208
|
+
* @param dims - the dimensions of the first value, recorded in extra.sourceDims
|
|
2209
|
+
* @returns the handle
|
|
2210
|
+
*/
|
|
2211
|
+
positionColumn(dims) {
|
|
2212
|
+
if (this.positionHandle === INVALID_INDEX) {
|
|
2213
|
+
this.positionHandle = this.declare("node", {
|
|
2214
|
+
name: POS_ATTRIBUTE,
|
|
2215
|
+
dtype: "f32",
|
|
2216
|
+
components: 3,
|
|
2217
|
+
nullable: true,
|
|
2218
|
+
mutable: true,
|
|
2219
|
+
role: "position",
|
|
2220
|
+
origin: { ...DOT_ORIGIN, type: "point" },
|
|
2221
|
+
extra: { sourceDims: dims, units: "file" }
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
return this.positionHandle;
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* The cluster marker column (bool).
|
|
2228
|
+
* @returns the handle
|
|
2229
|
+
*/
|
|
2230
|
+
clusterColumn() {
|
|
2231
|
+
if (this.clusterHandle === INVALID_INDEX) {
|
|
2232
|
+
this.clusterHandle = this.declare("node", {
|
|
2233
|
+
name: CLUSTER_COLUMN,
|
|
2234
|
+
dtype: "bool",
|
|
2235
|
+
nullable: true,
|
|
2236
|
+
origin: DOT_ORIGIN
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
return this.clusterHandle;
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* The parent column (u32, role parent, refersTo node).
|
|
2243
|
+
* @returns the handle
|
|
2244
|
+
*/
|
|
2245
|
+
parentColumn() {
|
|
2246
|
+
if (this.parentHandle === INVALID_INDEX) {
|
|
2247
|
+
this.parentHandle = this.declare("node", {
|
|
2248
|
+
name: PARENT_COLUMN,
|
|
2249
|
+
dtype: "u32",
|
|
2250
|
+
nullable: true,
|
|
2251
|
+
role: "parent",
|
|
2252
|
+
refersTo: "node",
|
|
2253
|
+
origin: DOT_ORIGIN
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
return this.parentHandle;
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* The edge key column (string, role id): cgraph's edge identity within a source / target pair.
|
|
2260
|
+
* @returns the handle
|
|
2261
|
+
*/
|
|
2262
|
+
keyColumn() {
|
|
2263
|
+
if (this.keyHandle === INVALID_INDEX) {
|
|
2264
|
+
this.keyHandle = this.declare("edge", {
|
|
2265
|
+
name: KEY_ATTRIBUTE,
|
|
2266
|
+
dtype: "string",
|
|
2267
|
+
nullable: true,
|
|
2268
|
+
role: "id",
|
|
2269
|
+
origin: { ...DOT_ORIGIN, id: KEY_ATTRIBUTE }
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
return this.keyHandle;
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* The source port column (string, role sourcePort).
|
|
2276
|
+
* @returns the handle
|
|
2277
|
+
*/
|
|
2278
|
+
sourcePortColumn() {
|
|
2279
|
+
if (this.sourcePortHandle === INVALID_INDEX) {
|
|
2280
|
+
this.sourcePortHandle = this.declare("edge", {
|
|
2281
|
+
name: SOURCE_PORT_COLUMN,
|
|
2282
|
+
dtype: "string",
|
|
2283
|
+
nullable: true,
|
|
2284
|
+
role: "sourcePort",
|
|
2285
|
+
origin: DOT_ORIGIN
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
return this.sourcePortHandle;
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* The target port column (string, role targetPort).
|
|
2292
|
+
* @returns the handle
|
|
2293
|
+
*/
|
|
2294
|
+
targetPortColumn() {
|
|
2295
|
+
if (this.targetPortHandle === INVALID_INDEX) {
|
|
2296
|
+
this.targetPortHandle = this.declare("edge", {
|
|
2297
|
+
name: TARGET_PORT_COLUMN,
|
|
2298
|
+
dtype: "string",
|
|
2299
|
+
nullable: true,
|
|
2300
|
+
role: "targetPort",
|
|
2301
|
+
origin: DOT_ORIGIN
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
return this.targetPortHandle;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* The inferred-column writer of an attribute name in a domain (design section 5.1: the column's
|
|
2308
|
+
* dtype follows the text grammar per column; a caller's column of the name receives parsed
|
|
2309
|
+
* values through the sink's own inference).
|
|
2310
|
+
* @param domain - node or edge
|
|
2311
|
+
* @param name - the attribute name
|
|
2312
|
+
* @returns the writer
|
|
2313
|
+
*/
|
|
2314
|
+
textWriter(domain, name) {
|
|
2315
|
+
const writers = domain === "node" ? this.nodeWriters : this.edgeWriters;
|
|
2316
|
+
let writer = writers.get(name);
|
|
2317
|
+
if (writer === void 0) {
|
|
2318
|
+
writer = new TextCellWriter(name, domain, this.sink, this.report);
|
|
2319
|
+
writers.set(name, writer);
|
|
2320
|
+
}
|
|
2321
|
+
return writer;
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Declare one of the importer's columns on the sink through the shared design section 5.6
|
|
2325
|
+
* rule: a caller's sink that already holds the role gets the column without it (reported), one
|
|
2326
|
+
* that holds the name with another shape gets it renamed `<name>#<id>` (reported).
|
|
2327
|
+
* @param domain - node or edge
|
|
2328
|
+
* @param decl - the declaration
|
|
2329
|
+
* @returns the handle
|
|
2330
|
+
*/
|
|
2331
|
+
declare(domain, decl) {
|
|
2332
|
+
const withId = decl.origin?.id === void 0 ? { ...decl, origin: { ...decl.origin, id: decl.name } } : decl;
|
|
2333
|
+
return declareResolved(this.sink, domain, withId, this.report, { element: decl.name }).handle;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
export {
|
|
2337
|
+
DOT_ISSUE as D,
|
|
2338
|
+
DOT_LOSS as a,
|
|
2339
|
+
dotImporter as b,
|
|
2340
|
+
dotExporter as d
|
|
2341
|
+
};
|
|
2342
|
+
//# sourceMappingURL=importer-DbnGYr3_.js.map
|