@graphty/graph-io 0.3.19 → 0.3.21
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 +49 -4
- package/dist/chunks/{escape-CWExcecC.js → escape-B-tkk_Cl.js} +74 -16
- package/dist/chunks/escape-B-tkk_Cl.js.map +1 -0
- package/dist/chunks/{writer-DQiKgQJc.js → export-Bh60Dv-n.js} +5 -79
- package/dist/chunks/export-Bh60Dv-n.js.map +1 -0
- package/dist/chunks/{exporter-CbMZyVt-.js → exporter-BGrsWamJ.js} +7 -6
- package/dist/chunks/{exporter-CbMZyVt-.js.map → exporter-BGrsWamJ.js.map} +1 -1
- package/dist/chunks/exporter-DIeGJXAZ.js +834 -0
- package/dist/chunks/exporter-DIeGJXAZ.js.map +1 -0
- package/dist/chunks/{importer-BNFuV1-K.js → importer-BWY2FFc5.js} +28 -7
- package/dist/chunks/importer-BWY2FFc5.js.map +1 -0
- package/dist/chunks/importer-DD-xv9_Y.js +1471 -0
- package/dist/chunks/importer-DD-xv9_Y.js.map +1 -0
- package/dist/chunks/{importer-BAP4PrxR.js → importer-DET1tg6s.js} +30 -20
- package/dist/chunks/importer-DET1tg6s.js.map +1 -0
- package/dist/chunks/{importer-ByPGO-09.js → importer-DEcKhsmQ.js} +2 -2
- package/dist/chunks/{importer-ByPGO-09.js.map → importer-DEcKhsmQ.js.map} +1 -1
- package/dist/chunks/{importer-BW-Ft2ps.js → importer-DIrbbnAf.js} +6 -5
- package/dist/chunks/{importer-BW-Ft2ps.js.map → importer-DIrbbnAf.js.map} +1 -1
- package/dist/chunks/importer-D_e7e7LX.js +3055 -0
- package/dist/chunks/importer-D_e7e7LX.js.map +1 -0
- package/dist/chunks/{importer-Bm1_5vPS.js → importer-eUREhBGj.js} +11 -8
- package/dist/chunks/importer-eUREhBGj.js.map +1 -0
- package/dist/chunks/{importer-DOepkgnG.js → importer-vi3bSdtw.js} +4 -4
- package/dist/chunks/{importer-DOepkgnG.js.map → importer-vi3bSdtw.js.map} +1 -1
- package/dist/chunks/{importer-BVGtU1NA.js → importer-zGLo8Dg_.js} +6 -5
- package/dist/chunks/{importer-BVGtU1NA.js.map → importer-zGLo8Dg_.js.map} +1 -1
- package/dist/chunks/{json-elements-CZY1wiZh.js → json-elements-DDS8N2Dd.js} +2 -2
- package/dist/chunks/{json-elements-CZY1wiZh.js.map → json-elements-DDS8N2Dd.js.map} +1 -1
- package/dist/chunks/{records-BzNicMsf.js → records-dbRkxwaq.js} +2 -2
- package/dist/chunks/{records-BzNicMsf.js.map → records-dbRkxwaq.js.map} +1 -1
- package/dist/chunks/{report-BcWboivV.js → report-B1z4WT9e.js} +143 -111
- package/dist/chunks/{report-BcWboivV.js.map → report-B1z4WT9e.js.map} +1 -1
- package/dist/chunks/{weights-CwISIpCP.js → weights-Dzba96G3.js} +5 -5
- package/dist/chunks/{weights-CwISIpCP.js.map → weights-Dzba96G3.js.map} +1 -1
- package/dist/chunks/writer-C7flM-Ih.js +77 -0
- package/dist/chunks/writer-C7flM-Ih.js.map +1 -0
- package/dist/csv.js +6 -5
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.d.ts +1 -0
- package/dist/cys.js +6 -0
- package/dist/cys.js.map +1 -0
- package/dist/dot.js +1 -1
- package/dist/gexf.js +5 -4
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +12 -14
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +288 -141
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +1 -1
- package/dist/json.js +1 -1
- package/dist/neo4j.js +6 -5
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/cell-budget.d.ts +84 -0
- package/dist/src/common/cell-budget.d.ts.map +1 -0
- package/dist/src/common/cell-budget.js +138 -0
- package/dist/src/common/cell-budget.js.map +1 -0
- package/dist/src/common/input.d.ts +10 -0
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +39 -0
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/options.d.ts +6 -0
- package/dist/src/common/options.d.ts.map +1 -1
- package/dist/src/common/options.js +1 -1
- package/dist/src/common/options.js.map +1 -1
- package/dist/src/common/xml.d.ts +37 -3
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +86 -6
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/common/zip.d.ts +92 -0
- package/dist/src/common/zip.d.ts.map +1 -0
- package/dist/src/common/zip.js +399 -0
- package/dist/src/common/zip.js.map +1 -0
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +26 -17
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cys/constants.d.ts +68 -0
- package/dist/src/formats/cys/constants.d.ts.map +1 -0
- package/dist/src/formats/cys/constants.js +76 -0
- package/dist/src/formats/cys/constants.js.map +1 -0
- package/dist/src/formats/cys/importer.d.ts +38 -0
- package/dist/src/formats/cys/importer.d.ts.map +1 -0
- package/dist/src/formats/cys/importer.js +830 -0
- package/dist/src/formats/cys/importer.js.map +1 -0
- package/dist/src/formats/cys/index.d.ts +7 -0
- package/dist/src/formats/cys/index.d.ts.map +1 -0
- package/dist/src/formats/cys/index.js +7 -0
- package/dist/src/formats/cys/index.js.map +1 -0
- package/dist/src/formats/cys/session.d.ts +132 -0
- package/dist/src/formats/cys/session.d.ts.map +1 -0
- package/dist/src/formats/cys/session.js +315 -0
- package/dist/src/formats/cys/session.js.map +1 -0
- package/dist/src/formats/cys/tables.d.ts +90 -0
- package/dist/src/formats/cys/tables.d.ts.map +1 -0
- package/dist/src/formats/cys/tables.js +293 -0
- package/dist/src/formats/cys/tables.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +2 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +12 -0
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.js +6 -2
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts +2 -2
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +1 -1
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts +0 -8
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +7 -17
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +3 -1
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +3 -2
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/xgmml/columns.d.ts +152 -0
- package/dist/src/formats/xgmml/columns.d.ts.map +1 -0
- package/dist/src/formats/xgmml/columns.js +593 -0
- package/dist/src/formats/xgmml/columns.js.map +1 -0
- package/dist/src/formats/xgmml/constants.d.ts +196 -0
- package/dist/src/formats/xgmml/constants.d.ts.map +1 -0
- package/dist/src/formats/xgmml/constants.js +197 -0
- package/dist/src/formats/xgmml/constants.js.map +1 -0
- package/dist/src/formats/xgmml/document.d.ts +361 -0
- package/dist/src/formats/xgmml/document.d.ts.map +1 -0
- package/dist/src/formats/xgmml/document.js +695 -0
- package/dist/src/formats/xgmml/document.js.map +1 -0
- package/dist/src/formats/xgmml/emit.d.ts +341 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -0
- package/dist/src/formats/xgmml/emit.js +1275 -0
- package/dist/src/formats/xgmml/emit.js.map +1 -0
- package/dist/src/formats/xgmml/exporter.d.ts +24 -0
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -0
- package/dist/src/formats/xgmml/exporter.js +999 -0
- package/dist/src/formats/xgmml/exporter.js.map +1 -0
- package/dist/src/formats/xgmml/importer.d.ts +58 -0
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -0
- package/dist/src/formats/xgmml/importer.js +360 -0
- package/dist/src/formats/xgmml/importer.js.map +1 -0
- package/dist/src/formats/xgmml/index.d.ts +8 -0
- package/dist/src/formats/xgmml/index.d.ts.map +1 -0
- package/dist/src/formats/xgmml/index.js +8 -0
- package/dist/src/formats/xgmml/index.js.map +1 -0
- package/dist/src/formats/xgmml/values.d.ts +60 -0
- package/dist/src/formats/xgmml/values.d.ts.map +1 -0
- package/dist/src/formats/xgmml/values.js +148 -0
- package/dist/src/formats/xgmml/values.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry.d.ts +7 -0
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +25 -10
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts +1 -1
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js +2 -0
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.d.ts +1 -0
- package/dist/xgmml.js +9 -0
- package/dist/xgmml.js.map +1 -0
- package/package.json +15 -2
- package/src/common/cell-budget.ts +170 -0
- package/src/common/input.ts +40 -0
- package/src/common/options.ts +1 -1
- package/src/common/xml.ts +127 -6
- package/src/common/zip.ts +472 -0
- package/src/formats/cx/importer.ts +27 -19
- package/src/formats/cys/constants.ts +99 -0
- package/src/formats/cys/importer.ts +1100 -0
- package/src/formats/cys/index.ts +7 -0
- package/src/formats/cys/session.ts +428 -0
- package/src/formats/cys/tables.ts +400 -0
- package/src/formats/dot/exporter.ts +17 -0
- package/src/formats/dot/importer.ts +6 -2
- package/src/formats/gexf/importer.ts +0 -1
- package/src/formats/gml/exporter.ts +7 -18
- package/src/formats/json/exporter.ts +3 -1
- package/src/formats/json/importer.ts +3 -2
- package/src/formats/xgmml/columns.ts +747 -0
- package/src/formats/xgmml/constants.ts +265 -0
- package/src/formats/xgmml/document.ts +975 -0
- package/src/formats/xgmml/emit.ts +1563 -0
- package/src/formats/xgmml/exporter.ts +1215 -0
- package/src/formats/xgmml/importer.ts +491 -0
- package/src/formats/xgmml/index.ts +8 -0
- package/src/formats/xgmml/values.ts +183 -0
- package/src/index.ts +9 -0
- package/src/registry.ts +41 -15
- package/src/sniff.ts +5 -1
- package/dist/chunks/escape-CWExcecC.js.map +0 -1
- package/dist/chunks/importer-BAP4PrxR.js.map +0 -1
- package/dist/chunks/importer-BNFuV1-K.js.map +0 -1
- package/dist/chunks/importer-Bm1_5vPS.js.map +0 -1
- package/dist/chunks/writer-DQiKgQJc.js.map +0 -1
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attribute columns of one XGMML (or `.cys`) import: every att and every untyped XML
|
|
3
|
+
* attribute is parsed by its own declared kind when it is read, then each column's dtype is
|
|
4
|
+
* decided once over all its values (design section 5.1 widening: int and long to f64, numbers
|
|
5
|
+
* and booleans to string, lists and scalars to json), and only then declared on the sink, so a
|
|
6
|
+
* column is never re-typed after rows were written and a caller's sink needs no widening call.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type ColumnDecl, type Dtype, type GraphSink, type ScalarDtype } from "@graphty/graph-format";
|
|
10
|
+
|
|
11
|
+
import { declareResolved, DictHeuristic, uniqueColumnName } from "../../common/attributes.js";
|
|
12
|
+
import { type ImportReportBuilder, type IssueLocation } from "../../common/report.js";
|
|
13
|
+
import { inferTextDtype, parseTextCell } from "../../common/text.js";
|
|
14
|
+
import { CYTOSCAPE_ORIGIN_NAMESPACE, FORMAT, XGMML_ISSUE } from "./constants.js";
|
|
15
|
+
import { type AttRec, isListLike } from "./document.js";
|
|
16
|
+
import { attType, elementKind, parseScalar, type ScalarKind, widenScalar } from "./values.js";
|
|
17
|
+
|
|
18
|
+
/** A table of the snapshot. */
|
|
19
|
+
type Domain = "node" | "edge" | "graph";
|
|
20
|
+
|
|
21
|
+
/** One item of a list cell. */
|
|
22
|
+
interface Item {
|
|
23
|
+
readonly value: boolean | number | string;
|
|
24
|
+
readonly text: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One parsed value. */
|
|
28
|
+
interface Cell {
|
|
29
|
+
/** The kind the value was parsed as. */
|
|
30
|
+
readonly kind: ScalarKind | "list" | "json";
|
|
31
|
+
/** A scalar's value, a json value, or null for a list. */
|
|
32
|
+
readonly value: unknown;
|
|
33
|
+
/** A scalar's text as written (what a column widened to string keeps). */
|
|
34
|
+
readonly text: string;
|
|
35
|
+
/** A list's items. */
|
|
36
|
+
readonly items: readonly Item[];
|
|
37
|
+
/** A list's item kind, or null for an empty list whose type nothing states. */
|
|
38
|
+
readonly itemKind: ScalarKind | null;
|
|
39
|
+
/** The kind came from a declared type (not the text grammar of an XML attribute). */
|
|
40
|
+
readonly typed: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One column being collected. */
|
|
44
|
+
interface Plan {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly namespace: string | null;
|
|
47
|
+
readonly cells: Map<number, Cell>;
|
|
48
|
+
readonly declared: Set<string>;
|
|
49
|
+
readonly lines: Map<number, number>;
|
|
50
|
+
declaredType: string | null;
|
|
51
|
+
hidden: boolean;
|
|
52
|
+
equation: boolean;
|
|
53
|
+
suid: boolean;
|
|
54
|
+
overflow: boolean;
|
|
55
|
+
precision: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Options of a column set. */
|
|
59
|
+
interface ColumnSetOptions {
|
|
60
|
+
/** Decode Cytoscape's `\n` / `\t` escapes in string values. */
|
|
61
|
+
readonly unescape: boolean;
|
|
62
|
+
/** The importer's `long` option. */
|
|
63
|
+
readonly long: "f64" | "string";
|
|
64
|
+
/** The column names the importer's own columns hold; an att of one of these names is renamed. */
|
|
65
|
+
readonly reserved: ReadonlySet<string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The dtype of each scalar kind. */
|
|
69
|
+
const DTYPES: Readonly<Record<ScalarKind, ScalarDtype>> = {
|
|
70
|
+
bool: "bool",
|
|
71
|
+
int: "i32",
|
|
72
|
+
long: "f64",
|
|
73
|
+
real: "f64",
|
|
74
|
+
string: "string",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Collects the attribute values of one table, then declares and writes the columns.
|
|
79
|
+
*/
|
|
80
|
+
export class ColumnSet {
|
|
81
|
+
private readonly domain: Domain;
|
|
82
|
+
|
|
83
|
+
private readonly report: ImportReportBuilder;
|
|
84
|
+
|
|
85
|
+
private readonly options: ColumnSetOptions;
|
|
86
|
+
|
|
87
|
+
private readonly plans = new Map<string, Plan>();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create a column set.
|
|
91
|
+
* @param domain - the table
|
|
92
|
+
* @param report - the report
|
|
93
|
+
* @param options - the options
|
|
94
|
+
*/
|
|
95
|
+
constructor(domain: Domain, report: ImportReportBuilder, options: ColumnSetOptions) {
|
|
96
|
+
this.domain = domain;
|
|
97
|
+
this.report = report;
|
|
98
|
+
this.options = options;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether a column of that name (any namespace) was collected.
|
|
103
|
+
* @param name - the name
|
|
104
|
+
* @returns true when some row has a value or the column was declared
|
|
105
|
+
*/
|
|
106
|
+
has(name: string): boolean {
|
|
107
|
+
return this.plans.has(key(null, name));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Add one att of a row: parse it by its declared kind (E_BAD_VALUE for a value that does not
|
|
112
|
+
* parse; the cell stays unset). A second value of the same column on one row wins with
|
|
113
|
+
* W_DUPLICATE_ATTRIBUTE.
|
|
114
|
+
* @param row - the row
|
|
115
|
+
* @param att - the att
|
|
116
|
+
* @param where - the element, for issues
|
|
117
|
+
*/
|
|
118
|
+
addAtt(row: number, att: AttRec, where: IssueLocation): void {
|
|
119
|
+
const { name } = att;
|
|
120
|
+
if (name === null || name.length === 0) {
|
|
121
|
+
this.badAtt("an <att> without a name was skipped", att, "name");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const plan = this.plan(name, att.namespace ?? null);
|
|
125
|
+
const type = attType(att);
|
|
126
|
+
plan.hidden ||= att.hidden;
|
|
127
|
+
plan.suid ||= name.endsWith(".SUID");
|
|
128
|
+
if (type.declared !== null) {
|
|
129
|
+
plan.declaredType ??= type.declared;
|
|
130
|
+
}
|
|
131
|
+
const at = { line: att.line, element: where.element ?? name };
|
|
132
|
+
if (type.unknown !== null) {
|
|
133
|
+
this.report.warnOnce(
|
|
134
|
+
"unsupported",
|
|
135
|
+
XGMML_ISSUE.UNKNOWN_ATTR_TYPE,
|
|
136
|
+
`att type "${type.unknown}" of "${name}" is not an XGMML or Cytoscape type; its values are kept as text`,
|
|
137
|
+
at,
|
|
138
|
+
`${XGMML_ISSUE.UNKNOWN_ATTR_TYPE}:${this.domain}:${name}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
let cell: Cell | null;
|
|
142
|
+
if (att.equation) {
|
|
143
|
+
plan.equation = true;
|
|
144
|
+
this.report.warnOnce(
|
|
145
|
+
"unsupported",
|
|
146
|
+
XGMML_ISSUE.EQUATION_AS_TEXT,
|
|
147
|
+
`"${name}" holds Cytoscape formulas; each is kept as its text, never evaluated`,
|
|
148
|
+
at,
|
|
149
|
+
`${XGMML_ISSUE.EQUATION_AS_TEXT}:${this.domain}:${name}`,
|
|
150
|
+
);
|
|
151
|
+
cell = att.value === null ? null : scalarCell("string", att.value, att.value, true);
|
|
152
|
+
if (cell !== null) {
|
|
153
|
+
plan.declared.add("string");
|
|
154
|
+
}
|
|
155
|
+
} else if (type.kind === "list") {
|
|
156
|
+
cell = this.listCell(att, name, plan, at);
|
|
157
|
+
plan.declared.add("list");
|
|
158
|
+
} else if (type.kind === "map") {
|
|
159
|
+
cell = jsonCell(recordOf(att));
|
|
160
|
+
this.recordList(`"${name}" is a 2.x map; it is kept as a json record`, at);
|
|
161
|
+
plan.declared.add("json");
|
|
162
|
+
} else {
|
|
163
|
+
cell = this.scalarAtt(att, type.kind, plan, at);
|
|
164
|
+
}
|
|
165
|
+
if (cell !== null) {
|
|
166
|
+
this.set(plan, row, cell, at);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Add one value read by the 5.1 text grammar (an XML attribute of an element).
|
|
172
|
+
* @param row - the row
|
|
173
|
+
* @param name - the column name
|
|
174
|
+
* @param text - the text
|
|
175
|
+
* @param where - the element, for issues
|
|
176
|
+
*/
|
|
177
|
+
addText(row: number, name: string, text: string, where: IssueLocation): void {
|
|
178
|
+
const plan = this.plan(name, null);
|
|
179
|
+
const value = parseTextCell(text);
|
|
180
|
+
const kind = textKind(inferTextDtype(text));
|
|
181
|
+
this.set(plan, row, { kind, value, text, items: [], itemKind: null, typed: false }, where);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Add one value of a known kind (a typed table cell of a session).
|
|
186
|
+
* @param row - the row
|
|
187
|
+
* @param name - the column name
|
|
188
|
+
* @param value - the value
|
|
189
|
+
* @param where - the element, for issues
|
|
190
|
+
*/
|
|
191
|
+
addJson(row: number, name: string, value: unknown, where: IssueLocation): void {
|
|
192
|
+
this.set(this.plan(name, null), row, jsonCell(value), where);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Declare every collected column on the sink and write its values.
|
|
197
|
+
* @param sink - the sink
|
|
198
|
+
* @param rowIndex - the sink index of a row (node or edge index), or -1 when the row was not added
|
|
199
|
+
*/
|
|
200
|
+
write(sink: GraphSink, rowIndex: (row: number) => number): void {
|
|
201
|
+
const taken = new Set<string>(this.options.reserved);
|
|
202
|
+
const ordered = [...this.plans.values()].sort(
|
|
203
|
+
(a, b) => Number(a.namespace !== null) - Number(b.namespace !== null),
|
|
204
|
+
);
|
|
205
|
+
for (const plan of ordered) {
|
|
206
|
+
let { name } = plan;
|
|
207
|
+
if (taken.has(name)) {
|
|
208
|
+
name = uniqueColumnName(plan.name, plan.namespace, (n) => taken.has(n));
|
|
209
|
+
this.report.warning(
|
|
210
|
+
"coercion",
|
|
211
|
+
XGMML_ISSUE.COLUMN_RENAMED,
|
|
212
|
+
`${this.domain} attribute "${plan.name}" renamed to "${name}": the name was taken`,
|
|
213
|
+
{ element: plan.name },
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
taken.add(name);
|
|
217
|
+
this.writePlan(sink, plan, name, rowIndex);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ------------------------------------------------------------------ cells
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The plan of a column, created on first use.
|
|
225
|
+
* @param name - the attribute name
|
|
226
|
+
* @param namespace - the table namespace, or null
|
|
227
|
+
* @returns the plan
|
|
228
|
+
*/
|
|
229
|
+
private plan(name: string, namespace: string | null): Plan {
|
|
230
|
+
const k = key(namespace, name);
|
|
231
|
+
let plan = this.plans.get(k);
|
|
232
|
+
if (plan === undefined) {
|
|
233
|
+
plan = {
|
|
234
|
+
name,
|
|
235
|
+
namespace,
|
|
236
|
+
cells: new Map(),
|
|
237
|
+
declared: new Set(),
|
|
238
|
+
lines: new Map(),
|
|
239
|
+
declaredType: null,
|
|
240
|
+
hidden: false,
|
|
241
|
+
equation: false,
|
|
242
|
+
suid: false,
|
|
243
|
+
overflow: false,
|
|
244
|
+
precision: false,
|
|
245
|
+
};
|
|
246
|
+
this.plans.set(k, plan);
|
|
247
|
+
}
|
|
248
|
+
return plan;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Store a cell, reporting a second value on the same row.
|
|
253
|
+
* @param plan - the column
|
|
254
|
+
* @param row - the row
|
|
255
|
+
* @param cell - the cell
|
|
256
|
+
* @param where - the location
|
|
257
|
+
*/
|
|
258
|
+
private set(plan: Plan, row: number, cell: Cell, where: IssueLocation): void {
|
|
259
|
+
if (plan.cells.has(row)) {
|
|
260
|
+
this.report.warning(
|
|
261
|
+
"validation-error",
|
|
262
|
+
XGMML_ISSUE.DUPLICATE_ATTRIBUTE,
|
|
263
|
+
`"${plan.name}" is given twice on one ${this.domain}; the later value is kept`,
|
|
264
|
+
{ line: where.line ?? null, element: where.element ?? plan.name },
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
plan.cells.set(row, cell);
|
|
268
|
+
if (where.line !== undefined && where.line !== null) {
|
|
269
|
+
plan.lines.set(row, where.line);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Parse a scalar att.
|
|
275
|
+
* @param att - the att
|
|
276
|
+
* @param kind - the declared kind
|
|
277
|
+
* @param plan - the column
|
|
278
|
+
* @param at - the location
|
|
279
|
+
* @returns the cell, or null when unset or invalid
|
|
280
|
+
*/
|
|
281
|
+
private scalarAtt(att: AttRec, kind: ScalarKind, plan: Plan, at: IssueLocation): Cell | null {
|
|
282
|
+
if (att.children.length > 0) {
|
|
283
|
+
this.badAtt(`the ${kind} att "${att.name ?? ""}" holds child atts; they are ignored`, att, "children");
|
|
284
|
+
}
|
|
285
|
+
if (att.xml !== null) {
|
|
286
|
+
this.recordList(`"${att.name ?? ""}" holds foreign XML; it is kept as text in a json column`, at);
|
|
287
|
+
plan.declared.add("json");
|
|
288
|
+
return jsonCell(att.xml);
|
|
289
|
+
}
|
|
290
|
+
if (att.value === null) {
|
|
291
|
+
plan.declared.add(kind);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const parsed = parseScalar(att.value, kind, this.options.unescape);
|
|
295
|
+
if (parsed === null || (parsed.overflow !== undefined && kind === "int" && exactInteger(att.cyType))) {
|
|
296
|
+
this.badValue(att.value, kind, at);
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
plan.declared.add(kind);
|
|
300
|
+
if (parsed.overflow === "i32") {
|
|
301
|
+
plan.overflow = true;
|
|
302
|
+
} else if (parsed.overflow === "precision") {
|
|
303
|
+
plan.precision = true;
|
|
304
|
+
plan.overflow ||= kind === "int";
|
|
305
|
+
}
|
|
306
|
+
return scalarCell(kind, parsed.value, att.value, true);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Parse a list att: items by `cy:elementType`, else by the widened kinds of the children.
|
|
311
|
+
* A list of lists, a list with records, or a list of named fields is a json value.
|
|
312
|
+
* @param att - the att
|
|
313
|
+
* @param name - the column name
|
|
314
|
+
* @param plan - the column (an item beyond i32 widens it, as a scalar does)
|
|
315
|
+
* @param at - the location
|
|
316
|
+
* @returns the cell
|
|
317
|
+
*/
|
|
318
|
+
private listCell(att: AttRec, name: string, plan: Plan, at: IssueLocation): Cell | null {
|
|
319
|
+
if (att.value !== null) {
|
|
320
|
+
this.badAtt(`the list att "${name}" has a value; it is ignored`, att, "list-value");
|
|
321
|
+
}
|
|
322
|
+
const { children } = att;
|
|
323
|
+
const names = new Set(children.map((c) => c.name).filter((n): n is string => n !== null && n !== name));
|
|
324
|
+
if (names.size > 1 || children.some((c) => c.children.length > 0 || c.xml !== null)) {
|
|
325
|
+
this.recordList(`"${name}" is a record or a list of lists; it is kept as json`, at);
|
|
326
|
+
return jsonCell(names.size > 1 ? recordOf(att) : children.map(jsonOfAtt));
|
|
327
|
+
}
|
|
328
|
+
let itemKind = elementKind(att.elementType);
|
|
329
|
+
if (itemKind === null) {
|
|
330
|
+
const kinds = new Set<ScalarKind>();
|
|
331
|
+
for (const child of children) {
|
|
332
|
+
const type = attType(child);
|
|
333
|
+
const kind: ScalarKind = type.kind === "list" || type.kind === "map" ? "string" : type.kind;
|
|
334
|
+
kinds.add(kind);
|
|
335
|
+
itemKind = itemKind === null ? kind : widenScalar(itemKind, kind);
|
|
336
|
+
}
|
|
337
|
+
if (kinds.size > 1) {
|
|
338
|
+
this.report.warnOnce(
|
|
339
|
+
"coercion",
|
|
340
|
+
XGMML_ISSUE.WIDENED,
|
|
341
|
+
`the items of list "${name}" declare different types; they are read as ${itemKind ?? "string"}`,
|
|
342
|
+
at,
|
|
343
|
+
`${XGMML_ISSUE.WIDENED}:${this.domain}:${name}`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const items: Item[] = [];
|
|
348
|
+
for (const child of children) {
|
|
349
|
+
if (child.value === null) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const parsed = parseScalar(child.value, itemKind ?? "string", this.options.unescape);
|
|
353
|
+
const exact = exactInteger(att.elementType) || exactInteger(child.cyType);
|
|
354
|
+
if (parsed === null || (parsed.overflow !== undefined && itemKind === "int" && exact)) {
|
|
355
|
+
this.badValue(child.value, itemKind ?? "string", { line: child.line, element: at.element });
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (parsed.overflow === "i32") {
|
|
359
|
+
plan.overflow = true;
|
|
360
|
+
} else if (parsed.overflow === "precision") {
|
|
361
|
+
plan.precision = true;
|
|
362
|
+
plan.overflow ||= itemKind === "int";
|
|
363
|
+
}
|
|
364
|
+
items.push({ value: parsed.value, text: child.value });
|
|
365
|
+
}
|
|
366
|
+
return { kind: "list", value: null, text: "", items, itemKind, typed: true };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ------------------------------------------------------------------ decision and writing
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Decide one column's dtype, declare it and write its values.
|
|
373
|
+
* @param sink - the sink
|
|
374
|
+
* @param plan - the column
|
|
375
|
+
* @param name - the name to declare it under
|
|
376
|
+
* @param rowIndex - the row to sink index map
|
|
377
|
+
*/
|
|
378
|
+
private writePlan(sink: GraphSink, plan: Plan, name: string, rowIndex: (row: number) => number): void {
|
|
379
|
+
const cells = [...plan.cells.values()];
|
|
380
|
+
const decision = this.decide(plan, cells);
|
|
381
|
+
const decl: ColumnDecl = {
|
|
382
|
+
name,
|
|
383
|
+
dtype: decision.dtype,
|
|
384
|
+
nullable: true,
|
|
385
|
+
origin: {
|
|
386
|
+
format: FORMAT,
|
|
387
|
+
id: null,
|
|
388
|
+
title: name === plan.name ? null : plan.name,
|
|
389
|
+
type: originType(plan, decision),
|
|
390
|
+
namespace: plan.namespace ?? (plan.hidden ? CYTOSCAPE_ORIGIN_NAMESPACE : null),
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
if (decision.itemDtype !== null) {
|
|
394
|
+
decl.itemDtype = decision.itemDtype;
|
|
395
|
+
}
|
|
396
|
+
const extra: Record<string, unknown> = {};
|
|
397
|
+
if (plan.hidden) {
|
|
398
|
+
extra.hidden = true;
|
|
399
|
+
}
|
|
400
|
+
if (plan.suid) {
|
|
401
|
+
extra.suidReference = true;
|
|
402
|
+
}
|
|
403
|
+
if (plan.equation) {
|
|
404
|
+
extra.equation = true;
|
|
405
|
+
}
|
|
406
|
+
if (Object.keys(extra).length > 0) {
|
|
407
|
+
decl.extra = extra;
|
|
408
|
+
}
|
|
409
|
+
const where = { element: name };
|
|
410
|
+
if (this.domain === "graph") {
|
|
411
|
+
const cell = plan.cells.get(0);
|
|
412
|
+
try {
|
|
413
|
+
sink.setGraphValue(name, cell === undefined ? undefined : convert(cell, decision), decl);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
this.report.recordError(err, where);
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
let handle;
|
|
420
|
+
try {
|
|
421
|
+
({ handle } = declareResolved(sink, this.domain, decl, this.report, where));
|
|
422
|
+
} catch (err) {
|
|
423
|
+
this.report.recordError(err, where);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
for (const [row, cell] of plan.cells) {
|
|
427
|
+
const index = rowIndex(row);
|
|
428
|
+
if (index < 0) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const value = convert(cell, decision);
|
|
432
|
+
try {
|
|
433
|
+
if (this.domain === "node") {
|
|
434
|
+
sink.setNodeValue(handle, index, value);
|
|
435
|
+
} else {
|
|
436
|
+
sink.setEdgeValue(handle, index, value);
|
|
437
|
+
}
|
|
438
|
+
} catch (err) {
|
|
439
|
+
this.report.recordError(err, { line: plan.lines.get(row) ?? null, element: name });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The dtype of a column from its cells, with the widening and precision warnings.
|
|
446
|
+
* @param plan - the column
|
|
447
|
+
* @param cells - its cells
|
|
448
|
+
* @returns the decision
|
|
449
|
+
*/
|
|
450
|
+
private decide(plan: Plan, cells: readonly Cell[]): Decision {
|
|
451
|
+
const where = { element: plan.name };
|
|
452
|
+
const typedKinds = new Set(cells.filter((c) => c.typed).map((c) => c.kind));
|
|
453
|
+
let widened = typedKinds.size > 1 || plan.declared.size > 1;
|
|
454
|
+
let decision: Decision;
|
|
455
|
+
if (
|
|
456
|
+
cells.some((c) => c.kind === "json") ||
|
|
457
|
+
(cells.some((c) => c.kind === "list") && cells.some((c) => c.kind !== "list"))
|
|
458
|
+
) {
|
|
459
|
+
decision = { dtype: "json", itemDtype: null, scalar: null, item: null, long: false };
|
|
460
|
+
} else if (cells.length > 0 && cells.every((c) => c.kind === "list")) {
|
|
461
|
+
let item: ScalarKind | null = null;
|
|
462
|
+
for (const cell of cells) {
|
|
463
|
+
if (cell.itemKind !== null) {
|
|
464
|
+
widened ||= item !== null && item !== cell.itemKind;
|
|
465
|
+
item = item === null ? cell.itemKind : widenScalar(item, cell.itemKind);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (item === null) {
|
|
469
|
+
this.report.warnOnce(
|
|
470
|
+
"coercion",
|
|
471
|
+
XGMML_ISSUE.EMPTY_LIST_TYPE,
|
|
472
|
+
`list "${plan.name}" has no element type and no items; it is a list of strings`,
|
|
473
|
+
where,
|
|
474
|
+
`${XGMML_ISSUE.EMPTY_LIST_TYPE}:${this.domain}:${plan.name}`,
|
|
475
|
+
);
|
|
476
|
+
item = "string";
|
|
477
|
+
}
|
|
478
|
+
if (plan.overflow && item === "int") {
|
|
479
|
+
item = "real";
|
|
480
|
+
widened = true;
|
|
481
|
+
}
|
|
482
|
+
decision = { dtype: "list", itemDtype: DTYPES[item], scalar: null, item, long: item === "long" };
|
|
483
|
+
} else {
|
|
484
|
+
decision = this.decideScalar(plan, cells);
|
|
485
|
+
widened ||= plan.overflow && decision.scalar === "real" && plan.declared.has("int");
|
|
486
|
+
}
|
|
487
|
+
if (widened) {
|
|
488
|
+
this.report.warnOnce(
|
|
489
|
+
"coercion",
|
|
490
|
+
XGMML_ISSUE.WIDENED,
|
|
491
|
+
`${this.domain} column "${plan.name}" was widened to ${decision.dtype}: its values or declared types disagree`,
|
|
492
|
+
where,
|
|
493
|
+
`${XGMML_ISSUE.WIDENED}:${this.domain}:${plan.name}`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (plan.precision) {
|
|
497
|
+
this.report.warnOnce(
|
|
498
|
+
"precision",
|
|
499
|
+
XGMML_ISSUE.PRECISION,
|
|
500
|
+
`values of "${plan.name}" beyond what a double holds exactly are stored as the nearest double (or infinity)`,
|
|
501
|
+
where,
|
|
502
|
+
`${XGMML_ISSUE.PRECISION}:${this.domain}:${plan.name}`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
return decision;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* The scalar decision of a column.
|
|
510
|
+
* @param plan - the column
|
|
511
|
+
* @param cells - its cells
|
|
512
|
+
* @returns the decision
|
|
513
|
+
*/
|
|
514
|
+
private decideScalar(plan: Plan, cells: readonly Cell[]): Decision {
|
|
515
|
+
let scalar: ScalarKind | null = null;
|
|
516
|
+
for (const cell of cells) {
|
|
517
|
+
const kind = cell.kind as ScalarKind;
|
|
518
|
+
scalar = scalar === null ? kind : widenScalar(scalar, kind);
|
|
519
|
+
}
|
|
520
|
+
if (scalar === null) {
|
|
521
|
+
const declared = [...plan.declared].find((k): k is ScalarKind => k in DTYPES);
|
|
522
|
+
scalar = declared ?? "string";
|
|
523
|
+
}
|
|
524
|
+
if (scalar === "int" && plan.overflow) {
|
|
525
|
+
scalar = "real";
|
|
526
|
+
}
|
|
527
|
+
const long = scalar === "long";
|
|
528
|
+
if (long && this.options.long === "string") {
|
|
529
|
+
return { dtype: "string", itemDtype: null, scalar: "string", item: null, long: true };
|
|
530
|
+
}
|
|
531
|
+
let dtype: Dtype = DTYPES[scalar];
|
|
532
|
+
if (scalar === "string") {
|
|
533
|
+
const heuristic = new DictHeuristic();
|
|
534
|
+
for (const cell of cells) {
|
|
535
|
+
if (heuristic.observe(cell.text)) {
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
dtype = heuristic.decide();
|
|
540
|
+
}
|
|
541
|
+
return { dtype, itemDtype: null, scalar, item: null, long };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Report a malformed att, once per kind of defect.
|
|
546
|
+
* @param message - the message
|
|
547
|
+
* @param att - the att
|
|
548
|
+
* @param kind - the defect, for the once key
|
|
549
|
+
*/
|
|
550
|
+
private badAtt(message: string, att: AttRec, kind: string): void {
|
|
551
|
+
this.report.warnOnce(
|
|
552
|
+
"parse-error",
|
|
553
|
+
XGMML_ISSUE.BAD_ATT,
|
|
554
|
+
message,
|
|
555
|
+
{ line: att.line, element: att.name },
|
|
556
|
+
`${XGMML_ISSUE.BAD_ATT}:${kind}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Report a value that does not parse as its declared kind (an error; the cell stays unset).
|
|
562
|
+
* @param text - the value
|
|
563
|
+
* @param kind - the kind
|
|
564
|
+
* @param at - the location
|
|
565
|
+
*/
|
|
566
|
+
private badValue(text: string, kind: ScalarKind, at: IssueLocation): void {
|
|
567
|
+
this.report.error(
|
|
568
|
+
"validation-error",
|
|
569
|
+
XGMML_ISSUE.BAD_VALUE,
|
|
570
|
+
`value ${JSON.stringify(text)} is not a valid ${KIND_NAMES[kind]}; the cell is left unset`,
|
|
571
|
+
at,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Report a record list, list of lists, map or foreign XML kept as json, once.
|
|
577
|
+
* @param message - the message
|
|
578
|
+
* @param at - the location
|
|
579
|
+
*/
|
|
580
|
+
private recordList(message: string, at: IssueLocation): void {
|
|
581
|
+
this.report.warnOnce("coercion", XGMML_ISSUE.RECORD_LIST, message, at);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Whether a Cytoscape type names Java's Integer exactly (`cy:type="Integer"`, a CyCSV
|
|
587
|
+
* `java.lang.Integer` column): a value beyond i32 is then not an Integer at all (Cytoscape cannot
|
|
588
|
+
* write one), where the XGMML `integer` of pre-3.3 Cytoscape also carried Longs and widens.
|
|
589
|
+
* @param cyType - the Cytoscape type, or null
|
|
590
|
+
* @returns true for Integer
|
|
591
|
+
*/
|
|
592
|
+
function exactInteger(cyType: string | null): boolean {
|
|
593
|
+
return cyType?.trim().toLowerCase() === "integer";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** A column's decided storage. */
|
|
597
|
+
interface Decision {
|
|
598
|
+
readonly dtype: Dtype;
|
|
599
|
+
readonly itemDtype: ScalarDtype | null;
|
|
600
|
+
readonly scalar: ScalarKind | null;
|
|
601
|
+
readonly item: ScalarKind | null;
|
|
602
|
+
/** The column is a declared long (origin.type "long"). */
|
|
603
|
+
readonly long: boolean;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* The `origin.type` of a column: "equation" for formulas, "long" for a declared long, else the
|
|
608
|
+
* declared type text.
|
|
609
|
+
* @param plan - the column
|
|
610
|
+
* @param decision - its decision
|
|
611
|
+
* @returns the type text, or null
|
|
612
|
+
*/
|
|
613
|
+
function originType(plan: Plan, decision: Decision): string | null {
|
|
614
|
+
if (plan.equation) {
|
|
615
|
+
return "equation";
|
|
616
|
+
}
|
|
617
|
+
return decision.long ? "long" : plan.declaredType;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** How a kind is named in messages. */
|
|
621
|
+
const KIND_NAMES: Readonly<Record<ScalarKind, string>> = {
|
|
622
|
+
bool: "boolean (1, 0, true, false, yes, no)",
|
|
623
|
+
int: "integer",
|
|
624
|
+
long: "long integer",
|
|
625
|
+
real: "real number",
|
|
626
|
+
string: "string",
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* The plans key of a column.
|
|
631
|
+
* @param namespace - the namespace, or null
|
|
632
|
+
* @param name - the name
|
|
633
|
+
* @returns the key
|
|
634
|
+
*/
|
|
635
|
+
function key(namespace: string | null, name: string): string {
|
|
636
|
+
return namespace === null ? name : `${namespace}\u0000${name}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* The scalar kind of a text grammar dtype.
|
|
641
|
+
* @param dtype - the dtype
|
|
642
|
+
* @returns the kind
|
|
643
|
+
*/
|
|
644
|
+
function textKind(dtype: "bool" | "i32" | "f64" | "string"): ScalarKind {
|
|
645
|
+
switch (dtype) {
|
|
646
|
+
case "bool":
|
|
647
|
+
return "bool";
|
|
648
|
+
case "i32":
|
|
649
|
+
return "int";
|
|
650
|
+
case "f64":
|
|
651
|
+
return "real";
|
|
652
|
+
default:
|
|
653
|
+
return "string";
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* A scalar cell.
|
|
659
|
+
* @param kind - its kind
|
|
660
|
+
* @param value - its value
|
|
661
|
+
* @param text - its text as written
|
|
662
|
+
* @param typed - whether a declared type gave the kind
|
|
663
|
+
* @returns the cell
|
|
664
|
+
*/
|
|
665
|
+
function scalarCell(kind: ScalarKind, value: unknown, text: string, typed: boolean): Cell {
|
|
666
|
+
return { kind, value, text, items: [], itemKind: null, typed };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* A json cell.
|
|
671
|
+
* @param value - the value
|
|
672
|
+
* @returns the cell
|
|
673
|
+
*/
|
|
674
|
+
function jsonCell(value: unknown): Cell {
|
|
675
|
+
return { kind: "json", value, text: JSON.stringify(value), items: [], itemKind: null, typed: true };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* An att with named children as a record of name to value.
|
|
680
|
+
* @param att - the att
|
|
681
|
+
* @returns the record
|
|
682
|
+
*/
|
|
683
|
+
function recordOf(att: AttRec): Record<string, unknown> {
|
|
684
|
+
const out: Record<string, unknown> = {};
|
|
685
|
+
for (const child of att.children) {
|
|
686
|
+
out[child.name ?? ""] = jsonOfAtt(child);
|
|
687
|
+
}
|
|
688
|
+
return out;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* An att as a json value: its value text, its children as an array or a record, or its XML.
|
|
693
|
+
* @param att - the att
|
|
694
|
+
* @returns the value
|
|
695
|
+
*/
|
|
696
|
+
function jsonOfAtt(att: AttRec): unknown {
|
|
697
|
+
if (att.xml !== null) {
|
|
698
|
+
return att.xml;
|
|
699
|
+
}
|
|
700
|
+
if (att.children.length === 0) {
|
|
701
|
+
return att.value;
|
|
702
|
+
}
|
|
703
|
+
return isListLike(att) ? att.children.map(jsonOfAtt) : recordOf(att);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* A cell as the value its column's dtype stores.
|
|
708
|
+
* @param cell - the cell
|
|
709
|
+
* @param decision - the column's decision
|
|
710
|
+
* @returns the value
|
|
711
|
+
*/
|
|
712
|
+
function convert(cell: Cell, decision: Decision): unknown {
|
|
713
|
+
if (decision.dtype === "json") {
|
|
714
|
+
if (cell.kind === "json") {
|
|
715
|
+
return cell.value;
|
|
716
|
+
}
|
|
717
|
+
return cell.kind === "list" ? cell.items.map((i) => i.value) : cell.value;
|
|
718
|
+
}
|
|
719
|
+
if (decision.dtype === "list") {
|
|
720
|
+
return cell.items.map((item) => convertScalar(item.value, item.text, decision.item ?? "string"));
|
|
721
|
+
}
|
|
722
|
+
return convertScalar(
|
|
723
|
+
cell.value,
|
|
724
|
+
cell.text,
|
|
725
|
+
decision.long && decision.dtype === "string" ? "string" : (decision.scalar ?? "string"),
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* A scalar as the kind its column stores.
|
|
731
|
+
* @param value - the parsed value
|
|
732
|
+
* @param text - its text as written
|
|
733
|
+
* @param kind - the column's kind
|
|
734
|
+
* @returns the value
|
|
735
|
+
*/
|
|
736
|
+
function convertScalar(value: unknown, text: string, kind: ScalarKind): unknown {
|
|
737
|
+
switch (kind) {
|
|
738
|
+
case "bool":
|
|
739
|
+
return value;
|
|
740
|
+
case "int":
|
|
741
|
+
case "long":
|
|
742
|
+
case "real":
|
|
743
|
+
return Number(value);
|
|
744
|
+
default:
|
|
745
|
+
return typeof value === "string" ? value : text;
|
|
746
|
+
}
|
|
747
|
+
}
|