@wizzlethorpe/vaults 0.7.0 → 0.7.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.
Files changed (88) hide show
  1. package/README.md +8 -7
  2. package/dist/build.js +425 -123
  3. package/dist/build.js.map +1 -1
  4. package/dist/commands/build.js +2 -1
  5. package/dist/commands/build.js.map +1 -1
  6. package/dist/commands/password.js +1 -1
  7. package/dist/commands/password.js.map +1 -1
  8. package/dist/commands/patreon.js +24 -20
  9. package/dist/commands/patreon.js.map +1 -1
  10. package/dist/commands/preview.js +5 -4
  11. package/dist/commands/preview.js.map +1 -1
  12. package/dist/commands/push.js +7 -8
  13. package/dist/commands/push.js.map +1 -1
  14. package/dist/config.js +21 -10
  15. package/dist/config.js.map +1 -1
  16. package/dist/escape.js +29 -0
  17. package/dist/escape.js.map +1 -0
  18. package/dist/favicon.js +3 -36
  19. package/dist/favicon.js.map +1 -1
  20. package/dist/foundry-importer.js +61 -0
  21. package/dist/foundry-importer.js.map +1 -0
  22. package/dist/images.js +0 -30
  23. package/dist/images.js.map +1 -1
  24. package/dist/index.js +3 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/migrate/0.6-legacy-auth-settings.js +3 -5
  27. package/dist/migrate/0.6-legacy-auth-settings.js.map +1 -1
  28. package/dist/migrate/0.7-vaults-dir.js +4 -0
  29. package/dist/migrate/0.7-vaults-dir.js.map +1 -1
  30. package/dist/migrate/registry.js +1 -7
  31. package/dist/migrate/registry.js.map +1 -1
  32. package/dist/paths.js +21 -6
  33. package/dist/paths.js.map +1 -1
  34. package/dist/render/auth-template.js +21 -142
  35. package/dist/render/auth-template.js.map +1 -1
  36. package/dist/render/bases.js +56 -44
  37. package/dist/render/bases.js.map +1 -1
  38. package/dist/render/callouts.js +29 -10
  39. package/dist/render/callouts.js.map +1 -1
  40. package/dist/render/embed.js +124 -26
  41. package/dist/render/embed.js.map +1 -1
  42. package/dist/render/extensions.js +68 -0
  43. package/dist/render/extensions.js.map +1 -0
  44. package/dist/render/external-links.js +32 -0
  45. package/dist/render/external-links.js.map +1 -0
  46. package/dist/render/frontmatter.js +17 -0
  47. package/dist/render/frontmatter.js.map +1 -0
  48. package/dist/render/handlers/assets.js +48 -15
  49. package/dist/render/handlers/assets.js.map +1 -1
  50. package/dist/render/handlers/builtin/battlemap.js +199 -0
  51. package/dist/render/handlers/builtin/battlemap.js.map +1 -0
  52. package/dist/render/handlers/builtin/dice.js +1 -1
  53. package/dist/render/handlers/builtin/dice.js.map +1 -1
  54. package/dist/render/handlers/builtin/fm-code.js +50 -0
  55. package/dist/render/handlers/builtin/fm-code.js.map +1 -0
  56. package/dist/render/handlers/builtin/fm.js +6 -12
  57. package/dist/render/handlers/builtin/fm.js.map +1 -1
  58. package/dist/render/handlers/builtin/index.js +5 -1
  59. package/dist/render/handlers/builtin/index.js.map +1 -1
  60. package/dist/render/handlers/builtin/inline-format.js +26 -0
  61. package/dist/render/handlers/builtin/inline-format.js.map +1 -0
  62. package/dist/render/handlers/builtin/statblock.js +158 -18
  63. package/dist/render/handlers/builtin/statblock.js.map +1 -1
  64. package/dist/render/handlers/dispatch.js +15 -20
  65. package/dist/render/handlers/dispatch.js.map +1 -1
  66. package/dist/render/handlers/types.js +41 -21
  67. package/dist/render/handlers/types.js.map +1 -1
  68. package/dist/render/image-srcs.js +42 -0
  69. package/dist/render/image-srcs.js.map +1 -0
  70. package/dist/render/layout.js +60 -9
  71. package/dist/render/layout.js.map +1 -1
  72. package/dist/render/pipeline.js +23 -10
  73. package/dist/render/pipeline.js.map +1 -1
  74. package/dist/render/preview.js +53 -18
  75. package/dist/render/preview.js.map +1 -1
  76. package/dist/render/slug.js +5 -0
  77. package/dist/render/slug.js.map +1 -1
  78. package/dist/render/styles.js +94 -14
  79. package/dist/render/styles.js.map +1 -1
  80. package/dist/render/wikilink.js +15 -4
  81. package/dist/render/wikilink.js.map +1 -1
  82. package/dist/scan.js +1 -1
  83. package/dist/scan.js.map +1 -1
  84. package/dist/settings.js +16 -1
  85. package/dist/settings.js.map +1 -1
  86. package/dist/version.js +36 -0
  87. package/dist/version.js.map +1 -0
  88. package/package.json +2 -1
