@graphty/graph-io 0.2.4 → 0.3.0

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 (180) hide show
  1. package/README.md +29 -7
  2. package/dist/chunks/{escape-DyI8JofU.js → escape-CKied3Ri.js} +16 -10
  3. package/dist/chunks/escape-CKied3Ri.js.map +1 -0
  4. package/dist/chunks/{importer-DbnGYr3_.js → importer-C6QcIRIb.js} +129 -39
  5. package/dist/chunks/importer-C6QcIRIb.js.map +1 -0
  6. package/dist/chunks/{importer-CpCpfbxr.js → importer-D5pAsweZ.js} +136 -31
  7. package/dist/chunks/importer-D5pAsweZ.js.map +1 -0
  8. package/dist/chunks/{importer-GozH8DkN.js → importer-DbMrAR_w.js} +10 -4
  9. package/dist/chunks/importer-DbMrAR_w.js.map +1 -0
  10. package/dist/chunks/{importer-CQnJuWJw.js → importer-DkzjTvHc.js} +82 -40
  11. package/dist/chunks/importer-DkzjTvHc.js.map +1 -0
  12. package/dist/chunks/{records-CGpxszm1.js → records-BKSMowhR.js} +3 -3
  13. package/dist/chunks/{records-CGpxszm1.js.map → records-BKSMowhR.js.map} +1 -1
  14. package/dist/chunks/{text-CajMdVFy.js → text-Dr0Ifpag.js} +2 -2
  15. package/dist/chunks/{text-CajMdVFy.js.map → text-Dr0Ifpag.js.map} +1 -1
  16. package/dist/chunks/{writer-DxSKC7TL.js → writer-BdMak4_J.js} +341 -112
  17. package/dist/chunks/writer-BdMak4_J.js.map +1 -0
  18. package/dist/csv.js +24 -6
  19. package/dist/csv.js.map +1 -1
  20. package/dist/dot.js +1 -1
  21. package/dist/gexf.js +42 -10
  22. package/dist/gexf.js.map +1 -1
  23. package/dist/gml.js +69 -25
  24. package/dist/gml.js.map +1 -1
  25. package/dist/graph-io.js +186 -129
  26. package/dist/graph-io.js.map +1 -1
  27. package/dist/graphml.js +1 -1
  28. package/dist/json.js +1 -1
  29. package/dist/neo4j.js +10 -4
  30. package/dist/neo4j.js.map +1 -1
  31. package/dist/pajek.js +1 -1
  32. package/dist/src/common/codes.d.ts +6 -0
  33. package/dist/src/common/codes.d.ts.map +1 -1
  34. package/dist/src/common/codes.js +6 -0
  35. package/dist/src/common/codes.js.map +1 -1
  36. package/dist/src/common/escape.d.ts +5 -4
  37. package/dist/src/common/escape.d.ts.map +1 -1
  38. package/dist/src/common/escape.js +6 -5
  39. package/dist/src/common/escape.js.map +1 -1
  40. package/dist/src/common/format.d.ts +3 -3
  41. package/dist/src/common/format.js +5 -5
  42. package/dist/src/common/format.js.map +1 -1
  43. package/dist/src/common/input.d.ts +24 -6
  44. package/dist/src/common/input.d.ts.map +1 -1
  45. package/dist/src/common/input.js +259 -23
  46. package/dist/src/common/input.js.map +1 -1
  47. package/dist/src/common/options.d.ts +2 -0
  48. package/dist/src/common/options.d.ts.map +1 -1
  49. package/dist/src/common/options.js +17 -0
  50. package/dist/src/common/options.js.map +1 -1
  51. package/dist/src/common/text.js +4 -4
  52. package/dist/src/common/text.js.map +1 -1
  53. package/dist/src/common/weights.d.ts.map +1 -1
  54. package/dist/src/common/weights.js +8 -0
  55. package/dist/src/common/weights.js.map +1 -1
  56. package/dist/src/common/xml.d.ts +7 -0
  57. package/dist/src/common/xml.d.ts.map +1 -1
  58. package/dist/src/common/xml.js +12 -0
  59. package/dist/src/common/xml.js.map +1 -1
  60. package/dist/src/formats/csv/exporter.d.ts +5 -1
  61. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  62. package/dist/src/formats/csv/exporter.js +8 -1
  63. package/dist/src/formats/csv/exporter.js.map +1 -1
  64. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  65. package/dist/src/formats/csv/importer.js +1 -0
  66. package/dist/src/formats/csv/importer.js.map +1 -1
  67. package/dist/src/formats/csv/index.d.ts +6 -0
  68. package/dist/src/formats/csv/index.d.ts.map +1 -1
  69. package/dist/src/formats/csv/index.js +7 -1
  70. package/dist/src/formats/csv/index.js.map +1 -1
  71. package/dist/src/formats/csv/records.js +1 -1
  72. package/dist/src/formats/csv/records.js.map +1 -1
  73. package/dist/src/formats/dot/exporter.d.ts +1 -1
  74. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  75. package/dist/src/formats/dot/exporter.js +3 -3
  76. package/dist/src/formats/dot/exporter.js.map +1 -1
  77. package/dist/src/formats/dot/importer.d.ts +6 -0
  78. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  79. package/dist/src/formats/dot/importer.js +151 -24
  80. package/dist/src/formats/dot/importer.js.map +1 -1
  81. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  82. package/dist/src/formats/gexf/exporter.js +32 -7
  83. package/dist/src/formats/gexf/exporter.js.map +1 -1
  84. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  85. package/dist/src/formats/gexf/importer.js +2 -2
  86. package/dist/src/formats/gexf/importer.js.map +1 -1
  87. package/dist/src/formats/gexf/index.d.ts +6 -0
  88. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  89. package/dist/src/formats/gexf/index.js +7 -1
  90. package/dist/src/formats/gexf/index.js.map +1 -1
  91. package/dist/src/formats/gml/importer.d.ts +2 -2
  92. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  93. package/dist/src/formats/gml/importer.js +66 -25
  94. package/dist/src/formats/gml/importer.js.map +1 -1
  95. package/dist/src/formats/gml/index.d.ts +7 -1
  96. package/dist/src/formats/gml/index.d.ts.map +1 -1
  97. package/dist/src/formats/gml/index.js +8 -2
  98. package/dist/src/formats/gml/index.js.map +1 -1
  99. package/dist/src/formats/graphml/constants.d.ts +6 -0
  100. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  101. package/dist/src/formats/graphml/constants.js +7 -1
  102. package/dist/src/formats/graphml/constants.js.map +1 -1
  103. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  104. package/dist/src/formats/graphml/importer.js +2 -2
  105. package/dist/src/formats/graphml/importer.js.map +1 -1
  106. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  107. package/dist/src/formats/json/exporter.js +14 -5
  108. package/dist/src/formats/json/exporter.js.map +1 -1
  109. package/dist/src/formats/json/importer.d.ts +8 -0
  110. package/dist/src/formats/json/importer.d.ts.map +1 -1
  111. package/dist/src/formats/json/importer.js +84 -37
  112. package/dist/src/formats/json/importer.js.map +1 -1
  113. package/dist/src/formats/neo4j/importer.js +1 -1
  114. package/dist/src/formats/neo4j/importer.js.map +1 -1
  115. package/dist/src/formats/neo4j/index.d.ts +6 -0
  116. package/dist/src/formats/neo4j/index.d.ts.map +1 -1
  117. package/dist/src/formats/neo4j/index.js +7 -1
  118. package/dist/src/formats/neo4j/index.js.map +1 -1
  119. package/dist/src/formats/pajek/exporter.d.ts +2 -0
  120. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  121. package/dist/src/formats/pajek/exporter.js +13 -2
  122. package/dist/src/formats/pajek/exporter.js.map +1 -1
  123. package/dist/src/formats/pajek/importer.d.ts +8 -2
  124. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  125. package/dist/src/formats/pajek/importer.js +122 -26
  126. package/dist/src/formats/pajek/importer.js.map +1 -1
  127. package/dist/src/index.d.ts +1 -1
  128. package/dist/src/index.d.ts.map +1 -1
  129. package/dist/src/index.js +1 -1
  130. package/dist/src/index.js.map +1 -1
  131. package/dist/src/registry.d.ts +26 -0
  132. package/dist/src/registry.d.ts.map +1 -1
  133. package/dist/src/registry.js +101 -35
  134. package/dist/src/registry.js.map +1 -1
  135. package/dist/src/sniff.d.ts +1 -1
  136. package/dist/src/sniff.d.ts.map +1 -1
  137. package/dist/src/sniff.js +6 -1
  138. package/dist/src/sniff.js.map +1 -1
  139. package/dist/src/types.d.ts +22 -3
  140. package/dist/src/types.d.ts.map +1 -1
  141. package/dist/src/types.js.map +1 -1
  142. package/package.json +4 -3
  143. package/src/common/codes.ts +9 -0
  144. package/src/common/escape.ts +6 -5
  145. package/src/common/format.ts +5 -5
  146. package/src/common/input.ts +293 -22
  147. package/src/common/options.ts +24 -0
  148. package/src/common/text.ts +4 -4
  149. package/src/common/weights.ts +9 -0
  150. package/src/common/xml.ts +14 -0
  151. package/src/formats/csv/exporter.ts +12 -1
  152. package/src/formats/csv/importer.ts +1 -0
  153. package/src/formats/csv/index.ts +9 -0
  154. package/src/formats/csv/records.ts +1 -1
  155. package/src/formats/dot/exporter.ts +3 -3
  156. package/src/formats/dot/importer.ts +172 -32
  157. package/src/formats/gexf/exporter.ts +38 -7
  158. package/src/formats/gexf/importer.ts +12 -2
  159. package/src/formats/gexf/index.ts +9 -0
  160. package/src/formats/gml/importer.ts +75 -22
  161. package/src/formats/gml/index.ts +10 -1
  162. package/src/formats/graphml/constants.ts +9 -0
  163. package/src/formats/graphml/importer.ts +9 -2
  164. package/src/formats/json/exporter.ts +14 -5
  165. package/src/formats/json/importer.ts +104 -36
  166. package/src/formats/neo4j/importer.ts +1 -1
  167. package/src/formats/neo4j/index.ts +9 -0
  168. package/src/formats/pajek/exporter.ts +19 -2
  169. package/src/formats/pajek/importer.ts +145 -28
  170. package/src/index.ts +1 -0
  171. package/src/registry.ts +131 -40
  172. package/src/sniff.ts +6 -1
  173. package/src/types.ts +26 -3
  174. package/dist/chunks/escape-DyI8JofU.js.map +0 -1
  175. package/dist/chunks/importer-CQnJuWJw.js.map +0 -1
  176. package/dist/chunks/importer-CpCpfbxr.js.map +0 -1
  177. package/dist/chunks/importer-DbnGYr3_.js.map +0 -1
  178. package/dist/chunks/importer-GozH8DkN.js.map +0 -1
  179. package/dist/chunks/writer-DxSKC7TL.js.map +0 -1
  180. package/dist/tsconfig.build.tsbuildinfo +0 -1
