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 +7 -0
- data/LICENSE +21 -0
- data/README.md +37 -0
- data/assets/jekyll-dice-tray/dice_tray.css +183 -0
- data/assets/jekyll-dice-tray/dice_tray.js +483 -0
- data/lib/jekyll/dice_tray/asset_file.rb +18 -0
- data/lib/jekyll/dice_tray/generator.rb +35 -0
- data/lib/jekyll/dice_tray/hooks.rb +29 -0
- data/lib/jekyll/dice_tray/html_rewriter.rb +211 -0
- data/lib/jekyll/dice_tray/version.rb +6 -0
- data/lib/jekyll-dice-tray.rb +9 -0
- metadata +82 -0
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,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: []
|