@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,830 @@
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
+ import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
17
+ import { declareResolved } from "../../common/attributes.js";
18
+ import { IdCoercer } from "../../common/ids.js";
19
+ import { decodeEntryName, readBytes, throwIfAborted } from "../../common/input.js";
20
+ import { chooseGraph, reportSinkOptions, reportUnusedOptions, resolveImportOptions, } from "../../common/options.js";
21
+ import { ImportReportBuilder } from "../../common/report.js";
22
+ import { startsLikeZip } from "../../common/zip.js";
23
+ import { ImportError, } from "../../types.js";
24
+ import { XGMML_ISSUE, XGMML_ORIGIN_NAMESPACE } from "../xgmml/constants.js";
25
+ import { isCyTrue, } from "../xgmml/document.js";
26
+ import { dialectOf, membersOf, XgmmlEmitter, } from "../xgmml/emit.js";
27
+ import { parseXgmml, resolveSettings } from "../xgmml/importer.js";
28
+ import { CYS_ISSUE, CYTOSCAPE_NAMESPACE, DEFAULT_MAX_UNCOMPRESSED, EXTENSIONS, FORMAT, HIDDEN_COLUMN, META_KEY, MIME_TYPES, SELECTED_COLUMN, VIEW_POSITION_PREFIX, } from "./constants.js";
29
+ import { elementsNamed, listed, openSession, readXmlTree, urlDecode, } from "./session.js";
30
+ import { cellAtt, readCyTable, virtualColumnsOf } from "./tables.js";
31
+ /** The per-format defaults: SUIDs as written, edges directed unless they say otherwise, no weight. */
32
+ const FORMAT_DEFAULTS = {
33
+ ids: "keep",
34
+ defaultDirected: true,
35
+ weightFrom: null,
36
+ addMissingNodes: false,
37
+ };
38
+ /** The common options the session importer reads. */
39
+ const USED_OPTIONS = new Set([
40
+ "ids",
41
+ "addMissingNodes",
42
+ "duplicateEdges",
43
+ "selfLoops",
44
+ "onMixedDirection",
45
+ "defaultDirected",
46
+ "weightFrom",
47
+ "weightDtype",
48
+ "long",
49
+ "errorLimit",
50
+ "signal",
51
+ "onProgress",
52
+ ]);
53
+ /**
54
+ * The maxUncompressedBytes option, checked.
55
+ * @param value - the caller's value
56
+ * @returns the byte budget; E_UNSUPPORTED for anything but a positive number
57
+ */
58
+ function budgetOption(value) {
59
+ if (value === undefined) {
60
+ return DEFAULT_MAX_UNCOMPRESSED;
61
+ }
62
+ if (typeof value !== "number" || !(value > 0)) {
63
+ throw new GraphFormatError("E_UNSUPPORTED", `option maxUncompressedBytes: ${JSON.stringify(value)} is not a positive number`, {
64
+ option: "maxUncompressedBytes",
65
+ found: value,
66
+ });
67
+ }
68
+ return value;
69
+ }
70
+ /**
71
+ * Resolve the options, open the archive and list its networks.
72
+ * @param input - the input
73
+ * @param sink - the sink, or null for listGraphs
74
+ * @param options - the options
75
+ * @returns what the import needs
76
+ */
77
+ async function prepare(input, sink, options) {
78
+ const common = resolveImportOptions(options, FORMAT_DEFAULTS);
79
+ const report = new ImportReportBuilder(FORMAT, common.errorLimit);
80
+ if (sink !== null) {
81
+ reportSinkOptions(sink, options, report, true);
82
+ reportUnusedOptions(options, report, USED_OPTIONS);
83
+ }
84
+ const { zAs } = resolveSettings({ zAs: options?.zAs }, false);
85
+ const maxBytes = budgetOption(options?.maxUncompressedBytes);
86
+ const session = await openSession(input, report, common, maxBytes);
87
+ const inner = Object.freeze({ ...common, onProgress: null, encoding: null });
88
+ const docs = new Map();
89
+ const choices = session.layout.era === "3"
90
+ ? await networks3(session, report, inner, docs)
91
+ : await networks2(session, report, inner);
92
+ return { report, common, inner, zAs, session, choices, docs };
93
+ }
94
+ /**
95
+ * Read an XGMML entry. Its issues are relayed into the import's report with the entry name in
96
+ * the message; a fatal one fails the import.
97
+ * @param session - the session
98
+ * @param entry - the entry
99
+ * @param report - the import's report
100
+ * @param inner - the options entries are read with
101
+ * @returns the document and its dialect
102
+ */
103
+ async function parseEntry(session, entry, report, inner) {
104
+ const bytes = await session.read(entry);
105
+ const scratch = new ImportReportBuilder(FORMAT, Number.MAX_SAFE_INTEGER);
106
+ try {
107
+ const doc = await parseXgmml(bytes, scratch, inner, undefined);
108
+ const dialect = dialectOf(doc, scratch);
109
+ relay(scratch.issues, report, entry.name);
110
+ return { doc, dialect };
111
+ }
112
+ catch (err) {
113
+ if (!(err instanceof ImportError)) {
114
+ throw err;
115
+ }
116
+ const { issues } = scratch;
117
+ const fatal = issues.at(-1);
118
+ relay(issues.slice(0, -1), report, entry.name);
119
+ return report.fail(fatal?.code ?? CYS_ISSUE.CORRUPT, `${entry.name}: ${fatal?.message ?? err.message}`, {
120
+ line: fatal?.line ?? null,
121
+ });
122
+ }
123
+ }
124
+ /**
125
+ * Record issues of an entry in the import's report, the entry name before each message.
126
+ * @param issues - the issues
127
+ * @param report - the import's report
128
+ * @param entry - the entry name
129
+ */
130
+ function relay(issues, report, entry) {
131
+ for (const issue of issues) {
132
+ const where = { line: issue.line, element: issue.element };
133
+ if (issue.severity === "error") {
134
+ report.error(issue.category, issue.code, `${entry}: ${issue.message}`, where);
135
+ }
136
+ else {
137
+ report.warning(issue.category, issue.code, `${entry}: ${issue.message}`, where);
138
+ }
139
+ }
140
+ }
141
+ // ---------------------------------------------------------------------------------------- 3.x
142
+ /**
143
+ * The registered subnetworks of a 3.x session, in `network_list.xml` order (else entry order).
144
+ * @param session - the session
145
+ * @param report - the report
146
+ * @param inner - the options entries are read with
147
+ * @param docs - where the parsed network files are kept
148
+ * @returns the networks
149
+ */
150
+ async function networks3(session, report, inner, docs) {
151
+ const { layout } = session;
152
+ const choices = [];
153
+ for (const network of layout.networks) {
154
+ const parsed = await parseEntry(session, network, report, inner);
155
+ docs.set(network.name, parsed);
156
+ for (const graph of registeredGraphs(parsed)) {
157
+ const members = membersOf(parsed.doc, graph);
158
+ choices.push({
159
+ era: "3",
160
+ network,
161
+ graphId: graph.id ?? "",
162
+ name: graph.label ?? graph.id,
163
+ nodes: new Set(members.nodes).size,
164
+ edges: members.edges.length,
165
+ });
166
+ }
167
+ }
168
+ const known = new Set(choices.map((c) => c.graphId));
169
+ const strayViews = layout.views.filter((v) => !known.has(v.network)).map((v) => v.name);
170
+ const strayTables = layout.tables.filter((t) => !known.has(t.network) &&
171
+ !layout.networks.some((n) => n.suid === t.network) &&
172
+ !isGroupTable(t, session, docs));
173
+ if (strayViews.length > 0 || strayTables.length > 0) {
174
+ report.warning("validation-error", CYS_ISSUE.DANGLING_REFERENCE, `${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)])}`);
175
+ }
176
+ if (layout.networkList === null) {
177
+ return choices;
178
+ }
179
+ const tree = await readXmlTree(await session.read(layout.networkList), layout.networkList.name, report, inner);
180
+ const order = new Map();
181
+ for (const node of elementsNamed(tree, "network")) {
182
+ const id = node.attrs.get("id");
183
+ const position = Number(node.attrs.get("order"));
184
+ if (id !== undefined && Number.isFinite(position)) {
185
+ order.set(id, position);
186
+ }
187
+ }
188
+ return choices
189
+ .map((choice, index) => ({ choice, index }))
190
+ .sort((a, b) => (order.get(a.choice.graphId) ?? Infinity) - (order.get(b.choice.graphId) ?? Infinity) ||
191
+ a.index - b.index)
192
+ .map((c) => c.choice);
193
+ }
194
+ /**
195
+ * Whether a table belongs to a graph declared in some network file (a group's network): those are
196
+ * read with their network, not reported.
197
+ * @param table - the table
198
+ * @param session - the session
199
+ * @param docs - the parsed network files
200
+ * @returns true for a table of a known graph
201
+ */
202
+ function isGroupTable(table, session, docs) {
203
+ for (const network of session.layout.networks) {
204
+ if (docs.get(network.name)?.doc.graphs.some((g) => g.id === table.network) === true) {
205
+ return true;
206
+ }
207
+ }
208
+ return false;
209
+ }
210
+ /**
211
+ * The registered subnetworks of a network file (`cy:registered` true under the root).
212
+ * @param parsed - the network file
213
+ * @returns the graphs
214
+ */
215
+ function registeredGraphs(parsed) {
216
+ return parsed.doc.root.subgraphs.filter((g) => isCyTrue(g.registered));
217
+ }
218
+ /**
219
+ * Import one registered subnetwork of a 3.x session.
220
+ * @param prepared - the prepared import
221
+ * @param choice - the subnetwork
222
+ * @param sink - the sink
223
+ * @param parsed - its network file, parsed for this import (its records are filled in)
224
+ */
225
+ async function import3(prepared, choice, sink, parsed) {
226
+ const { report, inner, session } = prepared;
227
+ const { layout } = session;
228
+ const { doc, dialect } = parsed;
229
+ const graph = doc.root.subgraphs.find((g) => g.id === choice.graphId);
230
+ const members = membersOf(doc, graph);
231
+ const nodes = byId(members.nodes);
232
+ const edges = byId(members.edges);
233
+ const cache = new Map();
234
+ const tableOf = (path) => {
235
+ let pending = cache.get(path);
236
+ if (pending === undefined) {
237
+ const entry = layout.tables.find((t) => t.tablePath === path);
238
+ pending =
239
+ entry === undefined
240
+ ? Promise.resolve(null)
241
+ : session.read(entry).then((bytes) => readCyTable(bytes, path, entry.name, report, inner));
242
+ cache.set(path, pending);
243
+ }
244
+ return pending;
245
+ };
246
+ const virtuals = layout.cytables === null ? [] : await readVirtuals(session, layout.cytables, report, inner);
247
+ // a row of an element the file declares outside this network (a collapsed group's member, a
248
+ // meta-edge) is Cytoscape's bookkeeping, not a stale row
249
+ const declared = new Set([
250
+ ...doc.nodes.flatMap((n) => (n.id === null ? [] : [n.id])),
251
+ ...doc.edges.flatMap((e) => (e.id === null ? [] : [e.id])),
252
+ ]);
253
+ let unmatched = 0;
254
+ for (const entry of layout.tables.filter((t) => t.network === choice.graphId && t.element !== null)) {
255
+ throwIfAborted(inner.signal);
256
+ const table = await tableOf(entry.tablePath);
257
+ if (table === null) {
258
+ continue;
259
+ }
260
+ const local = entry.namespace === "LOCAL_ATTRS";
261
+ const shared = entry.namespace === "SHARED_ATTRS";
262
+ const namespace = local ? null : entry.namespace;
263
+ const hidden = !local && !shared;
264
+ const target = (key) => {
265
+ if (entry.element === "network") {
266
+ return key === choice.graphId ? graph.atts : undefined;
267
+ }
268
+ return (entry.element === "node" ? nodes : edges).get(key)?.atts;
269
+ };
270
+ for (const [key, cells] of table.rows) {
271
+ const atts = target(key);
272
+ if (atts === undefined) {
273
+ if (!declared.has(key)) {
274
+ unmatched++;
275
+ }
276
+ continue;
277
+ }
278
+ const line = table.lines.get(key) ?? 0;
279
+ for (let i = 1; i < table.columns.length && i < cells.length; i++) {
280
+ const att = cellAtt(table.columns[i], cells[i], line, namespace, hidden);
281
+ if (att !== null) {
282
+ atts.push(att);
283
+ }
284
+ }
285
+ }
286
+ for (const virtual of await virtualColumnsOf(table, virtuals, tableOf, report)) {
287
+ // a shared column named like a local one is renamed; the local one keeps the name
288
+ const owned = table.columns.some((c) => c.name === virtual.column.name);
289
+ let ns = namespace;
290
+ if (local) {
291
+ ns = owned ? "SHARED_ATTRS" : null;
292
+ }
293
+ for (const [key, text] of virtual.values) {
294
+ const att = cellAtt(virtual.column, text, table.lines.get(key) ?? 0, ns, hidden);
295
+ if (att !== null) {
296
+ target(key)?.push(att);
297
+ }
298
+ }
299
+ }
300
+ }
301
+ if (unmatched > 0) {
302
+ report.warning("parse-error", CYS_ISSUE.TABLE_ROW, `${unmatched} table row(s) of network ${choice.graphId} name no node, edge or network of it (stale rows); they are not read`);
303
+ }
304
+ const views = layout.views.filter((v) => v.network === choice.graphId);
305
+ let visualStyle = null;
306
+ const extraViews = [];
307
+ for (const [i, view] of views.entries()) {
308
+ const viewDoc = (await parseEntry(session, view, report, inner)).doc;
309
+ if (i === 0) {
310
+ visualStyle = viewDoc.root.attrs.get("cy:visualStyle") ?? null;
311
+ applyView(viewDoc, graph, nodes, edges, report, view.name);
312
+ }
313
+ else {
314
+ extraViews.push(viewPositions(viewDoc, prepared.zAs));
315
+ }
316
+ }
317
+ const cyMeta = sessionMeta(prepared, {
318
+ collection: doc.root.label ?? choice.network.title,
319
+ network: choice.graphId,
320
+ parentNetwork: parentNetwork(graph, prepared.choices),
321
+ visualStyle,
322
+ });
323
+ const groups = [];
324
+ cyMeta.groups = groups;
325
+ const extras = {
326
+ sourceFormat: FORMAT,
327
+ entry: choice.network.name,
328
+ graphName: localName(graph) ?? choice.name,
329
+ metaExtra: { [META_KEY]: cyMeta },
330
+ groupNodes: new Set(members.nodes
331
+ .filter((n) => n.atts.some((a) => a.name === "__isGroup" && isCyTrue(a.value)))
332
+ .flatMap((n) => (n.id === null ? [] : [n.id]))),
333
+ resolvePointer: pointerResolver(prepared, doc),
334
+ onMissingMember: (group, member) => {
335
+ const record = groups.find((g) => g.group === group);
336
+ if (record === undefined) {
337
+ groups.push({ group, members: [member] });
338
+ }
339
+ else if (!record.members.includes(member)) {
340
+ record.members.push(member);
341
+ }
342
+ },
343
+ };
344
+ const settings = { labelAliases: false, cytoscapeEscapes: false, zAs: prepared.zAs };
345
+ new XgmmlEmitter(doc, dialect, sink, report, prepared.inner, settings, extras).emit(graph);
346
+ if (groups.length > 0) {
347
+ report.warning("unsupported", CYS_ISSUE.COLLAPSED_GROUP, `${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`);
348
+ }
349
+ reportRootOnly(parsed, report, choice.network.name);
350
+ writeViewPositions(sink, extraViews, views.slice(1).map((v) => v.view), report, prepared.inner);
351
+ }
352
+ /**
353
+ * The virtual columns of `tables/cytables.xml`.
354
+ * @param session - the session
355
+ * @param entry - the cytables.xml entry
356
+ * @param report - the report
357
+ * @param inner - the options entries are read with
358
+ * @returns the virtual columns in document order
359
+ */
360
+ async function readVirtuals(session, entry, report, inner) {
361
+ const tree = await readXmlTree(await session.read(entry), entry.name, report, inner);
362
+ const out = [];
363
+ for (const node of elementsNamed(tree, "virtualColumn")) {
364
+ const a = (key) => node.attrs.get(key) ?? "";
365
+ out.push({
366
+ name: a("name"),
367
+ targetTable: a("targetTable"),
368
+ sourceTable: a("sourceTable"),
369
+ sourceColumn: a("sourceColumn"),
370
+ sourceJoinKey: node.attrs.get("sourceJoinKey") ?? "SUID",
371
+ targetJoinKey: node.attrs.get("targetJoinKey") ?? "SUID",
372
+ });
373
+ }
374
+ return out;
375
+ }
376
+ /**
377
+ * Records by id.
378
+ * @param records - node or edge records
379
+ * @returns the first record of each id
380
+ */
381
+ function byId(records) {
382
+ const out = new Map();
383
+ for (const record of records) {
384
+ if (record.id !== null && !out.has(record.id)) {
385
+ out.set(record.id, record);
386
+ }
387
+ }
388
+ return out;
389
+ }
390
+ /**
391
+ * Copy a view's coordinates and graphics onto the network's records (by `cy:nodeId` and
392
+ * `cy:edgeId`), and its network graphics onto the graph. A view element naming nothing of the
393
+ * network is counted in one W_DANGLING_REFERENCE.
394
+ * @param view - the view document
395
+ * @param graph - the subnetwork
396
+ * @param nodes - its node records by id
397
+ * @param edges - its edge records by id
398
+ * @param report - the report
399
+ * @param entry - the view entry name
400
+ */
401
+ function applyView(view, graph, nodes, edges, report, entry) {
402
+ let dangling = 0;
403
+ for (const node of view.nodes) {
404
+ const target = node.viewId === null ? undefined : nodes.get(node.viewId);
405
+ if (target === undefined) {
406
+ dangling++;
407
+ continue;
408
+ }
409
+ target.x = node.x;
410
+ target.y = node.y;
411
+ target.z = node.z;
412
+ if (node.graphics !== null) {
413
+ target.graphics = { ...(target.graphics ?? {}), ...node.graphics };
414
+ }
415
+ }
416
+ for (const edge of view.edges) {
417
+ const target = edge.viewId === null ? undefined : edges.get(edge.viewId);
418
+ if (target === undefined) {
419
+ dangling++;
420
+ continue;
421
+ }
422
+ if (edge.graphics !== null) {
423
+ target.graphics = { ...(target.graphics ?? {}), ...edge.graphics };
424
+ }
425
+ }
426
+ if (view.root.graphics !== null) {
427
+ graph.graphics = { ...(graph.graphics ?? {}), ...view.root.graphics };
428
+ }
429
+ if (dangling > 0) {
430
+ report.warning("validation-error", CYS_ISSUE.DANGLING_REFERENCE, `${entry}: ${dangling} view element(s) name no node or edge of the network; they are not read`);
431
+ }
432
+ }
433
+ /**
434
+ * The positions of a further view, by model node id: y flipped to y-up, z in the position only
435
+ * under zAs "position".
436
+ * @param view - the view document
437
+ * @param zAs - where z goes
438
+ * @returns the positions
439
+ */
440
+ function viewPositions(view, zAs) {
441
+ const out = new Map();
442
+ for (const node of view.nodes) {
443
+ const x = Number(node.x);
444
+ const y = Number(node.y);
445
+ const z = zAs === "position" ? Number(node.z ?? 0) : 0;
446
+ if (node.viewId !== null && node.x !== null && node.y !== null && Number.isFinite(x) && Number.isFinite(y)) {
447
+ out.set(node.viewId, [x, y === 0 ? 0 : -y, Number.isFinite(z) ? z : 0]);
448
+ }
449
+ }
450
+ return out;
451
+ }
452
+ /**
453
+ * Write each further view's positions as a `position@<n>` column (n from 2), no role.
454
+ * @param sink - the sink
455
+ * @param views - the positions of each further view
456
+ * @param viewIds - each view's SUID
457
+ * @param report - the report
458
+ * @param common - the id rule
459
+ */
460
+ function writeViewPositions(sink, views, viewIds, report, common) {
461
+ const coercer = new IdCoercer(common.ids);
462
+ views.forEach((positions, i) => {
463
+ const name = `${VIEW_POSITION_PREFIX}${i + 2}`;
464
+ const { handle } = declareResolved(sink, "node", {
465
+ name,
466
+ dtype: "f32",
467
+ components: 3,
468
+ nullable: true,
469
+ origin: {
470
+ format: FORMAT,
471
+ id: viewIds[i],
472
+ title: null,
473
+ type: "graphics",
474
+ namespace: XGMML_ORIGIN_NAMESPACE,
475
+ },
476
+ extra: { sourceDims: 2, units: "file" },
477
+ }, report, { element: name });
478
+ for (const [id, xyz] of positions) {
479
+ let index;
480
+ try {
481
+ index = sink.indexOf(coercer.text(id));
482
+ }
483
+ catch {
484
+ continue; // an id the id rule refuses was refused, and recorded, as a node already
485
+ }
486
+ if (index !== INVALID_INDEX) {
487
+ sink.setNodeValue(handle, index, xyz);
488
+ }
489
+ }
490
+ });
491
+ }
492
+ /**
493
+ * The network's name from its own table (the LOCAL `name` att), else null.
494
+ * @param graph - the subnetwork with its table atts
495
+ * @returns the name
496
+ */
497
+ function localName(graph) {
498
+ const att = graph.atts.find((a) => a.name === "name" && (a.namespace ?? null) === null);
499
+ return att?.value ?? null;
500
+ }
501
+ /**
502
+ * The name of the network a subnetwork was made from: the hidden `__parentNetwork.SUID` (or the
503
+ * pre-3.4 `Cy2 Parent Network.SUID`), named when the session holds it.
504
+ * @param graph - the subnetwork with its table atts
505
+ * @param choices - the session's networks
506
+ * @returns the parent's name, its SUID when unnamed, or null
507
+ */
508
+ function parentNetwork(graph, choices) {
509
+ const att = graph.atts.find((a) => a.name === "__parentNetwork.SUID" || a.name === "Cy2 Parent Network.SUID");
510
+ if (att?.value === undefined || att.value === null || att.value.length === 0) {
511
+ return null;
512
+ }
513
+ const suid = att.value;
514
+ const named = choices.find((c) => c.era === "3" && c.graphId === suid);
515
+ return named?.name ?? suid;
516
+ }
517
+ /**
518
+ * The resolver of nested-network pointers into other network files of the session
519
+ * (`207-Set+2.xgmml#223`): the target graph's label. Only the files a pointer names are read.
520
+ * @param prepared - the prepared import
521
+ * @param doc - the network file being read
522
+ * @returns the resolver
523
+ */
524
+ function pointerResolver(prepared, doc) {
525
+ const names = new Map();
526
+ for (const graph of doc.graphs) {
527
+ const { href } = graph;
528
+ const hash = href?.indexOf("#") ?? -1;
529
+ if (href === null || hash <= 0 || names.has(href)) {
530
+ continue;
531
+ }
532
+ const file = urlDecode(href.slice(0, hash));
533
+ const id = href.slice(hash + 1);
534
+ const network = prepared.session.layout.networks.find((n) => urlDecode(n.path.slice("networks/".length)) === file);
535
+ const parsed = network === undefined ? undefined : prepared.docs.get(network.name);
536
+ const target = parsed?.doc.graphs.find((g) => g.id === id);
537
+ if (target !== undefined) {
538
+ names.set(href, target.label ?? id);
539
+ }
540
+ }
541
+ return (href) => names.get(href) ?? null;
542
+ }
543
+ /**
544
+ * W_XGMML_ROOT_ONLY_ELEMENTS for elements of a network file no registered subnetwork holds.
545
+ * @param parsed - the network file
546
+ * @param report - the report
547
+ * @param entry - the entry name
548
+ */
549
+ function reportRootOnly(parsed, report, entry) {
550
+ const held = new Set();
551
+ for (const graph of registeredGraphs(parsed)) {
552
+ const members = membersOf(parsed.doc, graph);
553
+ members.nodes.forEach((n) => held.add(n));
554
+ members.edges.forEach((e) => held.add(e));
555
+ }
556
+ const nodes = parsed.doc.nodes.filter((n) => !held.has(n)).length;
557
+ const edges = parsed.doc.edges.filter((e) => !held.has(e)).length;
558
+ if (nodes + edges > 0) {
559
+ report.warning("unsupported", XGMML_ISSUE.ROOT_ONLY_ELEMENTS, `${entry}: ${nodes} node(s) and ${edges} edge(s) belong to no registered network (group meta-edges, collapsed group members) and were not read`);
560
+ }
561
+ }
562
+ // ---------------------------------------------------------------------------------------- 2.x
563
+ /**
564
+ * The networks of a 2.x session: every network of `cysession.xml`'s tree except `Network Root`,
565
+ * in the order of their files in the archive.
566
+ * @param session - the session
567
+ * @param report - the report
568
+ * @param inner - the options entries are read with
569
+ * @returns the networks
570
+ */
571
+ async function networks2(session, report, inner) {
572
+ const { layout } = session;
573
+ if (layout.cysession === null) {
574
+ return report.fail(CYS_ISSUE.NOT_SESSION, "the session has no cysession.xml");
575
+ }
576
+ const tree = await readXmlTree(await session.read(layout.cysession), layout.cysession.name, report, inner);
577
+ const documentVersion = tree.attrs.get("documentVersion") ?? "";
578
+ if (documentVersion.startsWith("3")) {
579
+ report.fail(CYS_ISSUE.VERSION, `cysession.xml has documentVersion ${documentVersion} and no version marker: the 2011 Cytoscape 3.0 pre-release layout, which no released Cytoscape reads`);
580
+ }
581
+ const byDecoded = new Map();
582
+ for (const [path, entry] of layout.files) {
583
+ byDecoded.set(path, entry);
584
+ byDecoded.set(urlDecode(path), entry);
585
+ }
586
+ const order = [...layout.files.values()];
587
+ const choices = [];
588
+ const missing = [];
589
+ const named = new Set();
590
+ for (const record of elementsNamed(tree, "network")) {
591
+ const id = record.attrs.get("id") ?? "";
592
+ const filename = record.attrs.get("filename") ?? `${id}.xgmml`;
593
+ if (id === "Network Root") {
594
+ continue;
595
+ }
596
+ const file = byDecoded.get(filename);
597
+ if (file === undefined) {
598
+ missing.push(filename);
599
+ continue;
600
+ }
601
+ named.add(file);
602
+ const parent = record.children.find((c) => c.name === "parent")?.attrs.get("id") ?? null;
603
+ choices.push({
604
+ era: "2",
605
+ file,
606
+ name: id,
607
+ record,
608
+ parent: parent === "Network Root" || parent === "NULL" ? null : parent,
609
+ });
610
+ }
611
+ if (missing.length > 0) {
612
+ report.warning("validation-error", CYS_ISSUE.DANGLING_REFERENCE, `cysession.xml names ${missing.length} network file(s) the session does not hold: ${listed(missing)}`);
613
+ }
614
+ const unnamed = order.filter((e) => !named.has(e)).map((e) => e.name);
615
+ if (unnamed.length > 0) {
616
+ report.warning("unsupported", CYS_ISSUE.ENTRY_SKIPPED, `${unnamed.length} network file(s) are not in cysession.xml's network tree and were not read: ${listed(unnamed)}`);
617
+ }
618
+ return choices.sort((a, b) => order.indexOf(a.file) - order.indexOf(b.file));
619
+ }
620
+ /**
621
+ * Import one network of a 2.x session.
622
+ * @param prepared - the prepared import
623
+ * @param choice - the network
624
+ * @param sink - the sink
625
+ */
626
+ async function import2(prepared, choice, sink) {
627
+ const { report, inner, session } = prepared;
628
+ const { doc, dialect } = await parseEntry(session, choice.file, report, inner);
629
+ for (const [list, column] of [
630
+ ["selectedNodes", SELECTED_COLUMN],
631
+ ["hiddenNodes", HIDDEN_COLUMN],
632
+ ]) {
633
+ mark(doc.nodes, idsIn(choice.record, list), column);
634
+ }
635
+ for (const [list, column] of [
636
+ ["selectedEdges", SELECTED_COLUMN],
637
+ ["hiddenEdges", HIDDEN_COLUMN],
638
+ ]) {
639
+ mark(doc.edges, idsIn(choice.record, list), column);
640
+ }
641
+ const cyMeta = sessionMeta(prepared, {
642
+ collection: null,
643
+ network: choice.name,
644
+ parentNetwork: choice.parent,
645
+ visualStyle: choice.record.attrs.get("visualStyle") ?? null,
646
+ });
647
+ const extras = {
648
+ sourceFormat: FORMAT,
649
+ entry: choice.file.name,
650
+ graphName: choice.name,
651
+ metaExtra: { [META_KEY]: cyMeta },
652
+ };
653
+ const settings = { labelAliases: true, cytoscapeEscapes: true, zAs: prepared.zAs };
654
+ new XgmmlEmitter(doc, dialect, sink, report, inner, settings, extras).emit(null);
655
+ }
656
+ /**
657
+ * The element ids a `cysession.xml` network record lists under one key (`selectedNodes`, ...).
658
+ * @param record - the network record
659
+ * @param list - the list element's name
660
+ * @returns the ids (2.x node names, edge identifiers)
661
+ */
662
+ function idsIn(record, list) {
663
+ const out = new Set();
664
+ for (const holder of record.children.filter((c) => c.name === list)) {
665
+ for (const item of holder.children) {
666
+ const id = item.attrs.get("id");
667
+ if (id !== undefined) {
668
+ out.add(id);
669
+ }
670
+ }
671
+ }
672
+ return out;
673
+ }
674
+ /**
675
+ * Give the records 2.x lists by name (a node's name is its label; an edge is named by its id or
676
+ * label) a true cell in a bool column of the cytoscape namespace.
677
+ * @param records - the node or edge records
678
+ * @param ids - the listed names
679
+ * @param column - the column
680
+ */
681
+ function mark(records, ids, column) {
682
+ if (ids.size === 0) {
683
+ return;
684
+ }
685
+ for (const record of records) {
686
+ if ((record.id !== null && ids.has(record.id)) || (record.label !== null && ids.has(record.label))) {
687
+ record.atts.push({
688
+ name: column,
689
+ type: null,
690
+ cyType: "Boolean",
691
+ elementType: null,
692
+ value: "true",
693
+ hidden: false,
694
+ equation: false,
695
+ children: [],
696
+ extra: {},
697
+ xml: null,
698
+ hasGraph: false,
699
+ text: "",
700
+ line: record.line,
701
+ namespace: CYTOSCAPE_NAMESPACE,
702
+ });
703
+ }
704
+ }
705
+ }
706
+ /**
707
+ * The session facts of one network, and the once-per-import warnings about what is not read
708
+ * (styles, other entries).
709
+ * @param prepared - the prepared import
710
+ * @param facts - the network's facts
711
+ * @param facts.collection - the root network's name (3.x)
712
+ * @param facts.network - the network's id
713
+ * @param facts.parentNetwork - the network it was made from
714
+ * @param facts.visualStyle - the style its view uses
715
+ * @returns the record
716
+ */
717
+ function sessionMeta(prepared, facts) {
718
+ const { layout } = prepared.session;
719
+ const { report } = prepared;
720
+ if (layout.styles.length > 0) {
721
+ report.warning("unsupported", CYS_ISSUE.STYLES_NOT_IMPORTED, `the session's styles (${layout.styles.map((s) => s.path).join(", ")}) are not applied: style import is issue #706`);
722
+ }
723
+ if (layout.skipped.length > 0) {
724
+ report.warning("unsupported", CYS_ISSUE.ENTRY_SKIPPED, `${layout.skipped.length} entries hold no graph data and were not read (apps, global tables, properties, images): ${listed(layout.skipped)}`);
725
+ }
726
+ return {
727
+ sessionVersion: layout.version,
728
+ ...facts,
729
+ skippedEntries: [...layout.skipped],
730
+ };
731
+ }
732
+ /**
733
+ * Import one network of a prepared session into a sink.
734
+ * @param prepared - the prepared import
735
+ * @param index - the network
736
+ * @param sink - the sink
737
+ * @returns the report
738
+ */
739
+ async function importChoice(prepared, index, sink) {
740
+ const choice = prepared.choices[index];
741
+ if (choice.era === "3") {
742
+ const parsed = prepared.docs.get(choice.network.name);
743
+ await import3(prepared, choice, sink, parsed);
744
+ }
745
+ else {
746
+ await import2(prepared, choice, sink);
747
+ }
748
+ throwIfAborted(prepared.common.signal);
749
+ return prepared.report.finish();
750
+ }
751
+ /**
752
+ * Import one network of a session.
753
+ * @param input - the session bytes
754
+ * @param sink - the sink
755
+ * @param options - format-specific and common options
756
+ * @returns the report; ImportError when the session cannot be read or the error limit is exceeded
757
+ */
758
+ async function importCys(input, sink, options) {
759
+ const prepared = await prepare(input, sink, options);
760
+ const index = chooseGraph(prepared.choices.map((c) => c.name), options, prepared.report);
761
+ if (prepared.choices.length > 1) {
762
+ prepared.report.warning("unsupported", CYS_ISSUE.MULTIPLE_GRAPHS, `the session holds ${prepared.choices.length} networks; ${prepared.choices.length - 1} were not read (use importAll, graphIndex or graphName)`);
763
+ }
764
+ return importChoice(prepared, index, sink);
765
+ }
766
+ /**
767
+ * Import every network of a session, each into its own sink with its own report.
768
+ * @param input - the session bytes
769
+ * @param sinkFor - a sink per network
770
+ * @param options - format-specific and common options
771
+ * @returns one report per network
772
+ */
773
+ async function importAllCys(input, sinkFor, options) {
774
+ const bytes = (await readBytes(input, { signal: options?.signal ?? null })) ?? input;
775
+ const first = await prepare(bytes, null, options);
776
+ const reports = [];
777
+ for (let i = 0; i < first.choices.length; i++) {
778
+ const sink = sinkFor(i);
779
+ // every network gets fresh records and a fresh report
780
+ const prepared = i === 0 ? first : await prepare(bytes, null, options);
781
+ reportSinkOptions(sink, options, prepared.report, true);
782
+ reportUnusedOptions(options, prepared.report, USED_OPTIONS);
783
+ reports.push(await importChoice(prepared, i, sink));
784
+ }
785
+ return reports;
786
+ }
787
+ /**
788
+ * List the networks of a session from its network files, without reading tables or views.
789
+ * @param input - the session bytes
790
+ * @param options - the options
791
+ * @returns one listing per network; node and edge counts for 3.x sessions only
792
+ */
793
+ async function listCysGraphs(input, options) {
794
+ const prepared = await prepare(input, null, options);
795
+ return prepared.choices.map((choice, index) => Object.freeze({
796
+ index,
797
+ name: choice.name,
798
+ nodes: choice.era === "3" ? choice.nodes : null,
799
+ edges: choice.era === "3" ? choice.edges : null,
800
+ }));
801
+ }
802
+ /** A session's marker or folder name in the head of the archive. */
803
+ const SESSION_NAME = /CytoscapeSession|cysession\.xml|\d+\.\d+\.\d+\.version/;
804
+ /**
805
+ * Confidence that a head of bytes is a Cytoscape session: 0.95 for a zip whose head names the
806
+ * session folder, its version marker or cysession.xml; 0 for any other zip, so an `.xlsx`, a
807
+ * `.docx` or a zipped GraphML ranks as no format rather than as a broken session. The zip may
808
+ * start after a short stub (a self-extracting archive) within the head.
809
+ * @param head - the first bytes
810
+ * @returns the confidence
811
+ */
812
+ export function sniffCys(head) {
813
+ for (let at = 0; at + 4 <= head.byteLength; at++) {
814
+ if (startsLikeZip(head.subarray(at))) {
815
+ return SESSION_NAME.test(decodeEntryName(head.subarray(at))) ? 0.95 : 0;
816
+ }
817
+ }
818
+ return 0;
819
+ }
820
+ /** The Cytoscape session importer. */
821
+ export const cysImporter = Object.freeze({
822
+ format: FORMAT,
823
+ extensions: EXTENSIONS,
824
+ mimeTypes: MIME_TYPES,
825
+ sniff: sniffCys,
826
+ import: importCys,
827
+ importAll: importAllCys,
828
+ listGraphs: listCysGraphs,
829
+ });
830
+ //# sourceMappingURL=importer.js.map