From 1af632d0c5958550fd5942966a5b7744f5dec250 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Wed, 8 Apr 2026 10:38:13 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Filter=20table=20notes=20by=20r?= =?UTF-8?q?eference;=20resolves=20#862?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `onlyReferencedTables` config option. When enabled, table notes are only written if they are linked from other included content (via `{@table}` tags or inline table matching) or explicitly targeted by an include rule. Reference tracking is done at render time in Tools5eLinkifier.createLink() for table/tableGroup types. Tools5eMarkdownConverter.writeFiles() uses a two-pass approach when the option is enabled: non-tables render first (populating the reference set), then only referenced tables are written. Co-Authored-By: Claude Sonnet 4.6 --- docs/configuration.md | 16 +++++ examples/config/config.schema.json | 3 + .../convert/config/CompendiumConfig.java | 9 +++ .../ebullient/convert/config/UserConfig.java | 2 + .../convert/tools/dnd5e/Tools5eIndex.java | 12 ++++ .../convert/tools/dnd5e/Tools5eLinkifier.java | 9 +++ .../tools/dnd5e/Tools5eMarkdownConverter.java | 63 ++++++++++++++----- src/test/resources/5e/sample.yaml | 3 +- 8 files changed, 102 insertions(+), 15 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d8169284..26ef23c0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,6 +24,7 @@ This guide introduces you to configuring data transformations using the Command - [Reprint behavior](#reprint-behavior) - [Troubleshooting reprint behavior](#troubleshooting-reprint-behavior) - [Races as species](#races-as-species) +- [Only emit referenced tables](#only-emit-referenced-tables) - [Use the dice roller plugin](#use-the-dice-roller-plugin) - [Render with Fantasy Statblocks](#render-with-fantasy-statblocks) - [Tag prefix](#tag-prefix) @@ -412,6 +413,21 @@ If you prefer the term "species" over "race" (as used in newer D&D editions), se This changes the output directory from `races/` to `species/`, tags from `race/...` to `species/...`, and the CSS class from `json5e-race` to `json5e-species`. It does not affect internal indexing or source data processing. +## Only emit referenced tables + +By default, the CLI emits all table notes from included sources. Set `onlyReferencedTables` to `true` to restrict output to tables that are actually linked from other included content (monsters, spells, adventures, etc.). + +``` json + "onlyReferencedTables": true +``` + +When enabled, a table note is written only if: + +- something in the rendered output links to it (via a `{@table}` tag or inline table matching), **or** +- it is explicitly named in an [`include` filter](#including-specific-content-with-include). + +Unreferenced tables are logged as `(drop | unreferenced)` when run with `--log`. + ## Use the dice roller plugin The CLI can generate notes that include inline dice rolls. To enable this feature, set the `useDiceRoller` attribute to `true`. diff --git a/examples/config/config.schema.json b/examples/config/config.schema.json index 2063638c..e7b85c8d 100644 --- a/examples/config/config.schema.json +++ b/examples/config/config.schema.json @@ -60,6 +60,9 @@ "type" : "string" } }, + "onlyReferencedTables" : { + "type" : "boolean" + }, "paths" : { "type" : "object", "properties" : { diff --git a/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java b/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java index 885d5b58..1308cde0 100644 --- a/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java +++ b/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java @@ -90,6 +90,7 @@ static DiceRoller fromAttributes(Boolean useDiceRoller, Boolean yamlStatblocks) ReprintBehavior reprintBehavior = ReprintBehavior.newest; boolean racesAsSpecies = false; boolean splitRules = false; + boolean onlyReferencedTables = false; final Set allowedSources = new HashSet<>(); final Set includedKeys = new HashSet<>(); final Set includedGroups = new HashSet<>(); @@ -135,6 +136,10 @@ public boolean splitRules() { return splitRules; } + public boolean onlyReferencedTables() { + return onlyReferencedTables; + } + public boolean allSources() { return allSources; } @@ -442,6 +447,10 @@ private void readConfig(CompendiumConfig config, JsonNode node) { if (input.splitRules != null && input.splitRules) { config.splitRules = true; } + + if (input.onlyReferencedTables != null && input.onlyReferencedTables) { + config.onlyReferencedTables = true; + } } } diff --git a/src/main/java/dev/ebullient/convert/config/UserConfig.java b/src/main/java/dev/ebullient/convert/config/UserConfig.java index cba987a5..adc333d0 100644 --- a/src/main/java/dev/ebullient/convert/config/UserConfig.java +++ b/src/main/java/dev/ebullient/convert/config/UserConfig.java @@ -46,6 +46,7 @@ public class UserConfig { Boolean yamlStatblocks = null; Boolean racesAsSpecies = null; Boolean splitRules = null; + Boolean onlyReferencedTables = null; String tagPrefix = ""; @@ -75,6 +76,7 @@ enum ConfigKeys { sources(List.of("fullSource", "full-source", "convert")), tagPrefix, template, + onlyReferencedTables, racesAsSpecies, splitRules, useDiceRoller, diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java index 84b09589..e7b64ea4 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -83,6 +83,9 @@ public static boolean isSrdBasicOnly() { // Legendary group key -> monster keys that reference it private final Map> legendaryGroupMonsters = new HashMap<>(); + // Table keys that are actually linked from included (non-table) content + private final Set referencedTableKeys = new HashSet<>(); + private final Set unresolvableKeys = new TreeSet<>(); private final Map resolvedSkills = new HashMap<>(); @@ -1234,6 +1237,14 @@ public boolean isExcluded(String key) { return !isIncluded(key); } + public void recordTableReference(String key) { + referencedTableKeys.add(key); + } + + public boolean isTableReferenced(String key) { + return referencedTableKeys.contains(key); + } + public boolean differentSource(Tools5eSources sources, String source) { String primarySource = sources == null ? null : sources.primarySource(); if (primarySource == null || source == null) { @@ -1362,6 +1373,7 @@ public void cleanup() { subraces.clear(); tableIndex.clear(); legendaryGroupMonsters.clear(); + referencedTableKeys.clear(); if (filteredIndex != null) { filteredIndex.clear(); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eLinkifier.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eLinkifier.java index 3b138c24..85e76605 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eLinkifier.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eLinkifier.java @@ -188,6 +188,15 @@ private String createLink(String linkText, String key, Tools5eSources linkSource case deity -> linkDeity(linkText, key); case subclass -> linkSubclass(linkText, key); case variantrule -> linkVariantRules(linkText, key); + case table, tableGroup -> { + if (index.isIncluded(key)) { + index.recordTableReference(key); + } + JsonNode node = index.getNode(key); + yield linkOrText(linkText, key, + getRelativePath(type), + fixFileName(decoratedName(type, node), linkSource.primarySource(), type)); + } default -> { JsonNode node = index.getNode(key); yield linkOrText(linkText, key, diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java index 7b85ca7b..59a24d86 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java @@ -60,22 +60,24 @@ public Tools5eMarkdownConverter writeFiles(List types) { index.tui().verbosef("Converting data: %s", types); WritingQueue queue = new WritingQueue(); - for (var entry : index.includedEntries()) { - final String key = entry.getKey(); - final JsonNode jsonSource = entry.getValue(); - Tools5eIndexType nodeType = Tools5eIndexType.getTypeFromKey(key); - if (types.contains(Tools5eIndexType.race) && nodeType == Tools5eIndexType.subrace) { - // include subrace with race - } else if (!types.contains(nodeType)) { - continue; - } + boolean hasTables = types.stream() + .anyMatch(t -> t == Tools5eIndexType.table || t == Tools5eIndexType.tableGroup); + boolean hasNonTables = types.stream() + .anyMatch(t -> t != Tools5eIndexType.table && t != Tools5eIndexType.tableGroup); - if (nodeType.writeFile()) { - writeQuteBaseFiles(nodeType, key, jsonSource, queue); - } else if (nodeType.isOutputType() && nodeType.useQuteNote()) { - writeQuteNoteFiles(nodeType, key, jsonSource, queue); - } + if (hasTables && hasNonTables && index.cfg().onlyReferencedTables()) { + // Non-tables first: renders content and populates referencedTableKeys + _writeFiles(types.stream() + .filter(t -> t != Tools5eIndexType.table && t != Tools5eIndexType.tableGroup) + .toList(), queue, false); + // Tables second: only write those actually linked from included content + _writeFiles(types.stream() + .filter(t -> t == Tools5eIndexType.table || t == Tools5eIndexType.tableGroup) + .toList(), queue, true); + } else { + // Config not set, only tables, or only non-tables — single pass, no filtering + _writeFiles(types, queue, false); } writer.writeFiles(index.compendiumFilePath(), queue.baseCompendium); @@ -105,6 +107,39 @@ public Tools5eMarkdownConverter writeFiles(List types) { return this; } + private void _writeFiles(List types, WritingQueue queue, boolean filterTables) { + for (var entry : index.includedEntries()) { + final String key = entry.getKey(); + final JsonNode jsonSource = entry.getValue(); + + Tools5eIndexType nodeType = Tools5eIndexType.getTypeFromKey(key); + if (types.contains(Tools5eIndexType.race) && nodeType == Tools5eIndexType.subrace) { + // include subrace with race + } else if (!types.contains(nodeType)) { + continue; + } + + if (nodeType.writeFile()) { + writeQuteBaseFiles(nodeType, key, jsonSource, queue); + } else if (nodeType.isOutputType() && nodeType.useQuteNote()) { + if (filterTables && (nodeType == Tools5eIndexType.table || nodeType == Tools5eIndexType.tableGroup)) { + Tools5eSources sources = Tools5eSources.findSources(key); + // Write if linked from rendered content, or explicitly targeted by a filter rule + boolean explicitlyIncluded = sources != null + && sources.filterRuleApplied() + && sources.includedByConfig(); + if (index.isTableReferenced(key) || explicitlyIncluded) { + writeQuteNoteFiles(nodeType, key, jsonSource, queue); + } else { + index.tui().logf(Msg.FILTER, "(drop | unreferenced) %s", key); + } + } else { + writeQuteNoteFiles(nodeType, key, jsonSource, queue); + } + } + } + } + private void writeQuteBaseFiles(Tools5eIndexType type, String key, JsonNode jsonSource, WritingQueue queue) { var compendium = queue.baseCompendium; var rules = queue.baseRules; diff --git a/src/test/resources/5e/sample.yaml b/src/test/resources/5e/sample.yaml index 93188e35..6d8bce71 100644 --- a/src/test/resources/5e/sample.yaml +++ b/src/test/resources/5e/sample.yaml @@ -26,7 +26,6 @@ sources: - AAG - AI - BAM - - DMG - DoD - EGW - FTD @@ -42,6 +41,7 @@ sources: - XGE reference: - AWM + - DMG - EEPC - ESK - TftYP @@ -91,3 +91,4 @@ images: useDiceRoller: true yamlStatblocks: false +onlyReferencedTables: true From ab6227a6a4272f80a625a77ca677e4a77f6e0d47 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Wed, 8 Apr 2026 10:45:48 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20CHANGELOG=20for=203.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc002c15..1a5c6909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ **Note:** Entries marked with "🔥" indicate crucial or breaking changes that might affect your current setup. +## 3.3.0 Reference-based filtering and split rules + +- ✨ Filter table notes by reference: only emit tables linked from included content (`onlyReferencedTables`); resolves #862 +- ✨ Filter legendary groups by monster references; resolves #718 +- ✨ Option to break rules into separate documents (`splitRules`); resolves #781 +- 🐛 Use HTML to render complex tables; resolves #811 +- 🐛 Resolve reprint aliases before adding spell references; resolves #857 +- 🐛 Fix reprinted subclass spell list merging and wrong qualifiers; resolves #859 +- 🐛 Resolve reprinted class source in subclass link generation; refs #779 +- 🐛 Render psionic focus and modes in template; resolves #839 +- 🐛 Fix image floating (center, left); resolves #853 +- 🐛 Fix duplicated item properties +- 🐛 Remove length limit for captions; resolves #762 + ## 3.2.5 Races or species? - ✨ Add option to emit races as species