@phin-tech/dottie 0.10.0 → 0.11.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/PARITY.md CHANGED
@@ -24,6 +24,8 @@ fixture, so the gap is visible rather than silent.
24
24
  | Ports and edge-following, rotated shapes, free ends | Supported | [edge/connectors](../fixtures/edge/connectors.dottie) | |
25
25
  | Connectors and labels on outlines (ADR 0001) | Supported | [edge/extensions](../fixtures/edge/extensions.dottie) | Edge-following meets a `.path`'s actual outline, not its box (ray against 16-step flattened curves). Endpoints pinned with `portIndex` attach to the element's `ports`. A path's label goes in its `labelRect`. |
26
26
  | Extension shapes as stored (ADR 0001) | Supported | [edge/extensions](../fixtures/edge/extensions.dottie) | Drawn from the stored path with no extension installed. The `extension` field is kept as metadata and nothing runs. Parameters, commands and handles are Dottie-only. |
27
+ | Click logic (`logic`, a click's `run`) | Supported | [edge/logic](../fixtures/edge/logic.dottie) | One engine for both: `logic.ts` (QuickJS in WebAssembly), run by `LogicEngine.swift` in JavaScriptCore. Runs only with `allow-logic` (ADR 0007). |
28
+ | Number variables, expressions in conditions and clicks, `{value}` in labels | Supported | [edge/counters](../fixtures/edge/counters.dottie), [expressions.json](../fixtures/expressions.json) | One grammar in `Expression.swift` and `expression.ts` (ADR 0007). |
27
29
  | Containers and swimlanes | Supported | [edge/containers](../fixtures/edge/containers.dottie) | Drawn from the stored frame and title band (`labelRect`); the app keeps them fitted (ADR 0006). |
28
30
  | Themes of a page's own, and palette colors by name | Supported | [edge/palette](../fixtures/edge/palette.dottie) | Names are resolved when a file is read, by the same rules in `Theme.swift` and `theme.ts` (ADR 0005). |
29
31
  | Elbows that go round shapes (`avoid`) | Supported | [edge/elbow-around](../fixtures/edge/elbow-around.dottie) | The same grid router, point for point (`ElbowRouter.swift`, `elbow.ts`). |
package/README.md CHANGED
@@ -56,6 +56,21 @@ when it is showing. The classic script starts the worker from its own source (a
56
56
  loads `dottie-player.min.js` from beside itself, so host the two together. Where a worker can't start, as under a
57
57
  `worker-src` policy that refuses it, the player works it out in place as before. Small drawings always do.
58
58
 
59
+ **Click logic.** Variables can be numbers, and conditions and clicks are small expressions (`count >= 3`,
60
+ `"count + 1"`, `{count}` in a label). For more, a page can carry `logic`: JavaScript handlers a click `run`s, each
61
+ getting the variables and the click and returning new values. The player runs them only with `allow-logic`:
62
+
63
+ ```html
64
+ <dottie-player src="game.dottie" allow-logic></dottie-player>
65
+ ```
66
+
67
+ Each click's handler runs in a fresh QuickJS sandbox compiled to WebAssembly, never in your page's JavaScript: it has
68
+ no `window`, `document`, network, timers, `Date` or `Math.random`, and a time and memory limit, so the same click on
69
+ the same state gives the same result everywhere. A handler that fails leaves the state as it was and fires
70
+ `logicerror` (with `{element, handler, error}`). Without `allow-logic`, a click does its own `set` and `toggle`
71
+ instead. The engine (`dottie-logic.min.js`, about 850 KB) is loaded from beside the player only when a page with
72
+ logic is clicked, so host it with the player; `dottie-compile --allow-logic` puts it in the compiled script instead.
73
+
59
74
  **Fonts.** Text in Dottie's handwriting fonts (Caveat, Patrick Hand, Architects Daughter; SIL Open Font License)
60
75
  is drawn in them: the player loads the ones a drawing uses from `fonts/` beside its own script, as `dist/fonts` on
61
76
  npm and the CDNs. If you host the player yourself, copy that folder with it, or call `Dottie.setFontBase(url)`
package/dist/compile.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.10.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.11.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -109,12 +109,12 @@ function resolvingPalette(page) {
109
109
  }
110
110
 
111
111
  // src/model.ts
112
- var SUPPORTED_VERSION = 2;
112
+ var SUPPORTED_VERSION = 3;
113
113
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
114
114
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
115
115
  var KNOWN_FIELDS = {
116
116
  file: ["version", "pages", "images"],
117
- page: ["id", "name", "layers", "elements", "theme", "customTheme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
117
+ page: ["id", "name", "layers", "elements", "theme", "customTheme", "logic", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
118
118
  flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
119
119
  flowOverride: ["when", "volume"],
120
120
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
@@ -183,7 +183,7 @@ var KNOWN_FIELDS = {
183
183
  repeat: ["start", "end", "count"],
184
184
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
185
185
  marker: ["t", "name", "loop"],
186
- click: ["scene", "url", "set", "toggle", "reset"],
186
+ click: ["scene", "url", "set", "toggle", "reset", "run"],
187
187
  hover: ["stroke", "fill", "opacity"],
188
188
  override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
189
189
  };
@@ -430,7 +430,7 @@ var Reader = class {
430
430
  variables(v) {
431
431
  if (!isObj(v)) return null;
432
432
  const out = {};
433
- for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
433
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean" || typeof b === "number" && Number.isFinite(b)) out[k] = b;
434
434
  return Object.keys(out).length > 0 ? out : null;
435
435
  }
436
436
  click(v) {
@@ -442,12 +442,22 @@ var Reader = class {
442
442
  let set = null;
443
443
  if (isObj(v.set)) {
444
444
  set = {};
445
- for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
445
+ for (const [k, b] of Object.entries(v.set)) {
446
+ if (isName(k) && (typeof b === "boolean" || typeof b === "string" || typeof b === "number" && Number.isFinite(b))) set[k] = b;
447
+ }
446
448
  if (Object.keys(set).length === 0) set = null;
447
449
  }
448
450
  const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
449
451
  const toggle = raw ? raw.filter(isName) : null;
450
- return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
452
+ const run = str(v.run);
453
+ return {
454
+ scene: str(v.scene) ?? null,
455
+ url: safe,
456
+ set,
457
+ toggle: toggle && toggle.length > 0 ? toggle : null,
458
+ reset: v.reset === true ? true : null,
459
+ run: run !== void 0 && isName(run) ? run : null
460
+ };
451
461
  }
452
462
  playback(v) {
453
463
  const s = str(v);
@@ -610,6 +620,7 @@ var Reader = class {
610
620
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
611
621
  theme: str(v.theme) ?? null,
612
622
  customTheme: this.customTheme(v.customTheme),
623
+ logic: typeof v.logic === "string" && v.logic.length > 0 ? v.logic : null,
613
624
  timeline: this.timeline(v.timeline),
614
625
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
615
626
  variables: this.variables(v.variables),
@@ -751,6 +762,7 @@ function playerAttributes(options) {
751
762
  if (options.noAutoplay) out["no-autoplay"] = "";
752
763
  if (options.noLoop) out["no-loop"] = "";
753
764
  if (options.label !== void 0) out.label = options.label;
765
+ if (options.logicEngine) out["allow-logic"] = "";
754
766
  return out;
755
767
  }
756
768
  function compile(source, runtime, options = {}) {
@@ -783,10 +795,11 @@ function compile(source, runtime, options = {}) {
783
795
  }
784
796
  const fill = {
785
797
  DOTTIE_DATA: () => scriptJSON(data),
786
- DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(options)),
798
+ DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(file.pages[i].logic ? options : { ...options, logicEngine: void 0 })),
787
799
  DOTTIE_FONTS: () => scriptJSON(fonts)
788
800
  };
789
- return runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
801
+ const engine = options.logicEngine && file.pages[i].logic ? options.logicEngine.trimEnd() + "\n" : "";
802
+ return engine + runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
790
803
  }
791
804
  export {
792
805
  LOADER,
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /*! dottie.js 0.10.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
+ /*! dottie.js 0.11.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
3
3
  var __defProp = Object.defineProperty;
4
4
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
5
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -117,12 +117,12 @@ function resolvingPalette(page) {
117
117
  }
118
118
 
119
119
  // src/model.ts
120
- var SUPPORTED_VERSION = 2;
120
+ var SUPPORTED_VERSION = 3;
121
121
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
122
122
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
123
123
  var KNOWN_FIELDS = {
124
124
  file: ["version", "pages", "images"],
125
- page: ["id", "name", "layers", "elements", "theme", "customTheme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
125
+ page: ["id", "name", "layers", "elements", "theme", "customTheme", "logic", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
126
126
  flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
127
127
  flowOverride: ["when", "volume"],
128
128
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
@@ -191,7 +191,7 @@ var KNOWN_FIELDS = {
191
191
  repeat: ["start", "end", "count"],
192
192
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
193
193
  marker: ["t", "name", "loop"],
194
- click: ["scene", "url", "set", "toggle", "reset"],
194
+ click: ["scene", "url", "set", "toggle", "reset", "run"],
195
195
  hover: ["stroke", "fill", "opacity"],
196
196
  override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
197
197
  };
@@ -438,7 +438,7 @@ var Reader = class {
438
438
  variables(v) {
439
439
  if (!isObj(v)) return null;
440
440
  const out = {};
441
- for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
441
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean" || typeof b === "number" && Number.isFinite(b)) out[k] = b;
442
442
  return Object.keys(out).length > 0 ? out : null;
443
443
  }
444
444
  click(v) {
@@ -450,12 +450,22 @@ var Reader = class {
450
450
  let set = null;
451
451
  if (isObj(v.set)) {
452
452
  set = {};
453
- for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
453
+ for (const [k, b] of Object.entries(v.set)) {
454
+ if (isName(k) && (typeof b === "boolean" || typeof b === "string" || typeof b === "number" && Number.isFinite(b))) set[k] = b;
455
+ }
454
456
  if (Object.keys(set).length === 0) set = null;
455
457
  }
456
458
  const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
457
459
  const toggle = raw ? raw.filter(isName) : null;
458
- return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
460
+ const run = str(v.run);
461
+ return {
462
+ scene: str(v.scene) ?? null,
463
+ url: safe,
464
+ set,
465
+ toggle: toggle && toggle.length > 0 ? toggle : null,
466
+ reset: v.reset === true ? true : null,
467
+ run: run !== void 0 && isName(run) ? run : null
468
+ };
459
469
  }
460
470
  playback(v) {
461
471
  const s = str(v);
@@ -618,6 +628,7 @@ var Reader = class {
618
628
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
619
629
  theme: str(v.theme) ?? null,
620
630
  customTheme: this.customTheme(v.customTheme),
631
+ logic: typeof v.logic === "string" && v.logic.length > 0 ? v.logic : null,
621
632
  timeline: this.timeline(v.timeline),
622
633
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
623
634
  variables: this.variables(v.variables),
@@ -759,6 +770,7 @@ function playerAttributes(options2) {
759
770
  if (options2.noAutoplay) out["no-autoplay"] = "";
760
771
  if (options2.noLoop) out["no-loop"] = "";
761
772
  if (options2.label !== void 0) out.label = options2.label;
773
+ if (options2.logicEngine) out["allow-logic"] = "";
762
774
  return out;
763
775
  }
764
776
  function compile(source2, runtime, options2 = {}) {
@@ -791,14 +803,15 @@ function compile(source2, runtime, options2 = {}) {
791
803
  }
792
804
  const fill = {
793
805
  DOTTIE_DATA: () => scriptJSON(data),
794
- DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(options2)),
806
+ DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(file.pages[i].logic ? options2 : { ...options2, logicEngine: void 0 })),
795
807
  DOTTIE_FONTS: () => scriptJSON(fonts)
796
808
  };
797
- return runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
809
+ const engine = options2.logicEngine && file.pages[i].logic ? options2.logicEngine.trimEnd() + "\n" : "";
810
+ return engine + runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
798
811
  }
799
812
 
800
813
  // src/index.ts
801
- var VERSION = "0.10.0";
814
+ var VERSION = "0.11.0";
802
815
 
803
816
  // src/cli.ts
804
817
  var usage = `dottie-compile ${VERSION}: a Dottie drawing as one script that plays it on any page.
@@ -815,6 +828,7 @@ OPTIONS
815
828
  --at <seconds> Show that moment as a still
816
829
  --clean Draw precisely instead of sketchy
817
830
  --no-controls, --no-autoplay, --no-loop
831
+ --allow-logic let the page's click logic run (ADR 0007); it carries the engine (about 850 KB)
818
832
  --label <text> The player's accessible name
819
833
  `;
820
834
  function fail(message, code = 2) {
@@ -824,6 +838,7 @@ function fail(message, code = 2) {
824
838
  }
825
839
  var args = process.argv.slice(2);
826
840
  var options = {};
841
+ var allowLogic = false;
827
842
  var input;
828
843
  var output;
829
844
  for (let i = 0; i < args.length; i++) {
@@ -865,6 +880,9 @@ for (let i = 0; i < args.length; i++) {
865
880
  case "--no-loop":
866
881
  options.noLoop = true;
867
882
  break;
883
+ case "--allow-logic":
884
+ allowLogic = true;
885
+ break;
868
886
  case "--label":
869
887
  options.label = value();
870
888
  break;
@@ -895,7 +913,8 @@ try {
895
913
  return void 0;
896
914
  }
897
915
  };
898
- const js = compile(source, runtime, { ...options, fontData });
916
+ const logicEngine = allowLogic ? readFileSync(new URL("./dottie-logic.min.js", import.meta.url), "utf8") : void 0;
917
+ const js = compile(source, runtime, { ...options, fontData, logicEngine });
899
918
  if (output === "-") process.stdout.write(js);
900
919
  else {
901
920
  const target = output ?? input.replace(/\.dottie$/i, "") + ".js";