@@ -0,0 +1,199 @@
1
+ // Built-in `battlemap` code-block handler.
2
+ //
3
+ // Renders a layered, multi-level battle map with controls to switch level,
4
+ // toggle a grid overlay, and download the current level as a PNG. Syntax:
5
+ //
6
+ // ```battlemap
7
+ // grid: 140 # px per grid cell at the image's native size (optional)
8
+ // default_level: 1 # 0-based index of the level shown first (optional)
9
+ // name: Wizard Prison # download-filename prefix (optional)
10
+ // levels:
11
+ // - name: Rock
12
+ // layers: # composited bottom-to-top
13
+ // - "attachments/foundry/wizard-prison/...-Rock-...webp"
14
+ // - name: First Floor
15
+ // layers:
16
+ // - "attachments/foundry/wizard-prison/...-Rock-...webp"
17
+ // - "attachments/foundry/wizard-prison/...-First Floor-...webp"
18
+ // ```
19
+ //
20
+ // The browser runtime (BATTLEMAP_RUNTIME below) ships as a built-in asset
21
+ // concatenated into _handlers.js; styles go into _handlers.css.
22
+ //
23
+ // Layer paths are vault-relative and resolve to the absolute served URL. They
24
+ // must also be staged into the deploy — true when the page already references
25
+ // them (e.g. a Scene's foundry.data_json), which is the common case.
26
+ import yaml from "js-yaml";
27
+ import { htmlEscape } from "../../../escape.js";
28
+ import { registerBuiltinAssets } from "../assets.js";
29
+ /** Vault-relative path -> absolute, percent-encoded served URL. */
30
+ function servedSrc(path) {
31
+ return "/" + path.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
32
+ }
33
+ function errorBox(message) {
34
+ return { html: `<div class="vaults-bm-error">${htmlEscape(message)}</div>` };
35
+ }
36
+ export const battlemapHandler = {
37
+ codeBlock: "battlemap",
38
+ render(content) {
39
+ let spec;
40
+ try {
41
+ spec = (yaml.load(content) ?? {});
42
+ }
43
+ catch {
44
+ return errorBox("battlemap: could not parse YAML");
45
+ }
46
+ const levels = (Array.isArray(spec.levels) ? spec.levels : [])
47
+ .map((lv) => ({
48
+ name: typeof lv?.name === "string" ? lv.name : "",
49
+ layers: Array.isArray(lv?.layers)
50
+ ? lv.layers.filter((s) => typeof s === "string" && s.length > 0)
51
+ : [],
52
+ }))
53
+ .filter((lv) => lv.layers.length > 0);
54
+ if (levels.length === 0)
55
+ return errorBox("battlemap: no levels with layers");
56
+ const grid = Number(spec.grid) > 0 ? Number(spec.grid) : 0;
57
+ let active = Number.isInteger(Number(spec.default_level)) ? Number(spec.default_level) : 0;
58
+ if (active < 0 || active >= levels.length)
59
+ active = 0;
60
+ const mapName = typeof spec.name === "string" ? spec.name.trim() : "";
61
+ const levelBtns = levels
62
+ .map((lv, i) => `<button type="button" class="vaults-bm-level" role="tab" data-level="${i}"`
63
+ + ` aria-selected="${i === active ? "true" : "false"}">${htmlEscape(lv.name || `Level ${i + 1}`)}</button>`)
64
+ .join("");
65
+ const tools = (grid ? `<button type="button" class="vaults-bm-grid" aria-pressed="false" title="Toggle grid">Grid</button>` : "")
66
+ + `<button type="button" class="vaults-bm-download" title="Download this level as an image">Download</button>`;
67
+ const panes = levels
68
+ .map((lv, i) => {
69
+ const imgs = lv.layers
70
+ .map((p, j) => `<img src="${servedSrc(p)}" alt="${htmlEscape(lv.name)} layer ${j + 1}" loading="lazy">`)
71
+ .join("");
72
+ return `<div class="vaults-bm-pane${i === active ? " is-active" : ""}" data-level="${i}"`
73
+ + ` data-name="${htmlEscape(lv.name)}">${imgs}</div>`;
74
+ })
75
+ .join("");
76
+ const overlay = grid ? `<div class="vaults-bm-grid-overlay" aria-hidden="true"></div>` : "";
77
+ const html = `<div class="vaults-battlemap"${grid ? ` data-grid="${grid}"` : ""}`
78
+ + `${mapName ? ` data-name="${htmlEscape(mapName)}"` : ""}>`
79
+ + `<div class="vaults-bm-bar">`
80
+ + `<div class="vaults-bm-levels" role="tablist">${levelBtns}</div>`
81
+ + `<div class="vaults-bm-tools">${tools}</div>`
82
+ + `</div>`
83
+ + `<div class="vaults-bm-stage">${panes}${overlay}</div>`
84
+ + `</div>`;
85
+ return { html };
86
+ },
87
+ };
88
+ // ── Browser runtime ───────────────────────────────────────────────────────
89
+ const BATTLEMAP_RUNTIME = `
90
+ (function () {
91
+ function initMap(root) {
92
+ var grid = parseFloat(root.getAttribute('data-grid')) || 0;
93
+ var overlay = root.querySelector('.vaults-bm-grid-overlay');
94
+ var panes = root.querySelectorAll('.vaults-bm-pane');
95
+ var btns = root.querySelectorAll('.vaults-bm-level');
96
+
97
+ function active() { return root.querySelector('.vaults-bm-pane.is-active'); }
98
+
99
+ function sizeGrid() {
100
+ if (!grid || !overlay) return;
101
+ var pane = active();
102
+ var img = pane && pane.querySelector('img');
103
+ if (!img || !img.naturalWidth) return;
104
+ overlay.style.backgroundSize =
105
+ (grid / img.naturalWidth * 100) + '% ' + (grid / img.naturalHeight * 100) + '%';
106
+ }
107
+
108
+ function setLevel(i) {
109
+ panes.forEach(function (p) { p.classList.toggle('is-active', p.getAttribute('data-level') === i); });
110
+ btns.forEach(function (b) { b.setAttribute('aria-selected', b.getAttribute('data-level') === i ? 'true' : 'false'); });
111
+ sizeGrid();
112
+ }
113
+
114
+ btns.forEach(function (b) {
115
+ b.addEventListener('click', function () { setLevel(b.getAttribute('data-level')); });
116
+ });
117
+
118
+ var gridBtn = root.querySelector('.vaults-bm-grid');
119
+ if (gridBtn) gridBtn.addEventListener('click', function () {
120
+ var on = root.classList.toggle('show-grid');
121
+ gridBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
122
+ if (on) sizeGrid();
123
+ });
124
+
125
+ var dlBtn = root.querySelector('.vaults-bm-download');
126
+ if (dlBtn) dlBtn.addEventListener('click', function () { download(root); });
127
+
128
+ root.querySelectorAll('img').forEach(function (img) {
129
+ if (img.complete) sizeGrid(); else img.addEventListener('load', sizeGrid);
130
+ });
131
+ window.addEventListener('resize', sizeGrid);
132
+ }
133
+
134
+ function download(root) {
135
+ var pane = root.querySelector('.vaults-bm-pane.is-active');
136
+ var imgs = pane ? pane.querySelectorAll('img') : [];
137
+ if (!imgs.length || !imgs[0].naturalWidth) return;
138
+ var canvas = document.createElement('canvas');
139
+ canvas.width = imgs[0].naturalWidth;
140
+ canvas.height = imgs[0].naturalHeight;
141
+ var ctx = canvas.getContext('2d');
142
+ imgs.forEach(function (img) { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); });
143
+
144
+ // Bake in the grid only when it's currently shown, matching the overlay.
145
+ var grid = parseFloat(root.getAttribute('data-grid')) || 0;
146
+ if (grid > 0 && root.classList.contains('show-grid')) {
147
+ // Scale the line to the on-screen 1px so the baked grid looks the same.
148
+ var scale = imgs[0].width ? imgs[0].naturalWidth / imgs[0].width : 1;
149
+ ctx.strokeStyle = 'rgba(0,0,0,0.5)';
150
+ ctx.lineWidth = Math.max(1, scale);
151
+ ctx.beginPath();
152
+ for (var gx = 0; gx <= canvas.width; gx += grid) { ctx.moveTo(gx, 0); ctx.lineTo(gx, canvas.height); }
153
+ for (var gy = 0; gy <= canvas.height; gy += grid) { ctx.moveTo(0, gy); ctx.lineTo(canvas.width, gy); }
154
+ ctx.stroke();
155
+ }
156
+
157
+ var prefix = root.getAttribute('data-name');
158
+ var name = (prefix ? prefix + ' - ' : '') + (pane.getAttribute('data-name') || 'map');
159
+ try {
160
+ canvas.toBlob(function (blob) {
161
+ if (!blob) return;
162
+ var url = URL.createObjectURL(blob);
163
+ var a = document.createElement('a');
164
+ a.href = url; a.download = name + '.png';
165
+ document.body.appendChild(a); a.click(); a.remove();
166
+ setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
167
+ }, 'image/png');
168
+ } catch (e) {
169
+ // Tainted canvas (cross-origin image): fall back to opening the base layer.
170
+ window.open(imgs[0].src, '_blank');
171
+ }
172
+ }
173
+
174
+ function initAll() { document.querySelectorAll('.vaults-battlemap').forEach(initMap); }
175
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initAll);
176
+ else initAll();
177
+ })();
178
+ `;
179
+ const BATTLEMAP_STYLES = `
180
+ .vaults-battlemap { margin: 1rem 0; }
181
+ .vaults-bm-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; justify-content: space-between; margin-bottom: .5rem; }
182
+ .vaults-bm-levels, .vaults-bm-tools { display: flex; flex-wrap: wrap; gap: .25rem; }
183
+ .vaults-battlemap button { font: inherit; font-size: .85rem; line-height: 1.2; padding: .3rem .7rem; border: 1px solid var(--rule, #ccc); border-radius: 4px; background: var(--bg, #fff); color: var(--fg, #222); cursor: pointer; }
184
+ .vaults-battlemap button:hover { border-color: var(--accent, #888); }
185
+ .vaults-bm-level[aria-selected="true"], .vaults-bm-grid[aria-pressed="true"] { background: var(--accent, #333); color: var(--accent-fg, #fff); border-color: var(--accent, #333); }
186
+ .vaults-bm-stage { position: relative; line-height: 0; border: 1px solid var(--rule, #ccc); border-radius: 4px; overflow: hidden; background: #15151a; }
187
+ .vaults-bm-pane { display: none; position: relative; }
188
+ .vaults-bm-pane.is-active { display: block; }
189
+ .vaults-bm-pane img { display: block; width: 100%; height: auto; -webkit-user-drag: none; user-select: none; }
190
+ .vaults-bm-pane img:not(:first-child) { position: absolute; inset: 0; }
191
+ .vaults-bm-grid-overlay { position: absolute; inset: 0; display: none; pointer-events: none; background-image: linear-gradient(to right, rgba(0,0,0,.5) 0 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,.5) 0 1px, transparent 1px); }
192
+ .vaults-battlemap.show-grid .vaults-bm-grid-overlay { display: block; }
193
+ .vaults-bm-error { padding: .5rem .75rem; border: 1px solid #b94a3a; border-radius: 4px; color: #b94a3a; font-size: .85rem; }
194
+ `;
195
+ registerBuiltinAssets(battlemapHandler, {
196
+ scripts: [{ source: "builtin/battlemap.runtime.js", content: BATTLEMAP_RUNTIME }],
197
+ styles: [{ source: "builtin/battlemap.css", content: BATTLEMAP_STYLES }],
198
+ });
199
+ //# sourceMappingURL=battlemap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"battlemap.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/battlemap.ts"],"names":[],"mappings":"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"}
@@ -11,7 +11,7 @@
11
11
  //
