@phin-tech/dottie 0.10.0 → 0.11.1

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/dottie.js CHANGED
@@ -1,84 +1,334 @@
1
- /*! dottie.js 0.10.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.11.1 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
4
4
  var __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
5
5
 
6
- // src/state.ts
6
+ // src/expression.ts
7
+ var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
8
+ var FUNCTIONS = { min: null, max: null, abs: 1, floor: 1, ceil: 1, round: 1, clamp: 3 };
7
9
  var MAX_NAME = 64;
8
- function isName(name) {
9
- return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
10
+ var truthy = (v) => typeof v === "boolean" ? v : v !== 0 && !Number.isNaN(v);
11
+ var num = (v) => typeof v === "boolean" ? v ? 1 : 0 : v;
12
+ var parsed = /* @__PURE__ */ new Map();
13
+ function parseExpr(text) {
14
+ const hit = parsed.get(text);
15
+ if (hit !== void 0) return hit;
16
+ const p = new Parser(text);
17
+ let e = p.or();
18
+ if (e && !p.atEnd()) e = null;
19
+ if (parsed.size > 4096) parsed.clear();
20
+ parsed.set(text, e);
21
+ return e;
10
22
  }
11
- var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
12
- var OPS = [">=", "<=", "==", "!=", ">", "<"];
13
- function metricTerm(term) {
14
- const t = trim(term);
15
- if (!t.includes("(")) {
16
- const op2 = t.search(/[>=<!]/);
17
- if (op2 < 0) return null;
18
- const name2 = trim(t.slice(0, op2));
19
- if (!METRIC_NAMES.includes(name2)) return null;
20
- const m = metricTerm(name2 + "() " + t.slice(op2));
21
- return m ? { ...m, target: "" } : null;
22
- }
23
- const open = t.indexOf("("), close = t.indexOf(")");
24
- if (open < 0 || close < 0 || open >= close) return null;
25
- const name = trim(t.slice(0, open)), target = trim(t.slice(open + 1, close));
26
- let rest = trim(t.slice(close + 1));
27
- if (!METRIC_NAMES.includes(name) || target.includes("(")) return null;
28
- const op = OPS.find((o) => rest.startsWith(o));
29
- if (op === void 0) return null;
30
- rest = trim(rest.slice(op.length));
31
- if (!/^-?[0-9]+(\.[0-9]+)?$/.test(rest)) return null;
32
- return { metric: name, target, op, value: Number(rest) };
33
- }
34
- function passes(m, v) {
35
- switch (m.op) {
36
- case ">":
37
- return v > m.value;
38
- case ">=":
39
- return v >= m.value;
40
- case "<":
41
- return v < m.value;
42
- case "<=":
43
- return v <= m.value;
44
- case "==":
45
- return v === m.value;
46
- default:
47
- return v !== m.value;
23
+ function evaluate(e, state, metric = null) {
24
+ switch (e.k) {
25
+ case "lit":
26
+ return e.v;
27
+ case "name":
28
+ return state[e.name] ?? false;
29
+ case "metric":
30
+ return metric?.(e.metric, e.target) ?? NaN;
31
+ case "not":
32
+ return !truthy(evaluate(e.e, state, metric));
33
+ case "neg":
34
+ return -num(evaluate(e.e, state, metric));
35
+ case "call": {
36
+ const xs = e.args.map((a) => num(evaluate(a, state, metric)));
37
+ const fold = (better) => {
38
+ if (xs.some(Number.isNaN)) return NaN;
39
+ let out = xs[0];
40
+ for (const x of xs.slice(1)) if (better(x, out)) out = x;
41
+ return out;
42
+ };
43
+ switch (e.fn) {
44
+ case "min":
45
+ return fold((a, b) => a < b);
46
+ case "max":
47
+ return fold((a, b) => a > b);
48
+ case "abs":
49
+ return Math.abs(xs[0]);
50
+ case "floor":
51
+ return Math.floor(xs[0]);
52
+ case "ceil":
53
+ return Math.ceil(xs[0]);
54
+ case "round":
55
+ return Math.floor(xs[0] + 0.5);
56
+ default:
57
+ if (xs.some(Number.isNaN)) return NaN;
58
+ return xs[0] < xs[1] ? xs[1] : xs[0] > xs[2] ? xs[2] : xs[0];
59
+ }
60
+ }
61
+ case "bin": {
62
+ if (e.op === "&&") return truthy(evaluate(e.l, state, metric)) && truthy(evaluate(e.r, state, metric));
63
+ if (e.op === "||") return truthy(evaluate(e.l, state, metric)) || truthy(evaluate(e.r, state, metric));
64
+ const a = evaluate(e.l, state, metric), b = evaluate(e.r, state, metric);
65
+ const x = num(a), y = num(b);
66
+ switch (e.op) {
67
+ case "+":
68
+ return x + y;
69
+ case "-":
70
+ return x - y;
71
+ case "*":
72
+ return x * y;
73
+ case "/":
74
+ return x / y;
75
+ case "%":
76
+ return x % y;
77
+ }
78
+ if (Number.isNaN(x) || Number.isNaN(y)) return false;
79
+ switch (e.op) {
80
+ case "<":
81
+ return x < y;
82
+ case "<=":
83
+ return x <= y;
84
+ case ">":
85
+ return x > y;
86
+ case ">=":
87
+ return x >= y;
88
+ case "==":
89
+ return typeof a === "boolean" && typeof b === "boolean" ? a === b : x === y;
90
+ default:
91
+ return typeof a === "boolean" && typeof b === "boolean" ? a !== b : x !== y;
92
+ }
93
+ }
48
94
  }
49
95
  }
50
- function mentionsMetric(condition) {
51
- return condition.split("||").some((any) => any.split("&&").some((all) => metricTerm(all) !== null));
96
+ function exprNames(e) {
97
+ const out = [];
98
+ const walk = (x) => {
99
+ switch (x.k) {
100
+ case "name":
101
+ if (!out.includes(x.name)) out.push(x.name);
102
+ break;
103
+ case "not":
104
+ case "neg":
105
+ walk(x.e);
106
+ break;
107
+ case "bin":
108
+ walk(x.l);
109
+ walk(x.r);
110
+ break;
111
+ case "call":
112
+ x.args.forEach(walk);
113
+ break;
114
+ }
115
+ };
116
+ walk(e);
117
+ return out;
52
118
  }
53
- function conditionNames(condition) {
119
+ function exprMetrics(e) {
54
120
  const out = [];
55
- for (const any of condition.split("||")) {
56
- for (const all of any.split("&&")) {
57
- if (metricTerm(all) !== null) continue;
58
- let term = trim(all);
59
- if (term.startsWith("!")) term = trim(term.slice(1));
60
- if (!isName(term)) return null;
61
- out.push(term);
121
+ const walk = (x) => {
122
+ switch (x.k) {
123
+ case "metric":
124
+ out.push({ metric: x.metric, target: x.target });
125
+ break;
126
+ case "not":
127
+ case "neg":
128
+ walk(x.e);
129
+ break;
130
+ case "bin":
131
+ walk(x.l);
132
+ walk(x.r);
133
+ break;
134
+ case "call":
135
+ x.args.forEach(walk);
136
+ break;
62
137
  }
63
- }
138
+ };
139
+ walk(e);
64
140
  return out;
65
141
  }
142
+ var isDigit = (c) => c !== void 0 && c >= "0" && c <= "9";
143
+ var isLetter = (c) => c !== void 0 && (c === "_" || c >= "a" && c <= "z" || c >= "A" && c <= "Z");
144
+ var Parser = class {
145
+ constructor(text) {
146
+ __publicField(this, "s");
147
+ __publicField(this, "i", 0);
148
+ this.s = [...text];
149
+ }
150
+ atEnd() {
151
+ this.skip();
152
+ return this.i === this.s.length;
153
+ }
154
+ skip() {
155
+ while (this.i < this.s.length && " \n\r".includes(this.s[this.i])) this.i++;
156
+ }
157
+ eat(token) {
158
+ this.skip();
159
+ const t = [...token];
160
+ if (this.i + t.length > this.s.length) return false;
161
+ for (let k = 0; k < t.length; k++) if (this.s[this.i + k] !== t[k]) return false;
162
+ if (t.length === 1 && this.s[this.i + 1] === "=" && "<>!=".includes(t[0])) return false;
163
+ this.i += t.length;
164
+ return true;
165
+ }
166
+ or() {
167
+ let l = this.and();
168
+ if (!l) return null;
169
+ while (this.eat("||")) {
170
+ const r = this.and();
171
+ if (!r) return null;
172
+ l = { k: "bin", op: "||", l, r };
173
+ }
174
+ return l;
175
+ }
176
+ and() {
177
+ let l = this.compare();
178
+ if (!l) return null;
179
+ while (this.eat("&&")) {
180
+ const r = this.compare();
181
+ if (!r) return null;
182
+ l = { k: "bin", op: "&&", l, r };
183
+ }
184
+ return l;
185
+ }
186
+ compare() {
187
+ const l = this.sum();
188
+ if (!l) return null;
189
+ for (const op of ["<=", ">=", "==", "!=", "<", ">"]) {
190
+ if (this.eat(op)) {
191
+ const r = this.sum();
192
+ return r ? { k: "bin", op, l, r } : null;
193
+ }
194
+ }
195
+ return l;
196
+ }
197
+ sum() {
198
+ let l = this.product();
199
+ if (!l) return null;
200
+ for (; ; ) {
201
+ const op = this.eat("+") ? "+" : this.eat("-") ? "-" : null;
202
+ if (!op) return l;
203
+ const r = this.product();
204
+ if (!r) return null;
205
+ l = { k: "bin", op, l, r };
206
+ }
207
+ }
208
+ product() {
209
+ let l = this.unary();
210
+ if (!l) return null;
211
+ for (; ; ) {
212
+ const op = ["*", "/", "%"].find((o) => this.eat(o));
213
+ if (!op) return l;
214
+ const r = this.unary();
215
+ if (!r) return null;
216
+ l = { k: "bin", op, l, r };
217
+ }
218
+ }
219
+ unary() {
220
+ if (this.eat("!")) {
221
+ const e = this.unary();
222
+ return e ? { k: "not", e } : null;
223
+ }
224
+ if (this.eat("-")) {
225
+ const e = this.unary();
226
+ return e ? { k: "neg", e } : null;
227
+ }
228
+ return this.primary();
229
+ }
230
+ primary() {
231
+ this.skip();
232
+ if (this.i >= this.s.length) return null;
233
+ if (this.eat("(")) {
234
+ const e = this.or();
235
+ return e && this.eat(")") ? e : null;
236
+ }
237
+ const s = this.s;
238
+ const c = s[this.i];
239
+ if (isDigit(c)) {
240
+ const start3 = this.i;
241
+ while (isDigit(s[this.i])) this.i++;
242
+ if (s[this.i] === "." && isDigit(s[this.i + 1])) {
243
+ this.i++;
244
+ while (isDigit(s[this.i])) this.i++;
245
+ }
246
+ return { k: "lit", v: Number(s.slice(start3, this.i).join("")) };
247
+ }
248
+ if (!isLetter(c)) return null;
249
+ const start2 = this.i;
250
+ while (isLetter(s[this.i]) || isDigit(s[this.i])) this.i++;
251
+ const name = s.slice(start2, this.i).join("");
252
+ if (name.length > MAX_NAME) return null;
253
+ let j = this.i;
254
+ while (s[j] === " " || s[j] === " ") j++;
255
+ const called = s[j] === "(";
256
+ if (METRIC_NAMES.includes(name)) {
257
+ if (!called) return { k: "metric", metric: name, target: "" };
258
+ this.eat("(");
259
+ const from = this.i;
260
+ while (this.i < s.length && s[this.i] !== ")" && s[this.i] !== "(") this.i++;
261
+ if (s[this.i] !== ")") return null;
262
+ const target = s.slice(from, this.i).join("").replace(/^[ \t]+|[ \t]+$/g, "");
263
+ this.i++;
264
+ return { k: "metric", metric: name, target };
265
+ }
266
+ if (name in FUNCTIONS) {
267
+ if (!called || !this.eat("(")) return null;
268
+ const args = [];
269
+ if (!this.eat(")")) {
270
+ do {
271
+ const a = this.or();
272
+ if (!a) return null;
273
+ args.push(a);
274
+ } while (this.eat(","));
275
+ if (!this.eat(")")) return null;
276
+ }
277
+ const arity = FUNCTIONS[name];
278
+ if (arity !== null ? args.length !== arity : args.length === 0) return null;
279
+ return { k: "call", fn: name, args };
280
+ }
281
+ if (called) return null;
282
+ if (name === "true") return { k: "lit", v: true };
283
+ if (name === "false") return { k: "lit", v: false };
284
+ return { k: "name", name };
285
+ }
286
+ };
287
+ function describeVar(v) {
288
+ return typeof v === "boolean" ? v ? "true" : "false" : formatTwo(v);
289
+ }
290
+ function formatTwo(v) {
291
+ const n = Math.floor(Math.abs(v) * 100 + 0.5);
292
+ if (!(n < 1e15)) return v < 0 ? "-\u221E" : "\u221E";
293
+ const whole = Math.floor(n / 100), part = n - whole * 100;
294
+ let s = String(whole);
295
+ if (part > 0) s += "." + String(part).padStart(2, "0").replace(/0+$/, "");
296
+ return (v < 0 && n > 0 ? "-" : "") + s;
297
+ }
298
+ function filling(text, values) {
299
+ if (!text.includes("{")) return text;
300
+ let out = "", rest = text;
301
+ for (let open = rest.indexOf("{"); open >= 0; open = rest.indexOf("{")) {
302
+ out += rest.slice(0, open);
303
+ const close = rest.indexOf("}", open);
304
+ if (close < 0) {
305
+ out += rest.slice(open);
306
+ rest = "";
307
+ break;
308
+ }
309
+ const inner = rest.slice(open + 1, close);
310
+ const e = inner.includes("{") ? null : parseExpr(inner);
311
+ const names = e ? exprNames(e) : [];
312
+ if (e && exprMetrics(e).length === 0 && names.length > 0 && names.every((n) => values[n] !== void 0)) out += describeVar(evaluate(e, values));
313
+ else out += rest.slice(open, close + 1);
314
+ rest = rest.slice(close + 1);
315
+ }
316
+ return out + rest;
317
+ }
318
+
319
+ // src/state.ts
320
+ var MAX_NAME2 = 64;
321
+ function isName(name) {
322
+ return name.length <= MAX_NAME2 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
323
+ }
324
+ function mentionsMetric(condition) {
325
+ const e = parseExpr(condition);
326
+ return e !== null && exprMetrics(e).length > 0;
327
+ }
66
328
  function holds(condition, state, metric = null) {
67
- if (conditionNames(condition) === null) return false;
68
- return condition.split("||").some((any) => any.split("&&").every((all) => {
69
- const m = metricTerm(all);
70
- if (m !== null) {
71
- const v = metric ? metric(m) : null;
72
- return v !== null && passes(m, v);
73
- }
74
- let term = trim(all);
75
- let negated = false;
76
- if (term.startsWith("!")) {
77
- negated = true;
78
- term = trim(term.slice(1));
79
- }
80
- return (state[term] ?? false) !== negated;
81
- }));
329
+ const e = parseExpr(condition);
330
+ if (!e) return false;
331
+ return truthy(evaluate(e, state, metric ? (m, target) => metric({ metric: m, target, op: "", value: 0 }) : null));
82
332
  }
83
333
  function stateValues(page, choice) {
84
334
  if (choice === "all") return null;
@@ -117,7 +367,7 @@ function applyOverride(o, e) {
117
367
  return out;
118
368
  }
119
369
  function changesState(c) {
120
- return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
370
+ return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0 || !!c.run);
121
371
  }
122
372
  function applyClick(c, state, initial = {}) {
123
373
  let out = { ...state };
@@ -126,12 +376,15 @@ function applyClick(c, state, initial = {}) {
126
376
  for (const k of Object.keys(state)) out[k] = false;
127
377
  Object.assign(out, initial);
128
378
  }
129
- for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
130
- for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
379
+ const before = { ...out };
380
+ for (const [k, v] of Object.entries(c.set ?? {})) out[k] = clickValue(v, before, before[k]);
381
+ for (const k of c.toggle ?? []) out[k] = !truthy(out[k] ?? false);
131
382
  return out;
132
383
  }
133
- function trim(s) {
134
- return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
384
+ function clickValue(v, state, current) {
385
+ if (typeof v !== "string") return v;
386
+ const e = parseExpr(v);
387
+ return e ? evaluate(e, state) : current ?? false;
135
388
  }
136
389
 
137
390
  // src/theme.ts
@@ -193,12 +446,12 @@ function resolvingPalette(page) {
193
446
  }
194
447
 
195
448
  // src/model.ts
196
- var SUPPORTED_VERSION = 2;
449
+ var SUPPORTED_VERSION = 3;
197
450
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
198
451
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
199
452
  var KNOWN_FIELDS = {
200
453
  file: ["version", "pages", "images"],
201
- page: ["id", "name", "layers", "elements", "theme", "customTheme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
454
+ page: ["id", "name", "layers", "elements", "theme", "customTheme", "logic", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
202
455
  flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
203
456
  flowOverride: ["when", "volume"],
204
457
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
@@ -267,7 +520,7 @@ var KNOWN_FIELDS = {
267
520
  repeat: ["start", "end", "count"],
268
521
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
269
522
  marker: ["t", "name", "loop"],
270
- click: ["scene", "url", "set", "toggle", "reset"],
523
+ click: ["scene", "url", "set", "toggle", "reset", "run"],
271
524
  hover: ["stroke", "fill", "opacity"],
272
525
  override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
273
526
  };
@@ -281,7 +534,7 @@ var inkOnDark = { r: 0.93, g: 0.93, b: 0.94, a: 1 };
281
534
  var DottieFormatError = class extends Error {
282
535
  };
283
536
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
284
- var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
537
+ var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
285
538
  var str = (v) => typeof v === "string" ? v : void 0;
286
539
  var strings = (v) => isObj(v) && Object.values(v).every((x) => typeof x === "string") ? { ...v } : null;
287
540
  var bool = (v) => typeof v === "boolean" ? v : void 0;
@@ -305,9 +558,9 @@ function webLink(url) {
305
558
  }
306
559
  function rgba(v) {
307
560
  if (!isObj(v)) return void 0;
308
- const r = num(v.r), g = num(v.g), b = num(v.b);
561
+ const r = num2(v.r), g = num2(v.g), b = num2(v.b);
309
562
  if (r === void 0 || g === void 0 || b === void 0) return void 0;
310
- return { r, g, b, a: num(v.a) ?? 1 };
563
+ return { r, g, b, a: num2(v.a) ?? 1 };
311
564
  }
312
565
  var Reader = class {
313
566
  constructor() {
@@ -336,15 +589,15 @@ var Reader = class {
336
589
  stroke: rgba(s.stroke) ?? { ...black },
337
590
  fill: rgba(s.fill) ?? { ...clear },
338
591
  fillStyle: ["solid", "hachure", "crossHatch", "grid"].includes(fillStyle) ? fillStyle : "solid",
339
- strokeWidth: num(s.strokeWidth) ?? 2,
592
+ strokeWidth: num2(s.strokeWidth) ?? 2,
340
593
  dash: ["solid", "dashed", "dotted"].includes(dash) ? dash : "solid",
341
- roughness: num(s.roughness) ?? 1,
342
- bowing: num(s.bowing) ?? 1,
343
- fontSize: num(s.fontSize) ?? 18,
594
+ roughness: num2(s.roughness) ?? 1,
595
+ bowing: num2(s.bowing) ?? 1,
596
+ fontSize: num2(s.fontSize) ?? 18,
344
597
  font: str(s.font) || null,
345
- gridSize: num(s.gridSize) ?? null,
346
- opacity: num(s.opacity) ?? null,
347
- dashOffset: num(s.dashOffset) ?? null,
598
+ gridSize: num2(s.gridSize) ?? null,
599
+ opacity: num2(s.opacity) ?? null,
600
+ dashOffset: num2(s.dashOffset) ?? null,
348
601
  textColor: rgba(s.textColor) ?? null,
349
602
  named: this.colorNames(s.named)
350
603
  };
@@ -378,13 +631,13 @@ var Reader = class {
378
631
  endpoint(v) {
379
632
  const e = isObj(v) ? v : {};
380
633
  const port = str(e.port);
381
- const portIndex = num(e.portIndex);
634
+ const portIndex = num2(e.portIndex);
382
635
  return {
383
636
  elementID: uuid(e.elementID) ?? null,
384
637
  port: port && ["top", "right", "bottom", "left"].includes(port) ? port : null,
385
638
  portIndex: portIndex !== void 0 && Number.isInteger(portIndex) && portIndex >= 0 ? portIndex : null,
386
- x: num(e.x) ?? 0,
387
- y: num(e.y) ?? 0
639
+ x: num2(e.x) ?? 0,
640
+ y: num2(e.y) ?? 0
388
641
  };
389
642
  }
390
643
  connector(v) {
@@ -403,7 +656,7 @@ var Reader = class {
403
656
  endHead: head(v.endHead, "arrow"),
404
657
  waypoints: Array.isArray(v.waypoints) ? v.waypoints.filter(isObj).map((w) => {
405
658
  this.fields(w, "waypoint");
406
- return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
659
+ return { x: num2(w.x) ?? 0, y: num2(w.y) ?? 0 };
407
660
  }) : null,
408
661
  hops: typeof v.hops === "boolean" ? v.hops : null,
409
662
  avoid: typeof v.avoid === "boolean" ? v.avoid : null,
@@ -439,20 +692,20 @@ var Reader = class {
439
692
  id,
440
693
  kind,
441
694
  layerID,
442
- x: num(v.x) ?? 0,
443
- y: num(v.y) ?? 0,
444
- width: num(v.width) ?? 0,
445
- height: num(v.height) ?? 0,
695
+ x: num2(v.x) ?? 0,
696
+ y: num2(v.y) ?? 0,
697
+ width: num2(v.width) ?? 0,
698
+ height: num2(v.height) ?? 0,
446
699
  style: this.style(v.style),
447
- seed: (num(v.seed) ?? 1) >>> 0,
700
+ seed: (num2(v.seed) ?? 1) >>> 0,
448
701
  text: str(v.text) ?? null,
449
702
  connector: this.connector(v.connector),
450
703
  groupID: uuid(v.groupID) ?? null,
451
- points: Array.isArray(v.points) ? v.points.filter(isObj).map((p) => ({ x: num(p.x) ?? 0, y: num(p.y) ?? 0 })) : null,
704
+ points: Array.isArray(v.points) ? v.points.filter(isObj).map((p) => ({ x: num2(p.x) ?? 0, y: num2(p.y) ?? 0 })) : null,
452
705
  path: Array.isArray(v.path) ? v.path.filter((c) => typeof c === "number") : null,
453
706
  subpaths,
454
707
  evenOdd: bool(v.evenOdd) ?? null,
455
- rotation: num(v.rotation) ?? null,
708
+ rotation: num2(v.rotation) ?? null,
456
709
  link: uuid(v.link) ?? null,
457
710
  url: str(v.url) ?? null,
458
711
  anchor: str(v.anchor) ?? null,
@@ -462,18 +715,18 @@ var Reader = class {
462
715
  overrides: this.overrides(v.overrides),
463
716
  symbol: uuid(v.symbol) ?? null,
464
717
  playback: this.playback(v.playback),
465
- offset: num(v.offset) ?? null,
718
+ offset: num2(v.offset) ?? null,
466
719
  args: strings(v.args),
467
720
  image: this.image(v.image, images),
468
- reveal: num(v.reveal) ?? null,
721
+ reveal: num2(v.reveal) ?? null,
469
722
  ports: Array.isArray(v.ports) ? v.ports.map((p) => {
470
723
  const o = isObj(p) ? p : {};
471
- return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
724
+ return { x: num2(o.x) ?? 0, y: num2(o.y) ?? 0 };
472
725
  }) : null,
473
726
  labelRect: this.labelRect(v.labelRect),
474
727
  extension: this.extension(v.extension),
475
- capacity: num(v.capacity) ?? null,
476
- delay: num(v.delay) ?? null,
728
+ capacity: num2(v.capacity) ?? null,
729
+ delay: num2(v.delay) ?? null,
477
730
  queue: v.queue === true ? true : null,
478
731
  gauge: this.gauge(v.gauge),
479
732
  containerID: uuid(v.containerID) ?? null,
@@ -498,7 +751,7 @@ var Reader = class {
498
751
  labelRect(v) {
499
752
  if (!isObj(v)) return null;
500
753
  this.fields(v, "normalizedRect");
501
- return { x: num(v.x) ?? 0, y: num(v.y) ?? 0, width: num(v.width) ?? 0, height: num(v.height) ?? 0 };
754
+ return { x: num2(v.x) ?? 0, y: num2(v.y) ?? 0, width: num2(v.width) ?? 0, height: num2(v.height) ?? 0 };
502
755
  }
503
756
  extension(v) {
504
757
  if (!isObj(v)) return null;
@@ -519,7 +772,7 @@ var Reader = class {
519
772
  variables(v) {
520
773
  if (!isObj(v)) return null;
521
774
  const out = {};
522
- for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
775
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean" || typeof b === "number" && Number.isFinite(b)) out[k] = b;
523
776
  return Object.keys(out).length > 0 ? out : null;
524
777
  }
525
778
  click(v) {
@@ -531,12 +784,22 @@ var Reader = class {
531
784
  let set = null;
532
785
  if (isObj(v.set)) {
533
786
  set = {};
534
- for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
787
+ for (const [k, b] of Object.entries(v.set)) {
788
+ if (isName(k) && (typeof b === "boolean" || typeof b === "string" || typeof b === "number" && Number.isFinite(b))) set[k] = b;
789
+ }
535
790
  if (Object.keys(set).length === 0) set = null;
536
791
  }
537
792
  const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
538
793
  const toggle = raw ? raw.filter(isName) : null;
539
- return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
794
+ const run = str(v.run);
795
+ return {
796
+ scene: str(v.scene) ?? null,
797
+ url: safe,
798
+ set,
799
+ toggle: toggle && toggle.length > 0 ? toggle : null,
800
+ reset: v.reset === true ? true : null,
801
+ run: run !== void 0 && isName(run) ? run : null
802
+ };
540
803
  }
541
804
  playback(v) {
542
805
  const s = str(v);
@@ -552,7 +815,7 @@ var Reader = class {
552
815
  return null;
553
816
  }
554
817
  const size = (x) => {
555
- const n = num(x);
818
+ const n = num2(x);
556
819
  return Math.max(1, n ?? 100);
557
820
  };
558
821
  const layers = (Array.isArray(v.layers) ? v.layers : []).filter(isObj).flatMap((l) => {
@@ -589,8 +852,8 @@ var Reader = class {
589
852
  stroke: rgba(o.stroke) ?? null,
590
853
  fill: rgba(o.fill) ?? null,
591
854
  textColor: rgba(o.textColor) ?? null,
592
- opacity: num(o.opacity) ?? null,
593
- strokeWidth: num(o.strokeWidth) ?? null,
855
+ opacity: num2(o.opacity) ?? null,
856
+ strokeWidth: num2(o.strokeWidth) ?? null,
594
857
  dash: dash !== void 0 && ["solid", "dashed", "dotted"].includes(dash) ? dash : null,
595
858
  text: str(o.text) ?? null,
596
859
  reroute,
@@ -604,7 +867,7 @@ var Reader = class {
604
867
  hover(v) {
605
868
  if (!isObj(v)) return null;
606
869
  this.fields(v, "hover");
607
- return { stroke: rgba(v.stroke) ?? null, fill: rgba(v.fill) ?? null, opacity: num(v.opacity) ?? null };
870
+ return { stroke: rgba(v.stroke) ?? null, fill: rgba(v.fill) ?? null, opacity: num2(v.opacity) ?? null };
608
871
  }
609
872
  ease(v) {
610
873
  const s = str(v);
@@ -618,11 +881,11 @@ var Reader = class {
618
881
  keyframe(v) {
619
882
  if (!isObj(v)) return null;
620
883
  this.fields(v, "keyframe");
621
- const t = num(v.t);
884
+ const t = num2(v.t);
622
885
  if (t === void 0) return null;
623
886
  const k = { t };
624
887
  for (const p of ["x", "y", "width", "height", "rotation", "strokeWidth", "reveal", "opacity"]) {
625
- const n = num(v[p]);
888
+ const n = num2(v[p]);
626
889
  if (n !== void 0) k[p] = n;
627
890
  }
628
891
  const stroke = rgba(v.stroke), fill = rgba(v.fill), visible = bool(v.visible), ease = this.ease(v.ease);
@@ -640,39 +903,39 @@ var Reader = class {
640
903
  const keys = (Array.isArray(v.keys) ? v.keys : []).map((k) => this.keyframe(k)).filter((k) => k !== null && k.t >= 0 && k.t <= MAX_DURATION);
641
904
  stableSortBy(keys, (k) => k.t);
642
905
  const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
643
- const path = uuid(f.path), start2 = num(f.start), end = num(f.end);
906
+ const path = uuid(f.path), start2 = num2(f.start), end = num2(f.end);
644
907
  if (!path || start2 === void 0 || end === void 0 || start2 < 0 || end > MAX_DURATION) return [];
645
- return [{ path, start: start2, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
908
+ return [{ path, start: start2, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num2(f.laps) ?? null }];
646
909
  }) : null;
647
910
  const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
648
- const start2 = num(r.start), end = num(r.end);
911
+ const start2 = num2(r.start), end = num2(r.end);
649
912
  if (start2 === void 0 || end === void 0) return [];
650
- const count = num(r.count);
913
+ const count = num2(r.count);
651
914
  const rep = { start: start2, end, count: count === void 0 ? null : Math.trunc(count) };
652
915
  return repeatIsValid(rep) ? [rep] : [];
653
916
  }) : null;
654
- return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
917
+ return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num2(v.march)) };
655
918
  }
656
919
  timeline(v) {
657
920
  if (!isObj(v)) return null;
658
921
  this.fields(v, "timeline");
659
- const boil = num(v.boil);
922
+ const boil = num2(v.boil);
660
923
  const camera = Array.isArray(v.camera) ? v.camera.filter(isObj).flatMap((c) => {
661
- const t = num(c.t), x = num(c.x), y = num(c.y), width = num(c.width), height = num(c.height);
924
+ const t = num2(c.t), x = num2(c.x), y = num2(c.y), width = num2(c.width), height = num2(c.height);
662
925
  if (t === void 0 || x === void 0 || y === void 0 || width === void 0 || height === void 0) return [];
663
926
  if (!(width > 1 && height > 1 && t >= 0)) return [];
664
927
  return [{ t, x, y, width, height, ease: this.ease(c.ease) }];
665
928
  }) : null;
666
929
  if (camera) stableSortBy(camera, (c) => c.t);
667
930
  const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
668
- const t = num(m.t);
931
+ const t = num2(m.t);
669
932
  return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
670
933
  }) : null;
671
934
  if (markers) stableSortBy(markers, (m) => m.t);
672
- const poster = num(v.poster);
935
+ const poster = num2(v.poster);
673
936
  return {
674
- fps: clampFPS(num(v.fps) ?? 12),
675
- duration: clampTime(num(v.duration) ?? 0),
937
+ fps: clampFPS(num2(v.fps) ?? 12),
938
+ duration: clampTime(num2(v.duration) ?? 0),
676
939
  boil: boil !== void 0 && Number.isInteger(boil) && boil > 0 ? Math.min(boil, 1e4) : null,
677
940
  settle: bool(v.settle) ?? null,
678
941
  poster: poster === void 0 ? null : clampTime(poster),
@@ -699,6 +962,7 @@ var Reader = class {
699
962
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
700
963
  theme: str(v.theme) ?? null,
701
964
  customTheme: this.customTheme(v.customTheme),
965
+ logic: typeof v.logic === "string" && v.logic.length > 0 ? v.logic : null,
702
966
  timeline: this.timeline(v.timeline),
703
967
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
704
968
  variables: this.variables(v.variables),
@@ -710,7 +974,7 @@ var Reader = class {
710
974
  this.fields(f, "flow");
711
975
  const id = (x) => typeof x === "string" && UUID.test(x) ? x.toUpperCase() : NO_ID;
712
976
  const finite = (x) => {
713
- const n = num(x);
977
+ const n = num2(x);
714
978
  return n !== void 0 && Number.isFinite(n) ? n : null;
715
979
  };
716
980
  return {
@@ -750,7 +1014,7 @@ function parseDottie(source) {
750
1014
  }
751
1015
  }
752
1016
  if (!isObj(json)) throw new DottieFormatError("Not a Dottie drawing");
753
- const version = num(json.version);
1017
+ const version = num2(json.version);
754
1018
  if (version === void 0) throw new DottieFormatError("Not a Dottie drawing (no version)");
755
1019
  if (!Array.isArray(json.pages) || json.pages.length === 0) throw new DottieFormatError("A drawing needs at least one page");
756
1020
  const reader = new Reader();
@@ -2998,7 +3262,7 @@ function hopCrossings(e, r, page, index) {
2998
3262
  for (const samples of segments) {
2999
3263
  const along = runningLengths(samples);
3000
3264
  const length = along[along.length - 1];
3001
- const found = [];
3265
+ const found2 = [];
3002
3266
  const near = [];
3003
3267
  const whole = boxOf(samples);
3004
3268
  for (let n = 0; n < count; n++) if (touching(whole, lines[n].box)) near.push(lines[n]);
@@ -3015,12 +3279,12 @@ function hopCrossings(e, r, page, index) {
3015
3279
  if (Math.abs(det) < 1e-9) continue;
3016
3280
  const wx = a.x - p.x, wy = a.y - p.y;
3017
3281
  const t = (wx * ey - wy * ex) / det, s = (wx * dy - wy * dx) / det;
3018
- if (t > 0 && t < 1 && s >= 0 && s <= 1) found.push(along[i] + t * (along[i + 1] - along[i]));
3282
+ if (t > 0 && t < 1 && s >= 0 && s <= 1) found2.push(along[i] + t * (along[i + 1] - along[i]));
3019
3283
  }
3020
3284
  }
3021
3285
  }
3022
3286
  const kept = [];
3023
- for (const at of found.sort((x, y) => x - y)) {
3287
+ for (const at of found2.sort((x, y) => x - y)) {
3024
3288
  if (at < rad + 2 || at > length - rad - 2) continue;
3025
3289
  if (kept.length > 0 && at - kept[kept.length - 1] < 2 * rad + 2) continue;
3026
3290
  kept.push(at);
@@ -3234,7 +3498,7 @@ var lerpColor = (a, b, u) => ({
3234
3498
  b: mix01(a.b, b.b, u),
3235
3499
  a: mix01(a.a, b.a, u)
3236
3500
  });
3237
- function num2(track, t, p, base2) {
3501
+ function num3(track, t, p, base2) {
3238
3502
  return value(track, t, (k) => k[p], base2, lerpNum);
3239
3503
  }
3240
3504
  function isChanging(track, t) {
@@ -3260,20 +3524,20 @@ function isVisible(track, t) {
3260
3524
  function applyTrack(track, e, t) {
3261
3525
  e = cloneElement(e);
3262
3526
  if (track.keys.some((k) => k.reveal !== void 0)) {
3263
- const r = num2(track, t, "reveal", e.reveal ?? 1);
3527
+ const r = num3(track, t, "reveal", e.reveal ?? 1);
3264
3528
  e.reveal = r >= 1 ? null : Math.max(0, r);
3265
3529
  }
3266
3530
  if (track.keys.some((k) => k.opacity !== void 0)) {
3267
- const o = Math.min(Math.max(num2(track, t, "opacity", e.style.opacity ?? 1), 0), 1);
3531
+ const o = Math.min(Math.max(num3(track, t, "opacity", e.style.opacity ?? 1), 0), 1);
3268
3532
  e.style.opacity = o >= 1 ? null : o;
3269
3533
  }
3270
3534
  if (e.kind === "connector") return e;
3271
- e.x = num2(track, t, "x", e.x);
3272
- e.y = num2(track, t, "y", e.y);
3273
- e.width = Math.max(0, num2(track, t, "width", e.width));
3274
- e.height = Math.max(0, num2(track, t, "height", e.height));
3275
- if (track.keys.some((k) => k.rotation !== void 0)) e.rotation = num2(track, t, "rotation", angle(e));
3276
- e.style.strokeWidth = Math.max(0, num2(track, t, "strokeWidth", e.style.strokeWidth));
3535
+ e.x = num3(track, t, "x", e.x);
3536
+ e.y = num3(track, t, "y", e.y);
3537
+ e.width = Math.max(0, num3(track, t, "width", e.width));
3538
+ e.height = Math.max(0, num3(track, t, "height", e.height));
3539
+ if (track.keys.some((k) => k.rotation !== void 0)) e.rotation = num3(track, t, "rotation", angle(e));
3540
+ e.style.strokeWidth = Math.max(0, num3(track, t, "strokeWidth", e.style.strokeWidth));
3277
3541
  e.style.stroke = value(track, t, (k) => k.stroke, e.style.stroke, lerpColor);
3278
3542
  e.style.fill = value(track, t, (k) => k.fill, e.style.fill, lerpColor);
3279
3543
  return e;
@@ -3349,7 +3613,8 @@ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}
3349
3613
  }
3350
3614
  const flowing = (page.flows ?? []).length > 0;
3351
3615
  const measuring = flowing || page.elements.some((e) => e.gauge || (e.overrides ?? []).some((o) => mentionsMetric(o.when)));
3352
- const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring ? null : EMPTY_TIMELINE);
3616
+ const fills = values !== null && Object.keys(values).length > 0 && page.elements.some((e) => e.text?.includes("{") || (e.overrides ?? []).some((o) => o.text?.includes("{")));
3617
+ const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring && !fills ? null : EMPTY_TIMELINE);
3353
3618
  if (!timeline) return { frame: page, metrics: emptyMetrics() };
3354
3619
  t = clampTime(t);
3355
3620
  const tracks = trackMap(timeline);
@@ -3385,6 +3650,10 @@ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}
3385
3650
  const track = tracks.get(e.id);
3386
3651
  let out = track ? applyTrack(track, e, t) : e;
3387
3652
  for (const o of active.get(e.id) ?? []) out = applyOverride(o, out);
3653
+ if (fills && values && out.text) {
3654
+ const text = filling(out.text, values);
3655
+ if (text !== out.text) out = { ...out, text };
3656
+ }
3388
3657
  const list = active.get(e.id);
3389
3658
  if (list) {
3390
3659
  const caps = list.map((o) => o.capacity).filter((c) => c !== null && Number.isFinite(c));
@@ -3696,13 +3965,18 @@ function sceneNamed(name, markers) {
3696
3965
  const exact = markers.findIndex((m) => m.name === name);
3697
3966
  return exact >= 0 ? exact : markers.findIndex((m) => m.name.toLowerCase() === name.toLowerCase());
3698
3967
  }
3699
- function clickTargets(page, file) {
3968
+ function clickTargets(page, file, hrefs) {
3700
3969
  const markers = page.timeline?.markers ?? [];
3701
3970
  const out = /* @__PURE__ */ new Map();
3702
3971
  let pageOf = null;
3703
3972
  for (const e of page.elements) {
3704
3973
  const c = e.click;
3705
- const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {}, ...c.reset ? { reset: true } : {} } : null;
3974
+ const state = c && changesState(c) ? {
3975
+ ...c.set ? { set: c.set } : {},
3976
+ ...c.toggle ? { toggle: c.toggle } : {},
3977
+ ...c.reset ? { reset: true } : {},
3978
+ ...c.run ? { run: c.run } : {}
3979
+ } : null;
3706
3980
  if (c?.scene) {
3707
3981
  const i = sceneNamed(c.scene, markers);
3708
3982
  if (i >= 0) {
@@ -3727,11 +4001,19 @@ function clickTargets(page, file) {
3727
4001
  });
3728
4002
  }
3729
4003
  const target = pageOf.get(e.link);
3730
- if (target !== void 0 && file.pages[target] !== page) out.set(e.id, { page: target });
4004
+ if (target !== void 0 && file.pages[target] !== page) {
4005
+ out.set(e.id, { page: target });
4006
+ continue;
4007
+ }
3731
4008
  }
4009
+ const href = e.link && hrefs && Object.prototype.hasOwnProperty.call(hrefs, e.link) ? pageHref(hrefs[e.link]) : null;
4010
+ if (href) out.set(e.id, { href });
3732
4011
  }
3733
4012
  return out;
3734
4013
  }
4014
+ function pageHref(href) {
4015
+ return typeof href === "string" && /^[A-Za-z0-9._~%/-]+\.html(#[^\s:]*)?$/.test(href) && !href.startsWith("//") ? href : null;
4016
+ }
3735
4017
  function hoveredElement(e) {
3736
4018
  const h = e.hover;
3737
4019
  if (!h) return e;
@@ -4725,6 +5007,40 @@ function start() {
4725
5007
  return w;
4726
5008
  }
4727
5009
 
5010
+ // src/logic-loader.ts
5011
+ var scriptSrc = typeof document !== "undefined" ? document.currentScript?.src ?? null : null;
5012
+ var found = null;
5013
+ var present = () => globalThis.DottieLogic;
5014
+ function logicEngine() {
5015
+ found ?? (found = (async () => {
5016
+ if (!present()) {
5017
+ const here = import.meta.url;
5018
+ const base2 = here ?? scriptSrc;
5019
+ if (!base2) throw new Error("No dottie-logic.min.js on the page, and no address to find it from");
5020
+ const url = new URL("./dottie-logic.min.js", base2).href;
5021
+ if (here) await import(
5022
+ /* @vite-ignore */
5023
+ url
5024
+ );
5025
+ else await new Promise((resolve2, reject) => {
5026
+ const s = document.createElement("script");
5027
+ s.src = url;
5028
+ s.onload = () => resolve2();
5029
+ s.onerror = () => reject(new Error(`Couldn't load ${url}`));
5030
+ document.head.append(s);
5031
+ });
5032
+ }
5033
+ const engine = present();
5034
+ if (!engine) throw new Error("dottie-logic.min.js didn't load");
5035
+ await engine.loadLogic();
5036
+ return engine;
5037
+ })());
5038
+ found.catch(() => {
5039
+ found = null;
5040
+ });
5041
+ return found;
5042
+ }
5043
+
4728
5044
  // src/player.ts
4729
5045
  var WHITE = { r: 1, g: 1, b: 1, a: 1 };
4730
5046
  function pageIndex(file, spec) {
@@ -4755,7 +5071,8 @@ output { font-variant-numeric: tabular-nums; min-width: 4.5em; text-align: right
4755
5071
  .error { padding: 16px; font-family: ui-monospace, Menlo, monospace; white-space: pre-wrap; }
4756
5072
  ${CLICK_STYLE}`;
4757
5073
  var Playback = class {
4758
- /** `file` is the drawing `page` belongs to, so links to its other pages are clickable. */
5074
+ /** `file` is the drawing `page` belongs to, so links to its other pages are clickable; `hrefs`, the web pages that
5075
+ * show the elements links point at, for pages that aren't in it (clickTargets). */
4759
5076
  constructor(page, into, options = {}) {
4760
5077
  __publicField(this, "page");
4761
5078
  __publicField(this, "plan");
@@ -4797,7 +5114,7 @@ var Playback = class {
4797
5114
  this.page = options.clean ? cleanCopy(page) : page;
4798
5115
  addFonts(usedFonts(this.page));
4799
5116
  this.plan = plan(this.page);
4800
- this.clicks = clickTargets(page, options.file);
5117
+ this.clicks = clickTargets(page, options.file, options.hrefs);
4801
5118
  const table = symbolTable(this.page);
4802
5119
  for (const e of this.page.elements) {
4803
5120
  const s = e.kind === "symbol" && e.symbol !== null ? table.get(e.symbol) : void 0;
@@ -5066,6 +5383,14 @@ var DottiePlayer = class extends ElementBase {
5066
5383
  __publicField(this, "stillTime", null);
5067
5384
  __publicField(this, "visible", true);
5068
5385
  __publicField(this, "observer", null);
5386
+ /** The page last shown, so `page` fires only when it changes. */
5387
+ __publicField(this, "shownPage", null);
5388
+ __publicField(this, "onPopState", () => {
5389
+ if (this.file) this.mount();
5390
+ });
5391
+ /** Runs handler `handler` from the page's logic for a click on `id` (DottieDocument.clicked). Clicks take turns, so
5392
+ * each handler sees the state the one before left. */
5393
+ __publicField(this, "logicQueue", Promise.resolve());
5069
5394
  this.root = this.attachShadow({ mode: "open" });
5070
5395
  this.root.innerHTML = `<style>${STYLE}</style><div class="stage" part="stage" role="img"></div>
5071
5396
  <div class="bar" part="controls" hidden>
@@ -5111,15 +5436,21 @@ var DottiePlayer = class extends ElementBase {
5111
5436
  });
5112
5437
  this.observer.observe(this);
5113
5438
  }
5439
+ if (this.hasAttribute("history")) window.addEventListener("popstate", this.onPopState);
5114
5440
  void this.load();
5115
5441
  }
5116
5442
  disconnectedCallback() {
5117
5443
  cancelAnimationFrame(this.raf);
5118
5444
  this.raf = 0;
5119
5445
  this.observer?.disconnect();
5446
+ window.removeEventListener("popstate", this.onPopState);
5120
5447
  }
5121
5448
  attributeChangedCallback(name, old, value2) {
5122
5449
  if (old === value2 || !this.isConnected) return;
5450
+ if (name === "history") {
5451
+ if (value2 === null) window.removeEventListener("popstate", this.onPopState);
5452
+ else window.addEventListener("popstate", this.onPopState);
5453
+ }
5123
5454
  if (name === "src") void this.load();
5124
5455
  else if (name === "label") this.stage.setAttribute("aria-label", value2 ?? "");
5125
5456
  else if (this.file) this.mount();
@@ -5168,6 +5499,7 @@ var DottiePlayer = class extends ElementBase {
5168
5499
  }
5169
5500
  showFile(file) {
5170
5501
  this.file = file;
5502
+ this.shownPage = null;
5171
5503
  for (const w of file.warnings) console.warn(`dottie-player: ${w}`);
5172
5504
  this.mount(() => this.dispatchEvent(new CustomEvent("load", { detail: { file } })));
5173
5505
  }
@@ -5175,7 +5507,7 @@ var DottiePlayer = class extends ElementBase {
5175
5507
  * showing stays until it is ready; a newer mount, file or failed load in the meantime wins. */
5176
5508
  mount(mounted) {
5177
5509
  const file = this.file;
5178
- const page = file.pages[pageIndex(file, this.getAttribute("page"))];
5510
+ const page = file.pages[this.addressedPage(file) ?? pageIndex(file, this.getAttribute("page"))];
5179
5511
  const clean = this.hasAttribute("clean");
5180
5512
  const token = ++this.mountToken;
5181
5513
  const shown = clean ? cleanCopy(page) : page;
@@ -5190,10 +5522,41 @@ var DottiePlayer = class extends ElementBase {
5190
5522
  mounted?.();
5191
5523
  });
5192
5524
  }
5525
+ /** With `history`, the page the address names after its `#` (by name, any case), if it names one. */
5526
+ addressedPage(file) {
5527
+ if (!this.hasAttribute("history") || typeof location === "undefined" || location.hash.length < 2) return null;
5528
+ let name;
5529
+ try {
5530
+ name = decodeURIComponent(location.hash.slice(1));
5531
+ } catch {
5532
+ return null;
5533
+ }
5534
+ const i = file.pages.findIndex((p) => p.name.toLowerCase() === name.toLowerCase());
5535
+ return i >= 0 ? i : null;
5536
+ }
5537
+ /** The `links` attribute read: element id to web page, the pages that can't be named left out. */
5538
+ hrefs() {
5539
+ const raw = this.getAttribute("links");
5540
+ if (!raw) return void 0;
5541
+ let v;
5542
+ try {
5543
+ v = JSON.parse(raw);
5544
+ } catch {
5545
+ console.warn("dottie-player: links isn't JSON");
5546
+ return void 0;
5547
+ }
5548
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return void 0;
5549
+ return v;
5550
+ }
5193
5551
  mountPage(file, page, clean) {
5194
- const p = new Playback(page, this.stage, { clean, file });
5552
+ const p = new Playback(page, this.stage, { clean, file, hrefs: this.hrefs() });
5195
5553
  p.still = this.hasAttribute("at");
5196
5554
  this.playback = p;
5555
+ if (this.shownPage !== page) {
5556
+ const first = this.shownPage === null;
5557
+ this.shownPage = page;
5558
+ queueMicrotask(() => this.dispatchEvent(new CustomEvent("page", { detail: { index: file.pages.indexOf(page), name: page.name, first } })));
5559
+ }
5197
5560
  this.style.setProperty("--dottie-page", color(p.background));
5198
5561
  this.style.setProperty("--dottie-ink", color(p.ink));
5199
5562
  this.style.colorScheme = luminance(p.background) < 0.5 ? "dark" : "light";
@@ -5292,7 +5655,9 @@ var DottiePlayer = class extends ElementBase {
5292
5655
  this.scheduleReplay();
5293
5656
  }
5294
5657
  if (!click) return;
5295
- if (click.set || click.toggle || click.reset) {
5658
+ if (click.run && p.page.logic && this.hasAttribute("allow-logic")) {
5659
+ this.runLogic(click.run, id, click.reset === true);
5660
+ } else if (click.set || click.toggle || click.reset) {
5296
5661
  this.setState(applyClick(
5297
5662
  { scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null, reset: click.reset ?? null },
5298
5663
  p.state,
@@ -5303,7 +5668,15 @@ var DottiePlayer = class extends ElementBase {
5303
5668
  if ("url" in click) {
5304
5669
  if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
5305
5670
  } else if ("page" in click) {
5306
- this.setAttribute("page", String(click.page + 1));
5671
+ const target = this.file?.pages[click.page];
5672
+ if (this.hasAttribute("history") && target) {
5673
+ history.pushState(null, "", "#" + encodeURIComponent(target.name));
5674
+ this.mount();
5675
+ } else {
5676
+ this.setAttribute("page", String(click.page + 1));
5677
+ }
5678
+ } else if ("href" in click) {
5679
+ location.assign(click.href);
5307
5680
  } else if (click.scene >= 0 && click.scene < p.scenes.length) {
5308
5681
  if (reduced || this.hasAttribute("at") || p.frameCount < 2) {
5309
5682
  this.setPlaying(false);
@@ -5314,6 +5687,35 @@ var DottiePlayer = class extends ElementBase {
5314
5687
  }
5315
5688
  }
5316
5689
  }
5690
+ runLogic(handler, id, reset) {
5691
+ const p = this.playback;
5692
+ if (!p) return;
5693
+ const e = p.page.elements.find((x) => x.id === id);
5694
+ const s = p.sceneAt(p.frame);
5695
+ const event = {
5696
+ id,
5697
+ anchor: e?.anchor ?? null,
5698
+ text: e?.text?.split("\n")[0] ?? null,
5699
+ time: this.currentTime,
5700
+ scene: s < 0 ? null : p.sceneNames[s]
5701
+ };
5702
+ this.logicQueue = this.logicQueue.then(async () => {
5703
+ const engine = await logicEngine();
5704
+ const now = this.playback;
5705
+ if (!now || now !== p || !now.page.logic) return;
5706
+ let start2 = now.state;
5707
+ if (reset) {
5708
+ start2 = {};
5709
+ for (const k of Object.keys(now.state)) start2[k] = false;
5710
+ Object.assign(start2, now.page.variables ?? {});
5711
+ }
5712
+ const r = engine.runLogic(now.page.logic, handler, start2, event);
5713
+ if (r.ok) this.setState({ ...start2, ...r.set });
5714
+ else this.dispatchEvent(new CustomEvent("logicerror", { detail: { element: id, handler, error: r.error } }));
5715
+ }).catch((err) => {
5716
+ this.dispatchEvent(new CustomEvent("logicerror", { detail: { element: id, handler, error: err.message } }));
5717
+ });
5718
+ }
5317
5719
  /** The page's variables now. */
5318
5720
  get variables() {
5319
5721
  return { ...this.playback?.state ?? {} };
@@ -5420,7 +5822,7 @@ var DottiePlayer = class extends ElementBase {
5420
5822
  if (handled) e.preventDefault();
5421
5823
  }
5422
5824
  };
5423
- __publicField(DottiePlayer, "observedAttributes", ["src", "page", "at", "clean", "no-controls", "label"]);
5825
+ __publicField(DottiePlayer, "observedAttributes", ["src", "page", "at", "clean", "no-controls", "label", "allow-logic", "history", "links"]);
5424
5826
  function clickable(target) {
5425
5827
  const el = target;
5426
5828
  return el && typeof el.closest === "function" ? el.closest(".dottie-a") : null;
@@ -5525,7 +5927,7 @@ function render(source, options = {}) {
5525
5927
  function isFile(v) {
5526
5928
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
5527
5929
  }
5528
- var VERSION = "0.10.0";
5930
+ var VERSION = "0.11.1";
5529
5931
  export {
5530
5932
  BOIL_DRAWINGS,
5531
5933
  BUNDLED_FONTS,