@patterkit/dialect 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 ADDED
@@ -0,0 +1,374 @@
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
+ buildSchema: () => buildSchema,
24
+ dialectWithForeignScopes: () => dialectWithForeignScopes,
25
+ extractSlots: () => extractSlots,
26
+ hostScopesToSpec: () => hostScopesToSpec,
27
+ interpolate: () => interpolate,
28
+ patterDialect: () => patterDialect,
29
+ renderSlotValue: () => renderSlotValue,
30
+ splitRef: () => splitRef,
31
+ stripCaptions: () => stripCaptions
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+ var import_expr = require("@wildwinter/expr");
35
+ function host(h) {
36
+ return h.ctx.host ?? {};
37
+ }
38
+ var patterDialect = {
39
+ defaultScope: "patter",
40
+ scopes: [
41
+ { token: "patter" },
42
+ // global / world state (graceful-false on miss); bare @name
43
+ { token: "scene" }
44
+ // scene-local (per-flow or shared, per the property's `shared` flag)
45
+ ],
46
+ functions: {
47
+ random: {
48
+ minArgs: 2,
49
+ maxArgs: 2,
50
+ returnType: "number",
51
+ eval(args, h) {
52
+ if (args.length !== 2) throw new import_expr.EvalError("random(a, b) requires exactly 2 arguments");
53
+ const next = host(h).nextRandom;
54
+ if (!next) throw new import_expr.EvalError("random() called without a PRNG in context");
55
+ const a = h.evaluate(args[0]), b = h.evaluate(args[1]);
56
+ if (typeof a !== "number" || typeof b !== "number") throw new import_expr.EvalError("random(a, b) arguments must be numbers");
57
+ if (!Number.isInteger(a) || !Number.isInteger(b)) throw new import_expr.EvalError("random(a, b) arguments must be integers");
58
+ const lo = Math.min(a, b), hi = Math.max(a, b);
59
+ return Math.floor(next() * (hi - lo + 1)) + lo;
60
+ }
61
+ },
62
+ check_flags: {
63
+ minArgs: 1,
64
+ returnType: "boolean",
65
+ flagDeltaArgs: true,
66
+ validate: flagsCall("check_flags"),
67
+ eval(args, h) {
68
+ const flags = readFlags(args[0], h, "check_flags");
69
+ for (let i = 1; i < args.length; i++) {
70
+ const arg = args[i];
71
+ if (arg.kind !== "flagdelta") throw new import_expr.EvalError("check_flags() flag args must be +flagName or -flagName");
72
+ if (arg.sign === "+" ? !flags.includes(arg.name) : flags.includes(arg.name)) return false;
73
+ }
74
+ return true;
75
+ }
76
+ },
77
+ set_flags: {
78
+ minArgs: 1,
79
+ returnType: "flags",
80
+ flagDeltaArgs: true,
81
+ validate: flagsCall("set_flags"),
82
+ eval(args, h) {
83
+ const result = [...readFlags(args[0], h, "set_flags")];
84
+ for (let i = 1; i < args.length; i++) {
85
+ const arg = args[i];
86
+ if (arg.kind !== "flagdelta") throw new import_expr.EvalError("set_flags() flag args must be +flagName or -flagName");
87
+ if (arg.sign === "+") {
88
+ if (!result.includes(arg.name)) result.push(arg.name);
89
+ } else {
90
+ const idx = result.indexOf(arg.name);
91
+ if (idx >= 0) result.splice(idx, 1);
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+ },
97
+ // Visit counts (spec §7): how many times a scene / block / node has been
98
+ // *entered*. `visits` / `seen` are this flow's count (flow-local); the
99
+ // `patter_` variants are the world-wide (@patter / shared) count. The arg is a
100
+ // node id - the same id a jump targets.
101
+ visits: {
102
+ minArgs: 1,
103
+ maxArgs: 1,
104
+ returnType: "number",
105
+ validate: idArg("visits"),
106
+ eval: (args, h) => host(h).visits?.(nodeId(args, h, "visits")) ?? 0
107
+ },
108
+ seen: {
109
+ minArgs: 1,
110
+ maxArgs: 1,
111
+ returnType: "boolean",
112
+ validate: idArg("seen"),
113
+ eval: (args, h) => (host(h).visits?.(nodeId(args, h, "seen")) ?? 0) > 0
114
+ },
115
+ patter_visits: {
116
+ minArgs: 1,
117
+ maxArgs: 1,
118
+ returnType: "number",
119
+ validate: idArg("patter_visits"),
120
+ eval: (args, h) => host(h).patterVisits?.(nodeId(args, h, "patter_visits")) ?? 0
121
+ },
122
+ patter_seen: {
123
+ minArgs: 1,
124
+ maxArgs: 1,
125
+ returnType: "boolean",
126
+ validate: idArg("patter_seen"),
127
+ eval: (args, h) => (host(h).patterVisits?.(nodeId(args, h, "patter_seen")) ?? 0) > 0
128
+ }
129
+ }
130
+ };
131
+ function dialectWithForeignScopes(spec) {
132
+ if (!spec || spec.scopes.length === 0) return patterDialect;
133
+ const known = new Set(patterDialect.scopes.map((s) => s.token));
134
+ const extra = spec.scopes.filter((s) => !known.has(s.token)).map((s) => ({ token: s.token }));
135
+ if (extra.length === 0) return patterDialect;
136
+ return { ...patterDialect, scopes: [...patterDialect.scopes, ...extra] };
137
+ }
138
+ function splitRef(ref, isScope) {
139
+ const parts = ref.replace(/^@/, "").split(".");
140
+ if (parts.length === 2 && isScope(parts[0])) {
141
+ return { scope: parts[0], name: parts[1].toLowerCase() };
142
+ }
143
+ return { scope: "patter", name: parts.join(".").toLowerCase() };
144
+ }
145
+ function nodeId(args, h, fn) {
146
+ const v = h.evaluate(args[0]);
147
+ if (typeof v !== "string") throw new import_expr.EvalError(`${fn}(id) requires a string node id`);
148
+ return v;
149
+ }
150
+ function idArg(fnName) {
151
+ return (args, h) => {
152
+ const first = args[0];
153
+ if (first && first.kind !== "string") {
154
+ h.report({
155
+ path: [...h.path, "args", 0],
156
+ kind: "wrong-arg-type",
157
+ severity: "error",
158
+ message: `${fnName}(id): the argument must be a string id literal (a scene / block / node id)`
159
+ });
160
+ }
161
+ };
162
+ }
163
+ function readFlags(arg, h, fn) {
164
+ if (!arg) throw new import_expr.EvalError(`${fn}() requires at least one argument (the flags variable)`);
165
+ const v = h.evaluate(arg);
166
+ if (Array.isArray(v)) return v;
167
+ if (v === false || v === null || v === void 0) return [];
168
+ throw new import_expr.EvalError(`${fn}() first argument must be a flags property`);
169
+ }
170
+ function flagsCall(fnName) {
171
+ return (args, h) => {
172
+ if (args.length === 0) return;
173
+ const first = args[0];
174
+ if (first.kind !== "scopedvar") {
175
+ h.report({
176
+ path: [...h.path, "args", 0],
177
+ kind: "wrong-arg-type",
178
+ severity: "error",
179
+ message: `${fnName}(): first argument must be a flags property reference (@name or @scope.name)`
180
+ });
181
+ return;
182
+ }
183
+ const meta = h.schema.properties.get(first.scope)?.get(first.name);
184
+ if (meta && meta.type !== "flags") {
185
+ const ref = first.scope === h.defaultScope ? first.name : `${first.scope}.${first.name}`;
186
+ h.report({
187
+ path: [...h.path, "args", 0],
188
+ kind: "wrong-arg-type",
189
+ severity: "error",
190
+ message: `${fnName}(): '@${ref}' is not a flags property (got ${meta.type})`
191
+ });
192
+ return;
193
+ }
194
+ for (let i = 1; i < args.length; i++) {
195
+ const arg = args[i];
196
+ if (arg.kind !== "flagdelta") {
197
+ h.report({
198
+ path: [...h.path, "args", i],
199
+ kind: "wrong-arg-type",
200
+ severity: "error",
201
+ message: `${fnName}(): argument ${i + 1} must be +flagName or -flagName`
202
+ });
203
+ } else if (meta?.type === "flags" && meta.enumValues && !meta.enumValues.includes(arg.name)) {
204
+ h.report({
205
+ path: [...h.path, "args", i],
206
+ kind: "unknown-flag-name",
207
+ severity: "error",
208
+ message: `${fnName}(): unknown flag '${arg.name}'`,
209
+ reference: arg.name
210
+ });
211
+ }
212
+ }
213
+ };
214
+ }
215
+ function hostScopesToSpec(reg) {
216
+ if (!reg) return void 0;
217
+ return {
218
+ version: reg.version,
219
+ scopes: reg.scopes.map((s) => ({
220
+ token: s.token,
221
+ ...s.writable === false ? { writable: false } : {},
222
+ ...s.declarations ? {
223
+ declarations: s.declarations.map((d) => ({
224
+ name: d.name,
225
+ type: d.type,
226
+ ...d.values ? { values: d.values } : {},
227
+ ...d.default !== void 0 ? { default: d.default } : {},
228
+ ...d.writable === false ? { writable: false } : {}
229
+ }))
230
+ } : {}
231
+ }))
232
+ };
233
+ }
234
+ function buildSchema(project, sceneProps = [], foreign) {
235
+ const properties = /* @__PURE__ */ new Map();
236
+ const put = (scope, decl) => {
237
+ let m = properties.get(scope);
238
+ if (!m) {
239
+ m = /* @__PURE__ */ new Map();
240
+ properties.set(scope, m);
241
+ }
242
+ m.set(decl.name.toLowerCase(), { type: decl.type, enumValues: decl.values });
243
+ };
244
+ for (const decl of project.properties ?? []) put("patter", decl);
245
+ for (const decl of sceneProps) put("scene", decl);
246
+ const known = new Set(patterDialect.scopes.map((s) => s.token));
247
+ for (const scope of foreign?.scopes ?? []) {
248
+ if (known.has(scope.token)) continue;
249
+ if (!scope.declarations?.length) continue;
250
+ const m = /* @__PURE__ */ new Map();
251
+ for (const d of scope.declarations) m.set(d.name.toLowerCase(), { type: d.type, enumValues: d.values });
252
+ properties.set(scope.token, m);
253
+ }
254
+ return { properties };
255
+ }
256
+ var BARE_REF = /^@[A-Za-z0-9_.]+$/;
257
+ function* tokenise(text) {
258
+ let buf = "";
259
+ let i = 0;
260
+ while (i < text.length) {
261
+ const c = text[i];
262
+ if (c === "{" && text[i + 1] === "{") {
263
+ buf += "{";
264
+ i += 2;
265
+ continue;
266
+ }
267
+ if (c === "}" && text[i + 1] === "}") {
268
+ buf += "}";
269
+ i += 2;
270
+ continue;
271
+ }
272
+ if (c === "{") {
273
+ const close = text.indexOf("}", i + 1);
274
+ if (close !== -1) {
275
+ const raw = text.slice(i, close + 1);
276
+ const inner = text.slice(i + 1, close).trim();
277
+ if (inner.startsWith("@")) {
278
+ if (buf) {
279
+ yield { kind: "text", value: buf };
280
+ buf = "";
281
+ }
282
+ yield { kind: "slot", raw, inner, ref: BARE_REF.test(inner) ? inner : void 0 };
283
+ i = close + 1;
284
+ continue;
285
+ }
286
+ buf += raw;
287
+ i = close + 1;
288
+ continue;
289
+ }
290
+ }
291
+ buf += c;
292
+ i += 1;
293
+ }
294
+ if (buf) yield { kind: "text", value: buf };
295
+ }
296
+ function extractSlots(text) {
297
+ const out = [];
298
+ for (const tok of tokenise(text)) {
299
+ if (tok.kind === "slot") out.push({ raw: tok.raw, inner: tok.inner, ref: tok.ref });
300
+ }
301
+ return out;
302
+ }
303
+ function renderSlotValue(v) {
304
+ if (Array.isArray(v)) return v.join(", ");
305
+ if (typeof v === "boolean") return v ? "true" : "false";
306
+ return String(v);
307
+ }
308
+ function isCaptionWs(c) {
309
+ return c === " " || c === " " || c === "\n" || c === "\r" || c === "\f" || c === "\v";
310
+ }
311
+ function collapseCaptionWs(s) {
312
+ let out = "";
313
+ let pendingSpace = false;
314
+ for (const c of s) {
315
+ if (isCaptionWs(c)) {
316
+ pendingSpace = true;
317
+ continue;
318
+ }
319
+ if (pendingSpace && out.length > 0) out += " ";
320
+ pendingSpace = false;
321
+ out += c;
322
+ }
323
+ return out;
324
+ }
325
+ function stripCaptions(text, open, close) {
326
+ if (open.length === 0 || text.indexOf(open) < 0) return text;
327
+ let out = "";
328
+ let i = 0;
329
+ let removed = false;
330
+ while (i < text.length) {
331
+ if (text.startsWith(open, i)) {
332
+ const end = text.indexOf(close, i + open.length);
333
+ if (end >= 0) {
334
+ i = end + close.length;
335
+ removed = true;
336
+ continue;
337
+ }
338
+ out += text.slice(i);
339
+ break;
340
+ }
341
+ out += text[i];
342
+ i += 1;
343
+ }
344
+ return removed ? collapseCaptionWs(out) : text;
345
+ }
346
+ function interpolate(text, resolve) {
347
+ if (text.indexOf("{") < 0) return text;
348
+ let out = "";
349
+ for (const tok of tokenise(text)) {
350
+ if (tok.kind === "text") {
351
+ out += tok.value;
352
+ continue;
353
+ }
354
+ if (!tok.ref) {
355
+ out += tok.raw;
356
+ continue;
357
+ }
358
+ const v = resolve(tok.ref);
359
+ out += v === void 0 ? "" : renderSlotValue(v);
360
+ }
361
+ return out;
362
+ }
363
+ // Annotate the CommonJS export names for ESM import in node:
364
+ 0 && (module.exports = {
365
+ buildSchema,
366
+ dialectWithForeignScopes,
367
+ extractSlots,
368
+ hostScopesToSpec,
369
+ interpolate,
370
+ patterDialect,
371
+ renderSlotValue,
372
+ splitRef,
373
+ stripCaptions
374
+ });
@@ -0,0 +1,84 @@
1
+ import { ExpressionSchema, Dialect, ScalarValue } from '@wildwinter/expr';
2
+ import { ScopeRegistrySpec } from '@wildwinter/scoperegistry';
3
+ import { ProjectFile, PropertyDecl, HostScopeRegistry } from '@patterkit/model';
4
+
5
+ /** The Patter dialect: scopes patter/scene + built-in functions. */
6
+ declare const patterDialect: Dialect;
7
+ /**
8
+ * A Patter dialect extended with FOREIGN scope tokens imported from another
9
+ * owner's `scopeRegistrySpec` (e.g. a storylet's `@world` / `@player` / `@system`).
10
+ * The parser needs every referenced scope token registered, so authoring tools
11
+ * that allow cross-engine references must compile/validate with this dialect.
12
+ * Foreign scopes use the default missing policy (graceful-false). With no spec
13
+ * (or an empty one) this returns the base `patterDialect` unchanged.
14
+ */
15
+ declare function dialectWithForeignScopes(spec?: ScopeRegistrySpec): Dialect;
16
+ /**
17
+ * Split a property ref ("@name" / "@scope.name") into scope + name - THE one
18
+ * ref grammar, shared by the compiler's validators and the runtime so they
19
+ * cannot drift. `isScope` says which tokens are scopes in the caller's context
20
+ * (dialect tokens, plus any foreign tokens); anything else - including a bare
21
+ * `@name` and a dotted name whose head is not a scope - is a `patter` property.
22
+ */
23
+ declare function splitRef(ref: string, isScope: (token: string) => boolean): {
24
+ scope: string;
25
+ name: string;
26
+ };
27
+ /**
28
+ * Project the model's host-scope registry (`@world`, ...) onto the
29
+ * `scopeRegistrySpec` the compiler / validator consume. Patter's property types
30
+ * are now the same vocabulary as expr's, so this is a structural pass-through
31
+ * that drops authoring-only fields (`purpose`) and omits unset `writable`;
32
+ * returns undefined for an absent registry so callers can pass it straight through.
33
+ */
34
+ declare function hostScopesToSpec(reg?: HostScopeRegistry): ScopeRegistrySpec | undefined;
35
+ /**
36
+ * Build an ExpressionSchema for validating a scene's expressions: global
37
+ * properties (`@patter`) plus that scene's scene-local properties (`@scene`),
38
+ * plus any FOREIGN scopes imported from another owner's `scopeRegistrySpec`.
39
+ * Scope mapping:
40
+ * global (project.properties) -> @patter
41
+ * scene-local (scene.sceneProps)-> @scene
42
+ * foreign (spec) -> its own token (e.g. @world)
43
+ * The `shared` flag is orthogonal - it does not affect validation (both shared
44
+ * and not-shared globals are `@patter`; both scene props are `@scene`). A foreign
45
+ * scope with no declarations is left opaque (omitted) - matching `ScopeRegistry.toSchema`.
46
+ */
47
+ declare function buildSchema(project: ProjectFile, sceneProps?: PropertyDecl[], foreign?: ScopeRegistrySpec): ExpressionSchema;
48
+ /** A `{ ... }` candidate whose trimmed body starts with `@`. */
49
+ interface Slot {
50
+ /** The whole matched text including braces, e.g. "{@gold}". */
51
+ raw: string;
52
+ /** The trimmed inner body, e.g. "@gold" or (malformed) "@gold + 1". */
53
+ inner: string;
54
+ /** The bare property ref ("@gold") when well-formed; undefined if malformed. */
55
+ ref?: string;
56
+ }
57
+ /**
58
+ * Find interpolation slots in a localised string. Only `{ ... }` whose trimmed
59
+ * body begins with `@` is a slot; a slot whose body is not a bare ref is returned
60
+ * with `ref` undefined (so the validator can flag it - the committed surface is a
61
+ * bare property reference only). Escaped `{{ }}` braces are not slots.
62
+ */
63
+ declare function extractSlots(text: string): Slot[];
64
+ /** Render a resolved slot value as display text. */
65
+ declare function renderSlotValue(v: ScalarValue): string;
66
+ /**
67
+ * Closed-caption stripping (#214): with captions OFF, remove every `open`…`close` span (delimiters
68
+ * included) from a dialogue line and collapse the surrounding whitespace -
69
+ * `Oh dear. (sigh) What now?` -> `Oh dear. What now?`. A string that contains NO cue is returned
70
+ * unchanged (its original whitespace preserved); only a string we actually edited is whitespace-
71
+ * normalised. `open` and `close` may be the same token (e.g. `*…*`); an unclosed `open` keeps the
72
+ * remainder verbatim. An empty `open` is a no-op. Identical across every Patterplay runtime - part of
73
+ * the conformance contract.
74
+ */
75
+ declare function stripCaptions(text: string, open: string, close: string): string;
76
+ /**
77
+ * Expand `{@ref}` slots in a string using `resolve` (a property lookup).
78
+ * Well-formed slots become their rendered value (undefined -> empty string);
79
+ * malformed slots and non-slot braces are left verbatim; `{{`/`}}` unescape to
80
+ * literal `{`/`}`.
81
+ */
82
+ declare function interpolate(text: string, resolve: (ref: string) => ScalarValue | undefined): string;
83
+
84
+ export { type Slot, buildSchema, dialectWithForeignScopes, extractSlots, hostScopesToSpec, interpolate, patterDialect, renderSlotValue, splitRef, stripCaptions };
@@ -0,0 +1,84 @@
1
+ import { ExpressionSchema, Dialect, ScalarValue } from '@wildwinter/expr';
2
+ import { ScopeRegistrySpec } from '@wildwinter/scoperegistry';
3
+ import { ProjectFile, PropertyDecl, HostScopeRegistry } from '@patterkit/model';
4
+
5
+ /** The Patter dialect: scopes patter/scene + built-in functions. */
6
+ declare const patterDialect: Dialect;
7
+ /**
8
+ * A Patter dialect extended with FOREIGN scope tokens imported from another
9
+ * owner's `scopeRegistrySpec` (e.g. a storylet's `@world` / `@player` / `@system`).
10
+ * The parser needs every referenced scope token registered, so authoring tools
11
+ * that allow cross-engine references must compile/validate with this dialect.
12
+ * Foreign scopes use the default missing policy (graceful-false). With no spec
13
+ * (or an empty one) this returns the base `patterDialect` unchanged.
14
+ */
15
+ declare function dialectWithForeignScopes(spec?: ScopeRegistrySpec): Dialect;
16
+ /**
17
+ * Split a property ref ("@name" / "@scope.name") into scope + name - THE one
18
+ * ref grammar, shared by the compiler's validators and the runtime so they
19
+ * cannot drift. `isScope` says which tokens are scopes in the caller's context
20
+ * (dialect tokens, plus any foreign tokens); anything else - including a bare
21
+ * `@name` and a dotted name whose head is not a scope - is a `patter` property.
22
+ */
23
+ declare function splitRef(ref: string, isScope: (token: string) => boolean): {
24
+ scope: string;
25
+ name: string;
26
+ };
27
+ /**
28
+ * Project the model's host-scope registry (`@world`, ...) onto the
29
+ * `scopeRegistrySpec` the compiler / validator consume. Patter's property types
30
+ * are now the same vocabulary as expr's, so this is a structural pass-through
31
+ * that drops authoring-only fields (`purpose`) and omits unset `writable`;
32
+ * returns undefined for an absent registry so callers can pass it straight through.
33
+ */
34
+ declare function hostScopesToSpec(reg?: HostScopeRegistry): ScopeRegistrySpec | undefined;
35
+ /**
36
+ * Build an ExpressionSchema for validating a scene's expressions: global
37
+ * properties (`@patter`) plus that scene's scene-local properties (`@scene`),
38
+ * plus any FOREIGN scopes imported from another owner's `scopeRegistrySpec`.
39
+ * Scope mapping:
40
+ * global (project.properties) -> @patter
41
+ * scene-local (scene.sceneProps)-> @scene
42
+ * foreign (spec) -> its own token (e.g. @world)
43
+ * The `shared` flag is orthogonal - it does not affect validation (both shared
44
+ * and not-shared globals are `@patter`; both scene props are `@scene`). A foreign
45
+ * scope with no declarations is left opaque (omitted) - matching `ScopeRegistry.toSchema`.
46
+ */
47
+ declare function buildSchema(project: ProjectFile, sceneProps?: PropertyDecl[], foreign?: ScopeRegistrySpec): ExpressionSchema;
48
+ /** A `{ ... }` candidate whose trimmed body starts with `@`. */
49
+ interface Slot {
50
+ /** The whole matched text including braces, e.g. "{@gold}". */
51
+ raw: string;
52
+ /** The trimmed inner body, e.g. "@gold" or (malformed) "@gold + 1". */
53
+ inner: string;
54
+ /** The bare property ref ("@gold") when well-formed; undefined if malformed. */
55
+ ref?: string;
56
+ }
57
+ /**
58
+ * Find interpolation slots in a localised string. Only `{ ... }` whose trimmed
59
+ * body begins with `@` is a slot; a slot whose body is not a bare ref is returned
60
+ * with `ref` undefined (so the validator can flag it - the committed surface is a
61
+ * bare property reference only). Escaped `{{ }}` braces are not slots.
62
+ */
63
+ declare function extractSlots(text: string): Slot[];
64
+ /** Render a resolved slot value as display text. */
65
+ declare function renderSlotValue(v: ScalarValue): string;
66
+ /**
67
+ * Closed-caption stripping (#214): with captions OFF, remove every `open`…`close` span (delimiters
68
+ * included) from a dialogue line and collapse the surrounding whitespace -
69
+ * `Oh dear. (sigh) What now?` -> `Oh dear. What now?`. A string that contains NO cue is returned
70
+ * unchanged (its original whitespace preserved); only a string we actually edited is whitespace-
71
+ * normalised. `open` and `close` may be the same token (e.g. `*…*`); an unclosed `open` keeps the
72
+ * remainder verbatim. An empty `open` is a no-op. Identical across every Patterplay runtime - part of
73
+ * the conformance contract.
74
+ */
75
+ declare function stripCaptions(text: string, open: string, close: string): string;
76
+ /**
77
+ * Expand `{@ref}` slots in a string using `resolve` (a property lookup).
78
+ * Well-formed slots become their rendered value (undefined -> empty string);
79
+ * malformed slots and non-slot braces are left verbatim; `{{`/`}}` unescape to
80
+ * literal `{`/`}`.
81
+ */
82
+ declare function interpolate(text: string, resolve: (ref: string) => ScalarValue | undefined): string;
83
+
84
+ export { type Slot, buildSchema, dialectWithForeignScopes, extractSlots, hostScopesToSpec, interpolate, patterDialect, renderSlotValue, splitRef, stripCaptions };
package/dist/index.js ADDED
@@ -0,0 +1,341 @@
1
+ // src/index.ts
2
+ import { EvalError } from "@wildwinter/expr";
3
+ function host(h) {
4
+ return h.ctx.host ?? {};
5
+ }
6
+ var patterDialect = {
7
+ defaultScope: "patter",
8
+ scopes: [
9
+ { token: "patter" },
10
+ // global / world state (graceful-false on miss); bare @name
11
+ { token: "scene" }
12
+ // scene-local (per-flow or shared, per the property's `shared` flag)
13
+ ],
14
+ functions: {
15
+ random: {
16
+ minArgs: 2,
17
+ maxArgs: 2,
18
+ returnType: "number",
19
+ eval(args, h) {
20
+ if (args.length !== 2) throw new EvalError("random(a, b) requires exactly 2 arguments");
21
+ const next = host(h).nextRandom;
22
+ if (!next) throw new EvalError("random() called without a PRNG in context");
23
+ const a = h.evaluate(args[0]), b = h.evaluate(args[1]);
24
+ if (typeof a !== "number" || typeof b !== "number") throw new EvalError("random(a, b) arguments must be numbers");
25
+ if (!Number.isInteger(a) || !Number.isInteger(b)) throw new EvalError("random(a, b) arguments must be integers");
26
+ const lo = Math.min(a, b), hi = Math.max(a, b);
27
+ return Math.floor(next() * (hi - lo + 1)) + lo;
28
+ }
29
+ },
30
+ check_flags: {
31
+ minArgs: 1,
32
+ returnType: "boolean",
33
+ flagDeltaArgs: true,
34
+ validate: flagsCall("check_flags"),
35
+ eval(args, h) {
36
+ const flags = readFlags(args[0], h, "check_flags");
37
+ for (let i = 1; i < args.length; i++) {
38
+ const arg = args[i];
39
+ if (arg.kind !== "flagdelta") throw new EvalError("check_flags() flag args must be +flagName or -flagName");
40
+ if (arg.sign === "+" ? !flags.includes(arg.name) : flags.includes(arg.name)) return false;
41
+ }
42
+ return true;
43
+ }
44
+ },
45
+ set_flags: {
46
+ minArgs: 1,
47
+ returnType: "flags",
48
+ flagDeltaArgs: true,
49
+ validate: flagsCall("set_flags"),
50
+ eval(args, h) {
51
+ const result = [...readFlags(args[0], h, "set_flags")];
52
+ for (let i = 1; i < args.length; i++) {
53
+ const arg = args[i];
54
+ if (arg.kind !== "flagdelta") throw new EvalError("set_flags() flag args must be +flagName or -flagName");
55
+ if (arg.sign === "+") {
56
+ if (!result.includes(arg.name)) result.push(arg.name);
57
+ } else {
58
+ const idx = result.indexOf(arg.name);
59
+ if (idx >= 0) result.splice(idx, 1);
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+ },
65
+ // Visit counts (spec §7): how many times a scene / block / node has been
66
+ // *entered*. `visits` / `seen` are this flow's count (flow-local); the
67
+ // `patter_` variants are the world-wide (@patter / shared) count. The arg is a
68
+ // node id - the same id a jump targets.
69
+ visits: {
70
+ minArgs: 1,
71
+ maxArgs: 1,
72
+ returnType: "number",
73
+ validate: idArg("visits"),
74
+ eval: (args, h) => host(h).visits?.(nodeId(args, h, "visits")) ?? 0
75
+ },
76
+ seen: {
77
+ minArgs: 1,
78
+ maxArgs: 1,
79
+ returnType: "boolean",
80
+ validate: idArg("seen"),
81
+ eval: (args, h) => (host(h).visits?.(nodeId(args, h, "seen")) ?? 0) > 0
82
+ },
83
+ patter_visits: {
84
+ minArgs: 1,
85
+ maxArgs: 1,
86
+ returnType: "number",
87
+ validate: idArg("patter_visits"),
88
+ eval: (args, h) => host(h).patterVisits?.(nodeId(args, h, "patter_visits")) ?? 0
89
+ },
90
+ patter_seen: {
91
+ minArgs: 1,
92
+ maxArgs: 1,
93
+ returnType: "boolean",
94
+ validate: idArg("patter_seen"),
95
+ eval: (args, h) => (host(h).patterVisits?.(nodeId(args, h, "patter_seen")) ?? 0) > 0
96
+ }
97
+ }
98
+ };
99
+ function dialectWithForeignScopes(spec) {
100
+ if (!spec || spec.scopes.length === 0) return patterDialect;
101
+ const known = new Set(patterDialect.scopes.map((s) => s.token));
102
+ const extra = spec.scopes.filter((s) => !known.has(s.token)).map((s) => ({ token: s.token }));
103
+ if (extra.length === 0) return patterDialect;
104
+ return { ...patterDialect, scopes: [...patterDialect.scopes, ...extra] };
105
+ }
106
+ function splitRef(ref, isScope) {
107
+ const parts = ref.replace(/^@/, "").split(".");
108
+ if (parts.length === 2 && isScope(parts[0])) {
109
+ return { scope: parts[0], name: parts[1].toLowerCase() };
110
+ }
111
+ return { scope: "patter", name: parts.join(".").toLowerCase() };
112
+ }
113
+ function nodeId(args, h, fn) {
114
+ const v = h.evaluate(args[0]);
115
+ if (typeof v !== "string") throw new EvalError(`${fn}(id) requires a string node id`);
116
+ return v;
117
+ }
118
+ function idArg(fnName) {
119
+ return (args, h) => {
120
+ const first = args[0];
121
+ if (first && first.kind !== "string") {
122
+ h.report({
123
+ path: [...h.path, "args", 0],
124
+ kind: "wrong-arg-type",
125
+ severity: "error",
126
+ message: `${fnName}(id): the argument must be a string id literal (a scene / block / node id)`
127
+ });
128
+ }
129
+ };
130
+ }
131
+ function readFlags(arg, h, fn) {
132
+ if (!arg) throw new EvalError(`${fn}() requires at least one argument (the flags variable)`);
133
+ const v = h.evaluate(arg);
134
+ if (Array.isArray(v)) return v;
135
+ if (v === false || v === null || v === void 0) return [];
136
+ throw new EvalError(`${fn}() first argument must be a flags property`);
137
+ }
138
+ function flagsCall(fnName) {
139
+ return (args, h) => {
140
+ if (args.length === 0) return;
141
+ const first = args[0];
142
+ if (first.kind !== "scopedvar") {
143
+ h.report({
144
+ path: [...h.path, "args", 0],
145
+ kind: "wrong-arg-type",
146
+ severity: "error",
147
+ message: `${fnName}(): first argument must be a flags property reference (@name or @scope.name)`
148
+ });
149
+ return;
150
+ }
151
+ const meta = h.schema.properties.get(first.scope)?.get(first.name);
152
+ if (meta && meta.type !== "flags") {
153
+ const ref = first.scope === h.defaultScope ? first.name : `${first.scope}.${first.name}`;
154
+ h.report({
155
+ path: [...h.path, "args", 0],
156
+ kind: "wrong-arg-type",
157
+ severity: "error",
158
+ message: `${fnName}(): '@${ref}' is not a flags property (got ${meta.type})`
159
+ });
160
+ return;
161
+ }
162
+ for (let i = 1; i < args.length; i++) {
163
+ const arg = args[i];
164
+ if (arg.kind !== "flagdelta") {
165
+ h.report({
166
+ path: [...h.path, "args", i],
167
+ kind: "wrong-arg-type",
168
+ severity: "error",
169
+ message: `${fnName}(): argument ${i + 1} must be +flagName or -flagName`
170
+ });
171
+ } else if (meta?.type === "flags" && meta.enumValues && !meta.enumValues.includes(arg.name)) {
172
+ h.report({
173
+ path: [...h.path, "args", i],
174
+ kind: "unknown-flag-name",
175
+ severity: "error",
176
+ message: `${fnName}(): unknown flag '${arg.name}'`,
177
+ reference: arg.name
178
+ });
179
+ }
180
+ }
181
+ };
182
+ }
183
+ function hostScopesToSpec(reg) {
184
+ if (!reg) return void 0;
185
+ return {
186
+ version: reg.version,
187
+ scopes: reg.scopes.map((s) => ({
188
+ token: s.token,
189
+ ...s.writable === false ? { writable: false } : {},
190
+ ...s.declarations ? {
191
+ declarations: s.declarations.map((d) => ({
192
+ name: d.name,
193
+ type: d.type,
194
+ ...d.values ? { values: d.values } : {},
195
+ ...d.default !== void 0 ? { default: d.default } : {},
196
+ ...d.writable === false ? { writable: false } : {}
197
+ }))
198
+ } : {}
199
+ }))
200
+ };
201
+ }
202
+ function buildSchema(project, sceneProps = [], foreign) {
203
+ const properties = /* @__PURE__ */ new Map();
204
+ const put = (scope, decl) => {
205
+ let m = properties.get(scope);
206
+ if (!m) {
207
+ m = /* @__PURE__ */ new Map();
208
+ properties.set(scope, m);
209
+ }
210
+ m.set(decl.name.toLowerCase(), { type: decl.type, enumValues: decl.values });
211
+ };
212
+ for (const decl of project.properties ?? []) put("patter", decl);
213
+ for (const decl of sceneProps) put("scene", decl);
214
+ const known = new Set(patterDialect.scopes.map((s) => s.token));
215
+ for (const scope of foreign?.scopes ?? []) {
216
+ if (known.has(scope.token)) continue;
217
+ if (!scope.declarations?.length) continue;
218
+ const m = /* @__PURE__ */ new Map();
219
+ for (const d of scope.declarations) m.set(d.name.toLowerCase(), { type: d.type, enumValues: d.values });
220
+ properties.set(scope.token, m);
221
+ }
222
+ return { properties };
223
+ }
224
+ var BARE_REF = /^@[A-Za-z0-9_.]+$/;
225
+ function* tokenise(text) {
226
+ let buf = "";
227
+ let i = 0;
228
+ while (i < text.length) {
229
+ const c = text[i];
230
+ if (c === "{" && text[i + 1] === "{") {
231
+ buf += "{";
232
+ i += 2;
233
+ continue;
234
+ }
235
+ if (c === "}" && text[i + 1] === "}") {
236
+ buf += "}";
237
+ i += 2;
238
+ continue;
239
+ }
240
+ if (c === "{") {
241
+ const close = text.indexOf("}", i + 1);
242
+ if (close !== -1) {
243
+ const raw = text.slice(i, close + 1);
244
+ const inner = text.slice(i + 1, close).trim();
245
+ if (inner.startsWith("@")) {
246
+ if (buf) {
247
+ yield { kind: "text", value: buf };
248
+ buf = "";
249
+ }
250
+ yield { kind: "slot", raw, inner, ref: BARE_REF.test(inner) ? inner : void 0 };
251
+ i = close + 1;
252
+ continue;
253
+ }
254
+ buf += raw;
255
+ i = close + 1;
256
+ continue;
257
+ }
258
+ }
259
+ buf += c;
260
+ i += 1;
261
+ }
262
+ if (buf) yield { kind: "text", value: buf };
263
+ }
264
+ function extractSlots(text) {
265
+ const out = [];
266
+ for (const tok of tokenise(text)) {
267
+ if (tok.kind === "slot") out.push({ raw: tok.raw, inner: tok.inner, ref: tok.ref });
268
+ }
269
+ return out;
270
+ }
271
+ function renderSlotValue(v) {
272
+ if (Array.isArray(v)) return v.join(", ");
273
+ if (typeof v === "boolean") return v ? "true" : "false";
274
+ return String(v);
275
+ }
276
+ function isCaptionWs(c) {
277
+ return c === " " || c === " " || c === "\n" || c === "\r" || c === "\f" || c === "\v";
278
+ }
279
+ function collapseCaptionWs(s) {
280
+ let out = "";
281
+ let pendingSpace = false;
282
+ for (const c of s) {
283
+ if (isCaptionWs(c)) {
284
+ pendingSpace = true;
285
+ continue;
286
+ }
287
+ if (pendingSpace && out.length > 0) out += " ";
288
+ pendingSpace = false;
289
+ out += c;
290
+ }
291
+ return out;
292
+ }
293
+ function stripCaptions(text, open, close) {
294
+ if (open.length === 0 || text.indexOf(open) < 0) return text;
295
+ let out = "";
296
+ let i = 0;
297
+ let removed = false;
298
+ while (i < text.length) {
299
+ if (text.startsWith(open, i)) {
300
+ const end = text.indexOf(close, i + open.length);
301
+ if (end >= 0) {
302
+ i = end + close.length;
303
+ removed = true;
304
+ continue;
305
+ }
306
+ out += text.slice(i);
307
+ break;
308
+ }
309
+ out += text[i];
310
+ i += 1;
311
+ }
312
+ return removed ? collapseCaptionWs(out) : text;
313
+ }
314
+ function interpolate(text, resolve) {
315
+ if (text.indexOf("{") < 0) return text;
316
+ let out = "";
317
+ for (const tok of tokenise(text)) {
318
+ if (tok.kind === "text") {
319
+ out += tok.value;
320
+ continue;
321
+ }
322
+ if (!tok.ref) {
323
+ out += tok.raw;
324
+ continue;
325
+ }
326
+ const v = resolve(tok.ref);
327
+ out += v === void 0 ? "" : renderSlotValue(v);
328
+ }
329
+ return out;
330
+ }
331
+ export {
332
+ buildSchema,
333
+ dialectWithForeignScopes,
334
+ extractSlots,
335
+ hostScopesToSpec,
336
+ interpolate,
337
+ patterDialect,
338
+ renderSlotValue,
339
+ splitRef,
340
+ stripCaptions
341
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@patterkit/dialect",
3
+ "version": "0.1.0",
4
+ "description": "The Patter dialect for @wildwinter/expr (scopes shared/scene/flow, built-in functions), an ExpressionSchema builder from property declarations, and the inline {@ref} interpolation helpers (spec §16).",
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/dialect" },
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
+ "@wildwinter/expr": "^0.3.0",
30
+ "@wildwinter/scoperegistry": "^0.1.0"
31
+ }
32
+ }