12
12
  // Subset of Dice Roller's syntax: basic XdY +/- Z arithmetic, optional X.
13
13
  // More complex modifiers (kh, kl, explode, etc.) are not supported.
14
- import { htmlEscape } from "../types.js";
14
+ import { htmlEscape } from "../../../escape.js";
15
15
  import { registerBuiltinAssets } from "../assets.js";
16
16
  // Formula validation: optional X, then 'd', then Y, then optional ± integer.
17
17
  // Only used at build time to decide whether render() emits a clickable
@@ -1 +1 @@
1
- {"version":3,"file":"dice.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/dice.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,EAAE;AACF,mEAAmE;AACnE,EAAE;AACF,qEAAqE;AACrE,EAAE;AACF,0EAA0E;AAC1E,oEAAoE;AACpE,2EAA2E;AAC3E,kDAAkD;AAClD,EAAE;AACF,0EAA0E;AAC1E,oEAAoE;AAGpE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,6EAA6E;AAC7E,uEAAuE;AACvE,wEAAwE;AACxE,4DAA4D;AAC5D,MAAM,UAAU,GAAG,gCAAgC,CAAC;AAEpD,MAAM,CAAC,MAAM,WAAW,GAAkB;IACxC,MAAM,EAAE,MAAM;IACd,MAAM,CAAC,OAAe;QACpB,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,iEAAiE;YACjE,oDAAoD;YACpD,OAAO;gBACL,IAAI,EAAE,+EAA+E,OAAO,SAAS;aACtG,CAAC;QACJ,CAAC;QACD,OAAO;YACL,IAAI,EAAE,yDAAyD,OAAO,2BAA2B,OAAO,WAAW;SACpH,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,6EAA6E;AAC7E,wEAAwE;AACxE,uEAAuE;AACvE,yDAAyD;AAEzD,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgC3B,CAAC;AAEF,qBAAqB,CAAC,WAAW,EAAE;IACjC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,yBAAyB,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAC/E,CAAC,CAAC"}
1
+ {"version":3,"file":"dice.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/dice.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,EAAE;AACF,mEAAmE;AACnE,EAAE;AACF,qEAAqE;AACrE,EAAE;AACF,0EAA0E;AAC1E,oEAAoE;AACpE,2EAA2E;AAC3E,kDAAkD;AAClD,EAAE;AACF,0EAA0E;AAC1E,oEAAoE;AAGpE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,6EAA6E;AAC7E,uEAAuE;AACvE,wEAAwE;AACxE,4DAA4D;AAC5D,MAAM,UAAU,GAAG,gCAAgC,CAAC;AAEpD,MAAM,CAAC,MAAM,WAAW,GAAkB;IACxC,MAAM,EAAE,MAAM;IACd,MAAM,CAAC,OAAe;QACpB,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,iEAAiE;YACjE,oDAAoD;YACpD,OAAO;gBACL,IAAI,EAAE,+EAA+E,OAAO,SAAS;aACtG,CAAC;QACJ,CAAC;QACD,OAAO;YACL,IAAI,EAAE,yDAAyD,OAAO,2BAA2B,OAAO,WAAW;SACpH,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,6EAA6E;AAC7E,wEAAwE;AACxE,uEAAuE;AACvE,yDAAyD;AAEzD,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgC3B,CAAC;AAEF,qBAAqB,CAAC,WAAW,EAAE;IACjC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,yBAAyB,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAC/E,CAAC,CAAC"}
@@ -0,0 +1,50 @@
1
+ // Built-in code-block `fm` handler — renders a frontmatter value as a fenced
2
+ // code block, with an optional language hint via the fence meta string.
3
+ //
4
+ // Usage:
5
+ //
6
+ // ```fm javascript
7
+ // foundry.data.command
8
+ // ```
9
+ //
10
+ // Renders as:
11
+ //
12
+ // <pre><code class="language-javascript">…value of frontmatter.foundry.data.command…</code></pre>
13
+ //
14
+ // Companion to the inline `fm:` handler. Inline form is used in prose to
15
+ // pull a frontmatter value into a sentence; this code-block form is used
16
+ // when the value IS code (or otherwise wants a `<pre>` wrapper) and you
17
+ // want a single source of truth for that code rather than duplicating it
18
+ // between the frontmatter and the page body.
19
+ //
20
+ // The body of the fence is the dot-path (single token, leading/trailing
21
+ // whitespace tolerated). Anything beyond the lang on the fence is the
22
+ // optional language hint — `lang className="language-…"` so client-side
23
+ // highlighters can pick it up. Missing path or non-string value renders
24
+ // as a visible warning marker rather than failing the build, same policy
25
+ // as the inline handler.
26
+ import { htmlEscape } from "../../../escape.js";
27
+ import { lookup } from "./fm.js";
28
+ export const fmCodeHandler = {
29
+ codeBlock: "fm",
30
+ render(content, ctx) {
31
+ const path = content.trim();
32
+ if (!path)
33
+ return missing("(empty)");
34
+ const value = lookup(ctx.frontmatter, path);
35
+ if (typeof value !== "string")
36
+ return missing(`${path}: not a string`);
37
+ const langClass = ctx.codeBlockMeta
38
+ ? ` class="language-${htmlEscape(ctx.codeBlockMeta)}"`
39
+ : "";
40
+ return {
41
+ html: `<pre><code${langClass}>${htmlEscape(value)}</code></pre>`,
42
+ };
43
+ },
44
+ };
45
+ function missing(key) {
46
+ return {
47
+ html: `<pre><code class="fm-missing" title="frontmatter key not found">{{${htmlEscape(key)}}}</code></pre>`,
48
+ };
49
+ }
50
+ //# sourceMappingURL=fm-code.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fm-code.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/fm-code.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,wEAAwE;AACxE,EAAE;AACF,SAAS;AACT,EAAE;AACF,qBAAqB;AACrB,yBAAyB;AACzB,QAAQ;AACR,EAAE;AACF,cAAc;AACd,EAAE;AACF,oGAAoG;AACpG,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,wEAAwE;AACxE,yEAAyE;AACzE,6CAA6C;AAC7C,EAAE;AACF,wEAAwE;AACxE,sEAAsE;AACtE,wEAAwE;AACxE,wEAAwE;AACxE,yEAAyE;AACzE,yBAAyB;AAGzB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC,MAAM,CAAC,MAAM,aAAa,GAAqB;IAC7C,SAAS,EAAE,IAAI;IACf,MAAM,CAAC,OAAO,EAAE,GAAG;QACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI;YAAE,OAAO,OAAO,CAAC,SAAS,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,OAAO,CAAC,GAAG,IAAI,gBAAgB,CAAC,CAAC;QACvE,MAAM,SAAS,GAAG,GAAG,CAAC,aAAa;YACjC,CAAC,CAAC,oBAAoB,UAAU,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG;YACtD,CAAC,CAAC,EAAE,CAAC;QACP,OAAO;YACL,IAAI,EAAE,aAAa,SAAS,IAAI,UAAU,CAAC,KAAK,CAAC,eAAe;SACjE,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO;QACL,IAAI,EAAE,qEAAqE,UAAU,CAAC,GAAG,CAAC,iBAAiB;KAC5G,CAAC;AACJ,CAAC"}
@@ -26,7 +26,8 @@
26
26
  // Arrays are joined with ", ". Missing keys and unsupported value shapes
