jekyll-dice-tray 0.3.1 → 0.4.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: 8ee11a49a55ee16f40ee46a91b01a40d3074da6b6bc1cdde28b7bbe36b41386d
4
+ data.tar.gz: a35ce460b6c0181c03acc99d38916407eac12d1c6d89feae6412335302cedd64
5
5
  SHA512:
6
- metadata.gz: 9b5f1d58cfcced42951de3dc03660c4c1d683a7a8523af220d1817c0cf610935383a48f4e20b58409920cce03b2654040ce8a6e7ae753eb2971f5972576ad2fe
7
- data.tar.gz: ecf24959ae7c875c23923be6b231ea494fc4d7fa9d0837f8567a1b161e9120f9a48de4fb76e58522a3751697213469a6bf92f6abdaf25491792ebdd99a7b4627
6
+ metadata.gz: c754090c423cbd76a96b532e0bccc16d2cd2e76db5006fae17ed1766b5c8e19305a8838346640117de26bc6793e7c41021dbcf05ce61541095b97bd1dfde5edd
7
+ data.tar.gz: 03d2e7cdfaa42614d46fba9fac3eebf0722b6ba324def7a5c40d6f53576aadc0b4f73a17354218aa7ad508b002818c93e49db65f9b1170a37ce389ddbc26e3ea
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
 
@@ -158,21 +158,7 @@
158
158
  return node && node.innerHTML ? node.innerHTML.trim() : "";
159
159
  }
160
160
 
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) {
161
+ function parseKeyEntry(text) {
176
162
  if (!text) return null;
177
163
  var normalized = text.replace(/[\u2013\u2014]/g, "-");
178
164
  if (/^\d+$/.test(normalized)) {
@@ -187,72 +173,290 @@
187
173
  return null;
188
174
  }
189
175
 
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));
176
+ function rowCells(row) {
177
+ if (!row) return [];
178
+ return Array.prototype.slice.call(row.querySelectorAll("td, th"));
195
179
  }
196
180
 
