@patterkit/compiler 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/dist/index.cjs +367 -0
- package/dist/index.d.cts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +336 -0
- package/package.json +34 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
compileExpression: () => compileExpression,
|
|
24
|
+
exportBundle: () => exportBundle,
|
|
25
|
+
hostScopesToSpec: () => import_dialect3.hostScopesToSpec,
|
|
26
|
+
validateConditions: () => validateConditions,
|
|
27
|
+
validateInterpolation: () => validateInterpolation
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/expressions.ts
|
|
32
|
+
var import_expr = require("@wildwinter/expr");
|
|
33
|
+
var import_dialect = require("@patterkit/dialect");
|
|
34
|
+
var import_model = require("@patterkit/model");
|
|
35
|
+
function compileExpression(src, foreign) {
|
|
36
|
+
return (0, import_expr.compile)(src, (0, import_dialect.dialectWithForeignScopes)(foreign));
|
|
37
|
+
}
|
|
38
|
+
function validateConditions(input, options = {}) {
|
|
39
|
+
const issues = [];
|
|
40
|
+
const foreign = options.foreignScopes;
|
|
41
|
+
const dialect = (0, import_dialect.dialectWithForeignScopes)(foreign);
|
|
42
|
+
const readOnly = readOnlyForeignTargets(foreign);
|
|
43
|
+
const opaqueForeign = opaqueForeignTokens(foreign);
|
|
44
|
+
const isScopeToken = scopeTokenTest(foreign);
|
|
45
|
+
const nodeIds = collectNodeIds(input.scenes);
|
|
46
|
+
const checkVisitIds = (node, nodeId, field, src) => {
|
|
47
|
+
if (node.kind === "call") {
|
|
48
|
+
if (VISIT_FNS.has(node.name)) {
|
|
49
|
+
const arg = node.args[0];
|
|
50
|
+
if (arg && arg.kind === "string" && !nodeIds.has(arg.value)) {
|
|
51
|
+
issues.push({ nodeId, field, src, severity: "error", message: `${node.name}(): unknown node id '${arg.value}'` });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const a of node.args) checkVisitIds(a, nodeId, field, src);
|
|
55
|
+
} else if (node.kind === "binary") {
|
|
56
|
+
checkVisitIds(node.left, nodeId, field, src);
|
|
57
|
+
checkVisitIds(node.right, nodeId, field, src);
|
|
58
|
+
} else if (node.kind === "unary") {
|
|
59
|
+
checkVisitIds(node.operand, nodeId, field, src);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const validateElement = (blocks, sceneProps, onEntry, elementId) => {
|
|
63
|
+
const schema = (0, import_dialect.buildSchema)(input.project, sceneProps, foreign);
|
|
64
|
+
const check = (nodeId, field, src) => {
|
|
65
|
+
let ast;
|
|
66
|
+
try {
|
|
67
|
+
ast = (0, import_expr.parse)(src, dialect);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
issues.push({ nodeId, field, src, severity: "error", message: e instanceof Error ? e.message : String(e) });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const iss of (0, import_expr.validateExpr)(ast, schema, dialect)) {
|
|
73
|
+
issues.push({ nodeId, field, src, severity: iss.severity, message: iss.message });
|
|
74
|
+
}
|
|
75
|
+
checkVisitIds(ast, nodeId, field, src);
|
|
76
|
+
};
|
|
77
|
+
const checkTarget = (nodeId, field, target) => {
|
|
78
|
+
if (!TARGET_REF.test(target)) {
|
|
79
|
+
issues.push({
|
|
80
|
+
nodeId,
|
|
81
|
+
field,
|
|
82
|
+
src: target,
|
|
83
|
+
severity: "error",
|
|
84
|
+
message: `set target must be a property reference (@name / @scope.name), got '${target}'`
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (readOnly.has(refKey(target))) {
|
|
89
|
+
issues.push({
|
|
90
|
+
nodeId,
|
|
91
|
+
field,
|
|
92
|
+
src: target,
|
|
93
|
+
severity: "error",
|
|
94
|
+
message: `cannot assign to read-only property '${target}'`
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const { scope, name } = (0, import_dialect.splitRef)(target, isScopeToken);
|
|
99
|
+
if (opaqueForeign.has(scope)) return;
|
|
100
|
+
if (!schema.properties.get(scope)?.has(name)) {
|
|
101
|
+
issues.push({
|
|
102
|
+
nodeId,
|
|
103
|
+
field,
|
|
104
|
+
src: target,
|
|
105
|
+
severity: "error",
|
|
106
|
+
message: `set target '${target}' is not a declared property`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const checkEffects = (nodeId, list, label) => {
|
|
111
|
+
for (const e of list ?? []) {
|
|
112
|
+
checkTarget(nodeId, `${label}.set`, e.target);
|
|
113
|
+
check(nodeId, `${label}.set`, e.value);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
checkEffects(elementId, onEntry, "onEntry");
|
|
117
|
+
for (const block of blocks) {
|
|
118
|
+
(0, import_model.walkNodes)(block.children, (node) => {
|
|
119
|
+
if (node.condition) check(node.id, "condition", node.condition);
|
|
120
|
+
if (node.type === "snippet") {
|
|
121
|
+
checkEffects(node.id, node.onEnter, "onEnter");
|
|
122
|
+
checkEffects(node.id, node.onExit, "onExit");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
for (const scene of input.scenes) validateElement(scene.blocks, scene.sceneProps, scene.onEntry, scene.id);
|
|
128
|
+
return issues;
|
|
129
|
+
}
|
|
130
|
+
function validateInterpolation(input, options = {}) {
|
|
131
|
+
const issues = [];
|
|
132
|
+
const voiced = input.project.voiced ?? false;
|
|
133
|
+
const tables = (input.locales ?? []).map((l) => ({ locale: l.locale, strings: l.strings }));
|
|
134
|
+
const defaultLocale = input.project.locales.default;
|
|
135
|
+
const opaqueForeign = opaqueForeignTokens(options.foreignScopes);
|
|
136
|
+
const isScopeToken = scopeTokenTest(options.foreignScopes);
|
|
137
|
+
for (const scene of input.scenes) {
|
|
138
|
+
const schema = (0, import_dialect.buildSchema)(input.project, scene.sceneProps, options.foreignScopes);
|
|
139
|
+
const known = (ref) => {
|
|
140
|
+
const { scope, name } = (0, import_dialect.splitRef)(ref, isScopeToken);
|
|
141
|
+
if (opaqueForeign.has(scope)) return true;
|
|
142
|
+
return schema.properties.get(scope)?.has(name) ?? false;
|
|
143
|
+
};
|
|
144
|
+
const checkString = (nodeId, id, voicedLine) => {
|
|
145
|
+
for (const t of tables) {
|
|
146
|
+
const text = t.strings[id];
|
|
147
|
+
if (text === void 0) continue;
|
|
148
|
+
const field = `text[${t.locale}]`;
|
|
149
|
+
for (const slot of (0, import_dialect.extractSlots)(text)) {
|
|
150
|
+
if (voicedLine) {
|
|
151
|
+
issues.push({
|
|
152
|
+
nodeId,
|
|
153
|
+
field,
|
|
154
|
+
src: slot.raw,
|
|
155
|
+
severity: "error",
|
|
156
|
+
message: `voiced line beats cannot contain interpolation ${slot.raw} (spec \xA716)`
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!slot.ref) {
|
|
161
|
+
issues.push({
|
|
162
|
+
nodeId,
|
|
163
|
+
field,
|
|
164
|
+
src: slot.raw,
|
|
165
|
+
severity: "error",
|
|
166
|
+
message: `interpolation slot holds a bare property reference only, got '${slot.inner}' (spec \xA716)`
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!known(slot.ref)) {
|
|
171
|
+
issues.push({
|
|
172
|
+
nodeId,
|
|
173
|
+
field,
|
|
174
|
+
src: slot.raw,
|
|
175
|
+
severity: "error",
|
|
176
|
+
message: `unknown property in interpolation slot: '${slot.ref}'`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const checkBeat = (beat) => {
|
|
183
|
+
if (beat.kind === "gameEvent") return;
|
|
184
|
+
checkString(beat.id, beat.id, beat.kind === "line" && voiced);
|
|
185
|
+
};
|
|
186
|
+
for (const block of scene.blocks) {
|
|
187
|
+
(0, import_model.walkNodes)(block.children, (node) => {
|
|
188
|
+
if (node.type === "snippet") {
|
|
189
|
+
for (const beat of node.beats ?? []) checkBeat(beat);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (node.prompt) checkBeat(node.prompt);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return issues;
|
|
197
|
+
}
|
|
198
|
+
var VISIT_FNS = /* @__PURE__ */ new Set(["visits", "seen", "patter_visits", "patter_seen"]);
|
|
199
|
+
var TARGET_REF = /^@[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)?$/;
|
|
200
|
+
function collectNodeIds(scenes) {
|
|
201
|
+
const ids = /* @__PURE__ */ new Set();
|
|
202
|
+
for (const scene of scenes) {
|
|
203
|
+
ids.add(scene.id);
|
|
204
|
+
for (const block of scene.blocks) {
|
|
205
|
+
ids.add(block.id);
|
|
206
|
+
(0, import_model.walkNodes)(block.children, (n) => ids.add(n.id));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return ids;
|
|
210
|
+
}
|
|
211
|
+
function opaqueForeignTokens(spec) {
|
|
212
|
+
return new Set((spec?.scopes ?? []).filter((s) => !s.declarations?.length).map((s) => s.token));
|
|
213
|
+
}
|
|
214
|
+
function scopeTokenTest(spec) {
|
|
215
|
+
const foreign = new Set((spec?.scopes ?? []).map((s) => s.token));
|
|
216
|
+
return (t) => t === "patter" || t === "scene" || foreign.has(t);
|
|
217
|
+
}
|
|
218
|
+
function refKey(ref) {
|
|
219
|
+
return ref.replace(/^@/, "").toLowerCase();
|
|
220
|
+
}
|
|
221
|
+
function readOnlyForeignTargets(spec) {
|
|
222
|
+
const out = /* @__PURE__ */ new Set();
|
|
223
|
+
for (const scope of spec?.scopes ?? []) {
|
|
224
|
+
const scopeWritable = scope.writable ?? true;
|
|
225
|
+
for (const d of scope.declarations ?? []) {
|
|
226
|
+
const writable = d.writable ?? scopeWritable;
|
|
227
|
+
if (!writable) out.add(`${scope.token}.${d.name}`.toLowerCase());
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/index.ts
|
|
234
|
+
var import_dialect3 = require("@patterkit/dialect");
|
|
235
|
+
|
|
236
|
+
// src/export.ts
|
|
237
|
+
var import_core = require("@patterkit/core");
|
|
238
|
+
var import_dialect2 = require("@patterkit/dialect");
|
|
239
|
+
function compileEffect(e, foreign) {
|
|
240
|
+
return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
|
|
241
|
+
}
|
|
242
|
+
function compileNode(node, foreign) {
|
|
243
|
+
if (node.type === "group") {
|
|
244
|
+
return {
|
|
245
|
+
id: node.id,
|
|
246
|
+
type: "group",
|
|
247
|
+
condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
|
|
248
|
+
selector: node.selector,
|
|
249
|
+
shared: node.shared,
|
|
250
|
+
options: node.options,
|
|
251
|
+
children: node.children.map((c) => compileNode(c, foreign)),
|
|
252
|
+
gameData: node.gameData,
|
|
253
|
+
tags: node.tags,
|
|
254
|
+
// author tags (#215), accumulated at runtime
|
|
255
|
+
prompt: node.prompt,
|
|
256
|
+
// option-position only (spec §5)
|
|
257
|
+
secretUntilEligible: node.secretUntilEligible,
|
|
258
|
+
sticky: node.sticky,
|
|
259
|
+
fallback: node.fallback
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
id: node.id,
|
|
264
|
+
type: "snippet",
|
|
265
|
+
condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
|
|
266
|
+
beats: node.beats,
|
|
267
|
+
onEnter: node.onEnter?.map((e) => compileEffect(e, foreign)),
|
|
268
|
+
onExit: node.onExit?.map((e) => compileEffect(e, foreign)),
|
|
269
|
+
gameData: node.gameData,
|
|
270
|
+
tags: node.tags,
|
|
271
|
+
// author tags (#215)
|
|
272
|
+
jump: node.jump,
|
|
273
|
+
secretUntilEligible: node.secretUntilEligible,
|
|
274
|
+
sticky: node.sticky,
|
|
275
|
+
fallback: node.fallback
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function compileBlock(block, foreign) {
|
|
279
|
+
return {
|
|
280
|
+
id: block.id,
|
|
281
|
+
type: "block",
|
|
282
|
+
name: block.name,
|
|
283
|
+
...block.gameId ? { gameId: block.gameId } : {},
|
|
284
|
+
// host-facing address; runtime falls back to the name slug
|
|
285
|
+
children: block.children.map((c) => compileNode(c, foreign)),
|
|
286
|
+
gameData: block.gameData,
|
|
287
|
+
tags: block.tags
|
|
288
|
+
// author tags (#215)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function compileScene(scene, foreign) {
|
|
292
|
+
return {
|
|
293
|
+
id: scene.id,
|
|
294
|
+
type: "scene",
|
|
295
|
+
name: scene.name,
|
|
296
|
+
...scene.gameId ? { gameId: scene.gameId } : {},
|
|
297
|
+
// host-facing address; runtime falls back to the name slug
|
|
298
|
+
gameData: scene.gameData,
|
|
299
|
+
tags: scene.tags,
|
|
300
|
+
// author tags (#215)
|
|
301
|
+
onEntry: scene.onEntry?.map((e) => compileEffect(e, foreign)),
|
|
302
|
+
sceneProps: scene.sceneProps,
|
|
303
|
+
blocks: scene.blocks.map((b) => compileBlock(b, foreign))
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function exportBundle(input) {
|
|
307
|
+
const { project, scenes, locales = [], includeLocales } = input;
|
|
308
|
+
const foreignScopes = input.foreignScopes ?? (0, import_dialect2.hostScopesToSpec)(project.scopeRegistry);
|
|
309
|
+
const scenesOut = {};
|
|
310
|
+
for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, foreignScopes);
|
|
311
|
+
const strings = {};
|
|
312
|
+
for (const loc of locales) {
|
|
313
|
+
if (includeLocales && !includeLocales.includes(loc.locale)) continue;
|
|
314
|
+
const table = strings[loc.locale] ??= {};
|
|
315
|
+
for (const [key, text] of Object.entries(loc.strings)) {
|
|
316
|
+
if (key in table && table[key] !== text) {
|
|
317
|
+
throw new Error(`duplicate string key '${key}' in locale '${loc.locale}' with differing text`);
|
|
318
|
+
}
|
|
319
|
+
table[key] = text;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
schema: "patter/bundle@0",
|
|
324
|
+
content: {
|
|
325
|
+
project: project.project.id,
|
|
326
|
+
// Binds saves to the content they were taken against, and gates bundle
|
|
327
|
+
// staleness (schema §10.5). A stable strict-JSON fingerprint - independent
|
|
328
|
+
// of the source-form trailing-comma policy.
|
|
329
|
+
// `scopeRegistry` is folded in only when present, so projects without host scopes keep their exact
|
|
330
|
+
// prior hash (no ripple to existing bundles / the conformance corpus).
|
|
331
|
+
hash: (0, import_core.hash32)((0, import_core.canonicalStringify)({
|
|
332
|
+
scenes: scenesOut,
|
|
333
|
+
strings,
|
|
334
|
+
properties: project.properties ?? [],
|
|
335
|
+
...foreignScopes ? { scopeRegistry: foreignScopes } : {}
|
|
336
|
+
}, { trailingComma: false })),
|
|
337
|
+
// Structure-only fingerprint (live bundle refresh): the same hash with the string tables left
|
|
338
|
+
// out. Same structureHash + a different hash = a text-only edit, safe to swap in place with
|
|
339
|
+
// `engine.replaceStrings()`; a changed structureHash needs the full save/load hot swap.
|
|
340
|
+
structureHash: (0, import_core.hash32)((0, import_core.canonicalStringify)({
|
|
341
|
+
scenes: scenesOut,
|
|
342
|
+
properties: project.properties ?? [],
|
|
343
|
+
...foreignScopes ? { scopeRegistry: foreignScopes } : {}
|
|
344
|
+
}, { trailingComma: false }))
|
|
345
|
+
},
|
|
346
|
+
voiced: project.voiced ?? false,
|
|
347
|
+
locales: { default: project.locales.default, included: Object.keys(strings) },
|
|
348
|
+
cast: project.cast?.map(({ notes, ...c }) => c),
|
|
349
|
+
// strip authoring-only notes from the runtime
|
|
350
|
+
properties: project.properties,
|
|
351
|
+
scopeRegistry: project.scopeRegistry,
|
|
352
|
+
gameDataFields: project.gameDataFields,
|
|
353
|
+
// Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
|
|
354
|
+
// runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
|
|
355
|
+
...project.closedCaptions ? { closedCaptions: project.closedCaptions } : {},
|
|
356
|
+
scenes: scenesOut,
|
|
357
|
+
strings
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
361
|
+
0 && (module.exports = {
|
|
362
|
+
compileExpression,
|
|
363
|
+
exportBundle,
|
|
364
|
+
hostScopesToSpec,
|
|
365
|
+
validateConditions,
|
|
366
|
+
validateInterpolation
|
|
367
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ScopeRegistrySpec } from '@wildwinter/scoperegistry';
|
|
2
|
+
import { Expression, ProjectFile, Scene, LocaleFile, Bundle } from '@patterkit/model';
|
|
3
|
+
export { hostScopesToSpec } from '@patterkit/dialect';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compile a condition/effect `src` string to the bundle `{ src, ast }` envelope.
|
|
7
|
+
* Pass `foreign` to allow references into another owner's imported scopes (e.g.
|
|
8
|
+
* a storylet's `@world`); the parser needs every referenced scope registered.
|
|
9
|
+
*/
|
|
10
|
+
declare function compileExpression(src: string, foreign?: ScopeRegistrySpec): Expression;
|
|
11
|
+
interface ConditionIssue {
|
|
12
|
+
/** Id of the node whose expression has the issue. */
|
|
13
|
+
nodeId: string;
|
|
14
|
+
/** Where on the node: "condition" / "onEnter.set" / "onExit.set" / ... */
|
|
15
|
+
field: string;
|
|
16
|
+
/** The offending source expression. */
|
|
17
|
+
src: string;
|
|
18
|
+
message: string;
|
|
19
|
+
/** "error" blocks a clean build; "warning" is advisory. Condition issues carry the expr
|
|
20
|
+
* validator's own severity; the compiler's own structural pushes are always errors. */
|
|
21
|
+
severity: "error" | "warning";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse + validate every condition and effect expression in a project's scenes.
|
|
25
|
+
* Uses the Patter dialect and a per-scene ExpressionSchema (global properties +
|
|
26
|
+
* the scene's scene-local properties). Pass `options.foreignScopes` (another
|
|
27
|
+
* owner's `scopeRegistrySpec`) to permit + validate references into imported
|
|
28
|
+
* scopes; writes (`set` effects) that target a read-only foreign property are
|
|
29
|
+
* reported. Returns an empty array when all are valid.
|
|
30
|
+
*/
|
|
31
|
+
declare function validateConditions(input: {
|
|
32
|
+
project: ProjectFile;
|
|
33
|
+
scenes: Scene[];
|
|
34
|
+
}, options?: {
|
|
35
|
+
foreignScopes?: ScopeRegistrySpec;
|
|
36
|
+
}): ConditionIssue[];
|
|
37
|
+
/**
|
|
38
|
+
* Validate inline `{@ref}` interpolation slots in a project's localised strings
|
|
39
|
+
* (spec §16). Enforces the VO-safety guarantee - **a voiced project rejects any
|
|
40
|
+
* slot in a line beat** - plus the committed surface (a slot holds a bare
|
|
41
|
+
* property reference only) and that the referenced property is declared. Text
|
|
42
|
+
* beats and CHOICE LABELS always interpolate (labels are on-screen text even in
|
|
43
|
+
* voiced projects); game-event beats carry no localised content. Also flags an
|
|
44
|
+
* option `prompt` id with no string in the default locale (the runtime would
|
|
45
|
+
* display the raw id). Pass `options.foreignScopes` so slots into imported
|
|
46
|
+
* scopes (`{@world.x}`) resolve instead of reporting unknown. Returns an empty
|
|
47
|
+
* array when all are valid.
|
|
48
|
+
*/
|
|
49
|
+
declare function validateInterpolation(input: {
|
|
50
|
+
project: ProjectFile;
|
|
51
|
+
scenes: Scene[];
|
|
52
|
+
locales?: LocaleFile[];
|
|
53
|
+
}, options?: {
|
|
54
|
+
foreignScopes?: ScopeRegistrySpec;
|
|
55
|
+
}): ConditionIssue[];
|
|
56
|
+
|
|
57
|
+
interface ExportInput {
|
|
58
|
+
project: ProjectFile;
|
|
59
|
+
scenes: Scene[];
|
|
60
|
+
/** Locale files to assemble (one or more per locale). */
|
|
61
|
+
locales?: LocaleFile[];
|
|
62
|
+
/** Assemble only these locales (schema §10.1 "selected locales"); default: all provided. */
|
|
63
|
+
includeLocales?: string[];
|
|
64
|
+
/**
|
|
65
|
+
* Another owner's `scopeRegistrySpec` (e.g. a storylet's), to permit compiling
|
|
66
|
+
* references into its imported scopes (`@world.x`). The parser rejects an
|
|
67
|
+
* unregistered scope token, so cross-engine refs need this.
|
|
68
|
+
*/
|
|
69
|
+
foreignScopes?: ScopeRegistrySpec;
|
|
70
|
+
}
|
|
71
|
+
/** Compile a project's source into the runtime bundle. */
|
|
72
|
+
declare function exportBundle(input: ExportInput): Bundle;
|
|
73
|
+
|
|
74
|
+
export { type ConditionIssue, type ExportInput, compileExpression, exportBundle, validateConditions, validateInterpolation };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ScopeRegistrySpec } from '@wildwinter/scoperegistry';
|
|
2
|
+
import { Expression, ProjectFile, Scene, LocaleFile, Bundle } from '@patterkit/model';
|
|
3
|
+
export { hostScopesToSpec } from '@patterkit/dialect';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compile a condition/effect `src` string to the bundle `{ src, ast }` envelope.
|
|
7
|
+
* Pass `foreign` to allow references into another owner's imported scopes (e.g.
|
|
8
|
+
* a storylet's `@world`); the parser needs every referenced scope registered.
|
|
9
|
+
*/
|
|
10
|
+
declare function compileExpression(src: string, foreign?: ScopeRegistrySpec): Expression;
|
|
11
|
+
interface ConditionIssue {
|
|
12
|
+
/** Id of the node whose expression has the issue. */
|
|
13
|
+
nodeId: string;
|
|
14
|
+
/** Where on the node: "condition" / "onEnter.set" / "onExit.set" / ... */
|
|
15
|
+
field: string;
|
|
16
|
+
/** The offending source expression. */
|
|
17
|
+
src: string;
|
|
18
|
+
message: string;
|
|
19
|
+
/** "error" blocks a clean build; "warning" is advisory. Condition issues carry the expr
|
|
20
|
+
* validator's own severity; the compiler's own structural pushes are always errors. */
|
|
21
|
+
severity: "error" | "warning";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse + validate every condition and effect expression in a project's scenes.
|
|
25
|
+
* Uses the Patter dialect and a per-scene ExpressionSchema (global properties +
|
|
26
|
+
* the scene's scene-local properties). Pass `options.foreignScopes` (another
|
|
27
|
+
* owner's `scopeRegistrySpec`) to permit + validate references into imported
|
|
28
|
+
* scopes; writes (`set` effects) that target a read-only foreign property are
|
|
29
|
+
* reported. Returns an empty array when all are valid.
|
|
30
|
+
*/
|
|
31
|
+
declare function validateConditions(input: {
|
|
32
|
+
project: ProjectFile;
|
|
33
|
+
scenes: Scene[];
|
|
34
|
+
}, options?: {
|
|
35
|
+
foreignScopes?: ScopeRegistrySpec;
|
|
36
|
+
}): ConditionIssue[];
|
|
37
|
+
/**
|
|
38
|
+
* Validate inline `{@ref}` interpolation slots in a project's localised strings
|
|
39
|
+
* (spec §16). Enforces the VO-safety guarantee - **a voiced project rejects any
|
|
40
|
+
* slot in a line beat** - plus the committed surface (a slot holds a bare
|
|
41
|
+
* property reference only) and that the referenced property is declared. Text
|
|
42
|
+
* beats and CHOICE LABELS always interpolate (labels are on-screen text even in
|
|
43
|
+
* voiced projects); game-event beats carry no localised content. Also flags an
|
|
44
|
+
* option `prompt` id with no string in the default locale (the runtime would
|
|
45
|
+
* display the raw id). Pass `options.foreignScopes` so slots into imported
|
|
46
|
+
* scopes (`{@world.x}`) resolve instead of reporting unknown. Returns an empty
|
|
47
|
+
* array when all are valid.
|
|
48
|
+
*/
|
|
49
|
+
declare function validateInterpolation(input: {
|
|
50
|
+
project: ProjectFile;
|
|
51
|
+
scenes: Scene[];
|
|
52
|
+
locales?: LocaleFile[];
|
|
53
|
+
}, options?: {
|
|
54
|
+
foreignScopes?: ScopeRegistrySpec;
|
|
55
|
+
}): ConditionIssue[];
|
|
56
|
+
|
|
57
|
+
interface ExportInput {
|
|
58
|
+
project: ProjectFile;
|
|
59
|
+
scenes: Scene[];
|
|
60
|
+
/** Locale files to assemble (one or more per locale). */
|
|
61
|
+
locales?: LocaleFile[];
|
|
62
|
+
/** Assemble only these locales (schema §10.1 "selected locales"); default: all provided. */
|
|
63
|
+
includeLocales?: string[];
|
|
64
|
+
/**
|
|
65
|
+
* Another owner's `scopeRegistrySpec` (e.g. a storylet's), to permit compiling
|
|
66
|
+
* references into its imported scopes (`@world.x`). The parser rejects an
|
|
67
|
+
* unregistered scope token, so cross-engine refs need this.
|
|
68
|
+
*/
|
|
69
|
+
foreignScopes?: ScopeRegistrySpec;
|
|
70
|
+
}
|
|
71
|
+
/** Compile a project's source into the runtime bundle. */
|
|
72
|
+
declare function exportBundle(input: ExportInput): Bundle;
|
|
73
|
+
|
|
74
|
+
export { type ConditionIssue, type ExportInput, compileExpression, exportBundle, validateConditions, validateInterpolation };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// src/expressions.ts
|
|
2
|
+
import { compile, parse, validateExpr } from "@wildwinter/expr";
|
|
3
|
+
import { dialectWithForeignScopes, buildSchema, extractSlots, splitRef } from "@patterkit/dialect";
|
|
4
|
+
import { walkNodes } from "@patterkit/model";
|
|
5
|
+
function compileExpression(src, foreign) {
|
|
6
|
+
return compile(src, dialectWithForeignScopes(foreign));
|
|
7
|
+
}
|
|
8
|
+
function validateConditions(input, options = {}) {
|
|
9
|
+
const issues = [];
|
|
10
|
+
const foreign = options.foreignScopes;
|
|
11
|
+
const dialect = dialectWithForeignScopes(foreign);
|
|
12
|
+
const readOnly = readOnlyForeignTargets(foreign);
|
|
13
|
+
const opaqueForeign = opaqueForeignTokens(foreign);
|
|
14
|
+
const isScopeToken = scopeTokenTest(foreign);
|
|
15
|
+
const nodeIds = collectNodeIds(input.scenes);
|
|
16
|
+
const checkVisitIds = (node, nodeId, field, src) => {
|
|
17
|
+
if (node.kind === "call") {
|
|
18
|
+
if (VISIT_FNS.has(node.name)) {
|
|
19
|
+
const arg = node.args[0];
|
|
20
|
+
if (arg && arg.kind === "string" && !nodeIds.has(arg.value)) {
|
|
21
|
+
issues.push({ nodeId, field, src, severity: "error", message: `${node.name}(): unknown node id '${arg.value}'` });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const a of node.args) checkVisitIds(a, nodeId, field, src);
|
|
25
|
+
} else if (node.kind === "binary") {
|
|
26
|
+
checkVisitIds(node.left, nodeId, field, src);
|
|
27
|
+
checkVisitIds(node.right, nodeId, field, src);
|
|
28
|
+
} else if (node.kind === "unary") {
|
|
29
|
+
checkVisitIds(node.operand, nodeId, field, src);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const validateElement = (blocks, sceneProps, onEntry, elementId) => {
|
|
33
|
+
const schema = buildSchema(input.project, sceneProps, foreign);
|
|
34
|
+
const check = (nodeId, field, src) => {
|
|
35
|
+
let ast;
|
|
36
|
+
try {
|
|
37
|
+
ast = parse(src, dialect);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
issues.push({ nodeId, field, src, severity: "error", message: e instanceof Error ? e.message : String(e) });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const iss of validateExpr(ast, schema, dialect)) {
|
|
43
|
+
issues.push({ nodeId, field, src, severity: iss.severity, message: iss.message });
|
|
44
|
+
}
|
|
45
|
+
checkVisitIds(ast, nodeId, field, src);
|
|
46
|
+
};
|
|
47
|
+
const checkTarget = (nodeId, field, target) => {
|
|
48
|
+
if (!TARGET_REF.test(target)) {
|
|
49
|
+
issues.push({
|
|
50
|
+
nodeId,
|
|
51
|
+
field,
|
|
52
|
+
src: target,
|
|
53
|
+
severity: "error",
|
|
54
|
+
message: `set target must be a property reference (@name / @scope.name), got '${target}'`
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (readOnly.has(refKey(target))) {
|
|
59
|
+
issues.push({
|
|
60
|
+
nodeId,
|
|
61
|
+
field,
|
|
62
|
+
src: target,
|
|
63
|
+
severity: "error",
|
|
64
|
+
message: `cannot assign to read-only property '${target}'`
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const { scope, name } = splitRef(target, isScopeToken);
|
|
69
|
+
if (opaqueForeign.has(scope)) return;
|
|
70
|
+
if (!schema.properties.get(scope)?.has(name)) {
|
|
71
|
+
issues.push({
|
|
72
|
+
nodeId,
|
|
73
|
+
field,
|
|
74
|
+
src: target,
|
|
75
|
+
severity: "error",
|
|
76
|
+
message: `set target '${target}' is not a declared property`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const checkEffects = (nodeId, list, label) => {
|
|
81
|
+
for (const e of list ?? []) {
|
|
82
|
+
checkTarget(nodeId, `${label}.set`, e.target);
|
|
83
|
+
check(nodeId, `${label}.set`, e.value);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
checkEffects(elementId, onEntry, "onEntry");
|
|
87
|
+
for (const block of blocks) {
|
|
88
|
+
walkNodes(block.children, (node) => {
|
|
89
|
+
if (node.condition) check(node.id, "condition", node.condition);
|
|
90
|
+
if (node.type === "snippet") {
|
|
91
|
+
checkEffects(node.id, node.onEnter, "onEnter");
|
|
92
|
+
checkEffects(node.id, node.onExit, "onExit");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
for (const scene of input.scenes) validateElement(scene.blocks, scene.sceneProps, scene.onEntry, scene.id);
|
|
98
|
+
return issues;
|
|
99
|
+
}
|
|
100
|
+
function validateInterpolation(input, options = {}) {
|
|
101
|
+
const issues = [];
|
|
102
|
+
const voiced = input.project.voiced ?? false;
|
|
103
|
+
const tables = (input.locales ?? []).map((l) => ({ locale: l.locale, strings: l.strings }));
|
|
104
|
+
const defaultLocale = input.project.locales.default;
|
|
105
|
+
const opaqueForeign = opaqueForeignTokens(options.foreignScopes);
|
|
106
|
+
const isScopeToken = scopeTokenTest(options.foreignScopes);
|
|
107
|
+
for (const scene of input.scenes) {
|
|
108
|
+
const schema = buildSchema(input.project, scene.sceneProps, options.foreignScopes);
|
|
109
|
+
const known = (ref) => {
|
|
110
|
+
const { scope, name } = splitRef(ref, isScopeToken);
|
|
111
|
+
if (opaqueForeign.has(scope)) return true;
|
|
112
|
+
return schema.properties.get(scope)?.has(name) ?? false;
|
|
113
|
+
};
|
|
114
|
+
const checkString = (nodeId, id, voicedLine) => {
|
|
115
|
+
for (const t of tables) {
|
|
116
|
+
const text = t.strings[id];
|
|
117
|
+
if (text === void 0) continue;
|
|
118
|
+
const field = `text[${t.locale}]`;
|
|
119
|
+
for (const slot of extractSlots(text)) {
|
|
120
|
+
if (voicedLine) {
|
|
121
|
+
issues.push({
|
|
122
|
+
nodeId,
|
|
123
|
+
field,
|
|
124
|
+
src: slot.raw,
|
|
125
|
+
severity: "error",
|
|
126
|
+
message: `voiced line beats cannot contain interpolation ${slot.raw} (spec \xA716)`
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!slot.ref) {
|
|
131
|
+
issues.push({
|
|
132
|
+
nodeId,
|
|
133
|
+
field,
|
|
134
|
+
src: slot.raw,
|
|
135
|
+
severity: "error",
|
|
136
|
+
message: `interpolation slot holds a bare property reference only, got '${slot.inner}' (spec \xA716)`
|
|
137
|
+
});
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!known(slot.ref)) {
|
|
141
|
+
issues.push({
|
|
142
|
+
nodeId,
|
|
143
|
+
field,
|
|
144
|
+
src: slot.raw,
|
|
145
|
+
severity: "error",
|
|
146
|
+
message: `unknown property in interpolation slot: '${slot.ref}'`
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const checkBeat = (beat) => {
|
|
153
|
+
if (beat.kind === "gameEvent") return;
|
|
154
|
+
checkString(beat.id, beat.id, beat.kind === "line" && voiced);
|
|
155
|
+
};
|
|
156
|
+
for (const block of scene.blocks) {
|
|
157
|
+
walkNodes(block.children, (node) => {
|
|
158
|
+
if (node.type === "snippet") {
|
|
159
|
+
for (const beat of node.beats ?? []) checkBeat(beat);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (node.prompt) checkBeat(node.prompt);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return issues;
|
|
167
|
+
}
|
|
168
|
+
var VISIT_FNS = /* @__PURE__ */ new Set(["visits", "seen", "patter_visits", "patter_seen"]);
|
|
169
|
+
var TARGET_REF = /^@[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)?$/;
|
|
170
|
+
function collectNodeIds(scenes) {
|
|
171
|
+
const ids = /* @__PURE__ */ new Set();
|
|
172
|
+
for (const scene of scenes) {
|
|
173
|
+
ids.add(scene.id);
|
|
174
|
+
for (const block of scene.blocks) {
|
|
175
|
+
ids.add(block.id);
|
|
176
|
+
walkNodes(block.children, (n) => ids.add(n.id));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return ids;
|
|
180
|
+
}
|
|
181
|
+
function opaqueForeignTokens(spec) {
|
|
182
|
+
return new Set((spec?.scopes ?? []).filter((s) => !s.declarations?.length).map((s) => s.token));
|
|
183
|
+
}
|
|
184
|
+
function scopeTokenTest(spec) {
|
|
185
|
+
const foreign = new Set((spec?.scopes ?? []).map((s) => s.token));
|
|
186
|
+
return (t) => t === "patter" || t === "scene" || foreign.has(t);
|
|
187
|
+
}
|
|
188
|
+
function refKey(ref) {
|
|
189
|
+
return ref.replace(/^@/, "").toLowerCase();
|
|
190
|
+
}
|
|
191
|
+
function readOnlyForeignTargets(spec) {
|
|
192
|
+
const out = /* @__PURE__ */ new Set();
|
|
193
|
+
for (const scope of spec?.scopes ?? []) {
|
|
194
|
+
const scopeWritable = scope.writable ?? true;
|
|
195
|
+
for (const d of scope.declarations ?? []) {
|
|
196
|
+
const writable = d.writable ?? scopeWritable;
|
|
197
|
+
if (!writable) out.add(`${scope.token}.${d.name}`.toLowerCase());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/index.ts
|
|
204
|
+
import { hostScopesToSpec as hostScopesToSpec2 } from "@patterkit/dialect";
|
|
205
|
+
|
|
206
|
+
// src/export.ts
|
|
207
|
+
import { canonicalStringify, hash32 } from "@patterkit/core";
|
|
208
|
+
import { hostScopesToSpec } from "@patterkit/dialect";
|
|
209
|
+
function compileEffect(e, foreign) {
|
|
210
|
+
return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
|
|
211
|
+
}
|
|
212
|
+
function compileNode(node, foreign) {
|
|
213
|
+
if (node.type === "group") {
|
|
214
|
+
return {
|
|
215
|
+
id: node.id,
|
|
216
|
+
type: "group",
|
|
217
|
+
condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
|
|
218
|
+
selector: node.selector,
|
|
219
|
+
shared: node.shared,
|
|
220
|
+
options: node.options,
|
|
221
|
+
children: node.children.map((c) => compileNode(c, foreign)),
|
|
222
|
+
gameData: node.gameData,
|
|
223
|
+
tags: node.tags,
|
|
224
|
+
// author tags (#215), accumulated at runtime
|
|
225
|
+
prompt: node.prompt,
|
|
226
|
+
// option-position only (spec §5)
|
|
227
|
+
secretUntilEligible: node.secretUntilEligible,
|
|
228
|
+
sticky: node.sticky,
|
|
229
|
+
fallback: node.fallback
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
id: node.id,
|
|
234
|
+
type: "snippet",
|
|
235
|
+
condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
|
|
236
|
+
beats: node.beats,
|
|
237
|
+
onEnter: node.onEnter?.map((e) => compileEffect(e, foreign)),
|
|
238
|
+
onExit: node.onExit?.map((e) => compileEffect(e, foreign)),
|
|
239
|
+
gameData: node.gameData,
|
|
240
|
+
tags: node.tags,
|
|
241
|
+
// author tags (#215)
|
|
242
|
+
jump: node.jump,
|
|
243
|
+
secretUntilEligible: node.secretUntilEligible,
|
|
244
|
+
sticky: node.sticky,
|
|
245
|
+
fallback: node.fallback
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function compileBlock(block, foreign) {
|
|
249
|
+
return {
|
|
250
|
+
id: block.id,
|
|
251
|
+
type: "block",
|
|
252
|
+
name: block.name,
|
|
253
|
+
...block.gameId ? { gameId: block.gameId } : {},
|
|
254
|
+
// host-facing address; runtime falls back to the name slug
|
|
255
|
+
children: block.children.map((c) => compileNode(c, foreign)),
|
|
256
|
+
gameData: block.gameData,
|
|
257
|
+
tags: block.tags
|
|
258
|
+
// author tags (#215)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function compileScene(scene, foreign) {
|
|
262
|
+
return {
|
|
263
|
+
id: scene.id,
|
|
264
|
+
type: "scene",
|
|
265
|
+
name: scene.name,
|
|
266
|
+
...scene.gameId ? { gameId: scene.gameId } : {},
|
|
267
|
+
// host-facing address; runtime falls back to the name slug
|
|
268
|
+
gameData: scene.gameData,
|
|
269
|
+
tags: scene.tags,
|
|
270
|
+
// author tags (#215)
|
|
271
|
+
onEntry: scene.onEntry?.map((e) => compileEffect(e, foreign)),
|
|
272
|
+
sceneProps: scene.sceneProps,
|
|
273
|
+
blocks: scene.blocks.map((b) => compileBlock(b, foreign))
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function exportBundle(input) {
|
|
277
|
+
const { project, scenes, locales = [], includeLocales } = input;
|
|
278
|
+
const foreignScopes = input.foreignScopes ?? hostScopesToSpec(project.scopeRegistry);
|
|
279
|
+
const scenesOut = {};
|
|
280
|
+
for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, foreignScopes);
|
|
281
|
+
const strings = {};
|
|
282
|
+
for (const loc of locales) {
|
|
283
|
+
if (includeLocales && !includeLocales.includes(loc.locale)) continue;
|
|
284
|
+
const table = strings[loc.locale] ??= {};
|
|
285
|
+
for (const [key, text] of Object.entries(loc.strings)) {
|
|
286
|
+
if (key in table && table[key] !== text) {
|
|
287
|
+
throw new Error(`duplicate string key '${key}' in locale '${loc.locale}' with differing text`);
|
|
288
|
+
}
|
|
289
|
+
table[key] = text;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
schema: "patter/bundle@0",
|
|
294
|
+
content: {
|
|
295
|
+
project: project.project.id,
|
|
296
|
+
// Binds saves to the content they were taken against, and gates bundle
|
|
297
|
+
// staleness (schema §10.5). A stable strict-JSON fingerprint - independent
|
|
298
|
+
// of the source-form trailing-comma policy.
|
|
299
|
+
// `scopeRegistry` is folded in only when present, so projects without host scopes keep their exact
|
|
300
|
+
// prior hash (no ripple to existing bundles / the conformance corpus).
|
|
301
|
+
hash: hash32(canonicalStringify({
|
|
302
|
+
scenes: scenesOut,
|
|
303
|
+
strings,
|
|
304
|
+
properties: project.properties ?? [],
|
|
305
|
+
...foreignScopes ? { scopeRegistry: foreignScopes } : {}
|
|
306
|
+
}, { trailingComma: false })),
|
|
307
|
+
// Structure-only fingerprint (live bundle refresh): the same hash with the string tables left
|
|
308
|
+
// out. Same structureHash + a different hash = a text-only edit, safe to swap in place with
|
|
309
|
+
// `engine.replaceStrings()`; a changed structureHash needs the full save/load hot swap.
|
|
310
|
+
structureHash: hash32(canonicalStringify({
|
|
311
|
+
scenes: scenesOut,
|
|
312
|
+
properties: project.properties ?? [],
|
|
313
|
+
...foreignScopes ? { scopeRegistry: foreignScopes } : {}
|
|
314
|
+
}, { trailingComma: false }))
|
|
315
|
+
},
|
|
316
|
+
voiced: project.voiced ?? false,
|
|
317
|
+
locales: { default: project.locales.default, included: Object.keys(strings) },
|
|
318
|
+
cast: project.cast?.map(({ notes, ...c }) => c),
|
|
319
|
+
// strip authoring-only notes from the runtime
|
|
320
|
+
properties: project.properties,
|
|
321
|
+
scopeRegistry: project.scopeRegistry,
|
|
322
|
+
gameDataFields: project.gameDataFields,
|
|
323
|
+
// Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
|
|
324
|
+
// runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
|
|
325
|
+
...project.closedCaptions ? { closedCaptions: project.closedCaptions } : {},
|
|
326
|
+
scenes: scenesOut,
|
|
327
|
+
strings
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
export {
|
|
331
|
+
compileExpression,
|
|
332
|
+
exportBundle,
|
|
333
|
+
hostScopesToSpec2 as hostScopesToSpec,
|
|
334
|
+
validateConditions,
|
|
335
|
+
validateInterpolation
|
|
336
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@patterkit/compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Patter compiler: compile condition/effect expressions, validate them, and export source to the runtime bundle.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ian Thomas",
|
|
8
|
+
"homepage": "https://patterkit.dev",
|
|
9
|
+
"repository": { "type": "git", "url": "git+https://github.com/patterkit/patter.git", "directory": "packages/compiler" },
|
|
10
|
+
"bugs": "https://github.com/patterkit/patter/issues",
|
|
11
|
+
"publishConfig": { "access": "public" },
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"require": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": ["dist", "README.md"],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@patterkit/model": "0.1.0",
|
|
29
|
+
"@patterkit/core": "0.1.0",
|
|
30
|
+
"@patterkit/dialect": "0.1.0",
|
|
31
|
+
"@wildwinter/expr": "^0.3.0",
|
|
32
|
+
"@wildwinter/scoperegistry": "^0.1.0"
|
|
33
|
+
}
|
|
34
|
+
}
|