@graphty/graph-io 0.3.38 → 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/{choose-CsO24U-5.js → choose-CS3fvGT1.js} +2 -2
- package/dist/chunks/{choose-CsO24U-5.js.map → choose-CS3fvGT1.js.map} +1 -1
- package/dist/chunks/{constants-BPRiVyBu.js → constants-DqKZT6cH.js} +2 -2
- package/dist/chunks/{constants-BPRiVyBu.js.map → constants-DqKZT6cH.js.map} +1 -1
- package/dist/chunks/{escape-BRcyzD77.js → escape-BJRLd3vJ.js} +4 -3
- package/dist/chunks/escape-BJRLd3vJ.js.map +1 -0
- package/dist/chunks/{export-CTUlibzr.js → export-B-aVCwyG.js} +103 -92
- package/dist/chunks/export-B-aVCwyG.js.map +1 -0
- package/dist/chunks/{exporter-Bf5tZA0X.js → exporter-6ocL_9x_.js} +6 -5
- package/dist/chunks/exporter-6ocL_9x_.js.map +1 -0
- package/dist/chunks/{exporter-Bn4jdYfd.js → exporter-BVXnV9-9.js} +43 -30
- package/dist/chunks/exporter-BVXnV9-9.js.map +1 -0
- package/dist/chunks/{exporter-D-qQuRcG.js → exporter-Bcp-l4Bo.js} +6 -6
- package/dist/chunks/exporter-Bcp-l4Bo.js.map +1 -0
- package/dist/chunks/{exporter-F0vT919P.js → exporter-SFYkDzD0.js} +5 -4
- package/dist/chunks/exporter-SFYkDzD0.js.map +1 -0
- package/dist/chunks/{importer-Bgb9sgZZ.js → importer-AjwOJrn2.js} +6 -8
- package/dist/chunks/importer-AjwOJrn2.js.map +1 -0
- package/dist/chunks/{importer-BuzbaFFY.js → importer-BJlGAOht.js} +28 -13
- package/dist/chunks/importer-BJlGAOht.js.map +1 -0
- package/dist/chunks/{importer-DqMtyYLX.js → importer-BpuS3J8C.js} +12 -14
- package/dist/chunks/importer-BpuS3J8C.js.map +1 -0
- package/dist/chunks/{importer-CEuUmyPe.js → importer-CPqnIY8m.js} +6 -6
- package/dist/chunks/{importer-CEuUmyPe.js.map → importer-CPqnIY8m.js.map} +1 -1
- package/dist/chunks/{importer-yWUPR_nU.js → importer-DEp_fYjf.js} +30 -12
- package/dist/chunks/importer-DEp_fYjf.js.map +1 -0
- package/dist/chunks/{importer-CNMQnHDd.js → importer-D_srHyj6.js} +112 -62
- package/dist/chunks/importer-D_srHyj6.js.map +1 -0
- package/dist/chunks/{importer-oIUx9u_g.js → importer-DcKB97kW.js} +40 -18
- package/dist/chunks/importer-DcKB97kW.js.map +1 -0
- package/dist/chunks/{json-elements-AkglPS3m.js → json-elements-DpO6AXIc.js} +7 -5
- package/dist/chunks/{json-elements-AkglPS3m.js.map → json-elements-DpO6AXIc.js.map} +1 -1
- package/dist/chunks/{ontology-export-DDoG3egY.js → ontology-export-50L9DOV4.js} +2 -2
- package/dist/chunks/{ontology-export-DDoG3egY.js.map → ontology-export-50L9DOV4.js.map} +1 -1
- package/dist/chunks/{records-ChmWZGuz.js → records-r3SFHz11.js} +137 -96
- package/dist/chunks/records-r3SFHz11.js.map +1 -0
- package/dist/chunks/{text-Btw3sBe_.js → text-Bjfa1xYL.js} +2 -2
- package/dist/chunks/{text-Btw3sBe_.js.map → text-Bjfa1xYL.js.map} +1 -1
- package/dist/csv.js +4 -4
- 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 +6 -4
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +8 -6
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +26 -26
- 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/common/direction.js +1 -1
- package/dist/src/common/direction.js.map +1 -1
- package/dist/src/common/export.d.ts.map +1 -1
- package/dist/src/common/export.js +136 -71
- package/dist/src/common/export.js.map +1 -1
- package/dist/src/common/input.js +1 -1
- package/dist/src/common/input.js.map +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/dot/importer.d.ts +6 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +11 -6
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/dot/names.d.ts +5 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -1
- package/dist/src/formats/dot/names.js +5 -0
- package/dist/src/formats/dot/names.js.map +1 -1
- package/dist/src/formats/dot/tokenizer.js +9 -9
- package/dist/src/formats/dot/tokenizer.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/gml/importer.d.ts +6 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +6 -3
- package/dist/src/formats/gml/importer.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 +5 -6
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/obo/importer.d.ts.map +1 -1
- package/dist/src/formats/obo/importer.js +2 -1
- package/dist/src/formats/obo/importer.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +4 -5
- package/dist/src/formats/pajek/exporter.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/src/sniff.js +1 -1
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/common/direction.ts +1 -1
- package/src/common/export.ts +190 -97
- package/src/common/input.ts +1 -1
- 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/dot/importer.ts +35 -4
- package/src/formats/dot/names.ts +6 -0
- package/src/formats/dot/tokenizer.ts +9 -9
- package/src/formats/gexf/exporter.ts +4 -2
- package/src/formats/gml/importer.ts +14 -3
- package/src/formats/graphml/exporter.ts +2 -3
- package/src/formats/graphml/importer.ts +5 -6
- package/src/formats/obo/importer.ts +2 -1
- package/src/formats/pajek/exporter.ts +6 -4
- package/src/formats/xgmml/emit.ts +34 -10
- package/src/formats/xgmml/exporter.ts +63 -23
- package/src/sniff.ts +1 -1
- package/dist/chunks/escape-BRcyzD77.js.map +0 -1
- package/dist/chunks/export-CTUlibzr.js.map +0 -1
- package/dist/chunks/exporter-Bf5tZA0X.js.map +0 -1
- package/dist/chunks/exporter-Bn4jdYfd.js.map +0 -1
- package/dist/chunks/exporter-D-qQuRcG.js.map +0 -1
- package/dist/chunks/exporter-F0vT919P.js.map +0 -1
- package/dist/chunks/importer-Bgb9sgZZ.js.map +0 -1
- package/dist/chunks/importer-BuzbaFFY.js.map +0 -1
- package/dist/chunks/importer-CNMQnHDd.js.map +0 -1
- package/dist/chunks/importer-DqMtyYLX.js.map +0 -1
- package/dist/chunks/importer-oIUx9u_g.js.map +0 -1
- package/dist/chunks/importer-yWUPR_nU.js.map +0 -1
- package/dist/chunks/records-ChmWZGuz.js.map +0 -1
|
@@ -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. */
|
|
@@ -97,6 +97,7 @@ import {
|
|
|
97
97
|
PARENT_COLUMN,
|
|
98
98
|
PIN_ATTRIBUTE,
|
|
99
99
|
POS_ATTRIBUTE,
|
|
100
|
+
POSITION_COLUMN,
|
|
100
101
|
SOURCE_PORT_COLUMN,
|
|
101
102
|
TARGET_PORT_COLUMN,
|
|
102
103
|
} from "./names.js";
|
|
@@ -122,6 +123,12 @@ export interface DotImportOptions extends GraphChoiceOptions, CommonImportOption
|
|
|
122
123
|
* @defaultValue true
|
|
123
124
|
*/
|
|
124
125
|
positions?: boolean | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* With `positions`, also keep `pos` as the text the file wrote; the position column is then
|
|
128
|
+
* named `graphty.position`, so a reader gets both.
|
|
129
|
+
* @defaultValue false
|
|
130
|
+
*/
|
|
131
|
+
keepPositionSource?: boolean | undefined;
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
/**
|
|
@@ -323,7 +330,7 @@ interface Scope {
|
|
|
323
330
|
*/
|
|
324
331
|
export const dotImporter: GraphImporter<DotImportOptions> = Object.freeze({
|
|
325
332
|
format: DOT_FORMAT,
|
|
326
|
-
options: Object.freeze(["mismatchedEdgeOperator", "positions"]),
|
|
333
|
+
options: Object.freeze(["keepPositionSource", "mismatchedEdgeOperator", "positions"]),
|
|
327
334
|
extensions: EXTENSIONS,
|
|
328
335
|
mimeTypes: MIME_TYPES,
|
|
329
336
|
|
|
@@ -376,7 +383,15 @@ export const dotImporter: GraphImporter<DotImportOptions> = Object.freeze({
|
|
|
376
383
|
const reports = { current: report };
|
|
377
384
|
const lexer = dotLexer(text, reports);
|
|
378
385
|
const first = guard(report, () => lexer.next());
|
|
379
|
-
const parser = new DotParser(
|
|
386
|
+
const parser = new DotParser(
|
|
387
|
+
lexer,
|
|
388
|
+
sink,
|
|
389
|
+
report,
|
|
390
|
+
resolved,
|
|
391
|
+
mismatch,
|
|
392
|
+
options?.positions !== false,
|
|
393
|
+
options?.keepPositionSource === true,
|
|
394
|
+
);
|
|
380
395
|
parser.headCharset = head.charset;
|
|
381
396
|
const trailing = parseGraph(parser, report, first);
|
|
382
397
|
if (trailing.kind !== "eof") {
|
|
@@ -429,7 +444,15 @@ export const dotImporter: GraphImporter<DotImportOptions> = Object.freeze({
|
|
|
429
444
|
reports.current = report;
|
|
430
445
|
const sink = sinkFor(done.length);
|
|
431
446
|
reportSinkOptions(sink, options, report);
|
|
432
|
-
const parser = new DotParser(
|
|
447
|
+
const parser = new DotParser(
|
|
448
|
+
lexer,
|
|
449
|
+
sink,
|
|
450
|
+
report,
|
|
451
|
+
resolved,
|
|
452
|
+
mismatch,
|
|
453
|
+
options?.positions !== false,
|
|
454
|
+
options?.keepPositionSource === true,
|
|
455
|
+
);
|
|
433
456
|
parser.headCharset = head.charset;
|
|
434
457
|
token = parseGraph(parser, report, token);
|
|
435
458
|
throwIfAborted(resolved.signal);
|
|
@@ -686,6 +709,8 @@ class DotParser {
|
|
|
686
709
|
private readonly mismatch: "operator" | "header" | "error";
|
|
687
710
|
private readonly positions: boolean;
|
|
688
711
|
|
|
712
|
+
private readonly keepPositionSource: boolean;
|
|
713
|
+
|
|
689
714
|
private readonly ids: IdCoercer;
|
|
690
715
|
|
|
691
716
|
private readonly resolver: DirectionResolver;
|
|
@@ -758,6 +783,7 @@ class DotParser {
|
|
|
758
783
|
* @param options - the resolved common options
|
|
759
784
|
* @param mismatch - the resolved mismatchedEdgeOperator option
|
|
760
785
|
* @param positions - whether `pos` goes to the position column
|
|
786
|
+
* @param keepPositionSource - whether `pos` also stays as text
|
|
761
787
|
*/
|
|
762
788
|
constructor(
|
|
763
789
|
lexer: DotTokenizer,
|
|
@@ -766,6 +792,7 @@ class DotParser {
|
|
|
766
792
|
options: ResolvedImportOptions,
|
|
767
793
|
mismatch: "operator" | "header" | "error",
|
|
768
794
|
positions: boolean,
|
|
795
|
+
keepPositionSource: boolean,
|
|
769
796
|
) {
|
|
770
797
|
this.lexer = lexer;
|
|
771
798
|
this.sink = sink;
|
|
@@ -773,6 +800,7 @@ class DotParser {
|
|
|
773
800
|
this.options = options;
|
|
774
801
|
this.mismatch = mismatch;
|
|
775
802
|
this.positions = positions;
|
|
803
|
+
this.keepPositionSource = keepPositionSource;
|
|
776
804
|
this.ids = new IdCoercer(options.ids);
|
|
777
805
|
this.resolver = new DirectionResolver(sink, report, options.onMixedDirection);
|
|
778
806
|
}
|
|
@@ -1586,6 +1614,9 @@ class DotParser {
|
|
|
1586
1614
|
this.sink.setNodeValue(this.nodeLabel(), index, value);
|
|
1587
1615
|
} else if (name === POS_ATTRIBUTE && this.positions) {
|
|
1588
1616
|
this.setPosition(index, value, line, element);
|
|
1617
|
+
if (this.keepPositionSource) {
|
|
1618
|
+
this.textWriter("node", name).write(index, value);
|
|
1619
|
+
}
|
|
1589
1620
|
} else {
|
|
1590
1621
|
this.textWriter("node", name).write(index, value);
|
|
1591
1622
|
}
|
|
@@ -1766,7 +1797,7 @@ class DotParser {
|
|
|
1766
1797
|
private positionColumn(dims: 2 | 3): ColumnHandle {
|
|
1767
1798
|
if (this.positionHandle === INVALID_INDEX) {
|
|
1768
1799
|
this.positionHandle = this.declare("node", {
|
|
1769
|
-
name: POS_ATTRIBUTE,
|
|
1800
|
+
name: this.keepPositionSource ? POSITION_COLUMN : POS_ATTRIBUTE,
|
|
1770
1801
|
dtype: "f64",
|
|
1771
1802
|
components: 3,
|
|
1772
1803
|
nullable: true,
|
package/src/formats/dot/names.ts
CHANGED
|
@@ -54,6 +54,12 @@ export const LABEL_ATTRIBUTE = "label";
|
|
|
54
54
|
*/
|
|
55
55
|
export const POS_ATTRIBUTE = "pos";
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The node position column when the import keeps `pos` as text too (the `keepPositionSource` option).
|
|
59
|
+
* @category Plugin helpers
|
|
60
|
+
*/
|
|
61
|
+
export const POSITION_COLUMN = "graphty.position";
|
|
62
|
+
|
|
57
63
|
/**
|
|
58
64
|
* The edge attribute cgraph uses as an edge's identity within its endpoints, mapped to the edge id role.
|
|
59
65
|
* @category Plugin helpers
|
|
@@ -148,15 +148,15 @@ export class DotTokenizer {
|
|
|
148
148
|
const { line } = this;
|
|
149
149
|
const c = text.charCodeAt(start);
|
|
150
150
|
switch (c) {
|
|
151
|
-
case 0x7b: // {
|
|
152
|
-
case 0x7d: // }
|
|
153
|
-
case 0x5b: // [
|
|
154
|
-
case 0x5d: // ]
|
|
155
|
-
case 0x3b: // ;
|
|
156
|
-
case 0x2c: // ,
|
|
157
|
-
case 0x3d: // =
|
|
158
|
-
case 0x3a: // :
|
|
159
|
-
case 0x2b: // +
|
|
151
|
+
case 0x7b: // "{"
|
|
152
|
+
case 0x7d: // "}"
|
|
153
|
+
case 0x5b: // "["
|
|
154
|
+
case 0x5d: // "]"
|
|
155
|
+
case 0x3b: // ";"
|
|
156
|
+
case 0x2c: // ","
|
|
157
|
+
case 0x3d: // "="
|
|
158
|
+
case 0x3a: // ":"
|
|
159
|
+
case 0x2b: // "+"
|
|
160
160
|
this.pos++;
|
|
161
161
|
return { kind: "punct", text: text[start], quoted: false, html: false, line };
|
|
162
162
|
case 0x2d: {
|
|
@@ -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
|
}
|
|
@@ -97,6 +97,12 @@ export interface GmlImportOptions extends GraphChoiceOptions, CommonImportOption
|
|
|
97
97
|
* @defaultValue true
|
|
98
98
|
*/
|
|
99
99
|
positions?: boolean | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* With `positions`, also keep the whole `graphics` record, x / y / z included, as the JSON
|
|
102
|
+
* attribute named `graphics`, so a reader gets both the position and the record as written.
|
|
103
|
+
* @defaultValue false
|
|
104
|
+
*/
|
|
105
|
+
keepPositionSource?: boolean | undefined;
|
|
100
106
|
/**
|
|
101
107
|
* Store a text attribute whose values repeat a lot (fewer distinct values than half the rows)
|
|
102
108
|
* as a dictionary column, which uses less memory and reads the same. Such a column reports
|
|
@@ -378,6 +384,8 @@ class GmlImport {
|
|
|
378
384
|
|
|
379
385
|
private readonly positions: boolean;
|
|
380
386
|
|
|
387
|
+
private readonly keepPositionSource: boolean;
|
|
388
|
+
|
|
381
389
|
private readonly dictionaries: boolean;
|
|
382
390
|
|
|
383
391
|
private readonly coercer: IdCoercer;
|
|
@@ -470,6 +478,7 @@ class GmlImport {
|
|
|
470
478
|
this.report = report;
|
|
471
479
|
this.options = options;
|
|
472
480
|
this.positions = format?.positions ?? true;
|
|
481
|
+
this.keepPositionSource = format?.keepPositionSource ?? false;
|
|
473
482
|
this.dictionaries = format?.dictionaries ?? true;
|
|
474
483
|
this.coercer = new IdCoercer(options.ids);
|
|
475
484
|
}
|
|
@@ -755,7 +764,8 @@ class GmlImport {
|
|
|
755
764
|
}
|
|
756
765
|
const positioned = x && y;
|
|
757
766
|
entry.position ||= positioned;
|
|
758
|
-
|
|
767
|
+
// with keepPositionSource the whole record, x / y / z included, goes to the json column too
|
|
768
|
+
entry.rest ||= positioned ? other || this.keepPositionSource : true;
|
|
759
769
|
}
|
|
760
770
|
|
|
761
771
|
// ============================================================ pass 2: the push
|
|
@@ -1541,7 +1551,8 @@ class GmlImport {
|
|
|
1541
1551
|
const positionPlan = plan.position;
|
|
1542
1552
|
let rest: unknown = first;
|
|
1543
1553
|
if (isRecord(first) && positionPlan !== null) {
|
|
1544
|
-
|
|
1554
|
+
const left = this.writePosition(positionPlan, index, this.keepPositionSource ? { ...first } : first, where);
|
|
1555
|
+
rest = this.keepPositionSource ? first : left;
|
|
1545
1556
|
} else if (!isRecord(first)) {
|
|
1546
1557
|
this.graphicsWarning("scalar", `node ${where.element}'s graphics is not a record`, where);
|
|
1547
1558
|
}
|
|
@@ -1924,7 +1935,7 @@ function sniffGml(head: Uint8Array): number {
|
|
|
1924
1935
|
*/
|
|
1925
1936
|
export const gmlImporter: GraphImporter<GmlImportOptions> = Object.freeze({
|
|
1926
1937
|
format: "gml",
|
|
1927
|
-
options: Object.freeze(["dictionaries", "positions"]),
|
|
1938
|
+
options: Object.freeze(["dictionaries", "keepPositionSource", "positions"]),
|
|
1928
1939
|
extensions: Object.freeze([".gml"]),
|
|
1929
1940
|
mimeTypes: Object.freeze(["text/x-gml", "text/plain"]),
|
|
1930
1941
|
sniff: sniffGml,
|
|
@@ -1418,9 +1418,8 @@ function* writeNodeSubtree(
|
|
|
1418
1418
|
}
|
|
1419
1419
|
if (hasChildren(u)) {
|
|
1420
1420
|
parts.splice(open, 0, ">");
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
);
|
|
1421
|
+
const graphId = `${String(ids.idAt(u))}:`;
|
|
1422
|
+
parts.push(`\n${pad}${step}<graph id="${escapeXmlAttribute(graphId)}" edgedefault="${plan.edgedefault}">`);
|
|
1424
1423
|
return parts.join("");
|
|
1425
1424
|
}
|
|
1426
1425
|
if (parts.length === open) {
|
|
@@ -512,16 +512,14 @@ class GraphmlReader implements XmlHandler {
|
|
|
512
512
|
const parent = this.ctx.at(-1) ?? null;
|
|
513
513
|
switch (parent) {
|
|
514
514
|
case Ctx.Capture:
|
|
515
|
+
case Ctx.Data:
|
|
516
|
+
case Ctx.Default:
|
|
515
517
|
this.captureStart(name, attrs);
|
|
516
518
|
return;
|
|
517
519
|
case Ctx.Skip:
|
|
518
520
|
case Ctx.Desc:
|
|
519
521
|
this.ctx.push(Ctx.Skip);
|
|
520
522
|
return;
|
|
521
|
-
case Ctx.Data:
|
|
522
|
-
case Ctx.Default:
|
|
523
|
-
this.captureStart(name, attrs);
|
|
524
|
-
return;
|
|
525
523
|
case Ctx.Dropped:
|
|
526
524
|
this.dropStart(localName(name));
|
|
527
525
|
return;
|
|
@@ -2464,9 +2462,10 @@ class GraphmlReader implements XmlHandler {
|
|
|
2464
2462
|
*/
|
|
2465
2463
|
private expandStar(hyperedge: HyperedgeState, where: Where): void {
|
|
2466
2464
|
this.hyperedgeCount++;
|
|
2467
|
-
|
|
2465
|
+
const baseId = hyperedge.id ?? `hyperedge${this.hyperedgeCount}`;
|
|
2466
|
+
let hubId = baseId;
|
|
2468
2467
|
for (let n = 2; this.sink.indexOf(hubId) !== INVALID_INDEX; n++) {
|
|
2469
|
-
hubId = `${
|
|
2468
|
+
hubId = `${baseId}#${n}`;
|
|
2470
2469
|
}
|
|
2471
2470
|
const hub = this.sink.addNode(hubId);
|
|
2472
2471
|
this.declared.add(hub);
|
|
@@ -1429,7 +1429,8 @@ class OboReader {
|
|
|
1429
1429
|
return;
|
|
1430
1430
|
}
|
|
1431
1431
|
const map = (record.values.get("obo.qualifiers") as Record<string, unknown[]> | undefined) ?? {};
|
|
1432
|
-
|
|
1432
|
+
map[tag] ??= [];
|
|
1433
|
+
map[tag].push({ value, qualifiers });
|
|
1433
1434
|
record.values.set("obo.qualifiers", map);
|
|
1434
1435
|
}
|
|
1435
1436
|
|