@@ -7,12 +7,15 @@ import {
7
7
  COLUMN_RENAMED_CODE,
8
8
  DIRECTION_FORCED_CODE,
9
9
  DIRECTION_REFUSED_CODE,
10
+ ENCODING_FALLBACK_CODE,
10
11
  ID_MERGED_CODE,
12
+ INVALID_ENCODING_CODE,
11
13
  INVALID_UTF8_CODE,
12
14
  MIXED_DIRECTION_CODE,
13
15
  OPTION_IGNORED_CODE,
14
16
  SINK_OPTION_CODE,
15
17
  SYNTAX_CODE,
18
+ UNKNOWN_ENCODING_CODE,
16
19
  } from "../../common/codes.js";
17
20
  import {
18
21
  GRAPHICS_CONFLICT_CODE,
@@ -57,10 +60,16 @@ export const GML_ISSUE = Object.freeze({
57
60
  SYNTAX: SYNTAX_CODE,
58
61
  /** The input holds invalid UTF-8 (fatal). */
59
62
  INVALID_UTF8: INVALID_UTF8_CODE,
63
+ /** Invalid bytes in the encoding a BOM, a declaration or the encoding option chose (fatal). */
64
+ INVALID_ENCODING: INVALID_ENCODING_CODE,
65
+ /** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
66
+ ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
67
+ /** A declared encoding the platform cannot decode was ignored. */
68
+ UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
60
69
  /** No `graph [` block (fatal). */
61
70
  NO_GRAPH: NO_GRAPH_CODE,
62
71
  /** More than one `graph` block (fatal). */
63
- SECOND_GRAPH: SECOND_GRAPH_CODE,
72
+ MULTIPLE_GRAPHS: SECOND_GRAPH_CODE,
64
73
  /** A node without an `id`. */
65
74
  MISSING_ID: MISSING_ID_CODE,
66
75
  /** A node without a `label` under nodeIdFrom "label". */
@@ -13,9 +13,11 @@ import {
13
13
  DUPLICATE_EDGE_ID_CODE,
14
14
  DUPLICATE_KEY_CODE,
15
15
  DUPLICATE_NODE_CODE,
16
+ ENCODING_FALLBACK_CODE,
16
17
  HYPEREDGE_CODE,
17
18
  ID_MERGED_CODE,
18
19
  ID_TEXT_TYPE_CODE,
20
+ INVALID_ENCODING_CODE,
19
21
  INVALID_UTF8_CODE,
20
22
  MISSING_ENDPOINT_CODE,
21
23
  MISSING_ID_CODE,
@@ -32,6 +34,7 @@ import {
32
34
  STRAY_TEXT_CODE,
33
35
  UNKNOWN_ATTR_TYPE_CODE,
34
36
  UNKNOWN_ELEMENT_CODE,
37
+ UNKNOWN_ENCODING_CODE,
35
38
  XML_SYNTAX_CODE,
36
39
  } from "../../common/codes.js";
37
40
 
@@ -114,6 +117,12 @@ export const GRAPHML_ISSUE = Object.freeze({
114
117
  XML_SYNTAX: XML_SYNTAX_CODE,
115
118
  /** Fatal: the input holds invalid UTF-8. */
116
119
  INVALID_UTF8: INVALID_UTF8_CODE,
120
+ /** Invalid bytes in the encoding a BOM, a declaration or the encoding option chose (fatal). */
121
+ INVALID_ENCODING: INVALID_ENCODING_CODE,
122
+ /** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
123
+ ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
124
+ /** A declared encoding the platform cannot decode was ignored. */
125
+ UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
117
126
  /** Fatal: the root element is not `<graphml>`. */
118
127
  NOT_GRAPHML: "E_NOT_GRAPHML",
119
128
  /** Fatal: the document has no `<graph>`. */
@@ -51,7 +51,14 @@ import {
51
51
  } from "../../common/options.js";
52
52
  import { ImportReportBuilder } from "../../common/report.js";
53
53
  import { isWeightField, parseWeightText } from "../../common/weights.js";
54
- import { isWhitespace, localName, tokenizeXml, type XmlHandler, XmlSyntaxError } from "../../common/xml.js";
54
+ import {
55
+ isWhitespace,
56
+ localName,
57
+ tokenizeXml,
58
+ xmlDeclaredEncoding,
59
+ type XmlHandler,
60
+ XmlSyntaxError,
61
+ } from "../../common/xml.js";
55
62
  import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
56
63
  import {
57
64
  EDGE_ID_COLUMN,
@@ -2005,7 +2012,7 @@ async function importGraphml(
2005
2012
  reportUnusedOptions(options, report, USED_OPTIONS);
2006
2013
  const reader = new GraphmlReader(sink, report, common, yfilesMode);
2007
2014
  try {
2008
- await tokenizeXml(textChunks(input, report, common), reader);
2015
+ await tokenizeXml(textChunks(input, report, { ...common, declaredEncoding: xmlDeclaredEncoding }), reader);
2009
2016
  } catch (err) {
2010
2017
  if (err instanceof XmlSyntaxError) {
2011
2018
  report.fail(GRAPHML_ISSUE.XML_SYNTAX, err.message, { line: err.line });
@@ -22,7 +22,7 @@ import { type Column, GraphFormatError, type GraphSnapshot, INVALID_INDEX, type
22
22
 
23
23
  import { type PairFolding, pairFolding } from "../../common/direction.js";
24
24
  import { checkCapabilities, countMixedEdges, LOSS } from "../../common/export.js";
25
- import { formatF32 } from "../../common/format.js";
25
+ import { formatF32, formatF64 } from "../../common/format.js";
26
26
  import { type ResolvedExportOptions, resolveExportOptions } from "../../common/options.js";
27
27
  import { explicitWeights } from "../../common/weights.js";
28
28
  import { encodeChunks, joinText } from "../../common/writer.js";
@@ -283,7 +283,8 @@ function numberText(value: number, f32: boolean, nonfinite: Counter): string {
283
283
  if (f32) {
284
284
  return formatF32(value);
285
285
  }
286
- return Object.is(value, -0) ? "0" : String(value);
286
+ // -0 is a JSON number too ("-0"); JSON.parse reads it back as -0
287
+ return formatF64(value);
287
288
  }
288
289
 
289
290
  /**
@@ -776,7 +777,7 @@ function planSlot(ctx: PlanContext, column: Column, domain: Domain): ColumnPlan
776
777
  * What the importer's inference (design section 5.1: JSON numbers are i32 when integral, else
777
778
  * f64; strings are strings; arrays and objects are json) changes about a written attribute
778
779
  * column beyond what checkCapabilities() already reported for its dtype: an f64 column whose set
779
- * values are all integers reads back as i32.
780
+ * finite values are all integers reads back as i32 (its non-finite values are written as null).
780
781
  * @param ctx - the plan context
781
782
  * @param column - the column
782
783
  * @param domain - its domain
@@ -787,11 +788,19 @@ function inferenceNotes(ctx: PlanContext, column: Column, domain: Domain, setRow
787
788
  return;
788
789
  }
789
790
  const { data } = column;
791
+ let finite = 0;
790
792
  for (let r = 0; r < column.length; r++) {
791
- if (column.isSet(r) && !Number.isInteger(data[r])) {
792
- return;
793
+ // a non-finite value is written as null and reads back unset, so it does not keep the column f64
794
+ if (column.isSet(r) && Number.isFinite(data[r])) {
795
+ if (!Number.isInteger(data[r])) {
796
+ return;
797
+ }
798
+ finite++;
793
799
  }
794
800
  }
801
+ if (finite === 0) {
802
+ return;
803
+ }
795
804
  ctx.note(
796
805
  LOSS.INTEGRAL_F64,
797
806
  `${domain} column "${column.meta.name}" is f64 with integral values only; JSON declares no types and it reads back as i32`,
@@ -40,12 +40,16 @@ import {
40
40
  DUPLICATE_EDGE_ID_CODE,
41
41
  DUPLICATE_NODE_CODE,
42
42
  EMPTY_INPUT_CODE,
43
+ ENCODING_FALLBACK_CODE,
43
44
  HYPEREDGE_CODE,
45
+ INVALID_ENCODING_CODE,
46
+ INVALID_UTF8_CODE,
44
47
  MISSING_ENDPOINT_CODE,
45
48
  MISSING_ID_CODE,
46
49
  MULTIPLE_GRAPHS_CODE,
47
50
  OPTION_IGNORED_CODE,
48
51
  SYNTAX_CODE,
52
+ UNKNOWN_ENCODING_CODE,
49
53
  UNKNOWN_PARENT_CODE,
50
54
  } from "../../common/codes.js";
51
55
  import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
@@ -157,6 +161,14 @@ export const JSON_ISSUE = Object.freeze({
157
161
  SINK_OPTION: SINK_OPTION_CODE,
158
162
  /** A common option the dialect has no use for (nodeIdFrom outside node-link, long, restoreMangledIds). */
159
163
  OPTION_IGNORED: OPTION_IGNORED_CODE,
164
+ /** The input holds invalid UTF-8 (fatal). */
165
+ INVALID_UTF8: INVALID_UTF8_CODE,
166
+ /** Invalid bytes in the encoding a BOM, a declaration or the encoding option chose (fatal). */
167
+ INVALID_ENCODING: INVALID_ENCODING_CODE,
168
+ /** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
169
+ ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
170
+ /** A declared encoding the platform cannot decode was ignored. */
171
+ UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
160
172
  });
161
173
 
162
174
  /** The common options the JSON importer reads (the rest is reported by reportUnusedOptions). */
@@ -236,6 +248,8 @@ interface ResolvedJsonOptions {
236
248
  readonly targetKey: string | null;
237
249
  readonly indexLinks: boolean | "auto";
238
250
  readonly graphIndex: number;
251
+ /** Set by importAll(): every graph of a `graphs` array is read, so none is reported as skipped. */
252
+ readonly all?: boolean;
239
253
  }
240
254
 
241
255
  /**
@@ -1741,11 +1755,11 @@ function jgfGraphOf(ctx: ImportContext, root: JsonRecord): JsonRecord {
1741
1755
  if (graphs.length === 0) {
1742
1756
  report.fail(JSON_ISSUE.SHAPE, "a JGF document needs a graph object or a non-empty graphs array");
1743
1757
  }
1744
- if (graphs.length > 1) {
1758
+ if (graphs.length > 1 && ctx.json.all !== true) {
1745
1759
  report.warning(
1746
1760
  "unsupported",
1747
1761
  JSON_ISSUE.MULTIPLE_GRAPHS,
1748
- `the document holds ${graphs.length} graphs; only graphs[${ctx.json.graphIndex}] is read`,
1762
+ `the document holds ${graphs.length} graphs; only graphs[${ctx.json.graphIndex}] is read (${graphs.length - 1} skipped), importAll() reads every one`,
1749
1763
  { element: "graphs" },
1750
1764
  );
1751
1765
  }
@@ -2232,40 +2246,94 @@ export const jsonImporter: GraphImporter<JsonImportOptions> = Object.freeze({
2232
2246
  const text = await readText(input, report, resolved);
2233
2247
  const root = parseDocument(text, report);
2234
2248
  const dialect = detectDialect(root, json.dialect, report);
2235
- const ctx = new ImportContext(sink, report, resolved, json, options?.defaultDirected !== undefined);
2236
- reportSinkOptions(sink, options, report);
2237
- reportUnusedOptions(options, report, USED_OPTIONS);
2238
- if (dialect === "cytoscape") {
2239
- importCytoscape(ctx, root);
2240
- throwIfAborted(resolved.signal);
2241
- return report.finish();
2242
- }
2243
- const doc = isJsonObject(root)
2244
- ? root
2245
- : report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
2246
- switch (dialect) {
2247
- case "node-link":
2248
- case "d3":
2249
- importNodeLink(ctx, doc, dialect);
2250
- break;
2251
- case "jgf":
2252
- importJgf(ctx, doc);
2253
- break;
2254
- case "graphology":
2255
- importGraphology(ctx, doc);
2256
- break;
2257
- case "vis":
2258
- importVis(ctx, doc);
2259
- break;
2260
- default: {
2261
- const name: string = dialect;
2262
- throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, {
2263
- option: "dialect",
2264
- found: name,
2265
- });
2266
- }
2267
- }
2268
- throwIfAborted(resolved.signal);
2249
+ readGraph(root, dialect, sink, report, resolved, json, options);
2269
2250
  return report.finish();
2270
2251
  },
2252
+
2253
+ /**
2254
+ * Read every graph of a JSON document: each entry of a JGF `graphs` array into its own sink;
2255
+ * any other document holds one graph.
2256
+ * @param input - the text, bytes or stream
2257
+ * @param sinkFor - the sink of the graph with this index, called before its first push
2258
+ * @param options - format-specific and common options
2259
+ * @returns one report per graph
2260
+ */
2261
+ async importAll(
2262
+ input: ImportInput,
2263
+ sinkFor: (index: number) => GraphSink,
2264
+ options?: JsonImportOptions & CommonImportOptions,
2265
+ ): Promise<ImportReport[]> {
2266
+ const resolved = resolveImportOptions(options, FORMAT_DEFAULTS);
2267
+ const json = resolveJsonOptions(options);
2268
+ const first = new ImportReportBuilder("json", resolved.errorLimit);
2269
+ const text = await readText(input, first, resolved);
2270
+ const root = parseDocument(text, first);
2271
+ const dialect = detectDialect(root, json.dialect, first);
2272
+ const graphs =
2273
+ dialect === "jgf" && isJsonObject(root) && !isJsonObject(root.graph) && Array.isArray(root.graphs)
2274
+ ? root.graphs.length
2275
+ : 1;
2276
+ const reports: ImportReport[] = [];
2277
+ for (let i = 0; i < Math.max(graphs, 1); i++) {
2278
+ const report = i === 0 ? first : new ImportReportBuilder("json", resolved.errorLimit);
2279
+ readGraph(root, dialect, sinkFor(i), report, resolved, { ...json, graphIndex: i, all: true }, options);
2280
+ reports.push(report.finish());
2281
+ }
2282
+ return reports;
2283
+ },
2271
2284
  });
2285
+
2286
+ /**
2287
+ * Read one graph of a parsed document into a sink.
2288
+ * @param root - the parsed document
2289
+ * @param dialect - its dialect
2290
+ * @param sink - the sink
2291
+ * @param report - the graph's report
2292
+ * @param resolved - the resolved common options
2293
+ * @param json - the resolved JSON options (graphIndex picks the JGF graph)
2294
+ * @param options - the caller's options, for the sink and unused-option checks
2295
+ */
2296
+ function readGraph(
2297
+ root: unknown,
2298
+ dialect: JsonDialect,
2299
+ sink: GraphSink,
2300
+ report: ImportReportBuilder,
2301
+ resolved: ResolvedImportOptions,
2302
+ json: ResolvedJsonOptions,
2303
+ options: (JsonImportOptions & CommonImportOptions) | undefined,
2304
+ ): void {
2305
+ const ctx = new ImportContext(sink, report, resolved, json, options?.defaultDirected !== undefined);
2306
+ reportSinkOptions(sink, options, report);
2307
+ reportUnusedOptions(options, report, USED_OPTIONS);
2308
+ if (dialect === "cytoscape") {
2309
+ importCytoscape(ctx, root);
2310
+ throwIfAborted(resolved.signal);
2311
+ return;
2312
+ }
2313
+ const doc = isJsonObject(root)
2314
+ ? root
2315
+ : report.fail(JSON_ISSUE.SHAPE, `a ${dialect} document must be a JSON object, found ${describe(root)}`);
2316
+ switch (dialect) {
2317
+ case "node-link":
2318
+ case "d3":
2319
+ importNodeLink(ctx, doc, dialect);
2320
+ break;
2321
+ case "jgf":
2322
+ importJgf(ctx, doc);
2323
+ break;
2324
+ case "graphology":
2325
+ importGraphology(ctx, doc);
2326
+ break;
2327
+ case "vis":
2328
+ importVis(ctx, doc);
2329
+ break;
2330
+ default: {
2331
+ const name: string = dialect;
2332
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown dialect ${name}`, {
2333
+ option: "dialect",
2334
+ found: name,
2335
+ });
2336
+ }
2337
+ }
2338
+ throwIfAborted(resolved.signal);
2339
+ }
@@ -467,7 +467,7 @@ class Neo4jImportSession {
467
467
  async run(inputs: readonly ImportInput[]): Promise<void> {
468
468
  const progress = new ProgressTracker(this.common.onProgress, inputs);
469
469
  for (const input of inputs) {
470
- await this.readInput(input, progress.optionsFor(this.common.signal));
470
+ await this.readInput(input, { ...progress.optionsFor(this.common.signal), encoding: this.common.encoding });
471
471
  progress.finishInput();
472
472
  }
473
473
  if (this.ignoredColumns > 0) {
@@ -6,6 +6,8 @@
6
6
 
7
7
  import {
8
8
  COLUMN_RENAMED_CODE,
9
+ ENCODING_FALLBACK_CODE,
10
+ INVALID_ENCODING_CODE,
9
11
  INVALID_UTF8_CODE,
10
12
  MUTUAL_EXPANDED_CODE,
11
13
  OPTION_IGNORED_CODE,
@@ -13,6 +15,7 @@ import {
13
15
  ROLE_DROPPED_CODE,
14
16
  SINK_OPTION_CODE,
15
17
  UNKNOWN_ATTR_TYPE_CODE,
18
+ UNKNOWN_ENCODING_CODE,
16
19
  WEIGHT_KEY_CLASH_CODE,
17
20
  } from "../../common/codes.js";
18
21
  import { BAD_QUOTE_CODE, UNCLOSED_QUOTE_CODE } from "../csv/records.js";
@@ -50,6 +53,12 @@ export { ID_SPACE_COLUMN, LABELS_COLUMN, neo4jImporter, type Neo4jImportOptions,
50
53
  export const NEO4J_ISSUE = Object.freeze({
51
54
  /** The input holds invalid UTF-8 (fatal). */
52
55
  INVALID_UTF8: INVALID_UTF8_CODE,
56
+ /** Invalid bytes in the encoding a BOM, a declaration or the encoding option chose (fatal). */
57
+ INVALID_ENCODING: INVALID_ENCODING_CODE,
58
+ /** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
59
+ ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
60
+ /** A declared encoding the platform cannot decode was ignored. */
61
+ UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
53
62
  /** An unterminated quoted field (fatal). */
54
63
  CSV_UNCLOSED_QUOTE: UNCLOSED_QUOTE_CODE,
55
64
  /** Text after a closing quote (fatal). */
@@ -20,6 +20,7 @@ import { capabilities, checkCapabilities, LOSS, type SanitizedIds, sanitizeIds }
20
20
  import { formatDecimal, formatF32, formatF64, formatInteger } from "../../common/format.js";
21
21
  import { canonicalId } from "../../common/ids.js";
22
22
  import { resolveExportOptions } from "../../common/options.js";
23
+ import { inferTextDtype } from "../../common/text.js";
23
24
  import { explicitWeights } from "../../common/weights.js";
24
25
  import { encodeChunks, joinText } from "../../common/writer.js";
25
26
  import { type CommonExportOptions, type ExportCapabilities, type GraphExporter, type LossNote } from "../../types.js";
@@ -74,6 +75,8 @@ export const PAJEK_LOSS = Object.freeze({
74
75
  ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
75
76
  /** Under sanitizeIds "mangle": an original id whose text reads back as the other type under ids "canonical". */
76
77
  ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
78
+ /** Parameter text that looks like a number or a boolean reads back as one (parameters are untyped, design 5.1). */
79
+ TEXT_INFERRED: LOSS.TEXT_INFERRED,
77
80
  });
78
81
 
79
82
  /** The roles Pajek has a slot for beyond the structural, position and temporal ones. */
@@ -400,7 +403,7 @@ function cellText(column: Column, row: number): string {
400
403
 
401
404
  /**
402
405
  * Count the cells of a plan that cannot be written (text with a quote or line break, non-finite
403
- * f64) and add the notes.
406
+ * f64) or read back typed (parameter text that looks like a number or a boolean) and add the notes.
404
407
  * @param planned - the plan
405
408
  * @param domain - node or edge
406
409
  * @param notes - the note list
@@ -413,6 +416,7 @@ function checkCells(planned: readonly PlannedColumn[], domain: "node" | "edge",
413
416
  const { name } = column.meta;
414
417
  let badText = 0;
415
418
  let nonFinite = 0;
419
+ let typed = 0;
416
420
  for (let r = 0; r < column.length; r++) {
417
421
  if (!column.isSet(r)) {
418
422
  continue;
@@ -427,8 +431,11 @@ function checkCells(planned: readonly PlannedColumn[], domain: "node" | "edge",
427
431
  nonFinite++;
428
432
  }
429
433
  } else if (column.dtype === "string" || column.dtype === "dict") {
430
- if (!isPajekLabel(column.value(r) as string)) {
434
+ const text = column.value(r) as string;
435
+ if (!isPajekLabel(text)) {
431
436
  badText++;
437
+ } else if (slot === "param" && inferTextDtype(text) !== "string") {
438
+ typed++;
432
439
  }
433
440
  }
434
441
  }
@@ -442,6 +449,16 @@ function checkCells(planned: readonly PlannedColumn[], domain: "node" | "edge",
442
449
  ),
443
450
  );
444
451
  }
452
+ if (typed > 0) {
453
+ notes.push(
454
+ note(
455
+ PAJEK_LOSS.TEXT_INFERRED,
456
+ `${typed} value(s) of ${domain} column "${name}" look like numbers or booleans; Pajek parameters are untyped and they read back as such`,
457
+ name,
458
+ typed,
459
+ ),
460
+ );
461
+ }
445
462
  if (nonFinite > 0) {
446
463
  notes.push(
447
464
  note(