jekyll-dice-tray 0.3.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f1058c6701f813bd5bef233707af9e65bc2b1876de92db04675158a71a0adab
4
- data.tar.gz: 6550e68c8ef38aaefebffb822c85c4090402fbfda41585f3ece54f36a1bddf7b
3
+ metadata.gz: 1e3b5e6ffc89df4600f4c4a25d765e3bc5efcd96d3671817cfc19c0edf540e7f
4
+ data.tar.gz: eeb98f0a9a72e5caac9dfa0608f402cfefb90c914c0ecacf2dc170d6484decd8
5
5
  SHA512:
6
- metadata.gz: 9b5f1d58cfcced42951de3dc03660c4c1d683a7a8523af220d1817c0cf610935383a48f4e20b58409920cce03b2654040ce8a6e7ae753eb2971f5972576ad2fe
7
- data.tar.gz: ecf24959ae7c875c23923be6b231ea494fc4d7fa9d0837f8567a1b161e9120f9a48de4fb76e58522a3751697213469a6bf92f6abdaf25491792ebdd99a7b4627
6
+ metadata.gz: 76359d77b57187df74358db7c49116928b201ce19ffa3130e3af0584087cce008c220baacbea6185f2e5a39686c27b4c78c6010702a9d2576f258158a9df36b7
7
+ data.tar.gz: 0b6e4439a8265f518ee925cb7f497fe5c3daa6c2dfd180d67ba8e7dcbefab68495f25bb50805b4d594d2cf777b3f32ed4a9172c5564c512de072db5d80db1ccd
data/README.md CHANGED
@@ -4,7 +4,7 @@ A Jekyll plugin gem that:
4
4
 
5
5
  - Adds an overlay dice tray that accepts dice rolling input
6
6
  - Recognizes dice expressions in rendered pages like `d4`, `1d6`, `1d20+1` and turns them into clickable links that roll in the tray
7
- - Clicking a lookup-table die in the header row (e.g. `1d12`) rolls and shows the matching row
7
+ - Clicking a lookup-table die (any `dN`) rolls and shows the matching entry — supports any number of side-by-side key/value column groups, ranges like `3–12`, and tables with or without headers. One die in the header searches the whole table; multiple dice each search their own column group
8
8
  - Persists input history, result history, minimize status
9
9
 
10
10
  https://github.com/user-attachments/assets/6ef8d788-a1ee-49d4-ab26-f63899bb4a10
@@ -163,7 +163,8 @@
163
163
  text-underline-offset: 2px;
164
164
  }
165
165
 
166
- #jekyll-dice-tray .jdt-table-value a.dice-tray-roll {
166
+ #jekyll-dice-tray .jdt-table-value a.dice-tray-roll,
167
+ #jekyll-dice-tray .jdt-table-header a.dice-tray-roll {
167
168
  color: #ff0000;
168
169
  }
169
170
 
@@ -171,7 +172,8 @@
171
172
  color: inherit;
172
173
  }
173
174
 
174
- #jekyll-dice-tray .jdt-table-value a.dice-tray-roll:visited {
175
+ #jekyll-dice-tray .jdt-table-value a.dice-tray-roll:visited,
176
+ #jekyll-dice-tray .jdt-table-header a.dice-tray-roll:visited {
175
177
  color: #ff0000;
176
178
  }
177
179
 
@@ -46,8 +46,9 @@
46
46
  function parseExpr(input) {
47
47
  var s = String(input || "").trim();
48
48
  if (!s) return { kind: "empty" };
49
- if (s === "/help") return { kind: "help" };
50
- if (s === "/clear") return { kind: "clear" };
49
+ if (s === "/help" || s === "/h") return { kind: "help" };
50
+ if (s === "/clear" || s === "/c") return { kind: "clear" };
51
+ if (s === "/version" || s === "/v") return { kind: "version" };
51
52
 
52
53
  // Allow whitespace variations like "d20 + 2"
53
54
  var compact = s.replace(/\s+/g, "");
@@ -158,21 +159,7 @@
158
159
  return node && node.innerHTML ? node.innerHTML.trim() : "";
159
160
  }
