gofish-ir 0.1.0
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 +56 -0
- package/dist/frontend/examples.d.ts +48 -0
- package/dist/frontend/examples.d.ts.map +1 -0
- package/dist/frontend/examples.js +140 -0
- package/dist/frontend/index.d.ts +5 -0
- package/dist/frontend/index.d.ts.map +1 -0
- package/dist/frontend/index.js +4 -0
- package/dist/frontend/jsonSchema.d.ts +393 -0
- package/dist/frontend/jsonSchema.d.ts.map +1 -0
- package/dist/frontend/jsonSchema.js +288 -0
- package/dist/frontend/schema.d.ts +255 -0
- package/dist/frontend/schema.d.ts.map +1 -0
- package/dist/frontend/schema.js +69 -0
- package/dist/frontend/v0.json +506 -0
- package/dist/frontend/validate.d.ts +27 -0
- package/dist/frontend/validate.d.ts.map +1 -0
- package/dist/frontend/validate.js +591 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
// <gofish-wiki> AUTO-GENERATED — see covers: in the essay; run `pnpm --filter docs sync-backlinks`
|
|
2
|
+
// @wiki Frontend IR — /internals/frontend/serialization
|
|
3
|
+
// </gofish-wiki>
|
|
4
|
+
/**
|
|
5
|
+
* Runtime validator for the GoFish frontend IR.
|
|
6
|
+
*
|
|
7
|
+
* Hand-rolled, dependency-free. Strict mode rejects unknown fields (used in
|
|
8
|
+
* CI and tests); the default permissive mode ignores them, suitable for
|
|
9
|
+
* forward-compatible reading.
|
|
10
|
+
*/
|
|
11
|
+
import { COMBINATOR_MARK_TYPES, LEAF_MARK_TYPES, OPERATOR_TYPES, } from "./schema.js";
|
|
12
|
+
/** Validate a document against the frontend-IR schema. */
|
|
13
|
+
export function validate(doc, options = {}) {
|
|
14
|
+
const ctx = {
|
|
15
|
+
strict: options.strict === true,
|
|
16
|
+
errors: [],
|
|
17
|
+
};
|
|
18
|
+
walkDocument(doc, "$", ctx);
|
|
19
|
+
return ctx.errors.length === 0
|
|
20
|
+
? { valid: true }
|
|
21
|
+
: { valid: false, errors: ctx.errors };
|
|
22
|
+
}
|
|
23
|
+
function walkDocument(node, path, ctx) {
|
|
24
|
+
if (!isObject(node)) {
|
|
25
|
+
ctx.errors.push({ path, message: "expected object" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
expectField(node, "irVersion", path, ctx, (v, p) => {
|
|
29
|
+
if (v !== 0)
|
|
30
|
+
ctx.errors.push({
|
|
31
|
+
path: p,
|
|
32
|
+
message: `irVersion must be 0, got ${JSON.stringify(v)}`,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
expectField(node, "ir", path, ctx, (v, p) => {
|
|
36
|
+
if (v !== "gofish-frontend")
|
|
37
|
+
ctx.errors.push({
|
|
38
|
+
path: p,
|
|
39
|
+
message: `ir must be "gofish-frontend", got ${JSON.stringify(v)}`,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
optionalField(node, "$schema", path, ctx, expectString);
|
|
43
|
+
expectField(node, "root", path, ctx, walkRoot);
|
|
44
|
+
if (ctx.strict) {
|
|
45
|
+
rejectUnknown(node, ["irVersion", "ir", "$schema", "root"], path, ctx);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function walkRoot(node, path, ctx) {
|
|
49
|
+
if (!isObject(node)) {
|
|
50
|
+
ctx.errors.push({ path, message: "expected object" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
switch (node.type) {
|
|
54
|
+
case "chart":
|
|
55
|
+
walkChart(node, path, ctx);
|
|
56
|
+
return;
|
|
57
|
+
case "layer":
|
|
58
|
+
walkLayer(node, path, ctx);
|
|
59
|
+
return;
|
|
60
|
+
case "raw-mark":
|
|
61
|
+
walkRawMark(node, path, ctx);
|
|
62
|
+
return;
|
|
63
|
+
default:
|
|
64
|
+
ctx.errors.push({
|
|
65
|
+
path: `${path}.type`,
|
|
66
|
+
message: `root type must be "chart" | "layer" | "raw-mark", got ${JSON.stringify(node.type)}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function walkChart(node, path, ctx) {
|
|
71
|
+
walkBaseFields(node, path, ctx);
|
|
72
|
+
optionalField(node, "data", path, ctx, (v, p) => {
|
|
73
|
+
if (v === null)
|
|
74
|
+
return;
|
|
75
|
+
walkData(v, p, ctx);
|
|
76
|
+
});
|
|
77
|
+
optionalField(node, "operators", path, ctx, (v, p) => walkArray(v, p, ctx, walkOperator));
|
|
78
|
+
expectField(node, "mark", path, ctx, walkMark);
|
|
79
|
+
optionalField(node, "options", path, ctx, expectObject);
|
|
80
|
+
optionalField(node, "zOrder", path, ctx, expectNumber);
|
|
81
|
+
if (ctx.strict) {
|
|
82
|
+
rejectUnknown(node, [
|
|
83
|
+
"type",
|
|
84
|
+
"data",
|
|
85
|
+
"operators",
|
|
86
|
+
"mark",
|
|
87
|
+
"options",
|
|
88
|
+
"zOrder",
|
|
89
|
+
"origin",
|
|
90
|
+
"meta",
|
|
91
|
+
], path, ctx);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function walkLayer(node, path, ctx) {
|
|
95
|
+
walkBaseFields(node, path, ctx);
|
|
96
|
+
expectField(node, "charts", path, ctx, (v, p) => walkArray(v, p, ctx, walkRootChart));
|
|
97
|
+
optionalField(node, "options", path, ctx, expectObject);
|
|
98
|
+
if (ctx.strict) {
|
|
99
|
+
rejectUnknown(node, ["type", "charts", "options", "origin", "meta"], path, ctx);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function walkRootChart(node, path, ctx) {
|
|
103
|
+
if (!isObject(node) || node.type !== "chart") {
|
|
104
|
+
ctx.errors.push({
|
|
105
|
+
path,
|
|
106
|
+
message: 'layer children must be charts (type === "chart")',
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
walkChart(node, path, ctx);
|
|
111
|
+
}
|
|
112
|
+
function walkRawMark(node, path, ctx) {
|
|
113
|
+
walkBaseFields(node, path, ctx);
|
|
114
|
+
expectField(node, "mark", path, ctx, walkMark);
|
|
115
|
+
optionalField(node, "options", path, ctx, expectObject);
|
|
116
|
+
if (ctx.strict) {
|
|
117
|
+
rejectUnknown(node, ["type", "mark", "options", "origin", "meta"], path, ctx);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function walkData(node, path, ctx) {
|
|
121
|
+
if (!isObject(node)) {
|
|
122
|
+
ctx.errors.push({ path, message: "expected object" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
switch (node.type) {
|
|
126
|
+
case "inline":
|
|
127
|
+
expectField(node, "rows", path, ctx, (v, p) => {
|
|
128
|
+
if (!Array.isArray(v))
|
|
129
|
+
ctx.errors.push({ path: p, message: "rows must be an array" });
|
|
130
|
+
});
|
|
131
|
+
if (ctx.strict)
|
|
132
|
+
rejectUnknown(node, ["type", "rows"], path, ctx);
|
|
133
|
+
return;
|
|
134
|
+
case "select":
|
|
135
|
+
expectField(node, "layer", path, ctx, expectString);
|
|
136
|
+
if (ctx.strict)
|
|
137
|
+
rejectUnknown(node, ["type", "layer"], path, ctx);
|
|
138
|
+
return;
|
|
139
|
+
case "external":
|
|
140
|
+
optionalField(node, "id", path, ctx, expectString);
|
|
141
|
+
if (ctx.strict)
|
|
142
|
+
rejectUnknown(node, ["type", "id"], path, ctx);
|
|
143
|
+
return;
|
|
144
|
+
default:
|
|
145
|
+
ctx.errors.push({
|
|
146
|
+
path: `${path}.type`,
|
|
147
|
+
message: `data type must be "inline" | "select" | "external", got ${JSON.stringify(node.type)}`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function walkOperator(node, path, ctx) {
|
|
152
|
+
if (!isObject(node)) {
|
|
153
|
+
ctx.errors.push({ path, message: "expected object" });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (typeof node.type !== "string" ||
|
|
157
|
+
!OPERATOR_TYPES.includes(node.type)) {
|
|
158
|
+
ctx.errors.push({
|
|
159
|
+
path: `${path}.type`,
|
|
160
|
+
message: `operator type must be one of ${OPERATOR_TYPES.join(", ")}, got ${JSON.stringify(node.type)}`,
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
walkBaseFields(node, path, ctx);
|
|
165
|
+
// Per-type field validation. Each operator has a known set of optional
|
|
166
|
+
// and required fields; in strict mode, unknown fields are rejected.
|
|
167
|
+
const knownFields = {
|
|
168
|
+
derive: ["type", "lambdaId", "origin", "meta"],
|
|
169
|
+
spread: [
|
|
170
|
+
"type",
|
|
171
|
+
"by",
|
|
172
|
+
"dir",
|
|
173
|
+
"spacing",
|
|
174
|
+
"alignment",
|
|
175
|
+
"sharedScale",
|
|
176
|
+
"mode",
|
|
177
|
+
"reverse",
|
|
178
|
+
"origin",
|
|
179
|
+
"meta",
|
|
180
|
+
],
|
|
181
|
+
stack: [
|
|
182
|
+
"type",
|
|
183
|
+
"by",
|
|
184
|
+
"dir",
|
|
185
|
+
"alignment",
|
|
186
|
+
"sharedScale",
|
|
187
|
+
"mode",
|
|
188
|
+
"reverse",
|
|
189
|
+
"origin",
|
|
190
|
+
"meta",
|
|
191
|
+
],
|
|
192
|
+
group: ["type", "by", "origin", "meta"],
|
|
193
|
+
scatter: [
|
|
194
|
+
"type",
|
|
195
|
+
"by",
|
|
196
|
+
"x",
|
|
197
|
+
"y",
|
|
198
|
+
"xMin",
|
|
199
|
+
"xMax",
|
|
200
|
+
"yMin",
|
|
201
|
+
"yMax",
|
|
202
|
+
"alignment",
|
|
203
|
+
"origin",
|
|
204
|
+
"meta",
|
|
205
|
+
],
|
|
206
|
+
table: ["type", "by", "spacing", "numCols", "origin", "meta"],
|
|
207
|
+
log: ["type", "label", "origin", "meta"],
|
|
208
|
+
};
|
|
209
|
+
switch (node.type) {
|
|
210
|
+
case "derive":
|
|
211
|
+
optionalField(node, "lambdaId", path, ctx, expectString);
|
|
212
|
+
break;
|
|
213
|
+
case "spread":
|
|
214
|
+
case "stack":
|
|
215
|
+
optionalField(node, "by", path, ctx, expectString);
|
|
216
|
+
optionalField(node, "dir", path, ctx, (v, p) => {
|
|
217
|
+
if (v !== "x" && v !== "y")
|
|
218
|
+
ctx.errors.push({
|
|
219
|
+
path: p,
|
|
220
|
+
message: `dir must be "x" | "y", got ${JSON.stringify(v)}`,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
optionalField(node, "spacing", path, ctx, expectNumber);
|
|
224
|
+
optionalField(node, "alignment", path, ctx, expectString);
|
|
225
|
+
optionalField(node, "sharedScale", path, ctx, expectBoolean);
|
|
226
|
+
optionalField(node, "reverse", path, ctx, expectBoolean);
|
|
227
|
+
optionalField(node, "mode", path, ctx, (v, p) => {
|
|
228
|
+
if (v !== "edge" && v !== "center")
|
|
229
|
+
ctx.errors.push({
|
|
230
|
+
path: p,
|
|
231
|
+
message: `mode must be "edge" | "center"`,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
break;
|
|
235
|
+
case "group":
|
|
236
|
+
expectField(node, "by", path, ctx, expectString);
|
|
237
|
+
break;
|
|
238
|
+
case "scatter":
|
|
239
|
+
optionalField(node, "by", path, ctx, expectString);
|
|
240
|
+
for (const k of ["x", "y", "xMin", "xMax", "yMin", "yMax"]) {
|
|
241
|
+
optionalField(node, k, path, ctx, walkChannelValue);
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
case "table":
|
|
245
|
+
// `by` is required: the table operator can't run without an
|
|
246
|
+
// {x, y} field-name pair (Python's table() raises if missing).
|
|
247
|
+
expectField(node, "by", path, ctx, (v, p) => {
|
|
248
|
+
if (!isObject(v)) {
|
|
249
|
+
ctx.errors.push({ path: p, message: "table.by must be an object" });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
expectField(v, "x", p, ctx, expectString);
|
|
253
|
+
expectField(v, "y", p, ctx, expectString);
|
|
254
|
+
});
|
|
255
|
+
optionalField(node, "spacing", path, ctx, (v, p) => {
|
|
256
|
+
if (typeof v === "number")
|
|
257
|
+
return;
|
|
258
|
+
if (Array.isArray(v) &&
|
|
259
|
+
v.length === 2 &&
|
|
260
|
+
v.every((n) => typeof n === "number"))
|
|
261
|
+
return;
|
|
262
|
+
ctx.errors.push({
|
|
263
|
+
path: p,
|
|
264
|
+
message: "table.spacing must be a number or [number, number]",
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
optionalField(node, "numCols", path, ctx, expectNumber);
|
|
268
|
+
break;
|
|
269
|
+
case "log":
|
|
270
|
+
optionalField(node, "label", path, ctx, expectString);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
if (ctx.strict) {
|
|
274
|
+
rejectUnknown(node, knownFields[node.type] ?? ["type"], path, ctx);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* A channel value: bare primitive, the existing `{type:"datum"}` wrapper,
|
|
279
|
+
* the new `{type:"field"|"literal"}` constructors, or a Python-bridge
|
|
280
|
+
* sentinel. Permissive — only catches obviously-wrong shapes.
|
|
281
|
+
*/
|
|
282
|
+
function walkChannelValue(value, path, ctx) {
|
|
283
|
+
if (value === null)
|
|
284
|
+
return;
|
|
285
|
+
if (typeof value === "string")
|
|
286
|
+
return;
|
|
287
|
+
if (typeof value === "number")
|
|
288
|
+
return;
|
|
289
|
+
if (typeof value === "boolean")
|
|
290
|
+
return;
|
|
291
|
+
if (typeof value !== "object") {
|
|
292
|
+
ctx.errors.push({
|
|
293
|
+
path,
|
|
294
|
+
message: `channel value must be primitive or tagged object, got ${typeNameOf(value)}`,
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Object form: one of the recognized tagged shapes.
|
|
299
|
+
const obj = value;
|
|
300
|
+
if ("__gofish_lambda" in obj)
|
|
301
|
+
return; // Python-bridge sentinel
|
|
302
|
+
if (obj.type === "datum")
|
|
303
|
+
return;
|
|
304
|
+
if (obj.type === "field") {
|
|
305
|
+
if (typeof obj.name !== "string") {
|
|
306
|
+
ctx.errors.push({
|
|
307
|
+
path: `${path}.name`,
|
|
308
|
+
message: 'field channel must have a string "name"',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (obj.type === "literal") {
|
|
314
|
+
if (!("value" in obj)) {
|
|
315
|
+
ctx.errors.push({
|
|
316
|
+
path: `${path}.value`,
|
|
317
|
+
message: 'literal channel must have a "value" field',
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Permissive fallback: allow unknown object shapes for forward-compat.
|
|
323
|
+
}
|
|
324
|
+
function walkMark(node, path, ctx) {
|
|
325
|
+
if (!isObject(node)) {
|
|
326
|
+
ctx.errors.push({ path, message: "expected object" });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const t = node.type;
|
|
330
|
+
if (typeof t !== "string") {
|
|
331
|
+
ctx.errors.push({
|
|
332
|
+
path: `${path}.type`,
|
|
333
|
+
message: "mark type must be a string",
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (t === "ref") {
|
|
338
|
+
walkRefMark(node, path, ctx);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (node.__combinator === true) {
|
|
342
|
+
walkCombinatorMark(node, path, ctx);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (LEAF_MARK_TYPES.includes(t)) {
|
|
346
|
+
walkLeafMark(node, path, ctx);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
ctx.errors.push({
|
|
350
|
+
path: `${path}.type`,
|
|
351
|
+
message: `unrecognized mark type ${JSON.stringify(t)}`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function walkRefMark(node, path, ctx) {
|
|
355
|
+
walkBaseFields(node, path, ctx);
|
|
356
|
+
expectField(node, "selection", path, ctx, (v, p) => {
|
|
357
|
+
if (typeof v !== "string" && !Array.isArray(v)) {
|
|
358
|
+
ctx.errors.push({
|
|
359
|
+
path: p,
|
|
360
|
+
message: "ref.selection must be a string or array",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
optionalField(node, "name", path, ctx, expectNameOrToken);
|
|
365
|
+
optionalField(node, "label", path, ctx, walkLabel);
|
|
366
|
+
optionalField(node, "zOrder", path, ctx, expectNumber);
|
|
367
|
+
if (ctx.strict) {
|
|
368
|
+
rejectUnknown(node, ["type", "selection", "name", "label", "zOrder", "origin", "meta"], path, ctx);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function walkCombinatorMark(node, path, ctx) {
|
|
372
|
+
if (!COMBINATOR_MARK_TYPES.includes(node.type)) {
|
|
373
|
+
ctx.errors.push({
|
|
374
|
+
path: `${path}.type`,
|
|
375
|
+
message: `combinator mark type must be one of ${COMBINATOR_MARK_TYPES.join(", ")}`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
walkBaseFields(node, path, ctx);
|
|
379
|
+
optionalField(node, "options", path, ctx, expectObject);
|
|
380
|
+
expectField(node, "children", path, ctx, (v, p) => walkArray(v, p, ctx, walkMark));
|
|
381
|
+
optionalField(node, "name", path, ctx, expectNameOrToken);
|
|
382
|
+
optionalField(node, "label", path, ctx, walkLabel);
|
|
383
|
+
optionalField(node, "constraints", path, ctx, (v, p) => walkArray(v, p, ctx, walkConstraint));
|
|
384
|
+
optionalField(node, "zOrder", path, ctx, expectNumber);
|
|
385
|
+
if (ctx.strict) {
|
|
386
|
+
rejectUnknown(node, [
|
|
387
|
+
"type",
|
|
388
|
+
"__combinator",
|
|
389
|
+
"options",
|
|
390
|
+
"children",
|
|
391
|
+
"name",
|
|
392
|
+
"label",
|
|
393
|
+
"constraints",
|
|
394
|
+
"zOrder",
|
|
395
|
+
"origin",
|
|
396
|
+
"meta",
|
|
397
|
+
], path, ctx);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function walkLeafMark(node, path, ctx) {
|
|
401
|
+
walkBaseFields(node, path, ctx);
|
|
402
|
+
optionalField(node, "name", path, ctx, expectNameOrToken);
|
|
403
|
+
optionalField(node, "label", path, ctx, walkLabel);
|
|
404
|
+
optionalField(node, "constraints", path, ctx, (v, p) => walkArray(v, p, ctx, walkConstraint));
|
|
405
|
+
optionalField(node, "zOrder", path, ctx, expectNumber);
|
|
406
|
+
// Channel-valued props are unrestricted in v0 (mirrors widget IR).
|
|
407
|
+
// Strict mode does NOT reject unknown fields on leaf marks, because the
|
|
408
|
+
// entire point of a leaf is to carry channel-valued props with arbitrary
|
|
409
|
+
// names (h, w, fill, x, y, etc.).
|
|
410
|
+
}
|
|
411
|
+
function walkLabel(node, path, ctx) {
|
|
412
|
+
// Shorthand forms (matching the JS API):
|
|
413
|
+
// label: true → "label with default settings"
|
|
414
|
+
// label: "field" → "label with this field accessor, defaults elsewhere"
|
|
415
|
+
if (typeof node === "boolean")
|
|
416
|
+
return;
|
|
417
|
+
if (typeof node === "string")
|
|
418
|
+
return;
|
|
419
|
+
if (!isObject(node)) {
|
|
420
|
+
ctx.errors.push({
|
|
421
|
+
path,
|
|
422
|
+
message: "expected object, boolean, or string",
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
expectField(node, "accessor", path, ctx, expectString);
|
|
427
|
+
optionalField(node, "position", path, ctx, expectString);
|
|
428
|
+
optionalField(node, "fontSize", path, ctx, expectNumber);
|
|
429
|
+
optionalField(node, "color", path, ctx, expectString);
|
|
430
|
+
optionalField(node, "offset", path, ctx, expectNumber);
|
|
431
|
+
optionalField(node, "minSpace", path, ctx, expectNumber);
|
|
432
|
+
optionalField(node, "rotate", path, ctx, expectNumber);
|
|
433
|
+
if (ctx.strict) {
|
|
434
|
+
rejectUnknown(node, [
|
|
435
|
+
"accessor",
|
|
436
|
+
"position",
|
|
437
|
+
"fontSize",
|
|
438
|
+
"color",
|
|
439
|
+
"offset",
|
|
440
|
+
"minSpace",
|
|
441
|
+
"rotate",
|
|
442
|
+
], path, ctx);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function walkConstraint(node, path, ctx) {
|
|
446
|
+
if (!isObject(node)) {
|
|
447
|
+
ctx.errors.push({ path, message: "expected object" });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const t = node.type;
|
|
451
|
+
if (t !== "align" && t !== "distribute" && t !== "zAbove" && t !== "zBelow") {
|
|
452
|
+
ctx.errors.push({
|
|
453
|
+
path: `${path}.type`,
|
|
454
|
+
message: `constraint type must be "align" | "distribute" | "zAbove" | "zBelow"`,
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
expectField(node, "refs", path, ctx, (v, p) => {
|
|
459
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
|
|
460
|
+
ctx.errors.push({ path: p, message: "refs must be an array of strings" });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
optionalField(node, "options", path, ctx, expectObject);
|
|
464
|
+
if (ctx.strict) {
|
|
465
|
+
rejectUnknown(node, ["type", "refs", "options"], path, ctx);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function walkBaseFields(node, path, ctx) {
|
|
469
|
+
optionalField(node, "origin", path, ctx, walkOrigin);
|
|
470
|
+
optionalField(node, "meta", path, ctx, walkMeta);
|
|
471
|
+
}
|
|
472
|
+
function walkOrigin(node, path, ctx) {
|
|
473
|
+
if (!isObject(node)) {
|
|
474
|
+
ctx.errors.push({ path, message: "origin must be an object" });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
optionalField(node, "name", path, ctx, expectNameOrToken);
|
|
478
|
+
optionalField(node, "stack", path, ctx, expectString);
|
|
479
|
+
if (ctx.strict)
|
|
480
|
+
rejectUnknown(node, ["name", "stack"], path, ctx);
|
|
481
|
+
}
|
|
482
|
+
function walkMeta(node, path, _ctx) {
|
|
483
|
+
if (!isObject(node)) {
|
|
484
|
+
_ctx.errors.push({ path, message: "meta must be an object" });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// Meta is intentionally open — additional keys are reserved for future
|
|
488
|
+
// passes. v0 doesn't enforce per-key shapes; that lands with the passes
|
|
489
|
+
// that populate them.
|
|
490
|
+
}
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// Field helpers
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
function expectField(obj, key, parentPath, ctx, check) {
|
|
495
|
+
const childPath = `${parentPath}.${key}`;
|
|
496
|
+
if (!(key in obj)) {
|
|
497
|
+
ctx.errors.push({
|
|
498
|
+
path: childPath,
|
|
499
|
+
message: `required field "${key}" is missing`,
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
check(obj[key], childPath, ctx);
|
|
504
|
+
}
|
|
505
|
+
function optionalField(obj, key, parentPath, ctx, check) {
|
|
506
|
+
// Treat both `undefined` and explicit `null` as "absent". Python's
|
|
507
|
+
// `to_dict()` emits `null` for several optional fields (data, zOrder,
|
|
508
|
+
// ...) rather than omitting them.
|
|
509
|
+
if (!(key in obj) || obj[key] === undefined || obj[key] === null)
|
|
510
|
+
return;
|
|
511
|
+
check(obj[key], `${parentPath}.${key}`, ctx);
|
|
512
|
+
}
|
|
513
|
+
function walkArray(v, path, ctx, walkItem) {
|
|
514
|
+
if (!Array.isArray(v)) {
|
|
515
|
+
ctx.errors.push({ path, message: "expected array" });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
v.forEach((item, i) => walkItem(item, `${path}[${i}]`, ctx));
|
|
519
|
+
}
|
|
520
|
+
function rejectUnknown(obj, knownKeys, path, ctx) {
|
|
521
|
+
for (const k of Object.keys(obj)) {
|
|
522
|
+
if (!knownKeys.includes(k)) {
|
|
523
|
+
ctx.errors.push({
|
|
524
|
+
path: `${path}.${k}`,
|
|
525
|
+
message: `unknown field "${k}" (strict)`,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function expectString(value, path, ctx) {
|
|
531
|
+
if (typeof value !== "string") {
|
|
532
|
+
ctx.errors.push({
|
|
533
|
+
path,
|
|
534
|
+
message: `expected string, got ${typeNameOf(value)}`,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function expectNumber(value, path, ctx) {
|
|
539
|
+
if (typeof value !== "number") {
|
|
540
|
+
ctx.errors.push({
|
|
541
|
+
path,
|
|
542
|
+
message: `expected number, got ${typeNameOf(value)}`,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function expectBoolean(value, path, ctx) {
|
|
547
|
+
if (typeof value !== "boolean") {
|
|
548
|
+
ctx.errors.push({
|
|
549
|
+
path,
|
|
550
|
+
message: `expected boolean, got ${typeNameOf(value)}`,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* A name field may be a string or a Python-bridge token sentinel
|
|
556
|
+
* (`{__gofish_token, __tag}`) used to encode hygienic names across the
|
|
557
|
+
* widget bridge. The deserializer resolves the sentinel to a runtime
|
|
558
|
+
* Token at chart-build time.
|
|
559
|
+
*/
|
|
560
|
+
function expectNameOrToken(value, path, ctx) {
|
|
561
|
+
if (typeof value === "string")
|
|
562
|
+
return;
|
|
563
|
+
if (typeof value === "object" &&
|
|
564
|
+
value !== null &&
|
|
565
|
+
typeof value.__gofish_token === "string" &&
|
|
566
|
+
typeof value.__tag === "string") {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
ctx.errors.push({
|
|
570
|
+
path,
|
|
571
|
+
message: `expected string or token sentinel, got ${typeNameOf(value)}`,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function expectObject(value, path, ctx) {
|
|
575
|
+
if (!isObject(value)) {
|
|
576
|
+
ctx.errors.push({
|
|
577
|
+
path,
|
|
578
|
+
message: `expected object, got ${typeNameOf(value)}`,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function isObject(value) {
|
|
583
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
584
|
+
}
|
|
585
|
+
function typeNameOf(value) {
|
|
586
|
+
if (value === null)
|
|
587
|
+
return "null";
|
|
588
|
+
if (Array.isArray(value))
|
|
589
|
+
return "array";
|
|
590
|
+
return typeof value;
|
|
591
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Frontend from "./frontend/index.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gofish-ir",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Serialized intermediate representation (IR) for GoFish chart specifications.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./frontend": {
|
|
15
|
+
"import": "./dist/frontend/index.js",
|
|
16
|
+
"types": "./dist/frontend/index.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"gofish",
|
|
25
|
+
"ir",
|
|
26
|
+
"schema",
|
|
27
|
+
"visualization"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/gofish-graphics/gofish-graphics"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc && tsx scripts/emit-jsonschema.ts",
|
|
36
|
+
"test": "tsx src/tests/schema.test.ts",
|
|
37
|
+
"prepublishOnly": "pnpm run build"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"tsx": "^4.19.2",
|
|
41
|
+
"typescript": "^5.7.2"
|
|
42
|
+
}
|
|
43
|
+
}
|