diff --git a/src/main/java/dev/ebullient/convert/tools/ParseState.java b/src/main/java/dev/ebullient/convert/tools/ParseState.java index 330e232ca..b196fae20 100644 --- a/src/main/java/dev/ebullient/convert/tools/ParseState.java +++ b/src/main/java/dev/ebullient/convert/tools/ParseState.java @@ -2,10 +2,14 @@ import java.util.ArrayDeque; import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.config.TtrpgConfig; +import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.SourceAndPage; import dev.ebullient.convert.tools.dnd5e.Tools5eIndexType; import dev.ebullient.convert.tools.pf2e.Pf2eIndexType; @@ -13,7 +17,6 @@ public class ParseState { enum ParseStateField implements JsonNodeReader { - footnotes, page, source } @@ -149,6 +152,7 @@ private static ParseStateInfo indentList(ParseStateInfo prev, String value) { } private final Deque stack = new ArrayDeque<>(); + private final Map citations = new HashMap<>(); public boolean push(CompendiumSources sources, JsonNode rootNode) { if (rootNode != null && (rootNode.has("page") || rootNode.has("source"))) { @@ -219,7 +223,12 @@ public boolean indentList(String value) { public void pop(boolean pushed) { if (pushed) { - stack.removeFirst(); + String source = sourcePageString(); + ParseStateInfo removed = stack.removeFirst(); + if (stack.isEmpty() && !citations.isEmpty()) { + Tui.instance().errorf("%s left unreferenced citations behind", source.isEmpty() ? removed : source); + citations.clear(); + } } } @@ -258,7 +267,7 @@ public String sourcePageString() { if (current == null || current.page == 0) { return ""; } - return String.format("%s p. %s", + return String.format("%s p. %s", current.src, current.page); } @@ -308,4 +317,23 @@ public String getPage() { public SourceAndPage toSourceAndPage() { return new SourceAndPage(getSource(), getPage()); } + + public void addCitation(String key, String citationText) { + String old = citations.put(key, citationText); + if (old != null && !old.equals(citationText)) { + Tui.instance().errorf("Duplicate citation text for %s:\nOLD:\n%s\nNEW:\n%s", key, old, citationText); + } + } + + public void popCitations(List footerEntries) { + citations.forEach((k, v) -> { + if (v.startsWith("|")) { // we have a table, assume noted thing is in the footnote + footerEntries.add(v); + return; + } + footerEntries.add(String.format("[%s]: %s", + k, v)); + }); + citations.clear(); + } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java index 02e489cdd..74a287329 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java @@ -296,6 +296,7 @@ List monsterSpellcasting() { spellcasting.spells.put(f.getKey(), spells); }); } + parseState().popCitations(spellcasting.footerEntries); casting.add(spellcasting); }); return casting; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java index 18cac4463..c53f01653 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java @@ -41,6 +41,7 @@ public interface JsonTextReplacement extends JsonTextConverter Pattern optionalFeaturesFilter = Pattern.compile("\\{@filter ([^|}]+)\\|optionalfeatures\\|([^}]+)*}"); Pattern featureTypePattern = Pattern.compile("(?:[Ff]eature )?[Tt]ype=([^|}]+)"); Pattern featureSourcePattern = Pattern.compile("source=([^|}]+)"); + Pattern superscriptCitationPattern = Pattern.compile("\\{@(sup|cite) ([^}]+)}"); Tools5eIndex index(); @@ -155,6 +156,22 @@ default String _replaceTokenText(String input, boolean nested) { result = abilitySavePattern.matcher(result).replaceAll(this::replaceSkillOrAbility); result = skillCheckPattern.matcher(result).replaceAll(this::replaceSkillCheck); + result = superscriptCitationPattern.matcher(result).replaceAll((match) -> { + // {@sup {@cite Casting Times|FleeMortals|A}} + // {@sup whatever} + // {@cite Casting Times|FleeMortals|A} + // {@cite Casting Times|FleeMortals|{@sup A}} + if (match.group(1).equals("sup")) { + String text = replaceText(match.group(2)); + if (text.startsWith("[^") || text.startsWith("^[")) { + // do not put citations in superscript (obsidian/markdown will do it) + return text; + } + return "" + text + ""; + } + return handleCitation(match.group(2)); + }); + result = homebrewPattern.matcher(result).replaceAll((match) -> { // {@homebrew changes|modifications}, {@homebrew additions} or {@homebrew |removals} String s = match.group(1); @@ -258,7 +275,6 @@ default String _replaceTokenText(String input, boolean nested) { .replaceAll("\\{@italic ([^}]+)}", "*$1*") .replaceAll("\\{@s ([^}]+?)}", "~~$1~~") .replaceAll("\\{@strike ([^}]+)}", "~~$1~~") - .replaceAll("\\{@sup ([^}]+?)}", "$1") .replaceAll("\\{@u ([^}]+?)}", "_$1_") .replaceAll("\\{@underline ([^}]+?)}", "_$1_") .replaceAll("\\{@comic ([^}]+?)}", "$1") @@ -708,6 +724,32 @@ parts[0], index().rulesVaultRoot(), } } + default String handleCitation(String citationTag) { + // Casting Times|FleeMortals|A + // Casting Times|FleeMortals|{@sup A} + String[] parts = citationTag.split("\\|"); + if (parts.length < 3) { + tui().errorf("Badly formed citation %s in %s", citationTag, getSources().getKey()); + return citationTag; + } + String key = index().getAliasOrDefault(Tools5eIndexType.citation.createKey(parts[0], parts[1])); + String annotation = replaceText(parts[2]).replaceAll("", ""); + JsonNode jsonSource = index().getNode(key); + if (index().isExcluded(key) || jsonSource == null) { + return annotation; + } + String blockRef = "^" + slugify(key); + List text = new ArrayList<>(); + appendToText(text, jsonSource, null); + if (text.get(text.size() - 1).startsWith("^")) { + blockRef = text.get(text.size() - 1); + } else { + text.add(blockRef); + } + parseState().addCitation(key, String.join("\n", text)); + return String.format("[%s](#%s)", annotation, blockRef); + } + default String decoratedUaName(String name, Tools5eSources sources) { Optional uaSource = sources.uaSource(); if (uaSource.isPresent() && !name.contains("(UA")) { 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 00a909bac..9bdbea8a7 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -99,6 +99,7 @@ private void indexTypes(String filename, JsonNode node) { Tools5eIndexType.vehicleFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.language.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.citation.withArrayFrom(node, this::addToIndex); Tools5eIndexType.itemEntry.withArrayFrom(node, this::addToIndex); Tools5eIndexType.itemTypeAdditionalEntries.withArrayFrom(node, this::addToIndex); @@ -306,7 +307,7 @@ void addToIndex(Tools5eIndexType type, JsonNode node) { // add subclass to alias. Referenced from spells addAlias(lookupKey, key); } - if (type == Tools5eIndexType.table || type == Tools5eIndexType.tableGroup) { + if (type == Tools5eIndexType.table || type == Tools5eIndexType.tableGroup || type == Tools5eIndexType.citation) { SourceAndPage sp = new SourceAndPage(node); tableIndex.computeIfAbsent(sp, k -> new ArrayList<>()).add(node); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java index dd4c970dc..44cb1cc6c 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java @@ -24,6 +24,7 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { card, charoption, charoptionFluff, + citation, classtype("class"), classFluff, // not really a thing. classfeature, diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java index 8d6ae7639..b5abc375b 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java @@ -141,7 +141,7 @@ default void appendObjectToText(List text, JsonNode node, String heading /** Internal */ default void appendTextHeaderBlock(List text, JsonNode node, String heading) { - String pageRef = parseState().sourcePageString(); + String pageRef = parseState().sourcePageString("%s p. %s"); if (heading == null) { List inner = new ArrayList<>(); diff --git a/src/main/resources/convertData.json b/src/main/resources/convertData.json index 4f94e6990..63c4e3f19 100644 --- a/src/main/resources/convertData.json +++ b/src/main/resources/convertData.json @@ -340,9 +340,12 @@ ] }, "fallbackImage": { + "img/PSZ/Archon of Redemption.png": "img/PSZ/Archon Of Redemption.png", + "img/bestiary/ERLW/Inspired.png": "img/bestiary/ERLW/Inspired.webp", + "img/bestiary/MTF/Merrenoloth.jpg": "img/bestiary/MTF/Merrenoloth.webp", "img/bestiary/SDW/Lhammaruntosz.jpg": "img/SDW/Lhammaruntosz.png", - "img/items/CRCotN/Medal of the Maze.jpg": "img/items/CRCotN/Medal of the Maze.webp", - "img/PSZ/Archon of Redemption.png": "img/PSZ/Archon Of Redemption.png" + "img/bestiary/VGM/Deep Scion.jpg": "img/bestiary/VGM/Deep Scion.webp", + "img/items/CRCotN/Medal of the Maze.jpg": "img/items/CRCotN/Medal of the Maze.webp" }, "markerFiles": [ "cultsboons.json", @@ -399,30 +402,30 @@ "sources": [ "actions.json", "adventures.json", - "books.json", "backgrounds.json", - "fluff-backgrounds.json", "bestiary", "bestiary/legendarygroups.json", "bestiary/template.json", + "books.json", "class", "conditionsdiseases.json", "decks.json", "deities.json", "feats.json", - "optionalfeatures.json", + "fluff-backgrounds.json", + "fluff-items.json", + "fluff-races.json", "generated/gendata-tables.json", - "items.json", "items-base.json", - "fluff-items.json", + "items.json", "magicvariants.json", "objects.json", + "optionalfeatures.json", "psionics.json", "races.json", - "fluff-races.json", "rewards.json", - "skills.json", "senses.json", + "skills.json", "spells", "tables.json", "trapshazards.json",