27
27
  // (plain objects, null) render a visible warning marker so the page
28
28
  // surfaces the problem instead of silently emitting "undefined".
29
- import { htmlEscape } from "../types.js";
29
+ import { htmlEscape } from "../../../escape.js";
30
+ import { formatInline } from "./inline-format.js";
30
31
  export const fmHandler = {
31
32
  inline: "fm",
32
33
  render(content, ctx) {
@@ -48,7 +49,10 @@ export const fmHandler = {
48
49
  // wrapping in a <span> keeps the value visible. The span is invisible in
49
50
  // the final layout so it doesn't change typography.
50
51
  function wrap(inner) { return `<span class="fm-value">${inner}</span>`; }
51
- function lookup(root, path) {
52
+ /** Walk a dot-path into the page's frontmatter; numeric segments index
53
+ * arrays. Exported so the sibling fm code-block handler reuses the same
54
+ * resolution semantics. */
55
+ export function lookup(root, path) {
52
56
  const segments = path.split(".");
53
57
  let cursor = root;
54
58
  for (const seg of segments) {
@@ -60,16 +64,6 @@ function lookup(root, path) {
60
64
  }
61
65
  return cursor;
62
66
  }
63
- // HTML-escape, then a tiny inline-markdown pass: bold (** **) before italic
64
- // (* *) so bold doesn't get eaten, then code spans. Matches the same subset
65
- // the statblock handler's formatInline supports — keep these in sync.
66
- function formatInline(s) {
67
- let out = htmlEscape(s);
68
- out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
69
- out = out.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
70
- out = out.replace(/`([^`]+)`/g, "<code>$1</code>");
71
- return out;
72
- }
73
67
  function formatScalar(v) {
74
68
  if (v === null || v === undefined)
75
69
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"fm.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/fm.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fm.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/fm.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,EAAE;AACF,QAAQ;AACR,kBAAkB;AAClB,aAAa;AACb,WAAW;AACX,aAAa;AACb,QAAQ;AACR,iEAAiE;AACjE,EAAE;AACF,kDAAkD;AAClD,EAAE;AACF,wEAAwE;AACxE,yEAAyE;AACzE,6CAA6C;AAC7C,EAAE;AACF,yEAAyE;AACzE,wEAAwE;AACxE,0EAA0E;AAC1E,0EAA0E;AAC1E,yEAAyE;AACzE,2EAA2E;AAC3E,8BAA8B;AAC9B,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,oEAAoE;AACpE,iEAAiE;AAGjE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,CAAC,MAAM,SAAS,GAAkB;IACtC,MAAM,EAAE,IAAI;IACZ,MAAM,CAAC,OAAO,EAAE,GAAG;QACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI;YAAE,OAAO,OAAO,CAAC,SAAS,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QAC5C,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,SAAS,KAAK,IAAI;YAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;QACvE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,CAAC;QACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;CACF,CAAC;AAEF,oEAAoE;AACpE,0EAA0E;AAC1E,yEAAyE;AACzE,oDAAoD;AACpD,SAAS,IAAI,CAAC,KAAa,IAAY,OAAO,0BAA0B,KAAK,SAAS,CAAC,CAAC,CAAC;AAEzF;;4BAE4B;AAC5B,MAAM,UAAU,MAAM,CAAC,IAA6B,EAAE,IAAY;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,MAAM,GAAY,IAAI,CAAC;IAC3B,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC9D,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACjD,MAAM,GAAI,MAAkC,CAAC,GAAG,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,YAAY,CAAC,CAAU;IAC9B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC/C,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IACtE,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAClF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO;QACL,IAAI,EAAE,gEAAgE,UAAU,CAAC,GAAG,CAAC,WAAW;KACjG,CAAC;AACJ,CAAC"}
@@ -2,8 +2,12 @@
2
2
  //
3
3
  // User-defined handlers (loaded from .vaults/handlers/) override built-ins
4
4
  // of the same name; see buildRegistry() in handlers/types.ts.
5
+ import { battlemapHandler } from "./battlemap.js";
5
6
  import { diceHandler } from "./dice.js";
6
7
  import { fmHandler } from "./fm.js";
8
+ import { fmCodeHandler } from "./fm-code.js";
7
9
  import { statblockHandler } from "./statblock.js";
8
- export const BUILTIN_HANDLERS = [diceHandler, fmHandler, statblockHandler];
10
+ export const BUILTIN_HANDLERS = [
11
+ diceHandler, fmHandler, fmCodeHandler, statblockHandler, battlemapHandler,
12
+ ];
9
13
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,EAAE;AACF,2EAA2E;AAC3E,8DAA8D;AAG9D,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,CAAC,MAAM,gBAAgB,GAAc,CAAC,WAAW,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,EAAE;AACF,2EAA2E;AAC3E,8DAA8D;AAG9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,CAAC,MAAM,gBAAgB,GAAc;IACzC,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB;CAC1E,CAAC"}
@@ -0,0 +1,26 @@
1
+ // Tiny inline-markdown formatter shared by the built-in `fm` and `statblock`
2
+ // handlers. Both render frontmatter scalars or bespoke content directly to
3
+ // HTML rather than re-feeding it through the full markdown pipeline, but
4
+ // authors still expect basic ** ** / * * / `` markup to survive. This is
5
+ // the smallest formatter that supports that.
6
+ //
7
+ // Bold runs first (** **) so the italic regex can't gobble its asterisks.
8
+ // Italic uses a leading non-`*` guard so `**bold**` doesn't match. Code
9
+ // spans run last; their delimiter (`) is HTML-safe after htmlEscape.
10
+ //
11
+ // Wikilinks are intentionally NOT processed here — wikilink resolution
12
+ // requires the full RenderContext and lives in render/wikilink.ts. Authors
13
+ // who need wikilinks should put them in regular prose.
14
+ import { htmlEscape } from "../../../escape.js";
15
+ /**
16
+ * HTML-escape `s`, then apply a small inline-markdown subset
17
+ * (bold, italic, code spans). Returns ready-to-insert HTML.
18
+ */
19
+ export function formatInline(s) {
20
+ let out = htmlEscape(s);
21
+ out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
22
+ out = out.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
23
+ out = out.replace(/`([^`]+)`/g, "<code>$1</code>");
24
+ return out;
25
+ }
26
+ //# sourceMappingURL=inline-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-format.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/inline-format.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,2EAA2E;AAC3E,yEAAyE;AACzE,yEAAyE;AACzE,6CAA6C;AAC7C,EAAE;AACF,0EAA0E;AAC1E,wEAAwE;AACxE,qEAAqE;AACrE,EAAE;AACF,uEAAuE;AACvE,2EAA2E;AAC3E,uDAAuD;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,CAAS;IACpC,IAAI,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IACxB,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,CAAC;IAC7D,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,8BAA8B,EAAE,eAAe,CAAC,CAAC;IACnE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAAC;IACnD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -30,13 +30,13 @@
30
30
  //
31
31
  // Coverage: name/size/type/subtype/alignment, ac (+ ac_class), hp (+ hit_dice),
32
32
  // speed, six abilities (stats), saves, skillsaves, damage_*, condition_immunities,
33
- // senses, languages, cr, traits, actions, reactions, legendary_actions
34
- // (+ legendary_description). Inline markdown in `desc` fields supports **bold**,
35
- // *italic*, and `code`. Other Fantasy Statblocks features (spells nested
36
- // formatting, custom layouts, dice-roller integration, JS callbacks, image
37
- // fields) are not supported in v1.
33
+ // senses, languages, cr, traits, spells (basic spellcasting list), actions,
34
+ // reactions, legendary_actions (+ legendary_description). Inline markdown in
35
+ // `desc` fields supports **bold**, *italic*, and `code`. Other Fantasy Statblocks
36
+ // features (innate spellcasting, PF2e/13th-age custom layouts, dice-roller
37
+ // integration, JS callbacks, image fields) are not supported in v1.
38
38
  import yaml from "js-yaml";
39
- import { htmlEscape } from "../types.js";
39
+ import { htmlEscape } from "../../../escape.js";
40
40
  import { registerBuiltinAssets } from "../assets.js";
41
41
  const ABILITY_NAMES = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];
42
42
  const ABILITY_SHORT = {
@@ -109,10 +109,24 @@ async function preprocessHandlers(m, ctx) {
109
109
  if (typeof mAny[k] === "string")
110
110
  mAny[k] = await tokenize(mAny[k]);
111
111
  }
112
- // Per-entry name and desc inside trait/action lists.
112
+ // Per-entry name and desc inside trait/action lists. Nested traits are
113
+ // hoisted to the top level prefixed with the parent's name (v1: flat).
113
114
  const list = async (xs) => {
114
115
  if (!xs)
115
116
  return;
117
+ // Inline-flatten one level of nested .traits[].traits before tokenizing.
118
+ for (let i = 0; i < xs.length; i++) {
119
+ const x = xs[i];
120
+ if (Array.isArray(x.traits) && x.traits.length) {
121
+ const parentName = x.name ?? "";
122
+ const flattened = x.traits.map((nested) => ({
123
+ ...nested,
124
+ name: parentName ? `${parentName}: ${nested.name ?? ""}`.trim() : (nested.name ?? ""),
125
+ }));
126
+ delete x.traits;
127
+ xs.splice(i + 1, 0, ...flattened);
128
+ }
129
+ }
116
130
  for (const x of xs) {
117
131
  if (typeof x.name === "string")
118
132
  x.name = await tokenize(x.name);
@@ -122,8 +136,30 @@ async function preprocessHandlers(m, ctx) {
122
136
  };
123
137
  await list(m.traits);
124
138
  await list(m.actions);
139
+ await list(m.bonus_actions);
125
140
  await list(m.reactions);
126
141
  await list(m.legendary_actions);
142
+ await list(m.mythic_actions);
143
+ await list(m.lair_actions);
144
+ await list(m.triggered_actions);
145
+ // spells: each entry is a string OR object. Tokenize string values
146
+ // (top-level entries and per-key values inside object entries) so
147
+ // dice:/fm: work inside.
148
+ if (m.spells) {
149
+ for (let i = 0; i < m.spells.length; i++) {
150
+ const s = m.spells[i];
151
+ if (typeof s === "string") {
152
+ m.spells[i] = (await tokenize(s)) ?? s;
153
+ }
154
+ else if (s && typeof s === "object") {
155
+ for (const k of Object.keys(s)) {
156
+ const v = s[k];
157
+ if (typeof v === "string")
158
+ s[k] = (await tokenize(v)) ?? v;
159
+ }
160
+ }
161
+ }
162
+ }
127
163
  return spliceback;
128
164
  }
129
165
  function property(label, value, spliceback) {
@@ -135,10 +171,17 @@ function header(m, spliceback) {
135
171
  const name = m.name ?? "Unnamed";
136
172
  const sub = [m.size, m.type, m.subtype && `(${m.subtype})`, m.alignment]
137
173
  .filter(Boolean).join(" ");
174
+ // The image path is treated as a vault-relative reference; we emit it
175
+ // verbatim. Authors typically use a path their wiki's image cache serves
176
+ // (e.g. "portraits/foo.webp"). The sanitizer allows src/alt/class on img.
177
+ const img = m.image
178
+ ? `<img class="statblock-image" src="${htmlEscape(m.image)}" alt="${htmlEscape(name)}">`
179
+ : "";
138
180
  // Rendered as a div, not an h2: the page sanitizer restricts <h2> to
139
181
  // class="sr-only" only (GFM footnotes use it). The name is also not part
140
182
  // of the page's article outline — pages can have multiple statblocks.
141
183
  return (`<div class="statblock-header">` +
184
+ img +
142
185
  `<div class="statblock-name">${formatInline(name, spliceback)}</div>` +
143
186
  (sub ? `<p class="statblock-subheading">${formatInline(sub, spliceback)}</p>` : "") +
144
187
  `</div>`);
@@ -164,19 +207,32 @@ function stats(m) {
164
207
  `</div>`).join("");
165
208
  return `<div class="statblock-stats">${cells}</div>`;
166
209
  }
210
+ // FS accepts saves/skillsaves either as an array of single-key objects OR a
211
+ // single multi-key object. Normalize to a flat list of [key, val] pairs.
167
212
  function bonusList(entries, labelFor) {
168
- if (!entries?.length)
213
+ if (!entries)
169
214
  return "";
170
- return entries.map(entry => {
171
- const pair = Object.entries(entry)[0];
172
- if (!pair)
173
- return "";
174
- const [key, val] = pair;
175
- if (val === undefined)
176
- return "";
215
+ const pairs = [];
216
+ if (Array.isArray(entries)) {
217
+ for (const entry of entries) {
218
+ for (const [k, v] of Object.entries(entry)) {
219
+ if (typeof v === "number")
220
+ pairs.push([k, v]);
221
+ }
222
+ }
223
+ }
224
+ else if (typeof entries === "object") {
225
+ for (const [k, v] of Object.entries(entries)) {
226
+ if (typeof v === "number")
227
+ pairs.push([k, v]);
228
+ }
229
+ }
230
+ if (!pairs.length)
231
+ return "";
232
+ return pairs.map(([key, val]) => {
177
233
  const sign = val >= 0 ? "+" : "";
178
234
  return `${labelFor(key)} ${sign}${val}`;
179
- }).filter(Boolean).join(", ");
235
+ }).join(", ");
180
236
  }
181
237
  function midProperties(m, spliceback) {
182
238
  const saves = bonusList(m.saves, k => ABILITY_SHORT[k.toLowerCase()] ?? k);
@@ -199,6 +255,52 @@ function trait(t, spliceback) {
199
255
  formatInline(t.desc ?? "", spliceback) +
200
256
  `</p>`;
201
257
  }
258
+ // Render a single per-level line: bold label + comma-separated, auto-italicized
259
+ // spell list. Used for both string entries (split on first `:`) and object
260
+ // entries (key = label, value = spell list string).
261
+ function spellLevelLine(label, list, spliceback) {
262
+ const spellList = list.split(",")
263
+ .map(s => s.trim()).filter(Boolean)
264
+ .map(s => `<em>${formatInline(s, spliceback)}</em>`)
265
+ .join(", ");
266
+ return `<p class="statblock-spell-level">` +
267
+ `<strong>${formatInline(label, spliceback)}</strong>: ${spellList}` +
268
+ `</p>`;
269
+ }
270
+ // Render Fantasy Statblocks `spells:` — first entry is intro prose shown as
271
+ // a Spellcasting trait header; remaining entries are per-level lines.
272
+ //
273
+ // Each entry can be a string (split on `:`) OR an object whose keys are
274
+ // level labels and values are the spell list strings (FS Spell type).
275
+ // Entries without a `:` fall through as plain paragraphs.
276
+ function spellcasting(spells, spliceback) {
277
+ if (!spells?.length)
278
+ return "";
279
+ const [first, ...rest] = spells;
280
+ // Object as the first entry has no obvious "intro prose" interpretation;
281
+ // treat all entries as levels in that case (no Spellcasting header).
282
+ const introIsString = typeof first === "string";
283
+ const head = introIsString
284
+ ? `<p class="statblock-trait">` +
285
+ `<strong><em>Spellcasting.</em></strong> ` +
286
+ formatInline(first, spliceback) +
287
+ `</p>`
288
+ : "";
289
+ const levels = introIsString ? rest : spells;
290
+ const lines = levels.map(entry => {
291
+ if (typeof entry === "string") {
292
+ const colon = entry.indexOf(":");
293
+ if (colon < 0)
294
+ return `<p class="statblock-spell-level">${formatInline(entry, spliceback)}</p>`;
295
+ return spellLevelLine(entry.slice(0, colon), entry.slice(colon + 1), spliceback);
296
+ }
297
+ // Object form: render each key/value pair as its own level line.
298
+ return Object.entries(entry)
299
+ .map(([label, list]) => spellLevelLine(label, String(list ?? ""), spliceback))
300
+ .join("");
301
+ }).join("");
302
+ return head + lines;
303
+ }
202
304
  function section(label, items, spliceback, intro) {
203
305
  if (!items?.length)
204
306
  return "";
@@ -229,8 +331,16 @@ export const statblockHandler = {
229
331
  // fields with sentinel tokens; spliceback holds the resulting HTML and
230
332
  // formatInline weaves it back in after escaping.
231
333
  const spliceback = await preprocessHandlers(m, ctx);
232
- const traitsHtml = m.traits?.length
233
- ? `<div class="statblock-section">${m.traits.map(t => trait(t, spliceback)).join("")}</div>`
334
+ const spellsHtml = spellcasting(m.spells, spliceback);
335
+ const traitsHtml = m.traits?.length || spellsHtml
336
+ ? `<div class="statblock-section">${(m.traits ?? []).map(t => trait(t, spliceback)).join("")}${spellsHtml}</div>`
337
+ : "";
338
+ const sourceText = Array.isArray(m.source) ? m.source.join(", ") : m.source;
339
+ const sourceHtml = sourceText
340
+ ? `<p class="statblock-source"><em>${formatInline(sourceText, spliceback)}</em></p>`
341
+ : "";
342
+ const noteHtml = m.note
343
+ ? `<p class="statblock-note"><em>${formatInline(m.note, spliceback)}</em></p>`
234
344
  : "";
235
345
  const body = [
236
346
  header(m, spliceback),
@@ -243,8 +353,14 @@ export const statblockHandler = {
243
353
  `<div class="statblock-rule statblock-rule-tapered"></div>`,
244
354
  traitsHtml,
245
355
  section("Actions", m.actions, spliceback),
356
+ section("Bonus Actions", m.bonus_actions, spliceback),
246
357
  section("Reactions", m.reactions, spliceback),
247
358
  section("Legendary Actions", m.legendary_actions, spliceback, m.legendary_description),
359
+ section("Mythic Actions", m.mythic_actions, spliceback, m.mythic_description),
360
+ section("Lair Actions", m.lair_actions, spliceback),
361
+ section("Triggered Actions", m.triggered_actions, spliceback),
362
+ sourceHtml,
363
+ noteHtml,
248
364
  ].join("");
249
365
  return { html: `<div class="statblock">${body}</div>` };
250
366
  },
@@ -335,6 +451,11 @@ const STATBLOCK_CSS = `
335
451
  font-weight: bold;
336
452
  font-style: italic;
337
453
  }
454
+ .statblock-spell-level {
455
+ margin: 0.15rem 0 0.15rem 1rem;
456
+ text-indent: -1rem;
457
+ padding-left: 1rem;
458
+ }
338
459
  .statblock-section-intro {
339
460
  font-style: italic;
340
461
  margin: 0.3rem 0;
@@ -344,8 +465,27 @@ const STATBLOCK_CSS = `
344
465
  color: #7a200d;
345
466
  border-color: #7a200d;
346
467
  }
468
+ .statblock-image {
469
+ float: right;
470
+ width: 75px;
471
+ height: 75px;
472
+ object-fit: cover;
473
+ margin: 0 0 0.4rem 0.5rem;
474
+ border: 2px solid var(--statblock-rule-color);
475
+ border-radius: 2px;
476
+ }
477
+ .statblock-source,
478
+ .statblock-note {
479
+ font-size: 0.85rem;
480
+ margin-top: 0.4rem;
481
+ color: #555;
482
+ }
347
483
  `;
348
484
  registerBuiltinAssets(statblockHandler, {
349
485
  styles: [{ source: "builtin/statblock.css", content: STATBLOCK_CSS }],
486
+ // Styles ride into the Foundry-import bundle so synced statblocks
487
+ // render identically. Scripts intentionally omitted: dice runtime is
488
+ // replaced by Foundry's [[/r]] enricher in links.mjs.
489
+ foundry: { styles: true },
350
490
  });
351
491
  //# sourceMappingURL=statblock.js.map