@flux-lang/core 0.1.4 → 0.1.6-canary.18d439adc
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 +20 -9
- package/dist/ast.d.ts +149 -10
- package/dist/ast.d.ts.map +1 -1
- package/dist/checks.d.ts.map +1 -1
- package/dist/checks.js +184 -2
- package/dist/checks.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +28 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +40 -0
- package/dist/layout.js.map +1 -0
- package/dist/parser.d.ts +9 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +939 -29
- package/dist/parser.js.map +1 -1
- package/dist/render.d.ts +174 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1968 -0
- package/dist/render.js.map +1 -0
- package/dist/runtime/kernel.js +2 -0
- package/dist/runtime/kernel.js.map +1 -1
- package/dist/runtime.d.ts +91 -59
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +103 -372
- package/dist/runtime.js.map +1 -1
- package/dist/transform.d.ts +22 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/transform.js +383 -0
- package/dist/transform.js.map +1 -0
- package/package.json +2 -1
package/dist/render.js
ADDED
|
@@ -0,0 +1,1968 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { computeGridLayout } from "./layout.js";
|
|
4
|
+
import { createRuntime } from "./runtime.js";
|
|
5
|
+
export function createDocumentRuntime(doc, options = {}) {
|
|
6
|
+
const seed = options.seed ?? 0;
|
|
7
|
+
let time = options.time ?? 0;
|
|
8
|
+
let docstep = options.docstep ?? 0;
|
|
9
|
+
const baseBody = ensureBody(doc);
|
|
10
|
+
const assets = buildAssetCatalog(doc, options);
|
|
11
|
+
const baseParams = buildParams(doc.state.params);
|
|
12
|
+
const styleRegistry = buildStyleRegistry(doc);
|
|
13
|
+
const body = applyVisibility(baseBody, {
|
|
14
|
+
params: baseParams,
|
|
15
|
+
meta: doc.meta,
|
|
16
|
+
tokens: styleRegistry.tokens,
|
|
17
|
+
seed,
|
|
18
|
+
});
|
|
19
|
+
const counterRegistry = buildCounterRegistry(body);
|
|
20
|
+
const nodeCache = new Map();
|
|
21
|
+
let legacySnapshot = null;
|
|
22
|
+
let legacySnapshotDocstep = null;
|
|
23
|
+
const getLegacySnapshot = () => {
|
|
24
|
+
if (!doc.grids?.length)
|
|
25
|
+
return null;
|
|
26
|
+
if (legacySnapshot && legacySnapshotDocstep === docstep) {
|
|
27
|
+
return legacySnapshot;
|
|
28
|
+
}
|
|
29
|
+
const runtime = createRuntime(doc, { clock: "manual" });
|
|
30
|
+
let snap = runtime.snapshot();
|
|
31
|
+
if (docstep > 0) {
|
|
32
|
+
try {
|
|
33
|
+
for (let i = 0; i < docstep; i += 1) {
|
|
34
|
+
snap = runtime.step();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
snap = runtime.snapshot();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
legacySnapshot = snap;
|
|
42
|
+
legacySnapshotDocstep = docstep;
|
|
43
|
+
return legacySnapshot;
|
|
44
|
+
};
|
|
45
|
+
const render = () => {
|
|
46
|
+
const snap = getLegacySnapshot();
|
|
47
|
+
const params = snap?.params ?? baseParams;
|
|
48
|
+
const ctx = {
|
|
49
|
+
doc,
|
|
50
|
+
params,
|
|
51
|
+
time,
|
|
52
|
+
docstep,
|
|
53
|
+
seed,
|
|
54
|
+
assets,
|
|
55
|
+
legacySnapshot: snap,
|
|
56
|
+
styleRegistry,
|
|
57
|
+
counterRegistry,
|
|
58
|
+
};
|
|
59
|
+
const renderedBody = body.nodes.map((node, index) => renderNode(node, ctx, nodeCache, "root", undefined, undefined, index, false));
|
|
60
|
+
return {
|
|
61
|
+
meta: doc.meta,
|
|
62
|
+
seed,
|
|
63
|
+
time,
|
|
64
|
+
docstep,
|
|
65
|
+
pageConfig: doc.pageConfig,
|
|
66
|
+
assets: assetsToRender(assets),
|
|
67
|
+
body: renderedBody,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const tick = (seconds) => {
|
|
71
|
+
const delta = Number(seconds);
|
|
72
|
+
if (!Number.isFinite(delta)) {
|
|
73
|
+
throw new Error("tick(seconds) requires a finite number");
|
|
74
|
+
}
|
|
75
|
+
time += delta;
|
|
76
|
+
return render();
|
|
77
|
+
};
|
|
78
|
+
const step = (n = 1) => {
|
|
79
|
+
const amount = Number(n);
|
|
80
|
+
if (!Number.isFinite(amount)) {
|
|
81
|
+
throw new Error("step(n) requires a finite number");
|
|
82
|
+
}
|
|
83
|
+
docstep += amount;
|
|
84
|
+
return render();
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
get doc() {
|
|
88
|
+
return doc;
|
|
89
|
+
},
|
|
90
|
+
get seed() {
|
|
91
|
+
return seed;
|
|
92
|
+
},
|
|
93
|
+
get time() {
|
|
94
|
+
return time;
|
|
95
|
+
},
|
|
96
|
+
get docstep() {
|
|
97
|
+
return docstep;
|
|
98
|
+
},
|
|
99
|
+
render,
|
|
100
|
+
tick,
|
|
101
|
+
step,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function renderDocument(doc, options = {}) {
|
|
105
|
+
const runtime = createDocumentRuntime(doc, options);
|
|
106
|
+
return runtime.render();
|
|
107
|
+
}
|
|
108
|
+
export function createDocumentRuntimeIR(doc, options = {}) {
|
|
109
|
+
const runtime = createDocumentRuntime(doc, options);
|
|
110
|
+
const baseBody = ensureBody(doc);
|
|
111
|
+
const styleRegistry = buildStyleRegistry(doc);
|
|
112
|
+
const baseParams = buildParams(doc.state.params);
|
|
113
|
+
const body = applyVisibility(baseBody, {
|
|
114
|
+
params: baseParams,
|
|
115
|
+
meta: doc.meta,
|
|
116
|
+
tokens: styleRegistry.tokens,
|
|
117
|
+
seed: options.seed ?? 0,
|
|
118
|
+
});
|
|
119
|
+
const counterRegistry = buildCounterRegistry(body);
|
|
120
|
+
const toIr = (rendered) => buildRenderDocumentIR(rendered, body, styleRegistry, counterRegistry);
|
|
121
|
+
return {
|
|
122
|
+
get doc() {
|
|
123
|
+
return runtime.doc;
|
|
124
|
+
},
|
|
125
|
+
get seed() {
|
|
126
|
+
return runtime.seed;
|
|
127
|
+
},
|
|
128
|
+
get time() {
|
|
129
|
+
return runtime.time;
|
|
130
|
+
},
|
|
131
|
+
get docstep() {
|
|
132
|
+
return runtime.docstep;
|
|
133
|
+
},
|
|
134
|
+
render: () => toIr(runtime.render()),
|
|
135
|
+
tick: (seconds) => toIr(runtime.tick(seconds)),
|
|
136
|
+
step: (n) => toIr(runtime.step(n)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function renderDocumentIR(doc, options = {}) {
|
|
140
|
+
const runtime = createDocumentRuntimeIR(doc, options);
|
|
141
|
+
return runtime.render();
|
|
142
|
+
}
|
|
143
|
+
function ensureBody(doc) {
|
|
144
|
+
if (doc.body?.nodes?.length) {
|
|
145
|
+
return doc.body;
|
|
146
|
+
}
|
|
147
|
+
if (!doc.grids?.length) {
|
|
148
|
+
return { nodes: [] };
|
|
149
|
+
}
|
|
150
|
+
const pages = new Map();
|
|
151
|
+
for (const grid of doc.grids) {
|
|
152
|
+
const pageNumber = grid.page ?? 1;
|
|
153
|
+
const node = {
|
|
154
|
+
id: grid.name,
|
|
155
|
+
kind: "grid",
|
|
156
|
+
props: {
|
|
157
|
+
ref: { kind: "LiteralValue", value: grid.name },
|
|
158
|
+
},
|
|
159
|
+
children: [],
|
|
160
|
+
};
|
|
161
|
+
const list = pages.get(pageNumber) ?? [];
|
|
162
|
+
list.push(node);
|
|
163
|
+
pages.set(pageNumber, list);
|
|
164
|
+
}
|
|
165
|
+
const nodes = [];
|
|
166
|
+
const sortedPages = Array.from(pages.keys()).sort((a, b) => a - b);
|
|
167
|
+
for (const pageNumber of sortedPages) {
|
|
168
|
+
const pageNodes = pages.get(pageNumber) ?? [];
|
|
169
|
+
nodes.push({
|
|
170
|
+
id: `page${pageNumber}`,
|
|
171
|
+
kind: "page",
|
|
172
|
+
props: {},
|
|
173
|
+
children: pageNodes,
|
|
174
|
+
refresh: { kind: "docstep" },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return { nodes };
|
|
178
|
+
}
|
|
179
|
+
function buildRenderDocumentIR(rendered, body, styleRegistry, counterRegistry) {
|
|
180
|
+
const slotMeta = {};
|
|
181
|
+
return {
|
|
182
|
+
...rendered,
|
|
183
|
+
body: buildRenderNodesIR(body.nodes, rendered.body, "root", undefined, counterRegistry, slotMeta, { seed: rendered.seed, time: rendered.time, docstep: rendered.docstep }),
|
|
184
|
+
theme: styleRegistry.theme,
|
|
185
|
+
styles: styleRegistry.renderStyles,
|
|
186
|
+
slotMeta,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function buildRenderNodesIR(astNodes, renderedNodes, parentPath, parentPolicy, counterRegistry, slotMeta, presentationCtx) {
|
|
190
|
+
const count = Math.max(astNodes.length, renderedNodes.length);
|
|
191
|
+
const result = [];
|
|
192
|
+
for (let index = 0; index < count; index += 1) {
|
|
193
|
+
const rendered = renderedNodes[index];
|
|
194
|
+
const fallbackAst = rendered
|
|
195
|
+
? { id: rendered.id, kind: rendered.kind, props: {}, children: [] }
|
|
196
|
+
: { id: `node${index}`, kind: "node", props: {}, children: [] };
|
|
197
|
+
const astNode = astNodes[index] ?? fallbackAst;
|
|
198
|
+
const renderedNode = rendered ?? {
|
|
199
|
+
id: astNode.id,
|
|
200
|
+
kind: astNode.kind,
|
|
201
|
+
props: {},
|
|
202
|
+
children: [],
|
|
203
|
+
};
|
|
204
|
+
const nodePath = `${parentPath}/${astNode.kind}:${astNode.id}:${index}`;
|
|
205
|
+
const isSlot = astNode.kind === "slot" || astNode.kind === "inline_slot";
|
|
206
|
+
const effectivePolicy = isSlot ? astNode.refresh ?? { kind: "never" } : astNode.refresh ?? parentPolicy ?? { kind: "never" };
|
|
207
|
+
const children = buildRenderNodesIR(astNode.children ?? [], renderedNode.children ?? [], nodePath, effectivePolicy, counterRegistry, slotMeta, presentationCtx);
|
|
208
|
+
const slot = buildSlotInfo(astNode.kind, renderedNode.props);
|
|
209
|
+
if (isSlot) {
|
|
210
|
+
const transition = normalizeTransitionSpec(astNode.transition);
|
|
211
|
+
const trigger = didFire(effectivePolicy, {
|
|
212
|
+
seed: presentationCtx.seed,
|
|
213
|
+
slotId: nodePath,
|
|
214
|
+
timeSec: presentationCtx.time,
|
|
215
|
+
docstep: presentationCtx.docstep,
|
|
216
|
+
});
|
|
217
|
+
slotMeta[nodePath] = {
|
|
218
|
+
valueHash: computeSlotValueHash(renderedNode),
|
|
219
|
+
shouldRefresh: trigger.fired,
|
|
220
|
+
transition,
|
|
221
|
+
eventMeta: trigger.meta,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
result.push({
|
|
225
|
+
...renderedNode,
|
|
226
|
+
nodeId: nodePath,
|
|
227
|
+
refresh: effectivePolicy,
|
|
228
|
+
slot,
|
|
229
|
+
children,
|
|
230
|
+
style: renderedNode.style,
|
|
231
|
+
counters: counterRegistry.countersByNodePath.get(nodePath),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
function buildSlotInfo(kind, props) {
|
|
237
|
+
if (kind !== "slot" && kind !== "inline_slot")
|
|
238
|
+
return undefined;
|
|
239
|
+
const reserve = parseSlotReserve(props.reserve);
|
|
240
|
+
const fit = parseSlotFit(props.fit);
|
|
241
|
+
if (!reserve && !fit)
|
|
242
|
+
return undefined;
|
|
243
|
+
return { reserve, fit };
|
|
244
|
+
}
|
|
245
|
+
function normalizeTransitionSpec(spec) {
|
|
246
|
+
if (!spec || spec.kind === "none") {
|
|
247
|
+
return { type: "none", durationMs: 0, ease: "linear" };
|
|
248
|
+
}
|
|
249
|
+
switch (spec.kind) {
|
|
250
|
+
case "appear":
|
|
251
|
+
return { type: "appear", durationMs: 0, ease: "linear" };
|
|
252
|
+
case "fade":
|
|
253
|
+
return {
|
|
254
|
+
type: "fade",
|
|
255
|
+
durationMs: Math.max(0, spec.durationMs ?? 220),
|
|
256
|
+
ease: spec.ease ?? "inOut",
|
|
257
|
+
};
|
|
258
|
+
case "wipe":
|
|
259
|
+
return {
|
|
260
|
+
type: "wipe",
|
|
261
|
+
durationMs: Math.max(0, spec.durationMs ?? 260),
|
|
262
|
+
ease: spec.ease ?? "inOut",
|
|
263
|
+
direction: spec.direction ?? "left",
|
|
264
|
+
};
|
|
265
|
+
case "flash":
|
|
266
|
+
return {
|
|
267
|
+
type: "flash",
|
|
268
|
+
durationMs: Math.max(0, spec.durationMs ?? 120),
|
|
269
|
+
ease: "linear",
|
|
270
|
+
};
|
|
271
|
+
default: {
|
|
272
|
+
const _exhaustive = spec;
|
|
273
|
+
return _exhaustive;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function computeSlotValueHash(slotNode) {
|
|
278
|
+
const signature = slotNode.children.map((child) => buildValueSignature(child));
|
|
279
|
+
const hash = stableHash("slot", signature);
|
|
280
|
+
return hash.toString(16).padStart(8, "0");
|
|
281
|
+
}
|
|
282
|
+
function buildValueSignature(node) {
|
|
283
|
+
return {
|
|
284
|
+
kind: node.kind,
|
|
285
|
+
props: node.props,
|
|
286
|
+
children: (node.children ?? []).map((child) => buildValueSignature(child)),
|
|
287
|
+
grid: node.grid ?? undefined,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function parseSlotFit(value) {
|
|
291
|
+
if (typeof value !== "string")
|
|
292
|
+
return undefined;
|
|
293
|
+
switch (value) {
|
|
294
|
+
case "clip":
|
|
295
|
+
case "ellipsis":
|
|
296
|
+
case "shrink":
|
|
297
|
+
case "scaleDown":
|
|
298
|
+
return value;
|
|
299
|
+
default:
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function parseSlotReserve(value) {
|
|
304
|
+
if (value == null)
|
|
305
|
+
return undefined;
|
|
306
|
+
if (typeof value === "object") {
|
|
307
|
+
if (Array.isArray(value))
|
|
308
|
+
return parseSlotReserveArray(value);
|
|
309
|
+
const maybeAsset = value;
|
|
310
|
+
if (maybeAsset.kind === "asset")
|
|
311
|
+
return undefined;
|
|
312
|
+
return parseSlotReserveObject(value);
|
|
313
|
+
}
|
|
314
|
+
if (typeof value === "string") {
|
|
315
|
+
const fixedMatch = value.match(/^fixed\(\s*([0-9.+-]+)\s*,\s*([0-9.+-]+)\s*,\s*([^)]+)\s*\)$/i);
|
|
316
|
+
if (fixedMatch) {
|
|
317
|
+
const width = Number(fixedMatch[1]);
|
|
318
|
+
const height = Number(fixedMatch[2]);
|
|
319
|
+
const units = fixedMatch[3].trim();
|
|
320
|
+
if (Number.isFinite(width) && Number.isFinite(height) && units) {
|
|
321
|
+
return { kind: "fixed", width, height, units };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const fixedWidthMatch = value.match(/^fixedWidth\(\s*([0-9.+-]+)\s*,\s*([^)]+)\s*\)$/i);
|
|
325
|
+
if (fixedWidthMatch) {
|
|
326
|
+
const width = Number(fixedWidthMatch[1]);
|
|
327
|
+
const units = fixedWidthMatch[2].trim();
|
|
328
|
+
if (Number.isFinite(width) && units) {
|
|
329
|
+
return { kind: "fixedWidth", width, units };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
function parseSlotReserveObject(value) {
|
|
336
|
+
const kind = typeof value.kind === "string" ? value.kind : null;
|
|
337
|
+
if (kind === "fixed") {
|
|
338
|
+
const width = toFiniteNumber(value.width);
|
|
339
|
+
const height = toFiniteNumber(value.height);
|
|
340
|
+
const units = typeof value.units === "string" ? value.units : null;
|
|
341
|
+
if (width !== null && height !== null && units) {
|
|
342
|
+
return { kind: "fixed", width, height, units };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (kind === "fixedWidth") {
|
|
346
|
+
const width = toFiniteNumber(value.width);
|
|
347
|
+
const units = typeof value.units === "string" ? value.units : null;
|
|
348
|
+
if (width !== null && units) {
|
|
349
|
+
return { kind: "fixedWidth", width, units };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
function parseSlotReserveArray(value) {
|
|
355
|
+
if (value.length >= 3) {
|
|
356
|
+
const width = toFiniteNumber(value[0]);
|
|
357
|
+
const height = toFiniteNumber(value[1]);
|
|
358
|
+
const units = typeof value[2] === "string" ? value[2] : null;
|
|
359
|
+
if (width !== null && height !== null && units) {
|
|
360
|
+
return { kind: "fixed", width, height, units };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (value.length >= 2) {
|
|
364
|
+
const width = toFiniteNumber(value[0]);
|
|
365
|
+
const units = typeof value[1] === "string" ? value[1] : null;
|
|
366
|
+
if (width !== null && units) {
|
|
367
|
+
return { kind: "fixedWidth", width, units };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
function toFiniteNumber(value) {
|
|
373
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
374
|
+
return value;
|
|
375
|
+
if (typeof value === "string") {
|
|
376
|
+
const parsed = Number(value);
|
|
377
|
+
if (Number.isFinite(parsed))
|
|
378
|
+
return parsed;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
function buildParams(params) {
|
|
383
|
+
const values = {};
|
|
384
|
+
for (const param of params ?? []) {
|
|
385
|
+
values[param.name] = param.initial;
|
|
386
|
+
}
|
|
387
|
+
return values;
|
|
388
|
+
}
|
|
389
|
+
function renderNode(node, ctx, cache, parentPath, parentPolicy, parentRefreshOwnerId, index, insideSlot) {
|
|
390
|
+
const nodePath = `${parentPath}/${node.kind}:${node.id}:${index}`;
|
|
391
|
+
const isSlot = node.kind === "slot" || node.kind === "inline_slot";
|
|
392
|
+
const effectivePolicy = isSlot ? node.refresh ?? { kind: "never" } : node.refresh ?? parentPolicy ?? { kind: "never" };
|
|
393
|
+
const refreshOwnerId = isSlot ? nodePath : parentRefreshOwnerId ?? nodePath;
|
|
394
|
+
const cached = cache.get(nodePath);
|
|
395
|
+
const refreshState = computeRefreshState(effectivePolicy, { seed: ctx.seed, time: ctx.time, docstep: ctx.docstep, refreshOwnerId }, cached);
|
|
396
|
+
const refreshKey = refreshState.refreshKey;
|
|
397
|
+
let props;
|
|
398
|
+
let evalTime = cached?.time ?? 0;
|
|
399
|
+
let evalDocstep = cached?.docstep ?? 0;
|
|
400
|
+
if (!cached || cached.refreshKey !== refreshKey) {
|
|
401
|
+
evalTime = refreshState.evalTime;
|
|
402
|
+
evalDocstep = refreshState.evalDocstep;
|
|
403
|
+
props = resolveProps(node.props, {
|
|
404
|
+
params: ctx.params,
|
|
405
|
+
time: evalTime,
|
|
406
|
+
docstep: evalDocstep,
|
|
407
|
+
seed: ctx.seed,
|
|
408
|
+
assets: ctx.assets,
|
|
409
|
+
refreshKey,
|
|
410
|
+
nodePath,
|
|
411
|
+
nodeId: node.id,
|
|
412
|
+
nodeKind: node.kind,
|
|
413
|
+
meta: ctx.doc.meta,
|
|
414
|
+
tokens: ctx.styleRegistry.tokens,
|
|
415
|
+
refs: ctx.counterRegistry.refs,
|
|
416
|
+
});
|
|
417
|
+
cache.set(nodePath, { refreshKey, time: evalTime, docstep: evalDocstep, props });
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
props = cached.props;
|
|
421
|
+
}
|
|
422
|
+
const nextInsideSlot = insideSlot || isSlot;
|
|
423
|
+
const children = node.children.map((child, childIndex) => renderNode(child, ctx, cache, nodePath, effectivePolicy, refreshOwnerId, childIndex, nextInsideSlot));
|
|
424
|
+
const style = resolveNodeStyle(node, props, ctx, {
|
|
425
|
+
time: evalTime,
|
|
426
|
+
docstep: evalDocstep,
|
|
427
|
+
refreshKey,
|
|
428
|
+
nodePath,
|
|
429
|
+
nodeId: node.id,
|
|
430
|
+
nodeKind: node.kind,
|
|
431
|
+
}, insideSlot);
|
|
432
|
+
const rendered = {
|
|
433
|
+
id: node.id,
|
|
434
|
+
kind: node.kind,
|
|
435
|
+
props,
|
|
436
|
+
children,
|
|
437
|
+
};
|
|
438
|
+
if (style) {
|
|
439
|
+
rendered.style = style;
|
|
440
|
+
}
|
|
441
|
+
if (node.kind === "grid") {
|
|
442
|
+
const gridData = resolveGridData(node, props, ctx);
|
|
443
|
+
if (gridData) {
|
|
444
|
+
rendered.grid = gridData;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return rendered;
|
|
448
|
+
}
|
|
449
|
+
function resolveProps(props, ctx) {
|
|
450
|
+
const resolved = {};
|
|
451
|
+
for (const key of Object.keys(props)) {
|
|
452
|
+
const value = props[key];
|
|
453
|
+
if (value.kind === "LiteralValue") {
|
|
454
|
+
resolved[key] = toRenderValue(value.value);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const propSeed = stableHash(ctx.seed, ctx.nodePath, key, ctx.refreshKey);
|
|
458
|
+
const rng = mulberry32(propSeed);
|
|
459
|
+
const evalCtx = {
|
|
460
|
+
params: ctx.params,
|
|
461
|
+
time: ctx.time,
|
|
462
|
+
docstep: ctx.docstep,
|
|
463
|
+
rng,
|
|
464
|
+
propSeed,
|
|
465
|
+
assets: ctx.assets,
|
|
466
|
+
meta: ctx.meta,
|
|
467
|
+
tokens: ctx.tokens,
|
|
468
|
+
refs: ctx.refs,
|
|
469
|
+
};
|
|
470
|
+
try {
|
|
471
|
+
const exprValue = evalExpr(value.expr, evalCtx);
|
|
472
|
+
resolved[key] = toRenderValue(exprValue);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
const detail = error?.message ?? String(error);
|
|
476
|
+
const contextParts = [
|
|
477
|
+
ctx.nodeId ? `node '${ctx.nodeId}'` : "",
|
|
478
|
+
ctx.nodeKind ? `kind '${ctx.nodeKind}'` : "",
|
|
479
|
+
`prop '${key}'`,
|
|
480
|
+
].filter(Boolean);
|
|
481
|
+
const context = contextParts.length ? ` (${contextParts.join(", ")})` : "";
|
|
482
|
+
throw new Error(`${detail}${context}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return resolved;
|
|
486
|
+
}
|
|
487
|
+
const DEFAULT_TOKENS = {
|
|
488
|
+
"font.serif": '"Iowan Old Style", "Palatino Linotype", Palatino, "Times New Roman", serif',
|
|
489
|
+
"font.sans": '"Inter", "Helvetica Neue", Arial, sans-serif',
|
|
490
|
+
"font.mono": '"Source Code Pro", "Courier New", monospace',
|
|
491
|
+
"space.xs": 2,
|
|
492
|
+
"space.s": 4,
|
|
493
|
+
"space.m": 8,
|
|
494
|
+
"space.l": 12,
|
|
495
|
+
"space.xl": 18,
|
|
496
|
+
"color.text": "#1d1b17",
|
|
497
|
+
"color.muted": "#6b645a",
|
|
498
|
+
"color.link": "#2b4c7e",
|
|
499
|
+
"color.rule": "#d1c8bb",
|
|
500
|
+
"color.calloutBg": "#f6f1e8",
|
|
501
|
+
"color.calloutBorder": "#d9d0c4",
|
|
502
|
+
"rule.thin": 1,
|
|
503
|
+
};
|
|
504
|
+
function buildStyleRegistry(doc) {
|
|
505
|
+
const theme = resolveThemeName(doc);
|
|
506
|
+
const themeBlock = doc.themes?.find((t) => t.name === theme);
|
|
507
|
+
const tokenFlat = {
|
|
508
|
+
...DEFAULT_TOKENS,
|
|
509
|
+
...(doc.tokens?.tokens ?? {}),
|
|
510
|
+
...(themeBlock?.tokens?.tokens ?? {}),
|
|
511
|
+
};
|
|
512
|
+
const tokens = buildTokenTree(tokenFlat);
|
|
513
|
+
const baseStyles = buildDefaultStyles(tokenFlat);
|
|
514
|
+
const docStyles = doc.styles?.styles ?? [];
|
|
515
|
+
const themeStyles = themeBlock?.styles?.styles ?? [];
|
|
516
|
+
const mergedStyles = mergeStyleInputs(baseStyles, docStyles, themeStyles);
|
|
517
|
+
const styles = resolveStyleSpecs(mergedStyles, tokens);
|
|
518
|
+
const renderStyles = Array.from(styles.values()).map((style) => ({
|
|
519
|
+
name: style.name,
|
|
520
|
+
className: style.className,
|
|
521
|
+
props: style.staticProps,
|
|
522
|
+
}));
|
|
523
|
+
return {
|
|
524
|
+
theme,
|
|
525
|
+
tokens,
|
|
526
|
+
tokenFlat,
|
|
527
|
+
styles,
|
|
528
|
+
renderStyles,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function resolveThemeName(doc) {
|
|
532
|
+
const target = typeof doc.meta?.target === "string" ? doc.meta.target : null;
|
|
533
|
+
if (target)
|
|
534
|
+
return target;
|
|
535
|
+
return "screen";
|
|
536
|
+
}
|
|
537
|
+
function applyVisibility(body, ctx) {
|
|
538
|
+
return { nodes: filterVisibleNodes(body.nodes, ctx, "root") };
|
|
539
|
+
}
|
|
540
|
+
function filterVisibleNodes(nodes, ctx, parentPath) {
|
|
541
|
+
const result = [];
|
|
542
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
543
|
+
const node = nodes[index];
|
|
544
|
+
const nodePath = `${parentPath}/${node.kind}:${node.id}:${index}`;
|
|
545
|
+
const visible = evaluateVisibleIf(node, ctx, nodePath);
|
|
546
|
+
if (!visible)
|
|
547
|
+
continue;
|
|
548
|
+
const children = node.children?.length
|
|
549
|
+
? filterVisibleNodes(node.children, ctx, nodePath)
|
|
550
|
+
: [];
|
|
551
|
+
result.push({ ...node, children });
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
function evaluateVisibleIf(node, ctx, nodePath) {
|
|
556
|
+
const visibleProp = node.props?.visibleIf;
|
|
557
|
+
if (!visibleProp)
|
|
558
|
+
return true;
|
|
559
|
+
if (visibleProp.kind === "LiteralValue") {
|
|
560
|
+
return Boolean(visibleProp.value);
|
|
561
|
+
}
|
|
562
|
+
const propSeed = stableHash(ctx.seed, nodePath, "visibleIf");
|
|
563
|
+
const rng = mulberry32(propSeed);
|
|
564
|
+
const evalCtx = {
|
|
565
|
+
params: ctx.params,
|
|
566
|
+
time: 0,
|
|
567
|
+
docstep: 0,
|
|
568
|
+
rng,
|
|
569
|
+
propSeed,
|
|
570
|
+
assets: [],
|
|
571
|
+
meta: ctx.meta,
|
|
572
|
+
tokens: ctx.tokens,
|
|
573
|
+
refs: new Map(),
|
|
574
|
+
};
|
|
575
|
+
const value = evalExpr(visibleProp.expr, evalCtx);
|
|
576
|
+
return Boolean(value);
|
|
577
|
+
}
|
|
578
|
+
function buildCounterRegistry(body) {
|
|
579
|
+
const refs = new Map();
|
|
580
|
+
const countersByNodePath = new Map();
|
|
581
|
+
let figureCount = 0;
|
|
582
|
+
let tableCount = 0;
|
|
583
|
+
let footnoteCount = 0;
|
|
584
|
+
const sectionCounters = [];
|
|
585
|
+
const visit = (node, parentPath, index) => {
|
|
586
|
+
const nodePath = `${parentPath}/${node.kind}:${node.id}:${index}`;
|
|
587
|
+
const counters = {};
|
|
588
|
+
const headingLevel = getHeadingLevel(node);
|
|
589
|
+
if (headingLevel != null) {
|
|
590
|
+
while (sectionCounters.length < headingLevel)
|
|
591
|
+
sectionCounters.push(0);
|
|
592
|
+
sectionCounters.length = headingLevel;
|
|
593
|
+
sectionCounters[headingLevel - 1] += 1;
|
|
594
|
+
const sectionNumber = sectionCounters.join(".");
|
|
595
|
+
counters.section = sectionNumber;
|
|
596
|
+
}
|
|
597
|
+
if (node.kind === "figure") {
|
|
598
|
+
figureCount += 1;
|
|
599
|
+
counters.figure = figureCount;
|
|
600
|
+
}
|
|
601
|
+
if (node.kind === "table") {
|
|
602
|
+
tableCount += 1;
|
|
603
|
+
counters.table = tableCount;
|
|
604
|
+
}
|
|
605
|
+
if (node.kind === "footnote") {
|
|
606
|
+
footnoteCount += 1;
|
|
607
|
+
counters.footnote = footnoteCount;
|
|
608
|
+
}
|
|
609
|
+
const label = getLiteralString(node.props?.label);
|
|
610
|
+
if (label) {
|
|
611
|
+
counters.label = label;
|
|
612
|
+
const refText = formatRefText(node, counters);
|
|
613
|
+
counters.ref = refText;
|
|
614
|
+
refs.set(label, refText);
|
|
615
|
+
}
|
|
616
|
+
if (Object.keys(counters).length) {
|
|
617
|
+
countersByNodePath.set(nodePath, counters);
|
|
618
|
+
}
|
|
619
|
+
node.children?.forEach((child, childIndex) => visit(child, nodePath, childIndex));
|
|
620
|
+
};
|
|
621
|
+
body.nodes.forEach((node, index) => visit(node, "root", index));
|
|
622
|
+
return { refs, countersByNodePath };
|
|
623
|
+
}
|
|
624
|
+
function getHeadingLevel(node) {
|
|
625
|
+
if (node.kind !== "text")
|
|
626
|
+
return null;
|
|
627
|
+
const explicit = getLiteralNumber(node.props?.level);
|
|
628
|
+
if (explicit != null && explicit >= 1)
|
|
629
|
+
return explicit;
|
|
630
|
+
const style = getLiteralString(node.props?.style);
|
|
631
|
+
if (style === "H1")
|
|
632
|
+
return 1;
|
|
633
|
+
if (style === "H2")
|
|
634
|
+
return 2;
|
|
635
|
+
const variant = getLiteralString(node.props?.variant);
|
|
636
|
+
if (variant === "heading")
|
|
637
|
+
return 1;
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
function getLiteralString(value) {
|
|
641
|
+
if (!value || value.kind !== "LiteralValue")
|
|
642
|
+
return null;
|
|
643
|
+
return typeof value.value === "string" ? value.value : null;
|
|
644
|
+
}
|
|
645
|
+
function getLiteralNumber(value) {
|
|
646
|
+
if (!value || value.kind !== "LiteralValue")
|
|
647
|
+
return null;
|
|
648
|
+
return typeof value.value === "number" && Number.isFinite(value.value) ? value.value : null;
|
|
649
|
+
}
|
|
650
|
+
function formatRefText(node, counters) {
|
|
651
|
+
if (counters.section)
|
|
652
|
+
return `§${counters.section}`;
|
|
653
|
+
if (counters.figure != null)
|
|
654
|
+
return `Figure ${counters.figure}`;
|
|
655
|
+
if (counters.table != null)
|
|
656
|
+
return `Table ${counters.table}`;
|
|
657
|
+
if (counters.footnote != null)
|
|
658
|
+
return `Footnote ${counters.footnote}`;
|
|
659
|
+
return counters.label ?? "";
|
|
660
|
+
}
|
|
661
|
+
function buildTokenTree(tokens) {
|
|
662
|
+
const root = {};
|
|
663
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
664
|
+
const parts = key.split(".");
|
|
665
|
+
let cursor = root;
|
|
666
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
667
|
+
const part = parts[i];
|
|
668
|
+
if (i === parts.length - 1) {
|
|
669
|
+
cursor[part] = value;
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
if (!cursor[part] || typeof cursor[part] !== "object") {
|
|
673
|
+
cursor[part] = {};
|
|
674
|
+
}
|
|
675
|
+
cursor = cursor[part];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return root;
|
|
680
|
+
}
|
|
681
|
+
function buildDefaultStyles(tokens) {
|
|
682
|
+
const t = (key) => tokens[key] ?? DEFAULT_TOKENS[key] ?? "";
|
|
683
|
+
const lit = (value) => ({
|
|
684
|
+
kind: "LiteralValue",
|
|
685
|
+
value,
|
|
686
|
+
});
|
|
687
|
+
return [
|
|
688
|
+
{
|
|
689
|
+
name: "Body",
|
|
690
|
+
props: {
|
|
691
|
+
"font.family": lit(t("font.serif")),
|
|
692
|
+
"font.size": lit(10.8),
|
|
693
|
+
"line.height": lit(1.45),
|
|
694
|
+
color: lit(t("color.text")),
|
|
695
|
+
"space.after": lit(t("space.m")),
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: "H1",
|
|
700
|
+
extends: "Body",
|
|
701
|
+
props: {
|
|
702
|
+
"font.size": lit(16.5),
|
|
703
|
+
"font.weight": lit(600),
|
|
704
|
+
"space.before": lit(t("space.l")),
|
|
705
|
+
"space.after": lit(t("space.s")),
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
name: "H2",
|
|
710
|
+
extends: "Body",
|
|
711
|
+
props: {
|
|
712
|
+
"font.size": lit(13),
|
|
713
|
+
"font.weight": lit(600),
|
|
714
|
+
"space.before": lit(t("space.m")),
|
|
715
|
+
"space.after": lit(t("space.s")),
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: "Title",
|
|
720
|
+
extends: "H1",
|
|
721
|
+
props: {
|
|
722
|
+
"font.size": lit(26),
|
|
723
|
+
"letter.spacing": lit("0.02em"),
|
|
724
|
+
"space.after": lit(t("space.s")),
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
name: "Subtitle",
|
|
729
|
+
extends: "Body",
|
|
730
|
+
props: {
|
|
731
|
+
"font.size": lit(12.5),
|
|
732
|
+
color: lit(t("color.muted")),
|
|
733
|
+
"space.after": lit(t("space.m")),
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
name: "Byline",
|
|
738
|
+
extends: "Body",
|
|
739
|
+
props: {
|
|
740
|
+
"font.size": lit(9.5),
|
|
741
|
+
"letter.spacing": lit("0.08em"),
|
|
742
|
+
"text.transform": lit("uppercase"),
|
|
743
|
+
color: lit(t("color.muted")),
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "Abstract",
|
|
748
|
+
extends: "Body",
|
|
749
|
+
props: {
|
|
750
|
+
color: lit(t("color.muted")),
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: "Keywords",
|
|
755
|
+
extends: "Body",
|
|
756
|
+
props: {
|
|
757
|
+
"font.size": lit(9),
|
|
758
|
+
"letter.spacing": lit("0.06em"),
|
|
759
|
+
"text.transform": lit("uppercase"),
|
|
760
|
+
color: lit(t("color.muted")),
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: "Caption",
|
|
765
|
+
extends: "Body",
|
|
766
|
+
props: {
|
|
767
|
+
"font.size": lit(9.5),
|
|
768
|
+
color: lit(t("color.muted")),
|
|
769
|
+
"space.before": lit(t("space.s")),
|
|
770
|
+
"space.after": lit(t("space.xs")),
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: "Credit",
|
|
775
|
+
extends: "Body",
|
|
776
|
+
props: {
|
|
777
|
+
"font.size": lit(8.5),
|
|
778
|
+
color: lit(t("color.muted")),
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
name: "Code",
|
|
783
|
+
extends: "Body",
|
|
784
|
+
props: {
|
|
785
|
+
"font.family": lit(t("font.mono")),
|
|
786
|
+
"font.size": lit(9.5),
|
|
787
|
+
background: lit("#f4f0e9"),
|
|
788
|
+
padding: lit(t("space.s")),
|
|
789
|
+
"border.radius": lit(4),
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: "Quote",
|
|
794
|
+
extends: "Body",
|
|
795
|
+
props: {
|
|
796
|
+
"font.style": lit("italic"),
|
|
797
|
+
color: lit(t("color.muted")),
|
|
798
|
+
"space.before": lit(t("space.s")),
|
|
799
|
+
"space.after": lit(t("space.s")),
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
name: "Callout",
|
|
804
|
+
extends: "Body",
|
|
805
|
+
props: {
|
|
806
|
+
background: lit(t("color.calloutBg")),
|
|
807
|
+
border: lit(`1pt solid ${t("color.calloutBorder")}`),
|
|
808
|
+
padding: lit(t("space.m")),
|
|
809
|
+
"border.radius": lit(6),
|
|
810
|
+
"space.before": lit(t("space.m")),
|
|
811
|
+
"space.after": lit(t("space.m")),
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
// Legacy variant aliases
|
|
815
|
+
{
|
|
816
|
+
name: "Label",
|
|
817
|
+
extends: "Body",
|
|
818
|
+
props: {
|
|
819
|
+
"font.size": lit(8.5),
|
|
820
|
+
"text.transform": lit("uppercase"),
|
|
821
|
+
"letter.spacing": lit("0.14em"),
|
|
822
|
+
color: lit(t("color.muted")),
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
name: "Edition",
|
|
827
|
+
extends: "Body",
|
|
828
|
+
props: {
|
|
829
|
+
"font.size": lit(9.5),
|
|
830
|
+
"letter.spacing": lit("0.08em"),
|
|
831
|
+
"text.transform": lit("uppercase"),
|
|
832
|
+
color: lit(t("color.muted")),
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
name: "Note",
|
|
837
|
+
extends: "Body",
|
|
838
|
+
props: {
|
|
839
|
+
"font.size": lit(8.5),
|
|
840
|
+
color: lit(t("color.muted")),
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: "Sample",
|
|
845
|
+
extends: "Body",
|
|
846
|
+
props: {
|
|
847
|
+
"font.size": lit(10),
|
|
848
|
+
"letter.spacing": lit("0.08em"),
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: "List",
|
|
853
|
+
extends: "Body",
|
|
854
|
+
props: {
|
|
855
|
+
"space.after": lit(t("space.s")),
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: "End",
|
|
860
|
+
extends: "Body",
|
|
861
|
+
props: {
|
|
862
|
+
"font.size": lit(10),
|
|
863
|
+
"text.align": lit("right"),
|
|
864
|
+
"space.before": lit(t("space.m")),
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
];
|
|
868
|
+
}
|
|
869
|
+
function mergeStyleInputs(...styleLists) {
|
|
870
|
+
const map = new Map();
|
|
871
|
+
for (const list of styleLists) {
|
|
872
|
+
for (const style of list) {
|
|
873
|
+
const existing = map.get(style.name);
|
|
874
|
+
if (!existing) {
|
|
875
|
+
map.set(style.name, { ...style, props: { ...style.props } });
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
map.set(style.name, {
|
|
879
|
+
name: style.name,
|
|
880
|
+
extends: style.extends ?? existing.extends,
|
|
881
|
+
props: { ...existing.props, ...style.props },
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return map;
|
|
886
|
+
}
|
|
887
|
+
function resolveTokenExpr(expr, tokens) {
|
|
888
|
+
if (!exprUsesOnlyTokens(expr))
|
|
889
|
+
return null;
|
|
890
|
+
const propSeed = stableHash("style.tokens");
|
|
891
|
+
const rng = mulberry32(propSeed);
|
|
892
|
+
const evalCtx = {
|
|
893
|
+
params: {},
|
|
894
|
+
time: 0,
|
|
895
|
+
docstep: 0,
|
|
896
|
+
rng,
|
|
897
|
+
propSeed,
|
|
898
|
+
assets: [],
|
|
899
|
+
meta: { version: "0.0.0" },
|
|
900
|
+
tokens,
|
|
901
|
+
refs: new Map(),
|
|
902
|
+
};
|
|
903
|
+
const value = evalExpr(expr, evalCtx);
|
|
904
|
+
return toRenderValue(value);
|
|
905
|
+
}
|
|
906
|
+
function exprUsesOnlyTokens(expr) {
|
|
907
|
+
let ok = true;
|
|
908
|
+
const visit = (node) => {
|
|
909
|
+
if (!ok)
|
|
910
|
+
return;
|
|
911
|
+
switch (node.kind) {
|
|
912
|
+
case "Literal":
|
|
913
|
+
return;
|
|
914
|
+
case "Identifier":
|
|
915
|
+
if (node.name !== "tokens")
|
|
916
|
+
ok = false;
|
|
917
|
+
return;
|
|
918
|
+
case "ListExpression":
|
|
919
|
+
node.items.forEach(visit);
|
|
920
|
+
return;
|
|
921
|
+
case "UnaryExpression":
|
|
922
|
+
visit(node.argument);
|
|
923
|
+
return;
|
|
924
|
+
case "BinaryExpression":
|
|
925
|
+
visit(node.left);
|
|
926
|
+
visit(node.right);
|
|
927
|
+
return;
|
|
928
|
+
case "MemberExpression":
|
|
929
|
+
visit(node.object);
|
|
930
|
+
return;
|
|
931
|
+
case "CallExpression":
|
|
932
|
+
case "NeighborsCallExpression":
|
|
933
|
+
default:
|
|
934
|
+
ok = false;
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
visit(expr);
|
|
939
|
+
return ok;
|
|
940
|
+
}
|
|
941
|
+
function resolveStyleSpecs(styleMap, tokens) {
|
|
942
|
+
const resolved = new Map();
|
|
943
|
+
const visiting = new Set();
|
|
944
|
+
const resolve = (name) => {
|
|
945
|
+
const cached = resolved.get(name);
|
|
946
|
+
if (cached)
|
|
947
|
+
return cached;
|
|
948
|
+
if (visiting.has(name)) {
|
|
949
|
+
throw new Error(`Style inheritance cycle detected at '${name}'`);
|
|
950
|
+
}
|
|
951
|
+
visiting.add(name);
|
|
952
|
+
const input = styleMap.get(name);
|
|
953
|
+
const baseName = input?.extends;
|
|
954
|
+
const baseProps = baseName ? resolve(baseName).props : {};
|
|
955
|
+
const mergedProps = { ...baseProps, ...(input?.props ?? {}) };
|
|
956
|
+
const dynamicProps = {};
|
|
957
|
+
const staticProps = {};
|
|
958
|
+
let axesSafe = false;
|
|
959
|
+
for (const [key, value] of Object.entries(mergedProps)) {
|
|
960
|
+
if (key === "font.axes.safe") {
|
|
961
|
+
if (value.kind === "LiteralValue" && value.value === true) {
|
|
962
|
+
axesSafe = true;
|
|
963
|
+
}
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (value.kind === "LiteralValue") {
|
|
967
|
+
staticProps[key] = toRenderValue(value.value);
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
const tokenStatic = resolveTokenExpr(value.expr, tokens);
|
|
971
|
+
if (tokenStatic !== null) {
|
|
972
|
+
staticProps[key] = tokenStatic;
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
dynamicProps[key] = value.expr;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const spec = {
|
|
980
|
+
name,
|
|
981
|
+
className: makeStyleClassName(name),
|
|
982
|
+
props: mergedProps,
|
|
983
|
+
staticProps,
|
|
984
|
+
dynamicProps,
|
|
985
|
+
axesSafe,
|
|
986
|
+
};
|
|
987
|
+
visiting.delete(name);
|
|
988
|
+
resolved.set(name, spec);
|
|
989
|
+
return spec;
|
|
990
|
+
};
|
|
991
|
+
for (const name of styleMap.keys()) {
|
|
992
|
+
resolve(name);
|
|
993
|
+
}
|
|
994
|
+
return resolved;
|
|
995
|
+
}
|
|
996
|
+
function makeStyleClassName(name) {
|
|
997
|
+
return `flux-style-${name.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
998
|
+
}
|
|
999
|
+
const ROLE_STYLE_MAP = {
|
|
1000
|
+
title: "Title",
|
|
1001
|
+
subtitle: "Subtitle",
|
|
1002
|
+
caption: "Caption",
|
|
1003
|
+
credit: "Credit",
|
|
1004
|
+
abstract: "Abstract",
|
|
1005
|
+
keywords: "Keywords",
|
|
1006
|
+
byline: "Byline",
|
|
1007
|
+
};
|
|
1008
|
+
const VARIANT_STYLE_MAP = {
|
|
1009
|
+
title: "Title",
|
|
1010
|
+
subtitle: "Subtitle",
|
|
1011
|
+
heading: "H2",
|
|
1012
|
+
caption: "Caption",
|
|
1013
|
+
credit: "Credit",
|
|
1014
|
+
label: "Label",
|
|
1015
|
+
edition: "Edition",
|
|
1016
|
+
note: "Note",
|
|
1017
|
+
sample: "Sample",
|
|
1018
|
+
list: "List",
|
|
1019
|
+
end: "End",
|
|
1020
|
+
};
|
|
1021
|
+
const KIND_STYLE_MAP = {
|
|
1022
|
+
text: "Body",
|
|
1023
|
+
blockquote: "Quote",
|
|
1024
|
+
codeblock: "Code",
|
|
1025
|
+
callout: "Callout",
|
|
1026
|
+
};
|
|
1027
|
+
function resolveNodeStyle(node, resolvedProps, ctx, evalMeta, insideSlot) {
|
|
1028
|
+
const roleRaw = resolveRenderString(resolvedProps.role);
|
|
1029
|
+
const styleRaw = resolveRenderString(resolvedProps.style);
|
|
1030
|
+
const variantRaw = resolveRenderString(resolvedProps.variant);
|
|
1031
|
+
let name;
|
|
1032
|
+
let role;
|
|
1033
|
+
if (styleRaw) {
|
|
1034
|
+
name = styleRaw;
|
|
1035
|
+
}
|
|
1036
|
+
else if (roleRaw) {
|
|
1037
|
+
role = roleRaw;
|
|
1038
|
+
name = ROLE_STYLE_MAP[roleRaw] ?? roleRaw;
|
|
1039
|
+
}
|
|
1040
|
+
else if (variantRaw) {
|
|
1041
|
+
name = VARIANT_STYLE_MAP[variantRaw] ?? variantRaw;
|
|
1042
|
+
}
|
|
1043
|
+
else if (KIND_STYLE_MAP[node.kind]) {
|
|
1044
|
+
name = KIND_STYLE_MAP[node.kind];
|
|
1045
|
+
}
|
|
1046
|
+
if (!name)
|
|
1047
|
+
return undefined;
|
|
1048
|
+
const spec = ctx.styleRegistry.styles.get(name);
|
|
1049
|
+
const className = spec?.className ?? makeStyleClassName(name);
|
|
1050
|
+
const inline = {};
|
|
1051
|
+
if (spec) {
|
|
1052
|
+
for (const [key, expr] of Object.entries(spec.dynamicProps)) {
|
|
1053
|
+
if (isLayoutSensitiveStyleKey(key)) {
|
|
1054
|
+
const axesAllowed = key.startsWith("font.axes.") &&
|
|
1055
|
+
ctx.styleRegistry.theme === "screen" &&
|
|
1056
|
+
spec.axesSafe;
|
|
1057
|
+
if (!insideSlot && !axesAllowed) {
|
|
1058
|
+
throw new Error(`Dynamic style '${spec.name}.${key}' must be inside a slot or marked axes-safe for screen`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const propSeed = stableHash(ctx.seed, evalMeta.nodePath, "style", spec.name, key, evalMeta.refreshKey);
|
|
1062
|
+
const rng = mulberry32(propSeed);
|
|
1063
|
+
const evalCtx = {
|
|
1064
|
+
params: ctx.params,
|
|
1065
|
+
time: evalMeta.time,
|
|
1066
|
+
docstep: evalMeta.docstep,
|
|
1067
|
+
rng,
|
|
1068
|
+
propSeed,
|
|
1069
|
+
assets: ctx.assets,
|
|
1070
|
+
meta: ctx.doc.meta,
|
|
1071
|
+
tokens: ctx.styleRegistry.tokens,
|
|
1072
|
+
refs: ctx.counterRegistry.refs,
|
|
1073
|
+
};
|
|
1074
|
+
const value = evalExpr(expr, evalCtx);
|
|
1075
|
+
inline[key] = toRenderValue(value);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const style = { name, role, className };
|
|
1079
|
+
if (Object.keys(inline).length > 0) {
|
|
1080
|
+
style.inline = inline;
|
|
1081
|
+
}
|
|
1082
|
+
return style;
|
|
1083
|
+
}
|
|
1084
|
+
function resolveRenderString(value) {
|
|
1085
|
+
if (value == null)
|
|
1086
|
+
return "";
|
|
1087
|
+
if (typeof value === "string")
|
|
1088
|
+
return value;
|
|
1089
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
1090
|
+
return String(value);
|
|
1091
|
+
if (Array.isArray(value))
|
|
1092
|
+
return value.map((item) => resolveRenderString(item)).join(" ");
|
|
1093
|
+
if (typeof value === "object") {
|
|
1094
|
+
if (value.kind === "asset") {
|
|
1095
|
+
return value.name ?? "";
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return "";
|
|
1099
|
+
}
|
|
1100
|
+
function isLayoutSensitiveStyleKey(key) {
|
|
1101
|
+
if (key.startsWith("font.axes."))
|
|
1102
|
+
return true;
|
|
1103
|
+
switch (key) {
|
|
1104
|
+
case "font.family":
|
|
1105
|
+
case "font.size":
|
|
1106
|
+
case "font.weight":
|
|
1107
|
+
case "font.style":
|
|
1108
|
+
case "line.height":
|
|
1109
|
+
case "letter.spacing":
|
|
1110
|
+
case "space.before":
|
|
1111
|
+
case "space.after":
|
|
1112
|
+
case "space.indent":
|
|
1113
|
+
return true;
|
|
1114
|
+
default:
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Deterministic time buckets (avoid tick-rate dependence).
|
|
1119
|
+
const AT_BUCKET_SEC = 0.1;
|
|
1120
|
+
const POISSON_BUCKET_SEC = 0.25;
|
|
1121
|
+
export function didFire(policy, ctx) {
|
|
1122
|
+
switch (policy.kind) {
|
|
1123
|
+
case "never":
|
|
1124
|
+
return { fired: false };
|
|
1125
|
+
case "docstep":
|
|
1126
|
+
return { fired: true, meta: { bucket: ctx.docstep, firedAt: ctx.timeSec } };
|
|
1127
|
+
case "every": {
|
|
1128
|
+
if (policy.intervalSec <= 0)
|
|
1129
|
+
return { fired: false };
|
|
1130
|
+
if (ctx.timeSec < policy.phaseSec) {
|
|
1131
|
+
return { fired: false, meta: { nextTime: policy.phaseSec } };
|
|
1132
|
+
}
|
|
1133
|
+
const bucket = Math.floor((ctx.timeSec - policy.phaseSec) / policy.intervalSec);
|
|
1134
|
+
const firedAt = policy.phaseSec + bucket * policy.intervalSec;
|
|
1135
|
+
return {
|
|
1136
|
+
fired: true,
|
|
1137
|
+
meta: { bucket, firedAt, nextTime: firedAt + policy.intervalSec },
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
case "at": {
|
|
1141
|
+
const bucket = Math.floor(ctx.timeSec / AT_BUCKET_SEC);
|
|
1142
|
+
const targetBucket = Math.floor(policy.timeSec / AT_BUCKET_SEC);
|
|
1143
|
+
return {
|
|
1144
|
+
fired: bucket === targetBucket,
|
|
1145
|
+
meta: { bucket: targetBucket, firedAt: policy.timeSec },
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
case "atEach": {
|
|
1149
|
+
const bucket = Math.floor(ctx.timeSec / AT_BUCKET_SEC);
|
|
1150
|
+
let firedAt;
|
|
1151
|
+
let nextTime;
|
|
1152
|
+
for (const t of policy.timesSec ?? []) {
|
|
1153
|
+
const tBucket = Math.floor(t / AT_BUCKET_SEC);
|
|
1154
|
+
if (tBucket === bucket) {
|
|
1155
|
+
firedAt = firedAt ?? t;
|
|
1156
|
+
}
|
|
1157
|
+
else if (t > ctx.timeSec && nextTime == null) {
|
|
1158
|
+
nextTime = t;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (firedAt == null) {
|
|
1162
|
+
return { fired: false, meta: nextTime != null ? { nextTime } : undefined };
|
|
1163
|
+
}
|
|
1164
|
+
return { fired: true, meta: { bucket, firedAt, nextTime } };
|
|
1165
|
+
}
|
|
1166
|
+
case "poisson": {
|
|
1167
|
+
const bucket = Math.floor(ctx.timeSec / POISSON_BUCKET_SEC);
|
|
1168
|
+
const firedAt = bucket * POISSON_BUCKET_SEC;
|
|
1169
|
+
const rate = Math.max(0, policy.ratePerSec);
|
|
1170
|
+
const p = 1 - Math.exp(-rate * POISSON_BUCKET_SEC);
|
|
1171
|
+
const fired = rand01(ctx.seed, ctx.slotId, `poisson:${bucket}`) < p;
|
|
1172
|
+
return { fired, meta: fired ? { bucket, firedAt } : undefined };
|
|
1173
|
+
}
|
|
1174
|
+
case "chance": {
|
|
1175
|
+
const p = Math.max(0, Math.min(1, policy.p));
|
|
1176
|
+
if (policy.every.kind === "docstep") {
|
|
1177
|
+
const bucket = ctx.docstep;
|
|
1178
|
+
const fired = rand01(ctx.seed, ctx.slotId, `chance:docstep:${bucket}`) < p;
|
|
1179
|
+
return { fired, meta: fired ? { bucket, firedAt: ctx.timeSec } : undefined };
|
|
1180
|
+
}
|
|
1181
|
+
const bucket = Math.floor(ctx.timeSec / policy.every.intervalSec);
|
|
1182
|
+
const firedAt = bucket * policy.every.intervalSec;
|
|
1183
|
+
const fired = rand01(ctx.seed, ctx.slotId, `chance:${bucket}`) < p;
|
|
1184
|
+
return { fired, meta: fired ? { bucket, firedAt } : undefined };
|
|
1185
|
+
}
|
|
1186
|
+
default: {
|
|
1187
|
+
const _exhaustive = policy;
|
|
1188
|
+
return _exhaustive;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
function computeRefreshState(policy, ctx, cached) {
|
|
1193
|
+
const trigger = didFire(policy, {
|
|
1194
|
+
seed: ctx.seed,
|
|
1195
|
+
slotId: ctx.refreshOwnerId,
|
|
1196
|
+
timeSec: ctx.time,
|
|
1197
|
+
docstep: ctx.docstep,
|
|
1198
|
+
});
|
|
1199
|
+
const shouldRefresh = trigger.fired;
|
|
1200
|
+
let refreshKey = cached?.refreshKey ?? 0;
|
|
1201
|
+
let evalTime = cached?.time ?? ctx.time;
|
|
1202
|
+
let evalDocstep = cached?.docstep ?? ctx.docstep;
|
|
1203
|
+
switch (policy.kind) {
|
|
1204
|
+
case "never": {
|
|
1205
|
+
refreshKey = 0;
|
|
1206
|
+
if (!cached) {
|
|
1207
|
+
evalTime = ctx.time;
|
|
1208
|
+
evalDocstep = ctx.docstep;
|
|
1209
|
+
}
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
case "docstep": {
|
|
1213
|
+
refreshKey = ctx.docstep;
|
|
1214
|
+
evalTime = ctx.time;
|
|
1215
|
+
evalDocstep = ctx.docstep;
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
case "every": {
|
|
1219
|
+
if (ctx.time < policy.phaseSec) {
|
|
1220
|
+
if (!cached) {
|
|
1221
|
+
evalTime = ctx.time;
|
|
1222
|
+
evalDocstep = ctx.docstep;
|
|
1223
|
+
}
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
const bucket = Math.floor((ctx.time - policy.phaseSec) / policy.intervalSec);
|
|
1227
|
+
refreshKey = bucket;
|
|
1228
|
+
evalTime = policy.phaseSec + bucket * policy.intervalSec;
|
|
1229
|
+
evalDocstep = ctx.docstep;
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
case "at": {
|
|
1233
|
+
if (trigger.fired) {
|
|
1234
|
+
refreshKey = trigger.meta?.bucket ?? refreshKey;
|
|
1235
|
+
evalTime = policy.timeSec;
|
|
1236
|
+
evalDocstep = ctx.docstep;
|
|
1237
|
+
}
|
|
1238
|
+
else if (!cached) {
|
|
1239
|
+
evalTime = ctx.time;
|
|
1240
|
+
evalDocstep = ctx.docstep;
|
|
1241
|
+
}
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
case "atEach": {
|
|
1245
|
+
if (trigger.fired) {
|
|
1246
|
+
refreshKey = trigger.meta?.bucket ?? refreshKey;
|
|
1247
|
+
evalTime = trigger.meta?.firedAt ?? ctx.time;
|
|
1248
|
+
evalDocstep = ctx.docstep;
|
|
1249
|
+
}
|
|
1250
|
+
else if (!cached) {
|
|
1251
|
+
evalTime = ctx.time;
|
|
1252
|
+
evalDocstep = ctx.docstep;
|
|
1253
|
+
}
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
case "poisson":
|
|
1257
|
+
case "chance": {
|
|
1258
|
+
if (trigger.fired) {
|
|
1259
|
+
refreshKey = trigger.meta?.bucket ?? refreshKey;
|
|
1260
|
+
evalTime = trigger.meta?.firedAt ?? ctx.time;
|
|
1261
|
+
evalDocstep = ctx.docstep;
|
|
1262
|
+
}
|
|
1263
|
+
else if (!cached) {
|
|
1264
|
+
evalTime = ctx.time;
|
|
1265
|
+
evalDocstep = ctx.docstep;
|
|
1266
|
+
}
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
default: {
|
|
1270
|
+
const _exhaustive = policy;
|
|
1271
|
+
return _exhaustive;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return { refreshKey, evalTime, evalDocstep, shouldRefresh, eventMeta: trigger.meta };
|
|
1275
|
+
}
|
|
1276
|
+
function resolveGridData(node, props, ctx) {
|
|
1277
|
+
const snapshot = ctx.legacySnapshot;
|
|
1278
|
+
if (!snapshot)
|
|
1279
|
+
return null;
|
|
1280
|
+
const refValue = props.ref ?? props.name ?? props.grid ?? node.id;
|
|
1281
|
+
const ref = typeof refValue === "string" ? refValue : node.id;
|
|
1282
|
+
const layout = computeGridLayout(ctx.doc, snapshot);
|
|
1283
|
+
const view = layout.grids.find((grid) => grid.name === ref);
|
|
1284
|
+
if (!view)
|
|
1285
|
+
return null;
|
|
1286
|
+
return {
|
|
1287
|
+
name: view.name,
|
|
1288
|
+
rows: view.rows,
|
|
1289
|
+
cols: view.cols,
|
|
1290
|
+
cells: view.cells.map((cell) => ({
|
|
1291
|
+
id: cell.id,
|
|
1292
|
+
row: cell.row,
|
|
1293
|
+
col: cell.col,
|
|
1294
|
+
tags: [...cell.tags],
|
|
1295
|
+
content: cell.content ?? null,
|
|
1296
|
+
mediaId: cell.mediaId ?? null,
|
|
1297
|
+
dynamic: cell.dynamic ?? null,
|
|
1298
|
+
density: cell.density ?? null,
|
|
1299
|
+
salience: cell.salience ?? null,
|
|
1300
|
+
})),
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function assetsToRender(assets) {
|
|
1304
|
+
return assets
|
|
1305
|
+
.map((asset) => ({
|
|
1306
|
+
id: asset.id,
|
|
1307
|
+
name: asset.name,
|
|
1308
|
+
kind: asset.kind,
|
|
1309
|
+
path: asset.path,
|
|
1310
|
+
tags: [...asset.tags],
|
|
1311
|
+
weight: asset.weight,
|
|
1312
|
+
meta: asset.meta ? normalizeMeta(asset.meta) : undefined,
|
|
1313
|
+
source: asset.source,
|
|
1314
|
+
}))
|
|
1315
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1316
|
+
}
|
|
1317
|
+
function normalizeMeta(meta) {
|
|
1318
|
+
const keys = Object.keys(meta).sort();
|
|
1319
|
+
const normalized = {};
|
|
1320
|
+
for (const key of keys) {
|
|
1321
|
+
normalized[key] = meta[key];
|
|
1322
|
+
}
|
|
1323
|
+
return normalized;
|
|
1324
|
+
}
|
|
1325
|
+
function toRenderValue(value) {
|
|
1326
|
+
if (value == null)
|
|
1327
|
+
return null;
|
|
1328
|
+
if (Array.isArray(value)) {
|
|
1329
|
+
return value.map((item) => toRenderValue(item));
|
|
1330
|
+
}
|
|
1331
|
+
if (typeof value === "object") {
|
|
1332
|
+
if (isResolvedAsset(value)) {
|
|
1333
|
+
return {
|
|
1334
|
+
kind: "asset",
|
|
1335
|
+
id: value.id,
|
|
1336
|
+
path: value.path,
|
|
1337
|
+
name: value.name,
|
|
1338
|
+
assetKind: value.kind,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
const entries = Object.entries(value);
|
|
1342
|
+
const normalized = {};
|
|
1343
|
+
for (const [key, item] of entries) {
|
|
1344
|
+
normalized[key] = toRenderValue(item);
|
|
1345
|
+
}
|
|
1346
|
+
return normalized;
|
|
1347
|
+
}
|
|
1348
|
+
return value;
|
|
1349
|
+
}
|
|
1350
|
+
function evalExpr(expr, ctx) {
|
|
1351
|
+
switch (expr.kind) {
|
|
1352
|
+
case "Literal":
|
|
1353
|
+
return expr.value;
|
|
1354
|
+
case "ListExpression":
|
|
1355
|
+
return expr.items.map((item) => evalExpr(item, ctx));
|
|
1356
|
+
case "Identifier":
|
|
1357
|
+
return evalIdentifier(expr.name, ctx);
|
|
1358
|
+
case "UnaryExpression":
|
|
1359
|
+
return evalUnary(expr.op, expr.argument, ctx);
|
|
1360
|
+
case "BinaryExpression":
|
|
1361
|
+
return evalBinary(expr.op, expr.left, expr.right, ctx);
|
|
1362
|
+
case "MemberExpression":
|
|
1363
|
+
return evalMember(expr.object, expr.property, ctx);
|
|
1364
|
+
case "CallExpression":
|
|
1365
|
+
return evalCall(expr, ctx);
|
|
1366
|
+
case "NeighborsCallExpression":
|
|
1367
|
+
throw new Error("neighbors.*() is not supported in document expressions");
|
|
1368
|
+
default: {
|
|
1369
|
+
throw new Error(`Unsupported expression kind '${expr?.kind ?? "unknown"}'`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function evalIdentifier(name, ctx) {
|
|
1374
|
+
if (name === "params")
|
|
1375
|
+
return ctx.params;
|
|
1376
|
+
if (name === "time" || name === "timeSeconds")
|
|
1377
|
+
return ctx.time;
|
|
1378
|
+
if (name === "docstep")
|
|
1379
|
+
return ctx.docstep;
|
|
1380
|
+
if (name === "meta")
|
|
1381
|
+
return ctx.meta;
|
|
1382
|
+
if (name === "tokens")
|
|
1383
|
+
return ctx.tokens;
|
|
1384
|
+
return ctx.params[name];
|
|
1385
|
+
}
|
|
1386
|
+
function evalUnary(op, argument, ctx) {
|
|
1387
|
+
const value = evalExpr(argument, ctx);
|
|
1388
|
+
switch (op) {
|
|
1389
|
+
case "not":
|
|
1390
|
+
return !value;
|
|
1391
|
+
case "-":
|
|
1392
|
+
return -value;
|
|
1393
|
+
default:
|
|
1394
|
+
throw new Error(`Unsupported unary operator '${op}'`);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function evalBinary(op, left, right, ctx) {
|
|
1398
|
+
switch (op) {
|
|
1399
|
+
case "and": {
|
|
1400
|
+
const l = evalExpr(left, ctx);
|
|
1401
|
+
return Boolean(l) && Boolean(evalExpr(right, ctx));
|
|
1402
|
+
}
|
|
1403
|
+
case "or": {
|
|
1404
|
+
const l = evalExpr(left, ctx);
|
|
1405
|
+
return Boolean(l) || Boolean(evalExpr(right, ctx));
|
|
1406
|
+
}
|
|
1407
|
+
default: {
|
|
1408
|
+
const l = evalExpr(left, ctx);
|
|
1409
|
+
const r = evalExpr(right, ctx);
|
|
1410
|
+
switch (op) {
|
|
1411
|
+
case "==":
|
|
1412
|
+
return l === r;
|
|
1413
|
+
case "!=":
|
|
1414
|
+
return l !== r;
|
|
1415
|
+
case "===":
|
|
1416
|
+
return l === r;
|
|
1417
|
+
case "!==":
|
|
1418
|
+
return l !== r;
|
|
1419
|
+
case "<":
|
|
1420
|
+
return l < r;
|
|
1421
|
+
case "<=":
|
|
1422
|
+
return l <= r;
|
|
1423
|
+
case ">":
|
|
1424
|
+
return l > r;
|
|
1425
|
+
case ">=":
|
|
1426
|
+
return l >= r;
|
|
1427
|
+
case "+":
|
|
1428
|
+
return l + r;
|
|
1429
|
+
case "-":
|
|
1430
|
+
return l - r;
|
|
1431
|
+
case "*":
|
|
1432
|
+
return l * r;
|
|
1433
|
+
case "/":
|
|
1434
|
+
return l / r;
|
|
1435
|
+
default:
|
|
1436
|
+
throw new Error(`Unsupported binary operator '${op}'`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function evalMember(objectExpr, property, ctx) {
|
|
1442
|
+
const obj = evalExpr(objectExpr, ctx);
|
|
1443
|
+
if (obj == null) {
|
|
1444
|
+
throw new Error(`Cannot read property '${property}' of null/undefined`);
|
|
1445
|
+
}
|
|
1446
|
+
return obj[property];
|
|
1447
|
+
}
|
|
1448
|
+
function evalCall(expr, ctx) {
|
|
1449
|
+
// Supported document-expression builtins:
|
|
1450
|
+
// choose, chooseStep, now/timeSeconds, stableHash, assets.pick
|
|
1451
|
+
if (expr.callee.kind === "Identifier") {
|
|
1452
|
+
const name = expr.callee.name;
|
|
1453
|
+
if (name === "choose") {
|
|
1454
|
+
return evalChoose(expr.args, ctx);
|
|
1455
|
+
}
|
|
1456
|
+
if (name === "chooseStep") {
|
|
1457
|
+
return evalChooseStep(expr.args, ctx);
|
|
1458
|
+
}
|
|
1459
|
+
if (name === "cycle") {
|
|
1460
|
+
return evalCycle(expr.args, ctx);
|
|
1461
|
+
}
|
|
1462
|
+
if (name === "hashpick") {
|
|
1463
|
+
return evalHashpick(expr.args, ctx);
|
|
1464
|
+
}
|
|
1465
|
+
if (name === "phase") {
|
|
1466
|
+
return evalPhase(expr.args, ctx);
|
|
1467
|
+
}
|
|
1468
|
+
if (name === "lerp") {
|
|
1469
|
+
return evalLerp(expr.args, ctx);
|
|
1470
|
+
}
|
|
1471
|
+
if (name === "shuffle") {
|
|
1472
|
+
return evalShuffle(expr.args, ctx);
|
|
1473
|
+
}
|
|
1474
|
+
if (name === "sample") {
|
|
1475
|
+
return evalSample(expr.args, ctx);
|
|
1476
|
+
}
|
|
1477
|
+
if (name === "ref") {
|
|
1478
|
+
return evalRef(expr.args, ctx);
|
|
1479
|
+
}
|
|
1480
|
+
if (name === "now" || name === "timeSeconds") {
|
|
1481
|
+
return ctx.time;
|
|
1482
|
+
}
|
|
1483
|
+
if (name === "stableHash") {
|
|
1484
|
+
const values = evalCallArgs(expr.args, ctx);
|
|
1485
|
+
return stableHash(...values.positional, values.named);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (expr.callee.kind === "MemberExpression" &&
|
|
1489
|
+
expr.callee.object.kind === "Identifier" &&
|
|
1490
|
+
expr.callee.object.name === "assets" &&
|
|
1491
|
+
(expr.callee.property === "pick" || expr.callee.property === "shuffle")) {
|
|
1492
|
+
if (expr.callee.property === "shuffle") {
|
|
1493
|
+
return evalAssetsShuffle(expr.args, ctx);
|
|
1494
|
+
}
|
|
1495
|
+
return evalAssetsPick(expr.args, ctx);
|
|
1496
|
+
}
|
|
1497
|
+
const calleeName = describeCallee(expr.callee) ?? "unknown";
|
|
1498
|
+
const location = formatExprLocation(expr);
|
|
1499
|
+
throw new Error(`Unsupported function call '${calleeName}' in document expressions${location}`);
|
|
1500
|
+
}
|
|
1501
|
+
function evalCallArgs(args, ctx) {
|
|
1502
|
+
const positional = [];
|
|
1503
|
+
const named = {};
|
|
1504
|
+
for (const arg of args ?? []) {
|
|
1505
|
+
if (arg.kind === "NamedArg") {
|
|
1506
|
+
named[arg.name] = evalExpr(arg.value, ctx);
|
|
1507
|
+
}
|
|
1508
|
+
else {
|
|
1509
|
+
positional.push(evalExpr(arg, ctx));
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return { positional, named };
|
|
1513
|
+
}
|
|
1514
|
+
function resolveListArgument(positional, named, fnName) {
|
|
1515
|
+
if (named.list !== undefined) {
|
|
1516
|
+
if (!Array.isArray(named.list)) {
|
|
1517
|
+
throw new Error(`${fnName}(list) expects a list`);
|
|
1518
|
+
}
|
|
1519
|
+
return { list: named.list, usedVariadic: false };
|
|
1520
|
+
}
|
|
1521
|
+
if (Array.isArray(positional[0])) {
|
|
1522
|
+
return { list: positional[0], usedVariadic: false };
|
|
1523
|
+
}
|
|
1524
|
+
if (positional.length > 1) {
|
|
1525
|
+
return { list: positional, usedVariadic: true };
|
|
1526
|
+
}
|
|
1527
|
+
throw new Error(`${fnName}(list) expects a list`);
|
|
1528
|
+
}
|
|
1529
|
+
function evalChoose(args, ctx) {
|
|
1530
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1531
|
+
const { list } = resolveListArgument(positional, named, "choose");
|
|
1532
|
+
if (list.length === 0)
|
|
1533
|
+
return null;
|
|
1534
|
+
const idx = Math.floor(ctx.rng() * list.length);
|
|
1535
|
+
return list[idx];
|
|
1536
|
+
}
|
|
1537
|
+
function evalChooseStep(args, ctx) {
|
|
1538
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1539
|
+
const list = (named.list ?? positional[0]);
|
|
1540
|
+
if (!Array.isArray(list)) {
|
|
1541
|
+
throw new Error("chooseStep(list) expects a list");
|
|
1542
|
+
}
|
|
1543
|
+
if (list.length === 0) {
|
|
1544
|
+
throw new Error("chooseStep(list) expects a non-empty list");
|
|
1545
|
+
}
|
|
1546
|
+
const offsetRaw = named.offset ?? 0;
|
|
1547
|
+
const offset = typeof offsetRaw === "number" && Number.isFinite(offsetRaw) ? offsetRaw : 0;
|
|
1548
|
+
const idx = Math.abs(Math.floor(ctx.docstep + offset)) % list.length;
|
|
1549
|
+
return list[idx];
|
|
1550
|
+
}
|
|
1551
|
+
function evalCycle(args, ctx) {
|
|
1552
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1553
|
+
const { list, usedVariadic } = resolveListArgument(positional, named, "cycle");
|
|
1554
|
+
if (list.length === 0)
|
|
1555
|
+
return null;
|
|
1556
|
+
const rawIndex = named.index ?? (usedVariadic ? undefined : positional[1]) ?? (named.list !== undefined ? positional[0] : undefined) ?? ctx.docstep;
|
|
1557
|
+
const indexValue = typeof rawIndex === "number" && Number.isFinite(rawIndex) ? rawIndex : 0;
|
|
1558
|
+
const idx = ((Math.floor(indexValue) % list.length) + list.length) % list.length;
|
|
1559
|
+
return list[idx];
|
|
1560
|
+
}
|
|
1561
|
+
function evalHashpick(args, ctx) {
|
|
1562
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1563
|
+
const list = (named.list ?? positional[0]);
|
|
1564
|
+
if (!Array.isArray(list)) {
|
|
1565
|
+
throw new Error("hashpick(list, key) expects a list");
|
|
1566
|
+
}
|
|
1567
|
+
if (list.length === 0)
|
|
1568
|
+
return null;
|
|
1569
|
+
const keyRaw = named.key ?? positional[1];
|
|
1570
|
+
if (keyRaw == null) {
|
|
1571
|
+
throw new Error("hashpick(list, key) expects a key");
|
|
1572
|
+
}
|
|
1573
|
+
const hash = stableHash(String(keyRaw));
|
|
1574
|
+
const idx = hash % list.length;
|
|
1575
|
+
return list[idx];
|
|
1576
|
+
}
|
|
1577
|
+
function evalPhase(args, ctx) {
|
|
1578
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1579
|
+
const raw = named.value ?? positional[0] ?? ctx.docstep;
|
|
1580
|
+
const value = typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
|
|
1581
|
+
const floor = Math.floor(value);
|
|
1582
|
+
return value - floor;
|
|
1583
|
+
}
|
|
1584
|
+
function evalLerp(args, ctx) {
|
|
1585
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1586
|
+
const a = named.a ?? positional[0];
|
|
1587
|
+
const b = named.b ?? positional[1];
|
|
1588
|
+
const t = named.t ?? positional[2];
|
|
1589
|
+
if (![a, b, t].every((v) => typeof v === "number" && Number.isFinite(v))) {
|
|
1590
|
+
throw new Error("lerp(a, b, t) expects numeric arguments");
|
|
1591
|
+
}
|
|
1592
|
+
return a + (b - a) * t;
|
|
1593
|
+
}
|
|
1594
|
+
function evalShuffle(args, ctx) {
|
|
1595
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1596
|
+
const list = (named.list ?? positional[0]);
|
|
1597
|
+
if (!Array.isArray(list)) {
|
|
1598
|
+
throw new Error("shuffle(list) expects a list");
|
|
1599
|
+
}
|
|
1600
|
+
return shuffleList(list, ctx.rng);
|
|
1601
|
+
}
|
|
1602
|
+
function evalSample(args, ctx) {
|
|
1603
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1604
|
+
const list = (named.list ?? positional[0]);
|
|
1605
|
+
if (!Array.isArray(list)) {
|
|
1606
|
+
throw new Error("sample(list, n) expects a list");
|
|
1607
|
+
}
|
|
1608
|
+
const nRaw = named.n ?? positional[1] ?? 1;
|
|
1609
|
+
const n = typeof nRaw === "number" && Number.isFinite(nRaw) ? Math.floor(nRaw) : 1;
|
|
1610
|
+
if (n <= 0)
|
|
1611
|
+
return [];
|
|
1612
|
+
const shuffled = shuffleList(list, ctx.rng);
|
|
1613
|
+
return shuffled.slice(0, Math.min(n, shuffled.length));
|
|
1614
|
+
}
|
|
1615
|
+
function evalRef(args, ctx) {
|
|
1616
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1617
|
+
const label = named.label ?? positional[0];
|
|
1618
|
+
if (typeof label !== "string") {
|
|
1619
|
+
throw new Error("ref(label) expects a string label");
|
|
1620
|
+
}
|
|
1621
|
+
const resolved = ctx.refs.get(label);
|
|
1622
|
+
if (!resolved) {
|
|
1623
|
+
throw new Error(`ref('${label}') target not found`);
|
|
1624
|
+
}
|
|
1625
|
+
return resolved;
|
|
1626
|
+
}
|
|
1627
|
+
function shuffleList(list, rng) {
|
|
1628
|
+
const items = [...list];
|
|
1629
|
+
for (let i = items.length - 1; i > 0; i -= 1) {
|
|
1630
|
+
const j = Math.floor(rng() * (i + 1));
|
|
1631
|
+
[items[i], items[j]] = [items[j], items[i]];
|
|
1632
|
+
}
|
|
1633
|
+
return items;
|
|
1634
|
+
}
|
|
1635
|
+
function describeCallee(callee) {
|
|
1636
|
+
if (!callee)
|
|
1637
|
+
return null;
|
|
1638
|
+
if (callee.kind === "Identifier")
|
|
1639
|
+
return String(callee.name);
|
|
1640
|
+
if (callee.kind === "MemberExpression") {
|
|
1641
|
+
const object = callee.object;
|
|
1642
|
+
const objectName = object?.kind === "Identifier" ? object.name : null;
|
|
1643
|
+
const property = typeof callee.property === "string" ? callee.property : null;
|
|
1644
|
+
if (objectName && property) {
|
|
1645
|
+
return `${objectName}.${property}`;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
function formatExprLocation(expr) {
|
|
1651
|
+
const loc = expr?.location ?? expr?.loc ?? null;
|
|
1652
|
+
const line = typeof loc?.line === "number" ? loc.line : typeof expr?.line === "number" ? expr.line : null;
|
|
1653
|
+
const column = typeof loc?.column === "number"
|
|
1654
|
+
? loc.column
|
|
1655
|
+
: typeof expr?.column === "number"
|
|
1656
|
+
? expr.column
|
|
1657
|
+
: null;
|
|
1658
|
+
if (line != null && column != null) {
|
|
1659
|
+
return ` at ${line}:${column}`;
|
|
1660
|
+
}
|
|
1661
|
+
return "";
|
|
1662
|
+
}
|
|
1663
|
+
function evalAssetsPick(args, ctx) {
|
|
1664
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1665
|
+
const rawTags = named.tags ?? positional[0];
|
|
1666
|
+
const rawExclude = named.excludeTags;
|
|
1667
|
+
const rawStrategy = named.strategy ?? positional[1];
|
|
1668
|
+
const rawSeed = named.seed ?? positional[2];
|
|
1669
|
+
const rawNoRepeat = named.noRepeatSteps ?? named.noRepeat ?? positional[3];
|
|
1670
|
+
const tags = Array.isArray(rawTags)
|
|
1671
|
+
? rawTags.map((tag) => String(tag))
|
|
1672
|
+
: rawTags
|
|
1673
|
+
? [String(rawTags)]
|
|
1674
|
+
: [];
|
|
1675
|
+
const excludeTags = Array.isArray(rawExclude)
|
|
1676
|
+
? rawExclude.map((tag) => String(tag))
|
|
1677
|
+
: rawExclude
|
|
1678
|
+
? [String(rawExclude)]
|
|
1679
|
+
: [];
|
|
1680
|
+
const strategy = typeof rawStrategy === "string" ? rawStrategy : undefined;
|
|
1681
|
+
if (strategy && strategy !== "weighted" && strategy !== "uniform") {
|
|
1682
|
+
throw new Error(`Unknown asset pick strategy '${strategy}'`);
|
|
1683
|
+
}
|
|
1684
|
+
const candidates = filterAssets(ctx.assets, tags, excludeTags);
|
|
1685
|
+
if (candidates.length === 0)
|
|
1686
|
+
return null;
|
|
1687
|
+
const resolvedStrategy = strategy ??
|
|
1688
|
+
(candidates.every((asset) => asset.strategy === candidates[0].strategy)
|
|
1689
|
+
? candidates[0].strategy
|
|
1690
|
+
: undefined) ??
|
|
1691
|
+
"uniform";
|
|
1692
|
+
const noRepeatSteps = typeof rawNoRepeat === "number" && Number.isFinite(rawNoRepeat) ? Math.max(0, Math.floor(rawNoRepeat)) : 0;
|
|
1693
|
+
const baseRng = rawSeed != null
|
|
1694
|
+
? mulberry32(stableHash(ctx.propSeed, rawSeed, "assets.pick"))
|
|
1695
|
+
: ctx.rng;
|
|
1696
|
+
if (noRepeatSteps > 0) {
|
|
1697
|
+
const history = new Set();
|
|
1698
|
+
for (let i = 1; i <= noRepeatSteps; i += 1) {
|
|
1699
|
+
const prev = pickAssetAtStep(candidates, resolvedStrategy, ctx, ctx.docstep - i, 0);
|
|
1700
|
+
if (prev)
|
|
1701
|
+
history.add(prev.id);
|
|
1702
|
+
}
|
|
1703
|
+
for (let salt = 0; salt < candidates.length; salt += 1) {
|
|
1704
|
+
const candidate = pickAssetAtStep(candidates, resolvedStrategy, ctx, ctx.docstep, salt);
|
|
1705
|
+
if (candidate && !history.has(candidate.id))
|
|
1706
|
+
return candidate;
|
|
1707
|
+
}
|
|
1708
|
+
return pickAssetAtStep(candidates, resolvedStrategy, ctx, ctx.docstep, 0);
|
|
1709
|
+
}
|
|
1710
|
+
return pickAssetByStrategy(candidates, resolvedStrategy, baseRng);
|
|
1711
|
+
}
|
|
1712
|
+
function evalAssetsShuffle(args, ctx) {
|
|
1713
|
+
const { positional, named } = evalCallArgs(args, ctx);
|
|
1714
|
+
const rawTags = named.tags ?? positional[0];
|
|
1715
|
+
const rawExclude = named.excludeTags;
|
|
1716
|
+
const rawSeed = named.seed ?? positional[1];
|
|
1717
|
+
const tags = Array.isArray(rawTags)
|
|
1718
|
+
? rawTags.map((tag) => String(tag))
|
|
1719
|
+
: rawTags
|
|
1720
|
+
? [String(rawTags)]
|
|
1721
|
+
: [];
|
|
1722
|
+
const excludeTags = Array.isArray(rawExclude)
|
|
1723
|
+
? rawExclude.map((tag) => String(tag))
|
|
1724
|
+
: rawExclude
|
|
1725
|
+
? [String(rawExclude)]
|
|
1726
|
+
: [];
|
|
1727
|
+
const candidates = filterAssets(ctx.assets, tags, excludeTags);
|
|
1728
|
+
if (candidates.length === 0)
|
|
1729
|
+
return [];
|
|
1730
|
+
const rng = rawSeed != null
|
|
1731
|
+
? mulberry32(stableHash(ctx.propSeed, rawSeed, "assets.shuffle"))
|
|
1732
|
+
: mulberry32(stableHash(ctx.propSeed, "assets.shuffle"));
|
|
1733
|
+
return shuffleList(candidates, rng);
|
|
1734
|
+
}
|
|
1735
|
+
function filterAssets(assets, tags, excludeTags) {
|
|
1736
|
+
return assets.filter((asset) => {
|
|
1737
|
+
if (tags.length && !tags.every((tag) => asset.tags.includes(tag)))
|
|
1738
|
+
return false;
|
|
1739
|
+
if (excludeTags.length && excludeTags.some((tag) => asset.tags.includes(tag)))
|
|
1740
|
+
return false;
|
|
1741
|
+
return true;
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
function pickAssetAtStep(candidates, strategy, ctx, step, salt) {
|
|
1745
|
+
if (candidates.length === 0)
|
|
1746
|
+
return null;
|
|
1747
|
+
const rng = mulberry32(stableHash(ctx.propSeed, step, salt, "assets.pick.step"));
|
|
1748
|
+
return pickAssetByStrategy(candidates, strategy, rng);
|
|
1749
|
+
}
|
|
1750
|
+
function pickAssetByStrategy(candidates, strategy, rng) {
|
|
1751
|
+
if (strategy === "weighted") {
|
|
1752
|
+
const total = candidates.reduce((sum, asset) => sum + (Number.isFinite(asset.weight) ? asset.weight : 1), 0);
|
|
1753
|
+
if (total <= 0) {
|
|
1754
|
+
const idx = Math.floor(rng() * candidates.length);
|
|
1755
|
+
return candidates[idx];
|
|
1756
|
+
}
|
|
1757
|
+
let roll = rng() * total;
|
|
1758
|
+
for (const asset of candidates) {
|
|
1759
|
+
const weight = Number.isFinite(asset.weight) ? asset.weight : 1;
|
|
1760
|
+
roll -= weight;
|
|
1761
|
+
if (roll <= 0)
|
|
1762
|
+
return asset;
|
|
1763
|
+
}
|
|
1764
|
+
return candidates[candidates.length - 1];
|
|
1765
|
+
}
|
|
1766
|
+
const idx = Math.floor(rng() * candidates.length);
|
|
1767
|
+
return candidates[idx];
|
|
1768
|
+
}
|
|
1769
|
+
function buildAssetCatalog(doc, options) {
|
|
1770
|
+
const assets = [];
|
|
1771
|
+
const blocks = doc.assets;
|
|
1772
|
+
const resolver = options.assetResolver ?? defaultAssetResolver;
|
|
1773
|
+
if (blocks) {
|
|
1774
|
+
for (const asset of blocks.assets ?? []) {
|
|
1775
|
+
assets.push(materializeAssetDefinition(asset));
|
|
1776
|
+
}
|
|
1777
|
+
for (const bank of blocks.banks ?? []) {
|
|
1778
|
+
const entries = resolver(bank, { cwd: options.assetCwd });
|
|
1779
|
+
const root = normalizePath(bank.root);
|
|
1780
|
+
for (const rel of entries) {
|
|
1781
|
+
const relPath = normalizePath(rel);
|
|
1782
|
+
const fullPath = root ? `${root}/${relPath}` : relPath;
|
|
1783
|
+
assets.push(materializeBankAsset(bank, relPath, fullPath));
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (doc.materials) {
|
|
1788
|
+
assets.push(...materializeMaterials(doc.materials));
|
|
1789
|
+
}
|
|
1790
|
+
return assets;
|
|
1791
|
+
}
|
|
1792
|
+
function materializeAssetDefinition(asset) {
|
|
1793
|
+
const pathValue = normalizePath(asset.path);
|
|
1794
|
+
return {
|
|
1795
|
+
__asset: true,
|
|
1796
|
+
id: makeAssetId("asset", asset.name, asset.kind, pathValue),
|
|
1797
|
+
name: asset.name,
|
|
1798
|
+
kind: asset.kind,
|
|
1799
|
+
path: pathValue,
|
|
1800
|
+
tags: [...(asset.tags ?? [])],
|
|
1801
|
+
weight: typeof asset.weight === "number" && Number.isFinite(asset.weight)
|
|
1802
|
+
? asset.weight
|
|
1803
|
+
: 1,
|
|
1804
|
+
meta: asset.meta ? normalizeMeta(toRenderValue(asset.meta)) : undefined,
|
|
1805
|
+
source: { type: "asset", name: asset.name },
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
function materializeBankAsset(bank, relPath, fullPath) {
|
|
1809
|
+
return {
|
|
1810
|
+
__asset: true,
|
|
1811
|
+
id: makeAssetId("bank", bank.name, bank.kind, relPath),
|
|
1812
|
+
name: relPath,
|
|
1813
|
+
kind: bank.kind,
|
|
1814
|
+
path: fullPath,
|
|
1815
|
+
tags: [...(bank.tags ?? []), `bank:${bank.name}`],
|
|
1816
|
+
weight: 1,
|
|
1817
|
+
source: { type: "bank", name: bank.name },
|
|
1818
|
+
strategy: bank.strategy,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function materializeMaterials(block) {
|
|
1822
|
+
const assets = [];
|
|
1823
|
+
for (const material of block.materials ?? []) {
|
|
1824
|
+
const meta = {
|
|
1825
|
+
label: material.label ?? null,
|
|
1826
|
+
description: material.description ?? null,
|
|
1827
|
+
color: material.color ?? null,
|
|
1828
|
+
};
|
|
1829
|
+
if (material.score) {
|
|
1830
|
+
meta.score = toRenderValue(material.score);
|
|
1831
|
+
}
|
|
1832
|
+
if (material.midi) {
|
|
1833
|
+
meta.midi = toRenderValue(material.midi);
|
|
1834
|
+
}
|
|
1835
|
+
if (material.video) {
|
|
1836
|
+
meta.video = toRenderValue(material.video);
|
|
1837
|
+
}
|
|
1838
|
+
assets.push({
|
|
1839
|
+
__asset: true,
|
|
1840
|
+
id: makeAssetId("material", material.name, "material", material.name),
|
|
1841
|
+
name: material.name,
|
|
1842
|
+
kind: "material",
|
|
1843
|
+
path: "",
|
|
1844
|
+
tags: [...(material.tags ?? [])],
|
|
1845
|
+
weight: 1,
|
|
1846
|
+
meta,
|
|
1847
|
+
source: { type: "material", name: material.name },
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
return assets;
|
|
1851
|
+
}
|
|
1852
|
+
function defaultAssetResolver(bank, options) {
|
|
1853
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1854
|
+
const rootAbs = path.resolve(cwd, bank.root);
|
|
1855
|
+
if (!fs.existsSync(rootAbs)) {
|
|
1856
|
+
return [];
|
|
1857
|
+
}
|
|
1858
|
+
const files = walkFiles(rootAbs);
|
|
1859
|
+
const matcher = globToRegExp(normalizePath(bank.include));
|
|
1860
|
+
const matches = files
|
|
1861
|
+
.map((filePath) => normalizePath(path.relative(rootAbs, filePath)))
|
|
1862
|
+
.filter((rel) => matcher.test(rel))
|
|
1863
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1864
|
+
return matches;
|
|
1865
|
+
}
|
|
1866
|
+
function walkFiles(dir) {
|
|
1867
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1868
|
+
const results = [];
|
|
1869
|
+
for (const entry of entries) {
|
|
1870
|
+
const full = path.join(dir, entry.name);
|
|
1871
|
+
if (entry.isDirectory()) {
|
|
1872
|
+
results.push(...walkFiles(full));
|
|
1873
|
+
}
|
|
1874
|
+
else if (entry.isFile()) {
|
|
1875
|
+
results.push(full);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return results;
|
|
1879
|
+
}
|
|
1880
|
+
function globToRegExp(pattern) {
|
|
1881
|
+
let regex = "^";
|
|
1882
|
+
let i = 0;
|
|
1883
|
+
while (i < pattern.length) {
|
|
1884
|
+
const ch = pattern[i];
|
|
1885
|
+
if (ch === "*") {
|
|
1886
|
+
if (pattern[i + 1] === "*") {
|
|
1887
|
+
regex += ".*";
|
|
1888
|
+
i += 2;
|
|
1889
|
+
}
|
|
1890
|
+
else {
|
|
1891
|
+
regex += "[^/]*";
|
|
1892
|
+
i += 1;
|
|
1893
|
+
}
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
if (ch === "?") {
|
|
1897
|
+
regex += "[^/]";
|
|
1898
|
+
i += 1;
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
if ("\\.^$+()[]{}|".includes(ch)) {
|
|
1902
|
+
regex += `\\${ch}`;
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
regex += ch;
|
|
1906
|
+
}
|
|
1907
|
+
i += 1;
|
|
1908
|
+
}
|
|
1909
|
+
regex += "$";
|
|
1910
|
+
return new RegExp(regex);
|
|
1911
|
+
}
|
|
1912
|
+
function normalizePath(value) {
|
|
1913
|
+
if (!value)
|
|
1914
|
+
return "";
|
|
1915
|
+
return value.split(path.sep).join("/").replace(/\/+/g, "/").replace(/^\.\//, "");
|
|
1916
|
+
}
|
|
1917
|
+
function isResolvedAsset(value) {
|
|
1918
|
+
return Boolean(value && typeof value === "object" && value.__asset);
|
|
1919
|
+
}
|
|
1920
|
+
function makeAssetId(prefix, name, kind, pathValue) {
|
|
1921
|
+
const hash = stableHash(prefix, name, kind, pathValue);
|
|
1922
|
+
return `${prefix}_${hash.toString(16).padStart(8, "0")}`;
|
|
1923
|
+
}
|
|
1924
|
+
function rand01(seed, slotId, key) {
|
|
1925
|
+
const hash = stableHash(seed, slotId, key);
|
|
1926
|
+
const rng = mulberry32(hash);
|
|
1927
|
+
return rng();
|
|
1928
|
+
}
|
|
1929
|
+
function stableHash(...values) {
|
|
1930
|
+
const serialized = values.map((value) => stableSerialize(value)).join("|");
|
|
1931
|
+
let hash = 0x811c9dc5;
|
|
1932
|
+
for (let i = 0; i < serialized.length; i += 1) {
|
|
1933
|
+
hash ^= serialized.charCodeAt(i);
|
|
1934
|
+
hash = Math.imul(hash, 0x01000193);
|
|
1935
|
+
}
|
|
1936
|
+
return hash >>> 0;
|
|
1937
|
+
}
|
|
1938
|
+
function stableSerialize(value) {
|
|
1939
|
+
if (value == null)
|
|
1940
|
+
return "null";
|
|
1941
|
+
if (typeof value === "number")
|
|
1942
|
+
return `n:${String(value)}`;
|
|
1943
|
+
if (typeof value === "string")
|
|
1944
|
+
return `s:${value}`;
|
|
1945
|
+
if (typeof value === "boolean")
|
|
1946
|
+
return `b:${value}`;
|
|
1947
|
+
if (Array.isArray(value)) {
|
|
1948
|
+
return `a:[${value.map((item) => stableSerialize(item)).join(",")}]`;
|
|
1949
|
+
}
|
|
1950
|
+
if (typeof value === "object") {
|
|
1951
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
1952
|
+
return `o:{${entries
|
|
1953
|
+
.map(([key, val]) => `${key}:${stableSerialize(val)}`)
|
|
1954
|
+
.join(",")}}`;
|
|
1955
|
+
}
|
|
1956
|
+
return `u:${String(value)}`;
|
|
1957
|
+
}
|
|
1958
|
+
function mulberry32(seed) {
|
|
1959
|
+
let t = seed >>> 0;
|
|
1960
|
+
return () => {
|
|
1961
|
+
t += 0x6d2b79f5;
|
|
1962
|
+
let r = t;
|
|
1963
|
+
r = Math.imul(r ^ (r >>> 15), r | 1);
|
|
1964
|
+
r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
|
|
1965
|
+
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
//# sourceMappingURL=render.js.map
|