@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,1100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cytoscape session importer (design `design/graph-io/cytoscape-and-obo/design.md` sections
|
|
3
|
+
* 1.4 and 3; `research-session-and-style.md`). A session is a zip of every network of a Cytoscape
|
|
4
|
+
* desktop; the unit of import is the registered subnetwork (3.x) or the network (2.x), one
|
|
5
|
+
* snapshot each: `import()` reads the one `graphIndex` / `graphName` picks (the first by default),
|
|
6
|
+
* `importAll()` every one, and `listGraphs()` lists them from the network files alone.
|
|
7
|
+
*
|
|
8
|
+
* 3.x: the network file gives the topology (shared nodes through `xlink:href`, per-edge direction,
|
|
9
|
+
* groups, nested-network pointers); the subnetwork's CyCSV tables give its columns (LOCAL_ATTRS as
|
|
10
|
+
* they are, the SHARED_ATTRS columns `cytables.xml` joins in, HIDDEN and app tables as hidden
|
|
11
|
+
* columns under their namespace); its first view gives positions (y flipped) and the `graphics`
|
|
12
|
+
* column, further views `position@<n>` columns. 2.x: one full XGMML file per network, with
|
|
13
|
+
* selection and hidden state from `cysession.xml`. Everything is resolved by the XGMML emitter, so
|
|
14
|
+
* both readers share every column rule. Styles are not read (issue #706): W_STYLES_NOT_IMPORTED.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { GraphFormatError, type GraphSink, INVALID_INDEX } from "@graphty/graph-format";
|
|
18
|
+
|
|
19
|
+
import { declareResolved } from "../../common/attributes.js";
|
|
20
|
+
import { IdCoercer } from "../../common/ids.js";
|
|
21
|
+
import { decodeEntryName, readBytes, throwIfAborted } from "../../common/input.js";
|
|
22
|
+
import {
|
|
23
|
+
chooseGraph,
|
|
24
|
+
type ImportFormatDefaults,
|
|
25
|
+
reportSinkOptions,
|
|
26
|
+
reportUnusedOptions,
|
|
27
|
+
type ResolvedImportOptions,
|
|
28
|
+
resolveImportOptions,
|
|
29
|
+
} from "../../common/options.js";
|
|
30
|
+
import { ImportReportBuilder } from "../../common/report.js";
|
|
31
|
+
import { startsLikeZip } from "../../common/zip.js";
|
|
32
|
+
import {
|
|
33
|
+
type CommonImportOptions,
|
|
34
|
+
type GraphChoiceOptions,
|
|
35
|
+
type GraphImporter,
|
|
36
|
+
type GraphListing,
|
|
37
|
+
ImportError,
|
|
38
|
+
type ImportInput,
|
|
39
|
+
type ImportReport,
|
|
40
|
+
} from "../../types.js";
|
|
41
|
+
import { XGMML_ISSUE, XGMML_ORIGIN_NAMESPACE } from "../xgmml/constants.js";
|
|
42
|
+
import {
|
|
43
|
+
type AttRec,
|
|
44
|
+
type EdgeRec,
|
|
45
|
+
type GraphRec,
|
|
46
|
+
isCyTrue,
|
|
47
|
+
type NodeRec,
|
|
48
|
+
type XgmmlDocument,
|
|
49
|
+
} from "../xgmml/document.js";
|
|
50
|
+
import {
|
|
51
|
+
type Dialect,
|
|
52
|
+
dialectOf,
|
|
53
|
+
type EmitExtras,
|
|
54
|
+
membersOf,
|
|
55
|
+
XgmmlEmitter,
|
|
56
|
+
type XgmmlSettings,
|
|
57
|
+
} from "../xgmml/emit.js";
|
|
58
|
+
import { parseXgmml, resolveSettings } from "../xgmml/importer.js";
|
|
59
|
+
import {
|
|
60
|
+
CYS_ISSUE,
|
|
61
|
+
CYTOSCAPE_NAMESPACE,
|
|
62
|
+
DEFAULT_MAX_UNCOMPRESSED,
|
|
63
|
+
EXTENSIONS,
|
|
64
|
+
FORMAT,
|
|
65
|
+
HIDDEN_COLUMN,
|
|
66
|
+
META_KEY,
|
|
67
|
+
MIME_TYPES,
|
|
68
|
+
SELECTED_COLUMN,
|
|
69
|
+
VIEW_POSITION_PREFIX,
|
|
70
|
+
} from "./constants.js";
|
|
71
|
+
import {
|
|
72
|
+
elementsNamed,
|
|
73
|
+
listed,
|
|
74
|
+
type NetworkEntry,
|
|
75
|
+
openSession,
|
|
76
|
+
readXmlTree,
|
|
77
|
+
type Session,
|
|
78
|
+
type SessionEntry,
|
|
79
|
+
type TableEntry,
|
|
80
|
+
urlDecode,
|
|
81
|
+
type XmlNode,
|
|
82
|
+
} from "./session.js";
|
|
83
|
+
import { cellAtt, type CyTable, readCyTable, type VirtualColumn, virtualColumnsOf } from "./tables.js";
|
|
84
|
+
|
|
85
|
+
/** The format-specific options of the session importer. */
|
|
86
|
+
export interface CysImportOptions extends GraphChoiceOptions {
|
|
87
|
+
/** Where Cytoscape's z (a stacking order) goes: the `z` column (default) or the position. */
|
|
88
|
+
zAs?: "column" | "position" | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* The most bytes one import may inflate, in total (default 2 GiB); an entry beyond it, or one
|
|
91
|
+
* whose compression ratio is above 1000:1, is E_TOO_LARGE.
|
|
92
|
+
*/
|
|
93
|
+
maxUncompressedBytes?: number | undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The per-format defaults: SUIDs as written, edges directed unless they say otherwise, no weight. */
|
|
97
|
+
const FORMAT_DEFAULTS: ImportFormatDefaults = {
|
|
98
|
+
ids: "keep",
|
|
99
|
+
defaultDirected: true,
|
|
100
|
+
weightFrom: null,
|
|
101
|
+
addMissingNodes: false,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** The common options the session importer reads. */
|
|
105
|
+
const USED_OPTIONS: ReadonlySet<keyof CommonImportOptions> = new Set<keyof CommonImportOptions>([
|
|
106
|
+
"ids",
|
|
107
|
+
"addMissingNodes",
|
|
108
|
+
"duplicateEdges",
|
|
109
|
+
"selfLoops",
|
|
110
|
+
"onMixedDirection",
|
|
111
|
+
"defaultDirected",
|
|
112
|
+
"weightFrom",
|
|
113
|
+
"weightDtype",
|
|
114
|
+
"long",
|
|
115
|
+
"errorLimit",
|
|
116
|
+
"signal",
|
|
117
|
+
"onProgress",
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
/** One network the session holds: a registered subnetwork of a 3.x network file, or a 2.x network. */
|
|
121
|
+
type Choice = Choice3 | Choice2;
|
|
122
|
+
|
|
123
|
+
/** A registered subnetwork of a 3.x network file. */
|
|
124
|
+
interface Choice3 {
|
|
125
|
+
readonly era: "3";
|
|
126
|
+
readonly network: NetworkEntry;
|
|
127
|
+
readonly graphId: string;
|
|
128
|
+
readonly name: string | null;
|
|
129
|
+
readonly nodes: number;
|
|
130
|
+
readonly edges: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A 2.x network and its `cysession.xml` record. */
|
|
134
|
+
interface Choice2 {
|
|
135
|
+
readonly era: "2";
|
|
136
|
+
readonly file: SessionEntry;
|
|
137
|
+
readonly name: string;
|
|
138
|
+
readonly record: XmlNode;
|
|
139
|
+
readonly parent: string | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** What every import call sets up. */
|
|
143
|
+
interface Prepared {
|
|
144
|
+
readonly report: ImportReportBuilder;
|
|
145
|
+
readonly common: ResolvedImportOptions;
|
|
146
|
+
/** The options the entries inside are read with: no progress, no encoding override. */
|
|
147
|
+
readonly inner: ResolvedImportOptions;
|
|
148
|
+
readonly zAs: "column" | "position";
|
|
149
|
+
readonly session: Session;
|
|
150
|
+
readonly choices: readonly Choice[];
|
|
151
|
+
/** The network file documents parsed for the listing, by entry name (3.x). */
|
|
152
|
+
readonly docs: Map<string, Parsed>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A parsed XGMML entry. */
|
|
156
|
+
interface Parsed {
|
|
157
|
+
readonly doc: XgmmlDocument;
|
|
158
|
+
readonly dialect: Dialect;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The maxUncompressedBytes option, checked.
|
|
163
|
+
* @param value - the caller's value
|
|
164
|
+
* @returns the byte budget; E_UNSUPPORTED for anything but a positive number
|
|
165
|
+
*/
|
|
166
|
+
function budgetOption(value: unknown): number {
|
|
167
|
+
if (value === undefined) {
|
|
168
|
+
return DEFAULT_MAX_UNCOMPRESSED;
|
|
169
|
+
}
|
|
170
|
+
if (typeof value !== "number" || !(value > 0)) {
|
|
171
|
+
throw new GraphFormatError(
|
|
172
|
+
"E_UNSUPPORTED",
|
|
173
|
+
`option maxUncompressedBytes: ${JSON.stringify(value)} is not a positive number`,
|
|
174
|
+
{
|
|
175
|
+
option: "maxUncompressedBytes",
|
|
176
|
+
found: value,
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolve the options, open the archive and list its networks.
|
|
185
|
+
* @param input - the input
|
|
186
|
+
* @param sink - the sink, or null for listGraphs
|
|
187
|
+
* @param options - the options
|
|
188
|
+
* @returns what the import needs
|
|
189
|
+
*/
|
|
190
|
+
async function prepare(
|
|
191
|
+
input: ImportInput,
|
|
192
|
+
sink: GraphSink | null,
|
|
193
|
+
options: (CysImportOptions & CommonImportOptions) | undefined,
|
|
194
|
+
): Promise<Prepared> {
|
|
195
|
+
const common = resolveImportOptions(options, FORMAT_DEFAULTS);
|
|
196
|
+
const report = new ImportReportBuilder(FORMAT, common.errorLimit);
|
|
197
|
+
if (sink !== null) {
|
|
198
|
+
reportSinkOptions(sink, options, report, true);
|
|
199
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
200
|
+
}
|
|
201
|
+
const { zAs } = resolveSettings({ zAs: options?.zAs }, false);
|
|
202
|
+
const maxBytes = budgetOption(options?.maxUncompressedBytes);
|
|
203
|
+
const session = await openSession(input, report, common, maxBytes);
|
|
204
|
+
const inner: ResolvedImportOptions = Object.freeze({ ...common, onProgress: null, encoding: null });
|
|
205
|
+
const docs = new Map<string, Parsed>();
|
|
206
|
+
const choices =
|
|
207
|
+
session.layout.era === "3"
|
|
208
|
+
? await networks3(session, report, inner, docs)
|
|
209
|
+
: await networks2(session, report, inner);
|
|
210
|
+
return { report, common, inner, zAs, session, choices, docs };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Read an XGMML entry. Its issues are relayed into the import's report with the entry name in
|
|
215
|
+
* the message; a fatal one fails the import.
|
|
216
|
+
* @param session - the session
|
|
217
|
+
* @param entry - the entry
|
|
218
|
+
* @param report - the import's report
|
|
219
|
+
* @param inner - the options entries are read with
|
|
220
|
+
* @returns the document and its dialect
|
|
221
|
+
*/
|
|
222
|
+
async function parseEntry(
|
|
223
|
+
session: Session,
|
|
224
|
+
entry: SessionEntry,
|
|
225
|
+
report: ImportReportBuilder,
|
|
226
|
+
inner: ResolvedImportOptions,
|
|
227
|
+
): Promise<Parsed> {
|
|
228
|
+
const bytes = await session.read(entry);
|
|
229
|
+
const scratch = new ImportReportBuilder(FORMAT, Number.MAX_SAFE_INTEGER);
|
|
230
|
+
try {
|
|
231
|
+
const doc = await parseXgmml(bytes, scratch, inner, undefined);
|
|
232
|
+
const dialect = dialectOf(doc, scratch);
|
|
233
|
+
relay(scratch.issues, report, entry.name);
|
|
234
|
+
return { doc, dialect };
|
|
235
|
+
} catch (err) {
|
|
236
|
+
if (!(err instanceof ImportError)) {
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
const { issues } = scratch;
|
|
240
|
+
const fatal = issues.at(-1);
|
|
241
|
+
relay(issues.slice(0, -1), report, entry.name);
|
|
242
|
+
return report.fail(fatal?.code ?? CYS_ISSUE.CORRUPT, `${entry.name}: ${fatal?.message ?? err.message}`, {
|
|
243
|
+
line: fatal?.line ?? null,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Record issues of an entry in the import's report, the entry name before each message.
|
|
250
|
+
* @param issues - the issues
|
|
251
|
+
* @param report - the import's report
|
|
252
|
+
* @param entry - the entry name
|
|
253
|
+
*/
|
|
254
|
+
function relay(issues: ImportReport["issues"], report: ImportReportBuilder, entry: string): void {
|
|
255
|
+
for (const issue of issues) {
|
|
256
|
+
const where = { line: issue.line, element: issue.element };
|
|
257
|
+
if (issue.severity === "error") {
|
|
258
|
+
report.error(issue.category, issue.code, `${entry}: ${issue.message}`, where);
|
|
259
|
+
} else {
|
|
260
|
+
report.warning(issue.category, issue.code, `${entry}: ${issue.message}`, where);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------------------- 3.x
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The registered subnetworks of a 3.x session, in `network_list.xml` order (else entry order).
|
|
269
|
+
* @param session - the session
|
|
270
|
+
* @param report - the report
|
|
271
|
+
* @param inner - the options entries are read with
|
|
272
|
+
* @param docs - where the parsed network files are kept
|
|
273
|
+
* @returns the networks
|
|
274
|
+
*/
|
|
275
|
+
async function networks3(
|
|
276
|
+
session: Session,
|
|
277
|
+
report: ImportReportBuilder,
|
|
278
|
+
inner: ResolvedImportOptions,
|
|
279
|
+
docs: Map<string, Parsed>,
|
|
280
|
+
): Promise<Choice3[]> {
|
|
281
|
+
const { layout } = session;
|
|
282
|
+
const choices: Choice3[] = [];
|
|
283
|
+
for (const network of layout.networks) {
|
|
284
|
+
const parsed = await parseEntry(session, network, report, inner);
|
|
285
|
+
docs.set(network.name, parsed);
|
|
286
|
+
for (const graph of registeredGraphs(parsed)) {
|
|
287
|
+
const members = membersOf(parsed.doc, graph);
|
|
288
|
+
choices.push({
|
|
289
|
+
era: "3",
|
|
290
|
+
network,
|
|
291
|
+
graphId: graph.id ?? "",
|
|
292
|
+
name: graph.label ?? graph.id,
|
|
293
|
+
nodes: new Set(members.nodes).size,
|
|
294
|
+
edges: members.edges.length,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const known = new Set(choices.map((c) => c.graphId));
|
|
299
|
+
const strayViews = layout.views.filter((v) => !known.has(v.network)).map((v) => v.name);
|
|
300
|
+
const strayTables = layout.tables.filter(
|
|
301
|
+
(t) =>
|
|
302
|
+
!known.has(t.network) &&
|
|
303
|
+
!layout.networks.some((n) => n.suid === t.network) &&
|
|
304
|
+
!isGroupTable(t, session, docs),
|
|
305
|
+
);
|
|
306
|
+
if (strayViews.length > 0 || strayTables.length > 0) {
|
|
307
|
+
report.warning(
|
|
308
|
+
"validation-error",
|
|
309
|
+
CYS_ISSUE.DANGLING_REFERENCE,
|
|
310
|
+
`${strayViews.length + strayTables.length} view(s) or table(s) belong to a network the session does not hold and were not read: ${listed([...strayViews, ...strayTables.map((t) => t.name)])}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (layout.networkList === null) {
|
|
314
|
+
return choices;
|
|
315
|
+
}
|
|
316
|
+
const tree = await readXmlTree(await session.read(layout.networkList), layout.networkList.name, report, inner);
|
|
317
|
+
const order = new Map<string, number>();
|
|
318
|
+
for (const node of elementsNamed(tree, "network")) {
|
|
319
|
+
const id = node.attrs.get("id");
|
|
320
|
+
const position = Number(node.attrs.get("order"));
|
|
321
|
+
if (id !== undefined && Number.isFinite(position)) {
|
|
322
|
+
order.set(id, position);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return choices
|
|
326
|
+
.map((choice, index) => ({ choice, index }))
|
|
327
|
+
.sort(
|
|
328
|
+
(a, b) =>
|
|
329
|
+
(order.get(a.choice.graphId) ?? Infinity) - (order.get(b.choice.graphId) ?? Infinity) ||
|
|
330
|
+
a.index - b.index,
|
|
331
|
+
)
|
|
332
|
+
.map((c) => c.choice);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Whether a table belongs to a graph declared in some network file (a group's network): those are
|
|
337
|
+
* read with their network, not reported.
|
|
338
|
+
* @param table - the table
|
|
339
|
+
* @param session - the session
|
|
340
|
+
* @param docs - the parsed network files
|
|
341
|
+
* @returns true for a table of a known graph
|
|
342
|
+
*/
|
|
343
|
+
function isGroupTable(table: TableEntry, session: Session, docs: ReadonlyMap<string, Parsed>): boolean {
|
|
344
|
+
for (const network of session.layout.networks) {
|
|
345
|
+
if (docs.get(network.name)?.doc.graphs.some((g) => g.id === table.network) === true) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The registered subnetworks of a network file (`cy:registered` true under the root).
|
|
354
|
+
* @param parsed - the network file
|
|
355
|
+
* @returns the graphs
|
|
356
|
+
*/
|
|
357
|
+
function registeredGraphs(parsed: Parsed): GraphRec[] {
|
|
358
|
+
return parsed.doc.root.subgraphs.filter((g) => isCyTrue(g.registered));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Import one registered subnetwork of a 3.x session.
|
|
363
|
+
* @param prepared - the prepared import
|
|
364
|
+
* @param choice - the subnetwork
|
|
365
|
+
* @param sink - the sink
|
|
366
|
+
* @param parsed - its network file, parsed for this import (its records are filled in)
|
|
367
|
+
*/
|
|
368
|
+
async function import3(prepared: Prepared, choice: Choice3, sink: GraphSink, parsed: Parsed): Promise<void> {
|
|
369
|
+
const { report, inner, session } = prepared;
|
|
370
|
+
const { layout } = session;
|
|
371
|
+
const { doc, dialect } = parsed;
|
|
372
|
+
const graph = doc.root.subgraphs.find((g) => g.id === choice.graphId) as GraphRec;
|
|
373
|
+
const members = membersOf(doc, graph);
|
|
374
|
+
const nodes = byId(members.nodes);
|
|
375
|
+
const edges = byId(members.edges);
|
|
376
|
+
const cache = new Map<string, Promise<CyTable | null>>();
|
|
377
|
+
const tableOf = (path: string): Promise<CyTable | null> => {
|
|
378
|
+
let pending = cache.get(path);
|
|
379
|
+
if (pending === undefined) {
|
|
380
|
+
const entry = layout.tables.find((t) => t.tablePath === path);
|
|
381
|
+
pending =
|
|
382
|
+
entry === undefined
|
|
383
|
+
? Promise.resolve(null)
|
|
384
|
+
: session.read(entry).then((bytes) => readCyTable(bytes, path, entry.name, report, inner));
|
|
385
|
+
cache.set(path, pending);
|
|
386
|
+
}
|
|
387
|
+
return pending;
|
|
388
|
+
};
|
|
389
|
+
const virtuals = layout.cytables === null ? [] : await readVirtuals(session, layout.cytables, report, inner);
|
|
390
|
+
// a row of an element the file declares outside this network (a collapsed group's member, a
|
|
391
|
+
// meta-edge) is Cytoscape's bookkeeping, not a stale row
|
|
392
|
+
const declared = new Set<string>([
|
|
393
|
+
...doc.nodes.flatMap((n) => (n.id === null ? [] : [n.id])),
|
|
394
|
+
...doc.edges.flatMap((e) => (e.id === null ? [] : [e.id])),
|
|
395
|
+
]);
|
|
396
|
+
let unmatched = 0;
|
|
397
|
+
for (const entry of layout.tables.filter((t) => t.network === choice.graphId && t.element !== null)) {
|
|
398
|
+
throwIfAborted(inner.signal);
|
|
399
|
+
const table = await tableOf(entry.tablePath);
|
|
400
|
+
if (table === null) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const local = entry.namespace === "LOCAL_ATTRS";
|
|
404
|
+
const shared = entry.namespace === "SHARED_ATTRS";
|
|
405
|
+
const namespace = local ? null : entry.namespace;
|
|
406
|
+
const hidden = !local && !shared;
|
|
407
|
+
const target = (key: string): AttRec[] | undefined => {
|
|
408
|
+
if (entry.element === "network") {
|
|
409
|
+
return key === choice.graphId ? graph.atts : undefined;
|
|
410
|
+
}
|
|
411
|
+
return (entry.element === "node" ? nodes : edges).get(key)?.atts;
|
|
412
|
+
};
|
|
413
|
+
for (const [key, cells] of table.rows) {
|
|
414
|
+
const atts = target(key);
|
|
415
|
+
if (atts === undefined) {
|
|
416
|
+
if (!declared.has(key)) {
|
|
417
|
+
unmatched++;
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const line = table.lines.get(key) ?? 0;
|
|
422
|
+
for (let i = 1; i < table.columns.length && i < cells.length; i++) {
|
|
423
|
+
const att = cellAtt(table.columns[i], cells[i], line, namespace, hidden);
|
|
424
|
+
if (att !== null) {
|
|
425
|
+
atts.push(att);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
for (const virtual of await virtualColumnsOf(table, virtuals, tableOf, report)) {
|
|
430
|
+
// a shared column named like a local one is renamed; the local one keeps the name
|
|
431
|
+
const owned = table.columns.some((c) => c.name === virtual.column.name);
|
|
432
|
+
let ns = namespace;
|
|
433
|
+
if (local) {
|
|
434
|
+
ns = owned ? "SHARED_ATTRS" : null;
|
|
435
|
+
}
|
|
436
|
+
for (const [key, text] of virtual.values) {
|
|
437
|
+
const att = cellAtt(virtual.column, text, table.lines.get(key) ?? 0, ns, hidden);
|
|
438
|
+
if (att !== null) {
|
|
439
|
+
target(key)?.push(att);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (unmatched > 0) {
|
|
445
|
+
report.warning(
|
|
446
|
+
"parse-error",
|
|
447
|
+
CYS_ISSUE.TABLE_ROW,
|
|
448
|
+
`${unmatched} table row(s) of network ${choice.graphId} name no node, edge or network of it (stale rows); they are not read`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
const views = layout.views.filter((v) => v.network === choice.graphId);
|
|
452
|
+
let visualStyle: string | null = null;
|
|
453
|
+
const extraViews: Map<string, [number, number, number]>[] = [];
|
|
454
|
+
for (const [i, view] of views.entries()) {
|
|
455
|
+
const viewDoc = (await parseEntry(session, view, report, inner)).doc;
|
|
456
|
+
if (i === 0) {
|
|
457
|
+
visualStyle = viewDoc.root.attrs.get("cy:visualStyle") ?? null;
|
|
458
|
+
applyView(viewDoc, graph, nodes, edges, report, view.name);
|
|
459
|
+
} else {
|
|
460
|
+
extraViews.push(viewPositions(viewDoc, prepared.zAs));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const cyMeta = sessionMeta(prepared, {
|
|
464
|
+
collection: doc.root.label ?? choice.network.title,
|
|
465
|
+
network: choice.graphId,
|
|
466
|
+
parentNetwork: parentNetwork(graph, prepared.choices),
|
|
467
|
+
visualStyle,
|
|
468
|
+
});
|
|
469
|
+
const groups: { group: string; members: string[] }[] = [];
|
|
470
|
+
cyMeta.groups = groups;
|
|
471
|
+
const extras: EmitExtras = {
|
|
472
|
+
sourceFormat: FORMAT,
|
|
473
|
+
entry: choice.network.name,
|
|
474
|
+
graphName: localName(graph) ?? choice.name,
|
|
475
|
+
metaExtra: { [META_KEY]: cyMeta },
|
|
476
|
+
groupNodes: new Set(
|
|
477
|
+
members.nodes
|
|
478
|
+
.filter((n) => n.atts.some((a) => a.name === "__isGroup" && isCyTrue(a.value)))
|
|
479
|
+
.flatMap((n) => (n.id === null ? [] : [n.id])),
|
|
480
|
+
),
|
|
481
|
+
resolvePointer: pointerResolver(prepared, doc),
|
|
482
|
+
onMissingMember: (group, member): void => {
|
|
483
|
+
const record = groups.find((g) => g.group === group);
|
|
484
|
+
if (record === undefined) {
|
|
485
|
+
groups.push({ group, members: [member] });
|
|
486
|
+
} else if (!record.members.includes(member)) {
|
|
487
|
+
record.members.push(member);
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
const settings: XgmmlSettings = { labelAliases: false, cytoscapeEscapes: false, zAs: prepared.zAs };
|
|
492
|
+
new XgmmlEmitter(doc, dialect, sink, report, prepared.inner, settings, extras).emit(graph);
|
|
493
|
+
if (groups.length > 0) {
|
|
494
|
+
report.warning(
|
|
495
|
+
"unsupported",
|
|
496
|
+
CYS_ISSUE.COLLAPSED_GROUP,
|
|
497
|
+
`${groups.length} collapsed group(s) hold ${groups.reduce((n, g) => n + g.members.length, 0)} member(s) that are not in the network; they are listed in meta.extra.cytoscape.groups`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
reportRootOnly(parsed, report, choice.network.name);
|
|
501
|
+
writeViewPositions(
|
|
502
|
+
sink,
|
|
503
|
+
extraViews,
|
|
504
|
+
views.slice(1).map((v) => v.view),
|
|
505
|
+
report,
|
|
506
|
+
prepared.inner,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The virtual columns of `tables/cytables.xml`.
|
|
512
|
+
* @param session - the session
|
|
513
|
+
* @param entry - the cytables.xml entry
|
|
514
|
+
* @param report - the report
|
|
515
|
+
* @param inner - the options entries are read with
|
|
516
|
+
* @returns the virtual columns in document order
|
|
517
|
+
*/
|
|
518
|
+
async function readVirtuals(
|
|
519
|
+
session: Session,
|
|
520
|
+
entry: SessionEntry,
|
|
521
|
+
report: ImportReportBuilder,
|
|
522
|
+
inner: ResolvedImportOptions,
|
|
523
|
+
): Promise<VirtualColumn[]> {
|
|
524
|
+
const tree = await readXmlTree(await session.read(entry), entry.name, report, inner);
|
|
525
|
+
const out: VirtualColumn[] = [];
|
|
526
|
+
for (const node of elementsNamed(tree, "virtualColumn")) {
|
|
527
|
+
const a = (key: string): string => node.attrs.get(key) ?? "";
|
|
528
|
+
out.push({
|
|
529
|
+
name: a("name"),
|
|
530
|
+
targetTable: a("targetTable"),
|
|
531
|
+
sourceTable: a("sourceTable"),
|
|
532
|
+
sourceColumn: a("sourceColumn"),
|
|
533
|
+
sourceJoinKey: node.attrs.get("sourceJoinKey") ?? "SUID",
|
|
534
|
+
targetJoinKey: node.attrs.get("targetJoinKey") ?? "SUID",
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Records by id.
|
|
542
|
+
* @param records - node or edge records
|
|
543
|
+
* @returns the first record of each id
|
|
544
|
+
*/
|
|
545
|
+
function byId<T extends NodeRec | EdgeRec>(records: readonly T[]): Map<string, T> {
|
|
546
|
+
const out = new Map<string, T>();
|
|
547
|
+
for (const record of records) {
|
|
548
|
+
if (record.id !== null && !out.has(record.id)) {
|
|
549
|
+
out.set(record.id, record);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Copy a view's coordinates and graphics onto the network's records (by `cy:nodeId` and
|
|
557
|
+
* `cy:edgeId`), and its network graphics onto the graph. A view element naming nothing of the
|
|
558
|
+
* network is counted in one W_DANGLING_REFERENCE.
|
|
559
|
+
* @param view - the view document
|
|
560
|
+
* @param graph - the subnetwork
|
|
561
|
+
* @param nodes - its node records by id
|
|
562
|
+
* @param edges - its edge records by id
|
|
563
|
+
* @param report - the report
|
|
564
|
+
* @param entry - the view entry name
|
|
565
|
+
*/
|
|
566
|
+
function applyView(
|
|
567
|
+
view: XgmmlDocument,
|
|
568
|
+
graph: GraphRec,
|
|
569
|
+
nodes: ReadonlyMap<string, NodeRec>,
|
|
570
|
+
edges: ReadonlyMap<string, EdgeRec>,
|
|
571
|
+
report: ImportReportBuilder,
|
|
572
|
+
entry: string,
|
|
573
|
+
): void {
|
|
574
|
+
let dangling = 0;
|
|
575
|
+
for (const node of view.nodes) {
|
|
576
|
+
const target = node.viewId === null ? undefined : nodes.get(node.viewId);
|
|
577
|
+
if (target === undefined) {
|
|
578
|
+
dangling++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
target.x = node.x;
|
|
582
|
+
target.y = node.y;
|
|
583
|
+
target.z = node.z;
|
|
584
|
+
if (node.graphics !== null) {
|
|
585
|
+
target.graphics = { ...(target.graphics ?? {}), ...node.graphics };
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
for (const edge of view.edges) {
|
|
589
|
+
const target = edge.viewId === null ? undefined : edges.get(edge.viewId);
|
|
590
|
+
if (target === undefined) {
|
|
591
|
+
dangling++;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (edge.graphics !== null) {
|
|
595
|
+
target.graphics = { ...(target.graphics ?? {}), ...edge.graphics };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (view.root.graphics !== null) {
|
|
599
|
+
graph.graphics = { ...(graph.graphics ?? {}), ...view.root.graphics };
|
|
600
|
+
}
|
|
601
|
+
if (dangling > 0) {
|
|
602
|
+
report.warning(
|
|
603
|
+
"validation-error",
|
|
604
|
+
CYS_ISSUE.DANGLING_REFERENCE,
|
|
605
|
+
`${entry}: ${dangling} view element(s) name no node or edge of the network; they are not read`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* The positions of a further view, by model node id: y flipped to y-up, z in the position only
|
|
612
|
+
* under zAs "position".
|
|
613
|
+
* @param view - the view document
|
|
614
|
+
* @param zAs - where z goes
|
|
615
|
+
* @returns the positions
|
|
616
|
+
*/
|
|
617
|
+
function viewPositions(view: XgmmlDocument, zAs: "column" | "position"): Map<string, [number, number, number]> {
|
|
618
|
+
const out = new Map<string, [number, number, number]>();
|
|
619
|
+
for (const node of view.nodes) {
|
|
620
|
+
const x = Number(node.x);
|
|
621
|
+
const y = Number(node.y);
|
|
622
|
+
const z = zAs === "position" ? Number(node.z ?? 0) : 0;
|
|
623
|
+
if (node.viewId !== null && node.x !== null && node.y !== null && Number.isFinite(x) && Number.isFinite(y)) {
|
|
624
|
+
out.set(node.viewId, [x, y === 0 ? 0 : -y, Number.isFinite(z) ? z : 0]);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Write each further view's positions as a `position@<n>` column (n from 2), no role.
|
|
632
|
+
* @param sink - the sink
|
|
633
|
+
* @param views - the positions of each further view
|
|
634
|
+
* @param viewIds - each view's SUID
|
|
635
|
+
* @param report - the report
|
|
636
|
+
* @param common - the id rule
|
|
637
|
+
*/
|
|
638
|
+
function writeViewPositions(
|
|
639
|
+
sink: GraphSink,
|
|
640
|
+
views: readonly Map<string, [number, number, number]>[],
|
|
641
|
+
viewIds: readonly string[],
|
|
642
|
+
report: ImportReportBuilder,
|
|
643
|
+
common: ResolvedImportOptions,
|
|
644
|
+
): void {
|
|
645
|
+
const coercer = new IdCoercer(common.ids);
|
|
646
|
+
views.forEach((positions, i) => {
|
|
647
|
+
const name = `${VIEW_POSITION_PREFIX}${i + 2}`;
|
|
648
|
+
const { handle } = declareResolved(
|
|
649
|
+
sink,
|
|
650
|
+
"node",
|
|
651
|
+
{
|
|
652
|
+
name,
|
|
653
|
+
dtype: "f32",
|
|
654
|
+
components: 3,
|
|
655
|
+
nullable: true,
|
|
656
|
+
origin: {
|
|
657
|
+
format: FORMAT,
|
|
658
|
+
id: viewIds[i],
|
|
659
|
+
title: null,
|
|
660
|
+
type: "graphics",
|
|
661
|
+
namespace: XGMML_ORIGIN_NAMESPACE,
|
|
662
|
+
},
|
|
663
|
+
extra: { sourceDims: 2, units: "file" },
|
|
664
|
+
},
|
|
665
|
+
report,
|
|
666
|
+
{ element: name },
|
|
667
|
+
);
|
|
668
|
+
for (const [id, xyz] of positions) {
|
|
669
|
+
let index: number;
|
|
670
|
+
try {
|
|
671
|
+
index = sink.indexOf(coercer.text(id));
|
|
672
|
+
} catch {
|
|
673
|
+
continue; // an id the id rule refuses was refused, and recorded, as a node already
|
|
674
|
+
}
|
|
675
|
+
if (index !== INVALID_INDEX) {
|
|
676
|
+
sink.setNodeValue(handle, index, xyz);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* The network's name from its own table (the LOCAL `name` att), else null.
|
|
684
|
+
* @param graph - the subnetwork with its table atts
|
|
685
|
+
* @returns the name
|
|
686
|
+
*/
|
|
687
|
+
function localName(graph: GraphRec): string | null {
|
|
688
|
+
const att = graph.atts.find((a) => a.name === "name" && (a.namespace ?? null) === null);
|
|
689
|
+
return att?.value ?? null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* The name of the network a subnetwork was made from: the hidden `__parentNetwork.SUID` (or the
|
|
694
|
+
* pre-3.4 `Cy2 Parent Network.SUID`), named when the session holds it.
|
|
695
|
+
* @param graph - the subnetwork with its table atts
|
|
696
|
+
* @param choices - the session's networks
|
|
697
|
+
* @returns the parent's name, its SUID when unnamed, or null
|
|
698
|
+
*/
|
|
699
|
+
function parentNetwork(graph: GraphRec, choices: readonly Choice[]): string | null {
|
|
700
|
+
const att = graph.atts.find((a) => a.name === "__parentNetwork.SUID" || a.name === "Cy2 Parent Network.SUID");
|
|
701
|
+
if (att?.value === undefined || att.value === null || att.value.length === 0) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
const suid = att.value;
|
|
705
|
+
const named = choices.find((c) => c.era === "3" && c.graphId === suid);
|
|
706
|
+
return named?.name ?? suid;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* The resolver of nested-network pointers into other network files of the session
|
|
711
|
+
* (`207-Set+2.xgmml#223`): the target graph's label. Only the files a pointer names are read.
|
|
712
|
+
* @param prepared - the prepared import
|
|
713
|
+
* @param doc - the network file being read
|
|
714
|
+
* @returns the resolver
|
|
715
|
+
*/
|
|
716
|
+
function pointerResolver(prepared: Prepared, doc: XgmmlDocument): (href: string) => string | null {
|
|
717
|
+
const names = new Map<string, string>();
|
|
718
|
+
for (const graph of doc.graphs) {
|
|
719
|
+
const { href } = graph;
|
|
720
|
+
const hash = href?.indexOf("#") ?? -1;
|
|
721
|
+
if (href === null || hash <= 0 || names.has(href)) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const file = urlDecode(href.slice(0, hash));
|
|
725
|
+
const id = href.slice(hash + 1);
|
|
726
|
+
const network = prepared.session.layout.networks.find(
|
|
727
|
+
(n) => urlDecode(n.path.slice("networks/".length)) === file,
|
|
728
|
+
);
|
|
729
|
+
const parsed = network === undefined ? undefined : prepared.docs.get(network.name);
|
|
730
|
+
const target = parsed?.doc.graphs.find((g) => g.id === id);
|
|
731
|
+
if (target !== undefined) {
|
|
732
|
+
names.set(href, target.label ?? id);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return (href) => names.get(href) ?? null;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* W_XGMML_ROOT_ONLY_ELEMENTS for elements of a network file no registered subnetwork holds.
|
|
740
|
+
* @param parsed - the network file
|
|
741
|
+
* @param report - the report
|
|
742
|
+
* @param entry - the entry name
|
|
743
|
+
*/
|
|
744
|
+
function reportRootOnly(parsed: Parsed, report: ImportReportBuilder, entry: string): void {
|
|
745
|
+
const held = new Set<unknown>();
|
|
746
|
+
for (const graph of registeredGraphs(parsed)) {
|
|
747
|
+
const members = membersOf(parsed.doc, graph);
|
|
748
|
+
members.nodes.forEach((n) => held.add(n));
|
|
749
|
+
members.edges.forEach((e) => held.add(e));
|
|
750
|
+
}
|
|
751
|
+
const nodes = parsed.doc.nodes.filter((n) => !held.has(n)).length;
|
|
752
|
+
const edges = parsed.doc.edges.filter((e) => !held.has(e)).length;
|
|
753
|
+
if (nodes + edges > 0) {
|
|
754
|
+
report.warning(
|
|
755
|
+
"unsupported",
|
|
756
|
+
XGMML_ISSUE.ROOT_ONLY_ELEMENTS,
|
|
757
|
+
`${entry}: ${nodes} node(s) and ${edges} edge(s) belong to no registered network (group meta-edges, collapsed group members) and were not read`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ---------------------------------------------------------------------------------------- 2.x
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* The networks of a 2.x session: every network of `cysession.xml`'s tree except `Network Root`,
|
|
766
|
+
* in the order of their files in the archive.
|
|
767
|
+
* @param session - the session
|
|
768
|
+
* @param report - the report
|
|
769
|
+
* @param inner - the options entries are read with
|
|
770
|
+
* @returns the networks
|
|
771
|
+
*/
|
|
772
|
+
async function networks2(
|
|
773
|
+
session: Session,
|
|
774
|
+
report: ImportReportBuilder,
|
|
775
|
+
inner: ResolvedImportOptions,
|
|
776
|
+
): Promise<Choice2[]> {
|
|
777
|
+
const { layout } = session;
|
|
778
|
+
if (layout.cysession === null) {
|
|
779
|
+
return report.fail(CYS_ISSUE.NOT_SESSION, "the session has no cysession.xml");
|
|
780
|
+
}
|
|
781
|
+
const tree = await readXmlTree(await session.read(layout.cysession), layout.cysession.name, report, inner);
|
|
782
|
+
const documentVersion = tree.attrs.get("documentVersion") ?? "";
|
|
783
|
+
if (documentVersion.startsWith("3")) {
|
|
784
|
+
report.fail(
|
|
785
|
+
CYS_ISSUE.VERSION,
|
|
786
|
+
`cysession.xml has documentVersion ${documentVersion} and no version marker: the 2011 Cytoscape 3.0 pre-release layout, which no released Cytoscape reads`,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
const byDecoded = new Map<string, SessionEntry>();
|
|
790
|
+
for (const [path, entry] of layout.files) {
|
|
791
|
+
byDecoded.set(path, entry);
|
|
792
|
+
byDecoded.set(urlDecode(path), entry);
|
|
793
|
+
}
|
|
794
|
+
const order = [...layout.files.values()];
|
|
795
|
+
const choices: Choice2[] = [];
|
|
796
|
+
const missing: string[] = [];
|
|
797
|
+
const named = new Set<SessionEntry>();
|
|
798
|
+
for (const record of elementsNamed(tree, "network")) {
|
|
799
|
+
const id = record.attrs.get("id") ?? "";
|
|
800
|
+
const filename = record.attrs.get("filename") ?? `${id}.xgmml`;
|
|
801
|
+
if (id === "Network Root") {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const file = byDecoded.get(filename);
|
|
805
|
+
if (file === undefined) {
|
|
806
|
+
missing.push(filename);
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
named.add(file);
|
|
810
|
+
const parent = record.children.find((c) => c.name === "parent")?.attrs.get("id") ?? null;
|
|
811
|
+
choices.push({
|
|
812
|
+
era: "2",
|
|
813
|
+
file,
|
|
814
|
+
name: id,
|
|
815
|
+
record,
|
|
816
|
+
parent: parent === "Network Root" || parent === "NULL" ? null : parent,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
if (missing.length > 0) {
|
|
820
|
+
report.warning(
|
|
821
|
+
"validation-error",
|
|
822
|
+
CYS_ISSUE.DANGLING_REFERENCE,
|
|
823
|
+
`cysession.xml names ${missing.length} network file(s) the session does not hold: ${listed(missing)}`,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
const unnamed = order.filter((e) => !named.has(e)).map((e) => e.name);
|
|
827
|
+
if (unnamed.length > 0) {
|
|
828
|
+
report.warning(
|
|
829
|
+
"unsupported",
|
|
830
|
+
CYS_ISSUE.ENTRY_SKIPPED,
|
|
831
|
+
`${unnamed.length} network file(s) are not in cysession.xml's network tree and were not read: ${listed(unnamed)}`,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return choices.sort((a, b) => order.indexOf(a.file) - order.indexOf(b.file));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Import one network of a 2.x session.
|
|
839
|
+
* @param prepared - the prepared import
|
|
840
|
+
* @param choice - the network
|
|
841
|
+
* @param sink - the sink
|
|
842
|
+
*/
|
|
843
|
+
async function import2(prepared: Prepared, choice: Choice2, sink: GraphSink): Promise<void> {
|
|
844
|
+
const { report, inner, session } = prepared;
|
|
845
|
+
const { doc, dialect } = await parseEntry(session, choice.file, report, inner);
|
|
846
|
+
for (const [list, column] of [
|
|
847
|
+
["selectedNodes", SELECTED_COLUMN],
|
|
848
|
+
["hiddenNodes", HIDDEN_COLUMN],
|
|
849
|
+
] as const) {
|
|
850
|
+
mark(doc.nodes, idsIn(choice.record, list), column);
|
|
851
|
+
}
|
|
852
|
+
for (const [list, column] of [
|
|
853
|
+
["selectedEdges", SELECTED_COLUMN],
|
|
854
|
+
["hiddenEdges", HIDDEN_COLUMN],
|
|
855
|
+
] as const) {
|
|
856
|
+
mark(doc.edges, idsIn(choice.record, list), column);
|
|
857
|
+
}
|
|
858
|
+
const cyMeta = sessionMeta(prepared, {
|
|
859
|
+
collection: null,
|
|
860
|
+
network: choice.name,
|
|
861
|
+
parentNetwork: choice.parent,
|
|
862
|
+
visualStyle: choice.record.attrs.get("visualStyle") ?? null,
|
|
863
|
+
});
|
|
864
|
+
const extras: EmitExtras = {
|
|
865
|
+
sourceFormat: FORMAT,
|
|
866
|
+
entry: choice.file.name,
|
|
867
|
+
graphName: choice.name,
|
|
868
|
+
metaExtra: { [META_KEY]: cyMeta },
|
|
869
|
+
};
|
|
870
|
+
const settings: XgmmlSettings = { labelAliases: true, cytoscapeEscapes: true, zAs: prepared.zAs };
|
|
871
|
+
new XgmmlEmitter(doc, dialect, sink, report, inner, settings, extras).emit(null);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* The element ids a `cysession.xml` network record lists under one key (`selectedNodes`, ...).
|
|
876
|
+
* @param record - the network record
|
|
877
|
+
* @param list - the list element's name
|
|
878
|
+
* @returns the ids (2.x node names, edge identifiers)
|
|
879
|
+
*/
|
|
880
|
+
function idsIn(record: XmlNode, list: string): Set<string> {
|
|
881
|
+
const out = new Set<string>();
|
|
882
|
+
for (const holder of record.children.filter((c) => c.name === list)) {
|
|
883
|
+
for (const item of holder.children) {
|
|
884
|
+
const id = item.attrs.get("id");
|
|
885
|
+
if (id !== undefined) {
|
|
886
|
+
out.add(id);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return out;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Give the records 2.x lists by name (a node's name is its label; an edge is named by its id or
|
|
895
|
+
* label) a true cell in a bool column of the cytoscape namespace.
|
|
896
|
+
* @param records - the node or edge records
|
|
897
|
+
* @param ids - the listed names
|
|
898
|
+
* @param column - the column
|
|
899
|
+
*/
|
|
900
|
+
function mark(records: readonly (NodeRec | EdgeRec)[], ids: ReadonlySet<string>, column: string): void {
|
|
901
|
+
if (ids.size === 0) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
for (const record of records) {
|
|
905
|
+
if ((record.id !== null && ids.has(record.id)) || (record.label !== null && ids.has(record.label))) {
|
|
906
|
+
record.atts.push({
|
|
907
|
+
name: column,
|
|
908
|
+
type: null,
|
|
909
|
+
cyType: "Boolean",
|
|
910
|
+
elementType: null,
|
|
911
|
+
value: "true",
|
|
912
|
+
hidden: false,
|
|
913
|
+
equation: false,
|
|
914
|
+
children: [],
|
|
915
|
+
extra: {},
|
|
916
|
+
xml: null,
|
|
917
|
+
hasGraph: false,
|
|
918
|
+
text: "",
|
|
919
|
+
line: record.line,
|
|
920
|
+
namespace: CYTOSCAPE_NAMESPACE,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ---------------------------------------------------------------------------------------- shared
|
|
927
|
+
|
|
928
|
+
/** The session facts of `meta.extra.cytoscape`. */
|
|
929
|
+
interface SessionMeta {
|
|
930
|
+
sessionVersion: string;
|
|
931
|
+
collection: string | null;
|
|
932
|
+
network: string;
|
|
933
|
+
parentNetwork: string | null;
|
|
934
|
+
visualStyle: string | null;
|
|
935
|
+
skippedEntries: string[];
|
|
936
|
+
groups?: { group: string; members: string[] }[];
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* The session facts of one network, and the once-per-import warnings about what is not read
|
|
941
|
+
* (styles, other entries).
|
|
942
|
+
* @param prepared - the prepared import
|
|
943
|
+
* @param facts - the network's facts
|
|
944
|
+
* @param facts.collection - the root network's name (3.x)
|
|
945
|
+
* @param facts.network - the network's id
|
|
946
|
+
* @param facts.parentNetwork - the network it was made from
|
|
947
|
+
* @param facts.visualStyle - the style its view uses
|
|
948
|
+
* @returns the record
|
|
949
|
+
*/
|
|
950
|
+
function sessionMeta(
|
|
951
|
+
prepared: Prepared,
|
|
952
|
+
facts: { collection: string | null; network: string; parentNetwork: string | null; visualStyle: string | null },
|
|
953
|
+
): SessionMeta {
|
|
954
|
+
const { layout } = prepared.session;
|
|
955
|
+
const { report } = prepared;
|
|
956
|
+
if (layout.styles.length > 0) {
|
|
957
|
+
report.warning(
|
|
958
|
+
"unsupported",
|
|
959
|
+
CYS_ISSUE.STYLES_NOT_IMPORTED,
|
|
960
|
+
`the session's styles (${layout.styles.map((s) => s.path).join(", ")}) are not applied: style import is issue #706`,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
if (layout.skipped.length > 0) {
|
|
964
|
+
report.warning(
|
|
965
|
+
"unsupported",
|
|
966
|
+
CYS_ISSUE.ENTRY_SKIPPED,
|
|
967
|
+
`${layout.skipped.length} entries hold no graph data and were not read (apps, global tables, properties, images): ${listed(layout.skipped)}`,
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
sessionVersion: layout.version,
|
|
972
|
+
...facts,
|
|
973
|
+
skippedEntries: [...layout.skipped],
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Import one network of a prepared session into a sink.
|
|
979
|
+
* @param prepared - the prepared import
|
|
980
|
+
* @param index - the network
|
|
981
|
+
* @param sink - the sink
|
|
982
|
+
* @returns the report
|
|
983
|
+
*/
|
|
984
|
+
async function importChoice(prepared: Prepared, index: number, sink: GraphSink): Promise<ImportReport> {
|
|
985
|
+
const choice = prepared.choices[index];
|
|
986
|
+
if (choice.era === "3") {
|
|
987
|
+
const parsed = prepared.docs.get(choice.network.name) as Parsed;
|
|
988
|
+
await import3(prepared, choice, sink, parsed);
|
|
989
|
+
} else {
|
|
990
|
+
await import2(prepared, choice, sink);
|
|
991
|
+
}
|
|
992
|
+
throwIfAborted(prepared.common.signal);
|
|
993
|
+
return prepared.report.finish();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Import one network of a session.
|
|
998
|
+
* @param input - the session bytes
|
|
999
|
+
* @param sink - the sink
|
|
1000
|
+
* @param options - format-specific and common options
|
|
1001
|
+
* @returns the report; ImportError when the session cannot be read or the error limit is exceeded
|
|
1002
|
+
*/
|
|
1003
|
+
async function importCys(
|
|
1004
|
+
input: ImportInput,
|
|
1005
|
+
sink: GraphSink,
|
|
1006
|
+
options?: CysImportOptions & CommonImportOptions,
|
|
1007
|
+
): Promise<ImportReport> {
|
|
1008
|
+
const prepared = await prepare(input, sink, options);
|
|
1009
|
+
const index = chooseGraph(
|
|
1010
|
+
prepared.choices.map((c) => c.name),
|
|
1011
|
+
options,
|
|
1012
|
+
prepared.report,
|
|
1013
|
+
);
|
|
1014
|
+
if (prepared.choices.length > 1) {
|
|
1015
|
+
prepared.report.warning(
|
|
1016
|
+
"unsupported",
|
|
1017
|
+
CYS_ISSUE.MULTIPLE_GRAPHS,
|
|
1018
|
+
`the session holds ${prepared.choices.length} networks; ${prepared.choices.length - 1} were not read (use importAll, graphIndex or graphName)`,
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
return importChoice(prepared, index, sink);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Import every network of a session, each into its own sink with its own report.
|
|
1026
|
+
* @param input - the session bytes
|
|
1027
|
+
* @param sinkFor - a sink per network
|
|
1028
|
+
* @param options - format-specific and common options
|
|
1029
|
+
* @returns one report per network
|
|
1030
|
+
*/
|
|
1031
|
+
async function importAllCys(
|
|
1032
|
+
input: ImportInput,
|
|
1033
|
+
sinkFor: (index: number) => GraphSink,
|
|
1034
|
+
options?: CysImportOptions & CommonImportOptions,
|
|
1035
|
+
): Promise<ImportReport[]> {
|
|
1036
|
+
const bytes = (await readBytes(input, { signal: options?.signal ?? null })) ?? input;
|
|
1037
|
+
const first = await prepare(bytes, null, options);
|
|
1038
|
+
const reports: ImportReport[] = [];
|
|
1039
|
+
for (let i = 0; i < first.choices.length; i++) {
|
|
1040
|
+
const sink = sinkFor(i);
|
|
1041
|
+
// every network gets fresh records and a fresh report
|
|
1042
|
+
const prepared = i === 0 ? first : await prepare(bytes, null, options);
|
|
1043
|
+
reportSinkOptions(sink, options, prepared.report, true);
|
|
1044
|
+
reportUnusedOptions(options, prepared.report, USED_OPTIONS);
|
|
1045
|
+
reports.push(await importChoice(prepared, i, sink));
|
|
1046
|
+
}
|
|
1047
|
+
return reports;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* List the networks of a session from its network files, without reading tables or views.
|
|
1052
|
+
* @param input - the session bytes
|
|
1053
|
+
* @param options - the options
|
|
1054
|
+
* @returns one listing per network; node and edge counts for 3.x sessions only
|
|
1055
|
+
*/
|
|
1056
|
+
async function listCysGraphs(
|
|
1057
|
+
input: ImportInput,
|
|
1058
|
+
options?: CysImportOptions & CommonImportOptions,
|
|
1059
|
+
): Promise<readonly GraphListing[]> {
|
|
1060
|
+
const prepared = await prepare(input, null, options);
|
|
1061
|
+
return prepared.choices.map((choice, index) =>
|
|
1062
|
+
Object.freeze({
|
|
1063
|
+
index,
|
|
1064
|
+
name: choice.name,
|
|
1065
|
+
nodes: choice.era === "3" ? choice.nodes : null,
|
|
1066
|
+
edges: choice.era === "3" ? choice.edges : null,
|
|
1067
|
+
}),
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/** A session's marker or folder name in the head of the archive. */
|
|
1072
|
+
const SESSION_NAME = /CytoscapeSession|cysession\.xml|\d+\.\d+\.\d+\.version/;
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Confidence that a head of bytes is a Cytoscape session: 0.95 for a zip whose head names the
|
|
1076
|
+
* session folder, its version marker or cysession.xml; 0 for any other zip, so an `.xlsx`, a
|
|
1077
|
+
* `.docx` or a zipped GraphML ranks as no format rather than as a broken session. The zip may
|
|
1078
|
+
* start after a short stub (a self-extracting archive) within the head.
|
|
1079
|
+
* @param head - the first bytes
|
|
1080
|
+
* @returns the confidence
|
|
1081
|
+
*/
|
|
1082
|
+
export function sniffCys(head: Uint8Array): number {
|
|
1083
|
+
for (let at = 0; at + 4 <= head.byteLength; at++) {
|
|
1084
|
+
if (startsLikeZip(head.subarray(at))) {
|
|
1085
|
+
return SESSION_NAME.test(decodeEntryName(head.subarray(at))) ? 0.95 : 0;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return 0;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/** The Cytoscape session importer. */
|
|
1092
|
+
export const cysImporter: GraphImporter<CysImportOptions> = Object.freeze({
|
|
1093
|
+
format: FORMAT,
|
|
1094
|
+
extensions: EXTENSIONS,
|
|
1095
|
+
mimeTypes: MIME_TYPES,
|
|
1096
|
+
sniff: sniffCys,
|
|
1097
|
+
import: importCys,
|
|
1098
|
+
importAll: importAllCys,
|
|
1099
|
+
listGraphs: listCysGraphs,
|
|
1100
|
+
});
|