@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.
Files changed (201) hide show
  1. package/README.md +49 -4
  2. package/dist/chunks/{escape-CWExcecC.js → escape-B-tkk_Cl.js} +74 -16
  3. package/dist/chunks/escape-B-tkk_Cl.js.map +1 -0
  4. package/dist/chunks/{writer-DQiKgQJc.js → export-Bh60Dv-n.js} +5 -79
  5. package/dist/chunks/export-Bh60Dv-n.js.map +1 -0
  6. package/dist/chunks/{exporter-CbMZyVt-.js → exporter-BGrsWamJ.js} +7 -6
  7. package/dist/chunks/{exporter-CbMZyVt-.js.map → exporter-BGrsWamJ.js.map} +1 -1
  8. package/dist/chunks/exporter-DIeGJXAZ.js +834 -0
  9. package/dist/chunks/exporter-DIeGJXAZ.js.map +1 -0
  10. package/dist/chunks/{importer-BNFuV1-K.js → importer-BWY2FFc5.js} +28 -7
  11. package/dist/chunks/importer-BWY2FFc5.js.map +1 -0
  12. package/dist/chunks/importer-DD-xv9_Y.js +1471 -0
  13. package/dist/chunks/importer-DD-xv9_Y.js.map +1 -0
  14. package/dist/chunks/{importer-BAP4PrxR.js → importer-DET1tg6s.js} +30 -20
  15. package/dist/chunks/importer-DET1tg6s.js.map +1 -0
  16. package/dist/chunks/{importer-ByPGO-09.js → importer-DEcKhsmQ.js} +2 -2
  17. package/dist/chunks/{importer-ByPGO-09.js.map → importer-DEcKhsmQ.js.map} +1 -1
  18. package/dist/chunks/{importer-BW-Ft2ps.js → importer-DIrbbnAf.js} +6 -5
  19. package/dist/chunks/{importer-BW-Ft2ps.js.map → importer-DIrbbnAf.js.map} +1 -1
  20. package/dist/chunks/importer-D_e7e7LX.js +3055 -0
  21. package/dist/chunks/importer-D_e7e7LX.js.map +1 -0
  22. package/dist/chunks/{importer-Bm1_5vPS.js → importer-eUREhBGj.js} +11 -8
  23. package/dist/chunks/importer-eUREhBGj.js.map +1 -0
  24. package/dist/chunks/{importer-DOepkgnG.js → importer-vi3bSdtw.js} +4 -4
  25. package/dist/chunks/{importer-DOepkgnG.js.map → importer-vi3bSdtw.js.map} +1 -1
  26. package/dist/chunks/{importer-BVGtU1NA.js → importer-zGLo8Dg_.js} +6 -5
  27. package/dist/chunks/{importer-BVGtU1NA.js.map → importer-zGLo8Dg_.js.map} +1 -1
  28. package/dist/chunks/{json-elements-CZY1wiZh.js → json-elements-DDS8N2Dd.js} +2 -2
  29. package/dist/chunks/{json-elements-CZY1wiZh.js.map → json-elements-DDS8N2Dd.js.map} +1 -1
  30. package/dist/chunks/{records-BzNicMsf.js → records-dbRkxwaq.js} +2 -2
  31. package/dist/chunks/{records-BzNicMsf.js.map → records-dbRkxwaq.js.map} +1 -1
  32. package/dist/chunks/{report-BcWboivV.js → report-B1z4WT9e.js} +143 -111
  33. package/dist/chunks/{report-BcWboivV.js.map → report-B1z4WT9e.js.map} +1 -1
  34. package/dist/chunks/{weights-CwISIpCP.js → weights-Dzba96G3.js} +5 -5
  35. package/dist/chunks/{weights-CwISIpCP.js.map → weights-Dzba96G3.js.map} +1 -1
  36. package/dist/chunks/writer-C7flM-Ih.js +77 -0
  37. package/dist/chunks/writer-C7flM-Ih.js.map +1 -0
  38. package/dist/csv.js +6 -5
  39. package/dist/csv.js.map +1 -1
  40. package/dist/cx.js +1 -1
  41. package/dist/cx2.js +2 -2
  42. package/dist/cys.d.ts +1 -0
  43. package/dist/cys.js +6 -0
  44. package/dist/cys.js.map +1 -0
  45. package/dist/dot.js +1 -1
  46. package/dist/gexf.js +5 -4
  47. package/dist/gexf.js.map +1 -1
  48. package/dist/gml.js +12 -14
  49. package/dist/gml.js.map +1 -1
  50. package/dist/graph-io.js +288 -141
  51. package/dist/graph-io.js.map +1 -1
  52. package/dist/graphml.js +1 -1
  53. package/dist/json.js +1 -1
  54. package/dist/neo4j.js +6 -5
  55. package/dist/neo4j.js.map +1 -1
  56. package/dist/obo.js +1 -1
  57. package/dist/pajek.js +1 -1
  58. package/dist/src/common/cell-budget.d.ts +84 -0
  59. package/dist/src/common/cell-budget.d.ts.map +1 -0
  60. package/dist/src/common/cell-budget.js +138 -0
  61. package/dist/src/common/cell-budget.js.map +1 -0
  62. package/dist/src/common/input.d.ts +10 -0
  63. package/dist/src/common/input.d.ts.map +1 -1
  64. package/dist/src/common/input.js +39 -0
  65. package/dist/src/common/input.js.map +1 -1
  66. package/dist/src/common/options.d.ts +6 -0
  67. package/dist/src/common/options.d.ts.map +1 -1
  68. package/dist/src/common/options.js +1 -1
  69. package/dist/src/common/options.js.map +1 -1
  70. package/dist/src/common/xml.d.ts +37 -3
  71. package/dist/src/common/xml.d.ts.map +1 -1
  72. package/dist/src/common/xml.js +86 -6
  73. package/dist/src/common/xml.js.map +1 -1
  74. package/dist/src/common/zip.d.ts +92 -0
  75. package/dist/src/common/zip.d.ts.map +1 -0
  76. package/dist/src/common/zip.js +399 -0
  77. package/dist/src/common/zip.js.map +1 -0
  78. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  79. package/dist/src/formats/cx/importer.js +26 -17
  80. package/dist/src/formats/cx/importer.js.map +1 -1
  81. package/dist/src/formats/cys/constants.d.ts +68 -0
  82. package/dist/src/formats/cys/constants.d.ts.map +1 -0
  83. package/dist/src/formats/cys/constants.js +76 -0
  84. package/dist/src/formats/cys/constants.js.map +1 -0
  85. package/dist/src/formats/cys/importer.d.ts +38 -0
  86. package/dist/src/formats/cys/importer.d.ts.map +1 -0
  87. package/dist/src/formats/cys/importer.js +830 -0
  88. package/dist/src/formats/cys/importer.js.map +1 -0
  89. package/dist/src/formats/cys/index.d.ts +7 -0
  90. package/dist/src/formats/cys/index.d.ts.map +1 -0
  91. package/dist/src/formats/cys/index.js +7 -0
  92. package/dist/src/formats/cys/index.js.map +1 -0
  93. package/dist/src/formats/cys/session.d.ts +132 -0
  94. package/dist/src/formats/cys/session.d.ts.map +1 -0
  95. package/dist/src/formats/cys/session.js +315 -0
  96. package/dist/src/formats/cys/session.js.map +1 -0
  97. package/dist/src/formats/cys/tables.d.ts +90 -0
  98. package/dist/src/formats/cys/tables.d.ts.map +1 -0
  99. package/dist/src/formats/cys/tables.js +293 -0
  100. package/dist/src/formats/cys/tables.js.map +1 -0
  101. package/dist/src/formats/dot/exporter.d.ts +2 -0
  102. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  103. package/dist/src/formats/dot/exporter.js +12 -0
  104. package/dist/src/formats/dot/exporter.js.map +1 -1
  105. package/dist/src/formats/dot/importer.js +6 -2
  106. package/dist/src/formats/dot/importer.js.map +1 -1
  107. package/dist/src/formats/gexf/importer.d.ts +2 -2
  108. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  109. package/dist/src/formats/gexf/importer.js +1 -1
  110. package/dist/src/formats/gexf/importer.js.map +1 -1
  111. package/dist/src/formats/gml/exporter.d.ts +0 -8
  112. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  113. package/dist/src/formats/gml/exporter.js +7 -17
  114. package/dist/src/formats/gml/exporter.js.map +1 -1
  115. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  116. package/dist/src/formats/json/exporter.js +3 -1
  117. package/dist/src/formats/json/exporter.js.map +1 -1
  118. package/dist/src/formats/json/importer.d.ts.map +1 -1
  119. package/dist/src/formats/json/importer.js +3 -2
  120. package/dist/src/formats/json/importer.js.map +1 -1
  121. package/dist/src/formats/xgmml/columns.d.ts +152 -0
  122. package/dist/src/formats/xgmml/columns.d.ts.map +1 -0
  123. package/dist/src/formats/xgmml/columns.js +593 -0
  124. package/dist/src/formats/xgmml/columns.js.map +1 -0
  125. package/dist/src/formats/xgmml/constants.d.ts +196 -0
  126. package/dist/src/formats/xgmml/constants.d.ts.map +1 -0
  127. package/dist/src/formats/xgmml/constants.js +197 -0
  128. package/dist/src/formats/xgmml/constants.js.map +1 -0
  129. package/dist/src/formats/xgmml/document.d.ts +361 -0
  130. package/dist/src/formats/xgmml/document.d.ts.map +1 -0
  131. package/dist/src/formats/xgmml/document.js +695 -0
  132. package/dist/src/formats/xgmml/document.js.map +1 -0
  133. package/dist/src/formats/xgmml/emit.d.ts +341 -0
  134. package/dist/src/formats/xgmml/emit.d.ts.map +1 -0
  135. package/dist/src/formats/xgmml/emit.js +1275 -0
  136. package/dist/src/formats/xgmml/emit.js.map +1 -0
  137. package/dist/src/formats/xgmml/exporter.d.ts +24 -0
  138. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -0
  139. package/dist/src/formats/xgmml/exporter.js +999 -0
  140. package/dist/src/formats/xgmml/exporter.js.map +1 -0
  141. package/dist/src/formats/xgmml/importer.d.ts +58 -0
  142. package/dist/src/formats/xgmml/importer.d.ts.map +1 -0
  143. package/dist/src/formats/xgmml/importer.js +360 -0
  144. package/dist/src/formats/xgmml/importer.js.map +1 -0
  145. package/dist/src/formats/xgmml/index.d.ts +8 -0
  146. package/dist/src/formats/xgmml/index.d.ts.map +1 -0
  147. package/dist/src/formats/xgmml/index.js +8 -0
  148. package/dist/src/formats/xgmml/index.js.map +1 -0
  149. package/dist/src/formats/xgmml/values.d.ts +60 -0
  150. package/dist/src/formats/xgmml/values.d.ts.map +1 -0
  151. package/dist/src/formats/xgmml/values.js +148 -0
  152. package/dist/src/formats/xgmml/values.js.map +1 -0
  153. package/dist/src/index.d.ts +2 -0
  154. package/dist/src/index.d.ts.map +1 -1
  155. package/dist/src/index.js +2 -0
  156. package/dist/src/index.js.map +1 -1
  157. package/dist/src/registry.d.ts +7 -0
  158. package/dist/src/registry.d.ts.map +1 -1
  159. package/dist/src/registry.js +25 -10
  160. package/dist/src/registry.js.map +1 -1
  161. package/dist/src/sniff.d.ts +1 -1
  162. package/dist/src/sniff.d.ts.map +1 -1
  163. package/dist/src/sniff.js +2 -0
  164. package/dist/src/sniff.js.map +1 -1
  165. package/dist/xgmml.d.ts +1 -0
  166. package/dist/xgmml.js +9 -0
  167. package/dist/xgmml.js.map +1 -0
  168. package/package.json +15 -2
  169. package/src/common/cell-budget.ts +170 -0
  170. package/src/common/input.ts +40 -0
  171. package/src/common/options.ts +1 -1
  172. package/src/common/xml.ts +127 -6
  173. package/src/common/zip.ts +472 -0
  174. package/src/formats/cx/importer.ts +27 -19
  175. package/src/formats/cys/constants.ts +99 -0
  176. package/src/formats/cys/importer.ts +1100 -0
  177. package/src/formats/cys/index.ts +7 -0
  178. package/src/formats/cys/session.ts +428 -0
  179. package/src/formats/cys/tables.ts +400 -0
  180. package/src/formats/dot/exporter.ts +17 -0
  181. package/src/formats/dot/importer.ts +6 -2
  182. package/src/formats/gexf/importer.ts +0 -1
  183. package/src/formats/gml/exporter.ts +7 -18
  184. package/src/formats/json/exporter.ts +3 -1
  185. package/src/formats/json/importer.ts +3 -2
  186. package/src/formats/xgmml/columns.ts +747 -0
  187. package/src/formats/xgmml/constants.ts +265 -0
  188. package/src/formats/xgmml/document.ts +975 -0
  189. package/src/formats/xgmml/emit.ts +1563 -0
  190. package/src/formats/xgmml/exporter.ts +1215 -0
  191. package/src/formats/xgmml/importer.ts +491 -0
  192. package/src/formats/xgmml/index.ts +8 -0
  193. package/src/formats/xgmml/values.ts +183 -0
  194. package/src/index.ts +9 -0
  195. package/src/registry.ts +41 -15
  196. package/src/sniff.ts +5 -1
  197. package/dist/chunks/escape-CWExcecC.js.map +0 -1
  198. package/dist/chunks/importer-BAP4PrxR.js.map +0 -1
  199. package/dist/chunks/importer-BNFuV1-K.js.map +0 -1
  200. package/dist/chunks/importer-Bm1_5vPS.js.map +0 -1
  201. 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
+ }