@graphty/graph-io 0.0.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -28
- package/dist/chunks/children-CL3Cy0ez.js +238 -0
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
- package/dist/chunks/escape-DyI8JofU.js +938 -0
- package/dist/chunks/escape-DyI8JofU.js.map +1 -0
- package/dist/chunks/importer-CQnJuWJw.js +2987 -0
- package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
- package/dist/chunks/importer-CpCpfbxr.js +2015 -0
- package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
- package/dist/chunks/importer-DbnGYr3_.js +2342 -0
- package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
- package/dist/chunks/importer-GozH8DkN.js +3050 -0
- package/dist/chunks/importer-GozH8DkN.js.map +1 -0
- package/dist/chunks/records-CGpxszm1.js +605 -0
- package/dist/chunks/records-CGpxszm1.js.map +1 -0
- package/dist/chunks/text-CajMdVFy.js +189 -0
- package/dist/chunks/text-CajMdVFy.js.map +1 -0
- package/dist/chunks/writer-DxSKC7TL.js +2842 -0
- package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1702 -0
- package/dist/csv.js.map +1 -0
- package/dist/dot.d.ts +1 -0
- package/dist/dot.js +8 -0
- package/dist/dot.js.map +1 -0
- package/dist/gexf.d.ts +1 -0
- package/dist/gexf.js +3466 -0
- package/dist/gexf.js.map +1 -0
- package/dist/gml.d.ts +1 -0
- package/dist/gml.js +2647 -0
- package/dist/gml.js.map +1 -0
- package/dist/graph-io.d.ts +1 -0
- package/dist/graph-io.js +790 -0
- package/dist/graph-io.js.map +1 -0
- package/dist/graphml.d.ts +1 -0
- package/dist/graphml.js +8 -0
- package/dist/graphml.js.map +1 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +11 -0
- package/dist/json.js.map +1 -0
- package/dist/neo4j.d.ts +1 -0
- package/dist/neo4j.js +2046 -0
- package/dist/neo4j.js.map +1 -0
- package/dist/pajek.d.ts +1 -0
- package/dist/pajek.js +8 -0
- package/dist/pajek.js.map +1 -0
- package/dist/src/children.d.ts +134 -0
- package/dist/src/children.d.ts.map +1 -0
- package/dist/src/children.js +274 -0
- package/dist/src/children.js.map +1 -0
- package/dist/src/common/attributes.d.ts +229 -0
- package/dist/src/common/attributes.d.ts.map +1 -0
- package/dist/src/common/attributes.js +368 -0
- package/dist/src/common/attributes.js.map +1 -0
- package/dist/src/common/codes.d.ts +105 -0
- package/dist/src/common/codes.d.ts.map +1 -0
- package/dist/src/common/codes.js +107 -0
- package/dist/src/common/codes.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +84 -0
- package/dist/src/common/declared-types.d.ts.map +1 -0
- package/dist/src/common/declared-types.js +326 -0
- package/dist/src/common/declared-types.js.map +1 -0
- package/dist/src/common/direction.d.ts +206 -0
- package/dist/src/common/direction.d.ts.map +1 -0
- package/dist/src/common/direction.js +370 -0
- package/dist/src/common/direction.js.map +1 -0
- package/dist/src/common/escape.d.ts +92 -0
- package/dist/src/common/escape.d.ts.map +1 -0
- package/dist/src/common/escape.js +212 -0
- package/dist/src/common/escape.js.map +1 -0
- package/dist/src/common/export.d.ts +249 -0
- package/dist/src/common/export.d.ts.map +1 -0
- package/dist/src/common/export.js +594 -0
- package/dist/src/common/export.js.map +1 -0
- package/dist/src/common/format.d.ts +59 -0
- package/dist/src/common/format.d.ts.map +1 -0
- package/dist/src/common/format.js +106 -0
- package/dist/src/common/format.js.map +1 -0
- package/dist/src/common/ids.d.ts +83 -0
- package/dist/src/common/ids.d.ts.map +1 -0
- package/dist/src/common/ids.js +158 -0
- package/dist/src/common/ids.js.map +1 -0
- package/dist/src/common/input.d.ts +100 -0
- package/dist/src/common/input.d.ts.map +1 -0
- package/dist/src/common/input.js +335 -0
- package/dist/src/common/input.js.map +1 -0
- package/dist/src/common/lists.d.ts +34 -0
- package/dist/src/common/lists.d.ts.map +1 -0
- package/dist/src/common/lists.js +185 -0
- package/dist/src/common/lists.js.map +1 -0
- package/dist/src/common/options.d.ts +108 -0
- package/dist/src/common/options.d.ts.map +1 -0
- package/dist/src/common/options.js +265 -0
- package/dist/src/common/options.js.map +1 -0
- package/dist/src/common/report.d.ts +187 -0
- package/dist/src/common/report.d.ts.map +1 -0
- package/dist/src/common/report.js +274 -0
- package/dist/src/common/report.js.map +1 -0
- package/dist/src/common/temporal.d.ts +71 -0
- package/dist/src/common/temporal.d.ts.map +1 -0
- package/dist/src/common/temporal.js +266 -0
- package/dist/src/common/temporal.js.map +1 -0
- package/dist/src/common/text.d.ts +104 -0
- package/dist/src/common/text.d.ts.map +1 -0
- package/dist/src/common/text.js +255 -0
- package/dist/src/common/text.js.map +1 -0
- package/dist/src/common/weights.d.ts +77 -0
- package/dist/src/common/weights.d.ts.map +1 -0
- package/dist/src/common/weights.js +156 -0
- package/dist/src/common/weights.js.map +1 -0
- package/dist/src/common/writer.d.ts +51 -0
- package/dist/src/common/writer.d.ts.map +1 -0
- package/dist/src/common/writer.js +108 -0
- package/dist/src/common/writer.js.map +1 -0
- package/dist/src/common/xml.d.ts +245 -0
- package/dist/src/common/xml.d.ts.map +1 -0
- package/dist/src/common/xml.js +942 -0
- package/dist/src/common/xml.js.map +1 -0
- package/dist/src/formats/csv/exporter.d.ts +70 -0
- package/dist/src/formats/csv/exporter.d.ts.map +1 -0
- package/dist/src/formats/csv/exporter.js +682 -0
- package/dist/src/formats/csv/exporter.js.map +1 -0
- package/dist/src/formats/csv/header.d.ts +66 -0
- package/dist/src/formats/csv/header.d.ts.map +1 -0
- package/dist/src/formats/csv/header.js +152 -0
- package/dist/src/formats/csv/header.js.map +1 -0
- package/dist/src/formats/csv/importer.d.ts +82 -0
- package/dist/src/formats/csv/importer.d.ts.map +1 -0
- package/dist/src/formats/csv/importer.js +849 -0
- package/dist/src/formats/csv/importer.js.map +1 -0
- package/dist/src/formats/csv/index.d.ts +60 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -0
- package/dist/src/formats/csv/index.js +63 -0
- package/dist/src/formats/csv/index.js.map +1 -0
- package/dist/src/formats/csv/records.d.ts +188 -0
- package/dist/src/formats/csv/records.d.ts.map +1 -0
- package/dist/src/formats/csv/records.js +702 -0
- package/dist/src/formats/csv/records.js.map +1 -0
- package/dist/src/formats/csv/values.d.ts +105 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -0
- package/dist/src/formats/csv/values.js +192 -0
- package/dist/src/formats/csv/values.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +52 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -0
- package/dist/src/formats/dot/exporter.js +836 -0
- package/dist/src/formats/dot/exporter.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts +102 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -0
- package/dist/src/formats/dot/importer.js +1291 -0
- package/dist/src/formats/dot/importer.js.map +1 -0
- package/dist/src/formats/dot/index.d.ts +7 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -0
- package/dist/src/formats/dot/index.js +7 -0
- package/dist/src/formats/dot/index.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +29 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -0
- package/dist/src/formats/dot/names.js +28 -0
- package/dist/src/formats/dot/names.js.map +1 -0
- package/dist/src/formats/dot/tokenizer.d.ts +114 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
- package/dist/src/formats/dot/tokenizer.js +341 -0
- package/dist/src/formats/dot/tokenizer.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts +56 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
- package/dist/src/formats/gexf/exporter.js +1395 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +73 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -0
- package/dist/src/formats/gexf/importer.js +1880 -0
- package/dist/src/formats/gexf/importer.js.map +1 -0
- package/dist/src/formats/gexf/index.d.ts +96 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -0
- package/dist/src/formats/gexf/index.js +97 -0
- package/dist/src/formats/gexf/index.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts +135 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -0
- package/dist/src/formats/gexf/schema.js +323 -0
- package/dist/src/formats/gexf/schema.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +69 -0
- package/dist/src/formats/gml/exporter.d.ts.map +1 -0
- package/dist/src/formats/gml/exporter.js +1093 -0
- package/dist/src/formats/gml/exporter.js.map +1 -0
- package/dist/src/formats/gml/importer.d.ts +66 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -0
- package/dist/src/formats/gml/importer.js +1331 -0
- package/dist/src/formats/gml/importer.js.map +1 -0
- package/dist/src/formats/gml/index.d.ts +85 -0
- package/dist/src/formats/gml/index.d.ts.map +1 -0
- package/dist/src/formats/gml/index.js +88 -0
- package/dist/src/formats/gml/index.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +186 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -0
- package/dist/src/formats/gml/syntax.js +467 -0
- package/dist/src/formats/gml/syntax.js.map +1 -0
- package/dist/src/formats/graphml/constants.d.ts +169 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -0
- package/dist/src/formats/graphml/constants.js +165 -0
- package/dist/src/formats/graphml/constants.js.map +1 -0
- package/dist/src/formats/graphml/exporter.d.ts +34 -0
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
- package/dist/src/formats/graphml/exporter.js +1176 -0
- package/dist/src/formats/graphml/exporter.js.map +1 -0
- package/dist/src/formats/graphml/importer.d.ts +31 -0
- package/dist/src/formats/graphml/importer.d.ts.map +1 -0
- package/dist/src/formats/graphml/importer.js +1607 -0
- package/dist/src/formats/graphml/importer.js.map +1 -0
- package/dist/src/formats/graphml/index.d.ts +8 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -0
- package/dist/src/formats/graphml/index.js +8 -0
- package/dist/src/formats/graphml/index.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts +72 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -0
- package/dist/src/formats/graphml/tree.js +290 -0
- package/dist/src/formats/graphml/tree.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +125 -0
- package/dist/src/formats/json/dialect.d.ts.map +1 -0
- package/dist/src/formats/json/dialect.js +262 -0
- package/dist/src/formats/json/dialect.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts +89 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -0
- package/dist/src/formats/json/exporter.js +1358 -0
- package/dist/src/formats/json/exporter.js.map +1 -0
- package/dist/src/formats/json/importer.d.ts +108 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -0
- package/dist/src/formats/json/importer.js +1838 -0
- package/dist/src/formats/json/importer.js.map +1 -0
- package/dist/src/formats/json/index.d.ts +8 -0
- package/dist/src/formats/json/index.d.ts.map +1 -0
- package/dist/src/formats/json/index.js +8 -0
- package/dist/src/formats/json/index.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts +68 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
- package/dist/src/formats/neo4j/exporter.js +1055 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -0
- package/dist/src/formats/neo4j/header.d.ts +52 -0
- package/dist/src/formats/neo4j/header.d.ts.map +1 -0
- package/dist/src/formats/neo4j/header.js +131 -0
- package/dist/src/formats/neo4j/header.js.map +1 -0
- package/dist/src/formats/neo4j/importer.d.ts +73 -0
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
- package/dist/src/formats/neo4j/importer.js +932 -0
- package/dist/src/formats/neo4j/importer.js.map +1 -0
- package/dist/src/formats/neo4j/index.d.ts +79 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -0
- package/dist/src/formats/neo4j/index.js +83 -0
- package/dist/src/formats/neo4j/index.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts +58 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
- package/dist/src/formats/pajek/exporter.js +825 -0
- package/dist/src/formats/pajek/exporter.js.map +1 -0
- package/dist/src/formats/pajek/importer.d.ts +88 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -0
- package/dist/src/formats/pajek/importer.js +1047 -0
- package/dist/src/formats/pajek/importer.js.map +1 -0
- package/dist/src/formats/pajek/index.d.ts +7 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -0
- package/dist/src/formats/pajek/index.js +7 -0
- package/dist/src/formats/pajek/index.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +112 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
- package/dist/src/formats/pajek/syntax.js +269 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +207 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +481 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/sniff.d.ts +104 -0
- package/dist/src/sniff.d.ts.map +1 -0
- package/dist/src/sniff.js +357 -0
- package/dist/src/sniff.js.map +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +29 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +122 -7
- package/src/children.ts +335 -0
- package/src/common/attributes.ts +520 -0
- package/src/common/codes.ts +153 -0
- package/src/common/declared-types.ts +374 -0
- package/src/common/direction.ts +518 -0
- package/src/common/escape.ts +231 -0
- package/src/common/export.ts +817 -0
- package/src/common/format.ts +111 -0
- package/src/common/ids.ts +176 -0
- package/src/common/input.ts +378 -0
- package/src/common/lists.ts +196 -0
- package/src/common/options.ts +377 -0
- package/src/common/report.ts +352 -0
- package/src/common/temporal.ts +302 -0
- package/src/common/text.ts +294 -0
- package/src/common/weights.ts +202 -0
- package/src/common/writer.ts +123 -0
- package/src/common/xml.ts +1053 -0
- package/src/formats/csv/exporter.ts +894 -0
- package/src/formats/csv/header.ts +172 -0
- package/src/formats/csv/importer.ts +1104 -0
- package/src/formats/csv/index.ts +88 -0
- package/src/formats/csv/records.ts +813 -0
- package/src/formats/csv/values.ts +224 -0
- package/src/formats/dot/exporter.ts +1014 -0
- package/src/formats/dot/importer.ts +1549 -0
- package/src/formats/dot/index.ts +7 -0
- package/src/formats/dot/names.ts +40 -0
- package/src/formats/dot/tokenizer.ts +384 -0
- package/src/formats/gexf/exporter.ts +1696 -0
- package/src/formats/gexf/importer.ts +2333 -0
- package/src/formats/gexf/index.ts +142 -0
- package/src/formats/gexf/schema.ts +361 -0
- package/src/formats/gml/exporter.ts +1404 -0
- package/src/formats/gml/importer.ts +1591 -0
- package/src/formats/gml/index.ts +128 -0
- package/src/formats/gml/syntax.ts +545 -0
- package/src/formats/graphml/constants.ts +225 -0
- package/src/formats/graphml/exporter.ts +1458 -0
- package/src/formats/graphml/importer.ts +2027 -0
- package/src/formats/graphml/index.ts +8 -0
- package/src/formats/graphml/tree.ts +318 -0
- package/src/formats/json/dialect.ts +317 -0
- package/src/formats/json/exporter.ts +1616 -0
- package/src/formats/json/importer.ts +2271 -0
- package/src/formats/json/index.ts +8 -0
- package/src/formats/neo4j/exporter.ts +1287 -0
- package/src/formats/neo4j/header.ts +156 -0
- package/src/formats/neo4j/importer.ts +1220 -0
- package/src/formats/neo4j/index.ts +116 -0
- package/src/formats/pajek/exporter.ts +1000 -0
- package/src/formats/pajek/importer.ts +1311 -0
- package/src/formats/pajek/index.ts +7 -0
- package/src/formats/pajek/syntax.ts +307 -0
- package/src/index.ts +244 -0
- package/src/registry.ts +617 -0
- package/src/sniff.ts +397 -0
- package/src/types.ts +262 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number formatting for exporters (design section 3.7): an f32 weight is written as the shortest
|
|
3
|
+
* decimal that round-trips through Math.fround (never `String(x)`, which prints
|
|
4
|
+
* 0.10000000149011612 for a stored 0.1); an f64 value as its shortest JS text; a GML real always
|
|
5
|
+
* with a decimal point so the dtype survives (design section 8.5); non-finite values as the
|
|
6
|
+
* spelling the target syntax accepts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The shortest decimal text that reads back to the same f32 value through Math.fround.
|
|
11
|
+
* @param value - an f32 value (a JS number holding one)
|
|
12
|
+
* @returns the text; "Infinity" / "-Infinity" / "NaN" for the non-finite values, "0" for both zeros
|
|
13
|
+
*/
|
|
14
|
+
export function formatF32(value: number): string {
|
|
15
|
+
if (!Number.isFinite(value)) {
|
|
16
|
+
return String(value);
|
|
17
|
+
}
|
|
18
|
+
if (value === 0) {
|
|
19
|
+
return "0";
|
|
20
|
+
}
|
|
21
|
+
for (let digits = 1; digits <= 9; digits++) {
|
|
22
|
+
const text = value.toPrecision(digits);
|
|
23
|
+
if (Math.fround(Number(text)) === value) {
|
|
24
|
+
return String(Number(text));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return String(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The shortest text of an f64 value: `String(x)`, which is already the shortest round-tripping
|
|
32
|
+
* decimal in JS.
|
|
33
|
+
* @param value - the value
|
|
34
|
+
* @returns the text; "Infinity" / "-Infinity" / "NaN" for the non-finite values
|
|
35
|
+
*/
|
|
36
|
+
export function formatF64(value: number): string {
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A decimal text with a decimal point or an exponent guaranteed for a finite value (`2.0`,
|
|
42
|
+
* `1e+21`, `1.5e-7`), so an
|
|
43
|
+
* untyped re-import keeps the column f64 rather than i32 (design section 8.5); negative zero is
|
|
44
|
+
* written `-0.0` so it reads back as -0; non-finite values are the JS spellings (`Infinity`,
|
|
45
|
+
* `-Infinity`, `NaN`), which the CSV / DOT / Pajek importers read back as text. The one
|
|
46
|
+
* implementation of the "decimal point guaranteed" rule for every text format; GML has its own
|
|
47
|
+
* spellings of the non-finite values in formatGmlReal().
|
|
48
|
+
* @param value - the value
|
|
49
|
+
* @param dtype - the column dtype the value comes from; f32 values use the shortest fround-round-trip text
|
|
50
|
+
* @returns the text
|
|
51
|
+
*/
|
|
52
|
+
export function formatDecimal(value: number, dtype: "f32" | "f64" | "i32" | "u32" | "u8" = "f64"): string {
|
|
53
|
+
if (!Number.isFinite(value)) {
|
|
54
|
+
return String(value);
|
|
55
|
+
}
|
|
56
|
+
if (Object.is(value, -0)) {
|
|
57
|
+
return "-0.0";
|
|
58
|
+
}
|
|
59
|
+
const text = formatNumber(value, dtype);
|
|
60
|
+
// an exponent form (`1e+21`, `1.5e-7`) is already f64 text under the 5.1 grammar
|
|
61
|
+
return text.includes(".") || text.includes("e") ? text : `${text}.0`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A GML real: the shortest text with a decimal point guaranteed (`2.0`, `1.0e-7`), so a re-import
|
|
66
|
+
* keeps the column real rather than int. Non-finite values have no GML spelling and are written
|
|
67
|
+
* as the texts NetworkX's writer emits and its reader accepts, `+INF`, `-INF` and `NAN` (the
|
|
68
|
+
* lowercase `inf` / `nan` would lex as keys there).
|
|
69
|
+
* @param value - the value
|
|
70
|
+
* @param dtype - the column dtype the value comes from; f32 values use the shortest fround-round-trip text
|
|
71
|
+
* @returns the text
|
|
72
|
+
*/
|
|
73
|
+
export function formatGmlReal(value: number, dtype: "f32" | "f64" | "i32" | "u32" | "u8" = "f64"): string {
|
|
74
|
+
if (Number.isNaN(value)) {
|
|
75
|
+
return "NAN";
|
|
76
|
+
}
|
|
77
|
+
if (!Number.isFinite(value)) {
|
|
78
|
+
return value > 0 ? "+INF" : "-INF";
|
|
79
|
+
}
|
|
80
|
+
const text = formatDecimal(value, dtype);
|
|
81
|
+
if (text.includes(".")) {
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
// NetworkX's real pattern requires a decimal point even in the exponent form (`1.0e-7`)
|
|
85
|
+
const e = text.indexOf("e");
|
|
86
|
+
return `${text.slice(0, e)}.0${text.slice(e)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* An integer text for a value known to be integral (an i32 / u32 / u8 cell, an f64 that holds an
|
|
91
|
+
* integer), avoiding the exponent form `String()` uses above 1e21.
|
|
92
|
+
* @param value - an integral value
|
|
93
|
+
* @returns the digits
|
|
94
|
+
*/
|
|
95
|
+
export function formatInteger(value: number): string {
|
|
96
|
+
if (Number.isSafeInteger(value) || !Number.isFinite(value)) {
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
return BigInt(value).toString();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The text of a numeric cell of any numeric dtype, dispatching on the dtype: f32 through
|
|
104
|
+
* formatF32, everything else through formatF64.
|
|
105
|
+
* @param value - the value
|
|
106
|
+
* @param dtype - the column dtype the value came from
|
|
107
|
+
* @returns the text
|
|
108
|
+
*/
|
|
109
|
+
export function formatNumber(value: number, dtype: "f32" | "f64" | "i32" | "u32" | "u8"): string {
|
|
110
|
+
return dtype === "f32" ? formatF32(value) : formatF64(value);
|
|
111
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Id coercion (design section 4.1): the importer-side rule that turns a text cell or a JSON value
|
|
3
|
+
* into a NodeId before it reaches the sink. The core never coerces.
|
|
4
|
+
*
|
|
5
|
+
* - "keep": typed values pass through; anything that is not a string or a number is E_INVALID_ID.
|
|
6
|
+
* - "canonical": text becomes a number iff it is canonical integer text (`/^-?(0|[1-9][0-9]*)$/`
|
|
7
|
+
* and a safe integer, excluding "-0" which would collide with "0"); every other text stays a
|
|
8
|
+
* string. Injective on text, so `String(id)` on export reproduces the cell exactly: "01", "1.0"
|
|
9
|
+
* and "+1" stay strings, "1" becomes 1. Typed values pass through as under "keep".
|
|
10
|
+
* - "string": `String(v)` for strings, numbers, booleans, bigints and null; anything else is
|
|
11
|
+
* E_INVALID_ID.
|
|
12
|
+
* - "number": `Number(text)` for text (empty or whitespace-only text, NaN and non-finite results
|
|
13
|
+
* are E_INVALID_ID); numbers pass through; anything else is E_INVALID_ID. This rule can merge
|
|
14
|
+
* distinct cells ("01" and "1"); IdCoercer counts such merges so the importer can report them as
|
|
15
|
+
* coercion issues.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { GraphFormatError, type IdCoercion, type NodeId } from "@graphty/graph-format";
|
|
19
|
+
|
|
20
|
+
import { ID_MERGED_CODE } from "./codes.js";
|
|
21
|
+
|
|
22
|
+
const CANONICAL_INTEGER = /^-?(0|[1-9][0-9]*)$/;
|
|
23
|
+
|
|
24
|
+
export { ID_MERGED_CODE };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The "canonical" rule on one text cell.
|
|
28
|
+
* @param text - the cell text, exactly as read
|
|
29
|
+
* @returns the number for canonical safe-integer text (never for "-0"), the text itself otherwise
|
|
30
|
+
*/
|
|
31
|
+
export function canonicalId(text: string): NodeId {
|
|
32
|
+
if (text !== "-0" && CANONICAL_INTEGER.test(text)) {
|
|
33
|
+
const n = Number(text);
|
|
34
|
+
if (Number.isSafeInteger(n)) {
|
|
35
|
+
return n;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether a text cell is canonical integer text under the "canonical" rule (a number after import).
|
|
43
|
+
* @param text - the cell text
|
|
44
|
+
* @returns true when canonicalId(text) returns a number
|
|
45
|
+
*/
|
|
46
|
+
export function isCanonicalIntegerText(text: string): boolean {
|
|
47
|
+
return typeof canonicalId(text) === "number";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Coerce a text cell under a rule.
|
|
52
|
+
* @param text - the cell text, exactly as read
|
|
53
|
+
* @param mode - the coercion rule
|
|
54
|
+
* @returns the id; E_INVALID_ID when the rule rejects the text
|
|
55
|
+
*/
|
|
56
|
+
export function coerceIdText(text: string, mode: IdCoercion): NodeId {
|
|
57
|
+
switch (mode) {
|
|
58
|
+
case "keep":
|
|
59
|
+
case "string":
|
|
60
|
+
return text;
|
|
61
|
+
case "canonical":
|
|
62
|
+
return canonicalId(text);
|
|
63
|
+
case "number": {
|
|
64
|
+
if (text.trim().length === 0) {
|
|
65
|
+
throw invalidId(text, "empty text is not a number");
|
|
66
|
+
}
|
|
67
|
+
const n = Number(text);
|
|
68
|
+
if (!Number.isFinite(n)) {
|
|
69
|
+
throw invalidId(text, "not a finite number");
|
|
70
|
+
}
|
|
71
|
+
return n;
|
|
72
|
+
}
|
|
73
|
+
default: {
|
|
74
|
+
const name: string = mode;
|
|
75
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id coercion ${name}`, { option: "ids", found: name });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Coerce a typed value (a JSON scalar, a record field) under a rule.
|
|
82
|
+
* @param value - the value
|
|
83
|
+
* @param mode - the coercion rule
|
|
84
|
+
* @returns the id; E_INVALID_ID when the rule rejects the value
|
|
85
|
+
*/
|
|
86
|
+
export function coerceId(value: unknown, mode: IdCoercion): NodeId {
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
return coerceIdText(value, mode);
|
|
89
|
+
}
|
|
90
|
+
if (typeof value === "number") {
|
|
91
|
+
if (!Number.isFinite(value)) {
|
|
92
|
+
throw invalidId(value, "not a finite number");
|
|
93
|
+
}
|
|
94
|
+
return mode === "string" ? String(value) : value;
|
|
95
|
+
}
|
|
96
|
+
if (mode === "string" && (typeof value === "boolean" || typeof value === "bigint" || value === null)) {
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
throw invalidId(value, `a ${value === null ? "null" : typeof value} is not an id under ids: "${mode}"`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The E_INVALID_ID error of a rejected value.
|
|
104
|
+
* @param value - the rejected value
|
|
105
|
+
* @param reason - why
|
|
106
|
+
* @returns the error
|
|
107
|
+
*/
|
|
108
|
+
function invalidId(value: unknown, reason: string): GraphFormatError {
|
|
109
|
+
const shown = typeof value === "string" || typeof value === "number" ? value : typeof value;
|
|
110
|
+
return new GraphFormatError("E_INVALID_ID", `invalid node id ${JSON.stringify(shown)}: ${reason}`, {
|
|
111
|
+
reason,
|
|
112
|
+
value: typeof value === "bigint" ? value.toString() : value,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A stateful coercer for one import call: applies the rule and, under "number", detects merges
|
|
118
|
+
* (two distinct texts mapping to one number) so the importer can report them as coercion issues
|
|
119
|
+
* (design section 4.1).
|
|
120
|
+
*/
|
|
121
|
+
export class IdCoercer {
|
|
122
|
+
/** The rule. */
|
|
123
|
+
readonly mode: IdCoercion;
|
|
124
|
+
|
|
125
|
+
/** Texts merged into an id another text already produced. */
|
|
126
|
+
mergeCount = 0;
|
|
127
|
+
|
|
128
|
+
/** The text that first produced each numeric id, kept only under "number". */
|
|
129
|
+
private readonly firstText: Map<number, string> | null;
|
|
130
|
+
|
|
131
|
+
/** The merge detected by the most recent text() call, or null. */
|
|
132
|
+
lastMerge: { readonly id: number; readonly text: string; readonly previousText: string } | null = null;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a coercer.
|
|
136
|
+
* @param mode - the rule
|
|
137
|
+
*/
|
|
138
|
+
constructor(mode: IdCoercion) {
|
|
139
|
+
this.mode = mode;
|
|
140
|
+
this.firstText = mode === "number" ? new Map() : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Coerce a text cell, recording a merge under "number" when a different text already produced
|
|
145
|
+
* the same number.
|
|
146
|
+
* @param text - the cell text
|
|
147
|
+
* @returns the id
|
|
148
|
+
*/
|
|
149
|
+
text(text: string): NodeId {
|
|
150
|
+
const id = coerceIdText(text, this.mode);
|
|
151
|
+
this.lastMerge = null;
|
|
152
|
+
if (this.firstText !== null && typeof id === "number") {
|
|
153
|
+
const previous = this.firstText.get(id);
|
|
154
|
+
if (previous === undefined) {
|
|
155
|
+
this.firstText.set(id, text);
|
|
156
|
+
} else if (previous !== text) {
|
|
157
|
+
this.mergeCount++;
|
|
158
|
+
this.lastMerge = { id, text, previousText: previous };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return id;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Coerce a typed value.
|
|
166
|
+
* @param value - the value
|
|
167
|
+
* @returns the id
|
|
168
|
+
*/
|
|
169
|
+
value(value: unknown): NodeId {
|
|
170
|
+
if (typeof value === "string") {
|
|
171
|
+
return this.text(value);
|
|
172
|
+
}
|
|
173
|
+
this.lastMerge = null;
|
|
174
|
+
return coerceId(value, this.mode);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input handling shared by every importer (design section 8.4): the ImportInput union (whole
|
|
3
|
+
* text, whole bytes, a byte stream, an async iterable of text or byte chunks) read as a sequence of
|
|
4
|
+
* UTF-8 text chunks, as lines, or as one string, with BOM handling, cancellation through an
|
|
5
|
+
* AbortSignal and byte progress.
|
|
6
|
+
*
|
|
7
|
+
* Bytes are decoded with `new TextDecoder("utf-8", { fatal: true })` in streaming mode, so a
|
|
8
|
+
* multi-byte character split across two chunks decodes correctly and an invalid sequence is a
|
|
9
|
+
* parse-error (issue code E_INVALID_UTF8) that aborts the import, never a silent U+FFFD that could
|
|
10
|
+
* alias two ids. A leading U+FEFF is stripped from text and from decoded bytes alike.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { GraphFormatError } from "@graphty/graph-format";
|
|
14
|
+
|
|
15
|
+
import { type ImportInput } from "../types.js";
|
|
16
|
+
import { INVALID_UTF8_CODE } from "./codes.js";
|
|
17
|
+
import { type ImportReportBuilder } from "./report.js";
|
|
18
|
+
|
|
19
|
+
export { INVALID_UTF8_CODE };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Cancellation and progress hooks of the reader; the resolved importer options satisfy this shape.
|
|
23
|
+
* Consumed by the per-format importers and exporters under src/formats.
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export interface ReadOptions {
|
|
27
|
+
/** The cancellation signal, or null / undefined for none. */
|
|
28
|
+
readonly signal?: AbortSignal | null | undefined;
|
|
29
|
+
/** The progress callback, or null / undefined for none. */
|
|
30
|
+
readonly onProgress?: ((bytesDone: number, bytesTotal?: number) => void) | null | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Bytes decoded per call when the input is one in-memory Uint8Array: every consumer of textChunks()
|
|
35
|
+
* then sees bounded chunks whatever the input shape (a line reader or a record reader that holds
|
|
36
|
+
* one parse result per chunk never holds more than this much text at once), and progress stays
|
|
37
|
+
* granular.
|
|
38
|
+
*/
|
|
39
|
+
const DECODE_SLICE = 256 * 1024;
|
|
40
|
+
|
|
41
|
+
const BOM = String.fromCharCode(0xfeff);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether a value is an ImportInput this module can read.
|
|
45
|
+
* @param input - any value
|
|
46
|
+
* @returns true for a string, a Uint8Array, a ReadableStream or an async iterable
|
|
47
|
+
*/
|
|
48
|
+
export function isImportInput(input: unknown): input is ImportInput {
|
|
49
|
+
if (typeof input === "string" || input instanceof Uint8Array) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (typeof input !== "object" || input === null) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return typeof (input as { getReader?: unknown }).getReader === "function" || Symbol.asyncIterator in input;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The total size of an in-memory input (bytes for a Uint8Array, UTF-16 code units for a string),
|
|
60
|
+
* for the `bytesTotal` argument of onProgress; null for a stream or an iterable.
|
|
61
|
+
* @param input - the input
|
|
62
|
+
* @returns the size, or null when unknown up front
|
|
63
|
+
*/
|
|
64
|
+
export function inputLength(input: ImportInput): number | null {
|
|
65
|
+
if (typeof input === "string") {
|
|
66
|
+
return input.length;
|
|
67
|
+
}
|
|
68
|
+
if (input instanceof Uint8Array) {
|
|
69
|
+
return input.byteLength;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Throw the signal's reason when it is aborted, exactly as the platform's
|
|
76
|
+
* `AbortSignal.throwIfAborted()` does: the reason as it is (the DOMException named "AbortError"
|
|
77
|
+
* of a reason-less abort, or whatever the caller passed to `abort(reason)`, an Error or not), so a
|
|
78
|
+
* caller can compare the rejection with `signal.reason`. Only a runtime that stores no reason at
|
|
79
|
+
* all gets a synthesised AbortError.
|
|
80
|
+
* @param signal - the signal, or null
|
|
81
|
+
*/
|
|
82
|
+
export function throwIfAborted(signal: AbortSignal | null | undefined): void {
|
|
83
|
+
if (signal === null || signal === undefined || !signal.aborted) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const { reason }: { reason: unknown } = signal;
|
|
87
|
+
if (reason === undefined) {
|
|
88
|
+
throw abortError();
|
|
89
|
+
}
|
|
90
|
+
// the platform contract: the reason itself, whatever its type
|
|
91
|
+
throw reason as Error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* An abort error for a signal that carries no reason.
|
|
96
|
+
* @returns a DOMException named "AbortError" where DOMException exists, else an Error with that name
|
|
97
|
+
*/
|
|
98
|
+
function abortError(): Error {
|
|
99
|
+
const message = "The import was aborted";
|
|
100
|
+
if (typeof DOMException === "function") {
|
|
101
|
+
return new DOMException(message, "AbortError");
|
|
102
|
+
}
|
|
103
|
+
const err = new Error(message);
|
|
104
|
+
err.name = "AbortError";
|
|
105
|
+
return err;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read an ImportInput as a sequence of decoded text chunks. Chunk boundaries carry no meaning:
|
|
110
|
+
* a caller that needs lines uses LineReader, one that needs the whole document uses readText().
|
|
111
|
+
* The signal is checked before every chunk; a stream is cancelled when the consumer stops early
|
|
112
|
+
* or the signal fires. Progress is reported after every chunk.
|
|
113
|
+
* @param input - the input
|
|
114
|
+
* @param report - the report the decode error is recorded in (E_INVALID_UTF8, then ImportError)
|
|
115
|
+
* @param options - cancellation and progress
|
|
116
|
+
* @yields decoded text; the first chunk has any leading BOM removed
|
|
117
|
+
* @returns nothing
|
|
118
|
+
*/
|
|
119
|
+
export async function* textChunks(
|
|
120
|
+
input: ImportInput,
|
|
121
|
+
report: ImportReportBuilder,
|
|
122
|
+
options: ReadOptions = {},
|
|
123
|
+
): AsyncGenerator<string, void, undefined> {
|
|
124
|
+
const signal = options.signal ?? null;
|
|
125
|
+
const onProgress = options.onProgress ?? null;
|
|
126
|
+
const total = inputLength(input);
|
|
127
|
+
let done = 0;
|
|
128
|
+
let first = true;
|
|
129
|
+
const emit = (text: string): string => {
|
|
130
|
+
if (first && text.length > 0) {
|
|
131
|
+
first = false;
|
|
132
|
+
return text.startsWith(BOM) ? text.slice(1) : text;
|
|
133
|
+
}
|
|
134
|
+
return text;
|
|
135
|
+
};
|
|
136
|
+
throwIfAborted(signal);
|
|
137
|
+
if (typeof input === "string") {
|
|
138
|
+
const text = emit(input);
|
|
139
|
+
done = input.length;
|
|
140
|
+
if (text.length > 0) {
|
|
141
|
+
yield text;
|
|
142
|
+
}
|
|
143
|
+
onProgress?.(done, total ?? undefined);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
147
|
+
const decode = (bytes: Uint8Array, stream: boolean): string => {
|
|
148
|
+
try {
|
|
149
|
+
return decoder.decode(bytes, { stream });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err instanceof TypeError) {
|
|
152
|
+
report.fail(INVALID_UTF8_CODE, `invalid UTF-8 near byte ${done}`, undefined, { byteOffset: done });
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
if (input instanceof Uint8Array) {
|
|
158
|
+
for (let offset = 0; offset < input.byteLength; offset += DECODE_SLICE) {
|
|
159
|
+
throwIfAborted(signal);
|
|
160
|
+
const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, input.byteLength));
|
|
161
|
+
const text = emit(decode(slice, offset + DECODE_SLICE < input.byteLength));
|
|
162
|
+
done = Math.min(offset + DECODE_SLICE, input.byteLength);
|
|
163
|
+
if (text.length > 0) {
|
|
164
|
+
yield text;
|
|
165
|
+
}
|
|
166
|
+
onProgress?.(done, total ?? undefined);
|
|
167
|
+
}
|
|
168
|
+
if (input.byteLength === 0) {
|
|
169
|
+
onProgress?.(0, 0);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const chunks = isReadableStream(input) ? streamChunks(input, signal) : input;
|
|
174
|
+
for await (const chunk of chunks) {
|
|
175
|
+
throwIfAborted(signal);
|
|
176
|
+
let text: string;
|
|
177
|
+
if (typeof chunk === "string") {
|
|
178
|
+
// finish any byte sequence still pending in the decoder before switching to text
|
|
179
|
+
const pending = decode(new Uint8Array(0), false);
|
|
180
|
+
text = emit(pending + chunk);
|
|
181
|
+
done += chunk.length;
|
|
182
|
+
} else if (chunk instanceof Uint8Array) {
|
|
183
|
+
text = emit(decode(chunk, true));
|
|
184
|
+
done += chunk.byteLength;
|
|
185
|
+
} else {
|
|
186
|
+
throw new GraphFormatError("E_UNSUPPORTED", "an input chunk must be a string or a Uint8Array", {
|
|
187
|
+
reason: "chunk type",
|
|
188
|
+
found: typeof chunk,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (text.length > 0) {
|
|
192
|
+
yield text;
|
|
193
|
+
}
|
|
194
|
+
onProgress?.(done);
|
|
195
|
+
}
|
|
196
|
+
const tail = emit(decode(new Uint8Array(0), false));
|
|
197
|
+
if (tail.length > 0) {
|
|
198
|
+
yield tail;
|
|
199
|
+
}
|
|
200
|
+
onProgress?.(done, done);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whether an input is a ReadableStream (by duck type, so a stream from another realm qualifies).
|
|
205
|
+
* @param input - a non-string, non-Uint8Array input
|
|
206
|
+
* @returns true for a ReadableStream
|
|
207
|
+
*/
|
|
208
|
+
function isReadableStream(
|
|
209
|
+
input: ReadableStream<Uint8Array> | AsyncIterable<string | Uint8Array>,
|
|
210
|
+
): input is ReadableStream<Uint8Array> {
|
|
211
|
+
return typeof (input as { getReader?: unknown }).getReader === "function";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Iterate a ReadableStream through a reader, cancelling the stream when iteration stops early.
|
|
216
|
+
* @param stream - the stream
|
|
217
|
+
* @param signal - the cancellation signal, or null
|
|
218
|
+
* @yields the stream's chunks
|
|
219
|
+
* @returns nothing
|
|
220
|
+
*/
|
|
221
|
+
async function* streamChunks(
|
|
222
|
+
stream: ReadableStream<Uint8Array>,
|
|
223
|
+
signal: AbortSignal | null,
|
|
224
|
+
): AsyncGenerator<Uint8Array, void, undefined> {
|
|
225
|
+
const reader = stream.getReader();
|
|
226
|
+
let finished = false;
|
|
227
|
+
try {
|
|
228
|
+
for (;;) {
|
|
229
|
+
throwIfAborted(signal);
|
|
230
|
+
const { done, value } = await reader.read();
|
|
231
|
+
if (done) {
|
|
232
|
+
finished = true;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
yield value;
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
if (!finished) {
|
|
239
|
+
await reader.cancel(signal?.reason).catch(() => undefined);
|
|
240
|
+
}
|
|
241
|
+
reader.releaseLock();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Read the whole input as one string (the GML / DOT / JSON path, design section 8.4).
|
|
247
|
+
* @param input - the input
|
|
248
|
+
* @param report - the report the decode error is recorded in
|
|
249
|
+
* @param options - cancellation and progress
|
|
250
|
+
* @returns the decoded text without a leading BOM
|
|
251
|
+
*/
|
|
252
|
+
export async function readText(
|
|
253
|
+
input: ImportInput,
|
|
254
|
+
report: ImportReportBuilder,
|
|
255
|
+
options: ReadOptions = {},
|
|
256
|
+
): Promise<string> {
|
|
257
|
+
if (typeof input === "string") {
|
|
258
|
+
throwIfAborted(options.signal);
|
|
259
|
+
options.onProgress?.(input.length, input.length);
|
|
260
|
+
return input.startsWith(BOM) ? input.slice(1) : input;
|
|
261
|
+
}
|
|
262
|
+
const parts: string[] = [];
|
|
263
|
+
for await (const chunk of textChunks(input, report, options)) {
|
|
264
|
+
parts.push(chunk);
|
|
265
|
+
}
|
|
266
|
+
return parts.length === 1 ? parts[0] : parts.join("");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Lines of an ImportInput without allocating anything per line but the string itself: iterate with
|
|
271
|
+
* `for await (const text of reader)` and read `reader.line` (1-based) for the line just yielded.
|
|
272
|
+
* `\n`, `\r\n` and lone `\r` all end a line; the terminator is not part of the text; a final
|
|
273
|
+
* line without a terminator is yielded when non-empty, and every line in between is yielded even
|
|
274
|
+
* when empty (the importer decides what a blank line means).
|
|
275
|
+
*/
|
|
276
|
+
export class LineReader implements AsyncIterable<string> {
|
|
277
|
+
private readonly input: ImportInput;
|
|
278
|
+
|
|
279
|
+
private readonly report: ImportReportBuilder;
|
|
280
|
+
|
|
281
|
+
private readonly options: ReadOptions;
|
|
282
|
+
|
|
283
|
+
private lineNumber = 0;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Create a reader over an input; nothing is read until iteration starts.
|
|
287
|
+
* @param input - the input
|
|
288
|
+
* @param report - the report decode errors are recorded in
|
|
289
|
+
* @param options - cancellation and progress
|
|
290
|
+
*/
|
|
291
|
+
constructor(input: ImportInput, report: ImportReportBuilder, options: ReadOptions = {}) {
|
|
292
|
+
this.input = input;
|
|
293
|
+
this.report = report;
|
|
294
|
+
this.options = options;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The 1-based number of the line most recently yielded.
|
|
299
|
+
* @returns the line number; 0 before the first line
|
|
300
|
+
*/
|
|
301
|
+
get line(): number {
|
|
302
|
+
return this.lineNumber;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Iterate the lines.
|
|
307
|
+
* @yields one line at a time, terminator removed
|
|
308
|
+
* @returns nothing
|
|
309
|
+
*/
|
|
310
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<string, void, undefined> {
|
|
311
|
+
// The pieces of the line in progress (none of them holds a line break, except that the last
|
|
312
|
+
// may end with a `\r` whose meaning the next chunk decides); joined once when the line ends,
|
|
313
|
+
// so a line spanning many chunks costs its length, not its length times the chunk count.
|
|
314
|
+
const pending: string[] = [];
|
|
315
|
+
let trailingCr = false;
|
|
316
|
+
for await (const chunk of textChunks(this.input, this.report, this.options)) {
|
|
317
|
+
let start = 0;
|
|
318
|
+
const end = chunk.length;
|
|
319
|
+
if (trailingCr) {
|
|
320
|
+
// the previous chunk ended with `\r`: that ended a line, and a leading `\n` here is
|
|
321
|
+
// the second half of the same terminator
|
|
322
|
+
trailingCr = false;
|
|
323
|
+
this.lineNumber++;
|
|
324
|
+
const joined = pending.join("");
|
|
325
|
+
pending.length = 0;
|
|
326
|
+
yield joined.slice(0, -1);
|
|
327
|
+
if (chunk.charCodeAt(0) === 10) {
|
|
328
|
+
start = 1;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// the next `\n` and `\r` at or after `start`; each is searched for once per chunk and
|
|
332
|
+
// again only after it was consumed, so a chunk is scanned once whatever its line count
|
|
333
|
+
let nl = chunk.indexOf("\n", start);
|
|
334
|
+
let cr = chunk.indexOf("\r", start);
|
|
335
|
+
while (nl >= 0 || cr >= 0) {
|
|
336
|
+
let cut: number;
|
|
337
|
+
let next: number;
|
|
338
|
+
if (cr >= 0 && (nl < 0 || cr < nl)) {
|
|
339
|
+
if (cr === end - 1) {
|
|
340
|
+
// a trailing \r may be the first half of \r\n split across chunks
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
cut = cr;
|
|
344
|
+
next = chunk.charCodeAt(cr + 1) === 10 ? cr + 2 : cr + 1;
|
|
345
|
+
} else {
|
|
346
|
+
cut = nl;
|
|
347
|
+
next = nl + 1;
|
|
348
|
+
}
|
|
349
|
+
this.lineNumber++;
|
|
350
|
+
const piece = chunk.slice(start, cut);
|
|
351
|
+
if (pending.length === 0) {
|
|
352
|
+
yield piece;
|
|
353
|
+
} else {
|
|
354
|
+
pending.push(piece);
|
|
355
|
+
const joined = pending.join("");
|
|
356
|
+
pending.length = 0;
|
|
357
|
+
yield joined;
|
|
358
|
+
}
|
|
359
|
+
start = next;
|
|
360
|
+
if (nl >= 0 && nl < next) {
|
|
361
|
+
nl = chunk.indexOf("\n", next);
|
|
362
|
+
}
|
|
363
|
+
if (cr >= 0 && cr < next) {
|
|
364
|
+
cr = chunk.indexOf("\r", next);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (start < end) {
|
|
368
|
+
pending.push(start === 0 ? chunk : chunk.slice(start));
|
|
369
|
+
trailingCr = chunk.charCodeAt(end - 1) === 13;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (pending.length > 0) {
|
|
373
|
+
this.lineNumber++;
|
|
374
|
+
const joined = pending.join("");
|
|
375
|
+
yield joined.endsWith("\r") ? joined.slice(0, -1) : joined;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|