160
161
 
161
- function findTableHeaderRow(table) {
162
- if (!table) return null;
163
- var thead = table.querySelector("thead");
164
- if (thead) {
165
- var theadRows = thead.querySelectorAll("tr");
166
- if (theadRows.length) return theadRows[theadRows.length - 1];
167
- }
168
- var rows = table.querySelectorAll("tr");
169
- for (var i = 0; i < rows.length; i++) {
170
- if (rows[i].querySelector("th")) return rows[i];
171
- }
172
- return table.querySelector("tr");
173
- }
174
-
175
- function parseFirstColumnEntry(text) {
162
+ function parseKeyEntry(text) {
176
163
  if (!text) return null;
177
164
  var normalized = text.replace(/[\u2013\u2014]/g, "-");
178
165
  if (/^\d+$/.test(normalized)) {
@@ -187,72 +174,270 @@
187
174
  return null;
188
175
  }
189
176
 
190
- function firstColumnEntry(row) {
191
- if (!row) return null;
192
- var cell = row.querySelector("td, th");
193
- if (!cell) return null;
194
- return parseFirstColumnEntry(normalizeCellText(cell));
177
+ function rowCells(row) {
178
+ if (!row) return [];
179
+ return Array.prototype.slice.call(row.querySelectorAll("td, th"));
180
+ }
181
+
182
+ function cellColumnIndex(row, cell) {
183
+ var cells = rowCells(row);
184
+ for (var i = 0; i < cells.length; i++) {
185
+ if (cells[i] === cell) return i;
186
+ }
187
+ return -1;
195
188
  }
196
189
 
197
- function isLookupTable(table, headerRow) {
198
- if (!table || !headerRow) return false;
190
+ function rowLooksLikeHeader(row) {
191
+ if (!row) return false;
192
+ if (row.querySelector("th")) return true;
193
+ var cells = rowCells(row);
194
+ if (!cells.length) return false;
195
+ var keyCount = 0;
196
+ for (var i = 0; i < cells.length; i++) {
197
+ if (parseKeyEntry(normalizeCellText(cells[i]))) keyCount++;
198
+ }
199
+ // Data rows start with roll keys; header rows are labels / dice expr.
200
+ return keyCount === 0;
201
+ }
202
+
203
+ function findTableHeaderRow(table) {
204
+ if (!table) return null;
205
+ var thead = table.querySelector("thead");
206
+ if (thead) {
207
+ var theadRows = thead.querySelectorAll("tr");
208
+ if (theadRows.length) {
209
+ var head = theadRows[theadRows.length - 1];
210
+ // Markdown always wraps the first row in <thead>; skip if it's really data.
211
+ if (!parseKeyEntry(normalizeCellText(rowCells(head)[0] || null))) return head;
212
+ }
213
+ }
199
214
  var rows = table.querySelectorAll("tr");
200
- var matches = 0;
201
215
  for (var i = 0; i < rows.length; i++) {
202
- if (rows[i] === headerRow) continue;
203
- if (firstColumnEntry(rows[i]) !== null) matches++;
216
+ if (rows[i].querySelector("th") && rowLooksLikeHeader(rows[i])) return rows[i];
204
217
  }
205
- return matches >= 2;
218
+ if (rows.length && rowLooksLikeHeader(rows[0])) return rows[0];
219
+ return null;
206
220
  }
207
221
 
208
- function findLookupRow(table, headerRow, rollTotal) {
222
+ function eachDataRow(table, headerRow, fn) {
209
223
  var rows = table.querySelectorAll("tr");
210
- var rangeRow = null;
211
224
  for (var i = 0; i < rows.length; i++) {
212
- var row = rows[i];
213
- if (row === headerRow) continue;
214
- var entry = firstColumnEntry(row);
215
- if (!entry) continue;
216
- if (entry.kind === "exact" && entry.value === rollTotal) return row;
217
- if (entry.kind === "range" && !rangeRow && rollTotal >= entry.min && rollTotal <= entry.max) {
218
- rangeRow = row;
225
+ if (rows[i] === headerRow) continue;
226
+ fn(rows[i]);
227
+ }
228
+ }
229
+
230
+ // Infer key columns from data: any column that holds a roll number/range in enough rows.
231
+ // Supports 1..N side-by-side (key, value...) groups for any die size.
232
+ function inferKeyColumns(table, headerRow) {
233
+ var counts = [];
234
+ var rowCount = 0;
235
+ eachDataRow(table, headerRow, function (row) {
236
+ var cells = rowCells(row);
237
+ rowCount++;
238
+ for (var i = 0; i < cells.length; i++) {
239
+ if (!counts[i]) counts[i] = 0;
240
+ if (parseKeyEntry(normalizeCellText(cells[i]))) counts[i]++;
241
+ }
242
+ });
243
+ if (rowCount < 1) return [];
244
+ var minHits = Math.min(2, rowCount);
245
+ var keys = [];
246
+ for (var c = 0; c < counts.length; c++) {
247
+ if ((counts[c] || 0) >= minHits) keys.push(c);
248
+ }
249
+ return keys;
250
+ }
251
+
252
+ function buildColumnGroups(keyColumns, columnCount) {
253
+ var groups = [];
254
+ for (var k = 0; k < keyColumns.length; k++) {
255
+ var keyIndex = keyColumns[k];
256
+ var nextKey = k + 1 < keyColumns.length ? keyColumns[k + 1] : columnCount;
257
+ var valueIndices = [];
258
+ for (var v = keyIndex + 1; v < nextKey; v++) valueIndices.push(v);
259
+ groups.push({ keyIndex: keyIndex, valueIndices: valueIndices });
260
+ }
261
+ return groups;
262
+ }
263
+
264
+ function tableColumnCount(table, headerRow) {
265
+ var max = headerRow ? rowCells(headerRow).length : 0;
266
+ eachDataRow(table, headerRow, function (row) {
267
+ max = Math.max(max, rowCells(row).length);
268
+ });
269
+ return max;
270
+ }
271
+
272
+ function collectRowEntriesForGroups(row, groups, allowedKeyIndices) {
273
+ var cells = rowCells(row);
274
+ var entries = [];
275
+ var allowAll = !allowedKeyIndices || !allowedKeyIndices.length;
276
+ for (var g = 0; g < groups.length; g++) {
277
+ var group = groups[g];
278
+ if (!allowAll && allowedKeyIndices.indexOf(group.keyIndex) === -1) continue;
279
+ var keyCell = cells[group.keyIndex];
280
+ if (!keyCell) continue;
281
+ var key = parseKeyEntry(normalizeCellText(keyCell));
282
+ if (!key) continue;
283
+ var valueCells = [];
284
+ var valueIndices = [];
285
+ for (var vi = 0; vi < group.valueIndices.length; vi++) {
286
+ var idx = group.valueIndices[vi];
287
+ if (!cells[idx]) continue;
288
+ valueCells.push(cells[idx]);
289
+ valueIndices.push(idx);
290
+ }
291
+ entries.push({
292
+ key: key,
293
+ keyIndex: group.keyIndex,
294
+ valueCells: valueCells,
295
+ valueIndices: valueIndices,
296
+ });
297
+ }
298
+ return entries;
299
+ }
300
+
301
+ function isLookupTable(table, headerRow, groups) {
302
+ if (!table || !groups || !groups.length) return false;
303
+ var matches = 0;
304
+ eachDataRow(table, headerRow, function (row) {
305
+ matches += collectRowEntriesForGroups(row, groups, null).length;
306
+ });
307
+ return matches >= 2;
308
+ }
309
+
310
+ function findLookupMatch(table, headerRow, groups, rollTotal, allowedKeyIndices) {
311
+ var exact = null;
312
+ var range = null;
313
+ eachDataRow(table, headerRow, function (row) {
314
+ var entries = collectRowEntriesForGroups(row, groups, allowedKeyIndices);
315
+ for (var e = 0; e < entries.length; e++) {
316
+ var entry = entries[e];
317
+ if (entry.key.kind === "exact" && entry.key.value === rollTotal) {
318
+ if (!exact) exact = { row: row, entry: entry };
319
+ } else if (
320
+ entry.key.kind === "range" &&
321
+ !range &&
322
+ rollTotal >= entry.key.min &&
323
+ rollTotal <= entry.key.max
324
+ ) {
325
+ range = { row: row, entry: entry };
326
+ }
219
327
  }
328
+ });
329
+ return exact || range;
330
+ }
331
+
332
+ function headerDiceCount(headerRow) {
333
+ if (!headerRow) return 0;
334
+ return headerRow.querySelectorAll('a[data-dice]').length;
335
+ }
336
+
337
+ function groupForColumn(groups, columnIndex) {
338
+ for (var g = 0; g < groups.length; g++) {
339
+ var group = groups[g];
340
+ if (columnIndex === group.keyIndex) return group;
341
+ if (group.valueIndices.indexOf(columnIndex) !== -1) return group;
220
342
  }
221
- return rangeRow;
343
+ return null;
344
+ }
345
+
346
+ // One die in the header (e.g. d100 over many pairs) => search every group.
347
+ // Several dice (e.g. d10 | Weather | d10 | Terrain | d6 | Mood) => only the clicked group.
348
+ function allowedKeyIndicesForClick(headerRow, clickCell, groups) {
349
+ if (!headerRow || !clickCell || !groups.length) return null;
350
+ if (headerDiceCount(headerRow) <= 1) return null;
351
+ var col = cellColumnIndex(headerRow, clickCell);
352
+ if (col < 0) return null;
353
+ var group = groupForColumn(groups, col);
354
+ return group ? [group.keyIndex] : null;
222
355
  }
223
356
 
224
357
  function getLookupTableContext(anchor, rollTotal) {
225
358
  if (!anchor || !anchor.closest || rollTotal == null) return null;
226
- var cell = anchor.closest("td, th");
227
- if (!cell) return null;
228
- var clickRow = cell.closest("tr");
229
- if (!clickRow) return null;
230
- var table = clickRow.closest("table");
231
- if (!table) return null;
232
359
 
233
- var headerRow = findTableHeaderRow(table);
234
- if (!headerRow || !headerRow.contains(cell)) return null;
235
- if (!isLookupTable(table, headerRow)) return null;
360
+ var cell = null;
361
+ var table = null;
362
+ var headerRow = null;
363
+ var allowedKeys = null;
364
+ var sourceTable = anchor._jdtSourceTable;
365
+
366
+ if (sourceTable && sourceTable.querySelector) {
367
+ // Re-roll from a previous table result in the tray.
368
+ table = sourceTable;
369
+ headerRow = findTableHeaderRow(table);
370
+ allowedKeys = anchor._jdtAllowedKeyIndices || null;
371
+ } else {
372
+ cell = anchor.closest("td, th");
373
+ if (!cell) return null;
374
+ table = cell.closest("table");
375
+ if (!table) return null;
376
+ headerRow = findTableHeaderRow(table);
377
+ // With a header, only header-cell dice trigger lookup (data cells may contain other dice).
378
+ // Without a header, any in-table die can trigger lookup.
379
+ if (headerRow && !headerRow.contains(cell)) return null;
380
+ }
236
381
 
237
- var matchedRow = findLookupRow(table, headerRow, rollTotal);
238
- if (!matchedRow) return null;
382
+ var keyColumns = inferKeyColumns(table, headerRow);
383
+ if (!keyColumns.length) return null;
384
+ var groups = buildColumnGroups(keyColumns, tableColumnCount(table, headerRow));
385
+ if (!isLookupTable(table, headerRow, groups)) return null;
239
386
 
240
- var headerCells = Array.prototype.slice.call(headerRow.querySelectorAll("td, th"));
241
- var valueCells = Array.prototype.slice.call(matchedRow.querySelectorAll("td, th"));
242
- var columns = [];
387
+ if (allowedKeys == null && cell && headerRow) {
388
+ allowedKeys = allowedKeyIndicesForClick(headerRow, cell, groups);
389
+ }
390
+ var matched = findLookupMatch(table, headerRow, groups, rollTotal, allowedKeys);
391
+ if (!matched) return null;
243
392
 
244
- for (var c = 0; c < valueCells.length; c++) {
245
- var headerCell = headerCells[c];
246
- var valueCell = valueCells[c];
393
+ var headerCells = headerRow ? rowCells(headerRow) : [];
394
+ var entry = matched.entry;
395
+ var columns = [];
396
+ var indices = [entry.keyIndex].concat(entry.valueIndices);
397
+ var cells = [rowCells(matched.row)[entry.keyIndex]].concat(entry.valueCells);
398
+ var diceLabel = String(anchor.getAttribute("data-dice") || normalizeCellText(anchor) || "").trim();
399
+
400
+ for (var c = 0; c < cells.length; c++) {
401
+ var valueCell = cells[c];
402
+ if (!valueCell) continue;
403
+ var headerCell = headerCells[indices[c]];
404
+ var headerText = headerCell ? normalizeCellText(headerCell) : "";
405
+ var headerHtml = headerCell ? cellInnerHtml(headerCell) : "";
406
+ // Blank/whitespace key-column headers (common in side-by-side tables) inherit the die notation.
407
+ if (!headerText && c === 0 && diceLabel) {
408
+ headerText = diceLabel;
409
+ headerHtml = "";
410
+ } else if (!headerText) {
411
+ headerHtml = "";
412
+ }
247
413
  columns.push({
248
- header: headerCell ? normalizeCellText(headerCell) : "",
249
- header_html: headerCell ? cellInnerHtml(headerCell) : "",
414
+ header: headerText,
415
+ header_html: headerHtml,
250
416
  value: normalizeCellText(valueCell),
251
417
  value_html: cellInnerHtml(valueCell),
418
+ // Key-column die prefix is clickable for a table re-roll.
419
+ reroll_expr: c === 0 && diceLabel ? diceLabel : "",
252
420
  });
253
421
  }
254
422
 
255
- return columns.length ? { columns: columns } : null;
423
+ return columns.length
424
+ ? { columns: columns, _sourceTable: table, _allowedKeyIndices: allowedKeys }
425
+ : null;
426
+ }
427
+
428
+ function serializableTableContext(tableContext) {
429
+ if (!tableContext || !tableContext.columns || !tableContext.columns.length) return null;
430
+ return {
431
+ columns: tableContext.columns.map(function (col) {
432
+ return {
433
+ header: col.header || "",
434
+ header_html: col.header_html || "",
435
+ value: col.value || "",
436
+ value_html: col.value_html || "",
437
+ reroll_expr: col.reroll_expr || "",
438
+ };
439
+ }),
440
+ };
256
441
  }
257
442
 
258
443
  function appendTableContext(entry, tableContext) {
@@ -262,10 +447,27 @@
262
447
  tableContext.columns.forEach(function (col) {
263
448
  if (!col || (!col.value && !col.value_html)) return;
264
449
  var line = el("div", { class: "jdt-table-row" });
265
- if (col.header || col.header_html) {
450
+ var headerText = col.header ? String(col.header).trim() : "";
451
+ if (headerText) {
266
452
  var headerSpan = el("span", { class: "jdt-table-header" });
267
- if (col.header_html) headerSpan.innerHTML = col.header_html;
268
- else headerSpan.textContent = col.header;
453
+ if (col.reroll_expr) {
454
+ var reroll = el(
455
+ "a",
456
+ { class: "dice-tray-roll", href: "#", "data-dice": col.reroll_expr },
457
+ headerText
458
+ );
459
+ if (tableContext._sourceTable && tableContext._sourceTable.querySelector) {
460
+ reroll._jdtSourceTable = tableContext._sourceTable;
461
+ }
462
+ if (tableContext._allowedKeyIndices) {
463
+ reroll._jdtAllowedKeyIndices = tableContext._allowedKeyIndices;
464
+ }
465
+ headerSpan.appendChild(reroll);
466
+ } else if (col.header_html) {
467
+ headerSpan.innerHTML = col.header_html;
468
+ } else {
469
+ headerSpan.textContent = headerText;
470
+ }
269
471
  line.appendChild(headerSpan);
270
472
  line.appendChild(el("span", { class: "jdt-table-sep" }, ": "));
271
473
  }
@@ -466,7 +668,7 @@
466
668
  success: success,
467
669
  time: timeStr,
468
670
  };
469
- if (tableContext) item.table_context = tableContext;
671
+ if (tableContext) item.table_context = serializableTableContext(tableContext);
470
672
  pushHistory(item);
471
673
  }
472
674
 
@@ -490,7 +692,7 @@
490
692
  if (skipHistory) return;
491
693
 
492
694
  var item = { kind: "roll", expr: expr, total: total, rolls: rolls, mod: mod, time: timeStr };
493
- if (tableContext) item.table_context = tableContext;
695
+ if (tableContext) item.table_context = serializableTableContext(tableContext);
494
696
  pushHistory(item);
495
697
  }
496
698
 
@@ -545,18 +747,24 @@
545
747
  mod: mod,
546
748
  time: timeStr,
547
749
  };
548
- if (tableContext) item.table_context = tableContext;
750
+ if (tableContext) item.table_context = serializableTableContext(tableContext);
549
751
  pushHistory(item);
550
752
  }
551
753
 
552
754
  function showHelp() {
553
755
  addSystemEntry(
554
756
  "Usage: 1d6, d4, 2d8+1, 2-in-6",
555
- "Click linked dice like 1d20+5, chance like 2-in-6 (roll d6, 1-2 succeed), or bracket modifiers like [+1] (rolls d20+1). Clicking a table die (e.g. 1d12) shows the matching row (first column: single number or range like 3-12). Commands: /help, /clear",
757
+ "Type the dice you want to roll, without a prefix. Click linked dice notation in a table to show the matching entry (number or range).",
758
+ "Commands: /help, /clear, /version",
556
759
  nowTime()
557
760
  );
558
761
  }
559
762
 
763
+ function showVersion() {
764
+ var version = root.getAttribute("data-version") || "unknown";
765
+ addSystemEntry("dice tray " + version, "", nowTime());
766
+ }
767
+
560
768
  function doRoll(raw, meta) {
561
769
  meta = meta || {};
562
770
  var anchor = meta.anchor || null;
@@ -564,6 +772,7 @@
564
772
  if (p.kind === "empty") return;
565
773
  if (p.kind === "help") return showHelp();
566
774
  if (p.kind === "clear") return clearStorageAndUi();
775
+ if (p.kind === "version") return showVersion();
567
776
  if (p.kind !== "roll" && p.kind !== "bestof2" && p.kind !== "chance") {
568
777
  addSystemEntry("Unrecognized roll: " + p.raw, "Try: 1d6, d4, 2-in-6, 2d8+1 or /help", nowTime());
569
778
  return;
@@ -200,7 +200,7 @@ module Jekyll
200
200
  return html if html.include?('data-dice-tray-root="true"')
201
201
 
202
202
  tray = <<~HTML
203
- <div id="jekyll-dice-tray" data-dice-tray-root="true" aria-live="polite">
203
+ <div id="jekyll-dice-tray" data-dice-tray-root="true" data-version="#{VERSION}" aria-live="polite">
204
204
  <div class="jdt-header">
205
205
  <button type="button" class="jdt-toggle" aria-expanded="false" title="Toggle dice tray">Dice</button>
206
206
  </div>
@@ -1,6 +1,6 @@
1
1
  module Jekyll
2
2
  module DiceTray
3
- VERSION = "0.3.1"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  end
6
6
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-dice-tray
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - directsun