197
- function isLookupTable(table, headerRow) {
198
- if (!table || !headerRow) return false;
181
+ function cellColumnIndex(row, cell) {
182
+ var cells = rowCells(row);
183
+ for (var i = 0; i < cells.length; i++) {
184
+ if (cells[i] === cell) return i;
185
+ }
186
+ return -1;
187
+ }
188
+
189
+ function rowLooksLikeHeader(row) {
190
+ if (!row) return false;
191
+ if (row.querySelector("th")) return true;
192
+ var cells = rowCells(row);
193
+ if (!cells.length) return false;
194
+ var keyCount = 0;
195
+ for (var i = 0; i < cells.length; i++) {
196
+ if (parseKeyEntry(normalizeCellText(cells[i]))) keyCount++;
197
+ }
198
+ // Data rows start with roll keys; header rows are labels / dice expr.
199
+ return keyCount === 0;
200
+ }
201
+
202
+ function findTableHeaderRow(table) {
203
+ if (!table) return null;
204
+ var thead = table.querySelector("thead");
205
+ if (thead) {
206
+ var theadRows = thead.querySelectorAll("tr");
207
+ if (theadRows.length) {
208
+ var head = theadRows[theadRows.length - 1];
209
+ // Markdown always wraps the first row in <thead>; skip if it's really data.
210
+ if (!parseKeyEntry(normalizeCellText(rowCells(head)[0] || null))) return head;
211
+ }
212
+ }
199
213
  var rows = table.querySelectorAll("tr");
200
- var matches = 0;
201
214
  for (var i = 0; i < rows.length; i++) {
202
- if (rows[i] === headerRow) continue;
203
- if (firstColumnEntry(rows[i]) !== null) matches++;
215
+ if (rows[i].querySelector("th") && rowLooksLikeHeader(rows[i])) return rows[i];
204
216
  }
205
- return matches >= 2;
217
+ if (rows.length && rowLooksLikeHeader(rows[0])) return rows[0];
218
+ return null;
206
219
  }
207
220
 
208
- function findLookupRow(table, headerRow, rollTotal) {
221
+ function eachDataRow(table, headerRow, fn) {
209
222
  var rows = table.querySelectorAll("tr");
210
- var rangeRow = null;
211
223
  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;
224
+ if (rows[i] === headerRow) continue;
225
+ fn(rows[i]);
226
+ }
227
+ }
228
+
229
+ // Infer key columns from data: any column that holds a roll number/range in enough rows.
230
+ // Supports 1..N side-by-side (key, value...) groups for any die size.
231
+ function inferKeyColumns(table, headerRow) {
232
+ var counts = [];
233
+ var rowCount = 0;
234
+ eachDataRow(table, headerRow, function (row) {
235
+ var cells = rowCells(row);
236
+ rowCount++;
237
+ for (var i = 0; i < cells.length; i++) {
238
+ if (!counts[i]) counts[i] = 0;
239
+ if (parseKeyEntry(normalizeCellText(cells[i]))) counts[i]++;
240
+ }
241
+ });
242
+ if (rowCount < 1) return [];
243
+ var minHits = Math.min(2, rowCount);
244
+ var keys = [];
245
+ for (var c = 0; c < counts.length; c++) {
246
+ if ((counts[c] || 0) >= minHits) keys.push(c);
247
+ }
248
+ return keys;
249
+ }
250
+
251
+ function buildColumnGroups(keyColumns, columnCount) {
252
+ var groups = [];
253
+ for (var k = 0; k < keyColumns.length; k++) {
254
+ var keyIndex = keyColumns[k];
255
+ var nextKey = k + 1 < keyColumns.length ? keyColumns[k + 1] : columnCount;
256
+ var valueIndices = [];
257
+ for (var v = keyIndex + 1; v < nextKey; v++) valueIndices.push(v);
258
+ groups.push({ keyIndex: keyIndex, valueIndices: valueIndices });
259
+ }
260
+ return groups;
261
+ }
262
+
263
+ function tableColumnCount(table, headerRow) {
264
+ var max = headerRow ? rowCells(headerRow).length : 0;
265
+ eachDataRow(table, headerRow, function (row) {
266
+ max = Math.max(max, rowCells(row).length);
267
+ });
268
+ return max;
269
+ }
270
+
271
+ function collectRowEntriesForGroups(row, groups, allowedKeyIndices) {
272
+ var cells = rowCells(row);
273
+ var entries = [];
274
+ var allowAll = !allowedKeyIndices || !allowedKeyIndices.length;
275
+ for (var g = 0; g < groups.length; g++) {
276
+ var group = groups[g];
277
+ if (!allowAll && allowedKeyIndices.indexOf(group.keyIndex) === -1) continue;
278
+ var keyCell = cells[group.keyIndex];
279
+ if (!keyCell) continue;
280
+ var key = parseKeyEntry(normalizeCellText(keyCell));
281
+ if (!key) continue;
282
+ var valueCells = [];
283
+ var valueIndices = [];
284
+ for (var vi = 0; vi < group.valueIndices.length; vi++) {
285
+ var idx = group.valueIndices[vi];
286
+ if (!cells[idx]) continue;
287
+ valueCells.push(cells[idx]);
288
+ valueIndices.push(idx);
219
289
  }
290
+ entries.push({
291
+ key: key,
292
+ keyIndex: group.keyIndex,
293
+ valueCells: valueCells,
294
+ valueIndices: valueIndices,
295
+ });
220
296
  }
221
- return rangeRow;
297
+ return entries;
298
+ }
299
+
300
+ function isLookupTable(table, headerRow, groups) {
301
+ if (!table || !groups || !groups.length) return false;
302
+ var matches = 0;
303
+ eachDataRow(table, headerRow, function (row) {
304
+ matches += collectRowEntriesForGroups(row, groups, null).length;
305
+ });
306
+ return matches >= 2;
307
+ }
308
+
309
+ function findLookupMatch(table, headerRow, groups, rollTotal, allowedKeyIndices) {
310
+ var exact = null;
311
+ var range = null;
312
+ eachDataRow(table, headerRow, function (row) {
313
+ var entries = collectRowEntriesForGroups(row, groups, allowedKeyIndices);
314
+ for (var e = 0; e < entries.length; e++) {
315
+ var entry = entries[e];
316
+ if (entry.key.kind === "exact" && entry.key.value === rollTotal) {
317
+ if (!exact) exact = { row: row, entry: entry };
318
+ } else if (
319
+ entry.key.kind === "range" &&
320
+ !range &&
321
+ rollTotal >= entry.key.min &&
322
+ rollTotal <= entry.key.max
323
+ ) {
324
+ range = { row: row, entry: entry };
325
+ }
326
+ }
327
+ });
328
+ return exact || range;
329
+ }
330
+
331
+ function headerDiceCount(headerRow) {
332
+ if (!headerRow) return 0;
333
+ return headerRow.querySelectorAll('a[data-dice]').length;
334
+ }
335
+
336
+ function groupForColumn(groups, columnIndex) {
337
+ for (var g = 0; g < groups.length; g++) {
338
+ var group = groups[g];
339
+ if (columnIndex === group.keyIndex) return group;
340
+ if (group.valueIndices.indexOf(columnIndex) !== -1) return group;
341
+ }
342
+ return null;
343
+ }
344
+
345
+ // One die in the header (e.g. d100 over many pairs) => search every group.
346
+ // Several dice (e.g. d10 | Weather | d10 | Terrain | d6 | Mood) => only the clicked group.
347
+ function allowedKeyIndicesForClick(headerRow, clickCell, groups) {
348
+ if (!headerRow || !clickCell || !groups.length) return null;
349
+ if (headerDiceCount(headerRow) <= 1) return null;
350
+ var col = cellColumnIndex(headerRow, clickCell);
351
+ if (col < 0) return null;
352
+ var group = groupForColumn(groups, col);
353
+ return group ? [group.keyIndex] : null;
354
+ }
355
+
356
+ function findFollowingTable(node) {
357
+ var elNode = node;
358
+ while (elNode && elNode !== document.body) {
359
+ var sib = elNode.nextElementSibling;
360
+ while (sib) {
361
+ if (sib.tagName === "TABLE") return sib;
362
+ var nested = sib.querySelector && sib.querySelector("table");
363
+ if (nested) return nested;
364
+ sib = sib.nextElementSibling;
365
+ }
366
+ elNode = elNode.parentElement;
367
+ }
368
+ return null;
222
369
  }
223
370
 
224
371
  function getLookupTableContext(anchor, rollTotal) {
225
372
  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
373
 
233
- var headerRow = findTableHeaderRow(table);
234
- if (!headerRow || !headerRow.contains(cell)) return null;
235
- if (!isLookupTable(table, headerRow)) return null;
374
+ var cell = null;
375
+ var table = null;
376
+ var headerRow = null;
377
+ var allowedKeys = null;
378
+ var sourceTable = anchor._jdtSourceTable;
379
+
380
+ if (sourceTable && sourceTable.querySelector) {
381
+ // Re-roll from a previous table result in the tray.
382
+ table = sourceTable;
383
+ headerRow = findTableHeaderRow(table);
384
+ allowedKeys = anchor._jdtAllowedKeyIndices || null;
385
+ } else {
386
+ cell = anchor.closest("td, th");
387
+ if (cell) {
388
+ table = cell.closest("table");
389
+ if (!table) return null;
390
+ headerRow = findTableHeaderRow(table);
391
+ // With a header, only header-cell dice trigger lookup (data cells may contain other dice).
392
+ // Without a header, any in-table die can trigger lookup.
393
+ if (headerRow && !headerRow.contains(cell)) return null;
394
+ } else {
395
+ table = findFollowingTable(anchor);
396
+ if (!table) return null;
397
+ headerRow = findTableHeaderRow(table);
398
+ }
399
+ }
400
+
401
+ var keyColumns = inferKeyColumns(table, headerRow);
402
+ if (!keyColumns.length) return null;
403
+ var groups = buildColumnGroups(keyColumns, tableColumnCount(table, headerRow));
404
+ if (!isLookupTable(table, headerRow, groups)) return null;
236
405
 
237
- var matchedRow = findLookupRow(table, headerRow, rollTotal);
238
- if (!matchedRow) return null;
406
+ if (allowedKeys == null && cell && headerRow) {
407
+ allowedKeys = allowedKeyIndicesForClick(headerRow, cell, groups);
408
+ }
409
+ var matched = findLookupMatch(table, headerRow, groups, rollTotal, allowedKeys);
410
+ if (!matched) return null;
239
411
 
240
- var headerCells = Array.prototype.slice.call(headerRow.querySelectorAll("td, th"));
241
- var valueCells = Array.prototype.slice.call(matchedRow.querySelectorAll("td, th"));
412
+ var headerCells = headerRow ? rowCells(headerRow) : [];
413
+ var entry = matched.entry;
242
414
  var columns = [];
243
-
244
- for (var c = 0; c < valueCells.length; c++) {
245
- var headerCell = headerCells[c];
246
- var valueCell = valueCells[c];
415
+ var indices = [entry.keyIndex].concat(entry.valueIndices);
416
+ var cells = [rowCells(matched.row)[entry.keyIndex]].concat(entry.valueCells);
417
+ var diceLabel = String(anchor.getAttribute("data-dice") || normalizeCellText(anchor) || "").trim();
418
+
419
+ for (var c = 0; c < cells.length; c++) {
420
+ var valueCell = cells[c];
421
+ if (!valueCell) continue;
422
+ var headerCell = headerCells[indices[c]];
423
+ var headerText = headerCell ? normalizeCellText(headerCell) : "";
424
+ var headerHtml = headerCell ? cellInnerHtml(headerCell) : "";
425
+ // Blank/whitespace key-column headers (common in side-by-side tables) inherit the die notation.
426
+ if (!headerText && c === 0 && diceLabel) {
427
+ headerText = diceLabel;
428
+ headerHtml = "";
429
+ } else if (!headerText) {
430
+ headerHtml = "";
431
+ }
247
432
  columns.push({
248
- header: headerCell ? normalizeCellText(headerCell) : "",
249
- header_html: headerCell ? cellInnerHtml(headerCell) : "",
433
+ header: headerText,
434
+ header_html: headerHtml,
250
435
  value: normalizeCellText(valueCell),
251
436
  value_html: cellInnerHtml(valueCell),
437
+ // Key-column die prefix is clickable for a table re-roll.
438
+ reroll_expr: c === 0 && diceLabel ? diceLabel : "",
252
439
  });
253
440
  }
254
441
 
255
- return columns.length ? { columns: columns } : null;
442
+ return columns.length
443
+ ? { columns: columns, _sourceTable: table, _allowedKeyIndices: allowedKeys }
444
+ : null;
445
+ }
446
+
447
+ function serializableTableContext(tableContext) {
448
+ if (!tableContext || !tableContext.columns || !tableContext.columns.length) return null;
449
+ return {
450
+ columns: tableContext.columns.map(function (col) {
451
+ return {
452
+ header: col.header || "",
453
+ header_html: col.header_html || "",
454
+ value: col.value || "",
455
+ value_html: col.value_html || "",
456
+ reroll_expr: col.reroll_expr || "",
457
+ };
458
+ }),
459
+ };
256
460
  }
257
461
 
258
462
  function appendTableContext(entry, tableContext) {
@@ -262,10 +466,27 @@
262
466
  tableContext.columns.forEach(function (col) {
263
467
  if (!col || (!col.value && !col.value_html)) return;
264
468
  var line = el("div", { class: "jdt-table-row" });
265
- if (col.header || col.header_html) {
469
+ var headerText = col.header ? String(col.header).trim() : "";
470
+ if (headerText) {
266
471
  var headerSpan = el("span", { class: "jdt-table-header" });
267
- if (col.header_html) headerSpan.innerHTML = col.header_html;
268
- else headerSpan.textContent = col.header;
472
+ if (col.reroll_expr) {
473
+ var reroll = el(
474
+ "a",
475
+ { class: "dice-tray-roll", href: "#", "data-dice": col.reroll_expr },
476
+ headerText
477
+ );
478
+ if (tableContext._sourceTable && tableContext._sourceTable.querySelector) {
479
+ reroll._jdtSourceTable = tableContext._sourceTable;
480
+ }
481
+ if (tableContext._allowedKeyIndices) {
482
+ reroll._jdtAllowedKeyIndices = tableContext._allowedKeyIndices;
483
+ }
484
+ headerSpan.appendChild(reroll);
485
+ } else if (col.header_html) {
486
+ headerSpan.innerHTML = col.header_html;
487
+ } else {
488
+ headerSpan.textContent = headerText;
489
+ }
269
490
  line.appendChild(headerSpan);
270
491
  line.appendChild(el("span", { class: "jdt-table-sep" }, ": "));
271
492
  }
@@ -466,7 +687,7 @@
466
687
  success: success,
467
688
  time: timeStr,
468
689
  };
469
- if (tableContext) item.table_context = tableContext;
690
+ if (tableContext) item.table_context = serializableTableContext(tableContext);
470
691
  pushHistory(item);
471
692
  }
472
693
 
@@ -490,7 +711,7 @@
490
711
  if (skipHistory) return;
491
712
 
492
713
  var item = { kind: "roll", expr: expr, total: total, rolls: rolls, mod: mod, time: timeStr };
493
- if (tableContext) item.table_context = tableContext;
714
+ if (tableContext) item.table_context = serializableTableContext(tableContext);
494
715
  pushHistory(item);
495
716
  }
496
717
 
@@ -545,14 +766,14 @@
545
766
  mod: mod,
546
767
  time: timeStr,
547
768
  };
548
- if (tableContext) item.table_context = tableContext;
769
+ if (tableContext) item.table_context = serializableTableContext(tableContext);
549
770
  pushHistory(item);
550
771
  }
551
772
 
552
773
  function showHelp() {
553
774
  addSystemEntry(
554
775
  "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",
776
+ "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 (any dN) shows the matching entry (number or range; any number of side-by-side key/value column groups; headers optional). Commands: /help, /clear",
556
777
  nowTime()
557
778
  );
558
779
  }
@@ -1,6 +1,6 @@
1
1
  module Jekyll
2
2
  module DiceTray
3
- VERSION = "0.3.1"
3
+ VERSION = "0.4.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.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - directsun