@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 +374 -0
- package/dist/index.d.cts +84 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +341 -0
- package/package.json +32 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|