@graphty/graph-io 0.3.41 → 0.3.42
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/README.md +2 -0
- package/dist/chunks/{children-CL3Cy0ez.js → children-CA4coBi0.js} +36 -27
- package/dist/chunks/children-CA4coBi0.js.map +1 -0
- package/dist/chunks/{choose-CwFQamH9.js → choose-DvpbdRuz.js} +2 -2
- package/dist/chunks/{choose-CwFQamH9.js.map → choose-DvpbdRuz.js.map} +1 -1
- package/dist/chunks/{constants-Cf4Nn6tw.js → constants-fT3E8JYY.js} +2 -2
- package/dist/chunks/{constants-Cf4Nn6tw.js.map → constants-fT3E8JYY.js.map} +1 -1
- package/dist/chunks/{escape-DBb8zuNE.js → escape-B62ZGM_a.js} +2 -2
- package/dist/chunks/{escape-DBb8zuNE.js.map → escape-B62ZGM_a.js.map} +1 -1
- package/dist/chunks/{export-DD3Lvw-F.js → export-rKLeGQMo.js} +232 -128
- package/dist/chunks/export-rKLeGQMo.js.map +1 -0
- package/dist/chunks/{exporter-CS35R6Fg.js → exporter-8hLNemFk.js} +4 -4
- package/dist/chunks/{exporter-CS35R6Fg.js.map → exporter-8hLNemFk.js.map} +1 -1
- package/dist/chunks/{exporter-D8r1N2dU.js → exporter-BWwEuOPT.js} +5 -5
- package/dist/chunks/{exporter-D8r1N2dU.js.map → exporter-BWwEuOPT.js.map} +1 -1
- package/dist/chunks/{exporter-BqtmlxNr.js → exporter-CpRyHZ3H.js} +34 -21
- package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
- package/dist/chunks/{exporter-NMplSnYn.js → exporter-yLxKBvLE.js} +3 -3
- package/dist/chunks/{exporter-NMplSnYn.js.map → exporter-yLxKBvLE.js.map} +1 -1
- package/dist/chunks/{importer-S-abxJHn.js → importer-BbNb7eNg.js} +111 -71
- package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
- package/dist/chunks/{importer-BdWHmlmG.js → importer-C98dpSH4.js} +5 -5
- package/dist/chunks/{importer-BdWHmlmG.js.map → importer-C98dpSH4.js.map} +1 -1
- package/dist/chunks/{importer-r29cWnEf.js → importer-D794XUTe.js} +3 -3
- package/dist/chunks/{importer-r29cWnEf.js.map → importer-D794XUTe.js.map} +1 -1
- package/dist/chunks/{importer-BfbzWk6k.js → importer-Dz7tI8Sa.js} +5 -5
- package/dist/chunks/{importer-BfbzWk6k.js.map → importer-Dz7tI8Sa.js.map} +1 -1
- package/dist/chunks/{importer-DviEsHy4.js → importer-Ht7LNg8L.js} +4 -4
- package/dist/chunks/{importer-DviEsHy4.js.map → importer-Ht7LNg8L.js.map} +1 -1
- package/dist/chunks/{importer-BKC7AmCA.js → importer-WrGRZb0N.js} +6 -6
- package/dist/chunks/{importer-BKC7AmCA.js.map → importer-WrGRZb0N.js.map} +1 -1
- package/dist/chunks/{importer-BmaZcskq.js → importer-xZ-3suY1.js} +5 -5
- package/dist/chunks/{importer-BmaZcskq.js.map → importer-xZ-3suY1.js.map} +1 -1
- package/dist/chunks/{json-elements-DdF-etPT.js → json-elements-DKSqpNW1.js} +3 -3
- package/dist/chunks/{json-elements-DdF-etPT.js.map → json-elements-DKSqpNW1.js.map} +1 -1
- package/dist/chunks/{ontology-export-BC0zyPhj.js → ontology-export-CtRXjg_i.js} +2 -2
- package/dist/chunks/{ontology-export-BC0zyPhj.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
- package/dist/chunks/{records-Uc3LkABx.js → records-Cu4bxYOm.js} +56 -26
- package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
- package/dist/chunks/{text-Bh9SaAob.js → text-DHOJXnED.js} +2 -2
- package/dist/chunks/{text-Bh9SaAob.js.map → text-DHOJXnED.js.map} +1 -1
- package/dist/csv.js +260 -196
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +2 -2
- package/dist/gml.js +3 -3
- package/dist/graph-io.js +27 -27
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +3 -3
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/children.d.ts.map +1 -1
- package/dist/src/children.js +50 -27
- package/dist/src/children.js.map +1 -1
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +52 -33
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +178 -82
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/lists.js +30 -15
- package/dist/src/common/lists.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +211 -121
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +113 -74
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +60 -26
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/dot/tokenizer.d.ts +39 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
- package/dist/src/formats/dot/tokenizer.js +112 -66
- package/dist/src/formats/dot/tokenizer.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +54 -20
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/children.ts +54 -27
- package/src/common/attributes.ts +59 -30
- package/src/common/input.ts +223 -95
- package/src/common/lists.ts +29 -15
- package/src/formats/csv/exporter.ts +274 -136
- package/src/formats/csv/importer.ts +138 -78
- package/src/formats/csv/records.ts +71 -25
- package/src/formats/dot/tokenizer.ts +118 -68
- package/src/formats/json/dialect.ts +61 -22
- package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
- package/dist/chunks/export-DD3Lvw-F.js.map +0 -1
- package/dist/chunks/exporter-BqtmlxNr.js.map +0 -1
- package/dist/chunks/importer-S-abxJHn.js.map +0 -1
- package/dist/chunks/records-Uc3LkABx.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphty/graph-io",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.42",
|
|
4
4
|
"description": "Read and write 13 graph file formats (JSON, GraphML, GEXF, CSV, GML, DOT, Pajek, Neo4j CSV, XGMML, CX2, CX, OBO, Cytoscape sessions) as @graphty/graph-format snapshots",
|
|
5
5
|
"author": "Adam Powers <apowers@ato.ms>",
|
|
6
6
|
"type": "module",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
},
|
|
122
122
|
"homepage": "https://graphty.app/docs/graph-io/",
|
|
123
123
|
"dependencies": {
|
|
124
|
-
"@graphty/graph-format": "^1.4.
|
|
124
|
+
"@graphty/graph-format": "^1.4.3"
|
|
125
125
|
},
|
|
126
126
|
"peerDependencies": {
|
|
127
127
|
"@graphty/graph-format": "^1.0.0"
|
package/src/children.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* every node is written exactly once.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { type Column, type GraphSnapshot, INVALID_INDEX } from "@graphty/graph-format";
|
|
17
|
+
import { type Column, type GraphSnapshot, INVALID_INDEX, type ListColumn } from "@graphty/graph-format";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The role of the column a children CSR is built over.
|
|
@@ -282,7 +282,6 @@ export function childrenFromColumn(nodeCount: number, column: Column | null): Ch
|
|
|
282
282
|
let dropped = 0;
|
|
283
283
|
const references: number[] = [];
|
|
284
284
|
if (column !== null) {
|
|
285
|
-
const rows = Math.min(column.length, nodeCount);
|
|
286
285
|
const keep = (child: number, parent: number): void => {
|
|
287
286
|
if (!Number.isInteger(parent) || parent === INVALID_INDEX || parent >= nodeCount || parent === child) {
|
|
288
287
|
dropped++;
|
|
@@ -292,31 +291,9 @@ export function childrenFromColumn(nodeCount: number, column: Column | null): Ch
|
|
|
292
291
|
rowPtr[parent + 1]++;
|
|
293
292
|
parentCount[child]++;
|
|
294
293
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (column.isSet(i)) {
|
|
299
|
-
keep(i, data[i]);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
} else if (column.dtype === "list") {
|
|
303
|
-
for (let i = 0; i < rows; i++) {
|
|
304
|
-
if (!column.isSet(i)) {
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
for (const item of column.sliceOf(i)) {
|
|
308
|
-
if (typeof item === "number") {
|
|
309
|
-
keep(i, item);
|
|
310
|
-
} else {
|
|
311
|
-
dropped++;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
} else {
|
|
316
|
-
throw new TypeError(
|
|
317
|
-
`a containment column must be u32 or a list of u32, found ${column.dtype} column "${column.meta.name}"`,
|
|
318
|
-
);
|
|
319
|
-
}
|
|
294
|
+
// keep() counts into `dropped` while it runs, so add the non-numeric items after the call
|
|
295
|
+
const nonNumeric = readContainment(column, Math.min(column.length, nodeCount), keep);
|
|
296
|
+
dropped += nonNumeric;
|
|
320
297
|
}
|
|
321
298
|
for (let u = 0; u < nodeCount; u++) {
|
|
322
299
|
rowPtr[u + 1] += rowPtr[u];
|
|
@@ -330,6 +307,56 @@ export function childrenFromColumn(nodeCount: number, column: Column | null): Ch
|
|
|
330
307
|
return new ChildrenCsr(nodeCount, rowPtr, children, parentCount, dropped, column);
|
|
331
308
|
}
|
|
332
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Hand every (child, parent) reference of the first `rows` rows of a containment column to `keep`,
|
|
312
|
+
* in ascending child order.
|
|
313
|
+
* @param column - a u32 `parent`-shaped column or a list-of-u32 `parents`-shaped column
|
|
314
|
+
* @param rows - how many rows to read
|
|
315
|
+
* @param keep - receives each reference
|
|
316
|
+
* @returns how many list items were dropped for not being a number
|
|
317
|
+
*/
|
|
318
|
+
function readContainment(column: Column, rows: number, keep: (child: number, parent: number) => void): number {
|
|
319
|
+
if (column.dtype === "u32") {
|
|
320
|
+
const { data } = column;
|
|
321
|
+
for (let i = 0; i < rows; i++) {
|
|
322
|
+
if (column.isSet(i)) {
|
|
323
|
+
keep(i, data[i]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
if (column.dtype === "list") {
|
|
329
|
+
return readParentLists(column, rows, keep);
|
|
330
|
+
}
|
|
331
|
+
throw new TypeError(
|
|
332
|
+
`a containment column must be u32 or a list of u32, found ${column.dtype} column "${column.meta.name}"`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Hand every numeric item of a `parents`-shaped list column to `keep`.
|
|
338
|
+
* @param column - the list column
|
|
339
|
+
* @param rows - how many rows to read
|
|
340
|
+
* @param keep - receives each reference
|
|
341
|
+
* @returns how many items were not numbers
|
|
342
|
+
*/
|
|
343
|
+
function readParentLists(column: ListColumn, rows: number, keep: (child: number, parent: number) => void): number {
|
|
344
|
+
let dropped = 0;
|
|
345
|
+
for (let i = 0; i < rows; i++) {
|
|
346
|
+
if (!column.isSet(i)) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
for (const item of column.sliceOf(i)) {
|
|
350
|
+
if (typeof item === "number") {
|
|
351
|
+
keep(i, item);
|
|
352
|
+
} else {
|
|
353
|
+
dropped++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return dropped;
|
|
358
|
+
}
|
|
359
|
+
|
|
333
360
|
/**
|
|
334
361
|
* The column a ChildrenOptions selects.
|
|
335
362
|
* @param snapshot - the snapshot
|
package/src/common/attributes.ts
CHANGED
|
@@ -132,22 +132,7 @@ export const DICT_SAMPLE_ROWS = 1024;
|
|
|
132
132
|
export function declareAttribute(input: AttributeDeclarationInput): DeclaredAttribute {
|
|
133
133
|
const issues: DeclarationIssue[] = [];
|
|
134
134
|
const listSyntax = input.listSyntax ?? "gexf";
|
|
135
|
-
let spec
|
|
136
|
-
if (input.type === null) {
|
|
137
|
-
spec = stringSpec(null);
|
|
138
|
-
} else {
|
|
139
|
-
const mapped = mapDeclaredType(input.format, input.type, input.long);
|
|
140
|
-
if (mapped === null) {
|
|
141
|
-
spec = stringSpec(input.type);
|
|
142
|
-
issues.push({
|
|
143
|
-
category: "unsupported",
|
|
144
|
-
code: UNKNOWN_TYPE_CODE,
|
|
145
|
-
message: `${input.format} type "${input.type}" is not supported; values kept as text`,
|
|
146
|
-
});
|
|
147
|
-
} else {
|
|
148
|
-
spec = mapped;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
135
|
+
let spec = declaredSpec(input, issues);
|
|
151
136
|
const { name, renamed } = resolveName(input);
|
|
152
137
|
if (renamed) {
|
|
153
138
|
issues.push({
|
|
@@ -179,20 +164,7 @@ export function declareAttribute(input: AttributeDeclarationInput): DeclaredAttr
|
|
|
179
164
|
}
|
|
180
165
|
const optionsText = input.optionsText ?? null;
|
|
181
166
|
if (optionsText !== null) {
|
|
182
|
-
|
|
183
|
-
const options = splitListText(optionsText, listSyntax).map((item) => parseItem(item, spec));
|
|
184
|
-
decl.options = options;
|
|
185
|
-
if (spec.dtype === "string" && !spec.list && options.every((o) => typeof o === "string")) {
|
|
186
|
-
decl.dtype = "dict";
|
|
187
|
-
spec = { ...spec, dtype: "dict" };
|
|
188
|
-
}
|
|
189
|
-
} catch (err) {
|
|
190
|
-
issues.push({
|
|
191
|
-
category: "validation-error",
|
|
192
|
-
code: BAD_OPTIONS_CODE,
|
|
193
|
-
message: `options "${optionsText}" of attribute "${name}" do not parse as ${spec.declared}: ${messageOf(err)}`,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
167
|
+
spec = declareOptions(decl, spec, optionsText, listSyntax, issues);
|
|
196
168
|
}
|
|
197
169
|
const defaultText = input.defaultText ?? null;
|
|
198
170
|
if (defaultText !== null) {
|
|
@@ -210,6 +182,63 @@ export function declareAttribute(input: AttributeDeclarationInput): DeclaredAttr
|
|
|
210
182
|
return { decl, spec, listSyntax, companion, renamed, issues };
|
|
211
183
|
}
|
|
212
184
|
|
|
185
|
+
/**
|
|
186
|
+
* The value spec of a declared type: string for no type, string with UNKNOWN_TYPE_CODE for a type
|
|
187
|
+
* the format does not define.
|
|
188
|
+
* @param input - what the file declares
|
|
189
|
+
* @param issues - receives the unknown-type warning
|
|
190
|
+
* @returns the spec
|
|
191
|
+
*/
|
|
192
|
+
function declaredSpec(input: AttributeDeclarationInput, issues: DeclarationIssue[]): DeclaredTypeSpec {
|
|
193
|
+
if (input.type === null) {
|
|
194
|
+
return stringSpec(null);
|
|
195
|
+
}
|
|
196
|
+
const mapped = mapDeclaredType(input.format, input.type, input.long);
|
|
197
|
+
if (mapped !== null) {
|
|
198
|
+
return mapped;
|
|
199
|
+
}
|
|
200
|
+
issues.push({
|
|
201
|
+
category: "unsupported",
|
|
202
|
+
code: UNKNOWN_TYPE_CODE,
|
|
203
|
+
message: `${input.format} type "${input.type}" is not supported; values kept as text`,
|
|
204
|
+
});
|
|
205
|
+
return stringSpec(input.type);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Put the parsed options of an attribute on its declaration; a plain string attribute whose
|
|
210
|
+
* options are all strings becomes a dict column.
|
|
211
|
+
* @param decl - the declaration to complete
|
|
212
|
+
* @param spec - the value spec
|
|
213
|
+
* @param optionsText - the options text
|
|
214
|
+
* @param listSyntax - the list syntax the options are split with
|
|
215
|
+
* @param issues - receives BAD_OPTIONS_CODE when the options do not parse
|
|
216
|
+
* @returns the spec, with dtype dict when the column became a dict
|
|
217
|
+
*/
|
|
218
|
+
function declareOptions(
|
|
219
|
+
decl: ColumnDecl,
|
|
220
|
+
spec: DeclaredTypeSpec,
|
|
221
|
+
optionsText: string,
|
|
222
|
+
listSyntax: ListSyntax,
|
|
223
|
+
issues: DeclarationIssue[],
|
|
224
|
+
): DeclaredTypeSpec {
|
|
225
|
+
try {
|
|
226
|
+
const options = splitListText(optionsText, listSyntax).map((item) => parseItem(item, spec));
|
|
227
|
+
decl.options = options;
|
|
228
|
+
if (spec.dtype === "string" && !spec.list && options.every((o) => typeof o === "string")) {
|
|
229
|
+
decl.dtype = "dict";
|
|
230
|
+
return { ...spec, dtype: "dict" };
|
|
231
|
+
}
|
|
232
|
+
} catch (err) {
|
|
233
|
+
issues.push({
|
|
234
|
+
category: "validation-error",
|
|
235
|
+
code: BAD_OPTIONS_CODE,
|
|
236
|
+
message: `options "${optionsText}" of attribute "${decl.name}" do not parse as ${spec.declared}: ${messageOf(err)}`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return spec;
|
|
240
|
+
}
|
|
241
|
+
|
|
213
242
|
/**
|
|
214
243
|
* Parse a value text by a declaration: a scalar, or a list split by the syntax with each item
|
|
215
244
|
* parsed by the item kind.
|
package/src/common/input.ts
CHANGED
|
@@ -506,46 +506,81 @@ class ByteDecoder {
|
|
|
506
506
|
const bad = this.encoding === "utf-8" ? invalidUtf8(all) : null;
|
|
507
507
|
const at = base + (bad === null ? 0 : bad.index);
|
|
508
508
|
if (this.mayFallBack && this.asciiSoFar) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
return this.binary(this.control);
|
|
513
|
-
}
|
|
514
|
-
if (this.textFollows && bad?.cut === true) {
|
|
515
|
-
return this.report.fail(
|
|
516
|
-
INVALID_UTF8_CODE,
|
|
517
|
-
`invalid UTF-8 at byte ${at}: a byte chunk ends inside a multi-byte sequence and a text chunk follows`,
|
|
518
|
-
undefined,
|
|
519
|
-
{ byteOffset: at },
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
if (!stream && bad?.truncated === true) {
|
|
523
|
-
return this.report.fail(
|
|
524
|
-
INVALID_UTF8_CODE,
|
|
525
|
-
`the input ends in the middle of a UTF-8 sequence at byte ${at} (it may be truncated)`,
|
|
526
|
-
undefined,
|
|
527
|
-
{ byteOffset: at },
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
if (!startsWithUtf8(all)) {
|
|
531
|
-
const cutNote =
|
|
532
|
-
!stream && bad?.cut === true
|
|
533
|
-
? ": it ends inside a multi-byte UTF-8 sequence, so it may be truncated"
|
|
534
|
-
: "";
|
|
535
|
-
this.report.warning(
|
|
536
|
-
"coercion",
|
|
537
|
-
ENCODING_FALLBACK_CODE,
|
|
538
|
-
this.declaredUtf8
|
|
539
|
-
? `the input declares UTF-8 but is not valid UTF-8 (at byte ${at}); everything before was ASCII, so it is read as windows-1252 (pass the encoding option to choose another)`
|
|
540
|
-
: `the input is not valid UTF-8 (at byte ${at}${cutNote}) and declares no encoding; read as windows-1252 (pass the encoding option to choose another)`,
|
|
541
|
-
);
|
|
542
|
-
this.use("windows-1252", false);
|
|
543
|
-
this.fellBack = true;
|
|
544
|
-
this.mixedPending = true;
|
|
545
|
-
this.reportUtf8After(all, base);
|
|
546
|
-
return (this.decoder as TextDecoder).decode(all, { stream });
|
|
509
|
+
const text = this.fallBack(all, bad, at, base, stream);
|
|
510
|
+
if (text !== null) {
|
|
511
|
+
return text;
|
|
547
512
|
}
|
|
548
513
|
}
|
|
514
|
+
return this.failDecode(bytes, at, base, stream);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Undeclared input that was ASCII so far failed to decode: fail on binary data or a cut
|
|
519
|
+
* sequence, else switch to windows-1252 with a warning unless the bytes start with valid
|
|
520
|
+
* non-ASCII UTF-8.
|
|
521
|
+
* @param all - the held-back bytes followed by the bytes that failed
|
|
522
|
+
* @param bad - the first invalid UTF-8 sequence, or null when the encoding is not UTF-8
|
|
523
|
+
* @param at - the byte offset of the first invalid byte
|
|
524
|
+
* @param base - the byte offset of `all`
|
|
525
|
+
* @param stream - whether more bytes follow
|
|
526
|
+
* @returns the bytes decoded as windows-1252, or null when they do not fall back
|
|
527
|
+
*/
|
|
528
|
+
private fallBack(
|
|
529
|
+
all: Uint8Array,
|
|
530
|
+
bad: Utf8Problem | null,
|
|
531
|
+
at: number,
|
|
532
|
+
base: number,
|
|
533
|
+
stream: boolean,
|
|
534
|
+
): string | null {
|
|
535
|
+
// windows-1252 text holds no control bytes but TAB, LF, FF and CR: one anywhere in
|
|
536
|
+
// undeclared input that is not UTF-8 marks binary data (watch() saw every byte so far)
|
|
537
|
+
if (this.control !== null) {
|
|
538
|
+
return this.binary(this.control);
|
|
539
|
+
}
|
|
540
|
+
if (this.textFollows && bad?.cut === true) {
|
|
541
|
+
return this.report.fail(
|
|
542
|
+
INVALID_UTF8_CODE,
|
|
543
|
+
`invalid UTF-8 at byte ${at}: a byte chunk ends inside a multi-byte sequence and a text chunk follows`,
|
|
544
|
+
undefined,
|
|
545
|
+
{ byteOffset: at },
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (!stream && bad?.truncated === true) {
|
|
549
|
+
return this.report.fail(
|
|
550
|
+
INVALID_UTF8_CODE,
|
|
551
|
+
`the input ends in the middle of a UTF-8 sequence at byte ${at} (it may be truncated)`,
|
|
552
|
+
undefined,
|
|
553
|
+
{ byteOffset: at },
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (startsWithUtf8(all)) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const cutNote =
|
|
560
|
+
!stream && bad?.cut === true ? ": it ends inside a multi-byte UTF-8 sequence, so it may be truncated" : "";
|
|
561
|
+
this.report.warning(
|
|
562
|
+
"coercion",
|
|
563
|
+
ENCODING_FALLBACK_CODE,
|
|
564
|
+
this.declaredUtf8
|
|
565
|
+
? `the input declares UTF-8 but is not valid UTF-8 (at byte ${at}); everything before was ASCII, so it is read as windows-1252 (pass the encoding option to choose another)`
|
|
566
|
+
: `the input is not valid UTF-8 (at byte ${at}${cutNote}) and declares no encoding; read as windows-1252 (pass the encoding option to choose another)`,
|
|
567
|
+
);
|
|
568
|
+
this.use("windows-1252", false);
|
|
569
|
+
this.fellBack = true;
|
|
570
|
+
this.mixedPending = true;
|
|
571
|
+
this.reportUtf8After(all, base);
|
|
572
|
+
return (this.decoder as TextDecoder).decode(all, { stream });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Fail the import on bytes that are not valid in the encoding.
|
|
577
|
+
* @param bytes - the bytes that failed
|
|
578
|
+
* @param at - the byte offset of the first invalid UTF-8 byte
|
|
579
|
+
* @param base - the byte offset of the held-back bytes and these
|
|
580
|
+
* @param stream - whether more bytes follow
|
|
581
|
+
* @returns never: it throws
|
|
582
|
+
*/
|
|
583
|
+
private failDecode(bytes: Uint8Array, at: number, base: number, stream: boolean): never {
|
|
549
584
|
if (this.encoding === "utf-8") {
|
|
550
585
|
const after = this.mayFallBack ? " after valid non-ASCII UTF-8 text; pass the encoding option" : "";
|
|
551
586
|
return this.report.fail(INVALID_UTF8_CODE, `invalid UTF-8 at byte ${at}${after}`, undefined, {
|
|
@@ -1662,81 +1697,174 @@ export class LineReader implements AsyncIterable<string> {
|
|
|
1662
1697
|
* @returns nothing
|
|
1663
1698
|
*/
|
|
1664
1699
|
async *[Symbol.asyncIterator](): AsyncGenerator<string, void, undefined> {
|
|
1665
|
-
// The pieces of the line in progress
|
|
1666
|
-
//
|
|
1667
|
-
|
|
1668
|
-
const pending: string[] = [];
|
|
1669
|
-
let pendingLength = 0;
|
|
1700
|
+
// The pieces of the line in progress, joined once when the line ends, so a line spanning
|
|
1701
|
+
// many chunks costs its length, not its length times the chunk count.
|
|
1702
|
+
const pending = new PendingLine((line) => tooLarge(this.report, this.lineTooLong(), line));
|
|
1670
1703
|
let trailingCr = false;
|
|
1671
1704
|
for await (const chunk of textChunks(this.input, this.report, this.options)) {
|
|
1672
1705
|
let start = 0;
|
|
1673
|
-
const end = chunk.length;
|
|
1674
1706
|
if (trailingCr) {
|
|
1675
1707
|
// the previous chunk ended with `\r`: that ended a line, and a leading `\n` here is
|
|
1676
1708
|
// the second half of the same terminator
|
|
1677
1709
|
trailingCr = false;
|
|
1678
1710
|
this.lineNumber++;
|
|
1679
|
-
|
|
1680
|
-
pending.length = 0;
|
|
1681
|
-
pendingLength = 0;
|
|
1682
|
-
yield joined.slice(0, -1);
|
|
1711
|
+
yield pending.take().slice(0, -1);
|
|
1683
1712
|
if (chunk.charCodeAt(0) === 10) {
|
|
1684
1713
|
start = 1;
|
|
1685
1714
|
}
|
|
1686
1715
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
let nl = chunk.indexOf("\n", start);
|
|
1690
|
-
let cr = chunk.indexOf("\r", start);
|
|
1691
|
-
while (nl >= 0 || cr >= 0) {
|
|
1692
|
-
let cut: number;
|
|
1693
|
-
let next: number;
|
|
1694
|
-
if (cr >= 0 && (nl < 0 || cr < nl)) {
|
|
1695
|
-
if (cr === end - 1) {
|
|
1696
|
-
// a trailing \r may be the first half of \r\n split across chunks
|
|
1697
|
-
break;
|
|
1698
|
-
}
|
|
1699
|
-
cut = cr;
|
|
1700
|
-
next = chunk.charCodeAt(cr + 1) === 10 ? cr + 2 : cr + 1;
|
|
1701
|
-
} else {
|
|
1702
|
-
cut = nl;
|
|
1703
|
-
next = nl + 1;
|
|
1704
|
-
}
|
|
1716
|
+
const breaks = new LineBreaks(chunk, start);
|
|
1717
|
+
while (breaks.advance()) {
|
|
1705
1718
|
this.lineNumber++;
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
yield piece;
|
|
1709
|
-
} else {
|
|
1710
|
-
if (pendingLength + piece.length > MAX_TEXT_LENGTH) {
|
|
1711
|
-
tooLarge(this.report, this.lineTooLong(), this.lineNumber);
|
|
1712
|
-
}
|
|
1713
|
-
pending.push(piece);
|
|
1714
|
-
const joined = pending.join("");
|
|
1715
|
-
pending.length = 0;
|
|
1716
|
-
pendingLength = 0;
|
|
1717
|
-
yield joined;
|
|
1718
|
-
}
|
|
1719
|
-
start = next;
|
|
1720
|
-
if (nl >= 0 && nl < next) {
|
|
1721
|
-
nl = chunk.indexOf("\n", next);
|
|
1722
|
-
}
|
|
1723
|
-
if (cr >= 0 && cr < next) {
|
|
1724
|
-
cr = chunk.indexOf("\r", next);
|
|
1725
|
-
}
|
|
1719
|
+
yield pending.finish(chunk.slice(start, breaks.cut), this.lineNumber);
|
|
1720
|
+
start = breaks.next;
|
|
1726
1721
|
}
|
|
1727
|
-
if (start <
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
tooLarge(this.report, this.lineTooLong(), this.lineNumber + 1);
|
|
1731
|
-
}
|
|
1732
|
-
pending.push(start === 0 ? chunk : chunk.slice(start));
|
|
1733
|
-
trailingCr = chunk.charCodeAt(end - 1) === 13;
|
|
1722
|
+
if (start < chunk.length) {
|
|
1723
|
+
pending.appendRest(chunk, start, this.lineNumber + 1);
|
|
1724
|
+
trailingCr = chunk.codePointAt(chunk.length - 1) === 13;
|
|
1734
1725
|
}
|
|
1735
1726
|
}
|
|
1736
|
-
if (pending.
|
|
1727
|
+
if (!pending.empty) {
|
|
1737
1728
|
this.lineNumber++;
|
|
1738
|
-
const joined = pending.
|
|
1729
|
+
const joined = pending.take();
|
|
1739
1730
|
yield joined.endsWith("\r") ? joined.slice(0, -1) : joined;
|
|
1740
1731
|
}
|
|
1741
1732
|
}
|
|
1742
1733
|
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* The pieces of a line that spans chunks. None of them holds a line break, except that the last
|
|
1737
|
+
* may end with a `\r` whose meaning the next chunk decides.
|
|
1738
|
+
*/
|
|
1739
|
+
class PendingLine {
|
|
1740
|
+
private readonly pieces: string[] = [];
|
|
1741
|
+
|
|
1742
|
+
private length = 0;
|
|
1743
|
+
|
|
1744
|
+
private readonly tooLong: (line: number) => never;
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Start an empty line.
|
|
1748
|
+
* @param tooLong - fails the import when the line grows longer than one string can hold
|
|
1749
|
+
*/
|
|
1750
|
+
constructor(tooLong: (line: number) => never) {
|
|
1751
|
+
this.tooLong = tooLong;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Whether no piece is held.
|
|
1756
|
+
* @returns true when the line in progress is empty
|
|
1757
|
+
*/
|
|
1758
|
+
get empty(): boolean {
|
|
1759
|
+
return this.pieces.length === 0;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Hold the rest of a chunk from `start`: the line goes on in the next chunk.
|
|
1764
|
+
* @param chunk - the chunk
|
|
1765
|
+
* @param start - where the unfinished line starts in it
|
|
1766
|
+
* @param line - the 1-based number of the line, for the too-long error
|
|
1767
|
+
*/
|
|
1768
|
+
appendRest(chunk: string, start: number, line: number): void {
|
|
1769
|
+
this.append(start === 0 ? chunk : chunk.slice(start), line);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* End the line with its last piece.
|
|
1774
|
+
* @param piece - the text from the start of the chunk (or the last break) to the line break
|
|
1775
|
+
* @param line - the 1-based number of the line, for the too-long error
|
|
1776
|
+
* @returns the whole line
|
|
1777
|
+
*/
|
|
1778
|
+
finish(piece: string, line: number): string {
|
|
1779
|
+
if (this.pieces.length === 0) {
|
|
1780
|
+
return piece;
|
|
1781
|
+
}
|
|
1782
|
+
this.append(piece, line);
|
|
1783
|
+
return this.take();
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Join the held pieces and start a new line.
|
|
1788
|
+
* @returns the joined text
|
|
1789
|
+
*/
|
|
1790
|
+
take(): string {
|
|
1791
|
+
const joined = this.pieces.join("");
|
|
1792
|
+
this.pieces.length = 0;
|
|
1793
|
+
this.length = 0;
|
|
1794
|
+
return joined;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/**
|
|
1798
|
+
* Hold one more piece.
|
|
1799
|
+
* @param piece - the piece
|
|
1800
|
+
* @param line - the 1-based number of the line, for the too-long error
|
|
1801
|
+
*/
|
|
1802
|
+
private append(piece: string, line: number): void {
|
|
1803
|
+
this.length += piece.length;
|
|
1804
|
+
if (this.length > MAX_TEXT_LENGTH) {
|
|
1805
|
+
this.tooLong(line);
|
|
1806
|
+
}
|
|
1807
|
+
this.pieces.push(piece);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* The line breaks of one chunk, in order: `\n`, `\r\n` and a lone `\r`. Each of `\n` and `\r` is
|
|
1813
|
+
* searched for once per chunk and again only after it was consumed, so a chunk is scanned once
|
|
1814
|
+
* whatever its line count.
|
|
1815
|
+
*/
|
|
1816
|
+
class LineBreaks {
|
|
1817
|
+
/** Where the line just found ends (its terminator starts there). */
|
|
1818
|
+
cut = 0;
|
|
1819
|
+
|
|
1820
|
+
/** Where the next line starts. */
|
|
1821
|
+
next: number;
|
|
1822
|
+
|
|
1823
|
+
private readonly chunk: string;
|
|
1824
|
+
|
|
1825
|
+
private nl: number;
|
|
1826
|
+
|
|
1827
|
+
private cr: number;
|
|
1828
|
+
|
|
1829
|
+
/**
|
|
1830
|
+
* Scan a chunk from a position.
|
|
1831
|
+
* @param chunk - the chunk
|
|
1832
|
+
* @param start - where scanning starts
|
|
1833
|
+
*/
|
|
1834
|
+
constructor(chunk: string, start: number) {
|
|
1835
|
+
this.chunk = chunk;
|
|
1836
|
+
this.next = start;
|
|
1837
|
+
this.nl = chunk.indexOf("\n", start);
|
|
1838
|
+
this.cr = chunk.indexOf("\r", start);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Find the next line break.
|
|
1843
|
+
* @returns false when none is left, or the only one left is a trailing `\r` (it may be the first
|
|
1844
|
+
* half of a `\r\n` split across chunks)
|
|
1845
|
+
*/
|
|
1846
|
+
advance(): boolean {
|
|
1847
|
+
const { chunk, next } = this;
|
|
1848
|
+
if (this.nl >= 0 && this.nl < next) {
|
|
1849
|
+
this.nl = chunk.indexOf("\n", next);
|
|
1850
|
+
}
|
|
1851
|
+
if (this.cr >= 0 && this.cr < next) {
|
|
1852
|
+
this.cr = chunk.indexOf("\r", next);
|
|
1853
|
+
}
|
|
1854
|
+
const { nl, cr } = this;
|
|
1855
|
+
if (cr >= 0 && (nl < 0 || cr < nl)) {
|
|
1856
|
+
if (cr === chunk.length - 1) {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
this.cut = cr;
|
|
1860
|
+
this.next = chunk.codePointAt(cr + 1) === 10 ? cr + 2 : cr + 1;
|
|
1861
|
+
return true;
|
|
1862
|
+
}
|
|
1863
|
+
if (nl < 0) {
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
this.cut = nl;
|
|
1867
|
+
this.next = nl + 1;
|
|
1868
|
+
return true;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
package/src/common/lists.ts
CHANGED
|
@@ -144,21 +144,9 @@ function splitBracketed(text: string): string[] {
|
|
|
144
144
|
}
|
|
145
145
|
let item = "";
|
|
146
146
|
if (i < n && (inner[i] === '"' || inner[i] === "'")) {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (inner[i] === quote) {
|
|
151
|
-
if (inner[i + 1] === quote) {
|
|
152
|
-
item += quote;
|
|
153
|
-
i += 2;
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
i++;
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
item += inner[i];
|
|
160
|
-
i++;
|
|
161
|
-
}
|
|
147
|
+
const quoted = readQuoted(inner, i);
|
|
148
|
+
item = quoted.text;
|
|
149
|
+
i = quoted.end;
|
|
162
150
|
}
|
|
163
151
|
// unquoted text, or text after a closing quote that is not a separator, runs to the next comma
|
|
164
152
|
let end = inner.indexOf(",", i);
|
|
@@ -179,6 +167,32 @@ function splitBracketed(text: string): string[] {
|
|
|
179
167
|
}
|
|
180
168
|
}
|
|
181
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Read a quoted item of a bracketed list: a doubled quote inside it is one quote, and a missing
|
|
172
|
+
* closing quote ends it at the end of the text.
|
|
173
|
+
* @param inner - the inside of the brackets
|
|
174
|
+
* @param open - the index of the opening quote
|
|
175
|
+
* @returns the unquoted text and the index just past the closing quote
|
|
176
|
+
*/
|
|
177
|
+
function readQuoted(inner: string, open: number): { text: string; end: number } {
|
|
178
|
+
const quote = inner[open];
|
|
179
|
+
const n = inner.length;
|
|
180
|
+
let text = "";
|
|
181
|
+
let i = open + 1;
|
|
182
|
+
while (i < n) {
|
|
183
|
+
if (inner[i] !== quote) {
|
|
184
|
+
text += inner[i];
|
|
185
|
+
i++;
|
|
186
|
+
} else if (inner[i + 1] === quote) {
|
|
187
|
+
text += quote;
|
|
188
|
+
i += 2;
|
|
189
|
+
} else {
|
|
190
|
+
return { text, end: i + 1 };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { text, end: i };
|
|
194
|
+
}
|
|
195
|
+
|
|
182
196
|
/**
|
|
183
197
|
* Whether a char code is ASCII whitespace.
|
|
184
198
|
* @param c - the char code
|