@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,491 @@
1
+ /**
2
+ * The XGMML importer (design `design/graph-io/cytoscape-and-obo/design.md` section 1.1;
3
+ * `research-xgmml.md`): one reader for the 1.0 draft, the Cytoscape 2.x and 3.x exports and the
4
+ * Cytoscape 3 session network files. The document is read in one pass of the shared XML tokenizer
5
+ * into records (document.ts), and resolved into the sink once the outermost `</graph>` closes
6
+ * (emit.ts), so forward references and edges before their nodes need nothing special.
7
+ *
8
+ * Direction follows the specification: the root `directed` (0 by the DTD, else the
9
+ * `defaultDirected` option), then `cy:directed` per edge, overruling Cytoscape, which ignores the
10
+ * root attribute. Ids are kept as written (`ids: "keep"`), so `"1"`, `"01"` and `" 1 "` stay three
11
+ * nodes. Positions are stored y-up (Cytoscape writes screen coordinates). Graphics values are kept
12
+ * as a json column, never interpreted (style import is issue #706).
13
+ */
14
+
15
+ import { GraphFormatError, type GraphSink } from "@graphty/graph-format";
16
+
17
+ import { textChunks, throwIfAborted } from "../../common/input.js";
18
+ import {
19
+ chooseGraph,
20
+ type ImportFormatDefaults,
21
+ reportSinkOptions,
22
+ reportUnusedOptions,
23
+ type ResolvedImportOptions,
24
+ resolveImportOptions,
25
+ } from "../../common/options.js";
26
+ import { ImportReportBuilder } from "../../common/report.js";
27
+ import { isWhitespace, tokenizeXml, xmlDeclaredEncoding, type XmlRepairs, XmlSyntaxError } from "../../common/xml.js";
28
+ import {
29
+ type CommonImportOptions,
30
+ type GraphChoiceOptions,
31
+ type GraphImporter,
32
+ type GraphListing,
33
+ type ImportInput,
34
+ type ImportReport,
35
+ } from "../../types.js";
36
+ import { EXTENSIONS, FORMAT, MIME_TYPES, XGMML_ISSUE } from "./constants.js";
37
+ import { type GraphRec, type XgmmlDocument, XgmmlParser } from "./document.js";
38
+ import {
39
+ type Dialect,
40
+ dialectOf,
41
+ type EmitExtras,
42
+ graphsOf,
43
+ membersOf,
44
+ XgmmlEmitter,
45
+ type XgmmlSettings,
46
+ } from "./emit.js";
47
+
48
+ /** The format-specific options of the XGMML importer. */
49
+ export interface XgmmlImportOptions extends GraphChoiceOptions {
50
+ /**
51
+ * Resolve an edge endpoint that is missing or names no node through Cytoscape's
52
+ * `"source (interaction) target"` edge label, and fill a missing interaction from it. Default:
53
+ * on for files that use the Cytoscape (`cy`) namespace, off otherwise.
54
+ */
55
+ labelAliases?: boolean | undefined;
56
+ /**
57
+ * Decode Cytoscape's two-character `\n` and `\t` escapes in string values. Default: on for
58
+ * files that use the Cytoscape namespace, off otherwise.
59
+ */
60
+ cytoscapeEscapes?: boolean | undefined;
61
+ /** Read an `&` not followed by `;` within 7 characters as `&amp;` (warned per occurrence). Default false. */
62
+ repairBareAmpersands?: boolean | undefined;
63
+ /** Join two surrogate character references into one character (warned per pair). Default false. */
64
+ pairSurrogateReferences?: boolean | undefined;
65
+ /** Where Cytoscape's z (a stacking order) goes: the `z` column (default) or the position. */
66
+ zAs?: "column" | "position" | undefined;
67
+ }
68
+
69
+ /** The per-format defaults: ids as written, undirected by the DTD, the edge `weight` attribute. */
70
+ const FORMAT_DEFAULTS: ImportFormatDefaults = {
71
+ ids: "keep",
72
+ defaultDirected: false,
73
+ weightFrom: "weight",
74
+ addMissingNodes: false,
75
+ };
76
+
77
+ /** The common options the XGMML importer reads. */
78
+ const USED_OPTIONS: ReadonlySet<keyof CommonImportOptions> = new Set<keyof CommonImportOptions>([
79
+ "ids",
80
+ "addMissingNodes",
81
+ "duplicateEdges",
82
+ "selfLoops",
83
+ "onMixedDirection",
84
+ "defaultDirected",
85
+ "weightFrom",
86
+ "weightDtype",
87
+ "long",
88
+ "errorLimit",
89
+ "signal",
90
+ "onProgress",
91
+ "encoding",
92
+ ]);
93
+
94
+ /** Bytes inspected by sniff(). */
95
+ const SNIFF_BYTES = 4096;
96
+
97
+ /** The head of the document kept for the DOCTYPE check. */
98
+ const HEAD_CHARS = 2048;
99
+
100
+ /** An XGMML DOCTYPE (Cytoscape's file filter tests the same). */
101
+ const XGMML_DOCTYPE = /<!DOCTYPE\s+graph\s[^<>]*xgmml\.dtd/i;
102
+
103
+ /**
104
+ * The XGMML option values, checked.
105
+ * @param options - the caller's options
106
+ * @param cytoscape - whether the document uses the Cytoscape namespace
107
+ * @returns the settings; E_UNSUPPORTED for a value of the wrong type
108
+ */
109
+ export function resolveSettings(options: XgmmlImportOptions | undefined, cytoscape: boolean): XgmmlSettings {
110
+ const zAs = options?.zAs ?? "column";
111
+ if (zAs !== "column" && zAs !== "position") {
112
+ throw new GraphFormatError(
113
+ "E_UNSUPPORTED",
114
+ `option zAs: ${JSON.stringify(zAs)} is not "column" or "position"`,
115
+ {
116
+ option: "zAs",
117
+ found: zAs,
118
+ },
119
+ );
120
+ }
121
+ return {
122
+ labelAliases: booleanOption("labelAliases", options?.labelAliases, cytoscape),
123
+ cytoscapeEscapes: booleanOption("cytoscapeEscapes", options?.cytoscapeEscapes, cytoscape),
124
+ zAs,
125
+ };
126
+ }
127
+
128
+ /**
129
+ * One boolean option.
130
+ * @param name - the option name
131
+ * @param value - the caller's value
132
+ * @param fallback - the default
133
+ * @returns the value; E_UNSUPPORTED when not a boolean
134
+ */
135
+ function booleanOption(name: string, value: unknown, fallback: boolean): boolean {
136
+ if (value === undefined) {
137
+ return fallback;
138
+ }
139
+ if (typeof value !== "boolean") {
140
+ throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: ${JSON.stringify(value)} is not a boolean`, {
141
+ option: name,
142
+ found: value,
143
+ });
144
+ }
145
+ return value;
146
+ }
147
+
148
+ /**
149
+ * Read an XGMML document into records. Fatal conditions (not XML, not a graph, empty) fail the
150
+ * report.
151
+ * @param input - the input
152
+ * @param report - the report
153
+ * @param common - the resolved common options
154
+ * @param options - the XGMML options (for the repairs)
155
+ * @returns the document
156
+ */
157
+ export async function parseXgmml(
158
+ input: ImportInput,
159
+ report: ImportReportBuilder,
160
+ common: ResolvedImportOptions,
161
+ options: XgmmlImportOptions | undefined,
162
+ ): Promise<XgmmlDocument> {
163
+ const repairs: XmlRepairs = {
164
+ bareAmpersand: booleanOption("repairBareAmpersands", options?.repairBareAmpersands, false)
165
+ ? (line): void => {
166
+ report.warning("parse-error", XGMML_ISSUE.AMPERSAND_REPAIRED, "a bare & was read as &amp;", { line });
167
+ }
168
+ : undefined,
169
+ surrogatePair: booleanOption("pairSurrogateReferences", options?.pairSurrogateReferences, false)
170
+ ? (line): void => {
171
+ report.warning(
172
+ "parse-error",
173
+ XGMML_ISSUE.SURROGATE_PAIRED,
174
+ "two surrogate character references were joined into one character",
175
+ { line },
176
+ );
177
+ }
178
+ : undefined,
179
+ };
180
+ const parser = new XgmmlParser(report);
181
+ const seen = { head: "", content: false };
182
+ try {
183
+ await tokenizeXml(
184
+ watch(textChunks(input, report, { ...common, declaredEncoding: xmlDeclaredEncoding }), seen),
185
+ parser,
186
+ repairs,
187
+ );
188
+ } catch (err) {
189
+ if (err instanceof XmlSyntaxError) {
190
+ if (!seen.content) {
191
+ report.fail(XGMML_ISSUE.EMPTY_INPUT, "the input is empty");
192
+ }
193
+ report.fail(XGMML_ISSUE.XML_SYNTAX, err.message, { line: err.line });
194
+ }
195
+ throw err;
196
+ }
197
+ const doc = parser.document();
198
+ if (!doc.xgmmlNamespace && !XGMML_DOCTYPE.test(seen.head)) {
199
+ report.warning(
200
+ "validation-error",
201
+ XGMML_ISSUE.NO_NAMESPACE,
202
+ "the root <graph> has neither the XGMML namespace nor an XGMML DOCTYPE; it is read as XGMML",
203
+ { line: doc.root.line },
204
+ );
205
+ }
206
+ return doc;
207
+ }
208
+
209
+ /**
210
+ * Pass text chunks through while keeping the head and noting non-whitespace content.
211
+ * @param chunks - the chunks
212
+ * @param seen - where the head and the content flag are kept
213
+ * @param seen.head - the first characters of the document
214
+ * @param seen.content - whether non-whitespace text was seen
215
+ * @yields the chunks unchanged
216
+ * @returns nothing
217
+ */
218
+ async function* watch(
219
+ chunks: AsyncIterable<string>,
220
+ seen: { head: string; content: boolean },
221
+ ): AsyncGenerator<string, void, undefined> {
222
+ for await (const chunk of chunks) {
223
+ if (seen.head.length < HEAD_CHARS) {
224
+ seen.head += chunk.slice(0, HEAD_CHARS - seen.head.length);
225
+ }
226
+ seen.content ||= !isWhitespace(chunk);
227
+ yield chunk;
228
+ }
229
+ }
230
+
231
+ /** What an import call sets up before it emits. */
232
+ interface Prepared {
233
+ readonly report: ImportReportBuilder;
234
+ readonly common: ResolvedImportOptions;
235
+ readonly doc: XgmmlDocument;
236
+ readonly dialect: Dialect;
237
+ readonly settings: XgmmlSettings;
238
+ readonly graphs: readonly GraphRec[];
239
+ }
240
+
241
+ /**
242
+ * Resolve the options, read the document and check it is a graph document.
243
+ * @param input - the input
244
+ * @param sink - the sink, or null for listGraphs
245
+ * @param options - the options
246
+ * @returns what the import needs
247
+ */
248
+ async function prepare(
249
+ input: ImportInput,
250
+ sink: GraphSink | null,
251
+ options: (XgmmlImportOptions & CommonImportOptions) | undefined,
252
+ ): Promise<Prepared> {
253
+ const common = resolveImportOptions(options, FORMAT_DEFAULTS);
254
+ const report = new ImportReportBuilder(FORMAT, common.errorLimit);
255
+ if (sink !== null) {
256
+ reportSinkOptions(sink, options, report, true);
257
+ reportUnusedOptions(options, report, USED_OPTIONS);
258
+ }
259
+ resolveSettings(options, false);
260
+ const doc = await parseXgmml(input, report, common, options);
261
+ const dialect = dialectOf(doc, report);
262
+ if (dialect.view) {
263
+ report.fail(
264
+ XGMML_ISSUE.VIEW_DOCUMENT,
265
+ "this is a Cytoscape session view file (cy:view): it holds positions keyed by view ids and no topology; open the session (.cys) instead",
266
+ { line: doc.root.line },
267
+ );
268
+ }
269
+ return {
270
+ report,
271
+ common,
272
+ doc,
273
+ dialect,
274
+ settings: resolveSettings(options, doc.cytoscape),
275
+ graphs: graphsOf(doc, dialect),
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Emit one graph of a prepared document.
281
+ * @param prepared - the prepared import
282
+ * @param graph - the graph to read
283
+ * @param sink - the sink
284
+ * @param extras - what a session adds
285
+ * @returns the report
286
+ */
287
+ function emitOne(prepared: Prepared, graph: GraphRec, sink: GraphSink, extras?: EmitExtras): ImportReport {
288
+ const { doc, dialect, report, common, settings } = prepared;
289
+ const emitter = new XgmmlEmitter(doc, dialect, sink, report, common, settings, extras);
290
+ emitter.emit(dialect.session ? graph : null);
291
+ if (dialect.session) {
292
+ reportRootOnly(prepared);
293
+ }
294
+ throwIfAborted(common.signal);
295
+ return report.finish();
296
+ }
297
+
298
+ /**
299
+ * W_XGMML_ROOT_ONLY_ELEMENTS: elements of a session network document no registered subnetwork
300
+ * holds (group meta-edges, members of collapsed groups).
301
+ * @param prepared - the prepared import
302
+ */
303
+ function reportRootOnly(prepared: Prepared): void {
304
+ const held = new Set<unknown>();
305
+ for (const graph of prepared.graphs) {
306
+ const members = membersOf(prepared.doc, graph);
307
+ members.nodes.forEach((n) => held.add(n));
308
+ members.edges.forEach((e) => held.add(e));
309
+ }
310
+ const nodes = prepared.doc.nodes.filter((n) => !held.has(n)).length;
311
+ const edges = prepared.doc.edges.filter((e) => !held.has(e)).length;
312
+ if (nodes + edges > 0) {
313
+ prepared.report.warning(
314
+ "unsupported",
315
+ XGMML_ISSUE.ROOT_ONLY_ELEMENTS,
316
+ `${nodes} node(s) and ${edges} edge(s) belong to no registered network (group meta-edges, collapsed group members) and were not read`,
317
+ );
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Import one graph of an XGMML document.
323
+ * @param input - the document
324
+ * @param sink - the sink
325
+ * @param options - format-specific and common options
326
+ * @returns the report; ImportError when the document cannot be read or the error limit is exceeded
327
+ */
328
+ async function importXgmml(
329
+ input: ImportInput,
330
+ sink: GraphSink,
331
+ options?: XgmmlImportOptions & CommonImportOptions,
332
+ ): Promise<ImportReport> {
333
+ const prepared = await prepare(input, sink, options);
334
+ const index = chooseGraph(
335
+ prepared.graphs.map((g) => graphName(g, prepared.doc)),
336
+ options,
337
+ prepared.report,
338
+ );
339
+ if (prepared.graphs.length > 1) {
340
+ prepared.report.warning(
341
+ "unsupported",
342
+ XGMML_ISSUE.MULTIPLE_GRAPHS,
343
+ `the session network document holds ${prepared.graphs.length} registered networks; ${prepared.graphs.length - 1} were not read (use importAll, graphIndex or graphName)`,
344
+ );
345
+ }
346
+ return emitOne(prepared, prepared.graphs[index], sink);
347
+ }
348
+
349
+ /**
350
+ * Import every graph of an XGMML document: each registered subnetwork of a session network
351
+ * document, else the one graph.
352
+ * @param input - the document
353
+ * @param sinkFor - a sink per graph
354
+ * @param options - format-specific and common options
355
+ * @returns one report per graph
356
+ */
357
+ async function importAllXgmml(
358
+ input: ImportInput,
359
+ sinkFor: (index: number) => GraphSink,
360
+ options?: XgmmlImportOptions & CommonImportOptions,
361
+ ): Promise<ImportReport[]> {
362
+ const bytes = await reread(input);
363
+ const first = await prepare(bytes, null, options);
364
+ const reports: ImportReport[] = [];
365
+ for (let i = 0; i < first.graphs.length; i++) {
366
+ const sink = sinkFor(i);
367
+ const prepared = i === 0 ? first : await prepare(bytes, null, options);
368
+ reportSinkOptions(sink, options, prepared.report, true);
369
+ reportUnusedOptions(options, prepared.report, USED_OPTIONS);
370
+ reports.push(emitOne(prepared, prepared.graphs[i], sink));
371
+ }
372
+ return reports;
373
+ }
374
+
375
+ /**
376
+ * List the graphs of an XGMML document without importing them.
377
+ * @param input - the document
378
+ * @param options - the options
379
+ * @returns one listing per graph
380
+ */
381
+ async function listXgmmlGraphs(
382
+ input: ImportInput,
383
+ options?: XgmmlImportOptions & CommonImportOptions,
384
+ ): Promise<readonly GraphListing[]> {
385
+ const prepared = await prepare(input, null, options);
386
+ return prepared.graphs.map((graph, index) => {
387
+ const members = membersOf(prepared.doc, prepared.dialect.session ? graph : null);
388
+ return Object.freeze({
389
+ index,
390
+ name: graphName(graph, prepared.doc),
391
+ nodes: new Set(members.nodes.map((n) => n.id)).size,
392
+ edges: members.edges.length,
393
+ });
394
+ });
395
+ }
396
+
397
+ /**
398
+ * The name of a graph: its label, else the RDF title (root only), else its id.
399
+ * @param graph - the graph
400
+ * @param doc - the document
401
+ * @returns the name, or null
402
+ */
403
+ function graphName(graph: GraphRec, doc: XgmmlDocument): string | null {
404
+ return graph.label ?? (graph === doc.root ? (doc.rdf.title ?? null) : null) ?? graph.id ?? null;
405
+ }
406
+
407
+ /**
408
+ * The input in a form that can be read more than once (a stream is read into bytes).
409
+ * @param input - the input
410
+ * @returns the same input when it is a string or bytes, else the collected bytes
411
+ */
412
+ async function reread(input: ImportInput): Promise<string | Uint8Array> {
413
+ if (typeof input === "string" || input instanceof Uint8Array) {
414
+ return input;
415
+ }
416
+ const parts: (string | Uint8Array)[] = [];
417
+ const iterable: AsyncIterable<string | Uint8Array> =
418
+ typeof (input as { getReader?: unknown }).getReader === "function"
419
+ ? streamIterable(input as ReadableStream<Uint8Array>)
420
+ : (input as AsyncIterable<string | Uint8Array>);
421
+ for await (const chunk of iterable) {
422
+ parts.push(chunk);
423
+ }
424
+ if (parts.every((p) => typeof p === "string")) {
425
+ return parts.join("");
426
+ }
427
+ const bytes = parts.map((p) => (typeof p === "string" ? new TextEncoder().encode(p) : p));
428
+ const out = new Uint8Array(bytes.reduce((n, b) => n + b.byteLength, 0));
429
+ let at = 0;
430
+ for (const b of bytes) {
431
+ out.set(b, at);
432
+ at += b.byteLength;
433
+ }
434
+ return out;
435
+ }
436
+
437
+ /**
438
+ * A ReadableStream as an async iterable.
439
+ * @param stream - the stream
440
+ * @yields its chunks
441
+ * @returns nothing
442
+ */
443
+ async function* streamIterable(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array, void, undefined> {
444
+ const reader = stream.getReader();
445
+ try {
446
+ for (;;) {
447
+ const { done, value } = await reader.read();
448
+ if (done) {
449
+ return;
450
+ }
451
+ yield value;
452
+ }
453
+ } finally {
454
+ reader.releaseLock();
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Confidence that a head of bytes is XGMML: 0.95 for a root `graph` in the XGMML namespace or an
460
+ * XGMML DOCTYPE (Cytoscape's file filter tests the same two things, and a session view file still
461
+ * scores so its failure names it), 0.5 for a root local name `graph` without either.
462
+ * @param head - the first bytes
463
+ * @returns the confidence
464
+ */
465
+ function sniffXgmml(head: Uint8Array): number {
466
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(head.subarray(0, SNIFF_BYTES));
467
+ if (!/^\uFEFF?\s*</.test(text)) {
468
+ return 0;
469
+ }
470
+ if (XGMML_DOCTYPE.test(text)) {
471
+ return 0.95;
472
+ }
473
+ const root = /<(?!\?|!)([A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)[\s/>]/.exec(text.replace(/<!--[\s\S]*?-->/g, ""));
474
+ if (root === null || root[2] !== "graph") {
475
+ return 0;
476
+ }
477
+ const tagEnd = text.indexOf(">", root.index);
478
+ const tag = text.slice(root.index, tagEnd < 0 ? undefined : tagEnd);
479
+ return tag.includes("http://www.cs.rpi.edu/XGMML") ? 0.95 : 0.5;
480
+ }
481
+
482
+ /** The XGMML importer. */
483
+ export const xgmmlImporter: GraphImporter<XgmmlImportOptions> = Object.freeze({
484
+ format: FORMAT,
485
+ extensions: EXTENSIONS,
486
+ mimeTypes: MIME_TYPES,
487
+ sniff: sniffXgmml,
488
+ import: importXgmml,
489
+ importAll: importAllXgmml,
490
+ listGraphs: listXgmmlGraphs,
491
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The `@graphty/graph-io/xgmml` subpath: the XGMML importer and exporter, their option types and
3
+ * their issue and loss-note code tables.
4
+ */
5
+
6
+ export { XGMML_ISSUE, XGMML_LOSS } from "./constants.js";
7
+ export { xgmmlExporter, type XgmmlExportOptions } from "./exporter.js";
8
+ export { xgmmlImporter, type XgmmlImportOptions } from "./importer.js";
@@ -0,0 +1,183 @@
1
+ /**
2
+ * XGMML attribute types and values (`research-xgmml.md` sections 3.6, 3.8 and 4.3): which kind an
3
+ * `<att>` declares (Cytoscape's `cy:type` before the draft's `type`, case-insensitively), and the
4
+ * lexical rules of each kind as Cytoscape writes them. A value that does not parse is never
5
+ * guessed at: the caller records E_BAD_VALUE and leaves the cell unset.
6
+ */
7
+
8
+ import { type AttRec } from "./document.js";
9
+
10
+ /** The scalar kinds of a column, in widening order (string beats everything). */
11
+ export type ScalarKind = "bool" | "int" | "long" | "real" | "string";
12
+
13
+ /** What one att declares. */
14
+ type DeclaredKind = ScalarKind | "list" | "map";
15
+
16
+ /** The declared kind of an att and the type text to keep in `origin.type`. */
17
+ interface AttType {
18
+ /** The kind. */
19
+ readonly kind: DeclaredKind;
20
+ /** The type as declared (`cy:type` when given, else `type`), or null for an untyped att. */
21
+ readonly declared: string | null;
22
+ /** The type text no kind matched (kept as text, W_UNKNOWN_ATTR_TYPE), or null. */
23
+ readonly unknown: string | null;
24
+ }
25
+
26
+ /** Cytoscape's `cy:type` names (3.3+), lower-cased. */
27
+ const CY_TYPES: Readonly<Record<string, DeclaredKind>> = {
28
+ string: "string",
29
+ double: "real",
30
+ integer: "int",
31
+ long: "long",
32
+ boolean: "bool",
33
+ list: "list",
34
+ };
35
+
36
+ /** The XGMML draft's types, Cytoscape's `boolean` and the 2.x `map`, lower-cased. */
37
+ const XGMML_TYPES: Readonly<Record<string, DeclaredKind>> = {
38
+ string: "string",
39
+ real: "real",
40
+ integer: "int",
41
+ boolean: "bool",
42
+ list: "list",
43
+ map: "map",
44
+ };
45
+
46
+ /**
47
+ * The kind an att declares: `cy:type` first, then `type`, both case-insensitive; no type is a
48
+ * string; an unknown type is a string the caller reports. An untyped-by-Cytoscape `integer` att
49
+ * whose name ends in `.SUID` is a long (Cytoscape keeps SUID references as Long).
50
+ * @param att - the att
51
+ * @returns the declared kind
52
+ */
53
+ export function attType(att: AttRec): AttType {
54
+ let kind: DeclaredKind | undefined;
55
+ let declared: string | null = null;
56
+ if (att.cyType !== null && att.cyType.length > 0) {
57
+ kind = CY_TYPES[att.cyType.trim().toLowerCase()];
58
+ declared = att.cyType;
59
+ }
60
+ if (kind === undefined && att.type !== null && att.type.length > 0) {
61
+ kind = XGMML_TYPES[att.type.trim().toLowerCase()];
62
+ declared = att.type;
63
+ }
64
+ if (kind === undefined) {
65
+ const unknown = declared ?? null;
66
+ return { kind: "string", declared, unknown };
67
+ }
68
+ if (kind === "int" && att.cyType === null && att.name?.endsWith(".SUID") === true) {
69
+ // pre-3.3 Cytoscape wrote its Long SUID references as "integer"; a real one stays real
70
+ // (Cytoscape writes Double.toString forms such as "21.0", which no integer rule reads)
71
+ kind = "long";
72
+ }
73
+ return { kind, declared, unknown: null };
74
+ }
75
+
76
+ /**
77
+ * The scalar kind a list element type names (`cy:elementType`), or null when it names none.
78
+ * @param text - the element type text
79
+ * @returns the kind, or null
80
+ */
81
+ export function elementKind(text: string | null): ScalarKind | null {
82
+ if (text === null) {
83
+ return null;
84
+ }
85
+ const kind = CY_TYPES[text.trim().toLowerCase()] ?? XGMML_TYPES[text.trim().toLowerCase()];
86
+ return kind === undefined || kind === "list" || kind === "map" ? null : kind;
87
+ }
88
+
89
+ /**
90
+ * The wider of two scalar kinds (design section 5.1): int and long widen to long, any two numbers
91
+ * to real, a boolean and a number to string, anything and a string to string.
92
+ * @param a - one kind
93
+ * @param b - the other
94
+ * @returns the kind both fit
95
+ */
96
+ export function widenScalar(a: ScalarKind, b: ScalarKind): ScalarKind {
97
+ if (a === b) {
98
+ return a;
99
+ }
100
+ if (a === "string" || b === "string" || a === "bool" || b === "bool") {
101
+ return "string";
102
+ }
103
+ if (a === "real" || b === "real") {
104
+ return "real";
105
+ }
106
+ return "long";
107
+ }
108
+
109
+ const INTEGER_TEXT = /^[+-]?[0-9]+$/;
110
+
111
+ /** Java's Double.valueOf forms that Cytoscape writes: no hex, no `d` / `f` suffix. */
112
+ const REAL_TEXT = /^[+-]?(NaN|Infinity|([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?)$/;
113
+
114
+ const I32_MIN = -2147483648;
115
+ const I32_MAX = 2147483647;
116
+
117
+ /** A parsed scalar and what parsing it found. */
118
+ interface ParsedScalar {
119
+ /** The value. */
120
+ readonly value: boolean | number | string;
121
+ /** An integer beyond i32 (the column widens), or a long / real beyond what a double holds exactly. */
122
+ readonly overflow?: "i32" | "precision" | undefined;
123
+ }
124
+
125
+ /**
126
+ * Parse one value text by its declared scalar kind. Numbers and booleans are trimmed; strings
127
+ * never are. Booleans accept 1 / 0 / true / false / yes / no in any case.
128
+ * @param text - the value text
129
+ * @param kind - the declared kind
130
+ * @param unescape - decode Cytoscape's two-character `\n` and `\t` in strings
131
+ * @returns the value, or null when the text does not parse as the kind
132
+ */
133
+ export function parseScalar(text: string, kind: ScalarKind, unescape: boolean): ParsedScalar | null {
134
+ switch (kind) {
135
+ case "string":
136
+ return { value: unescape ? unescapeCytoscape(text) : text };
137
+ case "bool": {
138
+ const t = text.trim().toLowerCase();
139
+ if (t === "1" || t === "true" || t === "yes") {
140
+ return { value: true };
141
+ }
142
+ if (t === "0" || t === "false" || t === "no") {
143
+ return { value: false };
144
+ }
145
+ return null;
146
+ }
147
+ case "int":
148
+ case "long": {
149
+ const t = text.trim();
150
+ if (!INTEGER_TEXT.test(t)) {
151
+ return null;
152
+ }
153
+ const n = Number(t);
154
+ if (kind === "int" && (n < I32_MIN || n > I32_MAX)) {
155
+ return { value: n, overflow: Number.isSafeInteger(n) ? "i32" : "precision" };
156
+ }
157
+ return { value: n, overflow: Number.isSafeInteger(n) ? undefined : "precision" };
158
+ }
159
+ case "real": {
160
+ const t = text.trim();
161
+ if (!REAL_TEXT.test(t)) {
162
+ return null;
163
+ }
164
+ const n = Number(t);
165
+ // only a finite spelling that overflows (1e400) loses precision; NaN and the infinities are exact
166
+ const overflow = !Number.isFinite(n) && !/Infinity|NaN/.test(t) ? "precision" : undefined;
167
+ return { value: n, overflow };
168
+ }
169
+ default: {
170
+ const name: never = kind;
171
+ throw new Error(`unknown scalar kind ${String(name)}`);
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Decode the two-character escapes the Cytoscape writer uses for newline and tab.
178
+ * @param text - the text
179
+ * @returns the text with `\n` and `\t` decoded
180
+ */
181
+ function unescapeCytoscape(text: string): string {
182
+ return text.includes("\\") ? text.replace(/\\t/g, "\t").replace(/\\n/g, "\n") : text;
183
+ }