@graphty/graph-io 0.3.39 → 0.3.40
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/dist/chunks/{escape-BpYMQZVa.js → escape-BJRLd3vJ.js} +3 -2
- package/dist/chunks/escape-BJRLd3vJ.js.map +1 -0
- package/dist/chunks/{exporter-BqKs1Q12.js → exporter-6ocL_9x_.js} +5 -4
- package/dist/chunks/exporter-6ocL_9x_.js.map +1 -0
- package/dist/chunks/{exporter-DXqCPNrI.js → exporter-BVXnV9-9.js} +42 -29
- package/dist/chunks/exporter-BVXnV9-9.js.map +1 -0
- package/dist/chunks/{exporter-CNJZF_BN.js → exporter-Bcp-l4Bo.js} +2 -2
- package/dist/chunks/{exporter-CNJZF_BN.js.map → exporter-Bcp-l4Bo.js.map} +1 -1
- package/dist/chunks/{importer-CqgYq4Ld.js → importer-AjwOJrn2.js} +2 -2
- package/dist/chunks/{importer-CqgYq4Ld.js.map → importer-AjwOJrn2.js.map} +1 -1
- package/dist/chunks/{importer-BfJ1jnNr.js → importer-BJlGAOht.js} +27 -12
- package/dist/chunks/importer-BJlGAOht.js.map +1 -0
- package/dist/chunks/{importer-PVXkeoaS.js → importer-BpuS3J8C.js} +8 -8
- package/dist/chunks/importer-BpuS3J8C.js.map +1 -0
- package/dist/chunks/{importer-CSt-gaYE.js → importer-CPqnIY8m.js} +4 -4
- package/dist/chunks/{importer-CSt-gaYE.js.map → importer-CPqnIY8m.js.map} +1 -1
- package/dist/chunks/{importer-B7Ir269L.js → importer-DEp_fYjf.js} +28 -10
- package/dist/chunks/importer-DEp_fYjf.js.map +1 -0
- package/dist/chunks/{importer-DvFznt5-.js → importer-D_srHyj6.js} +111 -61
- package/dist/chunks/importer-D_srHyj6.js.map +1 -0
- package/dist/chunks/{importer--tQQsY4W.js → importer-DcKB97kW.js} +2 -2
- package/dist/chunks/{importer--tQQsY4W.js.map → importer-DcKB97kW.js.map} +1 -1
- package/dist/chunks/{json-elements-CXs4z1iL.js → json-elements-DpO6AXIc.js} +5 -3
- package/dist/chunks/{json-elements-CXs4z1iL.js.map → json-elements-DpO6AXIc.js.map} +1 -1
- package/dist/chunks/{records-C9jd7yLD.js → records-r3SFHz11.js} +136 -95
- package/dist/chunks/records-r3SFHz11.js.map +1 -0
- package/dist/csv.js +2 -2
- 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 +5 -3
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +1 -1
- package/dist/graph-io.js +19 -19
- package/dist/graphml.js +1 -1
- package/dist/json.js +1 -1
- package/dist/neo4j.js +2 -2
- package/dist/pajek.js +1 -1
- package/dist/src/common/json-elements.d.ts.map +1 -1
- package/dist/src/common/json-elements.js +4 -2
- package/dist/src/common/json-elements.js.map +1 -1
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +2 -1
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +138 -100
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +108 -53
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
- package/dist/src/formats/cx2/exporter.js +2 -1
- package/dist/src/formats/cx2/exporter.js.map +1 -1
- package/dist/src/formats/cx2/importer.d.ts.map +1 -1
- package/dist/src/formats/cx2/importer.js +28 -6
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +4 -2
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +2 -1
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +3 -2
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +6 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +33 -10
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +54 -22
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/common/json-elements.ts +4 -2
- package/src/common/xml.ts +2 -1
- package/src/formats/csv/records.ts +143 -99
- package/src/formats/cx/importer.ts +140 -58
- package/src/formats/cx2/exporter.ts +2 -1
- package/src/formats/cx2/importer.ts +34 -10
- package/src/formats/gexf/exporter.ts +4 -2
- package/src/formats/graphml/exporter.ts +2 -3
- package/src/formats/graphml/importer.ts +3 -2
- package/src/formats/xgmml/emit.ts +34 -10
- package/src/formats/xgmml/exporter.ts +63 -23
- package/dist/chunks/escape-BpYMQZVa.js.map +0 -1
- package/dist/chunks/exporter-BqKs1Q12.js.map +0 -1
- package/dist/chunks/exporter-DXqCPNrI.js.map +0 -1
- package/dist/chunks/importer-B7Ir269L.js.map +0 -1
- package/dist/chunks/importer-BfJ1jnNr.js.map +0 -1
- package/dist/chunks/importer-DvFznt5-.js.map +0 -1
- package/dist/chunks/importer-PVXkeoaS.js.map +0 -1
- package/dist/chunks/records-C9jd7yLD.js.map +0 -1
|
@@ -739,125 +739,169 @@ class RecordScanner {
|
|
|
739
739
|
* @returns the position after the last character consumed
|
|
740
740
|
*/
|
|
741
741
|
scan(chunk: string, from: number): number {
|
|
742
|
-
const {
|
|
742
|
+
const { delimiterCode } = this;
|
|
743
743
|
const n = chunk.length;
|
|
744
744
|
let i = from;
|
|
745
745
|
this.segment = from;
|
|
746
746
|
while (i < n) {
|
|
747
747
|
const c = chunk.charCodeAt(i);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
this.reader.leadingComments.push(this.comment);
|
|
752
|
-
this.reader.leadingCommentLines.push(this.line);
|
|
753
|
-
this.comment = "";
|
|
754
|
-
this.state = START;
|
|
755
|
-
this.endLine(c);
|
|
756
|
-
this.segment = i + 1;
|
|
757
|
-
}
|
|
748
|
+
const { state } = this;
|
|
749
|
+
if (state === COMMENT) {
|
|
750
|
+
this.commentChar(chunk, i, c);
|
|
758
751
|
i++;
|
|
759
752
|
continue;
|
|
760
753
|
}
|
|
761
|
-
if (
|
|
762
|
-
|
|
763
|
-
const q = chunk.indexOf(this.quoteText, i);
|
|
764
|
-
const end = q < 0 ? n : q;
|
|
765
|
-
if (end > i) {
|
|
766
|
-
this.countLineBreaks(chunk, i, end);
|
|
767
|
-
}
|
|
768
|
-
if (q < 0) {
|
|
769
|
-
i = n;
|
|
770
|
-
break;
|
|
771
|
-
}
|
|
772
|
-
this.field = this.grow(this.field, chunk.slice(this.segment, q));
|
|
773
|
-
this.state = CLOSING;
|
|
774
|
-
this.lastWasCr = false;
|
|
775
|
-
i = q + 1;
|
|
754
|
+
if (state === QUOTED) {
|
|
755
|
+
i = this.quotedRun(chunk, i);
|
|
776
756
|
continue;
|
|
777
757
|
}
|
|
778
758
|
const isDelimiter = c === delimiterCode || (this.collapse && c === TAB);
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
this.field = this.grow(this.field, this.quoteText);
|
|
782
|
-
this.segment = i + 1;
|
|
783
|
-
this.state = QUOTED;
|
|
784
|
-
this.lastWasCr = false;
|
|
785
|
-
i++;
|
|
786
|
-
continue;
|
|
787
|
-
}
|
|
788
|
-
if (!isDelimiter && c !== LF && c !== CR) {
|
|
789
|
-
this.reader.badQuote(this.startLine, this.count + 1);
|
|
790
|
-
}
|
|
791
|
-
this.state = AFTER_QUOTED;
|
|
792
|
-
this.segment = i;
|
|
793
|
-
// fall through to the delimiter / line-end handling below without consuming c
|
|
794
|
-
}
|
|
795
|
-
if (isDelimiter || c === LF || c === CR) {
|
|
796
|
-
if (c === LF && this.lastWasCr) {
|
|
797
|
-
// the second half of a CRLF already ended the record
|
|
798
|
-
this.lastWasCr = false;
|
|
799
|
-
this.segment = i + 1;
|
|
800
|
-
i++;
|
|
801
|
-
continue;
|
|
802
|
-
}
|
|
803
|
-
if (this.state === UNQUOTED) {
|
|
804
|
-
this.field = this.grow(this.field, chunk.slice(this.segment, i));
|
|
805
|
-
}
|
|
806
|
-
if (isDelimiter && this.collapse && this.state === START) {
|
|
807
|
-
// a run of whitespace, or indentation: one separator
|
|
808
|
-
this.lastWasCr = false;
|
|
809
|
-
this.segment = i + 1;
|
|
810
|
-
i++;
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
if (isDelimiter) {
|
|
814
|
-
this.push();
|
|
815
|
-
this.state = START;
|
|
816
|
-
this.lastWasCr = false;
|
|
817
|
-
this.segment = i + 1;
|
|
818
|
-
i++;
|
|
819
|
-
continue;
|
|
820
|
-
}
|
|
821
|
-
// a line ending right after a whitespace run in the whitespace dialect ends no field
|
|
822
|
-
if (!(this.state === START && (this.count === 0 || this.collapse))) {
|
|
823
|
-
this.push();
|
|
824
|
-
}
|
|
825
|
-
if (this.count > 0) {
|
|
826
|
-
this.ready = true;
|
|
827
|
-
this.recordLine = this.startLine;
|
|
828
|
-
}
|
|
829
|
-
this.state = START;
|
|
830
|
-
this.endLine(c);
|
|
831
|
-
this.segment = i + 1;
|
|
832
|
-
i++;
|
|
759
|
+
if (state === CLOSING || isDelimiter || c === LF || c === CR) {
|
|
760
|
+
i = this.boundary(chunk, i, c, isDelimiter);
|
|
833
761
|
if (this.ready) {
|
|
834
762
|
return i;
|
|
835
763
|
}
|
|
836
764
|
continue;
|
|
837
765
|
}
|
|
766
|
+
this.fieldChar(c, i);
|
|
767
|
+
i++;
|
|
768
|
+
}
|
|
769
|
+
return i;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* One character of a leading comment line: a line break ends the comment.
|
|
774
|
+
* @param chunk - the chunk
|
|
775
|
+
* @param i - the character's index
|
|
776
|
+
* @param c - the character's code
|
|
777
|
+
*/
|
|
778
|
+
private commentChar(chunk: string, i: number, c: number): void {
|
|
779
|
+
if (c !== LF && c !== CR) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
this.comment = this.grow(this.comment, chunk.slice(this.segment, i));
|
|
783
|
+
this.reader.leadingComments.push(this.comment);
|
|
784
|
+
this.reader.leadingCommentLines.push(this.line);
|
|
785
|
+
this.comment = "";
|
|
786
|
+
this.state = START;
|
|
787
|
+
this.endLine(c);
|
|
788
|
+
this.segment = i + 1;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Inside a quoted field: jump to the next quote; the run in between is field text whose line
|
|
793
|
+
* breaks are counted.
|
|
794
|
+
* @param chunk - the chunk
|
|
795
|
+
* @param i - where the run starts
|
|
796
|
+
* @returns the position after the quote, or the chunk end when there is none
|
|
797
|
+
*/
|
|
798
|
+
private quotedRun(chunk: string, i: number): number {
|
|
799
|
+
const q = chunk.indexOf(this.quoteText, i);
|
|
800
|
+
const end = q < 0 ? chunk.length : q;
|
|
801
|
+
if (end > i) {
|
|
802
|
+
this.countLineBreaks(chunk, i, end);
|
|
803
|
+
}
|
|
804
|
+
if (q < 0) {
|
|
805
|
+
return chunk.length;
|
|
806
|
+
}
|
|
807
|
+
this.field = this.grow(this.field, chunk.slice(this.segment, q));
|
|
808
|
+
this.state = CLOSING;
|
|
809
|
+
this.lastWasCr = false;
|
|
810
|
+
return q + 1;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* The character after a quote inside a quoted field, a delimiter or a line break: a doubled
|
|
815
|
+
* quote, the end of a field, or the end of a record.
|
|
816
|
+
* @param chunk - the chunk
|
|
817
|
+
* @param i - the character's index
|
|
818
|
+
* @param c - the character's code
|
|
819
|
+
* @param isDelimiter - whether it separates fields
|
|
820
|
+
* @returns the position after the character
|
|
821
|
+
*/
|
|
822
|
+
private boundary(chunk: string, i: number, c: number, isDelimiter: boolean): number {
|
|
823
|
+
if (this.state === CLOSING) {
|
|
824
|
+
if (c === this.quoteCode) {
|
|
825
|
+
this.field = this.grow(this.field, this.quoteText);
|
|
826
|
+
this.segment = i + 1;
|
|
827
|
+
this.state = QUOTED;
|
|
828
|
+
this.lastWasCr = false;
|
|
829
|
+
return i + 1;
|
|
830
|
+
}
|
|
831
|
+
if (!isDelimiter && c !== LF && c !== CR) {
|
|
832
|
+
this.reader.badQuote(this.startLine, this.count + 1);
|
|
833
|
+
}
|
|
834
|
+
this.state = AFTER_QUOTED;
|
|
835
|
+
this.segment = i;
|
|
836
|
+
}
|
|
837
|
+
if (c === LF && this.lastWasCr) {
|
|
838
|
+
// the second half of a CRLF already ended the record
|
|
838
839
|
this.lastWasCr = false;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
840
|
+
this.segment = i + 1;
|
|
841
|
+
return i + 1;
|
|
842
|
+
}
|
|
843
|
+
if (this.state === UNQUOTED) {
|
|
844
|
+
this.field = this.grow(this.field, chunk.slice(this.segment, i));
|
|
845
|
+
}
|
|
846
|
+
if (isDelimiter) {
|
|
847
|
+
// in the whitespace dialect a run of whitespace, or indentation, is one separator
|
|
848
|
+
if (!(this.collapse && this.state === START)) {
|
|
849
|
+
this.push();
|
|
850
|
+
this.state = START;
|
|
851
|
+
}
|
|
852
|
+
this.lastWasCr = false;
|
|
853
|
+
this.segment = i + 1;
|
|
854
|
+
return i + 1;
|
|
855
|
+
}
|
|
856
|
+
this.lineEnd(c);
|
|
857
|
+
this.segment = i + 1;
|
|
858
|
+
return i + 1;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* A line break that ends a record (or a blank line).
|
|
863
|
+
* @param c - the LF or CR
|
|
864
|
+
*/
|
|
865
|
+
private lineEnd(c: number): void {
|
|
866
|
+
// a line ending right after a whitespace run in the whitespace dialect ends no field
|
|
867
|
+
if (!(this.state === START && (this.count === 0 || this.collapse))) {
|
|
868
|
+
this.push();
|
|
869
|
+
}
|
|
870
|
+
if (this.count > 0) {
|
|
871
|
+
this.ready = true;
|
|
872
|
+
this.recordLine = this.startLine;
|
|
873
|
+
}
|
|
874
|
+
this.state = START;
|
|
875
|
+
this.endLine(c);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Any other character outside a quoted field: the start of a field or a comment, or field text.
|
|
880
|
+
* @param c - the character's code
|
|
881
|
+
* @param i - its index
|
|
882
|
+
*/
|
|
883
|
+
private fieldChar(c: number, i: number): void {
|
|
884
|
+
this.lastWasCr = false;
|
|
885
|
+
if (this.state !== START) {
|
|
886
|
+
if (c === this.quoteCode && this.state === UNQUOTED && this.reader.strayQuoteLine === 0) {
|
|
856
887
|
this.reader.strayQuoteLine = this.startLine;
|
|
857
888
|
}
|
|
858
|
-
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (this.leading && this.count === 0 && this.commentCodes.includes(c)) {
|
|
892
|
+
this.state = COMMENT;
|
|
893
|
+
this.segment = i;
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
this.leading = false;
|
|
897
|
+
if (c === this.quoteCode) {
|
|
898
|
+
this.state = QUOTED;
|
|
899
|
+
this.fieldQuoted = true;
|
|
900
|
+
this.segment = i + 1;
|
|
901
|
+
} else {
|
|
902
|
+
this.state = UNQUOTED;
|
|
903
|
+
this.segment = i;
|
|
859
904
|
}
|
|
860
|
-
return i;
|
|
861
905
|
}
|
|
862
906
|
|
|
863
907
|
/**
|
|
@@ -773,12 +773,13 @@ function checkNumberVerification(value: unknown, count: number, report: ImportRe
|
|
|
773
773
|
const raw = isRecord(value) ? value.longNumber : undefined;
|
|
774
774
|
const digits = raw instanceof ExactInteger ? raw.digits : String(raw);
|
|
775
775
|
if (count > 1 || !NUMBER_VERIFICATION_VALUES.has(digits)) {
|
|
776
|
+
const held = raw === undefined ? null : digits;
|
|
776
777
|
report.warning(
|
|
777
778
|
"validation-error",
|
|
778
779
|
CX_ISSUE.NUMBER_VERIFICATION,
|
|
779
780
|
count > 1
|
|
780
781
|
? "a second numberVerification element; ignored"
|
|
781
|
-
: `numberVerification holds ${shown(
|
|
782
|
+
: `numberVerification holds ${shown(held)}, not 281474976710655`,
|
|
782
783
|
{ line, element: "numberVerification" },
|
|
783
784
|
);
|
|
784
785
|
}
|
|
@@ -993,10 +994,12 @@ async function readDocument(
|
|
|
993
994
|
break;
|
|
994
995
|
}
|
|
995
996
|
countMember(event.block, null);
|
|
997
|
+
const what =
|
|
998
|
+
keys.length > 1 ? `an object with ${keys.length} keys (${keys.join(", ")})` : shown(value);
|
|
996
999
|
report.error(
|
|
997
1000
|
"parse-error",
|
|
998
1001
|
BAD_ASPECT_BLOCK_CODE,
|
|
999
|
-
`a member of the document is ${
|
|
1002
|
+
`a member of the document is ${what}, not a one-key aspect fragment; skipped`,
|
|
1000
1003
|
{ line: event.line, element: keys[0] ?? null },
|
|
1001
1004
|
);
|
|
1002
1005
|
break;
|
|
@@ -1361,6 +1364,26 @@ function memberCount(members: ReadonlySet<NodeId> | null, total: number): number
|
|
|
1361
1364
|
|
|
1362
1365
|
// ============================================================ the build
|
|
1363
1366
|
|
|
1367
|
+
/** The cyVisualProperties of a graph's view, gathered before they are written. */
|
|
1368
|
+
interface VisualCollection {
|
|
1369
|
+
/** per domain, per property: the [row, text] cells */
|
|
1370
|
+
columns: Record<"node" | "edge", Map<string, [number, string][]>>;
|
|
1371
|
+
/** the network values by property */
|
|
1372
|
+
network: Map<string, string>;
|
|
1373
|
+
defaults: number;
|
|
1374
|
+
mappings: number;
|
|
1375
|
+
dependencies: number;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* A visual property value as text: a string as it is, anything else as JSON.
|
|
1380
|
+
* @param raw - the value
|
|
1381
|
+
* @returns its text
|
|
1382
|
+
*/
|
|
1383
|
+
function visualText(raw: unknown): string {
|
|
1384
|
+
return typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1364
1387
|
/** One attribute column of the graph being built. */
|
|
1365
1388
|
interface AttributeColumn {
|
|
1366
1389
|
readonly name: string;
|
|
@@ -2849,57 +2872,99 @@ class CxReader {
|
|
|
2849
2872
|
* applied.
|
|
2850
2873
|
*/
|
|
2851
2874
|
private readVisualProperties(): void {
|
|
2852
|
-
const entries = aspect(this.doc, "cyVisualProperties");
|
|
2853
2875
|
// Cytoscape's table-cell styles: a visual property aspect graph-io keeps but does not read
|
|
2854
2876
|
const tableStyles = TABLE_STYLE_ASPECTS.reduce((n, name) => n + (this.doc.kept.get(name)?.length ?? 0), 0);
|
|
2855
2877
|
// the graph's own view (its first): the one its positions come from; the others stay in meta.extra.cx
|
|
2856
2878
|
const views = new Set(this.plan.views.slice(0, 1));
|
|
2857
|
-
const
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
if (view !== null && views.size > 0 && !views.has(view)) {
|
|
2868
|
-
continue;
|
|
2879
|
+
const visual: VisualCollection = {
|
|
2880
|
+
columns: { node: new Map(), edge: new Map() },
|
|
2881
|
+
network: new Map(),
|
|
2882
|
+
defaults: 0,
|
|
2883
|
+
mappings: 0,
|
|
2884
|
+
dependencies: 0,
|
|
2885
|
+
};
|
|
2886
|
+
for (const { value } of aspect(this.doc, "cyVisualProperties")) {
|
|
2887
|
+
if (isRecord(value)) {
|
|
2888
|
+
this.collectVisualEntry(value, views, visual);
|
|
2869
2889
|
}
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2890
|
+
}
|
|
2891
|
+
this.writeBypassColumns(visual.columns);
|
|
2892
|
+
this.writeBypassNetwork(visual.network);
|
|
2893
|
+
this.reportStyleRules(visual, tableStyles);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
/**
|
|
2897
|
+
* File one cyVisualProperties element of this graph's view into the collection: per-element and
|
|
2898
|
+
* network values by property, defaults, mappings and dependencies counted.
|
|
2899
|
+
* @param value - the element
|
|
2900
|
+
* @param views - the views read (empty: every view)
|
|
2901
|
+
* @param visual - the collection to add to
|
|
2902
|
+
*/
|
|
2903
|
+
private collectVisualEntry(
|
|
2904
|
+
value: Record<string, unknown>,
|
|
2905
|
+
views: ReadonlySet<NodeId>,
|
|
2906
|
+
visual: VisualCollection,
|
|
2907
|
+
): void {
|
|
2908
|
+
const view = value.view === undefined || value.view === null ? null : refId(value.view);
|
|
2909
|
+
if (view !== null && views.size > 0 && !views.has(view)) {
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
const properties = isRecord(value.properties) ? value.properties : {};
|
|
2913
|
+
visual.mappings += isRecord(value.mappings) ? Object.keys(value.mappings).length : 0;
|
|
2914
|
+
visual.dependencies += isRecord(value.dependencies) ? Object.keys(value.dependencies).length : 0;
|
|
2915
|
+
switch (value.properties_of) {
|
|
2916
|
+
case "nodes":
|
|
2917
|
+
this.collectElementProperties("node", value.applies_to, properties, visual.columns);
|
|
2918
|
+
break;
|
|
2919
|
+
case "edges":
|
|
2920
|
+
this.collectElementProperties("edge", value.applies_to, properties, visual.columns);
|
|
2921
|
+
break;
|
|
2922
|
+
case "network":
|
|
2923
|
+
for (const [property, raw] of Object.entries(properties)) {
|
|
2924
|
+
visual.network.set(property, visualText(raw));
|
|
2892
2925
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2926
|
+
break;
|
|
2927
|
+
default:
|
|
2928
|
+
visual.defaults += Object.keys(properties).length;
|
|
2929
|
+
break;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
/**
|
|
2934
|
+
* File the visual values of one node or edge under its row; an element not in this graph is
|
|
2935
|
+
* skipped, and reported dangling when it is in no graph of the file.
|
|
2936
|
+
* @param domain - node or edge
|
|
2937
|
+
* @param appliesTo - the element's applies_to
|
|
2938
|
+
* @param properties - its visual properties
|
|
2939
|
+
* @param columns - the per-property cells to add to
|
|
2940
|
+
*/
|
|
2941
|
+
private collectElementProperties(
|
|
2942
|
+
domain: "node" | "edge",
|
|
2943
|
+
appliesTo: unknown,
|
|
2944
|
+
properties: Record<string, unknown>,
|
|
2945
|
+
columns: VisualCollection["columns"],
|
|
2946
|
+
): void {
|
|
2947
|
+
const target = refId(appliesTo);
|
|
2948
|
+
const rows = domain === "node" ? this.nodeRows : this.edgeRows;
|
|
2949
|
+
const row = target === null ? undefined : rows.get(target);
|
|
2950
|
+
if (row === undefined) {
|
|
2951
|
+
if (target === null || !(domain === "node" ? this.rootNodes : this.rootEdges).has(target)) {
|
|
2952
|
+
this.dangle(`${domain} visual property entry`);
|
|
2901
2953
|
}
|
|
2954
|
+
return;
|
|
2902
2955
|
}
|
|
2956
|
+
for (const [property, raw] of Object.entries(properties)) {
|
|
2957
|
+
const list = columns[domain].get(property) ?? [];
|
|
2958
|
+
list.push([row, visualText(raw)]);
|
|
2959
|
+
columns[domain].set(property, list);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
/**
|
|
2964
|
+
* Write the collected per-element visual values as string columns in the cx.bypass namespace.
|
|
2965
|
+
* @param columns - the per-property cells of each domain
|
|
2966
|
+
*/
|
|
2967
|
+
private writeBypassColumns(columns: VisualCollection["columns"]): void {
|
|
2903
2968
|
for (const domain of ["node", "edge"] as const) {
|
|
2904
2969
|
for (const [property, cells] of columns[domain]) {
|
|
2905
2970
|
const handle = declareFresh(
|
|
@@ -2922,6 +2987,13 @@ class CxReader {
|
|
|
2922
2987
|
}
|
|
2923
2988
|
}
|
|
2924
2989
|
}
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
/**
|
|
2993
|
+
* Write the network visual values as graph values in the cx.bypass namespace.
|
|
2994
|
+
* @param network - the value of each property
|
|
2995
|
+
*/
|
|
2996
|
+
private writeBypassNetwork(network: ReadonlyMap<string, string>): void {
|
|
2925
2997
|
for (const [property, text] of network) {
|
|
2926
2998
|
try {
|
|
2927
2999
|
this.sink.setGraphValue(property, text, {
|
|
@@ -2932,20 +3004,30 @@ class CxReader {
|
|
|
2932
3004
|
this.report.recordError(err, { element: property });
|
|
2933
3005
|
}
|
|
2934
3006
|
}
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
{ element: "cyVisualProperties" },
|
|
2947
|
-
);
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
/**
|
|
3010
|
+
* Warn that the style rules (defaults, mappings, dependencies, table styles) are kept, not applied.
|
|
3011
|
+
* @param visual - the counted rules
|
|
3012
|
+
* @param tableStyles - the number of table style elements
|
|
3013
|
+
*/
|
|
3014
|
+
private reportStyleRules(visual: VisualCollection, tableStyles: number): void {
|
|
3015
|
+
const { defaults, mappings, dependencies } = visual;
|
|
3016
|
+
if (defaults + mappings + dependencies + tableStyles === 0) {
|
|
3017
|
+
return;
|
|
2948
3018
|
}
|
|
3019
|
+
const parts = [
|
|
3020
|
+
`cyVisualProperties: ${defaults} default${plural(defaults)}, ${mappings} mapping${plural(mappings)}, ${dependencies} dependenc(ies)`,
|
|
3021
|
+
];
|
|
3022
|
+
if (tableStyles > 0) {
|
|
3023
|
+
parts.push(`${tableStyles} table style element${plural(tableStyles)}`);
|
|
3024
|
+
}
|
|
3025
|
+
this.report.warning(
|
|
3026
|
+
"unsupported",
|
|
3027
|
+
STYLES_NOT_IMPORTED_CODE,
|
|
3028
|
+
`the file's style rules are not applied (${parts.join("; ")}); they are kept in meta.extra.cx`,
|
|
3029
|
+
{ element: "cyVisualProperties" },
|
|
3030
|
+
);
|
|
2949
3031
|
}
|
|
2950
3032
|
|
|
2951
3033
|
/**
|
|
@@ -304,9 +304,10 @@ function capabilityNotes(
|
|
|
304
304
|
continue;
|
|
305
305
|
}
|
|
306
306
|
if (gen.code === LOSS.JSON) {
|
|
307
|
+
const what = gen.column === null ? "a column" : `column "${gen.column}"`;
|
|
307
308
|
note(
|
|
308
309
|
CX2_LOSS.JSON_AS_STRING,
|
|
309
|
-
`${
|
|
310
|
+
`${what} holds nested values; written as a string attribute holding their JSON text`,
|
|
310
311
|
gen.column,
|
|
311
312
|
gen.count,
|
|
312
313
|
);
|
|
@@ -688,6 +688,25 @@ async function readDocument(
|
|
|
688
688
|
return doc;
|
|
689
689
|
}
|
|
690
690
|
|
|
691
|
+
/**
|
|
692
|
+
* Fail the import: the first member of the array is not a descriptor.
|
|
693
|
+
* @param value - the member
|
|
694
|
+
* @param line - its line
|
|
695
|
+
* @param report - the report
|
|
696
|
+
* @returns never: the import fails
|
|
697
|
+
*/
|
|
698
|
+
function failNoDescriptor(value: unknown, line: number, report: ImportReportBuilder): never {
|
|
699
|
+
const keys = isRecord(value) ? Object.keys(value) : [];
|
|
700
|
+
const keyList = keys.length > 0 ? ` (keys: ${keys.join(", ")})` : "";
|
|
701
|
+
return report.fail(
|
|
702
|
+
CX2_ISSUE.NO_DESCRIPTOR,
|
|
703
|
+
`the first member of the array is not a descriptor with CXVersion${keyList}`,
|
|
704
|
+
{
|
|
705
|
+
line,
|
|
706
|
+
},
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
691
710
|
/**
|
|
692
711
|
* Check the descriptor, the first member of the array.
|
|
693
712
|
* @param value - the member
|
|
@@ -697,12 +716,7 @@ async function readDocument(
|
|
|
697
716
|
*/
|
|
698
717
|
function readDescriptor(value: unknown, line: number, doc: Cx2Document, report: ImportReportBuilder): void {
|
|
699
718
|
if (!isRecord(value) || !("CXVersion" in value)) {
|
|
700
|
-
|
|
701
|
-
report.fail(
|
|
702
|
-
CX2_ISSUE.NO_DESCRIPTOR,
|
|
703
|
-
`the first member of the array is not a descriptor with CXVersion${keys.length > 0 ? ` (keys: ${keys.join(", ")})` : ""}`,
|
|
704
|
-
{ line },
|
|
705
|
-
);
|
|
719
|
+
failNoDescriptor(value, line, report);
|
|
706
720
|
}
|
|
707
721
|
const raw = value.CXVersion;
|
|
708
722
|
const text = typeof raw === "string" || typeof raw === "number" ? String(raw) : "";
|
|
@@ -779,10 +793,11 @@ function memberAspect(
|
|
|
779
793
|
collect(keys[0], value[keys[0]]);
|
|
780
794
|
return;
|
|
781
795
|
}
|
|
796
|
+
const what = keys.length > 1 ? `an object with ${keys.length} keys (${keys.join(", ")})` : shown(value);
|
|
782
797
|
report.error(
|
|
783
798
|
"parse-error",
|
|
784
799
|
BAD_ASPECT_BLOCK_CODE,
|
|
785
|
-
`a member of the document is ${
|
|
800
|
+
`a member of the document is ${what}, not a one-key aspect block; skipped`,
|
|
786
801
|
{ line, element: keys[0] ?? null },
|
|
787
802
|
);
|
|
788
803
|
}
|
|
@@ -1325,10 +1340,11 @@ class Cx2Reader {
|
|
|
1325
1340
|
}
|
|
1326
1341
|
const other = table.byName.get(alias) ?? table.byAlias.get(alias);
|
|
1327
1342
|
if (other !== undefined) {
|
|
1343
|
+
const clash = other.name === alias ? "an attribute's name" : `the alias of "${other.name}"`;
|
|
1328
1344
|
this.report.error(
|
|
1329
1345
|
"validation-error",
|
|
1330
1346
|
CX2_ISSUE.ALIAS_CONFLICT,
|
|
1331
|
-
`the alias "${alias}" of the ${domain} attribute "${decl.name}" is also ${
|
|
1347
|
+
`the alias "${alias}" of the ${domain} attribute "${decl.name}" is also ${clash}; the alias is ignored`,
|
|
1332
1348
|
{ element: decl.name },
|
|
1333
1349
|
);
|
|
1334
1350
|
decl.alias = null;
|
|
@@ -2259,6 +2275,14 @@ class Cx2Reader {
|
|
|
2259
2275
|
}
|
|
2260
2276
|
}
|
|
2261
2277
|
}
|
|
2278
|
+
this.checkStatus();
|
|
2279
|
+
structure.checkCounts();
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
/** The status block: present, and exactly one well-formed element. */
|
|
2283
|
+
private checkStatus(): void {
|
|
2284
|
+
const { report } = this;
|
|
2285
|
+
const { structure } = this.doc;
|
|
2262
2286
|
if (!structure.hasStatus) {
|
|
2263
2287
|
report.error("validation-error", CX2_ISSUE.NO_STATUS, "the document ends without a status block", {
|
|
2264
2288
|
element: "status",
|
|
@@ -2266,14 +2290,14 @@ class Cx2Reader {
|
|
|
2266
2290
|
} else if (!structure.statusWellFormed() || structure.counts.get("status") !== 1) {
|
|
2267
2291
|
// the specification: exactly one status aspect holding exactly one element
|
|
2268
2292
|
const count = structure.counts.get("status") ?? 0;
|
|
2293
|
+
const found = count === 1 ? shown(structure.status) : `${count} elements`;
|
|
2269
2294
|
report.error(
|
|
2270
2295
|
"validation-error",
|
|
2271
2296
|
CX2_ISSUE.NO_STATUS,
|
|
2272
|
-
`the status is malformed (${
|
|
2297
|
+
`the status is malformed (${found}): a document ends with one status block holding one object with a boolean success`,
|
|
2273
2298
|
{ element: "status" },
|
|
2274
2299
|
);
|
|
2275
2300
|
}
|
|
2276
|
-
structure.checkCounts();
|
|
2277
2301
|
}
|
|
2278
2302
|
|
|
2279
2303
|
/** Record the metadata: source format and version, name, description, id type, the kept aspects. */
|
|
@@ -1225,7 +1225,8 @@ function lifetimeAttrs(roles: RoleColumns, row: number, plan: ExportPlan): strin
|
|
|
1225
1225
|
const text = timeText(column, roles.companions.get(column.meta.name) ?? null, row, timeFormat);
|
|
1226
1226
|
// GEXF 1.2: `startopen` / `endopen` hold the time of a non-inclusive bound (dynamics.xsd
|
|
1227
1227
|
// time-type) and replace `start` / `end`
|
|
1228
|
-
|
|
1228
|
+
const attribute = open ? `${name}open` : name;
|
|
1229
|
+
out += ` ${attribute}="${escapeXmlAttribute(text)}"`;
|
|
1229
1230
|
};
|
|
1230
1231
|
if (roles.timestamp?.isSet(row)) {
|
|
1231
1232
|
if (version === "1.3" && plan.timeRepresentation === "timestamp") {
|
|
@@ -1240,7 +1241,8 @@ function lifetimeAttrs(roles: RoleColumns, row: number, plan: ExportPlan): strin
|
|
|
1240
1241
|
}
|
|
1241
1242
|
if (version === "1.3" && roles.timestamps?.isSet(row)) {
|
|
1242
1243
|
const items = listItems(roles.timestamps, row).map((t) => formatTimeValue(t as number, timeFormat));
|
|
1243
|
-
|
|
1244
|
+
const stamps = `<[${items.join(", ")}]>`;
|
|
1245
|
+
out += ` timestamps="${escapeXmlAttribute(stamps)}"`;
|
|
1244
1246
|
}
|
|
1245
1247
|
return out;
|
|
1246
1248
|
}
|