@graphty/graph-io 0.3.19 → 0.3.21
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 +49 -4
- package/dist/chunks/{escape-CWExcecC.js → escape-B-tkk_Cl.js} +74 -16
- package/dist/chunks/escape-B-tkk_Cl.js.map +1 -0
- package/dist/chunks/{writer-DQiKgQJc.js → export-Bh60Dv-n.js} +5 -79
- package/dist/chunks/export-Bh60Dv-n.js.map +1 -0
- package/dist/chunks/{exporter-CbMZyVt-.js → exporter-BGrsWamJ.js} +7 -6
- package/dist/chunks/{exporter-CbMZyVt-.js.map → exporter-BGrsWamJ.js.map} +1 -1
- package/dist/chunks/exporter-DIeGJXAZ.js +834 -0
- package/dist/chunks/exporter-DIeGJXAZ.js.map +1 -0
- package/dist/chunks/{importer-BNFuV1-K.js → importer-BWY2FFc5.js} +28 -7
- package/dist/chunks/importer-BWY2FFc5.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-BAP4PrxR.js → importer-DET1tg6s.js} +30 -20
- package/dist/chunks/importer-DET1tg6s.js.map +1 -0
- package/dist/chunks/{importer-ByPGO-09.js → importer-DEcKhsmQ.js} +2 -2
- package/dist/chunks/{importer-ByPGO-09.js.map → importer-DEcKhsmQ.js.map} +1 -1
- package/dist/chunks/{importer-BW-Ft2ps.js → importer-DIrbbnAf.js} +6 -5
- package/dist/chunks/{importer-BW-Ft2ps.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-Bm1_5vPS.js → importer-eUREhBGj.js} +11 -8
- package/dist/chunks/importer-eUREhBGj.js.map +1 -0
- package/dist/chunks/{importer-DOepkgnG.js → importer-vi3bSdtw.js} +4 -4
- package/dist/chunks/{importer-DOepkgnG.js.map → importer-vi3bSdtw.js.map} +1 -1
- package/dist/chunks/{importer-BVGtU1NA.js → importer-zGLo8Dg_.js} +6 -5
- package/dist/chunks/{importer-BVGtU1NA.js.map → importer-zGLo8Dg_.js.map} +1 -1
- package/dist/chunks/{json-elements-CZY1wiZh.js → json-elements-DDS8N2Dd.js} +2 -2
- package/dist/chunks/{json-elements-CZY1wiZh.js.map → json-elements-DDS8N2Dd.js.map} +1 -1
- package/dist/chunks/{records-BzNicMsf.js → records-dbRkxwaq.js} +2 -2
- package/dist/chunks/{records-BzNicMsf.js.map → records-dbRkxwaq.js.map} +1 -1
- package/dist/chunks/{report-BcWboivV.js → report-B1z4WT9e.js} +143 -111
- package/dist/chunks/{report-BcWboivV.js.map → report-B1z4WT9e.js.map} +1 -1
- package/dist/chunks/{weights-CwISIpCP.js → weights-Dzba96G3.js} +5 -5
- package/dist/chunks/{weights-CwISIpCP.js.map → weights-Dzba96G3.js.map} +1 -1
- package/dist/chunks/writer-C7flM-Ih.js +77 -0
- package/dist/chunks/writer-C7flM-Ih.js.map +1 -0
- package/dist/csv.js +6 -5
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- 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 +5 -4
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +12 -14
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +288 -141
- 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 -5
- 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/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.map +1 -1
- package/dist/src/formats/cx/importer.js +26 -17
- package/dist/src/formats/cx/importer.js.map +1 -1
- 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/importer.d.ts +2 -2
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +1 -1
- package/dist/src/formats/gexf/importer.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 +7 -17
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +3 -1
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +3 -2
- 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 +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -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 +25 -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 +2 -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 +15 -2
- package/src/common/cell-budget.ts +170 -0
- package/src/common/input.ts +40 -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 +27 -19
- 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/importer.ts +0 -1
- package/src/formats/gml/exporter.ts +7 -18
- package/src/formats/json/exporter.ts +3 -1
- package/src/formats/json/importer.ts +3 -2
- 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 +9 -0
- package/src/registry.ts +41 -15
- package/src/sniff.ts +5 -1
- package/dist/chunks/escape-CWExcecC.js.map +0 -1
- package/dist/chunks/importer-BAP4PrxR.js.map +0 -1
- package/dist/chunks/importer-BNFuV1-K.js.map +0 -1
- package/dist/chunks/importer-Bm1_5vPS.js.map +0 -1
- package/dist/chunks/writer-DQiKgQJc.js.map +0 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@graphty/graph-io/cys` subpath: the Cytoscape session importer, its option type and its
|
|
3
|
+
* issue code table. Sessions are read-only: graph-io writes XGMML and CX2, which Cytoscape opens.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { CYS_ISSUE } from "./constants.js";
|
|
7
|
+
export { cysImporter, type CysImportOptions } from "./importer.js";
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The layout of a Cytoscape session archive (`research-session-and-style.md` sections 2.1 to 2.3;
|
|
3
|
+
* Cytoscape's `Cy3SessionReaderImpl`, `Cy2SessionReaderImpl` and `SessionUtil`): which entries
|
|
4
|
+
* are the version marker, the networks, views, tables, styles and the rest, read from the central
|
|
5
|
+
* directory alone. Entries are inflated one at a time, only when asked for, within the import's
|
|
6
|
+
* byte budget.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readBytes, textChunks } from "../../common/input.js";
|
|
10
|
+
import { type ResolvedImportOptions } from "../../common/options.js";
|
|
11
|
+
import { ImportReportBuilder } from "../../common/report.js";
|
|
12
|
+
import { tokenizeXml, xmlDeclaredEncoding, XmlSyntaxError } from "../../common/xml.js";
|
|
13
|
+
import { readZipDirectory, readZipEntry, type ZipEntry, ZipError } from "../../common/zip.js";
|
|
14
|
+
import { ImportError, type ImportInput } from "../../types.js";
|
|
15
|
+
import { CYS_ISSUE, FORMAT, MAX_RATIO } from "./constants.js";
|
|
16
|
+
|
|
17
|
+
/** An entry of the session, by its path under the session's root folder. */
|
|
18
|
+
export interface SessionEntry {
|
|
19
|
+
/** The path under the root folder, as the archive spells it (URL-encoded parts). */
|
|
20
|
+
readonly path: string;
|
|
21
|
+
/** The full entry name, for messages. */
|
|
22
|
+
readonly name: string;
|
|
23
|
+
/** The zip entry. */
|
|
24
|
+
readonly zip: ZipEntry;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A 3.x network file: one root network (a collection). */
|
|
28
|
+
export interface NetworkEntry extends SessionEntry {
|
|
29
|
+
/** The root network's saved SUID. */
|
|
30
|
+
readonly suid: string;
|
|
31
|
+
/** The file name's network name, decoded. */
|
|
32
|
+
readonly title: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A 3.x view file. */
|
|
36
|
+
interface ViewEntry extends SessionEntry {
|
|
37
|
+
/** The SUID of the network the view shows. */
|
|
38
|
+
readonly network: string;
|
|
39
|
+
/** The view's SUID. */
|
|
40
|
+
readonly view: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A 3.x table file. */
|
|
44
|
+
export interface TableEntry extends SessionEntry {
|
|
45
|
+
/** The path under `tables/` (what `cytables.xml` names). */
|
|
46
|
+
readonly tablePath: string;
|
|
47
|
+
/** The SUID of the network the table belongs to. */
|
|
48
|
+
readonly network: string;
|
|
49
|
+
/** The namespace: LOCAL_ATTRS, SHARED_ATTRS, HIDDEN or an app's. */
|
|
50
|
+
readonly namespace: string;
|
|
51
|
+
/** The element class: CyNode, CyEdge or CyNetwork. */
|
|
52
|
+
readonly element: "node" | "edge" | "network" | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What a session archive holds, by kind. */
|
|
56
|
+
interface SessionLayout {
|
|
57
|
+
/** "3" for a 3.x session, "2" for a 2.x one. */
|
|
58
|
+
readonly era: "2" | "3";
|
|
59
|
+
/** The version marker as written (`3.0.0`), or the 2.x cysession.xml documentVersion. */
|
|
60
|
+
readonly version: string;
|
|
61
|
+
/** The root folder, with its trailing slash ("" when the entries have none). */
|
|
62
|
+
readonly root: string;
|
|
63
|
+
readonly networks: readonly NetworkEntry[];
|
|
64
|
+
readonly views: readonly ViewEntry[];
|
|
65
|
+
readonly tables: readonly TableEntry[];
|
|
66
|
+
/** `tables/cytables.xml`, if present. */
|
|
67
|
+
readonly cytables: SessionEntry | null;
|
|
68
|
+
/** `apps/org.cytoscape.swing-application/network_list.xml`, if present. */
|
|
69
|
+
readonly networkList: SessionEntry | null;
|
|
70
|
+
/** 2.x: `cysession.xml`. */
|
|
71
|
+
readonly cysession: SessionEntry | null;
|
|
72
|
+
/** 2.x: the network files by their path. */
|
|
73
|
+
readonly files: ReadonlyMap<string, SessionEntry>;
|
|
74
|
+
/** The style entries (`session_vizmap.xml`, `session_vizmap.props`). */
|
|
75
|
+
readonly styles: readonly SessionEntry[];
|
|
76
|
+
/** Every entry the importer does not read. */
|
|
77
|
+
readonly skipped: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** An opened session: its bytes, its layout and an entry reader within the byte budget. */
|
|
81
|
+
export interface Session {
|
|
82
|
+
readonly layout: SessionLayout;
|
|
83
|
+
/**
|
|
84
|
+
* Inflate one entry.
|
|
85
|
+
* @param entry - the entry
|
|
86
|
+
* @returns its bytes
|
|
87
|
+
*/
|
|
88
|
+
read(entry: SessionEntry): Promise<Uint8Array>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Java's `URLDecoder.decode` (UTF-8): `+` is a space, `%XX` a byte; a malformed escape leaves the
|
|
93
|
+
* text as it is.
|
|
94
|
+
* @param text - the encoded text
|
|
95
|
+
* @returns the decoded text
|
|
96
|
+
*/
|
|
97
|
+
export function urlDecode(text: string): string {
|
|
98
|
+
const plus = text.replace(/\+/g, " ");
|
|
99
|
+
try {
|
|
100
|
+
return decodeURIComponent(plus);
|
|
101
|
+
} catch {
|
|
102
|
+
return plus;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Read the input as a zip and lay out its session entries. Fatal: text input or not a zip
|
|
108
|
+
* (E_CYS_NOT_ZIP), an empty input, a damaged archive (E_CYS_CORRUPT), a split archive
|
|
109
|
+
* (E_CYS_UNSUPPORTED), no session marker (E_CYS_NOT_SESSION), a version graph-io cannot read
|
|
110
|
+
* (E_CYS_VERSION).
|
|
111
|
+
* @param input - the input
|
|
112
|
+
* @param report - the report
|
|
113
|
+
* @param common - cancellation and progress
|
|
114
|
+
* @param maxBytes - the import's uncompressed byte budget
|
|
115
|
+
* @returns the session
|
|
116
|
+
*/
|
|
117
|
+
export async function openSession(
|
|
118
|
+
input: ImportInput,
|
|
119
|
+
report: ImportReportBuilder,
|
|
120
|
+
common: ResolvedImportOptions,
|
|
121
|
+
maxBytes: number,
|
|
122
|
+
): Promise<Session> {
|
|
123
|
+
const bytes = await readBytes(input, common);
|
|
124
|
+
if (bytes === null) {
|
|
125
|
+
report.fail(
|
|
126
|
+
CYS_ISSUE.NOT_ZIP,
|
|
127
|
+
"a Cytoscape session is a zip archive; text input cannot hold one (pass the bytes)",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (bytes.byteLength === 0) {
|
|
131
|
+
report.fail(CYS_ISSUE.EMPTY_INPUT, "the input is empty");
|
|
132
|
+
}
|
|
133
|
+
let zipEntries: ZipEntry[];
|
|
134
|
+
try {
|
|
135
|
+
zipEntries = readZipDirectory(bytes);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return zipFailure(err, report);
|
|
138
|
+
}
|
|
139
|
+
const layout = layoutOf(zipEntries, report);
|
|
140
|
+
let budget = maxBytes;
|
|
141
|
+
let inflated = 0;
|
|
142
|
+
const total = zipEntries.reduce((sum, e) => sum + e.size, 0);
|
|
143
|
+
return {
|
|
144
|
+
layout,
|
|
145
|
+
read: async (entry: SessionEntry): Promise<Uint8Array> => {
|
|
146
|
+
const before = inflated;
|
|
147
|
+
try {
|
|
148
|
+
const data = await readZipEntry(bytes, entry.zip, {
|
|
149
|
+
signal: common.signal,
|
|
150
|
+
maxBytes: budget,
|
|
151
|
+
maxRatio: MAX_RATIO,
|
|
152
|
+
onBytes: (n) => common.onProgress?.(before + n, total),
|
|
153
|
+
});
|
|
154
|
+
budget -= data.byteLength;
|
|
155
|
+
inflated += data.byteLength;
|
|
156
|
+
return data;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return zipFailure(err, report);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Fail the import with the fatal issue of a zip error (any other error is rethrown).
|
|
166
|
+
* @param err - the error
|
|
167
|
+
* @param report - the report
|
|
168
|
+
*/
|
|
169
|
+
function zipFailure(err: unknown, report: ImportReportBuilder): never {
|
|
170
|
+
if (!(err instanceof ZipError)) {
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
const code = {
|
|
174
|
+
"not-zip": CYS_ISSUE.NOT_ZIP,
|
|
175
|
+
corrupt: CYS_ISSUE.CORRUPT,
|
|
176
|
+
unsupported: CYS_ISSUE.UNSUPPORTED,
|
|
177
|
+
"too-large": CYS_ISSUE.TOO_LARGE,
|
|
178
|
+
}[err.kind];
|
|
179
|
+
if (err.kind === "too-large") {
|
|
180
|
+
// the size limits are not a parse error: graph-io records E_TOO_LARGE as unsupported
|
|
181
|
+
report.error("unsupported", code, err.message);
|
|
182
|
+
throw report.abort(err.message, { code });
|
|
183
|
+
}
|
|
184
|
+
report.fail(code, err.message);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** A 3.x network entry name: `<SUID>[-<name>].xgmml`. */
|
|
188
|
+
const NETWORK_FILE = /^networks\/(\d+)(?:-([^/]*))?\.xgmml$/;
|
|
189
|
+
/** A 3.x view entry name: `<networkSUID>-<viewSUID>[-<title>].xgmml`. */
|
|
190
|
+
const VIEW_FILE = /^views\/(\d+)-(\d+)(?:-[^/]*)?\.xgmml$/;
|
|
191
|
+
/** A 3.x table: `tables/<networkSUID>[-<name>]/<namespace>-<class>-<title>.cytable`. */
|
|
192
|
+
const TABLE_FILE = /^tables\/((\d+)(?:-[^/]*)?\/([^/-]+)-([^/-]+)-[^/]*\.cytable)$/;
|
|
193
|
+
|
|
194
|
+
/** Entries of other tools that carry nothing: macOS resource forks and folder metadata. */
|
|
195
|
+
const NOISE = /(^|\/)(__MACOSX\/|\.DS_Store$)/;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Lay out the entries: drop directory entries and macOS noise, keep the first of a repeated name
|
|
199
|
+
* (W_CYS_DUPLICATE_ENTRY), find the session marker and its root folder, classify what is under
|
|
200
|
+
* it, and list what is not read.
|
|
201
|
+
* @param zipEntries - the central directory
|
|
202
|
+
* @param report - the report
|
|
203
|
+
* @returns the layout
|
|
204
|
+
*/
|
|
205
|
+
function layoutOf(zipEntries: readonly ZipEntry[], report: ImportReportBuilder): SessionLayout {
|
|
206
|
+
const byName = new Map<string, ZipEntry>();
|
|
207
|
+
const repeated: string[] = [];
|
|
208
|
+
for (const entry of zipEntries) {
|
|
209
|
+
if (entry.directory || NOISE.test(entry.name)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (byName.has(entry.name)) {
|
|
213
|
+
repeated.push(entry.name);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
byName.set(entry.name, entry);
|
|
217
|
+
}
|
|
218
|
+
if (repeated.length > 0) {
|
|
219
|
+
report.warning(
|
|
220
|
+
"unsupported",
|
|
221
|
+
CYS_ISSUE.DUPLICATE_ENTRY,
|
|
222
|
+
`${repeated.length} entry name(s) appear more than once; the first of each is read: ${listed(repeated)}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const names = [...byName.keys()];
|
|
226
|
+
const marker = names.find((n) => /^([^/]*\/)?[^/]+\.version$/.test(n));
|
|
227
|
+
const cysession = names.find((n) => /^([^/]*\/)?cysession\.xml$/.test(n));
|
|
228
|
+
if (marker === undefined && cysession === undefined) {
|
|
229
|
+
report.fail(
|
|
230
|
+
CYS_ISSUE.NOT_SESSION,
|
|
231
|
+
"the zip holds neither a session version marker (<x.y.z>.version) nor cysession.xml: it is not a Cytoscape session",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const anchor = marker ?? (cysession as string);
|
|
235
|
+
const root = anchor.slice(0, anchor.lastIndexOf("/") + 1);
|
|
236
|
+
let era: "2" | "3" = "2";
|
|
237
|
+
let version = "2.0.0";
|
|
238
|
+
if (marker !== undefined) {
|
|
239
|
+
version = marker.slice(root.length, -".version".length);
|
|
240
|
+
const major = Number(version.split(".")[0]);
|
|
241
|
+
if (!Number.isInteger(major) || major > 3) {
|
|
242
|
+
report.fail(
|
|
243
|
+
CYS_ISSUE.VERSION,
|
|
244
|
+
`the session version ${version} is newer than the Cytoscape 3 sessions graph-io reads`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
era = major === 3 ? "3" : "2";
|
|
248
|
+
}
|
|
249
|
+
const layout = {
|
|
250
|
+
era,
|
|
251
|
+
version,
|
|
252
|
+
root,
|
|
253
|
+
networks: [] as NetworkEntry[],
|
|
254
|
+
views: [] as ViewEntry[],
|
|
255
|
+
tables: [] as TableEntry[],
|
|
256
|
+
cytables: null as SessionEntry | null,
|
|
257
|
+
networkList: null as SessionEntry | null,
|
|
258
|
+
cysession: null as SessionEntry | null,
|
|
259
|
+
files: new Map<string, SessionEntry>(),
|
|
260
|
+
styles: [] as SessionEntry[],
|
|
261
|
+
skipped: [] as string[],
|
|
262
|
+
};
|
|
263
|
+
for (const [name, zip] of byName) {
|
|
264
|
+
if (!name.startsWith(root) || name === marker) {
|
|
265
|
+
if (name !== marker) {
|
|
266
|
+
layout.skipped.push(name);
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const path = name.slice(root.length);
|
|
271
|
+
const entry: SessionEntry = { path, name, zip };
|
|
272
|
+
if (era === "3") {
|
|
273
|
+
classify3(entry, layout);
|
|
274
|
+
} else {
|
|
275
|
+
classify2(entry, layout);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return layout;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Classify one entry of a 3.x session.
|
|
283
|
+
* @param entry - the entry
|
|
284
|
+
* @param layout - the layout being built
|
|
285
|
+
*/
|
|
286
|
+
function classify3(entry: SessionEntry, layout: Mutable): void {
|
|
287
|
+
const { path } = entry;
|
|
288
|
+
const network = NETWORK_FILE.exec(path);
|
|
289
|
+
const view = VIEW_FILE.exec(path);
|
|
290
|
+
const table = TABLE_FILE.exec(path);
|
|
291
|
+
if (network !== null) {
|
|
292
|
+
layout.networks.push({ ...entry, suid: network[1], title: urlDecode(network[2] ?? "") });
|
|
293
|
+
} else if (view !== null) {
|
|
294
|
+
layout.views.push({ ...entry, network: view[1], view: view[2] });
|
|
295
|
+
} else if (path === "tables/cytables.xml") {
|
|
296
|
+
layout.cytables = entry;
|
|
297
|
+
} else if (table !== null) {
|
|
298
|
+
const element = {
|
|
299
|
+
"org.cytoscape.model.CyNode": "node",
|
|
300
|
+
"org.cytoscape.model.CyEdge": "edge",
|
|
301
|
+
"org.cytoscape.model.CyNetwork": "network",
|
|
302
|
+
}[urlDecode(table[4])] as TableEntry["element"] | undefined;
|
|
303
|
+
layout.tables.push({
|
|
304
|
+
...entry,
|
|
305
|
+
tablePath: table[1],
|
|
306
|
+
network: table[2],
|
|
307
|
+
namespace: urlDecode(table[3]),
|
|
308
|
+
element: element ?? null,
|
|
309
|
+
});
|
|
310
|
+
} else if (path === "apps/org.cytoscape.swing-application/network_list.xml") {
|
|
311
|
+
layout.networkList = entry;
|
|
312
|
+
} else if (/(^|\/)session_vizmap\.(xml|props)$/.test(path)) {
|
|
313
|
+
layout.styles.push(entry);
|
|
314
|
+
} else {
|
|
315
|
+
layout.skipped.push(entry.name);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Classify one entry of a 2.x session.
|
|
321
|
+
* @param entry - the entry
|
|
322
|
+
* @param layout - the layout being built
|
|
323
|
+
*/
|
|
324
|
+
function classify2(entry: SessionEntry, layout: Mutable): void {
|
|
325
|
+
const { path } = entry;
|
|
326
|
+
if (path === "cysession.xml") {
|
|
327
|
+
layout.cysession = entry;
|
|
328
|
+
} else if (/^[^/]+\.xgmml$/.test(path)) {
|
|
329
|
+
layout.files.set(path, entry);
|
|
330
|
+
} else if (/^session_vizmap\.(props|xml)$/.test(path)) {
|
|
331
|
+
layout.styles.push(entry);
|
|
332
|
+
} else {
|
|
333
|
+
layout.skipped.push(entry.name);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** The layout while it is built. */
|
|
338
|
+
type Mutable = {
|
|
339
|
+
-readonly [K in keyof SessionLayout]: SessionLayout[K] extends readonly (infer T)[] ? T[] : SessionLayout[K];
|
|
340
|
+
} & { files: Map<string, SessionEntry> };
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* A list of names for a message: the first ten, then a count.
|
|
344
|
+
* @param names - the names
|
|
345
|
+
* @returns the text
|
|
346
|
+
*/
|
|
347
|
+
export function listed(names: readonly string[]): string {
|
|
348
|
+
const head = names.slice(0, 10).join(", ");
|
|
349
|
+
return names.length > 10 ? `${head} and ${names.length - 10} more` : head;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** One element of a small XML document, as a tree. */
|
|
353
|
+
export interface XmlNode {
|
|
354
|
+
/** The local name. */
|
|
355
|
+
readonly name: string;
|
|
356
|
+
/** The attributes by name as written (prefix included). */
|
|
357
|
+
readonly attrs: ReadonlyMap<string, string>;
|
|
358
|
+
/** The child elements. */
|
|
359
|
+
readonly children: XmlNode[];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Read a small XML entry (`cytables.xml`, `network_list.xml`, `cysession.xml`) as a tree. A
|
|
364
|
+
* document that is not well-formed is E_CYS_CORRUPT naming the entry.
|
|
365
|
+
* @param bytes - the entry's bytes
|
|
366
|
+
* @param entry - the entry name
|
|
367
|
+
* @param report - the report
|
|
368
|
+
* @param common - cancellation
|
|
369
|
+
* @returns the root element
|
|
370
|
+
*/
|
|
371
|
+
export async function readXmlTree(
|
|
372
|
+
bytes: Uint8Array,
|
|
373
|
+
entry: string,
|
|
374
|
+
report: ImportReportBuilder,
|
|
375
|
+
common: ResolvedImportOptions,
|
|
376
|
+
): Promise<XmlNode> {
|
|
377
|
+
const scratch = new ImportReportBuilder(FORMAT, 0);
|
|
378
|
+
const stack: XmlNode[] = [{ name: "", attrs: new Map(), children: [] }];
|
|
379
|
+
try {
|
|
380
|
+
await tokenizeXml(
|
|
381
|
+
textChunks(bytes, scratch, { signal: common.signal, declaredEncoding: xmlDeclaredEncoding }),
|
|
382
|
+
{
|
|
383
|
+
start(name, attrs): void {
|
|
384
|
+
const node: XmlNode = { name: name.slice(name.lastIndexOf(":") + 1), attrs, children: [] };
|
|
385
|
+
stack[stack.length - 1].children.push(node);
|
|
386
|
+
stack.push(node);
|
|
387
|
+
},
|
|
388
|
+
end(): void {
|
|
389
|
+
stack.pop();
|
|
390
|
+
},
|
|
391
|
+
text(): void {
|
|
392
|
+
// the session documents keep everything in attributes
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
if (err instanceof XmlSyntaxError || err instanceof ImportError) {
|
|
398
|
+
report.fail(CYS_ISSUE.CORRUPT, `${entry}: ${err.message}`);
|
|
399
|
+
}
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
const [root] = stack[0].children;
|
|
403
|
+
if (root === undefined) {
|
|
404
|
+
report.fail(CYS_ISSUE.CORRUPT, `${entry}: the document is empty`);
|
|
405
|
+
}
|
|
406
|
+
return root;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Every element of a tree with a local name, depth first.
|
|
411
|
+
* @param node - the tree
|
|
412
|
+
* @param name - the local name
|
|
413
|
+
* @returns the elements
|
|
414
|
+
*/
|
|
415
|
+
export function elementsNamed(node: XmlNode, name: string): XmlNode[] {
|
|
416
|
+
const out: XmlNode[] = [];
|
|
417
|
+
const stack = [node];
|
|
418
|
+
while (stack.length > 0) {
|
|
419
|
+
const n = stack.pop() as XmlNode;
|
|
420
|
+
if (n.name === name) {
|
|
421
|
+
out.push(n);
|
|
422
|
+
}
|
|
423
|
+
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
424
|
+
stack.push(n.children[i]);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|