@graphty/graph-io 0.3.18 → 0.3.20
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/README.md +80 -2
- package/dist/chunks/{escape-D-gZWO26.js → escape-B-tkk_Cl.js} +74 -16
- package/dist/chunks/escape-B-tkk_Cl.js.map +1 -0
- package/dist/chunks/{writer-GAdltGmC.js → export-Bh60Dv-n.js} +7 -252
- package/dist/chunks/export-Bh60Dv-n.js.map +1 -0
- package/dist/chunks/exporter-BGrsWamJ.js +736 -0
- package/dist/chunks/exporter-BGrsWamJ.js.map +1 -0
- package/dist/chunks/exporter-DIeGJXAZ.js +834 -0
- package/dist/chunks/exporter-DIeGJXAZ.js.map +1 -0
- package/dist/chunks/{importer-Br_QeAeE.js → importer-BWY2FFc5.js} +28 -6
- package/dist/chunks/importer-BWY2FFc5.js.map +1 -0
- package/dist/chunks/importer-BaQpCEcJ.js +2194 -0
- package/dist/chunks/importer-BaQpCEcJ.js.map +1 -0
- package/dist/chunks/importer-DD-xv9_Y.js +1471 -0
- package/dist/chunks/importer-DD-xv9_Y.js.map +1 -0
- package/dist/chunks/{importer-aNJfe0qu.js → importer-DEcKhsmQ.js} +2 -2
- package/dist/chunks/{importer-aNJfe0qu.js.map → importer-DEcKhsmQ.js.map} +1 -1
- package/dist/chunks/{importer-DHagxvDD.js → importer-DIrbbnAf.js} +6 -4
- package/dist/chunks/{importer-DHagxvDD.js.map → importer-DIrbbnAf.js.map} +1 -1
- package/dist/chunks/importer-D_e7e7LX.js +3055 -0
- package/dist/chunks/importer-D_e7e7LX.js.map +1 -0
- package/dist/chunks/{importer-Du5crN9l.js → importer-Wv4C4gaF.js} +278 -82
- package/dist/chunks/importer-Wv4C4gaF.js.map +1 -0
- package/dist/chunks/importer-vi3bSdtw.js +1713 -0
- package/dist/chunks/importer-vi3bSdtw.js.map +1 -0
- package/dist/chunks/{importer-d0uQxFp6.js → importer-zGLo8Dg_.js} +6 -4
- package/dist/chunks/{importer-d0uQxFp6.js.map → importer-zGLo8Dg_.js.map} +1 -1
- package/dist/chunks/json-elements-DDS8N2Dd.js +779 -0
- package/dist/chunks/json-elements-DDS8N2Dd.js.map +1 -0
- package/dist/chunks/{records-Bk9jgodz.js → records-dbRkxwaq.js} +2 -2
- package/dist/chunks/{records-Bk9jgodz.js.map → records-dbRkxwaq.js.map} +1 -1
- package/dist/chunks/{report-BOk0p5y8.js → report-B1z4WT9e.js} +143 -111
- package/dist/chunks/{report-BOk0p5y8.js.map → report-B1z4WT9e.js.map} +1 -1
- package/dist/chunks/weights-Dzba96G3.js +176 -0
- package/dist/chunks/weights-Dzba96G3.js.map +1 -0
- package/dist/chunks/writer-C7flM-Ih.js +77 -0
- package/dist/chunks/writer-C7flM-Ih.js.map +1 -0
- package/dist/csv.js +6 -4
- package/dist/csv.js.map +1 -1
- package/dist/cx.d.ts +1 -0
- package/dist/cx.js +6 -0
- package/dist/cx.js.map +1 -0
- package/dist/cx2.d.ts +1 -0
- package/dist/cx2.js +10 -0
- package/dist/cx2.js.map +1 -0
- package/dist/cys.d.ts +1 -0
- package/dist/cys.js +6 -0
- package/dist/cys.js.map +1 -0
- package/dist/dot.js +1 -1
- package/dist/gexf.js +16 -4
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +13 -14
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +302 -399
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +1 -1
- package/dist/json.js +1 -1
- package/dist/neo4j.js +6 -4
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/cell-budget.d.ts +84 -0
- package/dist/src/common/cell-budget.d.ts.map +1 -0
- package/dist/src/common/cell-budget.js +138 -0
- package/dist/src/common/cell-budget.js.map +1 -0
- package/dist/src/common/input.d.ts +10 -0
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +39 -0
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/json-elements.d.ts +286 -0
- package/dist/src/common/json-elements.d.ts.map +1 -0
- package/dist/src/common/json-elements.js +926 -0
- package/dist/src/common/json-elements.js.map +1 -0
- package/dist/src/common/options.d.ts +6 -0
- package/dist/src/common/options.d.ts.map +1 -1
- package/dist/src/common/options.js +1 -1
- package/dist/src/common/options.js.map +1 -1
- package/dist/src/common/xml.d.ts +37 -3
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +86 -6
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/common/zip.d.ts +92 -0
- package/dist/src/common/zip.d.ts.map +1 -0
- package/dist/src/common/zip.js +399 -0
- package/dist/src/common/zip.js.map +1 -0
- package/dist/src/formats/cx/importer.d.ts +133 -0
- package/dist/src/formats/cx/importer.d.ts.map +1 -0
- package/dist/src/formats/cx/importer.js +2220 -0
- package/dist/src/formats/cx/importer.js.map +1 -0
- package/dist/src/formats/cx/index.d.ts +7 -0
- package/dist/src/formats/cx/index.d.ts.map +1 -0
- package/dist/src/formats/cx/index.js +7 -0
- package/dist/src/formats/cx/index.js.map +1 -0
- package/dist/src/formats/cx2/exporter.d.ts +59 -0
- package/dist/src/formats/cx2/exporter.d.ts.map +1 -0
- package/dist/src/formats/cx2/exporter.js +864 -0
- package/dist/src/formats/cx2/exporter.js.map +1 -0
- package/dist/src/formats/cx2/importer.d.ts +169 -0
- package/dist/src/formats/cx2/importer.d.ts.map +1 -0
- package/dist/src/formats/cx2/importer.js +1652 -0
- package/dist/src/formats/cx2/importer.js.map +1 -0
- package/dist/src/formats/cx2/index.d.ts +7 -0
- package/dist/src/formats/cx2/index.d.ts.map +1 -0
- package/dist/src/formats/cx2/index.js +7 -0
- package/dist/src/formats/cx2/index.js.map +1 -0
- package/dist/src/formats/cys/constants.d.ts +68 -0
- package/dist/src/formats/cys/constants.d.ts.map +1 -0
- package/dist/src/formats/cys/constants.js +76 -0
- package/dist/src/formats/cys/constants.js.map +1 -0
- package/dist/src/formats/cys/importer.d.ts +38 -0
- package/dist/src/formats/cys/importer.d.ts.map +1 -0
- package/dist/src/formats/cys/importer.js +830 -0
- package/dist/src/formats/cys/importer.js.map +1 -0
- package/dist/src/formats/cys/index.d.ts +7 -0
- package/dist/src/formats/cys/index.d.ts.map +1 -0
- package/dist/src/formats/cys/index.js +7 -0
- package/dist/src/formats/cys/index.js.map +1 -0
- package/dist/src/formats/cys/session.d.ts +132 -0
- package/dist/src/formats/cys/session.d.ts.map +1 -0
- package/dist/src/formats/cys/session.js +315 -0
- package/dist/src/formats/cys/session.js.map +1 -0
- package/dist/src/formats/cys/tables.d.ts +90 -0
- package/dist/src/formats/cys/tables.d.ts.map +1 -0
- package/dist/src/formats/cys/tables.js +293 -0
- package/dist/src/formats/cys/tables.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +2 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +12 -0
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.js +6 -2
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts +2 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +6 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts +0 -2
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +1 -3
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gexf/index.d.ts +1 -1
- package/dist/src/formats/gexf/index.js +2 -2
- package/dist/src/formats/gexf/index.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts +0 -8
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +8 -18
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +6 -104
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/xgmml/columns.d.ts +152 -0
- package/dist/src/formats/xgmml/columns.d.ts.map +1 -0
- package/dist/src/formats/xgmml/columns.js +593 -0
- package/dist/src/formats/xgmml/columns.js.map +1 -0
- package/dist/src/formats/xgmml/constants.d.ts +196 -0
- package/dist/src/formats/xgmml/constants.d.ts.map +1 -0
- package/dist/src/formats/xgmml/constants.js +197 -0
- package/dist/src/formats/xgmml/constants.js.map +1 -0
- package/dist/src/formats/xgmml/document.d.ts +361 -0
- package/dist/src/formats/xgmml/document.d.ts.map +1 -0
- package/dist/src/formats/xgmml/document.js +695 -0
- package/dist/src/formats/xgmml/document.js.map +1 -0
- package/dist/src/formats/xgmml/emit.d.ts +341 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -0
- package/dist/src/formats/xgmml/emit.js +1275 -0
- package/dist/src/formats/xgmml/emit.js.map +1 -0
- package/dist/src/formats/xgmml/exporter.d.ts +24 -0
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -0
- package/dist/src/formats/xgmml/exporter.js +999 -0
- package/dist/src/formats/xgmml/exporter.js.map +1 -0
- package/dist/src/formats/xgmml/importer.d.ts +58 -0
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -0
- package/dist/src/formats/xgmml/importer.js +360 -0
- package/dist/src/formats/xgmml/importer.js.map +1 -0
- package/dist/src/formats/xgmml/index.d.ts +8 -0
- package/dist/src/formats/xgmml/index.d.ts.map +1 -0
- package/dist/src/formats/xgmml/index.js +8 -0
- package/dist/src/formats/xgmml/index.js.map +1 -0
- package/dist/src/formats/xgmml/values.d.ts +60 -0
- package/dist/src/formats/xgmml/values.d.ts.map +1 -0
- package/dist/src/formats/xgmml/values.js +148 -0
- package/dist/src/formats/xgmml/values.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry.d.ts +7 -0
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +30 -10
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts +1 -1
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js +4 -0
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.d.ts +1 -0
- package/dist/xgmml.js +9 -0
- package/dist/xgmml.js.map +1 -0
- package/package.json +25 -2
- package/src/common/cell-budget.ts +170 -0
- package/src/common/input.ts +40 -0
- package/src/common/json-elements.ts +1147 -0
- package/src/common/options.ts +1 -1
- package/src/common/xml.ts +127 -6
- package/src/common/zip.ts +472 -0
- package/src/formats/cx/importer.ts +2733 -0
- package/src/formats/cx/index.ts +7 -0
- package/src/formats/cx2/exporter.ts +1036 -0
- package/src/formats/cx2/importer.ts +2187 -0
- package/src/formats/cx2/index.ts +7 -0
- package/src/formats/cys/constants.ts +99 -0
- package/src/formats/cys/importer.ts +1100 -0
- package/src/formats/cys/index.ts +7 -0
- package/src/formats/cys/session.ts +428 -0
- package/src/formats/cys/tables.ts +400 -0
- package/src/formats/dot/exporter.ts +17 -0
- package/src/formats/dot/importer.ts +6 -2
- package/src/formats/gexf/exporter.ts +11 -0
- package/src/formats/gexf/importer.ts +1 -2
- package/src/formats/gexf/index.ts +1 -1
- package/src/formats/gml/exporter.ts +8 -19
- package/src/formats/json/importer.ts +6 -105
- package/src/formats/xgmml/columns.ts +747 -0
- package/src/formats/xgmml/constants.ts +265 -0
- package/src/formats/xgmml/document.ts +975 -0
- package/src/formats/xgmml/emit.ts +1563 -0
- package/src/formats/xgmml/exporter.ts +1215 -0
- package/src/formats/xgmml/importer.ts +491 -0
- package/src/formats/xgmml/index.ts +8 -0
- package/src/formats/xgmml/values.ts +183 -0
- package/src/index.ts +19 -0
- package/src/registry.ts +46 -15
- package/src/sniff.ts +18 -1
- package/dist/chunks/escape-D-gZWO26.js.map +0 -1
- package/dist/chunks/importer-Br_QeAeE.js.map +0 -1
- package/dist/chunks/importer-Du5crN9l.js.map +0 -1
- package/dist/chunks/writer-GAdltGmC.js.map +0 -1
|
@@ -0,0 +1,3055 @@
|
|
|
1
|
+
import { INVALID_INDEX, GraphFormatError } from "@graphty/graph-format";
|
|
2
|
+
import { af as MIXED_DIRECTION_CODE, D as DIRECTION_FORCED_CODE, a as DIRECTION_REFUSED_CODE, S as SINK_OPTION_CODE, O as OPTION_IGNORED_CODE, I as ID_MERGED_CODE, R as ROLE_TAKEN_CODE, C as COLUMN_RENAMED_CODE, A as AMBIGUOUS_GRAPH_NAME_CODE, G as GRAPH_NOT_FOUND_CODE, M as MULTIPLE_GRAPHS_CODE, au as STRAY_TEXT_CODE, Y as UNKNOWN_ELEMENT_CODE, k as UNKNOWN_ATTR_TYPE_CODE, am as EQUATION_AS_TEXT_CODE, h as PARENT_CYCLE_CODE, U as UNKNOWN_PARENT_CODE, i as DUPLICATE_ATTRIBUTE_CODE, j as DANGLING_REFERENCE_CODE, P as PRECISION_CODE, W as WIDENED_CODE, B as BAD_VALUE_CODE, c as DUPLICATE_EDGE_ID_CODE, d as DUPLICATE_NODE_CODE, e as MISSING_ENDPOINT_CODE, f as MISSING_ID_CODE, N as NO_GRAPH_CODE, q as UNKNOWN_ENCODING_CODE, E as ENCODING_FALLBACK_CODE, r as INVALID_ENCODING_CODE, s as INVALID_UTF8_CODE, u as EMPTY_INPUT_CODE, aw as XML_SYNTAX_CODE, a3 as STORAGE_CLASS_CODE, a7 as WEIGHT_KEY_CLASH_CODE, a6 as ROLE_ASSUMED_CODE, a5 as COLUMN_RENAMED_LOSS_CODE, b as ID_TEXT_TYPE_CODE, aa as MUTUAL_EXPANDED_CODE, ab as PARENTS_DROPPED_CODE, ac as ROLE_DROPPED_CODE, ad as TEMPORAL_TEXT_DROPPED_CODE, ae as TEMPORAL_DROPPED_CODE, a0 as XML_ILLEGAL_CHAR_CODE, Q as uniqueColumnName, L as declareResolved, _ as DictHeuristic, K as IdCoercer, J as DirectionResolver, y as throwIfAborted, F as reportSinkOptions, z as reportUnusedOptions, H as chooseGraph, v as resolveImportOptions, w as ImportReportBuilder, x as textChunks } from "./report-B1z4WT9e.js";
|
|
3
|
+
import { l as localName, a as escapeXmlText, g as isWhitespace, e as escapeXmlAttribute, t as tokenizeXml, f as xmlDeclaredEncoding, X as XmlSyntaxError } from "./escape-B-tkk_Cl.js";
|
|
4
|
+
import { p as parseWeightText } from "./weights-Dzba96G3.js";
|
|
5
|
+
import { p as parseTextCell, i as inferTextDtype } from "./text-Dr0Ifpag.js";
|
|
6
|
+
const FORMAT = "xgmml";
|
|
7
|
+
const EXTENSIONS = Object.freeze([".xgmml", ".xml"]);
|
|
8
|
+
const MIME_TYPES = Object.freeze(["application/xgmml", "text/xgmml", "text/xgmml+xml"]);
|
|
9
|
+
const XGMML_NAMESPACE = "http://www.cs.rpi.edu/XGMML";
|
|
10
|
+
const CY_NAMESPACE = "http://www.cytoscape.org";
|
|
11
|
+
const XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
|
|
12
|
+
const META_KEY = "xgmml";
|
|
13
|
+
const LABEL_COLUMN = "label";
|
|
14
|
+
const EDGE_ID_COLUMN = "id";
|
|
15
|
+
const POSITION_COLUMN = "position";
|
|
16
|
+
const Z_COLUMN = "z";
|
|
17
|
+
const GRAPHICS_COLUMN = "graphics";
|
|
18
|
+
const PARENT_COLUMN = "parent";
|
|
19
|
+
const PARENTS_COLUMN = "parents";
|
|
20
|
+
const SUBGRAPH_COLUMN = "xgmml.subgraph";
|
|
21
|
+
const NESTED_NETWORK_COLUMN = "cytoscape.nestedNetwork";
|
|
22
|
+
const NETWORK_POINTER_COLUMN = "xgmml.networkPointer";
|
|
23
|
+
const NETWORKS_COLUMN = "xgmml.networks";
|
|
24
|
+
const INTERACTION_COLUMN = "interaction";
|
|
25
|
+
const XGMML_ORIGIN_NAMESPACE = "xgmml";
|
|
26
|
+
const CYTOSCAPE_ORIGIN_NAMESPACE = "cytoscape";
|
|
27
|
+
const XGMML_ISSUE = Object.freeze({
|
|
28
|
+
/** Fatal: the input is not well-formed XML. */
|
|
29
|
+
XML_SYNTAX: XML_SYNTAX_CODE,
|
|
30
|
+
/** Fatal: the input is empty or whitespace only. */
|
|
31
|
+
EMPTY_INPUT: EMPTY_INPUT_CODE,
|
|
32
|
+
/** Fatal: the input holds invalid UTF-8. */
|
|
33
|
+
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
34
|
+
/** Fatal: bytes invalid in the encoding a BOM, a declaration or the option chose. */
|
|
35
|
+
INVALID_ENCODING: INVALID_ENCODING_CODE,
|
|
36
|
+
/** Undeclared non-UTF-8 bytes were read as windows-1252. */
|
|
37
|
+
ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
|
|
38
|
+
/** A declared encoding the platform cannot decode was ignored. */
|
|
39
|
+
UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
|
|
40
|
+
/** Fatal: the root element is not `<graph>` (an XHTML page embedding one, a GraphML file). */
|
|
41
|
+
NO_GRAPH: NO_GRAPH_CODE,
|
|
42
|
+
/** Fatal: a session view document (`cy:view="1"`): view SUIDs and no topology. */
|
|
43
|
+
VIEW_DOCUMENT: "E_XGMML_VIEW_DOCUMENT",
|
|
44
|
+
/** A node with neither an id nor a label; it is skipped with its subtree. */
|
|
45
|
+
MISSING_ID: MISSING_ID_CODE,
|
|
46
|
+
/** A node without an id; its label is used as the id. */
|
|
47
|
+
ID_FROM_LABEL: "W_XGMML_ID_FROM_LABEL",
|
|
48
|
+
/** An edge without a source or a target that no label alias resolves. */
|
|
49
|
+
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
50
|
+
/** An edge endpoint that names no node (addMissingNodes false); the edge is skipped. */
|
|
51
|
+
UNKNOWN_NODE: "E_UNKNOWN_NODE",
|
|
52
|
+
/** Endpoints resolved through Cytoscape's `"a (pp) b"` label aliases; interactions filled from labels. */
|
|
53
|
+
LABEL_ALIAS: "W_XGMML_LABEL_ALIAS",
|
|
54
|
+
/** A node id declared twice; the declarations are merged. */
|
|
55
|
+
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
56
|
+
/** An edge id used twice; the second edge is skipped. */
|
|
57
|
+
DUPLICATE_EDGE_ID: DUPLICATE_EDGE_ID_CODE,
|
|
58
|
+
/** A meta-edge the 2.x writer repeats inside a group; the copy is dropped. */
|
|
59
|
+
GROUP_DUPLICATE_EDGE: "W_XGMML_GROUP_DUPLICATE_EDGE",
|
|
60
|
+
/** A `directed` or `cy:directed` value other than 0 / 1. */
|
|
61
|
+
BAD_DIRECTED: "W_XGMML_BAD_DIRECTED",
|
|
62
|
+
/** A `documentVersion` that does not parse; the dialect is chosen from the content. */
|
|
63
|
+
DOCUMENT_VERSION: "W_XGMML_DOCUMENT_VERSION",
|
|
64
|
+
/** A root `<graph>` with neither the XGMML namespace nor an XGMML DOCTYPE. */
|
|
65
|
+
NO_NAMESPACE: "W_XGMML_NO_NAMESPACE",
|
|
66
|
+
/** A malformed `<att>`: a list with a value, a scalar with child atts, an att with no name. */
|
|
67
|
+
BAD_ATT: "W_XGMML_BAD_ATT",
|
|
68
|
+
/** A bare `&` read as `&` under repairBareAmpersands. */
|
|
69
|
+
AMPERSAND_REPAIRED: "W_XGMML_AMPERSAND_REPAIRED",
|
|
70
|
+
/** Two surrogate character references joined under pairSurrogateReferences. */
|
|
71
|
+
SURROGATE_PAIRED: "W_XGMML_SURROGATE_PAIRED",
|
|
72
|
+
/** An empty list whose element type nothing states; a list of strings is assumed. */
|
|
73
|
+
EMPTY_LIST_TYPE: "W_XGMML_EMPTY_LIST_TYPE",
|
|
74
|
+
/** A record list, a list of lists, a 2.x map or foreign XML in an att, kept as json. */
|
|
75
|
+
RECORD_LIST: "W_XGMML_RECORD_LIST",
|
|
76
|
+
/** A pointer into another file (`file.xgmml#id`); kept as text. */
|
|
77
|
+
CROSS_FILE_REFERENCE: "W_XGMML_CROSS_FILE_REFERENCE",
|
|
78
|
+
/** A graph nested in an edge's att; there is no model for it. */
|
|
79
|
+
EDGE_NESTED_GRAPH: "W_XGMML_EDGE_NESTED_GRAPH",
|
|
80
|
+
/** Elements of a session network declared outside every registered subnetwork. */
|
|
81
|
+
ROOT_ONLY_ELEMENTS: "W_XGMML_ROOT_ONLY_ELEMENTS",
|
|
82
|
+
/** A value that does not parse as its declared type; the cell is unset. */
|
|
83
|
+
BAD_VALUE: BAD_VALUE_CODE,
|
|
84
|
+
/** A column widened because its values or declared types disagree. */
|
|
85
|
+
WIDENED: WIDENED_CODE,
|
|
86
|
+
/** A long or real beyond what a double holds exactly. */
|
|
87
|
+
PRECISION: PRECISION_CODE,
|
|
88
|
+
/** A reference (xlink:href, nested-network pointer) that names nothing. */
|
|
89
|
+
DANGLING_REFERENCE: DANGLING_REFERENCE_CODE,
|
|
90
|
+
/** The same attribute twice on one element; the later value wins. */
|
|
91
|
+
DUPLICATE_ATTRIBUTE: DUPLICATE_ATTRIBUTE_CODE,
|
|
92
|
+
/** A group membership naming no node. */
|
|
93
|
+
UNKNOWN_PARENT: UNKNOWN_PARENT_CODE,
|
|
94
|
+
/** A group membership that would close a parent cycle; that link is dropped. */
|
|
95
|
+
PARENT_CYCLE: PARENT_CYCLE_CODE,
|
|
96
|
+
/** A Cytoscape formula kept as its text. */
|
|
97
|
+
EQUATION_AS_TEXT: EQUATION_AS_TEXT_CODE,
|
|
98
|
+
/** An att type XGMML and Cytoscape do not define; kept as text. */
|
|
99
|
+
UNKNOWN_ATTR_TYPE: UNKNOWN_ATTR_TYPE_CODE,
|
|
100
|
+
/** An element XGMML does not define at that place; skipped with its subtree. */
|
|
101
|
+
UNKNOWN_ELEMENT: UNKNOWN_ELEMENT_CODE,
|
|
102
|
+
/** Character data where XGMML allows only elements, or inside an att. */
|
|
103
|
+
STRAY_TEXT: STRAY_TEXT_CODE,
|
|
104
|
+
/** A session network document holds several registered networks; one was read. */
|
|
105
|
+
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
106
|
+
/** graphIndex / graphName names no network (fatal). */
|
|
107
|
+
GRAPH_NOT_FOUND: GRAPH_NOT_FOUND_CODE,
|
|
108
|
+
/** graphName names several networks (fatal). */
|
|
109
|
+
AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
|
|
110
|
+
/** A column renamed `<name>#<n>` because its name was taken. */
|
|
111
|
+
COLUMN_RENAMED: COLUMN_RENAMED_CODE,
|
|
112
|
+
/** A column declared without its role because the table already holds it. */
|
|
113
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
114
|
+
/** Two id texts merged into one number under ids "number". */
|
|
115
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
116
|
+
/** An option the format has no use for. */
|
|
117
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
118
|
+
/** A builder-policy option the sink does not honour. */
|
|
119
|
+
SINK_OPTION: SINK_OPTION_CODE,
|
|
120
|
+
/** The sink refused the file's direction. */
|
|
121
|
+
DIRECTION_REFUSED: DIRECTION_REFUSED_CODE,
|
|
122
|
+
/** Edges forced to the policy's direction. */
|
|
123
|
+
DIRECTION_FORCED: DIRECTION_FORCED_CODE,
|
|
124
|
+
/** A mixed file under onMixedDirection "error" (fatal). */
|
|
125
|
+
MIXED_DIRECTION: MIXED_DIRECTION_CODE
|
|
126
|
+
});
|
|
127
|
+
const XGMML_LOSS = Object.freeze({
|
|
128
|
+
/** A json or nested-list column written as a string att. */
|
|
129
|
+
JSON_AS_STRING: "W_XGMML_JSON_AS_STRING",
|
|
130
|
+
/** A dtype written as a wider Cytoscape type (f32 as Double, u32 above i32 as Long, u8 as Integer, dict as String). */
|
|
131
|
+
WIDENED_TYPE: "W_XGMML_WIDENED_TYPE",
|
|
132
|
+
/** A string holding a character XML 1.0 cannot carry; export() throws. */
|
|
133
|
+
XML_ILLEGAL_CHAR: XML_ILLEGAL_CHAR_CODE,
|
|
134
|
+
/** A temporal column written as plain numbers. */
|
|
135
|
+
TEMPORAL_DROPPED: TEMPORAL_DROPPED_CODE,
|
|
136
|
+
/** A temporal text companion written as a plain string att. */
|
|
137
|
+
TEMPORAL_TEXT_DROPPED: TEMPORAL_TEXT_DROPPED_CODE,
|
|
138
|
+
/** A role the format has no slot for, written as a plain att. */
|
|
139
|
+
ROLE_DROPPED: ROLE_DROPPED_CODE,
|
|
140
|
+
/** A parents column not written because the snapshot also has a parent column. */
|
|
141
|
+
PARENTS_DROPPED: PARENTS_DROPPED_CODE,
|
|
142
|
+
/** A mutual pair written as two directed edges. */
|
|
143
|
+
MUTUAL_EXPANDED: MUTUAL_EXPANDED_CODE,
|
|
144
|
+
/** Node ids whose text reads back as the other type. */
|
|
145
|
+
ID_TEXT_TYPE: ID_TEXT_TYPE_CODE,
|
|
146
|
+
/** An edge id column of another dtype written as text; it reads back as strings. */
|
|
147
|
+
EDGE_ID_TEXT: "W_XGMML_EDGE_ID_TEXT",
|
|
148
|
+
/** Strings holding a literal backslash-n or backslash-t read back as newline / tab (Cytoscape's escapes). */
|
|
149
|
+
BACKSLASH_ESCAPE: "W_XGMML_BACKSLASH_ESCAPE",
|
|
150
|
+
/** A position that cannot be written as it is: another shape, a non-finite coordinate, a z read back in the z column. */
|
|
151
|
+
POSITION: "W_XGMML_POSITION",
|
|
152
|
+
/** Nodes whose parent chain never reaches a root are written at the top level. */
|
|
153
|
+
PARENT_CYCLE: "W_XGMML_PARENT_CYCLE",
|
|
154
|
+
/** A column named like a column the importer owns reads back renamed. */
|
|
155
|
+
COLUMN_NAME_CHANGED: COLUMN_RENAMED_LOSS_CODE,
|
|
156
|
+
/** A role-less column written into a slot reads back with the slot's role. */
|
|
157
|
+
ROLE_ASSUMED: ROLE_ASSUMED_CODE,
|
|
158
|
+
/** A plain `weight` edge column reads back as THE weight. */
|
|
159
|
+
WEIGHT_KEY_CLASH: WEIGHT_KEY_CLASH_CODE,
|
|
160
|
+
/** A string / dict column that reads back as the other storage class. */
|
|
161
|
+
STORAGE_CLASS_CHANGED: STORAGE_CLASS_CODE,
|
|
162
|
+
/** Edge labels shaped `a (i) b` read back with an `interaction` column (Cytoscape's label alias). */
|
|
163
|
+
INTERACTION_FROM_LABEL: "W_XGMML_INTERACTION_FROM_LABEL"
|
|
164
|
+
});
|
|
165
|
+
const NODE_READ = /* @__PURE__ */ new Set(["id", "label", "name", "xlink:href", "cy:nodeId"]);
|
|
166
|
+
const EDGE_READ = /* @__PURE__ */ new Set([
|
|
167
|
+
"id",
|
|
168
|
+
"label",
|
|
169
|
+
"name",
|
|
170
|
+
"source",
|
|
171
|
+
"target",
|
|
172
|
+
"weight",
|
|
173
|
+
"cy:directed",
|
|
174
|
+
"xlink:href",
|
|
175
|
+
"cy:edgeId"
|
|
176
|
+
]);
|
|
177
|
+
const ATT_READ = /* @__PURE__ */ new Set([
|
|
178
|
+
"name",
|
|
179
|
+
"label",
|
|
180
|
+
"type",
|
|
181
|
+
"value",
|
|
182
|
+
"cy:type",
|
|
183
|
+
"cy:elementType",
|
|
184
|
+
"cy:hidden",
|
|
185
|
+
"cy:equation",
|
|
186
|
+
"cy:editable"
|
|
187
|
+
]);
|
|
188
|
+
const TRUE_TEXT = /^(1|true|yes)$/i;
|
|
189
|
+
function isCyTrue(text) {
|
|
190
|
+
return text !== null && text !== void 0 && TRUE_TEXT.test(text.trim());
|
|
191
|
+
}
|
|
192
|
+
function normaliseName(name, xlinkPrefixes) {
|
|
193
|
+
const colon = name.indexOf(":");
|
|
194
|
+
if (colon > 0 && xlinkPrefixes.has(name.slice(0, colon))) {
|
|
195
|
+
return `xlink:${name.slice(colon + 1)}`;
|
|
196
|
+
}
|
|
197
|
+
return name;
|
|
198
|
+
}
|
|
199
|
+
class XgmmlParser {
|
|
200
|
+
/**
|
|
201
|
+
* Create a parser.
|
|
202
|
+
* @param report - the report issues are recorded in
|
|
203
|
+
*/
|
|
204
|
+
constructor(report) {
|
|
205
|
+
this.frames = [];
|
|
206
|
+
this.xlinkPrefixes = /* @__PURE__ */ new Set(["xlink"]);
|
|
207
|
+
this.rootGraph = null;
|
|
208
|
+
this.nodes = [];
|
|
209
|
+
this.edges = [];
|
|
210
|
+
this.graphs = [];
|
|
211
|
+
this.cytoscape = false;
|
|
212
|
+
this.xgmmlNamespace = false;
|
|
213
|
+
this.rootPrefix = null;
|
|
214
|
+
this.rdf = {};
|
|
215
|
+
this.rdfXml = null;
|
|
216
|
+
this.rdfElement = null;
|
|
217
|
+
this.report = report;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The parsed document.
|
|
221
|
+
* @returns the document; E_NO_GRAPH (fatal) when no root graph was read
|
|
222
|
+
*/
|
|
223
|
+
document() {
|
|
224
|
+
const root = this.rootGraph;
|
|
225
|
+
if (root === null) {
|
|
226
|
+
return this.report.fail(XGMML_ISSUE.NO_GRAPH, "the document has no <graph> element");
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
root,
|
|
230
|
+
nodes: this.nodes,
|
|
231
|
+
edges: this.edges,
|
|
232
|
+
graphs: this.graphs,
|
|
233
|
+
cytoscape: this.cytoscape,
|
|
234
|
+
xgmmlNamespace: this.xgmmlNamespace,
|
|
235
|
+
rdf: this.rdf,
|
|
236
|
+
rdfXml: this.rdfXml
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* An element starts.
|
|
241
|
+
* @param rawName - the element name as written
|
|
242
|
+
* @param rawAttrs - its attributes
|
|
243
|
+
* @param line - the line
|
|
244
|
+
*/
|
|
245
|
+
start(rawName, rawAttrs, line) {
|
|
246
|
+
const top = this.frames.length === 0 ? null : this.frames[this.frames.length - 1];
|
|
247
|
+
if (top === null) {
|
|
248
|
+
this.startRoot(rawName, rawAttrs, line);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (top.kind === "skip") {
|
|
252
|
+
this.frames.push(top);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (top.kind === "capture") {
|
|
256
|
+
this.capture(top, rawName, rawAttrs);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const attrs = this.normalise(rawAttrs);
|
|
260
|
+
const name = localName(rawName);
|
|
261
|
+
switch (top.kind) {
|
|
262
|
+
case "graph":
|
|
263
|
+
this.startInGraph(top.graph, rawName, name, attrs, line);
|
|
264
|
+
return;
|
|
265
|
+
case "node":
|
|
266
|
+
this.startInElement(top.node, rawName, name, attrs, line);
|
|
267
|
+
return;
|
|
268
|
+
case "edge":
|
|
269
|
+
this.startInElement(top.edge, rawName, name, attrs, line);
|
|
270
|
+
return;
|
|
271
|
+
case "att":
|
|
272
|
+
this.startInAtt(top, rawName, name, attrs, line);
|
|
273
|
+
return;
|
|
274
|
+
case "graphics":
|
|
275
|
+
this.startInGraphics(top.target, rawName, name, attrs, line);
|
|
276
|
+
return;
|
|
277
|
+
case "line":
|
|
278
|
+
if (name === "point") {
|
|
279
|
+
top.points.push(Object.fromEntries(attrs));
|
|
280
|
+
this.frames.push({ kind: "skip" });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.unknownElement(rawName, line);
|
|
284
|
+
return;
|
|
285
|
+
default: {
|
|
286
|
+
const kind = top;
|
|
287
|
+
throw new Error(`unknown XGMML frame ${String(kind)}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* An element ends.
|
|
293
|
+
* @param name - the element name
|
|
294
|
+
* @param _line - the line
|
|
295
|
+
*/
|
|
296
|
+
end(name, _line) {
|
|
297
|
+
const frame = this.frames.pop();
|
|
298
|
+
const below = this.frames.length === 0 ? null : this.frames[this.frames.length - 1];
|
|
299
|
+
switch (frame?.kind) {
|
|
300
|
+
case "capture":
|
|
301
|
+
frame.att.xml = `${frame.att.xml ?? ""}</${name}>`;
|
|
302
|
+
frame.depth--;
|
|
303
|
+
if (frame.depth > 0) {
|
|
304
|
+
this.frames.push(frame);
|
|
305
|
+
} else if (frame.att.name === "networkMetadata") {
|
|
306
|
+
this.rdfXml = frame.att.xml;
|
|
307
|
+
}
|
|
308
|
+
this.rdfElement = null;
|
|
309
|
+
return;
|
|
310
|
+
case "att":
|
|
311
|
+
if (below?.kind === "graphics") {
|
|
312
|
+
this.graphicsAtt(below.target, frame.att);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
case "line":
|
|
316
|
+
if (below?.kind === "graphics") {
|
|
317
|
+
below.target.graphics = { ...below.target.graphics ?? {}, Line: frame.points };
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Character data.
|
|
324
|
+
* @param text - the text
|
|
325
|
+
* @param line - the line
|
|
326
|
+
*/
|
|
327
|
+
text(text, line) {
|
|
328
|
+
const top = this.frames.length === 0 ? null : this.frames[this.frames.length - 1];
|
|
329
|
+
if (top === null || top.kind === "skip") {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (top.kind === "capture") {
|
|
333
|
+
top.att.xml = `${top.att.xml ?? ""}${escapeXmlText(text)}`;
|
|
334
|
+
this.captureRdfText(text);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (isWhitespace(text)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (top.kind === "att") {
|
|
341
|
+
top.att.text += text;
|
|
342
|
+
}
|
|
343
|
+
this.report.warnOnce(
|
|
344
|
+
"validation-error",
|
|
345
|
+
XGMML_ISSUE.STRAY_TEXT,
|
|
346
|
+
top.kind === "att" ? "text inside <att> is ignored; an XGMML value is the value attribute" : "text where XGMML allows only elements was ignored",
|
|
347
|
+
{ line }
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
// ------------------------------------------------------------------ structure
|
|
351
|
+
/**
|
|
352
|
+
* The XML attribute map with every XLink prefix written `xlink:`.
|
|
353
|
+
* @param attrs - the attributes as written
|
|
354
|
+
* @returns the normalised map
|
|
355
|
+
*/
|
|
356
|
+
normalise(attrs) {
|
|
357
|
+
const out = /* @__PURE__ */ new Map();
|
|
358
|
+
for (const [key2, value] of attrs) {
|
|
359
|
+
out.set(normaliseName(key2, this.xlinkPrefixes), value);
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The root element: a `<graph>`, else the document is not XGMML.
|
|
365
|
+
* @param rawName - the name as written
|
|
366
|
+
* @param rawAttrs - its attributes
|
|
367
|
+
* @param line - the line
|
|
368
|
+
*/
|
|
369
|
+
startRoot(rawName, rawAttrs, line) {
|
|
370
|
+
const local = localName(rawName);
|
|
371
|
+
const prefix = rawName.includes(":") ? rawName.slice(0, rawName.indexOf(":")) : null;
|
|
372
|
+
for (const [key2, value] of rawAttrs) {
|
|
373
|
+
if (key2.startsWith("xmlns:") && value === "http://www.w3.org/1999/xlink") {
|
|
374
|
+
this.xlinkPrefixes.add(key2.slice(6));
|
|
375
|
+
}
|
|
376
|
+
if (key2 === "xmlns:cy" || key2.startsWith("cy:")) {
|
|
377
|
+
this.cytoscape = true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
this.rootPrefix = prefix;
|
|
381
|
+
const nsKey = prefix === null ? "xmlns" : `xmlns:${prefix}`;
|
|
382
|
+
this.xgmmlNamespace = rawAttrs.get(nsKey) === XGMML_NAMESPACE;
|
|
383
|
+
if (local !== "graph") {
|
|
384
|
+
this.report.fail(
|
|
385
|
+
XGMML_ISSUE.NO_GRAPH,
|
|
386
|
+
`the root element is <${rawName}>, not <graph>; an XGMML graph embedded in another document is not read`,
|
|
387
|
+
{ line }
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const graph = this.newGraph(this.normalise(rawAttrs), line, null, null);
|
|
391
|
+
this.rootGraph = graph;
|
|
392
|
+
this.frames.push({ kind: "graph", graph });
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* A graph record, registered in the document.
|
|
396
|
+
* @param attrs - its attributes
|
|
397
|
+
* @param line - the line
|
|
398
|
+
* @param owner - the node whose att holds it, or null
|
|
399
|
+
* @param parent - the graph it is nested in, or null
|
|
400
|
+
* @returns the record
|
|
401
|
+
*/
|
|
402
|
+
newGraph(attrs, line, owner, parent) {
|
|
403
|
+
const graph = {
|
|
404
|
+
id: attrs.get("id") ?? null,
|
|
405
|
+
label: attrs.get("label") ?? null,
|
|
406
|
+
href: attrs.get("xlink:href") ?? null,
|
|
407
|
+
registered: attrs.get("cy:registered") ?? null,
|
|
408
|
+
attrs,
|
|
409
|
+
atts: [],
|
|
410
|
+
graphics: null,
|
|
411
|
+
owner,
|
|
412
|
+
parent,
|
|
413
|
+
members: [],
|
|
414
|
+
subgraphs: [],
|
|
415
|
+
line
|
|
416
|
+
};
|
|
417
|
+
if (!this.cytoscape) {
|
|
418
|
+
for (const key2 of attrs.keys()) {
|
|
419
|
+
if (key2.startsWith("cy:")) {
|
|
420
|
+
this.cytoscape = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.graphs.push(graph);
|
|
425
|
+
return graph;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* A child of `<graph>`.
|
|
429
|
+
* @param graph - the graph
|
|
430
|
+
* @param rawName - the name as written
|
|
431
|
+
* @param name - its local name
|
|
432
|
+
* @param attrs - its attributes
|
|
433
|
+
* @param line - the line
|
|
434
|
+
*/
|
|
435
|
+
startInGraph(graph, rawName, name, attrs, line) {
|
|
436
|
+
switch (name) {
|
|
437
|
+
case "node":
|
|
438
|
+
this.beginNode(graph, attrs, line);
|
|
439
|
+
return;
|
|
440
|
+
case "edge":
|
|
441
|
+
this.beginEdge(graph, attrs, line);
|
|
442
|
+
return;
|
|
443
|
+
case "att": {
|
|
444
|
+
const att = this.newAtt(attrs, line);
|
|
445
|
+
graph.atts.push(att);
|
|
446
|
+
this.beginAtt(att, attrs, "graph");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
case "graphics":
|
|
450
|
+
this.beginGraphics(graph, attrs);
|
|
451
|
+
return;
|
|
452
|
+
default:
|
|
453
|
+
this.unknownElement(rawName, line);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* A child of `<node>` or `<edge>`.
|
|
458
|
+
* @param element - the node or edge
|
|
459
|
+
* @param rawName - the name as written
|
|
460
|
+
* @param name - its local name
|
|
461
|
+
* @param attrs - its attributes
|
|
462
|
+
* @param line - the line
|
|
463
|
+
*/
|
|
464
|
+
startInElement(element, rawName, name, attrs, line) {
|
|
465
|
+
switch (name) {
|
|
466
|
+
case "att": {
|
|
467
|
+
const att = this.newAtt(attrs, line);
|
|
468
|
+
element.atts.push(att);
|
|
469
|
+
this.beginAtt(att, attrs, element.kind);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
case "graphics":
|
|
473
|
+
this.beginGraphics(element, attrs);
|
|
474
|
+
return;
|
|
475
|
+
default:
|
|
476
|
+
this.unknownElement(rawName, line);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* A child of `<att>`: a child att, a nested graph, or foreign XML.
|
|
481
|
+
* @param frame - the att frame
|
|
482
|
+
* @param rawName - the name as written
|
|
483
|
+
* @param name - its local name
|
|
484
|
+
* @param attrs - its attributes
|
|
485
|
+
* @param line - the line
|
|
486
|
+
*/
|
|
487
|
+
startInAtt(frame, rawName, name, attrs, line) {
|
|
488
|
+
const { att } = frame;
|
|
489
|
+
const xgmml = this.isXgmmlName(rawName);
|
|
490
|
+
if (name === "att" && xgmml) {
|
|
491
|
+
const child = this.newAtt(attrs, line);
|
|
492
|
+
att.children.push(child);
|
|
493
|
+
this.beginAtt(child, attrs, frame.owner === "graphics" ? "graphics" : "att");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (name === "graph" && xgmml) {
|
|
497
|
+
att.hasGraph = true;
|
|
498
|
+
this.beginNestedGraph(frame, attrs, line);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
att.xml ??= "";
|
|
502
|
+
const capture = { kind: "capture", att, depth: 0 };
|
|
503
|
+
this.frames.push(capture);
|
|
504
|
+
this.capture(capture, rawName, attrs);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Whether an element name inside an att is XGMML's own (unprefixed, or with the root's prefix,
|
|
508
|
+
* as in a document whose root is `<xgmml:graph>`) rather than foreign XML such as `rdf:RDF`.
|
|
509
|
+
* @param rawName - the name as written
|
|
510
|
+
* @returns true for an XGMML element name
|
|
511
|
+
*/
|
|
512
|
+
isXgmmlName(rawName) {
|
|
513
|
+
const colon = rawName.indexOf(":");
|
|
514
|
+
return colon < 0 || this.rootPrefix !== null && rawName.slice(0, colon) === this.rootPrefix;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* A `<graph>` inside an `<att>`: a subnetwork of the root, a group or nested-network pointer
|
|
518
|
+
* of a node, or (in an edge) a graph with no model.
|
|
519
|
+
* @param frame - the att frame
|
|
520
|
+
* @param attrs - the graph's attributes
|
|
521
|
+
* @param line - the line
|
|
522
|
+
*/
|
|
523
|
+
beginNestedGraph(frame, attrs, line) {
|
|
524
|
+
const holder = this.attHolder();
|
|
525
|
+
if (holder === null || holder.kind === "edge" || frame.owner === "graphics") {
|
|
526
|
+
this.report.warnOnce(
|
|
527
|
+
"unsupported",
|
|
528
|
+
XGMML_ISSUE.EDGE_NESTED_GRAPH,
|
|
529
|
+
"a graph nested in an edge's (or a graphics) att has no model and was skipped with its content",
|
|
530
|
+
{ line }
|
|
531
|
+
);
|
|
532
|
+
this.frames.push({ kind: "skip" });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (holder.kind === "node") {
|
|
536
|
+
const graph2 = this.newGraph(attrs, line, holder.node, holder.node.graph);
|
|
537
|
+
holder.node.nested.push(graph2);
|
|
538
|
+
this.frames.push({ kind: "graph", graph: graph2 });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const graph = this.newGraph(attrs, line, null, holder.graph);
|
|
542
|
+
holder.graph.subgraphs.push(graph);
|
|
543
|
+
this.frames.push({ kind: "graph", graph });
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* The innermost node, edge or graph frame below the open atts.
|
|
547
|
+
* @returns the frame, or null
|
|
548
|
+
*/
|
|
549
|
+
attHolder() {
|
|
550
|
+
for (let i = this.frames.length - 1; i >= 0; i--) {
|
|
551
|
+
const frame = this.frames[i];
|
|
552
|
+
if (frame.kind === "graph" || frame.kind === "node" || frame.kind === "edge") {
|
|
553
|
+
return frame;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Report an element XGMML does not define here and skip its subtree.
|
|
560
|
+
* @param rawName - the name as written
|
|
561
|
+
* @param line - the line
|
|
562
|
+
*/
|
|
563
|
+
unknownElement(rawName, line) {
|
|
564
|
+
this.report.warnOnce(
|
|
565
|
+
"unsupported",
|
|
566
|
+
XGMML_ISSUE.UNKNOWN_ELEMENT,
|
|
567
|
+
`element <${rawName}> is not XGMML here and was skipped with its content`,
|
|
568
|
+
{ line, element: rawName },
|
|
569
|
+
`${XGMML_ISSUE.UNKNOWN_ELEMENT}:${rawName}`
|
|
570
|
+
);
|
|
571
|
+
this.frames.push({ kind: "skip" });
|
|
572
|
+
}
|
|
573
|
+
// ------------------------------------------------------------------ nodes and edges
|
|
574
|
+
/**
|
|
575
|
+
* Open a `<node>`.
|
|
576
|
+
* @param graph - the graph it is in
|
|
577
|
+
* @param attrs - its attributes
|
|
578
|
+
* @param line - the line
|
|
579
|
+
*/
|
|
580
|
+
beginNode(graph, attrs, line) {
|
|
581
|
+
const href = attrs.get("xlink:href") ?? null;
|
|
582
|
+
let id = attrs.get("id") ?? null;
|
|
583
|
+
const labelAttr = attrs.get("label") ?? null;
|
|
584
|
+
const nameAttr = attrs.get("name") ?? null;
|
|
585
|
+
const label = labelAttr ?? nameAttr;
|
|
586
|
+
if (href === null && id === null) {
|
|
587
|
+
if (label === null || label.length === 0) {
|
|
588
|
+
this.report.counts.skippedNodes++;
|
|
589
|
+
this.report.error("missing-value", XGMML_ISSUE.MISSING_ID, "<node> without an id or a label", {
|
|
590
|
+
line
|
|
591
|
+
});
|
|
592
|
+
this.frames.push({ kind: "skip" });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
this.report.warning(
|
|
596
|
+
"validation-error",
|
|
597
|
+
XGMML_ISSUE.ID_FROM_LABEL,
|
|
598
|
+
`<node> without an id: its label "${label}" is used as the id`,
|
|
599
|
+
{ line, element: label }
|
|
600
|
+
);
|
|
601
|
+
id = label;
|
|
602
|
+
}
|
|
603
|
+
const node = {
|
|
604
|
+
kind: "node",
|
|
605
|
+
id: href === null ? id : null,
|
|
606
|
+
href,
|
|
607
|
+
label,
|
|
608
|
+
name: labelAttr !== null ? nameAttr : null,
|
|
609
|
+
xmlAttrs: otherAttributes(attrs, NODE_READ),
|
|
610
|
+
atts: [],
|
|
611
|
+
graphics: null,
|
|
612
|
+
x: null,
|
|
613
|
+
y: null,
|
|
614
|
+
z: null,
|
|
615
|
+
nested: [],
|
|
616
|
+
graph,
|
|
617
|
+
line,
|
|
618
|
+
viewId: attrs.get("cy:nodeId") ?? null
|
|
619
|
+
};
|
|
620
|
+
graph.members.push(node);
|
|
621
|
+
if (href === null) {
|
|
622
|
+
this.nodes.push(node);
|
|
623
|
+
}
|
|
624
|
+
this.frames.push({ kind: "node", node });
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Open an `<edge>`.
|
|
628
|
+
* @param graph - the graph it is in
|
|
629
|
+
* @param attrs - its attributes
|
|
630
|
+
* @param line - the line
|
|
631
|
+
*/
|
|
632
|
+
beginEdge(graph, attrs, line) {
|
|
633
|
+
const href = attrs.get("xlink:href") ?? null;
|
|
634
|
+
const edge = {
|
|
635
|
+
kind: "edge",
|
|
636
|
+
id: href === null ? attrs.get("id") ?? null : null,
|
|
637
|
+
href,
|
|
638
|
+
source: attrs.get("source") ?? null,
|
|
639
|
+
target: attrs.get("target") ?? null,
|
|
640
|
+
label: attrs.get("label") ?? null,
|
|
641
|
+
name: attrs.get("name") ?? null,
|
|
642
|
+
weight: attrs.get("weight") ?? null,
|
|
643
|
+
directed: attrs.get("cy:directed") ?? null,
|
|
644
|
+
xmlAttrs: otherAttributes(attrs, EDGE_READ),
|
|
645
|
+
atts: [],
|
|
646
|
+
graphics: null,
|
|
647
|
+
graph,
|
|
648
|
+
line,
|
|
649
|
+
viewId: attrs.get("cy:edgeId") ?? null
|
|
650
|
+
};
|
|
651
|
+
graph.members.push(edge);
|
|
652
|
+
if (href === null) {
|
|
653
|
+
this.edges.push(edge);
|
|
654
|
+
}
|
|
655
|
+
this.frames.push({ kind: "edge", edge });
|
|
656
|
+
}
|
|
657
|
+
// ------------------------------------------------------------------ atts
|
|
658
|
+
/**
|
|
659
|
+
* An att record from its XML attributes.
|
|
660
|
+
* @param attrs - the attributes
|
|
661
|
+
* @param line - the line
|
|
662
|
+
* @returns the record
|
|
663
|
+
*/
|
|
664
|
+
newAtt(attrs, line) {
|
|
665
|
+
const extra = {};
|
|
666
|
+
for (const [key2, value] of attrs) {
|
|
667
|
+
if (!ATT_READ.has(key2) && key2 !== "xmlns" && !key2.startsWith("xmlns:")) {
|
|
668
|
+
extra[key2] = value;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
name: attrs.get("name") ?? attrs.get("label") ?? null,
|
|
673
|
+
type: attrs.get("type") ?? null,
|
|
674
|
+
cyType: attrs.get("cy:type") ?? null,
|
|
675
|
+
elementType: attrs.get("cy:elementType") ?? null,
|
|
676
|
+
value: attrs.get("value") ?? null,
|
|
677
|
+
hidden: isCyTrue(attrs.get("cy:hidden")),
|
|
678
|
+
equation: isCyTrue(attrs.get("cy:equation")),
|
|
679
|
+
children: [],
|
|
680
|
+
extra,
|
|
681
|
+
xml: null,
|
|
682
|
+
hasGraph: false,
|
|
683
|
+
text: "",
|
|
684
|
+
line
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Open an `<att>`.
|
|
689
|
+
* @param att - the record
|
|
690
|
+
* @param _attrs - its attributes
|
|
691
|
+
* @param owner - what holds it
|
|
692
|
+
*/
|
|
693
|
+
beginAtt(att, _attrs, owner) {
|
|
694
|
+
this.frames.push({ kind: "att", att, owner });
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Serialize one start tag of foreign XML into the capturing att.
|
|
698
|
+
* @param frame - the capture frame
|
|
699
|
+
* @param rawName - the name as written
|
|
700
|
+
* @param attrs - its attributes
|
|
701
|
+
*/
|
|
702
|
+
capture(frame, rawName, attrs) {
|
|
703
|
+
let tag = `<${rawName}`;
|
|
704
|
+
for (const [key2, value] of attrs) {
|
|
705
|
+
tag += ` ${key2}="${escapeXmlAttribute(value)}"`;
|
|
706
|
+
}
|
|
707
|
+
frame.att.xml = `${frame.att.xml ?? ""}${tag}>`;
|
|
708
|
+
frame.depth++;
|
|
709
|
+
if (frame.att.name === "networkMetadata") {
|
|
710
|
+
this.rdfElement = localName(rawName);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Keep the text of an RDF metadata field.
|
|
715
|
+
* @param text - the text
|
|
716
|
+
*/
|
|
717
|
+
captureRdfText(text) {
|
|
718
|
+
if (this.rdfElement !== null && !isWhitespace(text)) {
|
|
719
|
+
this.rdf[this.rdfElement] = (this.rdf[this.rdfElement] ?? "") + text;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// ------------------------------------------------------------------ graphics
|
|
723
|
+
/**
|
|
724
|
+
* Open a `<graphics>`: x, y and z (draft and Cytoscape) are read as coordinates; every other
|
|
725
|
+
* attribute is kept as written, merged key by key with an earlier `<graphics>`.
|
|
726
|
+
* @param target - the node, edge or graph
|
|
727
|
+
* @param attrs - its attributes
|
|
728
|
+
*/
|
|
729
|
+
beginGraphics(target, attrs) {
|
|
730
|
+
const graphics = { ...target.graphics ?? {} };
|
|
731
|
+
for (const [key2, value] of attrs) {
|
|
732
|
+
if ("x" in target && (key2 === "x" || key2 === "y" || key2 === "z")) {
|
|
733
|
+
target[key2] = value;
|
|
734
|
+
} else if (key2 !== "xmlns" && !key2.startsWith("xmlns:")) {
|
|
735
|
+
graphics[key2] = value;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
target.graphics = graphics;
|
|
739
|
+
this.frames.push({ kind: "graphics", target });
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* A child of `<graphics>`: a visual-property att, a `<center>` or a `<Line>`.
|
|
743
|
+
* @param target - the graphics target
|
|
744
|
+
* @param rawName - the name as written
|
|
745
|
+
* @param name - its local name
|
|
746
|
+
* @param attrs - its attributes
|
|
747
|
+
* @param line - the line
|
|
748
|
+
*/
|
|
749
|
+
startInGraphics(target, rawName, name, attrs, line) {
|
|
750
|
+
switch (name) {
|
|
751
|
+
case "att": {
|
|
752
|
+
this.frames.push({ kind: "att", att: this.newAtt(attrs, line), owner: "graphics" });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
case "center":
|
|
756
|
+
if ("x" in target) {
|
|
757
|
+
target.x ??= attrs.get("x") ?? null;
|
|
758
|
+
target.y ??= attrs.get("y") ?? null;
|
|
759
|
+
target.z ??= attrs.get("z") ?? null;
|
|
760
|
+
}
|
|
761
|
+
this.frames.push({ kind: "skip" });
|
|
762
|
+
return;
|
|
763
|
+
case "Line":
|
|
764
|
+
case "line":
|
|
765
|
+
this.frames.push({ kind: "line", points: [] });
|
|
766
|
+
return;
|
|
767
|
+
default:
|
|
768
|
+
this.unknownElement(rawName, line);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Keep a visual-property att of a `<graphics>` in its record, as written. A session view's
|
|
773
|
+
* `<att name="z">` repeats z and is read as z when the attributes gave none.
|
|
774
|
+
* @param target - the graphics target
|
|
775
|
+
* @param att - the att
|
|
776
|
+
*/
|
|
777
|
+
graphicsAtt(target, att) {
|
|
778
|
+
if (att.name === "z" && "z" in target && att.children.length === 0) {
|
|
779
|
+
target.z ??= att.value;
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (att.name === null) {
|
|
783
|
+
this.report.warnOnce(
|
|
784
|
+
"validation-error",
|
|
785
|
+
XGMML_ISSUE.BAD_ATT,
|
|
786
|
+
"a graphics <att> without a name was skipped",
|
|
787
|
+
{ line: att.line },
|
|
788
|
+
`${XGMML_ISSUE.BAD_ATT}:graphics-name`
|
|
789
|
+
);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
target.graphics = { ...target.graphics ?? {}, [att.name]: attJson(att) };
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function isListLike(att) {
|
|
796
|
+
const names = new Set(att.children.map((c) => c.name));
|
|
797
|
+
if (names.size !== 1) {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
const [only] = names;
|
|
801
|
+
return only === null || only === att.name || att.children.length > 1;
|
|
802
|
+
}
|
|
803
|
+
function attJson(att) {
|
|
804
|
+
if (att.children.length === 0) {
|
|
805
|
+
if (att.value !== null) {
|
|
806
|
+
return att.value;
|
|
807
|
+
}
|
|
808
|
+
return Object.keys(att.extra).length > 0 ? { ...att.extra } : null;
|
|
809
|
+
}
|
|
810
|
+
if (isListLike(att)) {
|
|
811
|
+
return att.children.map(attJson);
|
|
812
|
+
}
|
|
813
|
+
const out = {};
|
|
814
|
+
for (const child of att.children) {
|
|
815
|
+
out[child.name ?? ""] = attJson(child);
|
|
816
|
+
}
|
|
817
|
+
return out;
|
|
818
|
+
}
|
|
819
|
+
function otherAttributes(attrs, read) {
|
|
820
|
+
const out = [];
|
|
821
|
+
for (const [key2, value] of attrs) {
|
|
822
|
+
if (!read.has(key2) && key2 !== "xmlns" && !key2.startsWith("xmlns:")) {
|
|
823
|
+
out.push([key2, value]);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return out;
|
|
827
|
+
}
|
|
828
|
+
const CY_TYPES = {
|
|
829
|
+
string: "string",
|
|
830
|
+
double: "real",
|
|
831
|
+
integer: "int",
|
|
832
|
+
long: "long",
|
|
833
|
+
boolean: "bool",
|
|
834
|
+
list: "list"
|
|
835
|
+
};
|
|
836
|
+
const XGMML_TYPES = {
|
|
837
|
+
string: "string",
|
|
838
|
+
real: "real",
|
|
839
|
+
integer: "int",
|
|
840
|
+
boolean: "bool",
|
|
841
|
+
list: "list",
|
|
842
|
+
map: "map"
|
|
843
|
+
};
|
|
844
|
+
function attType(att) {
|
|
845
|
+
let kind;
|
|
846
|
+
let declared = null;
|
|
847
|
+
if (att.cyType !== null && att.cyType.length > 0) {
|
|
848
|
+
kind = CY_TYPES[att.cyType.trim().toLowerCase()];
|
|
849
|
+
declared = att.cyType;
|
|
850
|
+
}
|
|
851
|
+
if (kind === void 0 && att.type !== null && att.type.length > 0) {
|
|
852
|
+
kind = XGMML_TYPES[att.type.trim().toLowerCase()];
|
|
853
|
+
declared = att.type;
|
|
854
|
+
}
|
|
855
|
+
if (kind === void 0) {
|
|
856
|
+
const unknown = declared ?? null;
|
|
857
|
+
return { kind: "string", declared, unknown };
|
|
858
|
+
}
|
|
859
|
+
if (kind === "int" && att.cyType === null && att.name?.endsWith(".SUID") === true) {
|
|
860
|
+
kind = "long";
|
|
861
|
+
}
|
|
862
|
+
return { kind, declared, unknown: null };
|
|
863
|
+
}
|
|
864
|
+
function elementKind(text) {
|
|
865
|
+
if (text === null) {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
const kind = CY_TYPES[text.trim().toLowerCase()] ?? XGMML_TYPES[text.trim().toLowerCase()];
|
|
869
|
+
return kind === void 0 || kind === "list" || kind === "map" ? null : kind;
|
|
870
|
+
}
|
|
871
|
+
function widenScalar(a, b) {
|
|
872
|
+
if (a === b) {
|
|
873
|
+
return a;
|
|
874
|
+
}
|
|
875
|
+
if (a === "string" || b === "string" || a === "bool" || b === "bool") {
|
|
876
|
+
return "string";
|
|
877
|
+
}
|
|
878
|
+
if (a === "real" || b === "real") {
|
|
879
|
+
return "real";
|
|
880
|
+
}
|
|
881
|
+
return "long";
|
|
882
|
+
}
|
|
883
|
+
const INTEGER_TEXT = /^[+-]?[0-9]+$/;
|
|
884
|
+
const REAL_TEXT = /^[+-]?(NaN|Infinity|([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?)$/;
|
|
885
|
+
const I32_MIN = -2147483648;
|
|
886
|
+
const I32_MAX = 2147483647;
|
|
887
|
+
function parseScalar(text, kind, unescape) {
|
|
888
|
+
switch (kind) {
|
|
889
|
+
case "string":
|
|
890
|
+
return { value: unescape ? unescapeCytoscape(text) : text };
|
|
891
|
+
case "bool": {
|
|
892
|
+
const t = text.trim().toLowerCase();
|
|
893
|
+
if (t === "1" || t === "true" || t === "yes") {
|
|
894
|
+
return { value: true };
|
|
895
|
+
}
|
|
896
|
+
if (t === "0" || t === "false" || t === "no") {
|
|
897
|
+
return { value: false };
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
case "int":
|
|
902
|
+
case "long": {
|
|
903
|
+
const t = text.trim();
|
|
904
|
+
if (!INTEGER_TEXT.test(t)) {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
const n = Number(t);
|
|
908
|
+
if (kind === "int" && (n < I32_MIN || n > I32_MAX)) {
|
|
909
|
+
return { value: n, overflow: Number.isSafeInteger(n) ? "i32" : "precision" };
|
|
910
|
+
}
|
|
911
|
+
return { value: n, overflow: Number.isSafeInteger(n) ? void 0 : "precision" };
|
|
912
|
+
}
|
|
913
|
+
case "real": {
|
|
914
|
+
const t = text.trim();
|
|
915
|
+
if (!REAL_TEXT.test(t)) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
const n = Number(t);
|
|
919
|
+
const overflow = !Number.isFinite(n) && !/Infinity|NaN/.test(t) ? "precision" : void 0;
|
|
920
|
+
return { value: n, overflow };
|
|
921
|
+
}
|
|
922
|
+
default: {
|
|
923
|
+
const name = kind;
|
|
924
|
+
throw new Error(`unknown scalar kind ${String(name)}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function unescapeCytoscape(text) {
|
|
929
|
+
return text.includes("\\") ? text.replace(/\\t/g, " ").replace(/\\n/g, "\n") : text;
|
|
930
|
+
}
|
|
931
|
+
const DTYPES = {
|
|
932
|
+
bool: "bool",
|
|
933
|
+
int: "i32",
|
|
934
|
+
long: "f64",
|
|
935
|
+
real: "f64",
|
|
936
|
+
string: "string"
|
|
937
|
+
};
|
|
938
|
+
class ColumnSet {
|
|
939
|
+
/**
|
|
940
|
+
* Create a column set.
|
|
941
|
+
* @param domain - the table
|
|
942
|
+
* @param report - the report
|
|
943
|
+
* @param options - the options
|
|
944
|
+
*/
|
|
945
|
+
constructor(domain, report, options) {
|
|
946
|
+
this.plans = /* @__PURE__ */ new Map();
|
|
947
|
+
this.domain = domain;
|
|
948
|
+
this.report = report;
|
|
949
|
+
this.options = options;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Whether a column of that name (any namespace) was collected.
|
|
953
|
+
* @param name - the name
|
|
954
|
+
* @returns true when some row has a value or the column was declared
|
|
955
|
+
*/
|
|
956
|
+
has(name) {
|
|
957
|
+
return this.plans.has(key(null, name));
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Add one att of a row: parse it by its declared kind (E_BAD_VALUE for a value that does not
|
|
961
|
+
* parse; the cell stays unset). A second value of the same column on one row wins with
|
|
962
|
+
* W_DUPLICATE_ATTRIBUTE.
|
|
963
|
+
* @param row - the row
|
|
964
|
+
* @param att - the att
|
|
965
|
+
* @param where - the element, for issues
|
|
966
|
+
*/
|
|
967
|
+
addAtt(row, att, where) {
|
|
968
|
+
const { name } = att;
|
|
969
|
+
if (name === null || name.length === 0) {
|
|
970
|
+
this.badAtt("an <att> without a name was skipped", att, "name");
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const plan = this.plan(name, att.namespace ?? null);
|
|
974
|
+
const type = attType(att);
|
|
975
|
+
plan.hidden ||= att.hidden;
|
|
976
|
+
plan.suid ||= name.endsWith(".SUID");
|
|
977
|
+
if (type.declared !== null) {
|
|
978
|
+
plan.declaredType ??= type.declared;
|
|
979
|
+
}
|
|
980
|
+
const at = { line: att.line, element: where.element ?? name };
|
|
981
|
+
if (type.unknown !== null) {
|
|
982
|
+
this.report.warnOnce(
|
|
983
|
+
"unsupported",
|
|
984
|
+
XGMML_ISSUE.UNKNOWN_ATTR_TYPE,
|
|
985
|
+
`att type "${type.unknown}" of "${name}" is not an XGMML or Cytoscape type; its values are kept as text`,
|
|
986
|
+
at,
|
|
987
|
+
`${XGMML_ISSUE.UNKNOWN_ATTR_TYPE}:${this.domain}:${name}`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
let cell;
|
|
991
|
+
if (att.equation) {
|
|
992
|
+
plan.equation = true;
|
|
993
|
+
this.report.warnOnce(
|
|
994
|
+
"unsupported",
|
|
995
|
+
XGMML_ISSUE.EQUATION_AS_TEXT,
|
|
996
|
+
`"${name}" holds Cytoscape formulas; each is kept as its text, never evaluated`,
|
|
997
|
+
at,
|
|
998
|
+
`${XGMML_ISSUE.EQUATION_AS_TEXT}:${this.domain}:${name}`
|
|
999
|
+
);
|
|
1000
|
+
cell = att.value === null ? null : scalarCell("string", att.value, att.value, true);
|
|
1001
|
+
if (cell !== null) {
|
|
1002
|
+
plan.declared.add("string");
|
|
1003
|
+
}
|
|
1004
|
+
} else if (type.kind === "list") {
|
|
1005
|
+
cell = this.listCell(att, name, plan, at);
|
|
1006
|
+
plan.declared.add("list");
|
|
1007
|
+
} else if (type.kind === "map") {
|
|
1008
|
+
cell = jsonCell(recordOf(att));
|
|
1009
|
+
this.recordList(`"${name}" is a 2.x map; it is kept as a json record`, at);
|
|
1010
|
+
plan.declared.add("json");
|
|
1011
|
+
} else {
|
|
1012
|
+
cell = this.scalarAtt(att, type.kind, plan, at);
|
|
1013
|
+
}
|
|
1014
|
+
if (cell !== null) {
|
|
1015
|
+
this.set(plan, row, cell, at);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Add one value read by the 5.1 text grammar (an XML attribute of an element).
|
|
1020
|
+
* @param row - the row
|
|
1021
|
+
* @param name - the column name
|
|
1022
|
+
* @param text - the text
|
|
1023
|
+
* @param where - the element, for issues
|
|
1024
|
+
*/
|
|
1025
|
+
addText(row, name, text, where) {
|
|
1026
|
+
const plan = this.plan(name, null);
|
|
1027
|
+
const value = parseTextCell(text);
|
|
1028
|
+
const kind = textKind(inferTextDtype(text));
|
|
1029
|
+
this.set(plan, row, { kind, value, text, items: [], itemKind: null, typed: false }, where);
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Add one value of a known kind (a typed table cell of a session).
|
|
1033
|
+
* @param row - the row
|
|
1034
|
+
* @param name - the column name
|
|
1035
|
+
* @param value - the value
|
|
1036
|
+
* @param where - the element, for issues
|
|
1037
|
+
*/
|
|
1038
|
+
addJson(row, name, value, where) {
|
|
1039
|
+
this.set(this.plan(name, null), row, jsonCell(value), where);
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Declare every collected column on the sink and write its values.
|
|
1043
|
+
* @param sink - the sink
|
|
1044
|
+
* @param rowIndex - the sink index of a row (node or edge index), or -1 when the row was not added
|
|
1045
|
+
*/
|
|
1046
|
+
write(sink, rowIndex) {
|
|
1047
|
+
const taken = new Set(this.options.reserved);
|
|
1048
|
+
const ordered = [...this.plans.values()].sort(
|
|
1049
|
+
(a, b) => Number(a.namespace !== null) - Number(b.namespace !== null)
|
|
1050
|
+
);
|
|
1051
|
+
for (const plan of ordered) {
|
|
1052
|
+
let { name } = plan;
|
|
1053
|
+
if (taken.has(name)) {
|
|
1054
|
+
name = uniqueColumnName(plan.name, plan.namespace, (n) => taken.has(n));
|
|
1055
|
+
this.report.warning(
|
|
1056
|
+
"coercion",
|
|
1057
|
+
XGMML_ISSUE.COLUMN_RENAMED,
|
|
1058
|
+
`${this.domain} attribute "${plan.name}" renamed to "${name}": the name was taken`,
|
|
1059
|
+
{ element: plan.name }
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
taken.add(name);
|
|
1063
|
+
this.writePlan(sink, plan, name, rowIndex);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
// ------------------------------------------------------------------ cells
|
|
1067
|
+
/**
|
|
1068
|
+
* The plan of a column, created on first use.
|
|
1069
|
+
* @param name - the attribute name
|
|
1070
|
+
* @param namespace - the table namespace, or null
|
|
1071
|
+
* @returns the plan
|
|
1072
|
+
*/
|
|
1073
|
+
plan(name, namespace) {
|
|
1074
|
+
const k = key(namespace, name);
|
|
1075
|
+
let plan = this.plans.get(k);
|
|
1076
|
+
if (plan === void 0) {
|
|
1077
|
+
plan = {
|
|
1078
|
+
name,
|
|
1079
|
+
namespace,
|
|
1080
|
+
cells: /* @__PURE__ */ new Map(),
|
|
1081
|
+
declared: /* @__PURE__ */ new Set(),
|
|
1082
|
+
lines: /* @__PURE__ */ new Map(),
|
|
1083
|
+
declaredType: null,
|
|
1084
|
+
hidden: false,
|
|
1085
|
+
equation: false,
|
|
1086
|
+
suid: false,
|
|
1087
|
+
overflow: false,
|
|
1088
|
+
precision: false
|
|
1089
|
+
};
|
|
1090
|
+
this.plans.set(k, plan);
|
|
1091
|
+
}
|
|
1092
|
+
return plan;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Store a cell, reporting a second value on the same row.
|
|
1096
|
+
* @param plan - the column
|
|
1097
|
+
* @param row - the row
|
|
1098
|
+
* @param cell - the cell
|
|
1099
|
+
* @param where - the location
|
|
1100
|
+
*/
|
|
1101
|
+
set(plan, row, cell, where) {
|
|
1102
|
+
if (plan.cells.has(row)) {
|
|
1103
|
+
this.report.warning(
|
|
1104
|
+
"validation-error",
|
|
1105
|
+
XGMML_ISSUE.DUPLICATE_ATTRIBUTE,
|
|
1106
|
+
`"${plan.name}" is given twice on one ${this.domain}; the later value is kept`,
|
|
1107
|
+
{ line: where.line ?? null, element: where.element ?? plan.name }
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
plan.cells.set(row, cell);
|
|
1111
|
+
if (where.line !== void 0 && where.line !== null) {
|
|
1112
|
+
plan.lines.set(row, where.line);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Parse a scalar att.
|
|
1117
|
+
* @param att - the att
|
|
1118
|
+
* @param kind - the declared kind
|
|
1119
|
+
* @param plan - the column
|
|
1120
|
+
* @param at - the location
|
|
1121
|
+
* @returns the cell, or null when unset or invalid
|
|
1122
|
+
*/
|
|
1123
|
+
scalarAtt(att, kind, plan, at) {
|
|
1124
|
+
if (att.children.length > 0) {
|
|
1125
|
+
this.badAtt(`the ${kind} att "${att.name ?? ""}" holds child atts; they are ignored`, att, "children");
|
|
1126
|
+
}
|
|
1127
|
+
if (att.xml !== null) {
|
|
1128
|
+
this.recordList(`"${att.name ?? ""}" holds foreign XML; it is kept as text in a json column`, at);
|
|
1129
|
+
plan.declared.add("json");
|
|
1130
|
+
return jsonCell(att.xml);
|
|
1131
|
+
}
|
|
1132
|
+
if (att.value === null) {
|
|
1133
|
+
plan.declared.add(kind);
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
const parsed = parseScalar(att.value, kind, this.options.unescape);
|
|
1137
|
+
if (parsed === null || parsed.overflow !== void 0 && kind === "int" && exactInteger(att.cyType)) {
|
|
1138
|
+
this.badValue(att.value, kind, at);
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
plan.declared.add(kind);
|
|
1142
|
+
if (parsed.overflow === "i32") {
|
|
1143
|
+
plan.overflow = true;
|
|
1144
|
+
} else if (parsed.overflow === "precision") {
|
|
1145
|
+
plan.precision = true;
|
|
1146
|
+
plan.overflow ||= kind === "int";
|
|
1147
|
+
}
|
|
1148
|
+
return scalarCell(kind, parsed.value, att.value, true);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Parse a list att: items by `cy:elementType`, else by the widened kinds of the children.
|
|
1152
|
+
* A list of lists, a list with records, or a list of named fields is a json value.
|
|
1153
|
+
* @param att - the att
|
|
1154
|
+
* @param name - the column name
|
|
1155
|
+
* @param plan - the column (an item beyond i32 widens it, as a scalar does)
|
|
1156
|
+
* @param at - the location
|
|
1157
|
+
* @returns the cell
|
|
1158
|
+
*/
|
|
1159
|
+
listCell(att, name, plan, at) {
|
|
1160
|
+
if (att.value !== null) {
|
|
1161
|
+
this.badAtt(`the list att "${name}" has a value; it is ignored`, att, "list-value");
|
|
1162
|
+
}
|
|
1163
|
+
const { children } = att;
|
|
1164
|
+
const names = new Set(children.map((c) => c.name).filter((n) => n !== null && n !== name));
|
|
1165
|
+
if (names.size > 1 || children.some((c) => c.children.length > 0 || c.xml !== null)) {
|
|
1166
|
+
this.recordList(`"${name}" is a record or a list of lists; it is kept as json`, at);
|
|
1167
|
+
return jsonCell(names.size > 1 ? recordOf(att) : children.map(jsonOfAtt));
|
|
1168
|
+
}
|
|
1169
|
+
let itemKind = elementKind(att.elementType);
|
|
1170
|
+
if (itemKind === null) {
|
|
1171
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
1172
|
+
for (const child of children) {
|
|
1173
|
+
const type = attType(child);
|
|
1174
|
+
const kind = type.kind === "list" || type.kind === "map" ? "string" : type.kind;
|
|
1175
|
+
kinds.add(kind);
|
|
1176
|
+
itemKind = itemKind === null ? kind : widenScalar(itemKind, kind);
|
|
1177
|
+
}
|
|
1178
|
+
if (kinds.size > 1) {
|
|
1179
|
+
this.report.warnOnce(
|
|
1180
|
+
"coercion",
|
|
1181
|
+
XGMML_ISSUE.WIDENED,
|
|
1182
|
+
`the items of list "${name}" declare different types; they are read as ${itemKind ?? "string"}`,
|
|
1183
|
+
at,
|
|
1184
|
+
`${XGMML_ISSUE.WIDENED}:${this.domain}:${name}`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const items = [];
|
|
1189
|
+
for (const child of children) {
|
|
1190
|
+
if (child.value === null) {
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
const parsed = parseScalar(child.value, itemKind ?? "string", this.options.unescape);
|
|
1194
|
+
const exact = exactInteger(att.elementType) || exactInteger(child.cyType);
|
|
1195
|
+
if (parsed === null || parsed.overflow !== void 0 && itemKind === "int" && exact) {
|
|
1196
|
+
this.badValue(child.value, itemKind ?? "string", { line: child.line, element: at.element });
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (parsed.overflow === "i32") {
|
|
1200
|
+
plan.overflow = true;
|
|
1201
|
+
} else if (parsed.overflow === "precision") {
|
|
1202
|
+
plan.precision = true;
|
|
1203
|
+
plan.overflow ||= itemKind === "int";
|
|
1204
|
+
}
|
|
1205
|
+
items.push({ value: parsed.value, text: child.value });
|
|
1206
|
+
}
|
|
1207
|
+
return { kind: "list", value: null, text: "", items, itemKind, typed: true };
|
|
1208
|
+
}
|
|
1209
|
+
// ------------------------------------------------------------------ decision and writing
|
|
1210
|
+
/**
|
|
1211
|
+
* Decide one column's dtype, declare it and write its values.
|
|
1212
|
+
* @param sink - the sink
|
|
1213
|
+
* @param plan - the column
|
|
1214
|
+
* @param name - the name to declare it under
|
|
1215
|
+
* @param rowIndex - the row to sink index map
|
|
1216
|
+
*/
|
|
1217
|
+
writePlan(sink, plan, name, rowIndex) {
|
|
1218
|
+
const cells = [...plan.cells.values()];
|
|
1219
|
+
const decision = this.decide(plan, cells);
|
|
1220
|
+
const decl = {
|
|
1221
|
+
name,
|
|
1222
|
+
dtype: decision.dtype,
|
|
1223
|
+
nullable: true,
|
|
1224
|
+
origin: {
|
|
1225
|
+
format: FORMAT,
|
|
1226
|
+
id: null,
|
|
1227
|
+
title: name === plan.name ? null : plan.name,
|
|
1228
|
+
type: originType(plan, decision),
|
|
1229
|
+
namespace: plan.namespace ?? (plan.hidden ? CYTOSCAPE_ORIGIN_NAMESPACE : null)
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
if (decision.itemDtype !== null) {
|
|
1233
|
+
decl.itemDtype = decision.itemDtype;
|
|
1234
|
+
}
|
|
1235
|
+
const extra = {};
|
|
1236
|
+
if (plan.hidden) {
|
|
1237
|
+
extra.hidden = true;
|
|
1238
|
+
}
|
|
1239
|
+
if (plan.suid) {
|
|
1240
|
+
extra.suidReference = true;
|
|
1241
|
+
}
|
|
1242
|
+
if (plan.equation) {
|
|
1243
|
+
extra.equation = true;
|
|
1244
|
+
}
|
|
1245
|
+
if (Object.keys(extra).length > 0) {
|
|
1246
|
+
decl.extra = extra;
|
|
1247
|
+
}
|
|
1248
|
+
const where = { element: name };
|
|
1249
|
+
if (this.domain === "graph") {
|
|
1250
|
+
const cell = plan.cells.get(0);
|
|
1251
|
+
try {
|
|
1252
|
+
sink.setGraphValue(name, cell === void 0 ? void 0 : convert(cell, decision), decl);
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
this.report.recordError(err, where);
|
|
1255
|
+
}
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
let handle;
|
|
1259
|
+
try {
|
|
1260
|
+
({ handle } = declareResolved(sink, this.domain, decl, this.report, where));
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
this.report.recordError(err, where);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
for (const [row, cell] of plan.cells) {
|
|
1266
|
+
const index = rowIndex(row);
|
|
1267
|
+
if (index < 0) {
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
const value = convert(cell, decision);
|
|
1271
|
+
try {
|
|
1272
|
+
if (this.domain === "node") {
|
|
1273
|
+
sink.setNodeValue(handle, index, value);
|
|
1274
|
+
} else {
|
|
1275
|
+
sink.setEdgeValue(handle, index, value);
|
|
1276
|
+
}
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
this.report.recordError(err, { line: plan.lines.get(row) ?? null, element: name });
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* The dtype of a column from its cells, with the widening and precision warnings.
|
|
1284
|
+
* @param plan - the column
|
|
1285
|
+
* @param cells - its cells
|
|
1286
|
+
* @returns the decision
|
|
1287
|
+
*/
|
|
1288
|
+
decide(plan, cells) {
|
|
1289
|
+
const where = { element: plan.name };
|
|
1290
|
+
const typedKinds = new Set(cells.filter((c) => c.typed).map((c) => c.kind));
|
|
1291
|
+
let widened = typedKinds.size > 1 || plan.declared.size > 1;
|
|
1292
|
+
let decision;
|
|
1293
|
+
if (cells.some((c) => c.kind === "json") || cells.some((c) => c.kind === "list") && cells.some((c) => c.kind !== "list")) {
|
|
1294
|
+
decision = { dtype: "json", itemDtype: null, scalar: null, item: null, long: false };
|
|
1295
|
+
} else if (cells.length > 0 && cells.every((c) => c.kind === "list")) {
|
|
1296
|
+
let item = null;
|
|
1297
|
+
for (const cell of cells) {
|
|
1298
|
+
if (cell.itemKind !== null) {
|
|
1299
|
+
widened ||= item !== null && item !== cell.itemKind;
|
|
1300
|
+
item = item === null ? cell.itemKind : widenScalar(item, cell.itemKind);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (item === null) {
|
|
1304
|
+
this.report.warnOnce(
|
|
1305
|
+
"coercion",
|
|
1306
|
+
XGMML_ISSUE.EMPTY_LIST_TYPE,
|
|
1307
|
+
`list "${plan.name}" has no element type and no items; it is a list of strings`,
|
|
1308
|
+
where,
|
|
1309
|
+
`${XGMML_ISSUE.EMPTY_LIST_TYPE}:${this.domain}:${plan.name}`
|
|
1310
|
+
);
|
|
1311
|
+
item = "string";
|
|
1312
|
+
}
|
|
1313
|
+
if (plan.overflow && item === "int") {
|
|
1314
|
+
item = "real";
|
|
1315
|
+
widened = true;
|
|
1316
|
+
}
|
|
1317
|
+
decision = { dtype: "list", itemDtype: DTYPES[item], scalar: null, item, long: item === "long" };
|
|
1318
|
+
} else {
|
|
1319
|
+
decision = this.decideScalar(plan, cells);
|
|
1320
|
+
widened ||= plan.overflow && decision.scalar === "real" && plan.declared.has("int");
|
|
1321
|
+
}
|
|
1322
|
+
if (widened) {
|
|
1323
|
+
this.report.warnOnce(
|
|
1324
|
+
"coercion",
|
|
1325
|
+
XGMML_ISSUE.WIDENED,
|
|
1326
|
+
`${this.domain} column "${plan.name}" was widened to ${decision.dtype}: its values or declared types disagree`,
|
|
1327
|
+
where,
|
|
1328
|
+
`${XGMML_ISSUE.WIDENED}:${this.domain}:${plan.name}`
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
if (plan.precision) {
|
|
1332
|
+
this.report.warnOnce(
|
|
1333
|
+
"precision",
|
|
1334
|
+
XGMML_ISSUE.PRECISION,
|
|
1335
|
+
`values of "${plan.name}" beyond what a double holds exactly are stored as the nearest double (or infinity)`,
|
|
1336
|
+
where,
|
|
1337
|
+
`${XGMML_ISSUE.PRECISION}:${this.domain}:${plan.name}`
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
return decision;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* The scalar decision of a column.
|
|
1344
|
+
* @param plan - the column
|
|
1345
|
+
* @param cells - its cells
|
|
1346
|
+
* @returns the decision
|
|
1347
|
+
*/
|
|
1348
|
+
decideScalar(plan, cells) {
|
|
1349
|
+
let scalar = null;
|
|
1350
|
+
for (const cell of cells) {
|
|
1351
|
+
const kind = cell.kind;
|
|
1352
|
+
scalar = scalar === null ? kind : widenScalar(scalar, kind);
|
|
1353
|
+
}
|
|
1354
|
+
if (scalar === null) {
|
|
1355
|
+
const declared = [...plan.declared].find((k) => k in DTYPES);
|
|
1356
|
+
scalar = declared ?? "string";
|
|
1357
|
+
}
|
|
1358
|
+
if (scalar === "int" && plan.overflow) {
|
|
1359
|
+
scalar = "real";
|
|
1360
|
+
}
|
|
1361
|
+
const long = scalar === "long";
|
|
1362
|
+
if (long && this.options.long === "string") {
|
|
1363
|
+
return { dtype: "string", itemDtype: null, scalar: "string", item: null, long: true };
|
|
1364
|
+
}
|
|
1365
|
+
let dtype = DTYPES[scalar];
|
|
1366
|
+
if (scalar === "string") {
|
|
1367
|
+
const heuristic = new DictHeuristic();
|
|
1368
|
+
for (const cell of cells) {
|
|
1369
|
+
if (heuristic.observe(cell.text)) {
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
dtype = heuristic.decide();
|
|
1374
|
+
}
|
|
1375
|
+
return { dtype, itemDtype: null, scalar, item: null, long };
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Report a malformed att, once per kind of defect.
|
|
1379
|
+
* @param message - the message
|
|
1380
|
+
* @param att - the att
|
|
1381
|
+
* @param kind - the defect, for the once key
|
|
1382
|
+
*/
|
|
1383
|
+
badAtt(message, att, kind) {
|
|
1384
|
+
this.report.warnOnce(
|
|
1385
|
+
"parse-error",
|
|
1386
|
+
XGMML_ISSUE.BAD_ATT,
|
|
1387
|
+
message,
|
|
1388
|
+
{ line: att.line, element: att.name },
|
|
1389
|
+
`${XGMML_ISSUE.BAD_ATT}:${kind}`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Report a value that does not parse as its declared kind (an error; the cell stays unset).
|
|
1394
|
+
* @param text - the value
|
|
1395
|
+
* @param kind - the kind
|
|
1396
|
+
* @param at - the location
|
|
1397
|
+
*/
|
|
1398
|
+
badValue(text, kind, at) {
|
|
1399
|
+
this.report.error(
|
|
1400
|
+
"validation-error",
|
|
1401
|
+
XGMML_ISSUE.BAD_VALUE,
|
|
1402
|
+
`value ${JSON.stringify(text)} is not a valid ${KIND_NAMES[kind]}; the cell is left unset`,
|
|
1403
|
+
at
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Report a record list, list of lists, map or foreign XML kept as json, once.
|
|
1408
|
+
* @param message - the message
|
|
1409
|
+
* @param at - the location
|
|
1410
|
+
*/
|
|
1411
|
+
recordList(message, at) {
|
|
1412
|
+
this.report.warnOnce("coercion", XGMML_ISSUE.RECORD_LIST, message, at);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
function exactInteger(cyType) {
|
|
1416
|
+
return cyType?.trim().toLowerCase() === "integer";
|
|
1417
|
+
}
|
|
1418
|
+
function originType(plan, decision) {
|
|
1419
|
+
if (plan.equation) {
|
|
1420
|
+
return "equation";
|
|
1421
|
+
}
|
|
1422
|
+
return decision.long ? "long" : plan.declaredType;
|
|
1423
|
+
}
|
|
1424
|
+
const KIND_NAMES = {
|
|
1425
|
+
bool: "boolean (1, 0, true, false, yes, no)",
|
|
1426
|
+
int: "integer",
|
|
1427
|
+
long: "long integer",
|
|
1428
|
+
real: "real number",
|
|
1429
|
+
string: "string"
|
|
1430
|
+
};
|
|
1431
|
+
function key(namespace, name) {
|
|
1432
|
+
return namespace === null ? name : `${namespace}\0${name}`;
|
|
1433
|
+
}
|
|
1434
|
+
function textKind(dtype) {
|
|
1435
|
+
switch (dtype) {
|
|
1436
|
+
case "bool":
|
|
1437
|
+
return "bool";
|
|
1438
|
+
case "i32":
|
|
1439
|
+
return "int";
|
|
1440
|
+
case "f64":
|
|
1441
|
+
return "real";
|
|
1442
|
+
default:
|
|
1443
|
+
return "string";
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function scalarCell(kind, value, text, typed) {
|
|
1447
|
+
return { kind, value, text, items: [], itemKind: null, typed };
|
|
1448
|
+
}
|
|
1449
|
+
function jsonCell(value) {
|
|
1450
|
+
return { kind: "json", value, text: JSON.stringify(value), items: [], itemKind: null, typed: true };
|
|
1451
|
+
}
|
|
1452
|
+
function recordOf(att) {
|
|
1453
|
+
const out = {};
|
|
1454
|
+
for (const child of att.children) {
|
|
1455
|
+
out[child.name ?? ""] = jsonOfAtt(child);
|
|
1456
|
+
}
|
|
1457
|
+
return out;
|
|
1458
|
+
}
|
|
1459
|
+
function jsonOfAtt(att) {
|
|
1460
|
+
if (att.xml !== null) {
|
|
1461
|
+
return att.xml;
|
|
1462
|
+
}
|
|
1463
|
+
if (att.children.length === 0) {
|
|
1464
|
+
return att.value;
|
|
1465
|
+
}
|
|
1466
|
+
return isListLike(att) ? att.children.map(jsonOfAtt) : recordOf(att);
|
|
1467
|
+
}
|
|
1468
|
+
function convert(cell, decision) {
|
|
1469
|
+
if (decision.dtype === "json") {
|
|
1470
|
+
if (cell.kind === "json") {
|
|
1471
|
+
return cell.value;
|
|
1472
|
+
}
|
|
1473
|
+
return cell.kind === "list" ? cell.items.map((i) => i.value) : cell.value;
|
|
1474
|
+
}
|
|
1475
|
+
if (decision.dtype === "list") {
|
|
1476
|
+
return cell.items.map((item) => convertScalar(item.value, item.text, decision.item ?? "string"));
|
|
1477
|
+
}
|
|
1478
|
+
return convertScalar(
|
|
1479
|
+
cell.value,
|
|
1480
|
+
cell.text,
|
|
1481
|
+
decision.long && decision.dtype === "string" ? "string" : decision.scalar ?? "string"
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
function convertScalar(value, text, kind) {
|
|
1485
|
+
switch (kind) {
|
|
1486
|
+
case "bool":
|
|
1487
|
+
return value;
|
|
1488
|
+
case "int":
|
|
1489
|
+
case "long":
|
|
1490
|
+
case "real":
|
|
1491
|
+
return Number(value);
|
|
1492
|
+
default:
|
|
1493
|
+
return typeof value === "string" ? value : text;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
const META_ATTS = /* @__PURE__ */ new Set(["documentVersion", "networkMetadata"]);
|
|
1497
|
+
const GRAPH_STRUCTURE = /* @__PURE__ */ new Set([
|
|
1498
|
+
"id",
|
|
1499
|
+
"label",
|
|
1500
|
+
"directed",
|
|
1501
|
+
"xlink:href",
|
|
1502
|
+
"cy:documentVersion",
|
|
1503
|
+
"cy:view",
|
|
1504
|
+
"cy:registered",
|
|
1505
|
+
"cy:networkId",
|
|
1506
|
+
"cy:visualStyle",
|
|
1507
|
+
"cy:rendererId"
|
|
1508
|
+
]);
|
|
1509
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
1510
|
+
const ALIAS_SPLIT = /[()]/;
|
|
1511
|
+
function dialectOf(doc, report) {
|
|
1512
|
+
const { root } = doc;
|
|
1513
|
+
let versionText = root.attrs.get("cy:documentVersion") ?? null;
|
|
1514
|
+
for (const att of root.atts) {
|
|
1515
|
+
if (att.name === "documentVersion" && att.value !== null) {
|
|
1516
|
+
versionText ??= att.value;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
let version = 0;
|
|
1520
|
+
if (versionText !== null) {
|
|
1521
|
+
version = Number(versionText.trim());
|
|
1522
|
+
if (!Number.isFinite(version) || versionText.trim().length === 0) {
|
|
1523
|
+
report.warning(
|
|
1524
|
+
"validation-error",
|
|
1525
|
+
XGMML_ISSUE.DOCUMENT_VERSION,
|
|
1526
|
+
`documentVersion "${versionText}" does not parse; the dialect is chosen from the content`,
|
|
1527
|
+
{ line: root.line }
|
|
1528
|
+
);
|
|
1529
|
+
version = 0;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const registered = root.subgraphs.some((g) => isCyTrue(g.registered));
|
|
1533
|
+
return {
|
|
1534
|
+
versionText,
|
|
1535
|
+
version,
|
|
1536
|
+
cytoscape: doc.cytoscape,
|
|
1537
|
+
cy2: doc.cytoscape && version < 3,
|
|
1538
|
+
session: root.registered !== null && !isCyTrue(root.registered) && registered,
|
|
1539
|
+
view: isCyTrue(root.attrs.get("cy:view"))
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function graphsOf(doc, dialect) {
|
|
1543
|
+
return dialect.session ? doc.root.subgraphs.filter((g) => isCyTrue(g.registered)) : [doc.root];
|
|
1544
|
+
}
|
|
1545
|
+
function localRef(href) {
|
|
1546
|
+
return href.startsWith("#") ? href.slice(1) : null;
|
|
1547
|
+
}
|
|
1548
|
+
function membersOf(doc, graph) {
|
|
1549
|
+
if (graph === null) {
|
|
1550
|
+
return { nodes: doc.nodes, edges: doc.edges, dangling: 0 };
|
|
1551
|
+
}
|
|
1552
|
+
const nodeById = indexById(doc.nodes);
|
|
1553
|
+
const edgeById = indexById(doc.edges);
|
|
1554
|
+
const nodes = [];
|
|
1555
|
+
const edges = [];
|
|
1556
|
+
let dangling = 0;
|
|
1557
|
+
for (const member of graph.members) {
|
|
1558
|
+
if (member.href === null) {
|
|
1559
|
+
if (member.kind === "node") {
|
|
1560
|
+
nodes.push(member);
|
|
1561
|
+
} else {
|
|
1562
|
+
edges.push(member);
|
|
1563
|
+
}
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
const id = localRef(member.href);
|
|
1567
|
+
let target;
|
|
1568
|
+
if (id !== null) {
|
|
1569
|
+
target = member.kind === "node" ? nodeById.get(id) : edgeById.get(id);
|
|
1570
|
+
}
|
|
1571
|
+
if (target === void 0) {
|
|
1572
|
+
dangling++;
|
|
1573
|
+
} else if (target.kind === "node") {
|
|
1574
|
+
nodes.push(target);
|
|
1575
|
+
} else {
|
|
1576
|
+
edges.push(target);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return { nodes, edges, dangling };
|
|
1580
|
+
}
|
|
1581
|
+
function indexById(records) {
|
|
1582
|
+
const out = /* @__PURE__ */ new Map();
|
|
1583
|
+
for (const record of records) {
|
|
1584
|
+
if (record.id !== null && !out.has(record.id)) {
|
|
1585
|
+
out.set(record.id, record);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return out;
|
|
1589
|
+
}
|
|
1590
|
+
class XgmmlEmitter {
|
|
1591
|
+
/**
|
|
1592
|
+
* Create an emitter for one graph.
|
|
1593
|
+
* @param doc - the document
|
|
1594
|
+
* @param dialect - its dialect
|
|
1595
|
+
* @param sink - the sink
|
|
1596
|
+
* @param report - the report
|
|
1597
|
+
* @param options - the resolved common options
|
|
1598
|
+
* @param settings - the XGMML options
|
|
1599
|
+
* @param extras - what a session adds
|
|
1600
|
+
*/
|
|
1601
|
+
constructor(doc, dialect, sink, report, options, settings, extras = {}) {
|
|
1602
|
+
this.rows = /* @__PURE__ */ new Map();
|
|
1603
|
+
this.rowList = [];
|
|
1604
|
+
this.edgeIndex = [];
|
|
1605
|
+
this.sinceCheck = 0;
|
|
1606
|
+
this.aliasResolved = 0;
|
|
1607
|
+
this.aliasInteractions = 0;
|
|
1608
|
+
this.weighted = false;
|
|
1609
|
+
this.directions = /* @__PURE__ */ new Map();
|
|
1610
|
+
this.labelRows = null;
|
|
1611
|
+
this.doc = doc;
|
|
1612
|
+
this.dialect = dialect;
|
|
1613
|
+
this.sink = sink;
|
|
1614
|
+
this.report = report;
|
|
1615
|
+
this.options = options;
|
|
1616
|
+
this.settings = settings;
|
|
1617
|
+
this.extras = extras;
|
|
1618
|
+
this.coercer = new IdCoercer(options.ids);
|
|
1619
|
+
this.direction = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
1620
|
+
const base = { unescape: settings.cytoscapeEscapes, long: options.long };
|
|
1621
|
+
this.nodeColumns = new ColumnSet("node", report, {
|
|
1622
|
+
...base,
|
|
1623
|
+
reserved: /* @__PURE__ */ new Set([
|
|
1624
|
+
LABEL_COLUMN,
|
|
1625
|
+
POSITION_COLUMN,
|
|
1626
|
+
Z_COLUMN,
|
|
1627
|
+
GRAPHICS_COLUMN,
|
|
1628
|
+
PARENT_COLUMN,
|
|
1629
|
+
PARENTS_COLUMN,
|
|
1630
|
+
SUBGRAPH_COLUMN,
|
|
1631
|
+
NESTED_NETWORK_COLUMN,
|
|
1632
|
+
NETWORK_POINTER_COLUMN,
|
|
1633
|
+
NETWORKS_COLUMN
|
|
1634
|
+
])
|
|
1635
|
+
});
|
|
1636
|
+
this.edgeColumns = new ColumnSet("edge", report, {
|
|
1637
|
+
...base,
|
|
1638
|
+
reserved: /* @__PURE__ */ new Set([EDGE_ID_COLUMN, LABEL_COLUMN, GRAPHICS_COLUMN])
|
|
1639
|
+
});
|
|
1640
|
+
this.graphColumns = new ColumnSet("graph", report, { ...base, reserved: /* @__PURE__ */ new Set([GRAPHICS_COLUMN]) });
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Push the graph: nodes, containment, positions, edges, columns, metadata.
|
|
1644
|
+
* @param graph - the subnetwork, or null for the whole document
|
|
1645
|
+
*/
|
|
1646
|
+
emit(graph) {
|
|
1647
|
+
const members = membersOf(this.doc, graph);
|
|
1648
|
+
if (members.dangling > 0) {
|
|
1649
|
+
this.report.warning(
|
|
1650
|
+
"validation-error",
|
|
1651
|
+
XGMML_ISSUE.DANGLING_REFERENCE,
|
|
1652
|
+
`${this.prefix()}${members.dangling} xlink:href member reference(s) name no node or edge of the document`,
|
|
1653
|
+
{ line: graph?.line ?? null }
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
const header = this.headerDirected();
|
|
1657
|
+
this.direction.setHeader(this.sinkDirection(members.edges, header), { line: this.doc.root.line });
|
|
1658
|
+
this.addNodes(members.nodes);
|
|
1659
|
+
if (graph === null) {
|
|
1660
|
+
this.checkReferences();
|
|
1661
|
+
}
|
|
1662
|
+
const containment = new Containment(this);
|
|
1663
|
+
containment.resolve(this.rowList, this.groupTest());
|
|
1664
|
+
this.writeNodeExtras(graph);
|
|
1665
|
+
this.addEdges(members.edges, header);
|
|
1666
|
+
this.finishAliases();
|
|
1667
|
+
this.writeGraph(graph);
|
|
1668
|
+
const nodeIndex = (row) => this.rowList[row].index;
|
|
1669
|
+
this.nodeColumns.write(this.sink, nodeIndex);
|
|
1670
|
+
this.edgeColumns.write(this.sink, (row) => this.edgeIndex[row] ?? -1);
|
|
1671
|
+
containment.write();
|
|
1672
|
+
this.writeMeta(graph);
|
|
1673
|
+
throwIfAborted(this.options.signal);
|
|
1674
|
+
}
|
|
1675
|
+
// ------------------------------------------------------------------ shared helpers (also used by Containment)
|
|
1676
|
+
/**
|
|
1677
|
+
* The sink.
|
|
1678
|
+
* @returns the sink
|
|
1679
|
+
*/
|
|
1680
|
+
get target() {
|
|
1681
|
+
return this.sink;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* The report.
|
|
1685
|
+
* @returns the report
|
|
1686
|
+
*/
|
|
1687
|
+
get issues() {
|
|
1688
|
+
return this.report;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* The row of a node id in this graph.
|
|
1692
|
+
* @param id - the id text
|
|
1693
|
+
* @returns the row, or undefined
|
|
1694
|
+
*/
|
|
1695
|
+
rowOf(id) {
|
|
1696
|
+
return this.rows.get(id);
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* The network name a cross-file pointer names, when a session can tell.
|
|
1700
|
+
* @param href - the pointer
|
|
1701
|
+
* @returns the name, or null
|
|
1702
|
+
*/
|
|
1703
|
+
resolvePointer(href) {
|
|
1704
|
+
return this.extras.resolvePointer?.(href) ?? null;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Hand a group member the graph does not hold to the session, when one is reading.
|
|
1708
|
+
* @param group - the group node's id
|
|
1709
|
+
* @param member - the member's id
|
|
1710
|
+
* @returns true when the session took it
|
|
1711
|
+
*/
|
|
1712
|
+
missingMember(group, member) {
|
|
1713
|
+
if (this.extras.onMissingMember === void 0) {
|
|
1714
|
+
return false;
|
|
1715
|
+
}
|
|
1716
|
+
this.extras.onMissingMember(group, member);
|
|
1717
|
+
return true;
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* The first graph of the document with an id.
|
|
1721
|
+
* @param id - the id
|
|
1722
|
+
* @returns the graph, or undefined
|
|
1723
|
+
*/
|
|
1724
|
+
graphById(id) {
|
|
1725
|
+
return this.doc.graphs.find((g) => g.id === id);
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* The prefix of every message (the session entry), or "".
|
|
1729
|
+
* @returns the prefix
|
|
1730
|
+
*/
|
|
1731
|
+
prefix() {
|
|
1732
|
+
return this.extras.entry === void 0 ? "" : `${this.extras.entry}: `;
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Check the cancellation signal every few dozen elements.
|
|
1736
|
+
*/
|
|
1737
|
+
checkAbort() {
|
|
1738
|
+
if (++this.sinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
1739
|
+
this.sinceCheck = 0;
|
|
1740
|
+
throwIfAborted(this.options.signal);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Declare a column the importer owns on first use (the 5.6 rename rule applies).
|
|
1745
|
+
* @param domain - node or edge
|
|
1746
|
+
* @param decl - the declaration
|
|
1747
|
+
* @returns the handle, or null when the sink refused it (recorded)
|
|
1748
|
+
*/
|
|
1749
|
+
declare(domain, decl) {
|
|
1750
|
+
try {
|
|
1751
|
+
return declareResolved(this.sink, domain, decl, this.report, { element: decl.name }).handle;
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
this.report.recordError(err, { element: decl.name });
|
|
1754
|
+
return null;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
// ------------------------------------------------------------------ nodes
|
|
1758
|
+
/**
|
|
1759
|
+
* The header direction: the root's `directed` (0 by the DTD), else the defaultDirected option.
|
|
1760
|
+
* @returns whether the graph is directed by default
|
|
1761
|
+
*/
|
|
1762
|
+
headerDirected() {
|
|
1763
|
+
const text = this.doc.root.attrs.get("directed");
|
|
1764
|
+
if (text === void 0) {
|
|
1765
|
+
return this.options.defaultDirected;
|
|
1766
|
+
}
|
|
1767
|
+
return this.flag(text, this.options.defaultDirected, { line: this.doc.root.line, element: "directed" });
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* The direction of one edge: its cy:directed, else the header.
|
|
1771
|
+
* @param record - the edge
|
|
1772
|
+
* @param header - the header direction
|
|
1773
|
+
* @returns whether the edge is directed
|
|
1774
|
+
*/
|
|
1775
|
+
edgeDirection(record, header) {
|
|
1776
|
+
let directed = this.directions.get(record);
|
|
1777
|
+
if (directed === void 0) {
|
|
1778
|
+
directed = record.directed === null ? header : this.flag(record.directed, header, { line: record.line, element: record.id ?? record.label });
|
|
1779
|
+
this.directions.set(record, directed);
|
|
1780
|
+
}
|
|
1781
|
+
return directed;
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* The direction the sink starts with: the one every edge has when they all agree (a
|
|
1785
|
+
* Cytoscape file writes cy:directed on every edge and may omit the root attribute), else the
|
|
1786
|
+
* header, so a graph of one direction is never expanded into a mixed one.
|
|
1787
|
+
* @param edges - the edges of the graph
|
|
1788
|
+
* @param header - the header direction
|
|
1789
|
+
* @returns the sink direction
|
|
1790
|
+
*/
|
|
1791
|
+
sinkDirection(edges, header) {
|
|
1792
|
+
let first = null;
|
|
1793
|
+
for (const edge of edges) {
|
|
1794
|
+
const directed = this.edgeDirection(edge, header);
|
|
1795
|
+
if (first === null) {
|
|
1796
|
+
first = directed;
|
|
1797
|
+
} else if (first !== directed) {
|
|
1798
|
+
return header;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return first ?? header;
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* A 0 / 1 direction flag: true, false, yes and no are accepted with a warning; anything else
|
|
1805
|
+
* falls back.
|
|
1806
|
+
* @param text - the text
|
|
1807
|
+
* @param fallback - the direction for an unreadable value
|
|
1808
|
+
* @param where - the location
|
|
1809
|
+
* @returns the direction
|
|
1810
|
+
*/
|
|
1811
|
+
flag(text, fallback, where) {
|
|
1812
|
+
const t = text.trim();
|
|
1813
|
+
if (t === "1" || t === "0") {
|
|
1814
|
+
return t === "1";
|
|
1815
|
+
}
|
|
1816
|
+
const lower = t.toLowerCase();
|
|
1817
|
+
const readable = lower === "true" || lower === "yes" || lower === "false" || lower === "no";
|
|
1818
|
+
this.report.warnOnce(
|
|
1819
|
+
"validation-error",
|
|
1820
|
+
XGMML_ISSUE.BAD_DIRECTED,
|
|
1821
|
+
readable ? `${this.prefix()}directed="${text}" is not 0 or 1; read as ${lower === "true" || lower === "yes" ? "1" : "0"}` : `${this.prefix()}directed="${text}" is not 0 or 1; read as ${fallback ? "directed" : "undirected"}`,
|
|
1822
|
+
where,
|
|
1823
|
+
`${XGMML_ISSUE.BAD_DIRECTED}:${text}`
|
|
1824
|
+
);
|
|
1825
|
+
return readable ? lower === "true" || lower === "yes" : fallback;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Add every node record to the sink (one node per id; later declarations merge).
|
|
1829
|
+
* @param records - the node records
|
|
1830
|
+
*/
|
|
1831
|
+
addNodes(records) {
|
|
1832
|
+
for (const record of records) {
|
|
1833
|
+
const { id } = record;
|
|
1834
|
+
if (id === null) {
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
const existing = this.rows.get(id);
|
|
1838
|
+
if (existing !== void 0) {
|
|
1839
|
+
existing.records.push(record);
|
|
1840
|
+
this.report.warning(
|
|
1841
|
+
"validation-error",
|
|
1842
|
+
XGMML_ISSUE.DUPLICATE_NODE,
|
|
1843
|
+
`${this.prefix()}node "${id}" is declared twice; the declarations are merged`,
|
|
1844
|
+
{ line: record.line, element: id }
|
|
1845
|
+
);
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
const row = { id, records: [record], index: -1, row: this.rowList.length };
|
|
1849
|
+
try {
|
|
1850
|
+
row.index = this.sink.addNode(this.nodeId(id, record.line));
|
|
1851
|
+
this.report.counts.nodes++;
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
this.report.counts.skippedNodes++;
|
|
1854
|
+
this.report.recordError(err, { line: record.line, element: id });
|
|
1855
|
+
}
|
|
1856
|
+
this.rows.set(id, row);
|
|
1857
|
+
this.rowList.push(row);
|
|
1858
|
+
this.checkAbort();
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* The coerced id of a node text.
|
|
1863
|
+
* @param text - the id text
|
|
1864
|
+
* @param line - the line
|
|
1865
|
+
* @returns the id
|
|
1866
|
+
*/
|
|
1867
|
+
nodeId(text, line) {
|
|
1868
|
+
const id = this.coercer.text(text);
|
|
1869
|
+
const merge = this.coercer.lastMerge;
|
|
1870
|
+
if (merge !== null) {
|
|
1871
|
+
this.report.warning(
|
|
1872
|
+
"coercion",
|
|
1873
|
+
XGMML_ISSUE.ID_MERGED,
|
|
1874
|
+
`id "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids: "number"`,
|
|
1875
|
+
{ line, element: text }
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
return id;
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Whether a node's nested graph is a group (containment) rather than a nested-network
|
|
1882
|
+
* pointer: any graph in the 1.0 draft dialect, a 2.x node with `__groupState`, a 3.x node with
|
|
1883
|
+
* `__isGroup` true, or a node the session lists.
|
|
1884
|
+
* @returns the test
|
|
1885
|
+
*/
|
|
1886
|
+
groupTest() {
|
|
1887
|
+
const { groupNodes } = this.extras;
|
|
1888
|
+
const { cytoscape } = this.dialect;
|
|
1889
|
+
return (node) => {
|
|
1890
|
+
if (!cytoscape) {
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
if (node.id !== null && groupNodes?.has(node.id) === true) {
|
|
1894
|
+
return true;
|
|
1895
|
+
}
|
|
1896
|
+
return node.atts.some((a) => a.name === "__groupState" || a.name === "__isGroup" && isCyTrue(a.value));
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* W_DANGLING_REFERENCE for the `xlink:href` members of a generic document's non-group graphs
|
|
1901
|
+
* that name no node or edge (a group's are E_UNKNOWN_PARENT, reported by the containment).
|
|
1902
|
+
*/
|
|
1903
|
+
checkReferences() {
|
|
1904
|
+
const isGroup = this.groupTest();
|
|
1905
|
+
const edgeIds = new Set(this.doc.edges.map((e) => e.id));
|
|
1906
|
+
let dangling = 0;
|
|
1907
|
+
for (const graph of this.doc.graphs) {
|
|
1908
|
+
if (graph.owner !== null && isGroup(graph.owner)) {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
for (const member of graph.members) {
|
|
1912
|
+
if (member.href === null) {
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
const id = localRef(member.href);
|
|
1916
|
+
const known = id !== null && (member.kind === "node" ? this.rows.has(id) : edgeIds.has(id));
|
|
1917
|
+
if (!known) {
|
|
1918
|
+
dangling++;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (dangling > 0) {
|
|
1923
|
+
this.report.warning(
|
|
1924
|
+
"validation-error",
|
|
1925
|
+
XGMML_ISSUE.DANGLING_REFERENCE,
|
|
1926
|
+
`${this.prefix()}${dangling} xlink:href reference(s) name no node or edge of the document`
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Write the per-node values: label, attributes, XML attributes, graphics, positions, nested
|
|
1932
|
+
* network pointers and subgraph memberships.
|
|
1933
|
+
* @param graph - the subnetwork, or null
|
|
1934
|
+
*/
|
|
1935
|
+
writeNodeExtras(graph) {
|
|
1936
|
+
const labels = [];
|
|
1937
|
+
const positions = new Positions(this, this.dialect.cytoscape ? this.settings.zAs : "position");
|
|
1938
|
+
const graphics = new JsonColumn(this, "node", GRAPHICS_COLUMN, XGMML_ORIGIN_NAMESPACE, "graphics");
|
|
1939
|
+
const pointers = new Pointers(this, this.groupTest());
|
|
1940
|
+
for (const row of this.rowList) {
|
|
1941
|
+
const where = { line: row.records[0].line, element: row.id };
|
|
1942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1943
|
+
for (const record of row.records) {
|
|
1944
|
+
labels[row.row] ??= record.label ?? void 0;
|
|
1945
|
+
this.addElementColumns(this.nodeColumns, row.row, record, seen, where);
|
|
1946
|
+
positions.add(row, record);
|
|
1947
|
+
if (record.graphics !== null && Object.keys(record.graphics).length > 0) {
|
|
1948
|
+
graphics.set(row.index, record.graphics);
|
|
1949
|
+
}
|
|
1950
|
+
pointers.add(row, record);
|
|
1951
|
+
}
|
|
1952
|
+
this.checkAbort();
|
|
1953
|
+
}
|
|
1954
|
+
this.writeLabels("node", labels, (row) => this.rowList[row].index);
|
|
1955
|
+
positions.write();
|
|
1956
|
+
pointers.write();
|
|
1957
|
+
if (graph === null) {
|
|
1958
|
+
this.writeNetworks();
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Add an element's XML attributes (text grammar) and atts to a column set; atts of a later
|
|
1963
|
+
* duplicate declaration only fill names the earlier ones left unset.
|
|
1964
|
+
* @param columns - the column set
|
|
1965
|
+
* @param row - the row
|
|
1966
|
+
* @param record - the node or edge
|
|
1967
|
+
* @param seen - the names set by earlier declarations of the same node
|
|
1968
|
+
* @param where - the location
|
|
1969
|
+
*/
|
|
1970
|
+
addElementColumns(columns, row, record, seen, where) {
|
|
1971
|
+
const merging = seen.size > 0;
|
|
1972
|
+
const names = /* @__PURE__ */ new Set();
|
|
1973
|
+
if (record.name !== null) {
|
|
1974
|
+
if (!(merging && seen.has("name"))) {
|
|
1975
|
+
columns.addText(row, "name", record.name, where);
|
|
1976
|
+
}
|
|
1977
|
+
names.add("name");
|
|
1978
|
+
}
|
|
1979
|
+
for (const [key2, value] of record.xmlAttrs) {
|
|
1980
|
+
if (!(merging && seen.has(key2))) {
|
|
1981
|
+
columns.addText(row, key2, value, where);
|
|
1982
|
+
}
|
|
1983
|
+
names.add(key2);
|
|
1984
|
+
}
|
|
1985
|
+
for (const att of record.atts) {
|
|
1986
|
+
if (this.skipAtt(record, att) || merging && att.name !== null && seen.has(att.name)) {
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
if (record.kind === "node" && att.name === "__isGroup" && record.nested.length > 0 && isCyTrue(att.value)) {
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
if (att.hasGraph && att.name === null) {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
columns.addAtt(row, att, where);
|
|
1996
|
+
if (att.name !== null) {
|
|
1997
|
+
names.add(att.name);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
for (const name of names) {
|
|
2001
|
+
seen.add(name);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Whether an att is not a column: an edge's weight att under weightFrom.
|
|
2006
|
+
* @param record - the element
|
|
2007
|
+
* @param att - the att
|
|
2008
|
+
* @returns true to skip it
|
|
2009
|
+
*/
|
|
2010
|
+
skipAtt(record, att) {
|
|
2011
|
+
return record.kind === "edge" && record.weight === null && att.name === this.options.weightFrom;
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Write a label column (role label).
|
|
2015
|
+
* @param domain - node or edge
|
|
2016
|
+
* @param labels - the label of each row
|
|
2017
|
+
* @param index - the sink index of a row
|
|
2018
|
+
*/
|
|
2019
|
+
writeLabels(domain, labels, index) {
|
|
2020
|
+
if (!labels.some((l) => l !== void 0)) {
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
const handle = this.declare(domain, {
|
|
2024
|
+
name: LABEL_COLUMN,
|
|
2025
|
+
dtype: "string",
|
|
2026
|
+
role: "label",
|
|
2027
|
+
nullable: true,
|
|
2028
|
+
origin: { format: FORMAT, id: null, title: null, type: "label", namespace: null }
|
|
2029
|
+
});
|
|
2030
|
+
if (handle === null) {
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
labels.forEach((label, row) => {
|
|
2034
|
+
const i = index(row);
|
|
2035
|
+
if (label !== void 0 && i >= 0) {
|
|
2036
|
+
if (domain === "node") {
|
|
2037
|
+
this.sink.setNodeValue(handle, i, label);
|
|
2038
|
+
} else {
|
|
2039
|
+
this.sink.setEdgeValue(handle, i, label);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
/** The `xgmml.networks` membership of nodes in root-level subgraphs of a generic document. */
|
|
2045
|
+
writeNetworks() {
|
|
2046
|
+
const { subgraphs } = this.doc.root;
|
|
2047
|
+
if (subgraphs.length === 0) {
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
const member = /* @__PURE__ */ new Map();
|
|
2051
|
+
for (const graph of subgraphs) {
|
|
2052
|
+
const name = graph.id ?? graph.label ?? "";
|
|
2053
|
+
for (const m of graph.members) {
|
|
2054
|
+
const id = m.kind === "node" ? m.id ?? (m.href === null ? null : localRef(m.href)) : null;
|
|
2055
|
+
const row = id === null ? void 0 : this.rows.get(id);
|
|
2056
|
+
if (row !== void 0 && row.index >= 0) {
|
|
2057
|
+
const list = member.get(row.index) ?? [];
|
|
2058
|
+
list.push(name);
|
|
2059
|
+
member.set(row.index, list);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
if (member.size === 0) {
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
const handle = this.declare("node", {
|
|
2067
|
+
name: NETWORKS_COLUMN,
|
|
2068
|
+
dtype: "list",
|
|
2069
|
+
itemDtype: "string",
|
|
2070
|
+
nullable: true,
|
|
2071
|
+
origin: { format: FORMAT, id: null, title: null, type: "graph", namespace: XGMML_ORIGIN_NAMESPACE }
|
|
2072
|
+
});
|
|
2073
|
+
if (handle !== null) {
|
|
2074
|
+
for (const [index, list] of member) {
|
|
2075
|
+
this.sink.setNodeValue(handle, index, list);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
// ------------------------------------------------------------------ edges
|
|
2080
|
+
/**
|
|
2081
|
+
* Push every edge record.
|
|
2082
|
+
* @param records - the edge records
|
|
2083
|
+
* @param header - the header direction
|
|
2084
|
+
*/
|
|
2085
|
+
addEdges(records, header) {
|
|
2086
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2087
|
+
const dedupe = this.dialect.cy2 && this.doc.nodes.some((n) => n.nested.length > 0) ? /* @__PURE__ */ new Set() : null;
|
|
2088
|
+
const labels = [];
|
|
2089
|
+
const edgeIds = [];
|
|
2090
|
+
const graphics = [];
|
|
2091
|
+
let row = 0;
|
|
2092
|
+
for (const record of records) {
|
|
2093
|
+
const where = { line: record.line, element: record.id ?? record.label };
|
|
2094
|
+
const key2 = record.id ?? record.label;
|
|
2095
|
+
if (dedupe !== null && key2 !== null) {
|
|
2096
|
+
if (dedupe.has(key2)) {
|
|
2097
|
+
this.report.warnOnce(
|
|
2098
|
+
"merged",
|
|
2099
|
+
XGMML_ISSUE.GROUP_DUPLICATE_EDGE,
|
|
2100
|
+
`${this.prefix()}the Cytoscape 2.x writer repeats the edges of a group; each repeat is dropped`,
|
|
2101
|
+
where
|
|
2102
|
+
);
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
dedupe.add(key2);
|
|
2106
|
+
}
|
|
2107
|
+
const index = this.addEdge(record, header, ids, where);
|
|
2108
|
+
this.checkAbort();
|
|
2109
|
+
if (index < 0) {
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
this.edgeIndex[row] = index;
|
|
2113
|
+
labels[row] = record.label ?? void 0;
|
|
2114
|
+
edgeIds[row] = record.id ?? void 0;
|
|
2115
|
+
this.addElementColumns(this.edgeColumns, row, record, /* @__PURE__ */ new Set(), where);
|
|
2116
|
+
this.fillInteraction(record, row, where);
|
|
2117
|
+
if (record.graphics !== null && Object.keys(record.graphics).length > 0) {
|
|
2118
|
+
graphics.push([index, record.graphics]);
|
|
2119
|
+
}
|
|
2120
|
+
row++;
|
|
2121
|
+
}
|
|
2122
|
+
this.writeEdgeIds(edgeIds);
|
|
2123
|
+
this.writeLabels("edge", labels, (r) => this.edgeIndex[r] ?? -1);
|
|
2124
|
+
const column = new JsonColumn(this, "edge", GRAPHICS_COLUMN, XGMML_ORIGIN_NAMESPACE, "graphics");
|
|
2125
|
+
for (const [index, value] of graphics) {
|
|
2126
|
+
column.set(index, value);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Push one edge: endpoints (with label aliases), duplicate id check, direction and weight.
|
|
2131
|
+
* @param record - the edge
|
|
2132
|
+
* @param header - the header direction
|
|
2133
|
+
* @param ids - the edge ids used so far
|
|
2134
|
+
* @param where - the location
|
|
2135
|
+
* @returns the edge index, or -1 when it was skipped
|
|
2136
|
+
*/
|
|
2137
|
+
addEdge(record, header, ids, where) {
|
|
2138
|
+
const alias = this.settings.labelAliases ? aliasesOf(record.label) : null;
|
|
2139
|
+
let source;
|
|
2140
|
+
let target;
|
|
2141
|
+
try {
|
|
2142
|
+
source = this.endpoint(record.source, alias?.[0] ?? null, "source", where);
|
|
2143
|
+
target = this.endpoint(record.target, alias?.[2] ?? null, "target", where);
|
|
2144
|
+
} catch (err) {
|
|
2145
|
+
this.report.counts.skippedEdges++;
|
|
2146
|
+
this.report.recordError(err, where);
|
|
2147
|
+
return -1;
|
|
2148
|
+
}
|
|
2149
|
+
if (source === null || target === null) {
|
|
2150
|
+
this.report.counts.skippedEdges++;
|
|
2151
|
+
return -1;
|
|
2152
|
+
}
|
|
2153
|
+
if (record.id !== null && ids.has(record.id)) {
|
|
2154
|
+
this.report.counts.skippedEdges++;
|
|
2155
|
+
this.report.error(
|
|
2156
|
+
"validation-error",
|
|
2157
|
+
XGMML_ISSUE.DUPLICATE_EDGE_ID,
|
|
2158
|
+
`${this.prefix()}edge id "${record.id}" is used twice; the second edge is skipped`,
|
|
2159
|
+
where
|
|
2160
|
+
);
|
|
2161
|
+
return -1;
|
|
2162
|
+
}
|
|
2163
|
+
const directed = this.edgeDirection(record, header);
|
|
2164
|
+
const kind = directed ? "directed" : "undirected";
|
|
2165
|
+
let weight;
|
|
2166
|
+
try {
|
|
2167
|
+
weight = this.weightOf(record);
|
|
2168
|
+
const before = this.sink.edgeCount;
|
|
2169
|
+
const e = this.direction.addEdge(source, target, kind, weight, where);
|
|
2170
|
+
this.report.counts.edges += this.sink.edgeCount - before;
|
|
2171
|
+
if (record.id !== null) {
|
|
2172
|
+
ids.add(record.id);
|
|
2173
|
+
}
|
|
2174
|
+
return e;
|
|
2175
|
+
} catch (err) {
|
|
2176
|
+
this.report.counts.skippedEdges++;
|
|
2177
|
+
this.report.recordError(err, where);
|
|
2178
|
+
return -1;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* The weight of an edge: its `weight` XML attribute (or the att weightFrom names).
|
|
2183
|
+
* @param record - the edge
|
|
2184
|
+
* @returns the weight, or undefined
|
|
2185
|
+
*/
|
|
2186
|
+
weightOf(record) {
|
|
2187
|
+
const from = this.options.weightFrom;
|
|
2188
|
+
if (from === null) {
|
|
2189
|
+
return void 0;
|
|
2190
|
+
}
|
|
2191
|
+
let text = from === "weight" ? record.weight : null;
|
|
2192
|
+
if (text === null) {
|
|
2193
|
+
text = record.atts.find((a) => a.name === from && a.children.length === 0)?.value ?? null;
|
|
2194
|
+
}
|
|
2195
|
+
if (text === null) {
|
|
2196
|
+
return void 0;
|
|
2197
|
+
}
|
|
2198
|
+
const weight = parseWeightText(text);
|
|
2199
|
+
this.weighted ||= weight !== void 0;
|
|
2200
|
+
return weight;
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* Resolve one endpoint: a node of this graph by id, else (under labelAliases) the label alias
|
|
2204
|
+
* by id or by unique node label; a missing endpoint is E_MISSING_ENDPOINT, an unknown one
|
|
2205
|
+
* E_UNKNOWN_NODE unless addMissingNodes adds it.
|
|
2206
|
+
* @param text - the source or target text, or null
|
|
2207
|
+
* @param alias - the label alias, or null
|
|
2208
|
+
* @param side - which end
|
|
2209
|
+
* @param where - the location
|
|
2210
|
+
* @returns the node id, or null when the edge cannot be added
|
|
2211
|
+
*/
|
|
2212
|
+
endpoint(text, alias, side, where) {
|
|
2213
|
+
if (text !== null) {
|
|
2214
|
+
const row = this.rows.get(text);
|
|
2215
|
+
if (row !== void 0 && row.index >= 0) {
|
|
2216
|
+
return this.coercer.text(text);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
if (alias !== null) {
|
|
2220
|
+
const resolved = this.aliasRow(alias);
|
|
2221
|
+
if (resolved !== null) {
|
|
2222
|
+
this.aliasResolved++;
|
|
2223
|
+
return this.coercer.text(resolved.id);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (text === null) {
|
|
2227
|
+
this.report.error(
|
|
2228
|
+
"missing-value",
|
|
2229
|
+
XGMML_ISSUE.MISSING_ENDPOINT,
|
|
2230
|
+
`${this.prefix()}<edge> without a ${side}`,
|
|
2231
|
+
where
|
|
2232
|
+
);
|
|
2233
|
+
return null;
|
|
2234
|
+
}
|
|
2235
|
+
if (this.options.addMissingNodes) {
|
|
2236
|
+
const id = this.coercer.text(text);
|
|
2237
|
+
if (this.sink.indexOf(id) === INVALID_INDEX) {
|
|
2238
|
+
this.sink.addNode(id);
|
|
2239
|
+
this.report.counts.nodes++;
|
|
2240
|
+
}
|
|
2241
|
+
return id;
|
|
2242
|
+
}
|
|
2243
|
+
this.report.error(
|
|
2244
|
+
"validation-error",
|
|
2245
|
+
XGMML_ISSUE.UNKNOWN_NODE,
|
|
2246
|
+
`${this.prefix()}edge ${side} "${text}" is not a node of this graph; the edge is skipped`,
|
|
2247
|
+
where
|
|
2248
|
+
);
|
|
2249
|
+
return null;
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* The node a label alias names: by id, else by a label only one node has.
|
|
2253
|
+
* @param alias - the alias
|
|
2254
|
+
* @returns the row, or null
|
|
2255
|
+
*/
|
|
2256
|
+
aliasRow(alias) {
|
|
2257
|
+
const byId = this.rows.get(alias);
|
|
2258
|
+
if (byId !== void 0 && byId.index >= 0) {
|
|
2259
|
+
return byId;
|
|
2260
|
+
}
|
|
2261
|
+
if (this.labelRows === null) {
|
|
2262
|
+
this.labelRows = /* @__PURE__ */ new Map();
|
|
2263
|
+
for (const row of this.rowList) {
|
|
2264
|
+
const { label } = row.records[0];
|
|
2265
|
+
if (label !== null && row.index >= 0) {
|
|
2266
|
+
this.labelRows.set(label, this.labelRows.has(label) ? null : row);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
return this.labelRows.get(alias) ?? null;
|
|
2271
|
+
}
|
|
2272
|
+
/**
|
|
2273
|
+
* Fill the interaction of an edge without one from its label alias, as Cytoscape does.
|
|
2274
|
+
* @param record - the edge
|
|
2275
|
+
* @param row - the edge row
|
|
2276
|
+
* @param where - the location
|
|
2277
|
+
*/
|
|
2278
|
+
fillInteraction(record, row, where) {
|
|
2279
|
+
if (!this.settings.labelAliases || this.dialect.session || record.atts.some((a) => a.name === INTERACTION_COLUMN)) {
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const alias = aliasesOf(record.label);
|
|
2283
|
+
if (alias !== null) {
|
|
2284
|
+
this.edgeColumns.addText(row, INTERACTION_COLUMN, alias[1], where);
|
|
2285
|
+
this.aliasInteractions++;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
/** Report the label alias uses, once. */
|
|
2289
|
+
finishAliases() {
|
|
2290
|
+
if (this.aliasResolved === 0 && this.aliasInteractions === 0) {
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
this.report.warning(
|
|
2294
|
+
"coercion",
|
|
2295
|
+
XGMML_ISSUE.LABEL_ALIAS,
|
|
2296
|
+
`${this.prefix()}Cytoscape label aliases ("a (pp) b"): ${this.aliasResolved} endpoint(s) resolved, ${this.aliasInteractions} interaction(s) filled from edge labels`
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Write the edge `id` column (role id, unique).
|
|
2301
|
+
* @param ids - the id of each row
|
|
2302
|
+
*/
|
|
2303
|
+
writeEdgeIds(ids) {
|
|
2304
|
+
if (!ids.some((id) => id !== void 0)) {
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
const handle = this.declare("edge", {
|
|
2308
|
+
name: EDGE_ID_COLUMN,
|
|
2309
|
+
dtype: "string",
|
|
2310
|
+
role: "id",
|
|
2311
|
+
unique: true,
|
|
2312
|
+
nullable: true,
|
|
2313
|
+
origin: { format: FORMAT, id: null, title: null, type: "id", namespace: null }
|
|
2314
|
+
});
|
|
2315
|
+
if (handle === null) {
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
ids.forEach((id, row) => {
|
|
2319
|
+
const e = this.edgeIndex[row];
|
|
2320
|
+
if (id !== void 0 && e !== void 0) {
|
|
2321
|
+
try {
|
|
2322
|
+
this.sink.setEdgeValue(handle, e, id);
|
|
2323
|
+
} catch (err) {
|
|
2324
|
+
this.report.recordError(err, { element: id });
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
// ------------------------------------------------------------------ graph and metadata
|
|
2330
|
+
/**
|
|
2331
|
+
* The graph table: the graph's atts and other XML attributes, and its graphics.
|
|
2332
|
+
* @param graph - the subnetwork, or null for the root
|
|
2333
|
+
*/
|
|
2334
|
+
writeGraph(graph) {
|
|
2335
|
+
const g = graph ?? this.doc.root;
|
|
2336
|
+
const where = { line: g.line, element: g.id };
|
|
2337
|
+
if (graph === null) {
|
|
2338
|
+
for (const [key2, value] of g.attrs) {
|
|
2339
|
+
if (!GRAPH_STRUCTURE.has(key2) && key2 !== "xmlns" && !key2.startsWith("xmlns:")) {
|
|
2340
|
+
this.graphColumns.addText(0, key2, value, where);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
for (const att of g.atts) {
|
|
2345
|
+
if (att.name !== null && META_ATTS.has(att.name)) {
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
if (att.hasGraph && att.name === null) {
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
this.graphColumns.addAtt(0, att, where);
|
|
2352
|
+
}
|
|
2353
|
+
this.graphColumns.write(this.sink, () => 0);
|
|
2354
|
+
const json = { ...this.extras.graphJson ?? {} };
|
|
2355
|
+
if (g.graphics !== null && Object.keys(g.graphics).length > 0) {
|
|
2356
|
+
json[GRAPHICS_COLUMN] = g.graphics;
|
|
2357
|
+
}
|
|
2358
|
+
for (const [name, value] of Object.entries(json)) {
|
|
2359
|
+
this.sink.setGraphValue(name, value, {
|
|
2360
|
+
name,
|
|
2361
|
+
dtype: "json",
|
|
2362
|
+
nullable: true,
|
|
2363
|
+
origin: { format: FORMAT, id: null, title: null, type: name, namespace: XGMML_ORIGIN_NAMESPACE }
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* The graph metadata: name, description and date from the RDF metadata, the format, version
|
|
2369
|
+
* and what the exporter needs.
|
|
2370
|
+
* @param graph - the subnetwork, or null
|
|
2371
|
+
*/
|
|
2372
|
+
writeMeta(graph) {
|
|
2373
|
+
const g = graph ?? this.doc.root;
|
|
2374
|
+
const { rdf } = this.doc;
|
|
2375
|
+
const name = this.extras.graphName ?? g.label ?? rdf.title ?? g.id ?? void 0;
|
|
2376
|
+
this.sink.setMeta({
|
|
2377
|
+
name: name ?? void 0,
|
|
2378
|
+
description: rdf.description ?? void 0,
|
|
2379
|
+
created: rdf.date ?? void 0,
|
|
2380
|
+
sourceFormat: this.extras.sourceFormat ?? FORMAT,
|
|
2381
|
+
sourceVersion: this.dialect.versionText ?? void 0,
|
|
2382
|
+
weightOrigin: this.weighted ? { format: FORMAT, id: "weight", title: null, type: "weight", namespace: null } : void 0,
|
|
2383
|
+
extra: {
|
|
2384
|
+
[META_KEY]: {
|
|
2385
|
+
graphId: g.id,
|
|
2386
|
+
directed: this.doc.root.attrs.get("directed") ?? null,
|
|
2387
|
+
rdf: Object.keys(rdf).length > 0 ? { ...rdf } : null
|
|
2388
|
+
},
|
|
2389
|
+
...this.extras.metaExtra ?? {}
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
function aliasesOf(label) {
|
|
2395
|
+
if (label === null) {
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
const parts = label.split(ALIAS_SPLIT);
|
|
2399
|
+
return parts.length === 3 ? [parts[0].trim(), parts[1], parts[2].trim()] : null;
|
|
2400
|
+
}
|
|
2401
|
+
class JsonColumn {
|
|
2402
|
+
/**
|
|
2403
|
+
* Create the column.
|
|
2404
|
+
* @param emitter - the emitter
|
|
2405
|
+
* @param domain - node or edge
|
|
2406
|
+
* @param name - the column name
|
|
2407
|
+
* @param namespace - its origin namespace
|
|
2408
|
+
* @param type - its origin type
|
|
2409
|
+
* @param dtype - json (default) or string
|
|
2410
|
+
*/
|
|
2411
|
+
constructor(emitter, domain, name, namespace, type, dtype = "json") {
|
|
2412
|
+
this.emitter = emitter;
|
|
2413
|
+
this.domain = domain;
|
|
2414
|
+
this.name = name;
|
|
2415
|
+
this.namespace = namespace;
|
|
2416
|
+
this.type = type;
|
|
2417
|
+
this.dtype = dtype;
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Set one value.
|
|
2421
|
+
* @param index - the node or edge index
|
|
2422
|
+
* @param value - the value
|
|
2423
|
+
*/
|
|
2424
|
+
set(index, value) {
|
|
2425
|
+
if (index < 0) {
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
if (this.handle === void 0) {
|
|
2429
|
+
this.handle = this.emitter.declare(this.domain, {
|
|
2430
|
+
name: this.name,
|
|
2431
|
+
dtype: this.dtype,
|
|
2432
|
+
nullable: true,
|
|
2433
|
+
origin: { format: FORMAT, id: null, title: null, type: this.type, namespace: this.namespace }
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
if (this.handle === null) {
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
const sink = this.emitter.target;
|
|
2440
|
+
if (this.domain === "node") {
|
|
2441
|
+
sink.setNodeValue(this.handle, index, value);
|
|
2442
|
+
} else {
|
|
2443
|
+
sink.setEdgeValue(this.handle, index, value);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
class Positions {
|
|
2448
|
+
/**
|
|
2449
|
+
* Create the collector.
|
|
2450
|
+
* @param emitter - the emitter
|
|
2451
|
+
* @param zAs - where z goes
|
|
2452
|
+
*/
|
|
2453
|
+
constructor(emitter, zAs) {
|
|
2454
|
+
this.emitter = emitter;
|
|
2455
|
+
this.zAs = zAs;
|
|
2456
|
+
this.values = /* @__PURE__ */ new Map();
|
|
2457
|
+
this.zValues = /* @__PURE__ */ new Map();
|
|
2458
|
+
this.hasZ = false;
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Read one node record's coordinates.
|
|
2462
|
+
* @param row - the node row
|
|
2463
|
+
* @param record - the record
|
|
2464
|
+
*/
|
|
2465
|
+
add(row, record) {
|
|
2466
|
+
if (row.index < 0 || record.x === null && record.y === null && record.z === null) {
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
const where = { line: record.line, element: row.id };
|
|
2470
|
+
const x = this.number(record.x, where);
|
|
2471
|
+
const y = this.number(record.y, where);
|
|
2472
|
+
const z = this.number(record.z, where);
|
|
2473
|
+
if (z !== null) {
|
|
2474
|
+
this.hasZ = true;
|
|
2475
|
+
if (this.zAs === "column") {
|
|
2476
|
+
this.zValues.set(row.index, z);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
if (x !== null && y !== null) {
|
|
2480
|
+
const flipped = y === 0 ? 0 : -y;
|
|
2481
|
+
this.values.set(row.index, [x, flipped, this.zAs === "position" ? z ?? 0 : 0]);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* A coordinate.
|
|
2486
|
+
* @param text - the text, or null
|
|
2487
|
+
* @param where - the location
|
|
2488
|
+
* @returns the number, or null when absent or invalid (recorded)
|
|
2489
|
+
*/
|
|
2490
|
+
number(text, where) {
|
|
2491
|
+
if (text === null) {
|
|
2492
|
+
return null;
|
|
2493
|
+
}
|
|
2494
|
+
const n = Number(text.trim());
|
|
2495
|
+
if (text.trim().length === 0 || !Number.isFinite(n)) {
|
|
2496
|
+
this.emitter.issues.error(
|
|
2497
|
+
"validation-error",
|
|
2498
|
+
XGMML_ISSUE.BAD_VALUE,
|
|
2499
|
+
`${this.emitter.prefix()}graphics coordinate "${text}" is not a number`,
|
|
2500
|
+
where
|
|
2501
|
+
);
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
return n;
|
|
2505
|
+
}
|
|
2506
|
+
/** Declare and write the position and z columns. */
|
|
2507
|
+
write() {
|
|
2508
|
+
const sink = this.emitter.target;
|
|
2509
|
+
if (this.values.size > 0) {
|
|
2510
|
+
const handle = this.emitter.declare("node", {
|
|
2511
|
+
name: POSITION_COLUMN,
|
|
2512
|
+
dtype: "f32",
|
|
2513
|
+
components: 3,
|
|
2514
|
+
role: "position",
|
|
2515
|
+
nullable: true,
|
|
2516
|
+
origin: { format: FORMAT, id: null, title: null, type: "graphics", namespace: XGMML_ORIGIN_NAMESPACE },
|
|
2517
|
+
extra: { sourceDims: this.zAs === "position" && this.hasZ ? 3 : 2, units: "file" }
|
|
2518
|
+
});
|
|
2519
|
+
if (handle !== null) {
|
|
2520
|
+
for (const [index, xyz] of this.values) {
|
|
2521
|
+
sink.setNodeValue(handle, index, xyz);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
if (this.zValues.size > 0) {
|
|
2526
|
+
const handle = this.emitter.declare("node", {
|
|
2527
|
+
name: Z_COLUMN,
|
|
2528
|
+
dtype: "f64",
|
|
2529
|
+
nullable: true,
|
|
2530
|
+
origin: {
|
|
2531
|
+
format: FORMAT,
|
|
2532
|
+
id: null,
|
|
2533
|
+
title: null,
|
|
2534
|
+
type: "graphics",
|
|
2535
|
+
namespace: CYTOSCAPE_ORIGIN_NAMESPACE
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2538
|
+
if (handle !== null) {
|
|
2539
|
+
for (const [index, z] of this.zValues) {
|
|
2540
|
+
sink.setNodeValue(handle, index, z);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
class Pointers {
|
|
2547
|
+
/**
|
|
2548
|
+
* Create the collector.
|
|
2549
|
+
* @param emitter - the emitter
|
|
2550
|
+
* @param isGroup - the group test
|
|
2551
|
+
*/
|
|
2552
|
+
constructor(emitter, isGroup) {
|
|
2553
|
+
this.emitter = emitter;
|
|
2554
|
+
this.isGroup = isGroup;
|
|
2555
|
+
this.names = /* @__PURE__ */ new Map();
|
|
2556
|
+
this.crossFile = /* @__PURE__ */ new Map();
|
|
2557
|
+
this.dangling = 0;
|
|
2558
|
+
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Read one node record's pointers.
|
|
2561
|
+
* @param row - the node row
|
|
2562
|
+
* @param record - the record
|
|
2563
|
+
*/
|
|
2564
|
+
add(row, record) {
|
|
2565
|
+
if (record.nested.length === 0 || this.isGroup(record) || row.index < 0) {
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
for (const graph of record.nested) {
|
|
2569
|
+
if (graph.href === null) {
|
|
2570
|
+
this.names.set(row.index, graph.label ?? graph.id ?? "");
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
const id = localRef(graph.href);
|
|
2574
|
+
if (id === null) {
|
|
2575
|
+
const resolved = this.emitter.resolvePointer(graph.href);
|
|
2576
|
+
if (resolved !== null) {
|
|
2577
|
+
this.names.set(row.index, resolved);
|
|
2578
|
+
} else {
|
|
2579
|
+
this.crossFile.set(row.index, graph.href);
|
|
2580
|
+
}
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
const target = this.emitter.graphById(id);
|
|
2584
|
+
if (target === void 0) {
|
|
2585
|
+
this.dangling++;
|
|
2586
|
+
} else {
|
|
2587
|
+
this.names.set(row.index, target.label ?? target.id ?? id);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
/** Write the pointer columns and their warnings. */
|
|
2592
|
+
write() {
|
|
2593
|
+
const prefix = this.emitter.prefix();
|
|
2594
|
+
if (this.dangling > 0) {
|
|
2595
|
+
this.emitter.issues.warning(
|
|
2596
|
+
"validation-error",
|
|
2597
|
+
XGMML_ISSUE.DANGLING_REFERENCE,
|
|
2598
|
+
`${prefix}${this.dangling} nested-network pointer(s) name no graph of the document`
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
const names = new JsonColumn(
|
|
2602
|
+
this.emitter,
|
|
2603
|
+
"node",
|
|
2604
|
+
NESTED_NETWORK_COLUMN,
|
|
2605
|
+
CYTOSCAPE_ORIGIN_NAMESPACE,
|
|
2606
|
+
"nestedNetwork",
|
|
2607
|
+
"string"
|
|
2608
|
+
);
|
|
2609
|
+
for (const [index, name] of this.names) {
|
|
2610
|
+
names.set(index, name);
|
|
2611
|
+
}
|
|
2612
|
+
if (this.crossFile.size > 0) {
|
|
2613
|
+
this.emitter.issues.warning(
|
|
2614
|
+
"unsupported",
|
|
2615
|
+
XGMML_ISSUE.CROSS_FILE_REFERENCE,
|
|
2616
|
+
`${prefix}${this.crossFile.size} nested-network pointer(s) point into another file; each is kept as text in ${NETWORK_POINTER_COLUMN}`
|
|
2617
|
+
);
|
|
2618
|
+
const column = new JsonColumn(
|
|
2619
|
+
this.emitter,
|
|
2620
|
+
"node",
|
|
2621
|
+
NETWORK_POINTER_COLUMN,
|
|
2622
|
+
XGMML_ORIGIN_NAMESPACE,
|
|
2623
|
+
"xlink:href",
|
|
2624
|
+
"string"
|
|
2625
|
+
);
|
|
2626
|
+
for (const [index, href] of this.crossFile) {
|
|
2627
|
+
column.set(index, href);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
class Containment {
|
|
2633
|
+
/**
|
|
2634
|
+
* Create the resolver.
|
|
2635
|
+
* @param emitter - the emitter
|
|
2636
|
+
*/
|
|
2637
|
+
constructor(emitter) {
|
|
2638
|
+
this.emitter = emitter;
|
|
2639
|
+
this.parents = /* @__PURE__ */ new Map();
|
|
2640
|
+
this.subgraphs = /* @__PURE__ */ new Map();
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Collect the memberships of every group node of the graph.
|
|
2644
|
+
* @param rows - the node rows
|
|
2645
|
+
* @param isGroup - the group test
|
|
2646
|
+
*/
|
|
2647
|
+
resolve(rows, isGroup) {
|
|
2648
|
+
let unknown = 0;
|
|
2649
|
+
for (const row of rows) {
|
|
2650
|
+
for (const record of row.records) {
|
|
2651
|
+
if (row.index < 0 || record.nested.length === 0 || !isGroup(record)) {
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
for (const graph of record.nested) {
|
|
2655
|
+
const atts = graph.atts.filter((a) => a.name !== null);
|
|
2656
|
+
if (graph.id !== null || graph.label !== null || atts.length > 0) {
|
|
2657
|
+
this.subgraphs.set(row.index, {
|
|
2658
|
+
id: graph.id,
|
|
2659
|
+
label: graph.label,
|
|
2660
|
+
atts: Object.fromEntries(atts.map((a) => [a.name, attJson(a)]))
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
for (const member of graph.members) {
|
|
2664
|
+
if (member.kind !== "node") {
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
2667
|
+
const id = member.id ?? (member.href === null ? null : localRef(member.href));
|
|
2668
|
+
const child = id === null ? void 0 : this.emitter.rowOf(id);
|
|
2669
|
+
if (child === void 0) {
|
|
2670
|
+
if (id === null || !this.emitter.missingMember(row.id, id)) {
|
|
2671
|
+
unknown++;
|
|
2672
|
+
}
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
if (child.index >= 0) {
|
|
2676
|
+
this.link(child.index, row.index, member.line);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
if (unknown > 0) {
|
|
2683
|
+
this.emitter.issues.error(
|
|
2684
|
+
"missing-value",
|
|
2685
|
+
XGMML_ISSUE.UNKNOWN_PARENT,
|
|
2686
|
+
`${this.emitter.prefix()}${unknown} group member(s) name no node of this graph`
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Add one membership unless it closes a parent cycle.
|
|
2692
|
+
* @param child - the member's index
|
|
2693
|
+
* @param parent - the group node's index
|
|
2694
|
+
* @param line - the line
|
|
2695
|
+
*/
|
|
2696
|
+
link(child, parent, line) {
|
|
2697
|
+
if (child === parent || this.isAncestor(child, parent)) {
|
|
2698
|
+
this.emitter.issues.error(
|
|
2699
|
+
"validation-error",
|
|
2700
|
+
XGMML_ISSUE.PARENT_CYCLE,
|
|
2701
|
+
`${this.emitter.prefix()}a group membership would make a node its own ancestor; that membership is dropped`,
|
|
2702
|
+
{ line }
|
|
2703
|
+
);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const list = this.parents.get(child) ?? [];
|
|
2707
|
+
if (!list.includes(parent)) {
|
|
2708
|
+
list.push(parent);
|
|
2709
|
+
}
|
|
2710
|
+
this.parents.set(child, list);
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Whether a node is an ancestor of another through the memberships so far.
|
|
2714
|
+
* @param ancestor - the candidate ancestor
|
|
2715
|
+
* @param node - the node
|
|
2716
|
+
* @returns true when following parents from node reaches ancestor
|
|
2717
|
+
*/
|
|
2718
|
+
isAncestor(ancestor, node) {
|
|
2719
|
+
const stack = [node];
|
|
2720
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2721
|
+
while (stack.length > 0) {
|
|
2722
|
+
const n = stack.pop();
|
|
2723
|
+
if (n === ancestor) {
|
|
2724
|
+
return true;
|
|
2725
|
+
}
|
|
2726
|
+
if (seen.has(n)) {
|
|
2727
|
+
continue;
|
|
2728
|
+
}
|
|
2729
|
+
seen.add(n);
|
|
2730
|
+
stack.push(...this.parents.get(n) ?? []);
|
|
2731
|
+
}
|
|
2732
|
+
return false;
|
|
2733
|
+
}
|
|
2734
|
+
/** Declare and write the parent (or parents) and subgraph columns. */
|
|
2735
|
+
write() {
|
|
2736
|
+
const sink = this.emitter.target;
|
|
2737
|
+
if (this.parents.size > 0) {
|
|
2738
|
+
const multi = [...this.parents.values()].some((list) => list.length > 1);
|
|
2739
|
+
const handle = this.emitter.declare("node", {
|
|
2740
|
+
name: multi ? PARENTS_COLUMN : PARENT_COLUMN,
|
|
2741
|
+
dtype: multi ? "list" : "u32",
|
|
2742
|
+
itemDtype: multi ? "u32" : void 0,
|
|
2743
|
+
role: multi ? "parents" : "parent",
|
|
2744
|
+
refersTo: "node",
|
|
2745
|
+
nullable: true,
|
|
2746
|
+
origin: { format: FORMAT, id: null, title: null, type: "graph", namespace: null }
|
|
2747
|
+
});
|
|
2748
|
+
if (handle !== null) {
|
|
2749
|
+
for (const [child, list] of this.parents) {
|
|
2750
|
+
sink.setNodeValue(handle, child, multi ? list : list[0]);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (this.subgraphs.size > 0) {
|
|
2755
|
+
const column = new JsonColumn(this.emitter, "node", SUBGRAPH_COLUMN, XGMML_ORIGIN_NAMESPACE, "graph");
|
|
2756
|
+
for (const [index, value] of this.subgraphs) {
|
|
2757
|
+
column.set(index, value);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
const FORMAT_DEFAULTS = {
|
|
2763
|
+
ids: "keep",
|
|
2764
|
+
defaultDirected: false,
|
|
2765
|
+
weightFrom: "weight",
|
|
2766
|
+
addMissingNodes: false
|
|
2767
|
+
};
|
|
2768
|
+
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
2769
|
+
"ids",
|
|
2770
|
+
"addMissingNodes",
|
|
2771
|
+
"duplicateEdges",
|
|
2772
|
+
"selfLoops",
|
|
2773
|
+
"onMixedDirection",
|
|
2774
|
+
"defaultDirected",
|
|
2775
|
+
"weightFrom",
|
|
2776
|
+
"weightDtype",
|
|
2777
|
+
"long",
|
|
2778
|
+
"errorLimit",
|
|
2779
|
+
"signal",
|
|
2780
|
+
"onProgress",
|
|
2781
|
+
"encoding"
|
|
2782
|
+
]);
|
|
2783
|
+
const SNIFF_BYTES = 4096;
|
|
2784
|
+
const HEAD_CHARS = 2048;
|
|
2785
|
+
const XGMML_DOCTYPE = /<!DOCTYPE\s+graph\s[^<>]*xgmml\.dtd/i;
|
|
2786
|
+
function resolveSettings(options, cytoscape) {
|
|
2787
|
+
const zAs = options?.zAs ?? "column";
|
|
2788
|
+
if (zAs !== "column" && zAs !== "position") {
|
|
2789
|
+
throw new GraphFormatError(
|
|
2790
|
+
"E_UNSUPPORTED",
|
|
2791
|
+
`option zAs: ${JSON.stringify(zAs)} is not "column" or "position"`,
|
|
2792
|
+
{
|
|
2793
|
+
option: "zAs",
|
|
2794
|
+
found: zAs
|
|
2795
|
+
}
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
return {
|
|
2799
|
+
labelAliases: booleanOption("labelAliases", options?.labelAliases, cytoscape),
|
|
2800
|
+
cytoscapeEscapes: booleanOption("cytoscapeEscapes", options?.cytoscapeEscapes, cytoscape),
|
|
2801
|
+
zAs
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
function booleanOption(name, value, fallback) {
|
|
2805
|
+
if (value === void 0) {
|
|
2806
|
+
return fallback;
|
|
2807
|
+
}
|
|
2808
|
+
if (typeof value !== "boolean") {
|
|
2809
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: ${JSON.stringify(value)} is not a boolean`, {
|
|
2810
|
+
option: name,
|
|
2811
|
+
found: value
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
return value;
|
|
2815
|
+
}
|
|
2816
|
+
async function parseXgmml(input, report, common, options) {
|
|
2817
|
+
const repairs = {
|
|
2818
|
+
bareAmpersand: booleanOption("repairBareAmpersands", options?.repairBareAmpersands, false) ? (line) => {
|
|
2819
|
+
report.warning("parse-error", XGMML_ISSUE.AMPERSAND_REPAIRED, "a bare & was read as &", { line });
|
|
2820
|
+
} : void 0,
|
|
2821
|
+
surrogatePair: booleanOption("pairSurrogateReferences", options?.pairSurrogateReferences, false) ? (line) => {
|
|
2822
|
+
report.warning(
|
|
2823
|
+
"parse-error",
|
|
2824
|
+
XGMML_ISSUE.SURROGATE_PAIRED,
|
|
2825
|
+
"two surrogate character references were joined into one character",
|
|
2826
|
+
{ line }
|
|
2827
|
+
);
|
|
2828
|
+
} : void 0
|
|
2829
|
+
};
|
|
2830
|
+
const parser = new XgmmlParser(report);
|
|
2831
|
+
const seen = { head: "", content: false };
|
|
2832
|
+
try {
|
|
2833
|
+
await tokenizeXml(
|
|
2834
|
+
watch(textChunks(input, report, { ...common, declaredEncoding: xmlDeclaredEncoding }), seen),
|
|
2835
|
+
parser,
|
|
2836
|
+
repairs
|
|
2837
|
+
);
|
|
2838
|
+
} catch (err) {
|
|
2839
|
+
if (err instanceof XmlSyntaxError) {
|
|
2840
|
+
if (!seen.content) {
|
|
2841
|
+
report.fail(XGMML_ISSUE.EMPTY_INPUT, "the input is empty");
|
|
2842
|
+
}
|
|
2843
|
+
report.fail(XGMML_ISSUE.XML_SYNTAX, err.message, { line: err.line });
|
|
2844
|
+
}
|
|
2845
|
+
throw err;
|
|
2846
|
+
}
|
|
2847
|
+
const doc = parser.document();
|
|
2848
|
+
if (!doc.xgmmlNamespace && !XGMML_DOCTYPE.test(seen.head)) {
|
|
2849
|
+
report.warning(
|
|
2850
|
+
"validation-error",
|
|
2851
|
+
XGMML_ISSUE.NO_NAMESPACE,
|
|
2852
|
+
"the root <graph> has neither the XGMML namespace nor an XGMML DOCTYPE; it is read as XGMML",
|
|
2853
|
+
{ line: doc.root.line }
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
return doc;
|
|
2857
|
+
}
|
|
2858
|
+
async function* watch(chunks, seen) {
|
|
2859
|
+
for await (const chunk of chunks) {
|
|
2860
|
+
if (seen.head.length < HEAD_CHARS) {
|
|
2861
|
+
seen.head += chunk.slice(0, HEAD_CHARS - seen.head.length);
|
|
2862
|
+
}
|
|
2863
|
+
seen.content ||= !isWhitespace(chunk);
|
|
2864
|
+
yield chunk;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
async function prepare(input, sink, options) {
|
|
2868
|
+
const common = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
2869
|
+
const report = new ImportReportBuilder(FORMAT, common.errorLimit);
|
|
2870
|
+
if (sink !== null) {
|
|
2871
|
+
reportSinkOptions(sink, options, report, true);
|
|
2872
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
2873
|
+
}
|
|
2874
|
+
resolveSettings(options, false);
|
|
2875
|
+
const doc = await parseXgmml(input, report, common, options);
|
|
2876
|
+
const dialect = dialectOf(doc, report);
|
|
2877
|
+
if (dialect.view) {
|
|
2878
|
+
report.fail(
|
|
2879
|
+
XGMML_ISSUE.VIEW_DOCUMENT,
|
|
2880
|
+
"this is a Cytoscape session view file (cy:view): it holds positions keyed by view ids and no topology; open the session (.cys) instead",
|
|
2881
|
+
{ line: doc.root.line }
|
|
2882
|
+
);
|
|
2883
|
+
}
|
|
2884
|
+
return {
|
|
2885
|
+
report,
|
|
2886
|
+
common,
|
|
2887
|
+
doc,
|
|
2888
|
+
dialect,
|
|
2889
|
+
settings: resolveSettings(options, doc.cytoscape),
|
|
2890
|
+
graphs: graphsOf(doc, dialect)
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
function emitOne(prepared, graph, sink, extras) {
|
|
2894
|
+
const { doc, dialect, report, common, settings } = prepared;
|
|
2895
|
+
const emitter = new XgmmlEmitter(doc, dialect, sink, report, common, settings, extras);
|
|
2896
|
+
emitter.emit(dialect.session ? graph : null);
|
|
2897
|
+
if (dialect.session) {
|
|
2898
|
+
reportRootOnly(prepared);
|
|
2899
|
+
}
|
|
2900
|
+
throwIfAborted(common.signal);
|
|
2901
|
+
return report.finish();
|
|
2902
|
+
}
|
|
2903
|
+
function reportRootOnly(prepared) {
|
|
2904
|
+
const held = /* @__PURE__ */ new Set();
|
|
2905
|
+
for (const graph of prepared.graphs) {
|
|
2906
|
+
const members = membersOf(prepared.doc, graph);
|
|
2907
|
+
members.nodes.forEach((n) => held.add(n));
|
|
2908
|
+
members.edges.forEach((e) => held.add(e));
|
|
2909
|
+
}
|
|
2910
|
+
const nodes = prepared.doc.nodes.filter((n) => !held.has(n)).length;
|
|
2911
|
+
const edges = prepared.doc.edges.filter((e) => !held.has(e)).length;
|
|
2912
|
+
if (nodes + edges > 0) {
|
|
2913
|
+
prepared.report.warning(
|
|
2914
|
+
"unsupported",
|
|
2915
|
+
XGMML_ISSUE.ROOT_ONLY_ELEMENTS,
|
|
2916
|
+
`${nodes} node(s) and ${edges} edge(s) belong to no registered network (group meta-edges, collapsed group members) and were not read`
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
async function importXgmml(input, sink, options) {
|
|
2921
|
+
const prepared = await prepare(input, sink, options);
|
|
2922
|
+
const index = chooseGraph(
|
|
2923
|
+
prepared.graphs.map((g) => graphName(g, prepared.doc)),
|
|
2924
|
+
options,
|
|
2925
|
+
prepared.report
|
|
2926
|
+
);
|
|
2927
|
+
if (prepared.graphs.length > 1) {
|
|
2928
|
+
prepared.report.warning(
|
|
2929
|
+
"unsupported",
|
|
2930
|
+
XGMML_ISSUE.MULTIPLE_GRAPHS,
|
|
2931
|
+
`the session network document holds ${prepared.graphs.length} registered networks; ${prepared.graphs.length - 1} were not read (use importAll, graphIndex or graphName)`
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
return emitOne(prepared, prepared.graphs[index], sink);
|
|
2935
|
+
}
|
|
2936
|
+
async function importAllXgmml(input, sinkFor, options) {
|
|
2937
|
+
const bytes = await reread(input);
|
|
2938
|
+
const first = await prepare(bytes, null, options);
|
|
2939
|
+
const reports = [];
|
|
2940
|
+
for (let i = 0; i < first.graphs.length; i++) {
|
|
2941
|
+
const sink = sinkFor(i);
|
|
2942
|
+
const prepared = i === 0 ? first : await prepare(bytes, null, options);
|
|
2943
|
+
reportSinkOptions(sink, options, prepared.report, true);
|
|
2944
|
+
reportUnusedOptions(options, prepared.report, USED_OPTIONS);
|
|
2945
|
+
reports.push(emitOne(prepared, prepared.graphs[i], sink));
|
|
2946
|
+
}
|
|
2947
|
+
return reports;
|
|
2948
|
+
}
|
|
2949
|
+
async function listXgmmlGraphs(input, options) {
|
|
2950
|
+
const prepared = await prepare(input, null, options);
|
|
2951
|
+
return prepared.graphs.map((graph, index) => {
|
|
2952
|
+
const members = membersOf(prepared.doc, prepared.dialect.session ? graph : null);
|
|
2953
|
+
return Object.freeze({
|
|
2954
|
+
index,
|
|
2955
|
+
name: graphName(graph, prepared.doc),
|
|
2956
|
+
nodes: new Set(members.nodes.map((n) => n.id)).size,
|
|
2957
|
+
edges: members.edges.length
|
|
2958
|
+
});
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
function graphName(graph, doc) {
|
|
2962
|
+
return graph.label ?? (graph === doc.root ? doc.rdf.title ?? null : null) ?? graph.id ?? null;
|
|
2963
|
+
}
|
|
2964
|
+
async function reread(input) {
|
|
2965
|
+
if (typeof input === "string" || input instanceof Uint8Array) {
|
|
2966
|
+
return input;
|
|
2967
|
+
}
|
|
2968
|
+
const parts = [];
|
|
2969
|
+
const iterable = typeof input.getReader === "function" ? streamIterable(input) : input;
|
|
2970
|
+
for await (const chunk of iterable) {
|
|
2971
|
+
parts.push(chunk);
|
|
2972
|
+
}
|
|
2973
|
+
if (parts.every((p) => typeof p === "string")) {
|
|
2974
|
+
return parts.join("");
|
|
2975
|
+
}
|
|
2976
|
+
const bytes = parts.map((p) => typeof p === "string" ? new TextEncoder().encode(p) : p);
|
|
2977
|
+
const out = new Uint8Array(bytes.reduce((n, b) => n + b.byteLength, 0));
|
|
2978
|
+
let at = 0;
|
|
2979
|
+
for (const b of bytes) {
|
|
2980
|
+
out.set(b, at);
|
|
2981
|
+
at += b.byteLength;
|
|
2982
|
+
}
|
|
2983
|
+
return out;
|
|
2984
|
+
}
|
|
2985
|
+
async function* streamIterable(stream) {
|
|
2986
|
+
const reader = stream.getReader();
|
|
2987
|
+
try {
|
|
2988
|
+
for (; ; ) {
|
|
2989
|
+
const { done, value } = await reader.read();
|
|
2990
|
+
if (done) {
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
yield value;
|
|
2994
|
+
}
|
|
2995
|
+
} finally {
|
|
2996
|
+
reader.releaseLock();
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
function sniffXgmml(head) {
|
|
3000
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(head.subarray(0, SNIFF_BYTES));
|
|
3001
|
+
if (!/^\uFEFF?\s*</.test(text)) {
|
|
3002
|
+
return 0;
|
|
3003
|
+
}
|
|
3004
|
+
if (XGMML_DOCTYPE.test(text)) {
|
|
3005
|
+
return 0.95;
|
|
3006
|
+
}
|
|
3007
|
+
const root = /<(?!\?|!)([A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)[\s/>]/.exec(text.replace(/<!--[\s\S]*?-->/g, ""));
|
|
3008
|
+
if (root === null || root[2] !== "graph") {
|
|
3009
|
+
return 0;
|
|
3010
|
+
}
|
|
3011
|
+
const tagEnd = text.indexOf(">", root.index);
|
|
3012
|
+
const tag = text.slice(root.index, tagEnd < 0 ? void 0 : tagEnd);
|
|
3013
|
+
return tag.includes("http://www.cs.rpi.edu/XGMML") ? 0.95 : 0.5;
|
|
3014
|
+
}
|
|
3015
|
+
const xgmmlImporter = Object.freeze({
|
|
3016
|
+
format: FORMAT,
|
|
3017
|
+
extensions: EXTENSIONS,
|
|
3018
|
+
mimeTypes: MIME_TYPES,
|
|
3019
|
+
sniff: sniffXgmml,
|
|
3020
|
+
import: importXgmml,
|
|
3021
|
+
importAll: importAllXgmml,
|
|
3022
|
+
listGraphs: listXgmmlGraphs
|
|
3023
|
+
});
|
|
3024
|
+
export {
|
|
3025
|
+
CY_NAMESPACE as C,
|
|
3026
|
+
EDGE_ID_COLUMN as E,
|
|
3027
|
+
FORMAT as F,
|
|
3028
|
+
GRAPHICS_COLUMN as G,
|
|
3029
|
+
INTERACTION_COLUMN as I,
|
|
3030
|
+
LABEL_COLUMN as L,
|
|
3031
|
+
META_KEY as M,
|
|
3032
|
+
NESTED_NETWORK_COLUMN as N,
|
|
3033
|
+
POSITION_COLUMN as P,
|
|
3034
|
+
SUBGRAPH_COLUMN as S,
|
|
3035
|
+
XGMML_ISSUE as X,
|
|
3036
|
+
Z_COLUMN as Z,
|
|
3037
|
+
XGMML_LOSS as a,
|
|
3038
|
+
XLINK_NAMESPACE as b,
|
|
3039
|
+
XGMML_NAMESPACE as c,
|
|
3040
|
+
aliasesOf as d,
|
|
3041
|
+
NETWORK_POINTER_COLUMN as e,
|
|
3042
|
+
NETWORKS_COLUMN as f,
|
|
3043
|
+
PARENT_COLUMN as g,
|
|
3044
|
+
PARENTS_COLUMN as h,
|
|
3045
|
+
XGMML_ORIGIN_NAMESPACE as i,
|
|
3046
|
+
CYTOSCAPE_ORIGIN_NAMESPACE as j,
|
|
3047
|
+
isCyTrue as k,
|
|
3048
|
+
XgmmlEmitter as l,
|
|
3049
|
+
membersOf as m,
|
|
3050
|
+
dialectOf as n,
|
|
3051
|
+
parseXgmml as p,
|
|
3052
|
+
resolveSettings as r,
|
|
3053
|
+
xgmmlImporter as x
|
|
3054
|
+
};
|
|
3055
|
+
//# sourceMappingURL=importer-D_e7e7LX.js.map
|