jekyll-dice-tray 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 808fa6526e8321dc1118a09748a1e24818889bf88af1ecdf729b9a423c6b7291
4
+ data.tar.gz: d56b7f208211cb62b0ec536c41fc2a51d74756b99fbbbed4a9db6c10e42204cb
5
+ SHA512:
6
+ metadata.gz: e58e36fe768267500d132ec2a7d293ed49ef84421ddf39738bde6e1ef4f0d36d38cfee77f8322c00474888c567a20ba4285a6b2772f905c8b502c6527bbdffd3
7
+ data.tar.gz: fd4dc912ec0a339d843d3cedde8c33c418f0804e3b4db2609025e9dd49e8ec181c6a339ae37232d802979f8f274aa9c242430d3b912f68c0197d2ae035b008ba
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 directsun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # jekyll-dice-tray
2
+
3
+ A Jekyll plugin gem that:
4
+
5
+ - Adds an overlay dice tray that accepts dice rolling input
6
+ - Recognizes dice expressions in rendered pages like `d4`, `1d6`, `1d20+1` and turns them into clickable links that roll in the tray
7
+ - Recognizes THAC0-style bracket modifiers like `THAC0 18 [+1]` or `18 [+1]` in a THAC0 table row/column; the `+1` inside the brackets is clickable and rolls `d20+1` (other `[+n]` text is left alone)
8
+ - Persists input history, result history, minimize status
9
+ - `/help` - help
10
+ - `/clear` - clears results and input history
11
+
12
+ ## Install
13
+
14
+ Add to your site `Gemfile`:
15
+
16
+ ```ruby
17
+ gem "jekyll-dice-tray"
18
+ ```
19
+
20
+ Then in `_config.yml`:
21
+
22
+ ```yml
23
+ plugins:
24
+ - jekyll-dice-tray
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ Optional `_config.yml` settings:
30
+
31
+ ```yml
32
+ dice_tray:
33
+ enabled: true
34
+ assets_path: /assets/jekyll-dice-tray
35
+ inject_tray: true
36
+ link_dice_in_markdown: true
37
+ ```
@@ -0,0 +1,183 @@
1
+ .jekyll-dice-tray-hidden {
2
+ display: none !important;
3
+ }
4
+
5
+ #jekyll-dice-tray {
6
+ position: fixed;
7
+ right: 16px;
8
+ bottom: 16px;
9
+ z-index: 2147483647;
10
+ width: 320px;
11
+ max-width: calc(100vw - 32px);
12
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
13
+ }
14
+
15
+ #jekyll-dice-tray .jdt-header {
16
+ display: flex;
17
+ justify-content: flex-end;
18
+ }
19
+
20
+ #jekyll-dice-tray .jdt-toggle {
21
+ border: 1px solid rgba(0, 0, 0, 0.2);
22
+ background: rgba(255, 255, 255, 0.95);
23
+ color: #111;
24
+ border-radius: 999px;
25
+ padding: 8px 12px;
26
+ cursor: pointer;
27
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
28
+ }
29
+
30
+ #jekyll-dice-tray .jdt-body {
31
+ margin-top: 10px;
32
+ border: 1px solid rgba(0, 0, 0, 0.12);
33
+ background: rgba(255, 255, 255, 0.98);
34
+ border-radius: 12px;
35
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22);
36
+ overflow: hidden;
37
+ display: flex;
38
+ flex-direction: column;
39
+ max-height: 360px;
40
+ }
41
+
42
+ #jekyll-dice-tray[data-expanded="false"] .jdt-body {
43
+ display: none;
44
+ }
45
+
46
+ #jekyll-dice-tray .jdt-clue {
47
+ padding: 10px 12px 0 12px;
48
+ color: rgba(0, 0, 0, 0.55);
49
+ font-size: 12px;
50
+ line-height: 1.35;
51
+ }
52
+
53
+ #jekyll-dice-tray .jdt-clue code {
54
+ background: rgba(0, 0, 0, 0.05);
55
+ padding: 1px 4px;
56
+ border-radius: 6px;
57
+ }
58
+
59
+ #jekyll-dice-tray .jdt-input {
60
+ width: calc(100% - 24px);
61
+ margin: 10px 12px 12px 12px;
62
+ padding: 10px 10px;
63
+ border: 1px solid rgba(0, 0, 0, 0.15);
64
+ border-radius: 10px;
65
+ font-size: 14px;
66
+ }
67
+
68
+ #jekyll-dice-tray .jdt-log {
69
+ flex: 1 1 auto;
70
+ min-height: 120px;
71
+ overflow: auto;
72
+ padding: 0 12px 0 12px;
73
+ }
74
+
75
+ #jekyll-dice-tray .jdt-entry {
76
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
77
+ padding: 8px 0;
78
+ font-size: 13px;
79
+ }
80
+
81
+ #jekyll-dice-tray .jdt-entry:first-child {
82
+ border-top: none;
83
+ }
84
+
85
+ #jekyll-dice-tray .jdt-entry .jdt-expr {
86
+ color: rgba(0, 0, 0, 0.6);
87
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
88
+ }
89
+
90
+ #jekyll-dice-tray .jdt-entry .jdt-total {
91
+ font-weight: 700;
92
+ margin-left: 8px;
93
+ }
94
+
95
+ #jekyll-dice-tray .jdt-result {
96
+ margin-top: 4px;
97
+ font-size: 13px;
98
+ }
99
+
100
+ #jekyll-dice-tray .jdt-result strong {
101
+ font-weight: 800;
102
+ }
103
+
104
+ #jekyll-dice-tray .jdt-result .jdt-rolls {
105
+ color: rgba(0, 0, 0, 0.6);
106
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
107
+ }
108
+
109
+ #jekyll-dice-tray .jdt-result .jdt-loser {
110
+ color: rgba(0, 0, 0, 0.42);
111
+ }
112
+
113
+ #jekyll-dice-tray .jdt-result .jdt-vs-sep {
114
+ color: rgba(0, 0, 0, 0.45);
115
+ }
116
+
117
+ #jekyll-dice-tray .jdt-entry .jdt-details {
118
+ margin-top: 4px;
119
+ color: rgba(0, 0, 0, 0.55);
120
+ font-size: 12px;
121
+ }
122
+
123
+ a.dice-tray-roll {
124
+ text-decoration: underline;
125
+ text-underline-offset: 2px;
126
+ color: #ff0000;
127
+ }
128
+
129
+ a.dice-tray-roll:visited {
130
+ color: #ff0000;
131
+ }
132
+
133
+ @media (prefers-color-scheme: dark) {
134
+ #jekyll-dice-tray .jdt-toggle {
135
+ border-color: rgba(255, 255, 255, 0.18);
136
+ background: rgba(22, 22, 22, 0.96);
137
+ color: #f2f2f2;
138
+ }
139
+
140
+ #jekyll-dice-tray .jdt-body {
141
+ border-color: rgba(255, 255, 255, 0.12);
142
+ background: rgba(22, 22, 22, 0.98);
143
+ }
144
+
145
+ #jekyll-dice-tray .jdt-clue {
146
+ color: rgba(255, 255, 255, 0.6);
147
+ }
148
+
149
+ #jekyll-dice-tray .jdt-clue code {
150
+ background: rgba(255, 255, 255, 0.08);
151
+ }
152
+
153
+ #jekyll-dice-tray .jdt-input {
154
+ background: rgba(0, 0, 0, 0.2);
155
+ border-color: rgba(255, 255, 255, 0.12);
156
+ color: #f2f2f2;
157
+ }
158
+
159
+ #jekyll-dice-tray .jdt-entry {
160
+ border-top-color: rgba(255, 255, 255, 0.08);
161
+ }
162
+
163
+ #jekyll-dice-tray .jdt-entry .jdt-expr {
164
+ color: rgba(255, 255, 255, 0.65);
165
+ }
166
+
167
+ #jekyll-dice-tray .jdt-entry .jdt-details {
168
+ color: rgba(255, 255, 255, 0.6);
169
+ }
170
+
171
+ #jekyll-dice-tray .jdt-result .jdt-rolls {
172
+ color: rgba(255, 255, 255, 0.65);
173
+ }
174
+
175
+ #jekyll-dice-tray .jdt-result .jdt-loser {
176
+ color: rgba(255, 255, 255, 0.45);
177
+ }
178
+
179
+ #jekyll-dice-tray .jdt-result .jdt-vs-sep {
180
+ color: rgba(255, 255, 255, 0.48);
181
+ }
182
+ }
183
+
@@ -0,0 +1,483 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ var STORAGE_PREFIX = "jekyll_dice_tray:";
5
+ var STORAGE_EXPANDED = STORAGE_PREFIX + "expanded";
6
+ var STORAGE_HISTORY = STORAGE_PREFIX + "history_v1";
7
+ var STORAGE_INPUT_HISTORY = STORAGE_PREFIX + "input_history_v1";
8
+
9
+ function qs(sel, root) {
10
+ return (root || document).querySelector(sel);
11
+ }
12
+
13
+ function el(tag, attrs, text) {
14
+ var n = document.createElement(tag);
15
+ if (attrs) {
16
+ Object.keys(attrs).forEach(function (k) {
17
+ n.setAttribute(k, attrs[k]);
18
+ });
19
+ }
20
+ if (text != null) n.textContent = text;
21
+ return n;
22
+ }
23
+
24
+ function nowTime() {
25
+ try {
26
+ return new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
27
+ } catch (_) {
28
+ return new Date().toLocaleTimeString();
29
+ }
30
+ }
31
+
32
+ function randInt(min, max) {
33
+ min = Math.ceil(min);
34
+ max = Math.floor(max);
35
+ var span = max - min + 1;
36
+ if (span <= 0) return min;
37
+
38
+ if (window.crypto && window.crypto.getRandomValues) {
39
+ var arr = new Uint32Array(1);
40
+ window.crypto.getRandomValues(arr);
41
+ return min + (arr[0] % span);
42
+ }
43
+ return min + Math.floor(Math.random() * span);
44
+ }
45
+
46
+ function parseExpr(input) {
47
+ var s = String(input || "").trim();
48
+ if (!s) return { kind: "empty" };
49
+ if (s === "/help") return { kind: "help" };
50
+ if (s === "/clear") return { kind: "clear" };
51
+
52
+ // Allow whitespace variations like "d20 + 2"
53
+ var compact = s.replace(/\s+/g, "");
54
+
55
+ // Advantage/Disadvantage roll:
56
+ // - "d12+d6(+2)" => take higher of the two totals
57
+ // - "d12-d6(+2)" => take lower of the two totals
58
+ // Modifier applies after choosing.
59
+ // Only allow single-die expressions on each side: dX+dY or dX-dY (no leading counts).
60
+ var best = compact.match(/^d(\d{1,4})([+-])d(\d{1,4})([+-]\d{1,5})?$/i);
61
+ if (best) {
62
+ var c1 = 1;
63
+ var s1 = parseInt(best[1], 10);
64
+ var op = best[2]; // "+" => take higher, "-" => take lower
65
+ var c2 = 1;
66
+ var s2 = parseInt(best[3], 10);
67
+ var modB = best[4] ? parseInt(best[4], 10) : 0;
68
+
69
+ if (!Number.isFinite(c1) || !Number.isFinite(s1) || !Number.isFinite(c2) || !Number.isFinite(s2) || !Number.isFinite(modB)) {
70
+ return { kind: "invalid", raw: s };
71
+ }
72
+ if (c1 < 1) c1 = 1;
73
+ if (c1 > 100) c1 = 100;
74
+ if (s1 < 2) s1 = 2;
75
+ if (s1 > 10000) s1 = 10000;
76
+ if (c2 < 1) c2 = 1;
77
+ if (c2 > 100) c2 = 100;
78
+ if (s2 < 2) s2 = 2;
79
+ if (s2 > 10000) s2 = 10000;
80
+ if (modB < -100000) modB = -100000;
81
+ if (modB > 100000) modB = 100000;
82
+
83
+ var left = "d" + String(s1);
84
+ var right = "d" + String(s2);
85
+ var normalizedB = left + op + right + (modB ? (modB > 0 ? "+" + modB : "" + modB) : "");
86
+ return {
87
+ kind: "bestof2",
88
+ left: { count: c1, sides: s1 },
89
+ right: { count: c2, sides: s2 },
90
+ op: op,
91
+ mod: modB,
92
+ normalized: normalizedB,
93
+ };
94
+ }
95
+
96
+ var m = compact.match(/^(\d{0,3})d(\d{1,4})([+-]\d{1,5})?$/i);
97
+ if (!m) return { kind: "invalid", raw: s };
98
+
99
+ var count = m[1] ? parseInt(m[1], 10) : 1;
100
+ var sides = parseInt(m[2], 10);
101
+ var mod = m[3] ? parseInt(m[3], 10) : 0;
102
+
103
+ if (!Number.isFinite(count) || !Number.isFinite(sides) || !Number.isFinite(mod)) {
104
+ return { kind: "invalid", raw: s };
105
+ }
106
+ if (count < 1) count = 1;
107
+ if (count > 100) count = 100;
108
+ if (sides < 2) sides = 2;
109
+ if (sides > 10000) sides = 10000;
110
+ if (mod < -100000) mod = -100000;
111
+ if (mod > 100000) mod = 100000;
112
+
113
+ var normalized = String(count) + "d" + String(sides) + (mod ? (mod > 0 ? "+" + mod : "" + mod) : "");
114
+ return { kind: "roll", count: count, sides: sides, mod: mod, normalized: normalized };
115
+ }
116
+
117
+ function rollDice(count, sides) {
118
+ var rolls = [];
119
+ var total = 0;
120
+ for (var i = 0; i < count; i++) {
121
+ var r = randInt(1, sides);
122
+ rolls.push(r);
123
+ total += r;
124
+ }
125
+ return { rolls: rolls, total: total };
126
+ }
127
+
128
+ function mountTray(root) {
129
+ var toggle = qs(".jdt-toggle", root);
130
+ var body = qs(".jdt-body", root);
131
+ var input = qs(".jdt-input", root);
132
+ var log = qs(".jdt-log", root);
133
+
134
+ function loadBool(key, fallback) {
135
+ try {
136
+ var v = localStorage.getItem(key);
137
+ if (v === null) return fallback;
138
+ return v === "true";
139
+ } catch (_) {
140
+ return fallback;
141
+ }
142
+ }
143
+
144
+ function saveBool(key, val) {
145
+ try {
146
+ localStorage.setItem(key, val ? "true" : "false");
147
+ } catch (_) {}
148
+ }
149
+
150
+ function loadHistory() {
151
+ try {
152
+ var raw = localStorage.getItem(STORAGE_HISTORY);
153
+ if (!raw) return [];
154
+ var parsed = JSON.parse(raw);
155
+ return Array.isArray(parsed) ? parsed : [];
156
+ } catch (_) {
157
+ return [];
158
+ }
159
+ }
160
+
161
+ function loadInputHistory() {
162
+ try {
163
+ var raw = localStorage.getItem(STORAGE_INPUT_HISTORY);
164
+ if (!raw) return [];
165
+ var parsed = JSON.parse(raw);
166
+ return Array.isArray(parsed) ? parsed : [];
167
+ } catch (_) {
168
+ return [];
169
+ }
170
+ }
171
+
172
+ function saveInputHistory(items) {
173
+ try {
174
+ localStorage.setItem(STORAGE_INPUT_HISTORY, JSON.stringify(items));
175
+ } catch (_) {}
176
+ }
177
+
178
+ function saveHistory(items) {
179
+ try {
180
+ localStorage.setItem(STORAGE_HISTORY, JSON.stringify(items));
181
+ } catch (_) {}
182
+ }
183
+
184
+ var history = loadHistory();
185
+ var inputHistory = loadInputHistory();
186
+ var inputHistoryIdx = inputHistory.length; // points just past last
187
+
188
+ function clearStorageAndUi() {
189
+ try {
190
+ // clear dice tray history
191
+ localStorage.removeItem(STORAGE_HISTORY);
192
+ // clear input history (up-arrow)
193
+ localStorage.removeItem(STORAGE_INPUT_HISTORY);
194
+ } catch (_) {}
195
+
196
+ history = [];
197
+ inputHistory = [];
198
+ inputHistoryIdx = 0;
199
+ log.innerHTML = "";
200
+
201
+ // Confirmation (not persisted); keep current expanded/minimized state
202
+ var entry = el("div", { class: "jdt-entry", title: "/clear" });
203
+ entry.appendChild(el("div", { class: "jdt-expr" }, "Cleared dice tray history and input history."));
204
+ entry.appendChild(el("div", { class: "jdt-details" }, nowTime()));
205
+ log.appendChild(entry);
206
+ log.scrollTop = log.scrollHeight;
207
+ }
208
+
209
+ function setExpanded(expanded) {
210
+ toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
211
+ root.setAttribute("data-expanded", expanded ? "true" : "false");
212
+ // Some themes override [hidden], so enforce display too.
213
+ body.hidden = !expanded;
214
+ body.style.display = expanded ? "" : "none";
215
+ saveBool(STORAGE_EXPANDED, expanded);
216
+ if (expanded) {
217
+ setTimeout(function () {
218
+ input && input.focus();
219
+ }, 0);
220
+ }
221
+ }
222
+
223
+ function renderHistory() {
224
+ log.innerHTML = "";
225
+ history.forEach(function (item) {
226
+ if (!item || typeof item !== "object") return;
227
+ if (item.kind === "system") {
228
+ addSystemEntry(item.title || "", item.body || "", item.time || "");
229
+ } else if (item.kind === "bestof2") {
230
+ addBestOf2Entry(
231
+ item.expr || "",
232
+ item.chosen_total,
233
+ item.left_rolls || [],
234
+ item.right_rolls || [],
235
+ !!item.left_is_winner,
236
+ item.left_label || "",
237
+ item.right_label || "",
238
+ item.mod || 0,
239
+ item.time || "",
240
+ item.mode || "high"
241
+ );
242
+ } else if (item.kind === "roll") {
243
+ addRollEntry(item.expr || "", item.total, item.rolls || [], item.mod || 0, item.time || "");
244
+ }
245
+ });
246
+ log.scrollTop = log.scrollHeight;
247
+ }
248
+
249
+ function pushHistory(item) {
250
+ history.push(item);
251
+ // keep it bounded
252
+ if (history.length > 200) history = history.slice(history.length - 200);
253
+ saveHistory(history);
254
+ }
255
+
256
+ function addSystemEntry(title, body, timeStr) {
257
+ var entry = el("div", { class: "jdt-entry" });
258
+ entry.appendChild(el("div", { class: "jdt-expr" }, title));
259
+ if (body) entry.appendChild(el("div", { class: "jdt-details" }, body));
260
+ entry.appendChild(el("div", { class: "jdt-details" }, timeStr));
261
+ log.appendChild(entry); // newest at bottom
262
+ log.scrollTop = log.scrollHeight;
263
+
264
+ pushHistory({ kind: "system", title: title, body: body, time: timeStr });
265
+ }
266
+
267
+ function addRollEntry(expr, total, rolls, mod, timeStr) {
268
+ var entry = el("div", { class: "jdt-entry", title: expr });
269
+ // expression
270
+ //entry.appendChild(el("div", { class: "jdt-expr" }, expr));
271
+
272
+ var result = el("div", { class: "jdt-result" });
273
+ result.appendChild(el("strong", null, String(total)));
274
+ var rollsText = "[" + rolls.join(", ") + "]";
275
+ if (mod) {
276
+ rollsText += " " + (mod > 0 ? "+" + mod : "" + mod);
277
+ }
278
+ result.appendChild(el("span", { class: "jdt-rolls" }, " " + rollsText));
279
+
280
+ // result
281
+ entry.appendChild(result);
282
+
283
+ // time
284
+ //entry.appendChild(el("div", { class: "jdt-details" }, timeStr));
285
+ log.appendChild(entry); // newest at bottom
286
+ log.scrollTop = log.scrollHeight;
287
+
288
+ pushHistory({ kind: "roll", expr: expr, total: total, rolls: rolls, mod: mod, time: timeStr });
289
+ }
290
+
291
+ function addBestOf2Entry(expr, chosenTotal, leftRolls, rightRolls, leftIsWinner, leftLabel, rightLabel, mod, timeStr, mode) {
292
+ var entry = el("div", { class: "jdt-entry", title: expr });
293
+ entry.appendChild(el("div", { class: "jdt-expr" }, expr));
294
+
295
+ var result = el("div", { class: "jdt-result" });
296
+ result.appendChild(el("strong", null, String(chosenTotal)));
297
+
298
+ // Single bracket containing both pools in left->right order.
299
+ var bracket = el("span", { class: "jdt-rolls" }, " [");
300
+ var leftSpan = el("span", { class: "jdt-vs-part" }, leftRolls.join(", "));
301
+ leftSpan.setAttribute("title", leftLabel);
302
+ if (!leftIsWinner) leftSpan.className += " jdt-loser";
303
+ bracket.appendChild(leftSpan);
304
+
305
+ bracket.appendChild(el("span", { class: "jdt-vs-sep" }, ", "));
306
+
307
+ var rightSpan = el("span", { class: "jdt-vs-part" }, rightRolls.join(", "));
308
+ rightSpan.setAttribute("title", rightLabel);
309
+ if (leftIsWinner) rightSpan.className += " jdt-loser";
310
+ bracket.appendChild(rightSpan);
311
+
312
+ bracket.appendChild(el("span", null, "]"));
313
+ bracket.setAttribute("title", mode === "low" ? "Take lower" : "Take higher");
314
+ result.appendChild(bracket);
315
+
316
+ if (mod) {
317
+ result.appendChild(el("span", { class: "jdt-rolls" }, " " + (mod > 0 ? "+" + mod : "" + mod)));
318
+ }
319
+
320
+ entry.appendChild(result);
321
+ entry.appendChild(el("div", { class: "jdt-details" }, timeStr));
322
+
323
+ log.appendChild(entry);
324
+ log.scrollTop = log.scrollHeight;
325
+
326
+ pushHistory({
327
+ kind: "bestof2",
328
+ expr: expr,
329
+ chosen_total: chosenTotal,
330
+ left_rolls: leftRolls,
331
+ right_rolls: rightRolls,
332
+ left_is_winner: leftIsWinner,
333
+ left_label: leftLabel,
334
+ right_label: rightLabel,
335
+ mode: mode,
336
+ mod: mod,
337
+ time: timeStr,
338
+ });
339
+ }
340
+
341
+ function showHelp() {
342
+ addSystemEntry(
343
+ "Usage: 1d6, d4, 2d8+1",
344
+ "Click linked dice like 1d20+5 or bracket modifiers like [+1] (rolls d20+1). Commands: /help, /clear",
345
+ nowTime()
346
+ );
347
+ }
348
+
349
+ function doRoll(raw) {
350
+ var p = parseExpr(raw);
351
+ if (p.kind === "empty") return;
352
+ if (p.kind === "help") return showHelp();
353
+ if (p.kind === "clear") return clearStorageAndUi();
354
+ if (p.kind !== "roll" && p.kind !== "bestof2") {
355
+ addSystemEntry("Unrecognized roll: " + p.raw, "Try: 1d6, d4, 2d8+1 or /help", nowTime());
356
+ return;
357
+ }
358
+
359
+ if (p.kind === "bestof2") {
360
+ var left = rollDice(p.left.count, p.left.sides);
361
+ var right = rollDice(p.right.count, p.right.sides);
362
+ var leftLabel = String(p.left.count) + "d" + String(p.left.sides);
363
+ var rightLabel = String(p.right.count) + "d" + String(p.right.sides);
364
+
365
+ var mode = p.op === "-" ? "low" : "high";
366
+ var leftIsWinner = mode === "low" ? left.total <= right.total : left.total >= right.total;
367
+ var chosenPreMod = leftIsWinner ? left.total : right.total;
368
+ var chosenTotal = chosenPreMod + p.mod;
369
+
370
+ addBestOf2Entry(
371
+ p.normalized,
372
+ chosenTotal,
373
+ left.rolls,
374
+ right.rolls,
375
+ leftIsWinner,
376
+ leftLabel,
377
+ rightLabel,
378
+ p.mod,
379
+ nowTime(),
380
+ mode
381
+ );
382
+ return;
383
+ }
384
+
385
+ var r = rollDice(p.count, p.sides);
386
+ var total = r.total + p.mod;
387
+ addRollEntry(p.normalized, total, r.rolls, p.mod, nowTime());
388
+ }
389
+
390
+ toggle.addEventListener("click", function () {
391
+ var expanded = toggle.getAttribute("aria-expanded") === "true";
392
+ setExpanded(!expanded);
393
+ });
394
+
395
+ input.addEventListener("keydown", function (e) {
396
+ if (e.key !== "Enter") return;
397
+ var v = input.value;
398
+ input.value = "";
399
+ setExpanded(true);
400
+
401
+ var raw = String(v || "").trim();
402
+ if (raw) {
403
+ if (inputHistory.length === 0 || inputHistory[inputHistory.length - 1] !== raw) {
404
+ inputHistory.push(raw);
405
+ if (inputHistory.length > 100) inputHistory = inputHistory.slice(inputHistory.length - 100);
406
+ saveInputHistory(inputHistory);
407
+ }
408
+ }
409
+ inputHistoryIdx = inputHistory.length;
410
+
411
+ doRoll(v);
412
+ });
413
+
414
+ input.addEventListener("keydown", function (e) {
415
+ if (e.key !== "Escape") return;
416
+ setExpanded(false);
417
+ });
418
+
419
+ input.addEventListener("keydown", function (e) {
420
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
421
+ if (!inputHistory || inputHistory.length === 0) return;
422
+ e.preventDefault();
423
+
424
+ if (e.key === "ArrowUp") {
425
+ inputHistoryIdx = Math.max(0, inputHistoryIdx - 1);
426
+ input.value = inputHistory[inputHistoryIdx] || "";
427
+ } else {
428
+ inputHistoryIdx = Math.min(inputHistory.length, inputHistoryIdx + 1);
429
+ input.value = inputHistoryIdx === inputHistory.length ? "" : inputHistory[inputHistoryIdx] || "";
430
+ }
431
+ setTimeout(function () {
432
+ try {
433
+ input.setSelectionRange(input.value.length, input.value.length);
434
+ } catch (_) {}
435
+ }, 0);
436
+ });
437
+
438
+ input.addEventListener("input", function () {
439
+ inputHistoryIdx = inputHistory.length;
440
+ });
441
+
442
+ document.addEventListener("click", function (e) {
443
+ var a = e.target && e.target.closest ? e.target.closest("a.dice-tray-roll") : null;
444
+ if (!a) return;
445
+ var expr = a.getAttribute("data-dice") || a.textContent;
446
+ if (!expr) return;
447
+ e.preventDefault();
448
+ setExpanded(true);
449
+ doRoll(expr);
450
+ });
451
+
452
+ // Public API
453
+ window.JekyllDiceTray = {
454
+ roll: function (expr) {
455
+ setExpanded(true);
456
+ doRoll(expr);
457
+ },
458
+ open: function () {
459
+ setExpanded(true);
460
+ },
461
+ close: function () {
462
+ setExpanded(false);
463
+ },
464
+ };
465
+
466
+ // hydrate history and persisted expanded state (default minimized)
467
+ renderHistory();
468
+ setExpanded(loadBool(STORAGE_EXPANDED, false));
469
+ }
470
+
471
+ function boot() {
472
+ var root = document.getElementById("jekyll-dice-tray");
473
+ if (!root) return;
474
+ mountTray(root);
475
+ }
476
+
477
+ if (document.readyState === "loading") {
478
+ document.addEventListener("DOMContentLoaded", boot);
479
+ } else {
480
+ boot();
481
+ }
482
+ })();
483
+
@@ -0,0 +1,18 @@
1
+ module Jekyll
2
+ module DiceTray
3
+ class AssetFile < Jekyll::StaticFile
4
+ def initialize(site, base, dir, name, source_path:)
5
+ super(site, base, dir, name)
6
+ @source_path = source_path
7
+ end
8
+
9
+ def write(dest)
10
+ dest_path = destination(dest)
11
+ FileUtils.mkdir_p(File.dirname(dest_path))
12
+ FileUtils.cp(@source_path, dest_path)
13
+ true
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,35 @@
1
+ module Jekyll
2
+ module DiceTray
3
+ class Generator < Jekyll::Generator
4
+ safe true
5
+ priority :low
6
+
7
+ def generate(site)
8
+ cfg = (site.config["dice_tray"] || {})
9
+ return if cfg["enabled"] == false
10
+
11
+ assets_path = cfg["assets_path"] || "/assets/jekyll-dice-tray"
12
+ assets_path = "/#{assets_path}" unless assets_path.start_with?("/")
13
+
14
+ asset_dir = File.expand_path("../../../assets/jekyll-dice-tray", __dir__)
15
+
16
+ files = {
17
+ "dice_tray.js" => File.join(asset_dir, "dice_tray.js"),
18
+ "dice_tray.css" => File.join(asset_dir, "dice_tray.css"),
19
+ }
20
+
21
+ files.each do |name, source_path|
22
+ next unless File.file?(source_path)
23
+ site.static_files << AssetFile.new(
24
+ site,
25
+ site.source,
26
+ assets_path.sub(%r{\A/}, ""),
27
+ name,
28
+ source_path: source_path
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,29 @@
1
+ module Jekyll
2
+ module DiceTray
3
+ module Hooks
4
+ def self.register!
5
+ Jekyll::Hooks.register(%i[pages documents], :post_render) do |doc|
6
+ site = doc.site
7
+ cfg = (site.config["dice_tray"] || {})
8
+ next if cfg["enabled"] == false
9
+ next unless doc.respond_to?(:output_ext) && doc.output_ext == ".html"
10
+
11
+ assets_path = cfg["assets_path"] || "/assets/jekyll-dice-tray"
12
+ assets_path = "/#{assets_path}" unless assets_path.start_with?("/")
13
+
14
+ begin
15
+ out = doc.output.to_s
16
+ out = HtmlRewriter.rewrite(out) if cfg.fetch("link_dice_in_markdown", true)
17
+ out = HtmlRewriter.inject_tray(out, assets_path: assets_path) if cfg.fetch("inject_tray", true)
18
+ doc.output = out
19
+ rescue StandardError => e
20
+ Jekyll.logger.warn("jekyll-dice-tray:", "Failed to process #{doc.relative_path}: #{e.class}: #{e.message}")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ Jekyll::DiceTray::Hooks.register!
29
+
@@ -0,0 +1,211 @@
1
+ module Jekyll
2
+ module DiceTray
3
+ module HtmlRewriter
4
+ DICE_RE = /
5
+ (?<![A-Za-z0-9_])
6
+ (?:
7
+ (?:\d{1,3})?d\d{1,4}
8
+ (?:\s*[+-]\s*\d{1,5})?
9
+ (?:
10
+ \s*[+-]\s*(?:\d{1,3})?d\d{1,4}
11
+ (?:\s*[+-]\s*\d{1,5})?
12
+ )?
13
+ )
14
+ (?![A-Za-z0-9_])
15
+ /x
16
+
17
+ THAC0_WORD_RE = /THAC0/i
18
+
19
+ BRACKET_INNER_RE = /
20
+ [+-]?
21
+ \d{1,5}
22
+ (?:\s*[+-]\s*\d{1,5})*
23
+ /x
24
+
25
+ # "THAC0 18 [+1]" in one run of text — link the inner "+1", roll d20+1.
26
+ THAC0_INLINE_RE = /
27
+ THAC0\s*:?\s*
28
+ \d{1,2}
29
+ \s*
30
+ \[
31
+ \s*
32
+ (?<mod>#{BRACKET_INNER_RE})
33
+ \s*
34
+ \]
35
+ /ix
36
+
37
+ # "18 [+1]" in a THAC0 table row or under a THAC0 column header.
38
+ THAC0_VALUE_BRACKET_RE = /
39
+ (?<!\d)
40
+ \d{1,2}
41
+ \s*
42
+ \[
43
+ \s*
44
+ (?<mod>#{BRACKET_INNER_RE})
45
+ \s*
46
+ \]
47
+ /x
48
+
49
+ SKIP_ANCESTORS = %w[pre code a script style textarea].freeze
50
+
51
+ def self.bracket_mod_to_roll_expr(inner)
52
+ compact = inner.gsub(/\s+/, "")
53
+ mod = 0
54
+ compact.scan(/[+-]?\d+/) { |term| mod += term.to_i }
55
+ return "d20" if mod.zero?
56
+
57
+ mod.positive? ? "d20+#{mod}" : "d20#{mod}"
58
+ end
59
+
60
+ def self.thac0_row_context?(node)
61
+ tr = node.ancestors.find { |a| a.element? && a.name == "tr" }
62
+ tr && tr.text.match?(THAC0_WORD_RE)
63
+ end
64
+
65
+ def self.thac0_table_context?(node)
66
+ return false unless node.ancestors.any? { |a| a.element? && a.name == "td" }
67
+
68
+ table = node.ancestors.find { |a| a.element? && a.name == "table" }
69
+ return false unless table
70
+
71
+ caption = table.at_css("caption")
72
+ return true if caption&.text&.match?(THAC0_WORD_RE)
73
+
74
+ first_row = table.at_css("tr")
75
+ first_row && first_row.text.match?(THAC0_WORD_RE)
76
+ end
77
+
78
+ def self.thac0_bracket_context?(node)
79
+ thac0_row_context?(node) || thac0_table_context?(node)
80
+ end
81
+
82
+ def self.bracket_mod_hit(text, m)
83
+ {
84
+ start: m.begin(0),
85
+ end: m.end(0),
86
+ expr: bracket_mod_to_roll_expr(m[:mod]),
87
+ label_start: m.begin(:mod),
88
+ label_end: m.end(:mod),
89
+ prefix: text[m.begin(0)...m.begin(:mod)],
90
+ suffix: text[m.end(:mod)...m.end(0)],
91
+ }
92
+ end
93
+
94
+ def self.collect_roll_matches(text, node)
95
+ matches = []
96
+
97
+ text.to_enum(:scan, DICE_RE).each do
98
+ m = Regexp.last_match
99
+ matches << {
100
+ start: m.begin(0),
101
+ end: m.end(0),
102
+ expr: m[0],
103
+ label_start: m.begin(0),
104
+ label_end: m.end(0),
105
+ }
106
+ end
107
+
108
+ text.to_enum(:scan, THAC0_INLINE_RE).each do
109
+ matches << bracket_mod_hit(text, Regexp.last_match)
110
+ end
111
+
112
+ if thac0_bracket_context?(node)
113
+ text.to_enum(:scan, THAC0_VALUE_BRACKET_RE).each do
114
+ matches << bracket_mod_hit(text, Regexp.last_match)
115
+ end
116
+ end
117
+
118
+ matches.sort_by! { |hit| hit[:start] }
119
+ accepted = []
120
+ matches.each do |hit|
121
+ next if accepted.any? { |prev| hit[:start] < prev[:end] && hit[:end] > prev[:start] }
122
+
123
+ accepted << hit
124
+ end
125
+ accepted
126
+ end
127
+
128
+ def self.rewrite(html)
129
+ frag = Nokogiri::HTML::DocumentFragment.parse(html)
130
+
131
+ frag.traverse do |node|
132
+ next unless node.text?
133
+ next if node.content.nil? || node.content.empty?
134
+ next if node.ancestors.any? { |a| SKIP_ANCESTORS.include?(a.name) }
135
+
136
+ text = node.content
137
+ hits = collect_roll_matches(text, node)
138
+ next if hits.empty?
139
+
140
+ new_nodes = []
141
+ last = 0
142
+
143
+ hits.each do |hit|
144
+ start_idx = hit[:start]
145
+ end_idx = hit[:end]
146
+
147
+ new_nodes << Nokogiri::XML::Text.new(text[last...start_idx], frag.document) if start_idx > last
148
+
149
+ if hit[:prefix]
150
+ new_nodes << Nokogiri::XML::Text.new(hit[:prefix], frag.document) if !hit[:prefix].empty?
151
+
152
+ a = Nokogiri::XML::Node.new("a", frag.document)
153
+ a["href"] = "#"
154
+ a["class"] = "dice-tray-roll"
155
+ a["data-dice"] = hit[:expr]
156
+ a.content = text[hit[:label_start]...hit[:label_end]]
157
+ new_nodes << a
158
+
159
+ new_nodes << Nokogiri::XML::Text.new(hit[:suffix], frag.document) if !hit[:suffix].empty?
160
+ else
161
+ a = Nokogiri::XML::Node.new("a", frag.document)
162
+ a["href"] = "#"
163
+ a["class"] = "dice-tray-roll"
164
+ a["data-dice"] = hit[:expr]
165
+ a.content = text[hit[:label_start]...hit[:label_end]]
166
+ new_nodes << a
167
+ end
168
+
169
+ last = end_idx
170
+ end
171
+
172
+ new_nodes << Nokogiri::XML::Text.new(text[last..], frag.document) if last < text.length
173
+
174
+ node.replace(new_nodes.map(&:to_html).join)
175
+ end
176
+
177
+ frag.to_html
178
+ end
179
+
180
+ def self.inject_tray(html, assets_path:)
181
+ return html if html.include?('data-dice-tray-root="true"')
182
+
183
+ tray = <<~HTML
184
+ <div id="jekyll-dice-tray" data-dice-tray-root="true" aria-live="polite">
185
+ <div class="jdt-header">
186
+ <button type="button" class="jdt-toggle" aria-expanded="false" title="Toggle dice tray">Dice</button>
187
+ </div>
188
+ <div class="jdt-body" hidden>
189
+ <div class="jdt-clue">Type <code>1d20+5</code> or <code>/help</code>, then press Enter.</div>
190
+ <div class="jdt-log" role="log" aria-label="Dice roll log"></div>
191
+ <input class="jdt-input" type="text" inputmode="text" autocomplete="off" spellcheck="false"
192
+ placeholder="Roll: 1d6, d4, 2d8+1, /help" />
193
+ </div>
194
+ </div>
195
+ HTML
196
+
197
+ tags = <<~HTML
198
+ <link rel="stylesheet" href="#{assets_path}/dice_tray.css" />
199
+ <script defer src="#{assets_path}/dice_tray.js"></script>
200
+ HTML
201
+
202
+ if html.include?("</body>")
203
+ html.sub("</body>", "#{tray}\n#{tags}\n</body>")
204
+ else
205
+ "#{html}\n#{tray}\n#{tags}\n"
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+
@@ -0,0 +1,6 @@
1
+ module Jekyll
2
+ module DiceTray
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
6
+
@@ -0,0 +1,9 @@
1
+ require "jekyll"
2
+ require "nokogiri"
3
+
4
+ require_relative "jekyll/dice_tray/version"
5
+ require_relative "jekyll/dice_tray/asset_file"
6
+ require_relative "jekyll/dice_tray/generator"
7
+ require_relative "jekyll/dice_tray/html_rewriter"
8
+ require_relative "jekyll/dice_tray/hooks"
9
+
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-dice-tray
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - directsun
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jekyll
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.7'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '5.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '3.7'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '5.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: nokogiri
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '1.14'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '1.14'
46
+ email: []
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - LICENSE
52
+ - README.md
53
+ - assets/jekyll-dice-tray/dice_tray.css
54
+ - assets/jekyll-dice-tray/dice_tray.js
55
+ - lib/jekyll-dice-tray.rb
56
+ - lib/jekyll/dice_tray/asset_file.rb
57
+ - lib/jekyll/dice_tray/generator.rb
58
+ - lib/jekyll/dice_tray/hooks.rb
59
+ - lib/jekyll/dice_tray/html_rewriter.rb
60
+ - lib/jekyll/dice_tray/version.rb
61
+ homepage: https://github.com/sunflowermans/dice-tray
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '3.0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.6.9
80
+ specification_version: 4
81
+ summary: Jekyll plugin that adds an overlay dice tray and clickable dice rolls.
82
+ test_files: []