@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.
- package/README.md +8 -7
- package/dist/build.js +425 -123
- package/dist/build.js.map +1 -1
- package/dist/commands/build.js +2 -1
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/password.js +1 -1
- package/dist/commands/password.js.map +1 -1
- package/dist/commands/patreon.js +24 -20
- package/dist/commands/patreon.js.map +1 -1
- package/dist/commands/preview.js +5 -4
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/push.js +7 -8
- package/dist/commands/push.js.map +1 -1
- package/dist/config.js +21 -10
- package/dist/config.js.map +1 -1
- package/dist/escape.js +29 -0
- package/dist/escape.js.map +1 -0
- package/dist/favicon.js +3 -36
- package/dist/favicon.js.map +1 -1
- package/dist/foundry-importer.js +61 -0
- package/dist/foundry-importer.js.map +1 -0
- package/dist/images.js +0 -30
- package/dist/images.js.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/migrate/0.6-legacy-auth-settings.js +3 -5
- package/dist/migrate/0.6-legacy-auth-settings.js.map +1 -1
- package/dist/migrate/0.7-vaults-dir.js +4 -0
- package/dist/migrate/0.7-vaults-dir.js.map +1 -1
- package/dist/migrate/registry.js +1 -7
- package/dist/migrate/registry.js.map +1 -1
- package/dist/paths.js +21 -6
- package/dist/paths.js.map +1 -1
- package/dist/render/auth-template.js +21 -142
- package/dist/render/auth-template.js.map +1 -1
- package/dist/render/bases.js +56 -44
- package/dist/render/bases.js.map +1 -1
- package/dist/render/callouts.js +29 -10
- package/dist/render/callouts.js.map +1 -1
- package/dist/render/embed.js +124 -26
- package/dist/render/embed.js.map +1 -1
- package/dist/render/extensions.js +68 -0
- package/dist/render/extensions.js.map +1 -0
- package/dist/render/external-links.js +32 -0
- package/dist/render/external-links.js.map +1 -0
- package/dist/render/frontmatter.js +17 -0
- package/dist/render/frontmatter.js.map +1 -0
- package/dist/render/handlers/assets.js +48 -15
- package/dist/render/handlers/assets.js.map +1 -1
- package/dist/render/handlers/builtin/battlemap.js +199 -0
- package/dist/render/handlers/builtin/battlemap.js.map +1 -0
- package/dist/render/handlers/builtin/dice.js +1 -1
- package/dist/render/handlers/builtin/dice.js.map +1 -1
- package/dist/render/handlers/builtin/fm-code.js +50 -0
- package/dist/render/handlers/builtin/fm-code.js.map +1 -0
- package/dist/render/handlers/builtin/fm.js +6 -12
- package/dist/render/handlers/builtin/fm.js.map +1 -1
- package/dist/render/handlers/builtin/index.js +5 -1
- package/dist/render/handlers/builtin/index.js.map +1 -1
- package/dist/render/handlers/builtin/inline-format.js +26 -0
- package/dist/render/handlers/builtin/inline-format.js.map +1 -0
- package/dist/render/handlers/builtin/statblock.js +158 -18
- package/dist/render/handlers/builtin/statblock.js.map +1 -1
- package/dist/render/handlers/dispatch.js +15 -20
- package/dist/render/handlers/dispatch.js.map +1 -1
- package/dist/render/handlers/types.js +41 -21
- package/dist/render/handlers/types.js.map +1 -1
- package/dist/render/image-srcs.js +42 -0
- package/dist/render/image-srcs.js.map +1 -0
- package/dist/render/layout.js +60 -9
- package/dist/render/layout.js.map +1 -1
- package/dist/render/pipeline.js +23 -10
- package/dist/render/pipeline.js.map +1 -1
- package/dist/render/preview.js +53 -18
- package/dist/render/preview.js.map +1 -1
- package/dist/render/slug.js +5 -0
- package/dist/render/slug.js.map +1 -1
- package/dist/render/styles.js +94 -14
- package/dist/render/styles.js.map +1 -1
- package/dist/render/wikilink.js +15 -4
- package/dist/render/wikilink.js.map +1 -1
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/settings.js +16 -1
- package/dist/settings.js.map +1 -1
- package/dist/version.js +36 -0
- package/dist/version.js.map +1 -0
- 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 "
|
|
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,
|
|
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 "
|
|
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
|
-
|
|
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":"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,
|
|
1
|
+
{"version":3,"file":"fm.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/fm.ts"],"names":[],"mappings":"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"}
|
|
@@ -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 = [
|
|
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,
|
|
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,
|
|
34
|
-
// (+ legendary_description). Inline markdown in
|
|
35
|
-
// *italic*, and `code`. Other Fantasy Statblocks
|
|
36
|
-
//
|
|
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 "
|
|
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
|
|
213
|
+
if (!entries)
|
|
169
214
|
return "";
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
}).
|
|
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
|
|
233
|
-
|
|
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
|