@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
|
@@ -385,6 +385,13 @@ interface AdjacencyPlan {
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
/** Everything one import call shares between its tables. */
|
|
388
|
+
/** A row read before the plan exists, kept to be pushed once it does. */
|
|
389
|
+
interface PendingRow {
|
|
390
|
+
readonly row: string[];
|
|
391
|
+
readonly quoted: readonly boolean[];
|
|
392
|
+
readonly line: number;
|
|
393
|
+
}
|
|
394
|
+
|
|
388
395
|
interface ImportState {
|
|
389
396
|
readonly sink: GraphSink;
|
|
390
397
|
readonly report: ImportReportBuilder;
|
|
@@ -455,6 +462,43 @@ function isPadded(text: string, quoted: boolean | undefined): boolean {
|
|
|
455
462
|
*/
|
|
456
463
|
function resolveCsvOptions(options: (CsvImportOptions & CommonImportOptions) | undefined): ResolvedCsvOptions {
|
|
457
464
|
const o: CsvImportOptions = options ?? {};
|
|
465
|
+
checkDialectOptions(o);
|
|
466
|
+
for (const name of ["sourceColumn", "targetColumn", "idColumn", "labelColumn"] as const) {
|
|
467
|
+
checkColumnRef(name, o[name]);
|
|
468
|
+
}
|
|
469
|
+
if (o.typeColumn !== null) {
|
|
470
|
+
checkColumnRef("typeColumn", o.typeColumn);
|
|
471
|
+
}
|
|
472
|
+
if (o.table === "adjacency") {
|
|
473
|
+
checkAdjacencyOptions(o);
|
|
474
|
+
}
|
|
475
|
+
if (o.rowNumberIds !== undefined && typeof o.rowNumberIds !== "boolean") {
|
|
476
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: expected a boolean", {
|
|
477
|
+
option: "rowNumberIds",
|
|
478
|
+
found: o.rowNumberIds,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
delimiter: o.delimiter ?? null,
|
|
483
|
+
decimalComma: o.decimal === ",",
|
|
484
|
+
header: o.header ?? "auto",
|
|
485
|
+
table: o.table ?? "auto",
|
|
486
|
+
sourceColumn: o.sourceColumn ?? null,
|
|
487
|
+
targetColumn: o.targetColumn ?? null,
|
|
488
|
+
typeColumn: o.typeColumn,
|
|
489
|
+
idColumn: o.idColumn ?? null,
|
|
490
|
+
labelColumn: o.labelColumn ?? null,
|
|
491
|
+
nodes: nodesInput(o.nodes),
|
|
492
|
+
rowNumberIds: o.rowNumberIds ?? false,
|
|
493
|
+
explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Check the dialect and table options: delimiter, decimal, header and table.
|
|
499
|
+
* @param o - the caller's CSV options
|
|
500
|
+
*/
|
|
501
|
+
function checkDialectOptions(o: CsvImportOptions): void {
|
|
458
502
|
if (o.delimiter !== undefined && (typeof o.delimiter !== "string" || o.delimiter.length === 0)) {
|
|
459
503
|
throw new GraphFormatError("E_UNSUPPORTED", "option delimiter: expected a non-empty string", {
|
|
460
504
|
option: "delimiter",
|
|
@@ -489,48 +533,27 @@ function resolveCsvOptions(options: (CsvImportOptions & CommonImportOptions) | u
|
|
|
489
533
|
found: o.table,
|
|
490
534
|
});
|
|
491
535
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (o.rowNumberIds === true) {
|
|
508
|
-
throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: an adjacency table names its nodes", {
|
|
509
|
-
option: "rowNumberIds",
|
|
510
|
-
found: o.rowNumberIds,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Check the options an adjacency table has no use for: column options and rowNumberIds.
|
|
540
|
+
* @param o - the caller's CSV options
|
|
541
|
+
*/
|
|
542
|
+
function checkAdjacencyOptions(o: CsvImportOptions): void {
|
|
543
|
+
for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn", "labelColumn"] as const) {
|
|
544
|
+
if (o[name] !== undefined) {
|
|
545
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
|
|
546
|
+
option: name,
|
|
547
|
+
found: o[name],
|
|
511
548
|
});
|
|
512
549
|
}
|
|
513
550
|
}
|
|
514
|
-
if (o.rowNumberIds
|
|
515
|
-
throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds:
|
|
551
|
+
if (o.rowNumberIds === true) {
|
|
552
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: an adjacency table names its nodes", {
|
|
516
553
|
option: "rowNumberIds",
|
|
517
554
|
found: o.rowNumberIds,
|
|
518
555
|
});
|
|
519
556
|
}
|
|
520
|
-
return {
|
|
521
|
-
delimiter: o.delimiter ?? null,
|
|
522
|
-
decimalComma: o.decimal === ",",
|
|
523
|
-
header: o.header ?? "auto",
|
|
524
|
-
table: o.table ?? "auto",
|
|
525
|
-
sourceColumn: o.sourceColumn ?? null,
|
|
526
|
-
targetColumn: o.targetColumn ?? null,
|
|
527
|
-
typeColumn: o.typeColumn,
|
|
528
|
-
idColumn: o.idColumn ?? null,
|
|
529
|
-
labelColumn: o.labelColumn ?? null,
|
|
530
|
-
nodes: nodesInput(o.nodes),
|
|
531
|
-
rowNumberIds: o.rowNumberIds ?? false,
|
|
532
|
-
explicitDirected: typeof options?.defaultDirected === "boolean" ? options.defaultDirected : null,
|
|
533
|
-
};
|
|
534
557
|
}
|
|
535
558
|
|
|
536
559
|
/**
|
|
@@ -823,43 +846,12 @@ class TableReader {
|
|
|
823
846
|
: first.value;
|
|
824
847
|
const firstLine = this.reader.line;
|
|
825
848
|
const firstQuoted = this.reader.quoted.slice(0, firstRow.length);
|
|
826
|
-
const pending:
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
mode = false;
|
|
833
|
-
} else if (mode === "auto" && this.rowNumbers) {
|
|
834
|
-
mode = true;
|
|
835
|
-
}
|
|
836
|
-
if (mode === "auto") {
|
|
837
|
-
const second = await iterator.next();
|
|
838
|
-
const secondRow: string[] | null = second.done ? null : second.value;
|
|
839
|
-
const secondLine = this.reader.line;
|
|
840
|
-
const secondQuoted = this.reader.quoted.slice(0, secondRow?.length ?? 0);
|
|
841
|
-
// a column option naming a cell of the first row makes that row the header
|
|
842
|
-
header = looksLikeHeader(firstRow, secondRow) || this.namesExplicitColumn(firstRow);
|
|
843
|
-
if (!header) {
|
|
844
|
-
pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
|
|
845
|
-
}
|
|
846
|
-
if (secondRow !== null) {
|
|
847
|
-
pending.push({ row: secondRow, quoted: secondQuoted, line: secondLine });
|
|
848
|
-
}
|
|
849
|
-
} else {
|
|
850
|
-
header = mode;
|
|
851
|
-
if (!header) {
|
|
852
|
-
pending.push({ row: firstRow, quoted: firstQuoted, line: firstLine });
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
if (this.rowNumbers && !header) {
|
|
856
|
-
report.warning(
|
|
857
|
-
"unsupported",
|
|
858
|
-
OPTION_IGNORED_CODE,
|
|
859
|
-
"option rowNumberIds ignored: a headerless node table takes its ids from column 0",
|
|
860
|
-
{ line: firstLine, element: "rowNumberIds" },
|
|
861
|
-
);
|
|
862
|
-
}
|
|
849
|
+
const pending: PendingRow[] = [];
|
|
850
|
+
const header = await this.decideHeader(
|
|
851
|
+
iterator,
|
|
852
|
+
{ row: firstRow, quoted: firstQuoted, line: firstLine },
|
|
853
|
+
pending,
|
|
854
|
+
);
|
|
863
855
|
const names = header ? uniqueNames(headerNames(firstRow), report, firstLine) : positionalNames(firstRow.length);
|
|
864
856
|
const last = firstRow.length - 1;
|
|
865
857
|
const optionalLast = header && last > 0 && isUnset(firstRow[last], firstQuoted[last]);
|
|
@@ -889,6 +881,80 @@ class TableReader {
|
|
|
889
881
|
for (const { row, quoted, line } of pending) {
|
|
890
882
|
this.processRow(row, quoted, line);
|
|
891
883
|
}
|
|
884
|
+
await this.readRemainingRows(iterator);
|
|
885
|
+
if (this.dataRows === 0 && header) {
|
|
886
|
+
report.warning("missing-value", NO_DATA_ROWS_CODE, "the table has a header and no data rows", {
|
|
887
|
+
line: firstLine,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
this.reportReaderFindings();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Decide whether the first row is the header, reading the second row when the header option is
|
|
895
|
+
* "auto", and queue the rows read so far that are data.
|
|
896
|
+
* @param iterator - the record iterator, past the first row
|
|
897
|
+
* @param first - the first row
|
|
898
|
+
* @param pending - receives the data rows read so far, in order
|
|
899
|
+
* @returns whether the first row is the header
|
|
900
|
+
*/
|
|
901
|
+
private async decideHeader(
|
|
902
|
+
iterator: AsyncGenerator<string[], void, undefined>,
|
|
903
|
+
first: PendingRow,
|
|
904
|
+
pending: PendingRow[],
|
|
905
|
+
): Promise<boolean> {
|
|
906
|
+
const mode = this.headerMode();
|
|
907
|
+
let second: PendingRow | null = null;
|
|
908
|
+
let header: boolean;
|
|
909
|
+
if (mode === "auto") {
|
|
910
|
+
const next = await iterator.next();
|
|
911
|
+
second = next.done
|
|
912
|
+
? null
|
|
913
|
+
: { row: next.value, quoted: this.reader.quoted.slice(0, next.value.length), line: this.reader.line };
|
|
914
|
+
// a column option naming a cell of the first row makes that row the header
|
|
915
|
+
header = looksLikeHeader(first.row, second?.row ?? null) || this.namesExplicitColumn(first.row);
|
|
916
|
+
} else {
|
|
917
|
+
header = mode;
|
|
918
|
+
}
|
|
919
|
+
if (!header) {
|
|
920
|
+
pending.push(first);
|
|
921
|
+
}
|
|
922
|
+
if (second !== null) {
|
|
923
|
+
pending.push(second);
|
|
924
|
+
}
|
|
925
|
+
if (this.rowNumbers && !header) {
|
|
926
|
+
this.state.report.warning(
|
|
927
|
+
"unsupported",
|
|
928
|
+
OPTION_IGNORED_CODE,
|
|
929
|
+
"option rowNumberIds ignored: a headerless node table takes its ids from column 0",
|
|
930
|
+
{ line: first.line, element: "rowNumberIds" },
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
return header;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* The header option as this table reads it: an adjacency table has no header unless the caller
|
|
938
|
+
* says so (its rows vary in width); a table numbered by rowNumberIds has one, since a headerless
|
|
939
|
+
* table takes its ids from column 0.
|
|
940
|
+
* @returns true, false or "auto"
|
|
941
|
+
*/
|
|
942
|
+
private headerMode(): boolean | "auto" {
|
|
943
|
+
const mode = this.state.csv.header;
|
|
944
|
+
if (mode === "auto" && this.kind === "adjacency") {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
if (mode === "auto" && this.rowNumbers) {
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
return mode;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Push every row after the header and the rows read to decide it, then finish the column writers.
|
|
955
|
+
* @param iterator - the record iterator
|
|
956
|
+
*/
|
|
957
|
+
private async readRemainingRows(iterator: AsyncGenerator<string[], void, undefined>): Promise<void> {
|
|
892
958
|
const { signal } = this.state.common;
|
|
893
959
|
let sinceCheck = 0;
|
|
894
960
|
for (;;) {
|
|
@@ -905,12 +971,6 @@ class TableReader {
|
|
|
905
971
|
for (const writer of this.writers) {
|
|
906
972
|
writer?.finish();
|
|
907
973
|
}
|
|
908
|
-
if (this.dataRows === 0 && header) {
|
|
909
|
-
report.warning("missing-value", NO_DATA_ROWS_CODE, "the table has a header and no data rows", {
|
|
910
|
-
line: firstLine,
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
this.reportReaderFindings();
|
|
914
974
|
}
|
|
915
975
|
|
|
916
976
|
/**
|
|
@@ -586,14 +586,8 @@ export class RecordReader implements AsyncIterable<number> {
|
|
|
586
586
|
}
|
|
587
587
|
const pieces: string[] = [];
|
|
588
588
|
let length = 0;
|
|
589
|
-
let breaks = 0;
|
|
590
|
-
let lastWasCr = false;
|
|
591
589
|
let sniffed = false;
|
|
592
|
-
|
|
593
|
-
const commentCodes = (this.syntax.comments ?? []).map((c) => c.codePointAt(0));
|
|
594
|
-
let leading = commentCodes.length > 0;
|
|
595
|
-
let lineStart = true;
|
|
596
|
-
let inComment = false;
|
|
590
|
+
const preview = new PreviewBreaks((this.syntax.comments ?? []).map((c) => c.codePointAt(0)));
|
|
597
591
|
for await (const chunk of source) {
|
|
598
592
|
if (sniffed) {
|
|
599
593
|
yield chunk;
|
|
@@ -601,24 +595,8 @@ export class RecordReader implements AsyncIterable<number> {
|
|
|
601
595
|
}
|
|
602
596
|
pieces.push(chunk);
|
|
603
597
|
length += chunk.length;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (c === LF || c === CR) {
|
|
607
|
-
if (!(c === LF && lastWasCr) && !inComment) {
|
|
608
|
-
breaks++;
|
|
609
|
-
}
|
|
610
|
-
inComment = false;
|
|
611
|
-
lineStart = true;
|
|
612
|
-
} else {
|
|
613
|
-
if (lineStart && leading) {
|
|
614
|
-
inComment = commentCodes.includes(c);
|
|
615
|
-
leading = inComment;
|
|
616
|
-
}
|
|
617
|
-
lineStart = false;
|
|
618
|
-
}
|
|
619
|
-
lastWasCr = c === CR;
|
|
620
|
-
}
|
|
621
|
-
if (breaks < PREVIEW_ROWS && length < PREVIEW_CHARS) {
|
|
598
|
+
preview.scan(chunk);
|
|
599
|
+
if (preview.breaks < PREVIEW_ROWS && length < PREVIEW_CHARS) {
|
|
622
600
|
continue;
|
|
623
601
|
}
|
|
624
602
|
const text = pieces.join("");
|
|
@@ -668,6 +646,74 @@ export class RecordReader implements AsyncIterable<number> {
|
|
|
668
646
|
}
|
|
669
647
|
}
|
|
670
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Counts the line breaks of the text buffered for the delimiter sniff, up to PREVIEW_ROWS. The
|
|
651
|
+
* leading comment lines are left out of the sniff, so they do not count toward its rows.
|
|
652
|
+
*/
|
|
653
|
+
class PreviewBreaks {
|
|
654
|
+
/** The line breaks counted so far (at most PREVIEW_ROWS). */
|
|
655
|
+
breaks = 0;
|
|
656
|
+
|
|
657
|
+
private readonly commentCodes: readonly (number | undefined)[];
|
|
658
|
+
|
|
659
|
+
private lastWasCr = false;
|
|
660
|
+
|
|
661
|
+
private leading: boolean;
|
|
662
|
+
|
|
663
|
+
private lineStart = true;
|
|
664
|
+
|
|
665
|
+
private inComment = false;
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Start counting at the start of the input.
|
|
669
|
+
* @param commentCodes - the code points of the comment characters
|
|
670
|
+
*/
|
|
671
|
+
constructor(commentCodes: readonly (number | undefined)[]) {
|
|
672
|
+
this.commentCodes = commentCodes;
|
|
673
|
+
this.leading = commentCodes.length > 0;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Count the line breaks of the next chunk, stopping at PREVIEW_ROWS.
|
|
678
|
+
* @param chunk - the chunk
|
|
679
|
+
*/
|
|
680
|
+
scan(chunk: string): void {
|
|
681
|
+
for (let i = 0; i < chunk.length && this.breaks < PREVIEW_ROWS; i++) {
|
|
682
|
+
const c = chunk.charCodeAt(i);
|
|
683
|
+
if (c === LF || c === CR) {
|
|
684
|
+
this.lineBreak(c);
|
|
685
|
+
} else {
|
|
686
|
+
this.character(c);
|
|
687
|
+
}
|
|
688
|
+
this.lastWasCr = c === CR;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* A line break: counted unless it is the LF of a CR LF or ends a leading comment line.
|
|
694
|
+
* @param c - LF or CR
|
|
695
|
+
*/
|
|
696
|
+
private lineBreak(c: number): void {
|
|
697
|
+
if (!(c === LF && this.lastWasCr) && !this.inComment) {
|
|
698
|
+
this.breaks++;
|
|
699
|
+
}
|
|
700
|
+
this.inComment = false;
|
|
701
|
+
this.lineStart = true;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Any other character: the first of a line decides whether a leading comment line starts.
|
|
706
|
+
* @param c - the character code
|
|
707
|
+
*/
|
|
708
|
+
private character(c: number): void {
|
|
709
|
+
if (this.lineStart && this.leading) {
|
|
710
|
+
this.inComment = this.commentCodes.includes(c);
|
|
711
|
+
this.leading = this.inComment;
|
|
712
|
+
}
|
|
713
|
+
this.lineStart = false;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
671
717
|
/**
|
|
672
718
|
* The record state machine of RecordReader, resumable at every record end so the reader can
|
|
673
719
|
* yield a record before the next one overwrites the shared cell arrays. Every state lives on the
|
|
@@ -87,6 +87,16 @@ function isDigit(c: number): boolean {
|
|
|
87
87
|
return c >= 48 && c <= 57;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Whether a code unit is a blank between tokens: space, tab, vertical tab, form feed, or a U+FEFF
|
|
92
|
+
* after the start (concatenated files), which the shared text check reports.
|
|
93
|
+
* @param c - the code unit
|
|
94
|
+
* @returns true for a blank
|
|
95
|
+
*/
|
|
96
|
+
function isBlank(c: number): boolean {
|
|
97
|
+
return c === 0x20 || c === 0x09 || c === 0x0b || c === 0x0c || c === BOM_CODE;
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
/**
|
|
91
101
|
* A pull lexer over one DOT document with arbitrary lookahead.
|
|
92
102
|
* @category Plugin helpers
|
|
@@ -247,40 +257,68 @@ export class DotTokenizer {
|
|
|
247
257
|
this.pos = i + 1;
|
|
248
258
|
return { kind: "id", text: out, quoted: true, html: false, line };
|
|
249
259
|
}
|
|
250
|
-
if (c === 0x5c) {
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
out +=
|
|
254
|
-
i += 2;
|
|
255
|
-
segment = i;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (d === 0x5c) {
|
|
259
|
-
// Graphviz's scanner consumes a backslash pair as one unit (both kept), so the
|
|
260
|
-
// second backslash never escapes a closing quote: "x\\" is the text x\\
|
|
261
|
-
i += 2;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (d === 0x0a || d === 0x0d) {
|
|
265
|
-
out += text.slice(segment, i);
|
|
266
|
-
i += d === 0x0d && text.charCodeAt(i + 2) === 0x0a ? 3 : 2;
|
|
267
|
-
this.line++;
|
|
268
|
-
segment = i;
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
i++;
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
if (c === 0x0a || c === 0x0d) {
|
|
275
|
-
if (c === 0x0d && text.charCodeAt(i + 1) === 0x0a) {
|
|
276
|
-
i++;
|
|
260
|
+
if (c === 0x5c && this.isQuotedSplice(i)) {
|
|
261
|
+
out += text.slice(segment, i);
|
|
262
|
+
if (text.codePointAt(i + 1) === 0x22) {
|
|
263
|
+
out += '"';
|
|
277
264
|
}
|
|
278
|
-
this.
|
|
265
|
+
i = this.quotedSpliceEnd(i);
|
|
266
|
+
segment = i;
|
|
267
|
+
} else {
|
|
268
|
+
i = this.quotedStep(i, c);
|
|
279
269
|
}
|
|
280
|
-
i++;
|
|
281
270
|
}
|
|
282
271
|
}
|
|
283
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Whether the backslash at `i` inside a quoted string starts a sequence that changes the decoded
|
|
275
|
+
* text: `\"` (an escaped quote) or a backslash before a line break (a line continuation).
|
|
276
|
+
* @param i - the offset of the backslash
|
|
277
|
+
* @returns true for an escaped quote or a line continuation
|
|
278
|
+
*/
|
|
279
|
+
private isQuotedSplice(i: number): boolean {
|
|
280
|
+
const d = this.text.codePointAt(i + 1);
|
|
281
|
+
return d === 0x22 || d === 0x0a || d === 0x0d;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The offset just past an escaped quote or a line continuation, counting the line a
|
|
286
|
+
* continuation ends.
|
|
287
|
+
* @param i - the offset of the backslash
|
|
288
|
+
* @returns the offset after the sequence
|
|
289
|
+
*/
|
|
290
|
+
private quotedSpliceEnd(i: number): number {
|
|
291
|
+
const { text } = this;
|
|
292
|
+
const d = text.charCodeAt(i + 1);
|
|
293
|
+
if (d === 0x22) {
|
|
294
|
+
return i + 2;
|
|
295
|
+
}
|
|
296
|
+
this.line++;
|
|
297
|
+
return d === 0x0d && text.codePointAt(i + 2) === 0x0a ? i + 3 : i + 2;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Step over one unit of a quoted string kept as written: a backslash pair (consumed as one unit,
|
|
302
|
+
* as Graphviz's scanner does, so the second backslash never escapes a closing quote: "x\\" is
|
|
303
|
+
* the text x\\), a lone backslash, a raw line break (counted) or any other character.
|
|
304
|
+
* @param i - the offset of the unit
|
|
305
|
+
* @param c - the code unit at `i`
|
|
306
|
+
* @returns the offset after the unit
|
|
307
|
+
*/
|
|
308
|
+
private quotedStep(i: number, c: number): number {
|
|
309
|
+
const { text } = this;
|
|
310
|
+
if (c === 0x5c) {
|
|
311
|
+
return text.codePointAt(i + 1) === 0x5c ? i + 2 : i + 1;
|
|
312
|
+
}
|
|
313
|
+
if (c === 0x0a) {
|
|
314
|
+
this.line++;
|
|
315
|
+
} else if (c === 0x0d) {
|
|
316
|
+
this.line++;
|
|
317
|
+
return text.codePointAt(i + 1) === 0x0a ? i + 2 : i + 1;
|
|
318
|
+
}
|
|
319
|
+
return i + 1;
|
|
320
|
+
}
|
|
321
|
+
|
|
284
322
|
/**
|
|
285
323
|
* Lex an HTML string starting at the opening angle bracket: the text up to the matching
|
|
286
324
|
* closing bracket, brackets balanced, kept verbatim including the outer pair.
|
|
@@ -313,51 +351,63 @@ export class DotTokenizer {
|
|
|
313
351
|
*/
|
|
314
352
|
private skipTrivia(): void {
|
|
315
353
|
const { text } = this;
|
|
316
|
-
|
|
317
|
-
if (this.pos >= text.length) {
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
354
|
+
while (this.pos < text.length) {
|
|
320
355
|
const c = text.charCodeAt(this.pos);
|
|
321
|
-
if (c
|
|
322
|
-
this.line++;
|
|
323
|
-
this.pos++;
|
|
324
|
-
this.skipPreprocessorLine();
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (c === 0x0d) {
|
|
328
|
-
this.line++;
|
|
329
|
-
this.pos += text.charCodeAt(this.pos + 1) === 0x0a ? 2 : 1;
|
|
330
|
-
this.skipPreprocessorLine();
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (c === 0x20 || c === 0x09 || c === 0x0b || c === 0x0c) {
|
|
334
|
-
this.pos++;
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
if (c === BOM_CODE) {
|
|
338
|
-
// a U+FEFF after the start (concatenated files) is whitespace; the shared text check reports it
|
|
356
|
+
if (isBlank(c)) {
|
|
339
357
|
this.pos++;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (c === 0x2f) {
|
|
343
|
-
const d = text.charCodeAt(this.pos + 1);
|
|
344
|
-
if (d === 0x2f) {
|
|
345
|
-
this.skipToLineEnd();
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
if (d === 0x2a) {
|
|
349
|
-
this.skipBlockComment();
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
if (c === 0x23 && this.pos === 0) {
|
|
354
|
-
this.skipToLineEnd();
|
|
355
|
-
continue;
|
|
358
|
+
} else if (!this.skipBreakOrComment(c)) {
|
|
359
|
+
return;
|
|
356
360
|
}
|
|
357
|
-
return;
|
|
358
361
|
}
|
|
359
362
|
}
|
|
360
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Skip one line break, comment or leading `#` line at the current position.
|
|
366
|
+
* @param c - the code unit at the current position
|
|
367
|
+
* @returns false when the current position starts a token
|
|
368
|
+
*/
|
|
369
|
+
private skipBreakOrComment(c: number): boolean {
|
|
370
|
+
if (c === 0x0a || c === 0x0d) {
|
|
371
|
+
this.skipLineBreak(c);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
if (c === 0x2f) {
|
|
375
|
+
return this.skipComment();
|
|
376
|
+
}
|
|
377
|
+
if (c === 0x23 && this.pos === 0) {
|
|
378
|
+
this.skipToLineEnd();
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Consume the line break at the current position (CR LF as one) and a `#` line after it.
|
|
386
|
+
* @param c - the code unit at the current position, LF or CR
|
|
387
|
+
*/
|
|
388
|
+
private skipLineBreak(c: number): void {
|
|
389
|
+
this.line++;
|
|
390
|
+
this.pos += c === 0x0d && this.text.codePointAt(this.pos + 1) === 0x0a ? 2 : 1;
|
|
391
|
+
this.skipPreprocessorLine();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Skip a `//` or block comment starting at the current `/`.
|
|
396
|
+
* @returns false when the `/` starts no comment
|
|
397
|
+
*/
|
|
398
|
+
private skipComment(): boolean {
|
|
399
|
+
const d = this.text.codePointAt(this.pos + 1);
|
|
400
|
+
if (d === 0x2f) {
|
|
401
|
+
this.skipToLineEnd();
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
if (d === 0x2a) {
|
|
405
|
+
this.skipBlockComment();
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
361
411
|
/**
|
|
362
412
|
* Discard a `#` line when one starts at the current position (the start of a line).
|
|
363
413
|
*/
|