diff --git a/examples/config/config.5e.json b/examples/config/config.5e.json index ed6ad7f0b..162034c7c 100644 --- a/examples/config/config.5e.json +++ b/examples/config/config.5e.json @@ -1,47 +1,47 @@ -{ - "sources" : { - "toolsRoot" : "local/5etools/data", - "reference" : [ - "DMG" - ], - "adventure" : [ - "LMoP" - ], - "book" : [ - "PHB" - ], - "homebrew" : [ - "homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json" - ] - }, - "paths" : { - "compendium" : "/compendium/", - "rules" : "/compendium/rules/" - }, - "include" : [ - "race|changeling|mpmm" - ], - "includeGroup" : [ - "familiars" - ], - "exclude" : [ - "monster|expert|dc", - "monster|expert|sdw", - "monster|expert|slw" - ], - "excludePattern" : [ - "race\\|.*\\|dmg" - ], - "reprintBehavior" : "newest", - "template" : { - "background" : "examples/templates/tools5e/images-background2md.txt" - }, - "useDiceRoller" : true, - "yamlStatblocks" : true, - "tagPrefix" : "ttrpg-cli", - "images" : { - "internalRoot" : "local/path/for/remote/images", - "copyInternal" : true, - "copyExternal" : true - } +{ + "sources" : { + "toolsRoot" : "local/5etools/data", + "reference" : [ + "DMG" + ], + "adventure" : [ + "LMoP" + ], + "book" : [ + "PHB" + ], + "homebrew" : [ + "homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json" + ] + }, + "paths" : { + "compendium" : "/compendium/", + "rules" : "/compendium/rules/" + }, + "include" : [ + "race|changeling|mpmm" + ], + "includeGroup" : [ + "familiars" + ], + "exclude" : [ + "monster|expert|dc", + "monster|expert|sdw", + "monster|expert|slw" + ], + "excludePattern" : [ + "race\\|.*\\|dmg" + ], + "reprintBehavior" : "newest", + "template" : { + "background" : "examples/templates/tools5e/images-background2md.txt" + }, + "useDiceRoller" : true, + "yamlStatblocks" : true, + "tagPrefix" : "ttrpg-cli", + "images" : { + "internalRoot" : "local/path/for/remote/images", + "copyInternal" : true, + "copyExternal" : true + } } \ No newline at end of file diff --git a/examples/config/config.pf2e.json b/examples/config/config.pf2e.json index f6a82e75e..cdeccf927 100644 --- a/examples/config/config.pf2e.json +++ b/examples/config/config.pf2e.json @@ -1,32 +1,32 @@ -{ - "sources" : { - "reference" : [ - "CRB", - "GMG" - ], - "book" : [ - "crb", - "gmg" - ] - }, - "paths" : { - "compendium" : "compendium/", - "rules" : "compendium/rules/" - }, - "include" : [ - "ability|buck|b1" - ], - "exclude" : [ - "background|insurgent|apg" - ], - "excludePattern" : [ - "background\\|.*\\|lowg" - ], - "reprintBehavior" : "newest", - "template" : { - "ability" : "../path/to/ability2md.txt" - }, - "useDiceRoller" : true, - "tagPrefix" : "ttrpg-cli", - "images" : { } +{ + "sources" : { + "reference" : [ + "CRB", + "GMG" + ], + "book" : [ + "crb", + "gmg" + ] + }, + "paths" : { + "compendium" : "compendium/", + "rules" : "compendium/rules/" + }, + "include" : [ + "ability|buck|b1" + ], + "exclude" : [ + "background|insurgent|apg" + ], + "excludePattern" : [ + "background\\|.*\\|lowg" + ], + "reprintBehavior" : "newest", + "template" : { + "ability" : "../path/to/ability2md.txt" + }, + "useDiceRoller" : true, + "tagPrefix" : "ttrpg-cli", + "images" : { } } \ No newline at end of file diff --git a/examples/config/config.schema.json b/examples/config/config.schema.json index 4b2daa621..6e11c401c 100644 --- a/examples/config/config.schema.json +++ b/examples/config/config.schema.json @@ -1,146 +1,146 @@ -{ - "$schema" : "https://json-schema.org/draft/2020-12/schema", - "$defs" : { - "Map(String,String)" : { - "type" : "object" - } - }, - "type" : "object", - "properties" : { - "exclude" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "excludePattern" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "from" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "fullSource" : { - "type" : "object", - "properties" : { - "adventure" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "book" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "homebrew" : { - "type" : "array", - "items" : { - "type" : "string" - } - } - } - }, - "images" : { - "type" : "object", - "properties" : { - "copyExternal" : { - "type" : "boolean" - }, - "copyInternal" : { - "type" : "boolean" - }, - "fallbackPaths" : { - "$ref" : "#/$defs/Map(String,String)" - }, - "internalRoot" : { - "type" : "string" - } - } - }, - "include" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "includeGroup" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "includePattern" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "paths" : { - "type" : "object", - "properties" : { - "compendium" : { - "type" : "string" - }, - "rules" : { - "type" : "string" - } - } - }, - "reprintBehavior" : { - "type" : "string", - "enum" : [ "newest", "edition", "all" ] - }, - "sources" : { - "type" : "object", - "properties" : { - "adventure" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "book" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "homebrew" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "reference" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "toolsRoot" : { - "type" : "string" - } - } - }, - "tagPrefix" : { - "type" : "string" - }, - "template" : { - "$ref" : "#/$defs/Map(String,String)" - }, - "useDiceRoller" : { - "type" : "boolean" - }, - "yamlStatblocks" : { - "type" : "boolean" - } - } +{ + "$schema" : "https://json-schema.org/draft/2020-12/schema", + "$defs" : { + "Map(String,String)" : { + "type" : "object" + } + }, + "type" : "object", + "properties" : { + "exclude" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "excludePattern" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "from" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "fullSource" : { + "type" : "object", + "properties" : { + "adventure" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "book" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "homebrew" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + } + }, + "images" : { + "type" : "object", + "properties" : { + "copyExternal" : { + "type" : "boolean" + }, + "copyInternal" : { + "type" : "boolean" + }, + "fallbackPaths" : { + "$ref" : "#/$defs/Map(String,String)" + }, + "internalRoot" : { + "type" : "string" + } + } + }, + "include" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "includeGroup" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "includePattern" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "paths" : { + "type" : "object", + "properties" : { + "compendium" : { + "type" : "string" + }, + "rules" : { + "type" : "string" + } + } + }, + "reprintBehavior" : { + "type" : "string", + "enum" : [ "newest", "edition", "all" ] + }, + "sources" : { + "type" : "object", + "properties" : { + "adventure" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "book" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "homebrew" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "reference" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "toolsRoot" : { + "type" : "string" + } + } + }, + "tagPrefix" : { + "type" : "string" + }, + "template" : { + "$ref" : "#/$defs/Map(String,String)" + }, + "useDiceRoller" : { + "type" : "boolean" + }, + "yamlStatblocks" : { + "type" : "boolean" + } + } } \ No newline at end of file diff --git a/examples/templates/pf2etools/creature2md-yamlStatblock.txt b/examples/templates/pf2etools/creature2md-yamlStatblock.txt new file mode 100644 index 000000000..46a5360d8 --- /dev/null +++ b/examples/templates/pf2etools/creature2md-yamlStatblock.txt @@ -0,0 +1,174 @@ +{#with resource} +--- +obsidianUIMode: preview +cssclasses: pf2e,pf2e-creature +statblock: inline +{#if tags} +tags: +{#each tags} +- {it} +{/each} +{/if} +{#if aliases} +aliases: +{#each aliases} +- {it} +{/each} +{/if} +--- +# {name} *Creature {level}* +{traits join " "} + +```statblock +layout: Pathfinder 2e Creature Layout +source: {sources.primarySource.quoted} +sourcebook: {source.quoted} +name: {name.quoted} +level: Creature {level} + +{#let alignment=traits.getFirst("Alignment") rarity=traits.getFirst("Rarity") size=traits.getFirst("Size") genericTraits=traits.genericTraits} +{#if alignment} +alignment: {alignment.withoutTitle.quoted} +{/if}{#if rarity} +rarity: {rarity.withoutTitle.quoted} +{/if}{#if size} +size: {size.withoutTitle.quoted} +{/if}{#if genericTraits} +traits: + {#each genericTraits} + - {it.withoutTitle.quoted} + {/each} +{/if} +{/let} + +{#if perception} +modifier: {perception} +{/if}{#if senses} +senses: {senses.join(", ").quoted} +{/if}{#if languages} +languages: {languages.quoted} +{/if}{#if skills} +skills: + {#for skill in skills.skills} + - {skill.name.quoted}: {skill.value} + {#each skill.otherBonuses} + {it.key.quoted}: {it.value} + {/each} + {#if skill.notes} + note: {skill.formattedNotes} + {/if} + {/for} + {#if skills.notes} + - note: {skills.notes.join(", ").quoted} + {/if} +{/if}{#if abilityMods} +attributes: + {#each abilityMods} + - {it.key}: {it.value} + {/each} +{/if}{#if speed} +speed: {speed.quoted} +{/if} + +{#if defenses} +{#if defenses.ac} +ac: {defenses.ac.value} +{#if defenses.ac.formattedNotes} +acNote: {defenses.ac.formattedNotes.quoted} +{/if} +{/if} +{#if defenses.savingThrows} +saves: + {#for save in defenses.savingThrows.saves} + - {save.name.toLowerCase().quoted}: {save.value} + {#each save.otherBonuses} + {it.key.quoted}: {it.value} + {/each} + {#if save.notes} + note: {save.formattedNotes} + {/if} + {/for} + {#if defenses.savingThrows.abilities} + - note: {defenses.savingThrows.abilities.join(", ").quoted} + {/if} +{/if} +{#if defenses.hpHardnessBt} +{#if defenses.hpHardnessBt.hp} +hp: {defenses.hpHardnessBt.hp.value} +{/if} +{#if defenses.hpHardnessBt.hp.formattedNotes || defenses.additionalHp} +hpNote: "{defenses.hpHardnessBt.hp.formattedNotes}{#if defenses.additionalHp}; {defenses.additionalHp}{/if}" +{/if}{#if defenses.additionalHardness} +hardness: "{#if defenses.hpHardnessBt.hardness}{defenses.hpHardnessBt.hardness}; {/if}{defenses.additionalHardness}" +{#else if defenses.hpHardnessBt.hardness} +hardness: {defenses.hpHardnessBt.hardness} +{/if} +{/if} +{#if defenses.immunities} +immunities: "{#each defenses.immunities}{it.withoutTitle}{#if it_hasNext}, {/if}{/each}" +{/if}{#if defenses.resistances} +resistances: "{#each defenses.resistances}{it.key} {it.value}{#if it_hasNext}, {/if}{/each}" +{/if}{#if defenses.weaknesses} +weaknesses: "{#each defenses.weaknesses}{it.key} {it.value}{#if it_hasNext}, {/if}{/each}" +{/if} +{/if} + +{#if items} +items: {items.join(", ").quoted} +{/if} + +{#for abilitySet in abilities.abilityMap.orEmpty} +{#if abilitySet.value} +abilities_{abilitySet.key}: + {#each abilitySet.value} + {it.renderAsYamlStatblock indent " "} + {/each} +{/if} +{/for} + +{#if attacks} +attacks: +{#each attacks} + {it.renderAsYamlStatblock indent " "} +{/each} +{/if} + +{#if spellcasting} +spellcasting: +{#for spells in spellcasting} + - name: {spells.name.quoted} + {#if spells.dc} + dc: {spells.dc} + {/if}{#if spells.attackBonus} + bonus: {spells.attackBonus} + {/if}{#if spells.focusPoints} + fp: {spells.focusPoints} + {/if} + desc: > + {#if spells.notes}{spells.notes join ", "}; {/if}{#if spells.ranks}{spells.ranks join "; "};{/if} + {#each spells.constantRanks} + **Constant ({it.rank})** {it.spells join ", "}{#if it_hasNext}; {/if} + {/each} +{/for} +{#for rituals in ritualCasting} + - name: {rituals.name.quoted} + {#if rituals.dc} + dc: {rituals.dc} + {/if} + desc: > + {rituals.ranks.join("; ")} +{/for} +{/if} +``` +^statblock + +{#if hasSections && description} +## Summary +{/if}{#if description} +{description} +{/if}{#if text} +{text} +{/if} + +*Source: {source}* +{/with} diff --git a/src/main/java/dev/ebullient/convert/StringUtil.java b/src/main/java/dev/ebullient/convert/StringUtil.java index 0481bd4df..165d5a46f 100644 --- a/src/main/java/dev/ebullient/convert/StringUtil.java +++ b/src/main/java/dev/ebullient/convert/StringUtil.java @@ -25,16 +25,24 @@ */ public class StringUtil { + /** Return the number with prefixed with the appropriate sign (+ or -), or an empty string if null. */ + public static String formatAsModifier(Integer n) { + if (n == null) { + return ""; + } + return (n < 0 ? "-" : "+") + Math.abs(n); + } + /** * Return {@code formatString} formatted with {@code o} as the first parameter. * If {@code o} is null, then return an empty string. */ - public static String format(String formatString, Object val) { - return val == null || (val instanceof String && ((String) val).isBlank()) ? "" : formatString.formatted(val); + public static String formatIfPresent(String formatString, Object val) { + return val == null || val.toString().isBlank() ? "" : formatString.formatted(val); } - public static String valueOrDefault(String value, String fallback) { - return value == null || value.isEmpty() ? fallback : value; + public static String valueOrDefault(Object value, String fallback) { + return value == null || value.toString().isEmpty() ? fallback : value.toString(); } public static String valueOrDefault(String[] parts, int index, String fallback) { @@ -82,33 +90,6 @@ public static String flatJoin(String joiner, Collection... lists) { return join(joiner, Arrays.stream(lists).flatMap(Collection::stream).toList()); } - /** - * Like {@link #joinWithPrefix(String, String, Collection)} but accept vararg inputs. This is mostly to get around - * being unable to pass null values to {@code List.of}. - * - * @see #joinWithPrefix(String, String, Collection) - * @see #join(String, Object, Object...) - */ - public static String joinWithPrefix(String joiner, String prefix, Object o1, Object... rest) { - List args = new ArrayList<>(); - args.add(o1); - args.addAll(Arrays.asList(rest)); - return joinWithPrefix(joiner, prefix, args); - } - - /** - * Like {@link #join(String, Collection)} but add a prefix to the resulting string if it's non-empty. - * - * @see #join(String, Collection) - */ - public static String joinWithPrefix(String joiner, String prefix, Collection list) { - String s = join(joiner, list); - if (s.isEmpty()) { - return ""; - } - return isPresent(prefix) ? prefix + s : s; - } - /** * {@link #joinConjunct(String, String, List)} with a {@code ", "} joiner. * diff --git a/src/main/java/dev/ebullient/convert/VersionProvider.java b/src/main/java/dev/ebullient/convert/VersionProvider.java index b556649c6..4761a38a5 100644 --- a/src/main/java/dev/ebullient/convert/VersionProvider.java +++ b/src/main/java/dev/ebullient/convert/VersionProvider.java @@ -13,12 +13,13 @@ public String[] getVersion() { Properties properties = new Properties(); try { properties.load(TtrpgConfig.class.getResourceAsStream("/git.properties")); - return new String[] { - "${COMMAND-FULL-NAME} version " + properties.getProperty("git.build.version"), - "Git commit: " + properties.get("git.commit.id.abbrev") - }; - } catch (IOException e) { + } catch (IOException | NullPointerException e) { return new String[] { "${COMMAND-FULL-NAME} version unknown " }; } + + return new String[] { + "${COMMAND-FULL-NAME} version " + properties.getProperty("git.build.version"), + "Git commit: " + properties.get("git.commit.id.abbrev") + }; } } diff --git a/src/main/java/dev/ebullient/convert/io/Templates.java b/src/main/java/dev/ebullient/convert/io/Templates.java index d2c127cba..87d0fbf22 100644 --- a/src/main/java/dev/ebullient/convert/io/Templates.java +++ b/src/main/java/dev/ebullient/convert/io/Templates.java @@ -78,11 +78,12 @@ public String render(QuteBase resource) { } } - public String renderInlineEmbedded(QuteUtil resource) { + public String renderInlineEmbedded(QuteUtil resource, boolean asYamlStatblock) { Template tpl = customTemplateOrDefault(resource.template()); try { return tpl .data("resource", resource) + .data("asYamlStatblock", asYamlStatblock) .render().trim(); } catch (TemplateException tex) { Throwable cause = tex.getCause(); diff --git a/src/main/java/dev/ebullient/convert/io/Tui.java b/src/main/java/dev/ebullient/convert/io/Tui.java index 2ab66d554..dd8e234b3 100644 --- a/src/main/java/dev/ebullient/convert/io/Tui.java +++ b/src/main/java/dev/ebullient/convert/io/Tui.java @@ -605,8 +605,8 @@ public void writeYamlFile(Path outputFile, Object obj) throws IOException { yamlMapper().writer().writeValue(outputFile.toFile(), obj); } - public String renderEmbedded(QuteUtil resource) { - return templates.renderInlineEmbedded(resource); + public String renderEmbedded(QuteUtil resource, boolean asYamlStatblock) { + return templates.renderInlineEmbedded(resource, asYamlStatblock); } public T readJsonValue(JsonNode node, TypeReference targetRef) { diff --git a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java index 0bbd42c8d..8d25ac64c 100644 --- a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java +++ b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java @@ -96,6 +96,23 @@ default IndexType indexType() { @JavadocIgnore interface Renderable { /** Return this object rendered using its template. */ - String render(); + String render(boolean asYamlStatblock); + + /** Return the object rendered using its template. */ + default String render() { + return render(false); + } + + /** Return the object rendered using its template with {@code asYamlStatblock} set to true. */ + default String renderAsYamlStatblock() { + return render(true) + // Manually remove the dice roller syntax - the yaml statblocks handle dice roller syntax differently. At this + // point, the parsing has already finished, so we can't use parseState to stop them from being added in the + // first place. So all we can do is post-process to remove them again. + .replaceAll("`dice: [^`]+` \\(`([^`]+)`\\)", "$1") + // This usage is usually a footnote. With the Markdown rendering the asterisk is unnecessary, so just don't + // add the asterisk, so this doesn't get treated as Markdown formatting. + .replaceAll("\\* \\^\\[", " ^["); + } } } diff --git a/src/main/java/dev/ebullient/convert/qute/TtrpgTemplateExtension.java b/src/main/java/dev/ebullient/convert/qute/TtrpgTemplateExtension.java index 014a39e35..4f7e5b1f0 100644 --- a/src/main/java/dev/ebullient/convert/qute/TtrpgTemplateExtension.java +++ b/src/main/java/dev/ebullient/convert/qute/TtrpgTemplateExtension.java @@ -7,6 +7,7 @@ import dev.ebullient.convert.StringUtil; import dev.ebullient.convert.io.JavadocVerbatim; +import dev.ebullient.convert.config.TtrpgConfig; import io.quarkus.qute.TemplateExtension; /** @@ -31,7 +32,6 @@ static String capitalized(String s) { /** * Return the string pluralized based on the size of the collection. - * * Example: `{resource.name.pluralized(resource.components)}` */ @JavadocVerbatim @@ -54,7 +54,6 @@ static String prefixSpace(Object obj) { /** * Return the given collection converted into a string and joined using the specified joiner. - * * Example: `{resource.components.join(", ")}` */ @JavadocVerbatim @@ -64,11 +63,47 @@ static String join(Collection collection, String joiner) { /** * Return the given list joined into a single string, using a different delimiter for the last element. - * * Example: `{resource.components.joinConjunct(", ", " or ")}` */ @JavadocVerbatim static String joinConjunct(Collection collection, String joiner, String lastjoiner) { return StringUtil.joinConjunct(joiner, lastjoiner, collection.stream().map(o -> o.toString()).toList()); } + + /** Indent each line of the given string with the given indent. */ + static String indent(String lines, String indent) { + return lines.replaceAll("\n", "\n" + indent); + } + + /** + * Double all newlines in the text (eg replace every newline with two newlines). For use with embedded YAML, where the + * {@code >} folding operator will ignore newlines that aren't 'doubled'. This only replaces single newlines, not newlines + * that are already doubled. + */ + static String unfoldNewlines(String text) { + return text.replaceAll("([^\n])\n([^\n])", "$1\n\n$2"); + } + + /** + * Quote the input according to YAML property rules. Only quote if necessary for YAML to interpret it as a string. Escape + * quotes in the input string if necessary. + */ + static String quoted(Object obj) { + if (obj == null) { + return ""; + } + String text = obj.toString(); + if (text == null || text.isBlank()) { + return ""; + } + if (text.contains("\n")) { + TtrpgConfig.getConfig().tui().errorf("Asked to quote a multiline string: %s", text); + } + if (!text.startsWith("[") && !text.startsWith("*") && !text.contains(":") && !text.startsWith("\"")) { + // No quoting required + return text; + } + // Escape any quotes in the text before we quote it + return "\"%s\"".formatted(text.replaceAll("\"", "\\\\\"")); + } } diff --git a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java index 9877497e6..e40748cca 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java @@ -48,18 +48,6 @@ default void appendUnlessEmptyFrom(JsonNode x, List text, JsonTextConver } } - default String bonusOrNull(JsonNode x) { - JsonNode value = getFrom(x); - if (value == null) { - return null; - } - if (!value.isNumber()) { - throw new IllegalArgumentException("bonusOrNull can only work with numbers: " + value); - } - int n = value.asInt(); - return (n >= 0 ? "+" : "") + n; - } - /** * Return the boolean value of the field in the node: * - if the field is a boolean, return the value @@ -364,15 +352,20 @@ default String transformTextFrom(JsonNode source, String delimiter, JsonTextConv * Parse this field from {@code source} as potentially-nested array of entries, and return a list of strings. This * calls {@link JsonTextConverter#appendToText(List, JsonNode, String)} to recursively parse the input. */ - default List transformListFrom(JsonNode source, JsonTextConverter convert) { + default List transformListFrom(JsonNode source, JsonTextConverter convert, String heading) { if (!isArrayIn(source)) { return List.of(); } List inner = new ArrayList<>(); - convert.appendToText(inner, getFrom(source), null); + convert.appendToText(inner, getFrom(source), heading); return inner; } + /** @see #transformListFrom(JsonNode, JsonTextConverter, String) */ + default List transformListFrom(JsonNode source, JsonTextConverter convert) { + return transformListFrom(source, convert, null); + } + /** Returns the enum value of {@code enumClass} that this field in {@code source} contains, or null. */ default > E getEnumValueFrom(JsonNode source, Class enumClass) { String value = getTextOrNull(source); diff --git a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java index f06d226a9..ad940ce45 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java @@ -495,9 +495,9 @@ default List removePreamble(List content) { * @param resource QuteBase containing required template resource data * @param admonition Type of embedded/encapsulating admonition */ - default String renderEmbeddedTemplate(QuteBase resource, String admonition) { + default String renderEmbeddedTemplate(QuteUtil resource, String admonition, boolean asYamlStatblock, String... prepend) { List inner = new ArrayList<>(); - renderEmbeddedTemplate(inner, resource, admonition, List.of()); + renderEmbeddedTemplate(inner, resource, admonition, asYamlStatblock, prepend); return String.join("\n", inner); } @@ -509,11 +509,12 @@ default String renderEmbeddedTemplate(QuteBase resource, String admonition) { * @param admonition Type of embedded/encapsulating admonition * @param prepend Text to prepend at beginning of admonition (e.g. title) */ - default void renderEmbeddedTemplate(List text, QuteBase resource, String admonition, List prepend) { - boolean pushed = parseState().push(resource.sources()); + default void renderEmbeddedTemplate( + List text, QuteUtil resource, String admonition, boolean asYamlStatblock, String... prepend) { + Boolean pushed = (resource instanceof QuteBase) ? parseState().push(((QuteBase)resource).sources()) : null; try { - String rendered = tui().renderEmbedded(resource); - List inner = new ArrayList<>(prepend); + String rendered = tui().renderEmbedded(resource, asYamlStatblock); + List inner = new ArrayList<>(Arrays.asList(prepend)); inner.addAll(removePreamble(new ArrayList<>(List.of(rendered.split("\n"))))); maybeAddBlankLine(text); @@ -524,41 +525,10 @@ default void renderEmbeddedTemplate(List text, QuteBase resource, String } text.addAll(inner); } finally { - parseState().pop(pushed); - } - } - - /** - * Return the rendered contents of an (always) inline template. - * - * @param resource QuteUtil containing required template resource data - * @param admonition Type of inline admonition - */ - default String renderInlineTemplate(QuteUtil resource, String admonition) { - List inner = new ArrayList<>(); - renderInlineTemplate(inner, resource, admonition); - return String.join("\n", inner); - } - - /** - * Add rendered contents of an (always) inline template - * to collected text - * - * @param text List of text content should be added to - * @param resource QuteUtil containing required template resource data - * @param admonition Type of inline admonition - */ - default void renderInlineTemplate(List text, QuteUtil resource, String admonition) { - String rendered = tui().renderEmbedded(resource); - List inner = removePreamble(new ArrayList<>(List.of(rendered.split("\n")))); - - maybeAddBlankLine(text); - if (admonition != null) { - wrapAdmonition(inner, "inline-" + admonition); - } else { - balanceBackticks(inner); + if (pushed != null) { + parseState().pop(pushed); + } } - text.addAll(inner); } /** Wrap {@code inner} in an admonition with the name {@code admonition}. */ @@ -622,7 +592,12 @@ default int[] outerAdmonitionIndices(List inner) { return new int[] { firstLineIdx, lastLineIdx }; } - String replaceText(String s); + default String replaceText(String input) { + return replaceTokens(input, this::replaceTokenText); + } + + /** Replace specific tokens (e.g. "@"-tags) in the input string. */ + String replaceTokenText(String input, boolean nested); default String replaceText(JsonNode input) { if (input == null) { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java index b03115721..b8fe53a75 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java @@ -605,11 +605,10 @@ default void appendStatblockInline(List text, JsonNode entry, String hea .withTargetFile(embedFileName) .withTargetPath(relativePath)); } else { - List prepend = new ArrayList<>(List.of( - "title: " + name, - "collapse: closed", - existingNode == null ? "" : "%% See " + type.linkify(this, data) + " %%")); - renderEmbeddedTemplate(text, qs, type.name(), prepend); + renderEmbeddedTemplate(text, qs, type.name(), false, + "title: " + name, + "collapse: closed", + existingNode == null ? "" : "%% See " + type.linkify(this, data) + " %%"); } } } 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 64541760b..dc8be4dbf 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java @@ -125,10 +125,6 @@ default String joinAndReplace(ArrayNode array) { return String.join(", ", list); } - default String replaceText(String input) { - return replaceTokens(input, (s, b) -> this._replaceTokenText(s, b)); - } - default String tableHeader(String x) { if (x.contains("dice")) { // don't do the usual dice formatting in a column header @@ -179,7 +175,8 @@ default String replacePromptStrings(String s) { }); } - default String _replaceTokenText(String input, boolean nested) { + @Override + default String replaceTokenText(String input, boolean nested) { String result = input; // render.js this._renderString_renderTag diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAbility.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAbility.java index d31bd8624..3727f366e 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAbility.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAbility.java @@ -3,22 +3,23 @@ import static dev.ebullient.convert.StringUtil.join; import static dev.ebullient.convert.StringUtil.parenthesize; -import java.util.Set; - import com.fasterxml.jackson.databind.JsonNode; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.QuteAbility; public class Json2QuteAbility extends Json2QuteBase { - public Json2QuteAbility(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode) { - super(index, type, rootNode); + private final boolean isEmbedded; + + public Json2QuteAbility(Pf2eIndex index, JsonNode rootNode, boolean isEmbedded) { + super(index, Pf2eIndexType.ability, rootNode, + isEmbedded ? null : Pf2eSources.findOrTemporary(Pf2eIndexType.ability, rootNode)); + this.isEmbedded = isEmbedded; } @Override protected QuteAbility buildQuteNote() { - return Pf2eAbility.createAbility(rootNode, this, sources); + return Pf2eAbility.createAbility(this); } public enum Pf2eAbility implements Pf2eJsonNodeReader { @@ -63,33 +64,29 @@ public enum Pf2eAbility implements Pf2eJsonNodeReader { /** Nestable entries for the ability effect. */ entries; - private static QuteAbility createAbility(JsonNode node, JsonSource convert, Pf2eSources sources) { - Tags tags = new Tags(); - Set traits = convert.collectTraitsFrom(node, tags); - - return new QuteAbility(sources, + private static QuteAbility createAbility(Json2QuteAbility convert) { + JsonNode node = convert.rootNode; + return new QuteAbility(convert.sources, name.getTextFrom(node).map(convert::replaceText).orElse("Activate"), generic.getLinkFrom(node, convert), - entries.transformTextFrom(node, "\n", convert), - tags, - traits, + convert.entries, + convert.tags, + convert.traits, activity.getActivityFrom(node, convert), range.getRangeFrom(node, convert), - components.getActivationComponentsFrom(node, traits, convert), + components.getActivationComponentsFrom(node, convert.traits, convert), requirements.replaceTextFrom(node, convert), prerequisites.replaceTextFrom(node, convert), cost.replaceTextFrom(node, convert) // remove trailing period - .replaceFirst("^(.*)\\.$", "\1"), - trigger.replaceTextFrom(node, convert), + .replaceFirst("(^[.])\\.$", "$1"), + trigger.replaceTextFrom(node, convert) + // remove trailing period + .replaceFirst("(^[.])\\.$", "$1"), frequency.getFrequencyFrom(node, convert), special.transformTextFrom(node, "\n", convert), note.replaceTextFrom(node, convert), - sources == null, convert); - } - - public static QuteAbility createEmbeddedAbility(JsonNode node, JsonSource convert) { - return createAbility(node, convert, null); + convert.isEmbedded, convert); } public String getLinkFrom(JsonNode node, JsonSource convert) { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAction.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAction.java index 3ad2b2f4f..4ad8944b2 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAction.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAction.java @@ -19,11 +19,7 @@ public Json2QuteAction(Pf2eIndex index, JsonNode node) { @Override protected QuteAction buildQuteResource() { - Tags tags = new Tags(sources); - List text = new ArrayList<>(); - - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - appendToText(text, Pf2eAction.info.getFrom(rootNode), null); + entries.addAll(Pf2eAction.info.transformListFrom(rootNode, this)); ActionType actionType = Pf2eAction.actionType.fieldFromTo(rootNode, ActionType.class, tui()); @@ -33,12 +29,11 @@ protected QuteAction buildQuteResource() { actionType.addTags(this, tags); } - return new QuteAction( - getSources(), text, tags, + return new QuteAction(sources, entries, tags, Pf2eAction.cost.transformTextFrom(rootNode, ", ", this), Pf2eAction.trigger.transformTextFrom(rootNode, ", ", this), Field.alias.replaceTextFromList(rootNode, this), - collectTraitsFrom(rootNode, tags), + traits, Pf2eAction.prerequisites.transformTextFrom(rootNode, ", ", this), Field.requirements.replaceTextFrom(rootNode, this), Pf2eAction.frequency.getFrequencyFrom(rootNode, this), diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAffliction.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAffliction.java index 3d2b73765..066cccba9 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAffliction.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAffliction.java @@ -4,7 +4,6 @@ import static dev.ebullient.convert.StringUtil.toTitleCase; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -12,22 +11,23 @@ import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; - import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.StringUtil; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.QuteAffliction; public class Json2QuteAffliction extends Json2QuteBase { - public Json2QuteAffliction(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode) { - super(index, type, rootNode); + private final boolean isEmbedded; + + public Json2QuteAffliction(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode, boolean isEmbedded) { + super(index, type, rootNode, isEmbedded ? null : Pf2eSources.findOrTemporary(type, rootNode)); + this.isEmbedded = isEmbedded; } @Override protected QuteAffliction buildQuteNote() { - return Pf2eAffliction.createAffliction(rootNode, this, getSources()); + return Pf2eAffliction.createAffliction(this); } public enum Pf2eAffliction implements Pf2eJsonNodeReader { @@ -96,24 +96,20 @@ public enum Pf2eAffliction implements Pf2eJsonNodeReader { * ], * */ - private static QuteAffliction createAffliction( - JsonNode node, JsonSource convert, Pf2eSources sources) { - boolean isEmbedded = sources == null; + private static QuteAffliction createAffliction(Json2QuteAffliction convert) { + JsonNode node = convert.rootNode; // Sometimes the affliction data is nested as an entry within the parent node. Optional nestedAfflictionNode = Optional.ofNullable(getNestedAffliction(node)); - if (!isEmbedded && nestedAfflictionNode.isEmpty()) { + if (!convert.isEmbedded && nestedAfflictionNode.isEmpty()) { // For standalone notes, we should always have a nested affliction node. convert.tui().errorf("Unable to extract affliction entry from %s", node.toPrettyString()); return null; } JsonNode dataNode = nestedAfflictionNode.orElse(node); - Tags tags = new Tags(sources); - Collection traits = convert.collectTraitsFrom(node, tags); - Optional afflictionLevel = level.intFrom(node).map(Objects::toString); - afflictionLevel.ifPresent(lv -> tags.add("affliction", "level", lv)); + afflictionLevel.ifPresent(lv -> convert.tags.add("affliction", "level", lv)); String temptedCurseText = temptedCurse.transformTextFrom(node, "\n", convert); Optional afflictionType = type.getTextFrom(node) @@ -121,18 +117,18 @@ private static QuteAffliction createAffliction( .filter(StringUtil::isPresent); afflictionType.ifPresent(type -> { if (isPresent(temptedCurseText)) { - tags.add("affliction", type, "tempted"); + convert.tags.add("affliction", type, "tempted"); } else { - tags.add("affliction", type); + convert.tags.add("affliction", type); } }); Optional afflictionName = name.getTextFrom(node); return new QuteAffliction( - sources, + convert.sources, // Standalone notes must have a valid affliction name so that we can name the file - isEmbedded ? afflictionName.orElse("") : afflictionName.orElseThrow(), + convert.isEmbedded ? afflictionName.orElse("") : afflictionName.orElseThrow(), // Any entries which were alongside the nested affliction block nestedAfflictionNode.isEmpty() ? List.of() @@ -142,8 +138,8 @@ private static QuteAffliction createAffliction( ArrayList::new, (acc, n) -> convert.appendToText(acc, n, "##"), ArrayList::addAll), - tags, - traits, + convert.tags, + convert.traits, Field.alias.replaceTextFromList(dataNode, convert), // Level may be e.g. "varies" afflictionLevel.or(() -> level.getTextFrom(node)) @@ -177,14 +173,10 @@ private static QuteAffliction createAffliction( entry.transformTextFrom(e.getValue(), "\n", convert, e.getKey())), (x, y) -> y, LinkedHashMap::new)), - isEmbedded, + convert.isEmbedded, convert); } - static QuteAffliction createInlineAffliction(JsonNode node, JsonSource convert) { - return createAffliction(node, convert, null); - } - /** Try to extract the affliction node from the entries. Returns null if we couldn't extract one. */ private static JsonNode getNestedAffliction(JsonNode node) { if (!entries.isArrayIn(node)) { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteArchetype.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteArchetype.java index 4590a25fb..55dd5cf6c 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteArchetype.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteArchetype.java @@ -6,10 +6,8 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; - import com.fasterxml.jackson.databind.JsonNode; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.QuteArchetype; import dev.ebullient.convert.tools.pf2e.qute.QuteFeat; @@ -21,18 +19,12 @@ public Json2QuteArchetype(Pf2eIndex index, JsonNode rootNode) { @Override protected QuteArchetype buildQuteResource() { - Tags tags = new Tags(sources); - List text = new ArrayList<>(); - - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - List benefits = ArchetypeField.benefits.getListOfStrings(rootNode, tui()); benefits.forEach(b -> tags.add("archetype", "benefit", b)); int dedicationLevel = ArchetypeField.dedicationLevel.intOrDefault(rootNode, 2); - return new QuteArchetype(sources, text, tags, - collectTraitsFrom(rootNode, tags), + return new QuteArchetype(sources, entries, tags, traits, dedicationLevel, benefits, getFeatures(dedicationLevel)); @@ -120,12 +112,9 @@ QuteFeat findFeat(String levelKey) { } String render(QuteFeat quteFeat, boolean archetypeFeat) { - List inner = new ArrayList<>(); - renderEmbeddedTemplate(inner, quteFeat, "feat", List.of( - String.format("title: %s, Feat %s", quteFeat.getName(), quteFeat.level + (archetypeFeat ? "*" : "")), - "collapse: closed")); - - return String.join("\n", inner); + return renderEmbeddedTemplate(quteFeat, "feat", false, + "title: %s, Feat %s".formatted(quteFeat.getName(), quteFeat.level + (archetypeFeat ? "*" : "")), + "collapse: closed"); } enum ArchetypeField implements Pf2eJsonNodeReader { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBackground.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBackground.java index 761ce44f5..1a1aa61a5 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBackground.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBackground.java @@ -1,11 +1,7 @@ package dev.ebullient.convert.tools.pf2e; -import java.util.ArrayList; -import java.util.List; - import com.fasterxml.jackson.databind.JsonNode; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.QuteBackground; public class Json2QuteBackground extends Json2QuteBase { @@ -16,11 +12,6 @@ public Json2QuteBackground(Pf2eIndex index, JsonNode rootNode) { @Override protected QuteBackground buildQuteResource() { - Tags tags = new Tags(sources); - List text = new ArrayList<>(); - - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - Pf2eBackground.boosts.getListOfStrings(rootNode, tui()) .stream() .filter(b -> !b.equalsIgnoreCase("Free")) @@ -32,7 +23,7 @@ protected QuteBackground buildQuteResource() { Pf2eBackground.feat.getListOfStrings(rootNode, tui()) .forEach(s -> tags.add("background", "feat", s)); - return new QuteBackground(sources, text, tags); + return new QuteBackground(sources, entries, tags); } enum Pf2eBackground implements Pf2eJsonNodeReader { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBase.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBase.java index fc5b2e2d6..7fe17696f 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBase.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBase.java @@ -1,15 +1,22 @@ package dev.ebullient.convert.tools.pf2e; +import java.util.ArrayList; +import java.util.List; import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase; import dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataTraits; public abstract class Json2QuteBase implements JsonSource { protected final Pf2eIndex index; protected final Pf2eIndexType type; protected final JsonNode rootNode; protected final Pf2eSources sources; + protected final Tags tags; + protected final QuteDataTraits traits; + protected final List entries; public Json2QuteBase(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode) { this(index, type, rootNode, Pf2eSources.findOrTemporary(type, rootNode)); @@ -20,6 +27,9 @@ public Json2QuteBase(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode, Pf2 this.type = type; this.rootNode = rootNode; this.sources = sources; + this.tags = new Tags(sources); + this.traits = getTraits(rootNode).addToTags(tags); + this.entries = new ArrayList<>(SourceField.entries.transformListFrom(rootNode, this, "##")); } @Override diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java index 97f14bcfa..918638f97 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java @@ -2,18 +2,16 @@ import static dev.ebullient.convert.StringUtil.join; -import java.util.Collection; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; - import com.fasterxml.jackson.databind.JsonNode; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.QuteCreature; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataRef; public class Json2QuteCreature extends Json2QuteBase { @@ -23,7 +21,7 @@ public Json2QuteCreature(Pf2eIndex index, JsonNode rootNode) { @Override protected QuteCreature buildQuteResource() { - return Pf2eCreature.create(rootNode, this); + return Pf2eCreature.create(this); } /** @@ -59,14 +57,14 @@ protected QuteCreature buildQuteResource() { enum Pf2eCreature implements Pf2eJsonNodeReader { abilities, abilityMods, - alignment, + alignment, // unused in the data but defined in the schema alias, attacks, defenses, description, entries, hasImages, - inflicts, // not actually present in any of the entries + inflicts, // unused in the data but defined in the schema isNpc, items, languages, @@ -83,15 +81,9 @@ enum Pf2eCreature implements Pf2eJsonNodeReader { std, traits; - private static QuteCreature create(JsonNode node, JsonSource convert) { - Tags tags = new Tags(convert.getSources()); - Collection traits = convert.collectTraitsFrom(node, tags); - traits.addAll(alignment.getAlignmentsFrom(node, convert)); - - return new QuteCreature(convert.getSources(), - entries.transformTextFrom(node, "\n", convert, "##"), - tags, - traits, + private static QuteCreature create(Json2QuteCreature convert) { + JsonNode node = convert.rootNode; + return new QuteCreature(convert.sources, convert.entries, convert.tags, convert.traits, alias.replaceTextFromList(node, convert), description.replaceTextFrom(node, convert), level.intOrNull(node), @@ -290,7 +282,8 @@ private static QuteCreature.CreatureSpellReference getSpellReference(JsonNode no String spellName = name.getTextOrThrow(node); return new QuteCreature.CreatureSpellReference( spellName, - convert.linkify(Pf2eIndexType.spell, join("|", spellName, source.getTextOrNull(node))), + QuteDataRef.fromMarkdownLink( + convert.linkify(Pf2eIndexType.spell, join("|", spellName, source.getTextOrNull(node)))), amount.getTextFrom(node) .filter(s -> s.equalsIgnoreCase("at will")) .map(unused -> 0) diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java index d63967031..4ec4d3e73 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java @@ -6,21 +6,18 @@ import static dev.ebullient.convert.StringUtil.toOrdinal; import static dev.ebullient.convert.StringUtil.toTitleCase; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; import java.util.stream.Stream; - import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.StringUtil; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.NamedText; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.Pf2eJsonNodeReader.Pf2eAttack; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity.Activity; import dev.ebullient.convert.tools.pf2e.qute.QuteDeity; import dev.ebullient.convert.tools.pf2e.qute.QuteInlineAttack; import dev.ebullient.convert.tools.pf2e.qute.QuteInlineAttack.AttackRangeType; @@ -34,30 +31,25 @@ public Json2QuteDeity(Pf2eIndex index, JsonNode rootNode) { @Override protected QuteDeity buildQuteResource() { - List text = new ArrayList<>(); - Tags tags = new Tags(sources); - Pf2eDeity.domains.getListOfStrings(rootNode, tui()).forEach(d -> tags.add("domain", d, "deity")); Pf2eDeity.alternateDomains.getListOfStrings(rootNode, tui()).forEach(d -> tags.add("domain", d, "deity")); String category = Pf2eDeity.category.getTextOrDefault(rootNode, "Deity"); tags.add("deity", category); - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - JsonNode alignNode = Pf2eDeity.alignment.getFrom(rootNode); - - return new QuteDeity(sources, text, tags, + // TODO handle "entry" field in alignment + return new QuteDeity(sources, entries, tags, Field.alias.replaceTextFromList(rootNode, this), category, join(", ", Pf2eDeity.pantheon.linkifyListFrom(rootNode, Pf2eIndexType.deity, this)), - join(", ", Pf2eDeity.alignment.getAlignmentsFrom(alignNode, this)), - join(", ", Pf2eDeity.followerAlignment.getAlignmentsFrom(alignNode, this)), + join(", ", Pf2eDeity.alignment.linkifyListFrom(alignNode, Pf2eIndexType.trait, this)), + join(", ", Pf2eDeity.followerAlignment.linkifyListFrom(alignNode, Pf2eIndexType.trait, this)), Pf2eDeity.areasOfConcern.transformTextFrom(rootNode, ", ", this), commandmentToString(Pf2eDeity.edict.replaceTextFromList(rootNode, this)), commandmentToString(Pf2eDeity.anathema.replaceTextFromList(rootNode, this)), buildCleric(), - buildAvatar(tags), + buildAvatar(), buildIntercession()); } @@ -114,7 +106,7 @@ QuteDeity.QuteDeityCleric buildCleric() { return cleric; } - QuteDeity.QuteDivineAvatar buildAvatar(Tags tags) { + QuteDeity.QuteDivineAvatar buildAvatar() { JsonNode avatarNode = Pf2eDeity.avatar.getFrom(rootNode); if (avatarNode == null) { return null; @@ -154,7 +146,7 @@ QuteDeity.QuteDivineAvatar buildAvatar(Tags tags) { avatar.attacks = Stream.concat( Pf2eDeity.melee.streamFrom(avatarNode).map(n -> Map.entry(n, AttackRangeType.MELEE)), Pf2eDeity.ranged.streamFrom(avatarNode).map(n -> Map.entry(n, AttackRangeType.RANGED))) - .map(e -> buildAvatarAttack(e.getKey(), tags, e.getValue())) + .map(e -> buildAvatarAttack(e.getKey(), e.getValue())) .toList(); avatar.ability = Pf2eDeity.ability.streamFrom(avatarNode) .map(this::buildAvatarAbility) @@ -169,21 +161,19 @@ private NamedText buildAvatarAbility(JsonNode abilityNode) { SourceField.entries.transformTextFrom(abilityNode, "; ", this)); } - private QuteInlineAttack buildAvatarAttack(JsonNode actionNode, Tags tags, AttackRangeType rangeType) { - Collection traits = collectTraitsFrom(actionNode, tags); - traits.addAll(Pf2eDeity.preciousMetal.getListOfStrings(actionNode, tui())); - Pf2eDeity.traitNote.getTextFrom(actionNode).ifPresent(traits::add); - + private QuteInlineAttack buildAvatarAttack(JsonNode actionNode, AttackRangeType rangeType) { return new QuteInlineAttack( Pf2eAttack.name.getTextOrDefault(actionNode, "attack"), - Pf2eActivity.single.toQuteActivity(this, null), + Pf2eActivity.toQuteActivity(this, Activity.single, null), rangeType, Json2QuteItem.Pf2eWeaponData.getDamageString(actionNode, this), Stream.of(Json2QuteItem.Pf2eWeaponData.damageType, Json2QuteItem.Pf2eWeaponData.damageType2) .map(field -> field.getTextOrEmpty(actionNode)) .filter(StringUtil::isPresent) .toList(), - traits, + getTraits(actionNode).addToTags(tags) + .addTraits(Pf2eDeity.preciousMetal.getListOfStrings(actionNode, tui())) + .addTrait(Pf2eDeity.traitNote.getTextOrEmpty(actionNode)), Pf2eDeity.note.replaceTextFrom(actionNode, this), this); } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteFeat.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteFeat.java index 6b44546b6..01b6bcf5d 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteFeat.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteFeat.java @@ -1,12 +1,10 @@ package dev.ebullient.convert.tools.pf2e; -import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; - import com.fasterxml.jackson.databind.JsonNode; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.QuteFeat; public class Json2QuteFeat extends Json2QuteBase { @@ -17,18 +15,12 @@ public Json2QuteFeat(Pf2eIndex index, JsonNode rootNode) { @Override protected QuteFeat buildQuteResource() { - Tags tags = new Tags(sources); - List text = new ArrayList<>(); - - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - List leadsTo = Pf2eFeat.leadsTo.getListOfStrings(rootNode, tui()) .stream() .map(x -> linkify(Pf2eIndexType.feat, x)) .collect(Collectors.toList()); - return new QuteFeat(sources, text, tags, - collectTraitsFrom(rootNode, tags), + return new QuteFeat(sources, entries, tags, traits, Field.alias.replaceTextFromList(rootNode, this), Pf2eFeat.level.getTextOrDefault(rootNode, "1"), Pf2eFeat.access.transformTextFrom(rootNode, ", ", this), @@ -44,21 +36,16 @@ protected QuteFeat buildQuteResource() { public QuteFeat buildArchetype(String archetypeName, String dedicationLevel) { String featLevel = Pf2eFeat.level.getTextOrDefault(rootNode, "1"); - List text = new ArrayList<>(); - Tags tags = new Tags(); String note = null; - if (dedicationLevel != featLevel) { + if (!Objects.equals(dedicationLevel, featLevel)) { note = String.format( "> [!pf2-note] This version of %s is intended for use with the %s Archetype. Its level has been changed accordingly.", index.linkify(this.type, String.join("|", List.of(sources.getName(), sources.primarySource()))), archetypeName); } - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - - return new QuteFeat(sources, text, tags, - collectTraitsFrom(rootNode, tags), + return new QuteFeat(sources, entries, tags, traits, List.of(), dedicationLevel, Pf2eFeat.access.transformTextFrom(rootNode, ", ", this), diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteHazard.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteHazard.java index 07fa0e798..e00910f5e 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteHazard.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteHazard.java @@ -1,13 +1,8 @@ package dev.ebullient.convert.tools.pf2e; -import java.util.ArrayList; -import java.util.List; - import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.tools.JsonTextConverter; -import dev.ebullient.convert.tools.Tags; -import dev.ebullient.convert.tools.pf2e.Json2QuteAbility.Pf2eAbility; import dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat; import dev.ebullient.convert.tools.pf2e.qute.QuteHazard; @@ -19,13 +14,9 @@ public Json2QuteHazard(Pf2eIndex index, JsonNode rootNode) { @Override protected QuteHazard buildQuteResource() { - Tags tags = new Tags(sources); - List text = new ArrayList<>(); - - appendToText(text, Pf2eHazard.description.getFrom(rootNode), "##"); + entries.addAll(Pf2eHazard.description.transformListFrom(rootNode, this, "##")); - return new QuteHazard(sources, text, tags, - collectTraitsFrom(rootNode, tags), + return new QuteHazard(sources, entries, tags, traits, Pf2eHazard.level.getTextOrDefault(rootNode, "0"), Pf2eHazard.disable.transformTextFrom(rootNode, "\n", index), Pf2eHazard.reset.transformTextFrom(rootNode, "\n", index), @@ -33,7 +24,7 @@ protected QuteHazard buildQuteResource() { Pf2eHazard.defenses.getDefensesFrom(rootNode, this), Pf2eHazard.attacks.getAttacksFrom(rootNode, this), Pf2eHazard.abilities.streamFrom(rootNode) - .map(n -> Pf2eAbility.createEmbeddedAbility(n, this)) + .map(n -> new Json2QuteAbility(index, n, true).buildQuteNote()) .toList(), Pf2eHazard.actions.getAbilityOrAfflictionsFrom(rootNode, this), Pf2eHazard.stealth.getObjectFrom(rootNode) diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteItem.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteItem.java index 39750df07..90d6bb363 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteItem.java @@ -8,9 +8,7 @@ import java.util.Collection; import java.util.List; import java.util.Map.Entry; -import java.util.Set; import java.util.stream.Collectors; - import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.qute.NamedText; @@ -35,18 +33,12 @@ public Json2QuteItem(Pf2eIndex index, JsonNode rootNode) { @Override protected Pf2eQuteBase buildQuteResource() { - Tags tags = new Tags(sources); - List text = new ArrayList<>(); - List aliases = new ArrayList<>(Field.alias.replaceTextFromList(rootNode, this)); - Set traits = collectTraitsFrom(rootNode, tags); - - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - String duration = Pf2eItem.duration.existsIn(rootNode) ? SourceField.entry.getTextOrEmpty(Pf2eItem.duration.getFrom(rootNode)) : null; - return new QuteItem(sources, text, tags, traits, aliases, + return new QuteItem(sources, entries, tags, traits, + Field.alias.replaceTextFromList(rootNode, this), buildActivate(), getPrice(rootNode), join(", ", Pf2eItem.ammunition.linkifyListFrom(rootNode, Pf2eIndexType.item, this)), @@ -54,15 +46,15 @@ protected Pf2eQuteBase buildQuteResource() { Pf2eItem.onset.transformTextFrom(rootNode, ", ", this), replaceText(Pf2eItem.access.getTextOrEmpty(rootNode)), duration, - getCategory(tags), + getCategory(), linkify(Pf2eIndexType.group, getGroup()), Pf2eItem.hands.getTextOrEmpty(rootNode), keysToList(List.of(Pf2eItem.usage, Pf2eItem.bulk)), - getContract(tags), + getContract(), getShieldData(), - getArmorData(), - getWeaponData(tags), - getVariants(tags), + Pf2eItem.armorData.getArmorFrom(rootNode), + getWeaponData(), + getVariants(), Pf2eItem.craftReq.transformTextFrom(rootNode, "; ", this)); } @@ -115,28 +107,7 @@ private QuteItemShieldData getShieldData() { penalty(Pf2eItem.speedPen.getTextOrEmpty(shieldNode), " ft.")); } - private QuteItemArmorData getArmorData() { - JsonNode armorDataNode = Pf2eItem.armorData.getFrom(rootNode); - if (armorDataNode == null) { - return null; - } - - QuteItemArmorData armorData = new QuteItemArmorData(); - Pf2eItem.ac.intFrom(armorDataNode).ifPresent(ac -> armorData.ac = new QuteDataArmorClass(ac)); - armorData.dexCap = Pf2eItem.dexCap.bonusOrNull(armorDataNode); - - armorData.strength = Pf2eItem.str.getTextOrDefault(armorDataNode, "—"); - - String checkPen = Pf2eItem.checkPen.getTextOrDefault(armorDataNode, null); - armorData.checkPenalty = penalty(checkPen, ""); - - String speedPen = Pf2eItem.speedPen.getTextOrDefault(armorDataNode, null); - armorData.speedPenalty = penalty(speedPen, " ft."); - - return armorData; - } - - private List getWeaponData(Tags tags) { + private List getWeaponData() { JsonNode weaponDataNode = Pf2eItem.weaponData.getFrom(rootNode); if (weaponDataNode == null) { return null; @@ -152,7 +123,7 @@ private List getWeaponData(Tags tags) { return weaponDataList; } - private List getVariants(Tags tags) { + private List getVariants() { JsonNode variantsNode = Pf2eItem.variants.getFrom(rootNode); if (variantsNode == null) @@ -176,7 +147,7 @@ private List getVariants(Tags tags) { return variantList; } - private Collection getContract(Tags tags) { + private Collection getContract() { JsonNode contractNode = Pf2eItem.contract.getFrom(rootNode); if (contractNode == null) { return null; @@ -241,7 +212,7 @@ private String getGroup() { return Pf2eWeaponData.group.getTextOrEmpty(rootNode); } - String getCategory(Tags tags) { + String getCategory() { String category = Pf2eItem.category.getTextOrEmpty(rootNode); String subcategory = Pf2eItem.subCategory.getTextOrEmpty(rootNode); if (category == null) { @@ -302,6 +273,18 @@ enum Pf2eItem implements Pf2eJsonNodeReader { String properName() { return toTitleCase(this.nodeName()); } + + private QuteItemArmorData getArmorFrom(JsonNode source) { + return getObjectFrom(source) + .map(node -> new QuteItemArmorData( + ac.intFrom(node).map(QuteDataArmorClass::new).orElseThrow(), + dexCap.intOrNull(node), + str.intOrNull(node), + checkPen.intFrom(node).map(n -> -Math.abs(n)).orElse(0), + speedPen.intFrom(node).map(n -> -Math.abs(n)).orElse(0) + )) + .orElse(null); + } } enum Pf2eItemVariant implements Pf2eJsonNodeReader { @@ -326,7 +309,7 @@ public static QuteItemWeaponData buildWeaponData(JsonNode source, JsonSource convert, Tags tags) { QuteItemWeaponData weaponData = new QuteItemWeaponData(); - weaponData.traits = convert.collectTraitsFrom(source, tags); + weaponData.traits = convert.getTraits(source).addToTags(tags); weaponData.type = SourceField.type.getTextOrEmpty(source); weaponData.damage = getDamageString(source, convert); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteRitual.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteRitual.java index 5a0a13729..3b51a52b1 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteRitual.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteRitual.java @@ -2,13 +2,10 @@ import static dev.ebullient.convert.StringUtil.joinConjunct; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; - import com.fasterxml.jackson.databind.JsonNode; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase; import dev.ebullient.convert.tools.pf2e.qute.QuteDataRange; import dev.ebullient.convert.tools.pf2e.qute.QuteRitual; @@ -26,27 +23,22 @@ public Json2QuteRitual(Pf2eIndex index, JsonNode rootNode) { @Override protected Pf2eQuteBase buildQuteResource() { - Tags tags = new Tags(sources); - List text = new ArrayList<>(); - - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - String level = Pf2eSpell.level.getTextOrDefault(rootNode, "1"); tags.add(RITUAL_TAG, level); - return new QuteRitual(sources, text, tags, + return new QuteRitual(sources, entries, tags, level, "Ritual", - collectTraitsFrom(rootNode, tags), + traits, Field.alias.replaceTextFromList(rootNode, this), getQuteRitualCast(), getQuteRitualChecks(), - getQuteRitualSpellTarget(tags), + getQuteRitualSpellTarget(), Field.requirements.transformTextFrom(rootNode, ", ", this), null, getHeightenedCast()); } - QuteSpellTarget getQuteRitualSpellTarget(Tags tags) { + QuteSpellTarget getQuteRitualSpellTarget() { String targets = replaceText(Pf2eSpell.targets.getTextOrEmpty(rootNode)); QuteDataRange range = Pf2eSpell.range.getRangeFrom(rootNode, this); SpellArea area = Pf2eSpell.area.fieldFromTo(rootNode, SpellArea.class, tui()); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteSpell.java index 6a8b4d646..349d879d5 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteSpell.java @@ -11,15 +11,14 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.stream.Stream; - import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.qute.NamedText; import dev.ebullient.convert.tools.JsonNodeReader.FieldValue; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase; import dev.ebullient.convert.tools.pf2e.qute.QuteDataDuration; import dev.ebullient.convert.tools.pf2e.qute.QuteDataRange; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataRef; import dev.ebullient.convert.tools.pf2e.qute.QuteDataTimedDuration; import dev.ebullient.convert.tools.pf2e.qute.QuteSpell; import dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellAmp; @@ -41,13 +40,6 @@ protected Json2QuteSpell(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode) @Override protected Pf2eQuteBase buildQuteResource() { - Tags tags = new Tags(sources); - List text = new ArrayList<>(); - - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - - Collection traits = collectTraitsFrom(rootNode, tags); - boolean focus = Pf2eSpell.focus.booleanOrDefault(rootNode, false); String level = Pf2eSpell.level.getTextOrDefault(rootNode, "1"); String type = "spell"; @@ -89,9 +81,9 @@ protected Pf2eQuteBase buildQuteResource() { List components = Pf2eSpell.components.getComponentsFrom(rootNode, this); // Add additional traits according to present components components.stream().map(Pf2eSpellComponent::getAddedTrait) - .distinct().map(this::linkifyTrait).forEach(traits::add); + .distinct().map(this::linkifyTrait).map(QuteDataRef::fromMarkdownLink).forEach(traits::add); - return new QuteSpell(sources, text, tags, + return new QuteSpell(sources, entries, tags, level, toTitleCase(type), traits, Field.alias.replaceTextFromList(rootNode, this), @@ -100,7 +92,7 @@ level, toTitleCase(type), Pf2eSpell.cost.transformTextFrom(rootNode, ", ", this), Pf2eSpell.trigger.transformTextFrom(rootNode, ", ", this), Pf2eSpell.requirements.transformTextFrom(rootNode, ", ", this), - getQuteSpellTarget(tags), + getQuteSpellTarget(), Pf2eSpell.savingThrow.getSpellSaveFrom(rootNode, this), Pf2eSpell.duration.getSpellDurationFrom(rootNode, this), Pf2eSpell.domains.linkifyListFrom(rootNode, Pf2eIndexType.domain, this), @@ -111,7 +103,7 @@ level, toTitleCase(type), getAmpEffects()); } - QuteSpellTarget getQuteSpellTarget(Tags tags) { + QuteSpellTarget getQuteSpellTarget() { String targets = Pf2eSpell.targets.replaceTextFrom(rootNode, this); SpellArea area = Pf2eSpell.area.fieldFromTo(rootNode, SpellArea.class, tui()); QuteDataRange range = Pf2eSpell.range.getRangeFrom(rootNode, this); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTable.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTable.java index eaac32255..87446a3bc 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTable.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTable.java @@ -4,11 +4,9 @@ import java.util.ArrayList; import java.util.List; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote; public class Json2QuteTable extends Json2QuteBase { @@ -19,9 +17,7 @@ public Json2QuteTable(Pf2eIndex index, JsonNode rootNode) { @Override protected Pf2eQuteNote buildQuteNote() { - Tags tags = new Tags(sources); List text = new ArrayList<>(); - ((ObjectNode) rootNode).put(SourceField.type.name(), "table"); appendToText(text, rootNode, null); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTrait.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTrait.java index f867a587e..4a7a2a955 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTrait.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTrait.java @@ -2,10 +2,8 @@ import java.util.ArrayList; import java.util.List; - import com.fasterxml.jackson.databind.JsonNode; -import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote; import dev.ebullient.convert.tools.pf2e.qute.QuteTrait; import dev.ebullient.convert.tools.pf2e.qute.QuteTraitIndex; @@ -18,8 +16,6 @@ public Json2QuteTrait(Pf2eIndex index, JsonNode rootNode) { @Override protected QuteTrait buildQuteResource() { - Tags tags = new Tags(sources); - List text = new ArrayList<>(); List categories = new ArrayList<>(); Field.categories.getListOfStrings(rootNode, tui()).forEach(c -> { @@ -28,7 +24,7 @@ protected QuteTrait buildQuteResource() { JsonNode implied = TraitField.implies.getFrom(rootNode); if (implied != null) { implied.fieldNames().forEachRemaining(n -> { - if ("spell".equals(n.toLowerCase())) { + if ("spell".equalsIgnoreCase(n)) { String school = implied.get(n).get("_fSchool").asText(); tags.add("trait", "category", "spell", school); categories.add(String.format("%s (%s)", c, school)); @@ -41,9 +37,7 @@ protected QuteTrait buildQuteResource() { } }); - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); - - return new QuteTrait(sources, text, tags, List.of(), categories); + return new QuteTrait(sources, entries, tags, List.of(), categories); } static Pf2eQuteNote buildIndex(Pf2eIndex index) { 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 79ac45ff8..b613e99fc 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java @@ -3,12 +3,8 @@ import static dev.ebullient.convert.StringUtil.join; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Set; -import java.util.TreeSet; import java.util.stream.Collectors; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -17,31 +13,12 @@ import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.QuteUtil; import dev.ebullient.convert.tools.JsonNodeReader.FieldValue; -import dev.ebullient.convert.tools.Tags; -import dev.ebullient.convert.tools.pf2e.Json2QuteAbility.Pf2eAbility; -import dev.ebullient.convert.tools.pf2e.Json2QuteAffliction.Pf2eAffliction; import dev.ebullient.convert.tools.pf2e.Json2QuteItem.Pf2eItem; import dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase; import dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity; public interface JsonSource extends JsonTextReplacement { - /** - * Collect and linkify traits from the specified node. - * - * @param tags The tags to populate while collecting traits. If null, then don't populate any tags. - * - * @return an empty or sorted/linkified list of traits (never null) - */ - default Set collectTraitsFrom(JsonNode sourceNode, Tags tags) { - return Field.traits.getListOfStrings(sourceNode, tui()).stream() - .peek(tags == null ? t -> { - } : t -> tags.add("trait", t)) - .sorted() - .map(s -> linkify(Pf2eIndexType.trait, s)) - .collect(Collectors.toCollection(TreeSet::new)); - } - /** * External (and recursive) entry point for content parsing. * @@ -115,8 +92,10 @@ default void appendObjectToText(List text, JsonNode node, String heading case quote -> appendQuote(text, node); // special inline types - case ability -> appendRenderable(text, Pf2eAbility.createEmbeddedAbility(node, this)); - case affliction -> appendAffliction(text, node); + case ability -> appendRenderable(text, + new Json2QuteAbility(index(), node, true).buildQuteNote()); + case affliction -> appendRenderable(text, + new Json2QuteAffliction(index(), Pf2eIndexType.affliction, node, true).buildQuteNote()); case attack -> appendRenderable(text, Pf2eJsonNodeReader.Pf2eAttack.getAttack(node, this)); case data -> embedData(text, node); case lvlEffect -> appendLevelEffect(text, node); @@ -299,11 +278,6 @@ default void appendSuccessDegree(List text, JsonNode node) { + x)); } - /** Internal */ - default void appendAffliction(List text, JsonNode node) { - appendRenderable(text, Pf2eAffliction.createInlineAffliction(node, this)); - } - /** Internal */ private void appendRenderable(List text, QuteUtil.Renderable renderable) { text.addAll(List.of(renderable.render().split("\n"))); @@ -555,14 +529,14 @@ default void embedData(List text, JsonNode dataNode) { // (This might be the case anyway, but we know it probably is the case with these). // So try to get the renderable embedded object first, and then add the collapsed // tag to the outermost admonition. - QuteUtil.Renderable renderable = switch (dataType) { - case ability -> Pf2eAbility.createEmbeddedAbility(data, this); - case affliction, curse, disease -> Pf2eAffliction.createInlineAffliction(data, this); + Json2QuteBase json2Renderable = switch (dataType) { + case ability -> new Json2QuteAbility(index(), data, true); + case affliction, curse, disease -> new Json2QuteAffliction(index(), dataType, data, true); default -> null; }; - if (renderable != null) { + if (json2Renderable != null) { List renderedData = new ArrayList<>(); - appendRenderable(renderedData, renderable); + appendRenderable(renderedData, (QuteUtil.Renderable) json2Renderable.buildQuteNote()); // Make the outermost admonition collapsed, if there is one int[] adIndices = outerAdmonitionIndices(renderedData); if (adIndices != null) { @@ -577,9 +551,9 @@ default void embedData(List text, JsonNode dataNode) { // and add the collapsible admonition ourselves Pf2eQuteBase converted = dataType.convertJson2QuteBase(index(), data); if (converted != null) { - renderEmbeddedTemplate(text, converted, tag, - List.of(String.format("title: %s", converted.title()), - "collapse: closed")); + renderEmbeddedTemplate(text, converted, tag, false, + "title: %s".formatted(converted.title()), + "collapse: closed"); } else { tui().errorf("Unable to process data for %s: %s", tag, dataNode.toString()); } @@ -608,9 +582,7 @@ default List embedGenericData(String tag, JsonNode data) { text.add("title: " + title); // Add traits - Tags tags = new Tags(); - Collection traits = collectTraitsFrom(data, tags); - text.add(join(" ", traits) + " "); + text.add(join(" ", getTraits(data)) + " "); maybeAddBlankLine(text); // Add rendered sections diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java index 24dc0c4b2..bd67d3149 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java @@ -3,12 +3,13 @@ import static dev.ebullient.convert.StringUtil.join; import static dev.ebullient.convert.StringUtil.toAnchorTag; import static dev.ebullient.convert.StringUtil.toTitleCase; +import static dev.ebullient.convert.tools.pf2e.Pf2eActivity.linkifyActivity; import java.util.ArrayList; import java.util.List; import java.util.regex.MatchResult; import java.util.regex.Pattern; - +import java.util.stream.Collector; import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.config.CompendiumConfig; @@ -17,9 +18,11 @@ import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.JsonNodeReader.FieldValue; import dev.ebullient.convert.tools.JsonTextConverter; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity.Activity; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataRef; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataTraits; public interface JsonTextReplacement extends JsonTextConverter { - enum Field implements Pf2eJsonNodeReader { alias, auto, @@ -66,11 +69,8 @@ default CompendiumConfig cfg() { return index().cfg(); } - default String replaceText(String input) { - return replaceTokens(input, (s, b) -> this._replaceTokenText(s, b)); - } - - default String _replaceTokenText(String input, boolean nested) { + @Override + default String replaceTokenText(String input, boolean nested) { if (input == null || input.isEmpty()) { return input; } @@ -193,31 +193,29 @@ default String replaceFootnoteReference(MatchResult match) { } default String replaceActionAs(MatchResult match) { - final Pf2eActivity type; - switch (match.group(1).toLowerCase()) { - case "1": - case "a": - type = Pf2eActivity.single; - break; - case "2": - case "d": - type = Pf2eActivity.two; - break; - case "3": - case "t": - type = Pf2eActivity.three; - break; - case "f": - type = Pf2eActivity.free; - break; - case "r": - type = Pf2eActivity.reaction; - break; - default: - type = Pf2eActivity.varies; - break; - } - return type.linkify(index().rulesVaultRoot()); + Activity type = switch (match.group(1).toLowerCase()) { + case "1", "a" -> Activity.single; + case "2", "d" -> Activity.two; + case "3", "t" -> Activity.three; + case "f" -> Activity.free; + case "r" -> Activity.reaction; + default -> Activity.varies; + }; + return linkifyActivity(type, index().rulesVaultRoot()); + } + + /** + * Collect and linkify traits from the specified node. + * + * @return a {@link QuteDataTraits} which may be empty (never null) + */ + default QuteDataTraits getTraits(JsonNode sourceNode) { + return Field.traits.getListOfStrings(sourceNode, tui()).stream() + .map(s -> QuteDataRef.fromMarkdownLink(linkify(Pf2eIndexType.trait, s))) + .collect(Collector.of(QuteDataTraits::new, QuteDataTraits::add, (a, b) -> { + a.addAll(b); + return b; + })); } default String linkifyRuneItem(MatchResult match) { @@ -361,6 +359,8 @@ default String linkifyTrait(String match) { default String linkifyTrait(JsonNode traitNode, String linkText) { if (traitNode != null) { String source = SourceField.source.getTextOrEmpty(traitNode); + // Some traits are surrounded in square brackets. Strip this out to avoid messing up link rendering. + linkText = linkText.replaceFirst("^\\[(.*)]$", "$1"); return "[%s](%s/%s%s.md \"%s\")".formatted( linkText, diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eActivity.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eActivity.java index 5b5512920..8f1e68781 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eActivity.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eActivity.java @@ -7,86 +7,55 @@ import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity.Activity; -public enum Pf2eActivity { - single("Single Action", ">", "single_action.svg"), - two("Two-Action", ">>", "two_actions.svg"), - three("Three-Action", ">>>", "three_actions.svg"), - free("Free Action", "F", "delay.svg"), - reaction("Reaction", "R", "reaction.svg"), - varies("Varies", "V", "load.svg"), - timed("Duration or Frequency", "⏲", "hour-glass.svg"); - +public class Pf2eActivity { final static String DOC_PATH = "core-rulebook/chapter-9-playing-the-game.md#Actions"; - final String longName; - final String markdownName; - final String textGlyph; - final String glyph; - final String targetFileName; - - Pf2eActivity(String longName, String textGlyph, String glyph) { - this.longName = longName; - this.markdownName = longName.replace(" ", "%20"); - this.textGlyph = textGlyph; - this.glyph = glyph; - - int x = glyph.lastIndexOf('.'); - this.targetFileName = Tui.slugify(glyph.substring(0, x)) + glyph.substring(x); - } - - public static Pf2eActivity toActivity(String unit, int number) { - switch (unit) { - case "single": - case "action": - switch (number) { - case 1: - return single; - case 2: - return two; - case 3: - return three; - } - break; - case "free": - return free; - case "reaction": - return reaction; - case "varies": - return varies; - case "timed": - return timed; + public static void addImageRef(Activity activity, JsonSource convert) { + String glyph = switch (activity) { + case single -> "single_action"; + case two -> "two_actions"; + case three -> "three_actions"; + case free -> "delay"; + case reaction -> "reaction"; + case varies -> "load"; + case timed -> "hour-glass"; + }; + String targetFileName = Tui.slugify(glyph) + ".svg"; + Pf2eSources.buildStreamImageRef( + convert.index(), glyph + ".svg", Path.of("img", targetFileName), activity.longName); + } + + public static String linkifyActivity(Activity activity, String rulesRoot) { + return "[%s](%s \"%s\")".formatted(activity.textGlyph, rulesRoot + DOC_PATH, activity.longName); + } + + public static QuteDataActivity toQuteActivity(JsonSource convert, String unit, int number, String text) { + Activity activity = switch (unit) { + case "single", "action" -> switch (number) { + case 1 -> Activity.single; + case 2 -> Activity.two; + case 3 -> Activity.three; + default -> null; + }; + case "free" -> Activity.free; + case "reaction" -> Activity.reaction; + case "varies" -> Activity.varies; + case "timed" -> Activity.timed; + default -> null; + }; + if (activity == null) { + return null; } - return null; - } - - public String getLongName() { - return this.longName; - } - - public String getTextGlyph() { - return this.textGlyph; - } - - public String getGlyph() { - return this.glyph; - } - - public String linkify(String rulesRoot) { - return String.format("[%s](%s \"%s\")", - this.textGlyph, getRulesPath(rulesRoot), longName); - } - - public String getRulesPath(String rulesRoot) { - return String.format("%s%s", rulesRoot, DOC_PATH); + return toQuteActivity(convert, activity, text); } - public QuteDataActivity toQuteActivity(JsonSource convert, String text) { - Path relativeTarget = Path.of("img", targetFileName); + public static QuteDataActivity toQuteActivity(JsonSource convert, Activity activity, String text) { + addImageRef(activity, convert); return new QuteDataActivity( - this != timed && isPresent(text) ? join(" ", getLongName(), text) : text, - Pf2eSources.buildStreamImageRef(convert.index(), glyph, relativeTarget, longName), - textGlyph, - this.getRulesPath(convert.index().rulesVaultRoot())); + activity, + convert.index().rulesVaultRoot() + DOC_PATH, + activity != Activity.timed && isPresent(text) ? join(" ", activity.longName, text) : text); } } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonNodeReader.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonNodeReader.java index a4db54aad..8fc1e3a7d 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonNodeReader.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonNodeReader.java @@ -7,26 +7,24 @@ import static java.util.Objects.requireNonNullElse; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; - import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.StringUtil; import dev.ebullient.convert.tools.JsonNodeReader; -import dev.ebullient.convert.tools.pf2e.Json2QuteAbility.Pf2eAbility; -import dev.ebullient.convert.tools.pf2e.Json2QuteAffliction.Pf2eAffliction; import dev.ebullient.convert.tools.pf2e.JsonSource.AppendTypeValue; import dev.ebullient.convert.tools.pf2e.qute.QuteAbilityOrAffliction; import dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity.Activity; import dev.ebullient.convert.tools.pf2e.qute.QuteDataArmorClass; import dev.ebullient.convert.tools.pf2e.qute.QuteDataDefenses; import dev.ebullient.convert.tools.pf2e.qute.QuteDataDuration; @@ -35,6 +33,7 @@ import dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat.QuteDataNamedBonus; import dev.ebullient.convert.tools.pf2e.qute.QuteDataHpHardnessBt; import dev.ebullient.convert.tools.pf2e.qute.QuteDataRange; +import dev.ebullient.convert.tools.pf2e.qute.QuteDataRef; import dev.ebullient.convert.tools.pf2e.qute.QuteDataSpeed; import dev.ebullient.convert.tools.pf2e.qute.QuteDataTimedDuration; import dev.ebullient.convert.tools.pf2e.qute.QuteInlineAttack; @@ -42,16 +41,6 @@ /** A utility class which extends {@link JsonNodeReader} with PF2e-specific functionality. */ public interface Pf2eJsonNodeReader extends JsonNodeReader { - /** - * Return alignments as a list of formatted strings from this field in the given node. - * Returns an empty list if we couldn't get alignments. - */ - default List getAlignmentsFrom(JsonNode alignNode, JsonSource convert) { - return streamFrom(alignNode) - .map(JsonNode::asText) - .map(a -> a.length() > 2 ? a : convert.linkifyTrait(a.toUpperCase())) - .toList(); - } /** Return a {@link QuteDataSpeed} read from this field of the {@code source} node, or null. */ default QuteDataSpeed getSpeedFrom(JsonNode source, JsonSource convert) { @@ -106,7 +95,7 @@ default List getAttacksFrom(JsonNode source, JsonSource conver * traits from these activation components to {@code traits}. Return an empty list if we couldn't get activation * components. */ - default List getActivationComponentsFrom(JsonNode source, Set traits, JsonSource convert) { + default List getActivationComponentsFrom(JsonNode source, Collection traits, JsonSource convert) { List rawComponents = getListOfStrings(source, convert.tui()).stream() .map(s -> s.replaceFirst("^\\((%s)\\)$", "\1")) // remove parens .toList(); @@ -124,6 +113,7 @@ default List getActivationComponentsFrom(JsonNode source, Set tr return Stream.of(); }).distinct() .map(convert::linkifyTrait) + .map(QuteDataRef::fromMarkdownLink) .forEach(traits::add); return rawComponents.stream().map(convert::replaceText).toList(); @@ -133,14 +123,15 @@ default List getActivationComponentsFrom(JsonNode source, Set tr default List getAbilityOrAfflictionsFrom(JsonNode source, JsonSource convert) { return streamFrom(source) .map(n -> switch (requireNonNullElse(AppendTypeValue.getBlockType(n), AppendTypeValue.ability)) { - case affliction -> (QuteAbilityOrAffliction) Pf2eAffliction.createInlineAffliction(n, convert); - case ability -> Pf2eAbility.createEmbeddedAbility(n, convert); + case affliction -> new Json2QuteAffliction(convert.index(), Pf2eIndexType.affliction, n, true); + case ability -> new Json2QuteAbility(convert.index(), n, true); default -> { convert.tui().debugf("Unexpected block type in %s", source.toPrettyString()); yield null; } }) .filter(Objects::nonNull) + .map(json2Qute -> (QuteAbilityOrAffliction) json2Qute.buildNote()) .toList(); } @@ -518,26 +509,21 @@ enum Pf2eNumberUnitEntry implements Pf2eJsonNodeReader { */ private static QuteDataActivity getActivity(JsonNode node, JsonSource convert) { String actionType = unit.getTextOrNull(node); - Pf2eActivity activity = switch (actionType) { - case "single", "action", "free", "reaction" -> - Pf2eActivity.toActivity(actionType, number.intOrThrow(node)); - case "varies" -> Pf2eActivity.varies; - case "day", "minute", "hour", "round" -> Pf2eActivity.timed; - default -> null; - }; - - if (activity == null) { - throw new IllegalArgumentException("Can't parse activity from: %s".formatted(node)); - } String extra = entry.getTextFrom(node) - .filter(s -> !s.toLowerCase().contains("varies")) - .filter(Predicate.not(String::isBlank)) - .map(convert::replaceText).map(StringUtil::parenthesize) - .orElse(""); - - return activity.toQuteActivity( - convert, activity == Pf2eActivity.timed ? join(" ", number.intOrThrow(node), actionType, extra) : extra); + .filter(s -> !s.toLowerCase().contains("varies")) + .filter(Predicate.not(String::isBlank)) + .map(convert::replaceText).map(StringUtil::parenthesize) + .orElse(""); + + return switch (actionType) { + case "single", "action", "free", "reaction", "varies" -> + Pf2eActivity.toQuteActivity(convert, actionType, number.intOrDefault(node, 0), extra); + case "day", "minute", "hour", "round" -> + Pf2eActivity.toQuteActivity( + convert, Activity.timed, join(" ", number.intOrThrow(node), actionType, extra)); + default -> throw new IllegalArgumentException("Can't parse activity from: %s".formatted(node)); + }; } /** @@ -565,9 +551,8 @@ private static QuteDataDuration getDuration(JsonNode node, JsonSource convert) { return null; } // The activity is more specific unless we have a custom display. Otherwise, fall back to the timed duration - return Optional.ofNullable(Pf2eActivity.toActivity(unitText, timedDuration.value())) - .map(a -> (QuteDataDuration) a.toQuteActivity(convert, null)) - .orElse(timedDuration); + return requireNonNullElse( + Pf2eActivity.toQuteActivity(convert, unitText, timedDuration.value(), null), timedDuration); } /** @@ -677,12 +662,12 @@ public static QuteInlineAttack getAttack(JsonNode node, JsonSource convert) { return new QuteInlineAttack( name.replaceTextFrom(node, convert), Optional.ofNullable(activity.getActivityFrom(node, convert)) - .orElse(Pf2eActivity.single.toQuteActivity(convert, "")), + .orElse(Pf2eActivity.toQuteActivity(convert, Activity.single, "")), QuteInlineAttack.AttackRangeType.valueOf(range.getTextOrDefault(node, "Melee").toUpperCase()), attack.intOrNull(node), formattedDamage, types.replaceTextFromList(node, convert), - convert.collectTraitsFrom(node, null), + convert.getTraits(node), hasMultilineEffect ? List.of() : attackEffects, hasMultilineEffect ? String.join("\n", attackEffects) : null, noMAP.booleanOrDefault(node, false) ? List.of() : List.of("no multiple attack penalty"), @@ -729,10 +714,11 @@ public static QuteDataNamedBonus getNamedBonus( return new QuteDataNamedBonus( displayName, std.intOrThrow(source), - convert.streamPropsExcluding(source, std, note) + convert.streamPropsExcluding(source, std, note, abilities, notes) .collect(Collectors.toMap(e -> convert.replaceText(e.getKey()), e -> e.getValue().asInt())), - note.getTextFrom(source).map(convert::replaceText).map(List::of) - .orElse((abilities.existsIn(source) ? abilities : notes).replaceTextFromList(source, convert))); + Stream.of(abilities, note, notes) + .flatMap(field -> field.replaceTextFromList(source, convert).stream()) + .toList()); } } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eMarkdown.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eMarkdown.java index 569eda6b9..613ef1f3f 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eMarkdown.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eMarkdown.java @@ -107,9 +107,9 @@ private Pf2eMarkdown writeNotesAndTables(List types) { } switch (type) { - case ability -> rules.add(new Json2QuteAbility(index, type, node).buildNote()); + case ability -> rules.add(new Json2QuteAbility(index, node, false).buildNote()); case affliction, curse, disease -> - compendium.add(new Json2QuteAffliction(index, type, node).buildNote()); + compendium.add(new Json2QuteAffliction(index, type, node, false).buildNote()); case book -> { index.tui().progressf("book %s", e.getKey()); JsonNode data = index.getIncludedNode(key.replace("book|", "data|")); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbility.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbility.java index b26591dd3..ca941b17a 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbility.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbility.java @@ -29,10 +29,10 @@ public final class QuteAbility extends Pf2eQuteNote implements QuteUtil.Renderab /** A formatted string which is a link to the base ability that this ability references. Embedded only. */ public final String reference; /** - * Collection of trait links. Use `{#for}` or `{#each}` to iterate over the collection. - * See [traitList](#traitlist) or [bareTraitList](#baretraitlist). + * Collection of trait links as {@link QuteDataRef}. Use `{#for}` or `{#each}` to iterate over the collection. + * See {@link QuteAbility#getBareTraitList()}. */ - public final Collection traits; + public final Collection traits; /** {@link QuteDataRange}. The targeting range for this ability. */ public final QuteDataRange range; /** List of formatted strings. Activation components for this ability, e.g. command, envision */ @@ -65,8 +65,8 @@ public final class QuteAbility extends Pf2eQuteNote implements QuteUtil.Renderab // Internal only. private final JsonSource _converter; - public QuteAbility(Pf2eSources sources, String name, String reference, String text, Tags tags, - Collection traits, QuteDataActivity activity, QuteDataRange range, + public QuteAbility(Pf2eSources sources, String name, String reference, List text, Tags tags, + Collection traits, QuteDataActivity activity, QuteDataRange range, List components, String requirements, String prerequisites, String cost, String trigger, QuteDataFrequency frequency, String special, String note, boolean embedded, JsonSource converter) { @@ -88,28 +88,24 @@ public QuteAbility(Pf2eSources sources, String name, String reference, String te this._converter = converter; } - /** True if an activity (with text), components, or traits are present. */ - public boolean getHasActivity() { - return activity != null || isPresent(components) || isPresent(traits); - } - /** - * True if hasActivity is true, hasEffect is true or cost is present. - * In other words, this is true if a list of attributes could have been rendered. + * True if we have any details other than an activity, an effect, and components. e.g. if we have a cost, range, + * requirements, prerequisites, trigger, frequency, or special. * - * Use this to test for the end of those attributes (add whitespace or a special - * character ahead of ability text) + *

Use this to test for the end of those attributes (e.g. to add whitespace or a special + * character ahead of ability text)

*/ public boolean getHasAttributes() { - return getHasActivity() || getHasEffect() || isPresent(cost); + return isPresent(range) || isPresent(requirements) || isPresent(prerequisites) || isPresent(cost) + || isPresent(trigger) || isPresent(frequency) || isPresent(special) || isPresent(note); } /** - * True if the ability is a short, one-line name and description. + * False if the ability is a short, one-line name and description. * Use this to test to choose between a detailed or simple rendering. */ public boolean getHasDetails() { - return getHasAttributes() || isPresent(special) || text.contains("\n") || text.split(" ").length > 5; + return getHasAttributes() || text.contains("\n") || text.split(" ").length > 5; } @Deprecated @@ -117,18 +113,13 @@ public boolean getHasBullets() { return getHasAttributes(); } - /** True if frequency, trigger, and requirements are present. In other words, this is true if the ability has an effect. */ - public boolean getHasEffect() { - return isPresent(frequency) || isPresent(trigger) || isPresent(requirements); - } - /** Return a comma-separated list of de-styled trait links (no title attributes) */ public String getBareTraitList() { if (traits == null || traits.isEmpty()) { return ""; } return traits.stream() - .map(x -> x.replaceAll(" \".*\"", "")) + .map(QuteDataRef::withoutTitle) .collect(Collectors.joining(", ")); } @@ -143,7 +134,7 @@ public String toString() { } @Override - public String render() { - return _converter.renderEmbeddedTemplate(this, null); + public String render(boolean asYamlStatblock) { + return _converter.renderEmbeddedTemplate(this, null, asYamlStatblock); } } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAction.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAction.java index 7397fe053..f95538f8d 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAction.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAction.java @@ -20,8 +20,8 @@ public class QuteAction extends Pf2eQuteBase { public final String trigger; /** Aliases for this note */ public final List aliases; - /** Collection of traits (decorated links) */ - public final Collection traits; + /** Collection of traits (collection of {@link QuteDataRef}) */ + public final Collection traits; /** Situational requirements for performing this action */ public final String requirements; /** Prerequisite trait or characteristic for performing this action */ @@ -39,7 +39,7 @@ public class QuteAction extends Pf2eQuteBase { public final QuteDataActivity activity; public QuteAction(Pf2eSources sources, List text, Tags tags, - String cost, String trigger, List aliases, Collection traits, + String cost, String trigger, List aliases, Collection traits, String prerequisites, String requirements, QuteDataFrequency frequency, QuteDataActivity activity, ActionType actionType) { super(sources, text, tags); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAffliction.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAffliction.java index 25068da6e..b8665ac8a 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAffliction.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAffliction.java @@ -22,8 +22,8 @@ @TemplateData public final class QuteAffliction extends Pf2eQuteNote implements QuteUtil.Renderable, QuteAbilityOrAffliction { - /** Collection of traits (decorated links) */ - public final Collection traits; + /** Collection of traits ({@link QuteDataRef}) */ + public final Collection traits; /** Integer from 1 to 10. Level of the affliction. */ public final String level; /** Aliases for this note. Only populated if not embedded. */ @@ -58,7 +58,7 @@ public final class QuteAffliction extends Pf2eQuteNote implements QuteUtil.Rende public QuteAffliction( Pf2eSources sources, String name, List text, Tags tags, - Collection traits, List aliases, String level, + Collection traits, List aliases, String level, String category, String maxDuration, String onset, QuteAfflictionSave savingThrow, String effect, String temptedCurse, List notes, Map stages, boolean isEmbedded, JsonTextConverter _converter) { @@ -95,8 +95,8 @@ public String template() { } @Override - public String render() { - return _converter.renderInlineTemplate(this, null); + public String render(boolean asYamlStatblock) { + return _converter.renderEmbeddedTemplate(this, null, asYamlStatblock); } @Override diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteArchetype.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteArchetype.java index 53ec98b96..cdcd377ec 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteArchetype.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteArchetype.java @@ -15,15 +15,15 @@ @TemplateData public class QuteArchetype extends Pf2eQuteBase { - /** Collection of traits (decorated links) */ - public final Collection traits; + /** Collection of traits (collection of {@link QuteDataRef}) */ + public final Collection traits; public final int dedicationLevel; public final List benefits; public final List feats; public QuteArchetype(Pf2eSources sources, List text, Tags tags, - Collection traits, int dedicationLevel, List benefits, List feats) { + Collection traits, int dedicationLevel, List benefits, List feats) { super(sources, text, tags); this.traits = traits; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java index b24c918eb..46dc498be 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java @@ -1,7 +1,7 @@ package dev.ebullient.convert.tools.pf2e.qute; import static dev.ebullient.convert.StringUtil.flatJoin; -import static dev.ebullient.convert.StringUtil.format; +import static dev.ebullient.convert.StringUtil.formatIfPresent; import static dev.ebullient.convert.StringUtil.join; import static dev.ebullient.convert.StringUtil.parenthesize; import static dev.ebullient.convert.StringUtil.pluralize; @@ -11,9 +11,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import dev.ebullient.convert.StringUtil; import dev.ebullient.convert.io.JavadocVerbatim; import dev.ebullient.convert.qute.QuteUtil; import dev.ebullient.convert.tools.Tags; @@ -30,8 +28,8 @@ public class QuteCreature extends Pf2eQuteBase { /** Aliases for this note (optional) */ public final List aliases; - /** Collection of traits (decorated links, optional) */ - public final Collection traits; + /** Collection of traits (collection of {@link QuteDataRef}) */ + public final Collection traits; /** Short creature description (optional) */ public final String description; /** Creature level (number, optional) */ @@ -70,8 +68,8 @@ public class QuteCreature extends Pf2eQuteBase { public final List ritualCasting; public QuteCreature( - Pf2eSources sources, String text, Tags tags, - Collection traits, List aliases, + Pf2eSources sources, List text, Tags tags, + Collection traits, List aliases, String description, Integer level, Integer perception, QuteDataDefenses defenses, CreatureLanguages languages, CreatureSkills skills, List senses, Map abilityMods, @@ -97,6 +95,27 @@ public QuteCreature( this.ritualCasting = ritualCasting; } + /** Return the size of this creature, or null if it has none. */ + public String getSize() { + return traits.stream() + .filter(ref -> ref.title() != null && ref.title().contains("Size Trait")) + .findAny().map(QuteDataRef::displayText).orElse(null); + } + + /** The alignment of this creature, or null if it has none. */ + public String getAlignment() { + return traits.stream() + .filter(ref -> ref.title() != null && ref.title().contains("Alignment Trait")) + .findAny().map(QuteDataRef::displayText).orElse(null); + } + + /** The rarity of this creature, or null if it has none. */ + public String getRarity() { + return traits.stream() + .filter(ref -> ref.title() != null && ref.title().contains("Rarity Trait")) + .findAny().map(QuteDataRef::displayText).orElse(null); + } + /** * The languages and language features known by a creature. Example default output: * `Common, Sylvan; telepathy 100ft; knows any language the summoner does` @@ -184,6 +203,10 @@ public record CreatureAbilities( List top, List middle, List bottom) implements QuteUtil { + /** Return abilities as a map. */ + public Map> getAbilityMap() { + return Map.of("top", top, "mid", middle, "bot", bottom); + } } /** @@ -252,8 +275,8 @@ public String name() { @JavadocVerbatim public String formattedStats() { return join(", ", - format("DC %d", dc), - format("attack %+d", attackBonus), + formatIfPresent("DC %d", dc), + formatIfPresent("attack %+d", attackBonus), focusPoints == null ? "" : focusPoints + " Focus " + pluralize("Point", focusPoints)); } } @@ -301,9 +324,9 @@ public String rank() { @Override public String toString() { return join(" ", - format("**%s**", rank()), + formatIfPresent("**%s**", rank()), join(", ", spells), - format("(%d slots)", slots)); + formatIfPresent("(%d slots)", slots)); } } @@ -315,7 +338,7 @@ public String toString() { * ``` * * @param name The name of the spell - * @param link A formatted link to the spell's note, or just the spell's name if we couldn't get a link. + * @param spellRef A {@link QuteDataRef} to the spell's note, or null if we couldn't find a note * @param amount The number of casts available for this spell. A value of 0 represents an at will spell. Use * {@link QuteCreature.CreatureSpellReference#formattedAmount()} to get this as a formatted string. * @param notes Any notes associated with this spell, e.g. "at will only" @@ -323,22 +346,28 @@ public String toString() { @TemplateData public record CreatureSpellReference( String name, - String link, + QuteDataRef spellRef, Integer amount, - List notes) { + List notes) implements QuteDataGenericStat { + + @Override + public Integer value() { + return amount; + } /** The number of casts as a formatted string, e.g. "(at will)" or "(×2)". Empty when the amount is 1. */ public String formattedAmount() { return amount == 1 ? "" : parenthesize(amount == 0 ? "at will" : "×" + amount); } - public String formattedNotes() { - return notes.stream().map(StringUtil::parenthesize).collect(Collectors.joining(" ")); - } - @Override public String toString() { - return join(" ", link, formattedAmount(), formattedNotes()); + if (notes.size() == 1 && notes.get(0).equals("*")) { + // Workaround for specific statblocks which use "*" to tag particular spells. Use a carat instead so it doesn't + // get Markdown formatted. + return join(" ", (spellRef == null ? name : spellRef.toString()) + "^", formattedAmount()); + } + return join(" ", spellRef == null ? name : spellRef, formattedAmount(), formattedNotes()); } } } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataActivity.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataActivity.java index f89ad9a53..f442dacbc 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataActivity.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataActivity.java @@ -2,7 +2,6 @@ import static dev.ebullient.convert.StringUtil.join; -import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.QuteUtil; import io.quarkus.qute.TemplateData; @@ -10,25 +9,55 @@ * Pf2eTools activity attributes. This attribute will render itself as a formatted link: * *
- *     [textGlyph](rulesPath "glyph.title")<optional text>
+ *     [textGlyph](rulesPath "action name")<optional text>
  * 
* - * @param text The text associated with the action - may be null. - * @param glyph icon/image representing this activity as a {@link dev.ebullient.convert.qute.ImageRef ImageRef} - * @param textGlyph A textual representation of the glyph, used as the link text - * @param rulesPath The path which leads to an explanation of this particular activity + * @param activity The type of activity, as a {@link QuteDataActivity.Activity} + * @param actionRef A {@link QuteDataRef} to the rules for this particular action type + * @param note Text associated with this activity */ @TemplateData -public record QuteDataActivity(String text, ImageRef glyph, String textGlyph, - String rulesPath) implements QuteUtil, QuteDataDuration { +public record QuteDataActivity(Activity activity, QuteDataRef actionRef, String note) implements QuteUtil, QuteDataDuration { + + public QuteDataActivity(Activity activity, String rulesPath, String note) { + this(activity, new QuteDataRef(activity.textGlyph, rulesPath, activity.longName), note); + } /** Return the text associated with the action. */ - @Override public String text() { - return isPresent(text) ? text : glyph.title; + return isPresent(note) ? note : activity.longName; + } + + /** + * Return the single-character Pathfinder 2e font unicode glyph used to represent this action, or if there is no single + * character (eg for varies and duration activities), return {@link QuteDataActivity#text()}. + */ + public String getUnicodeGlyphOrText() { + return activity.unicodeChar != null ? activity.unicodeChar.toString() : text(); } + @Override public String toString() { - return join(" ", "[%s](%s \"%s\")".formatted(textGlyph, rulesPath, glyph.title), text); + return join(" ", actionRef.toString(), note); + } + + public enum Activity { + single("Single Action", ">", '⬻'), + two("Two-Action", ">>", '⬺'), + three("Three-Action", ">>>", '⬽'), + free("Free Action", "F", '⭓'), + reaction("Reaction", "R", '⬲'), + varies("Varies", "V", null), + timed("Duration or Frequency", "⏲", null); + + public final String longName; + public final String textGlyph; + public final Character unicodeChar; + + Activity(String longName, String textGlyph, Character unicodeChar) { + this.longName = longName; + this.textGlyph = textGlyph; + this.unicodeChar = unicodeChar; + } } } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java index 7e72e158d..0511e9b12 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java @@ -47,6 +47,11 @@ private String formattedAlternates(boolean asBonus) { formatMap(alternateValues, (k, v) -> parenthesize(join(" ", valFormat.formatted(v), k)))); } + @Override + public String formattedNotes() { + return flatJoin(", ", List.of(formattedAlternates(false)), notes, abilities); + } + @Override public String bonus() { return join(" ", QuteDataGenericStat.super.bonus(), formattedAlternates(true)); @@ -54,6 +59,6 @@ public String bonus() { @Override public String toString() { - return flatJoin(" ", List.of("**AC**", value, formattedAlternates(false)), notes, abilities); + return join(" ", value, formattedNotes()); } } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDefenses.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDefenses.java index 5fd5abd1d..7ba14b272 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDefenses.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDefenses.java @@ -1,13 +1,14 @@ package dev.ebullient.convert.tools.pf2e.qute; +import static dev.ebullient.convert.StringUtil.formatIfPresent; import static dev.ebullient.convert.StringUtil.formatMap; import static dev.ebullient.convert.StringUtil.join; -import static dev.ebullient.convert.StringUtil.joinWithPrefix; import static dev.ebullient.convert.StringUtil.joiningNonEmpty; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import java.util.stream.Stream; import dev.ebullient.convert.qute.QuteUtil; @@ -50,20 +51,37 @@ public record QuteDataDefenses( Map resistances, Map weaknesses) implements QuteUtil { + @SuppressWarnings("unused") // for template use + public String getAdditionalHp() { + return additionalHpHardnessBt.entrySet().stream() + .filter(e -> e.getValue().hp() != null) + .map(e -> "__%s HP__ %s".formatted(e.getKey(), e.getValue().hp())) + .collect(Collectors.joining("; ")); + } + + @SuppressWarnings("unused") // for template use + public String getAdditionalHardness() { + return additionalHpHardnessBt.entrySet().stream() + .filter(e -> e.getValue().hardness() != null) + .map(e -> "__%s Hardness__ %s".formatted(e.getKey(), e.getValue().hardness())) + .collect(Collectors.joining("; ")); + } + @Override public String toString() { return join("\n", // - **AC** 21; **Fort** +15, **Ref** +12, **Will** +10 - joinWithPrefix("; ", "- ", ac, savingThrows), + formatIfPresent("- %s", + join("; ", formatIfPresent("**AC** %s", ac), savingThrows)), // - **Hardness** 18, **HP (BT)** 10; **Immunities** critical hits; **Resistances** fire 5 - joinWithPrefix("; ", "- ", + formatIfPresent("- %s", join("; ", hpHardnessBt, join("; ", formatMap(additionalHpHardnessBt, (k, v) -> v.toStringWithName(k))), - joinWithPrefix(", ", "**Immunities** ", immunities), + formatIfPresent("**Immunities** %s", join(", ", immunities)), formatMap(resistances, (k, v) -> join(" ", k, v)).stream().sorted() .collect(joiningNonEmpty(", ", "**Resistances** ")), formatMap(weaknesses, (k, v) -> join(" ", k, v)).stream().sorted() - .collect(joiningNonEmpty(", ", "**Weaknesses** ")))); + .collect(joiningNonEmpty(", ", "**Weaknesses** "))))); } /** @@ -83,6 +101,12 @@ public String toString() { public record QuteSavingThrows( QuteDataNamedBonus fort, QuteDataNamedBonus ref, QuteDataNamedBonus will, List abilities) implements QuteUtil { + + /** Return the saves as a list of {@link QuteDataGenericStat.QuteDataNamedBonus}. */ + public List getSaves() { + return List.of(fort, ref, will); + } + /** Returns all abilities as a formatted, comma-separated string. */ public String formattedAbilities() { return join(", ", abilities); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataRef.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataRef.java new file mode 100644 index 000000000..d689a82b7 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataRef.java @@ -0,0 +1,68 @@ +package dev.ebullient.convert.tools.pf2e.qute; + +import static dev.ebullient.convert.StringUtil.formatIfPresent; +import static dev.ebullient.convert.StringUtil.isPresent; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A reference to another note. This will render itself as a formatted link. + * + * @param displayText The display text for the link + * @param notePath The path to the note that this link references. Null if we couldn't find a note to reference. + * @param title The hover title to use for the reference (optional) + */ +public record QuteDataRef(String displayText, String notePath, String title) implements Comparable { + + private static final Pattern MARKDOWN_LINK_PAT = Pattern.compile("^\\[(?.+)]\\((?.*?)(?: \"(?.*)\")?\\)$"); + + public QuteDataRef(String displayText) { + this(displayText, null, null); + } + + public static QuteDataRef fromMarkdownLink(String link) { + if (!isPresent(link)) { + return null; + } + Matcher matcher = MARKDOWN_LINK_PAT.matcher(link); + return matcher.matches() + ? new QuteDataRef(matcher.group("display"), matcher.group("path"), matcher.group("title")) + : new QuteDataRef(link); + } + + /** Return this reference as a Markdown link, without the title attribute. */ + public String withoutTitle() { + return notePath != null ? "[%s](%s)".formatted(displayText, notePath) : displayText; + } + + @Override + public String toString() { + return notePath != null + ? "[%s](%s%s)".formatted(displayText, notePath, formatIfPresent(" \"%s\"", title)) + : displayText; + } + + @Override + public int compareTo(QuteDataRef o) { + if (!displayText.equals(o.displayText)) { + return displayText.compareTo(o.displayText); + } else if (!Objects.equals(notePath, o.notePath)) { + if (notePath == null) { + return -1; + } else if (o.notePath == null) { + return 1; + } + return notePath.compareTo(o.notePath); + } else if (!Objects.equals(title, o.title)) { + if (title == null) { + return -1; + } else if (o.title == null) { + return 1; + } + return title.compareTo(o.title); + } + return 0; + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataTraits.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataTraits.java new file mode 100644 index 000000000..e207e20c2 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataTraits.java @@ -0,0 +1,142 @@ +package dev.ebullient.convert.tools.pf2e.qute; + +import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.joiningNonEmpty; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import dev.ebullient.convert.tools.Tags; + +/** A collection of traits stored as {@link QuteDataRef}s to the trait note. */ +public class QuteDataTraits implements Collection<QuteDataRef> { + private final Set<QuteDataRef> refs = new TreeSet<>(); + + public QuteDataTraits() {} + + /** The first trait which has this category, as a {@link QuteDataRef}. Case-sensitive. */ + public QuteDataRef getFirst(String category) { + return refs.stream().filter(ref -> ref.title() != null && ref.title().contains(category)).findFirst().orElse(null); + } + + /** Return these traits as a comma-delimited string without any extra formatting (eg no title attributes). */ + public String formattedWithoutTitles() { + return refs.stream().map(QuteDataRef::withoutTitle).collect(joiningNonEmpty(", ")); + } + + /** Return traits without any size, alignment, or rarity traits. */ + public QuteDataTraits getGenericTraits() { + return refs.stream() + .filter(ref -> !isPresent(ref.title()) || + !(ref.title().contains("Alignment") || ref.title().contains("Size") || ref.title().contains("Rarity"))) + .collect(Collectors.toCollection(QuteDataTraits::new)); + } + + public QuteDataTraits addToTags(Tags tags) { + for (QuteDataRef ref : this) { + tags.add("trait", ref.displayText()); + } + return this; + } + + public QuteDataTraits addTrait(String trait) { + if (isPresent(trait)) { + refs.add(new QuteDataRef(trait)); + } + return this; + } + + public QuteDataTraits addTraits(Collection<String> c) { + c.forEach(this::addTrait); + return this; + } + + @Override + public String toString() { + return join(", ", refs); + } + + @Override + public int size() { + return refs.size(); + } + + @Override + public boolean isEmpty() { + return refs.isEmpty(); + } + + @Override + public boolean contains(Object o) { + if (o instanceof QuteDataRef) { + return refs.contains(o); + } else if (o instanceof String) { + return refs.stream().anyMatch(ref -> ref.displayText().equals(o)); + } + return false; + } + + @Override + public boolean remove(Object o) { + if (o instanceof QuteDataRef) { + return refs.remove(o); + } else if (o instanceof String) { + Optional<QuteDataRef> ref = refs.stream().filter(r -> r.displayText().equals(o)).findAny(); + if (ref.isEmpty()) { + return false; + } + return refs.remove(ref); + } + return false; + } + + @Override + public boolean add(QuteDataRef quteDataRef) { + return refs.add(quteDataRef); + } + + @Override + public Iterator<QuteDataRef> iterator() { + return refs.iterator(); + } + + @Override + public Object[] toArray() { + return refs.toArray(); + } + + @Override + public <T> T[] toArray(T[] a) { + return refs.toArray(a); + } + + @Override + public boolean containsAll(Collection<?> c) { + return c.stream().allMatch(this::contains); + } + + @Override + public boolean addAll(Collection<? extends QuteDataRef> c) { + return c.stream().allMatch(this::add); + } + + @Override + public boolean removeAll(Collection<?> c) { + return c.stream().allMatch(this::remove); + } + + @Override + public boolean retainAll(Collection<?> c) { + return refs.removeIf(ref -> !c.contains(ref) && !c.contains(ref.displayText())); + } + + @Override + public void clear() { + refs.clear(); + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteFeat.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteFeat.java index fc9f64fe0..1285281ee 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteFeat.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteFeat.java @@ -22,8 +22,8 @@ @TemplateData public class QuteFeat extends Pf2eQuteBase { - /** Collection of traits (decorated links) */ - public final Collection<String> traits; + /** Collection of traits (collection of {@link QuteDataRef}) */ + public final Collection<QuteDataRef> traits; /** Aliases for this note */ public final List<String> aliases; @@ -51,7 +51,7 @@ public class QuteFeat extends Pf2eQuteBase { public final boolean embedded; public QuteFeat(Pf2eSources sources, List<String> text, Tags tags, - Collection<String> traits, List<String> aliases, + Collection<QuteDataRef> traits, List<String> aliases, String level, String access, QuteDataFrequency frequency, QuteDataActivity activity, String trigger, String cost, String requirements, String prerequisites, String special, String note, List<String> leadsTo, boolean embedded) { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteHazard.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteHazard.java index a869db30c..0035c1def 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteHazard.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteHazard.java @@ -26,8 +26,8 @@ @TemplateData public class QuteHazard extends Pf2eQuteBase { - /** Collection of traits (decorated links) */ - public final Collection<String> traits; + /** Collection of traits (collection of {@link QuteDataRef}) */ + public final Collection<QuteDataRef> traits; public final String level; public final String disable; @@ -83,7 +83,7 @@ public class QuteHazard extends Pf2eQuteBase { public final QuteDataGenericStat perception; public QuteHazard(Pf2eSources sources, List<String> text, Tags tags, - Collection<String> traits, String level, String disable, + Collection<QuteDataRef> traits, String level, String disable, String reset, String routine, QuteDataDefenses defenses, List<QuteInlineAttack> attacks, List<QuteAbility> abilities, List<QuteAbilityOrAffliction> actions, QuteHazardStealth stealth, QuteDataGenericStat perception) { @@ -102,7 +102,7 @@ public QuteHazard(Pf2eSources sources, List<String> text, Tags tags, } public String getComplexity() { - if (traits == null || traits.stream().noneMatch(t -> t.contains("complex"))) { + if (traits == null || traits.stream().noneMatch(ref -> ref.displayText().equalsIgnoreCase("complex"))) { return "Simple"; } return "Complex"; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteInlineAttack.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteInlineAttack.java index 071c97558..a36258a09 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteInlineAttack.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteInlineAttack.java @@ -1,7 +1,5 @@ package dev.ebullient.convert.tools.pf2e.qute; -import static dev.ebullient.convert.StringUtil.join; -import static dev.ebullient.convert.StringUtil.parenthesize; import static dev.ebullient.convert.StringUtil.toTitleCase; import java.util.Collection; @@ -46,8 +44,8 @@ public final class QuteInlineAttack implements QuteDataGenericStat, QuteUtil.Ren */ public final Collection<String> damageTypes; - /** Any traits associated with the attack (collection of decorated links) */ - public final Collection<String> traits; + /** Traits associated with the attack as a {@link QuteDataTraits} */ + public final QuteDataTraits traits; /** * Any additional effects associated with the attack e.g. grab (list of strings). Effects listed here @@ -66,7 +64,7 @@ public final class QuteInlineAttack implements QuteDataGenericStat, QuteUtil.Ren public QuteInlineAttack( String name, QuteDataActivity activity, AttackRangeType rangeType, Integer attackBonus, String damage, - Collection<String> damageTypes, Collection<String> traits, List<String> effects, String multilineEffect, + Collection<String> damageTypes, QuteDataTraits traits, List<String> effects, String multilineEffect, List<String> notes, JsonTextConverter<?> converter) { this.name = name; this.activity = activity; @@ -83,7 +81,7 @@ public QuteInlineAttack( public QuteInlineAttack( String name, QuteDataActivity activity, AttackRangeType rangeType, String damage, - Collection<String> damageTypes, Collection<String> traits, String note, + Collection<String> damageTypes, QuteDataTraits traits, String note, JsonTextConverter<?> converter) { this( name, activity, rangeType, null, @@ -107,8 +105,8 @@ public String template() { } @Override - public String render() { - return _converter.renderInlineTemplate(this, null); + public String render(boolean asYamlStatblock) { + return _converter.renderEmbeddedTemplate(this, null, asYamlStatblock); } @Override @@ -116,11 +114,6 @@ public String toString() { return render(); } - /** Return traits formatted as a single string, e.g. {@code (agile, trip, finesse)} */ - public String formattedTraits() { - return parenthesize(join(", ", traits)); - } - public enum AttackRangeType { RANGED, MELEE; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteItem.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteItem.java index e599d4fe6..5fb9bf53d 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteItem.java @@ -1,5 +1,9 @@ package dev.ebullient.convert.tools.pf2e.qute; +import static dev.ebullient.convert.StringUtil.formatAsModifier; +import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.valueOrDefault; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -19,8 +23,8 @@ @TemplateData public class QuteItem extends Pf2eQuteBase { - /** Collection of traits (decorated links) */ - public final Collection<String> traits; + /** Collection of traits (collection of {@link QuteDataRef}) */ + public final Collection<QuteDataRef> traits; /** Aliases for this note */ public final List<String> aliases; /** @@ -66,7 +70,7 @@ public class QuteItem extends Pf2eQuteBase { public final List<QuteItemVariant> variants; public QuteItem(Pf2eSources sources, List<String> text, Tags tags, - Collection<String> traits, List<String> aliases, QuteItemActivate activate, + Collection<QuteDataRef> traits, List<String> aliases, QuteItemActivate activate, String price, String ammunition, String level, String onset, String access, String duration, String category, String group, String hands, Collection<NamedText> usage, Collection<NamedText> contract, @@ -167,41 +171,38 @@ public String toString() { } /** - * Pf2eTools item armor attributes + * Armor statistics + * <blockquote> + * <b>AC Bonus</b> +2; <b>Dex Cap</b> +0; <b>Check Penalty</b> -3; <b>Speed Penalty</b> -10 ft; <b>Strength</b> 14 + * </blockquote> * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly: `{resource.armor}`. + * + * @param acBonus AC bonus granted by the armor as a {@link QuteDataArmorClass}. Never null. + * @param dexCap The dex modifier cap that applies while wearing this armor, or null. + * @param strengthReq The strength requirement to reduce some penalties while wearing the armor, or null. + * @param checkPenalty The penalty to Strength-and-Dex-based skill checks that apply if the strength requirement is not met. + * Integer, always negative or 0. + * @param speedPenalty The penalty to speed that applies when wearing this armor. Integer, always negative or 0. */ @TemplateData - public static class QuteItemArmorData implements QuteUtil { - /** {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataArmorClass Item armor class details} */ - public QuteDataArmorClass ac; - /** Formatted string. Dex cap */ - public String dexCap; - /** Formatted string. Armor strength */ - public String strength; - /** Formatted string. Check penalty */ - public String checkPenalty; - /** Formatted string. Speed penalty */ - public String speedPenalty; - + public record QuteItemArmorData( + QuteDataArmorClass acBonus, + Integer dexCap, + Integer strengthReq, + int checkPenalty, + int speedPenalty + ) implements QuteUtil { + @Override public String toString() { - List<String> parts = new ArrayList<>(); - parts.add("**AC Bonus** " + ac.bonus()); - if (isPresent(dexCap)) { - parts.add("**Dex Cap** " + dexCap); - } - if (isPresent(strength)) { - parts.add("**Strength** " + strength); - } - if (isPresent(checkPenalty)) { - parts.add("**Check Penalty** " + checkPenalty); - } - if (isPresent(speedPenalty)) { - parts.add("**Speed Penalty** " + speedPenalty); - } - return "- " + String.join("; ", parts); + return join("; ", + "**AC Bonus** " + acBonus.bonus(), + "**Dex Cap** " + valueOrDefault(formatAsModifier(dexCap), "—"), + "**Check Penalty** " + (checkPenalty != 0 ? formatAsModifier(checkPenalty) : "—"), + "**Speed Penalty** " + (speedPenalty != 0 ? (formatAsModifier(speedPenalty) + " ft.") : "—"), + "**Strength** " + valueOrDefault(strengthReq, "—")); } } @@ -231,8 +232,8 @@ public String toString() { public static class QuteItemWeaponData implements QuteUtil { /** Formatted string. Weapon type */ public String type; - /** Formatted string. List of traits (links) */ - public Collection<String> traits; + /** Traits as a {@link QuteDataTraits} */ + public QuteDataTraits traits; public Collection<NamedText> ranged; public String damage; public String group; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteRitual.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteRitual.java index 562aff6b8..4a57d0fd1 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteRitual.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteRitual.java @@ -23,8 +23,8 @@ public class QuteRitual extends Pf2eQuteBase { public final String level; /** Type: Ritual (usually) */ public final String ritualType; - /** Collection of traits (decorated links) */ - public final Collection<String> traits; + /** Collection of traits (collection of {@link QuteDataRef}) */ + public final Collection<QuteDataRef> traits; /** Aliases for this note */ public final List<String> aliases; @@ -44,7 +44,7 @@ public class QuteRitual extends Pf2eQuteBase { public final Collection<NamedText> heightened; public QuteRitual(Pf2eSources sources, List<String> text, Tags tags, - String level, String ritualType, Collection<String> traits, List<String> aliases, + String level, String ritualType, Collection<QuteDataRef> traits, List<String> aliases, QuteRitualCasting casting, QuteRitualChecks checks, QuteSpellTarget targeting, String requirements, String duration, Collection<NamedText> heightened) { super(sources, text, tags); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteSpell.java index c53d30dc1..b0c2465ba 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteSpell.java @@ -26,8 +26,8 @@ public class QuteSpell extends Pf2eQuteBase { public final String level; /** Type: spell, cantrip, or focus */ public final String spellType; - /** Collection of traits (decorated links) */ - public final Collection<String> traits; + /** Collection of traits (collection of {@link QuteDataRef}) */ + public final Collection<QuteDataRef> traits; /** Aliases for this note */ public final List<String> aliases; /** @@ -71,7 +71,7 @@ public class QuteSpell extends Pf2eQuteBase { public final Collection<NamedText> heightened; public QuteSpell(Pf2eSources sources, List<String> text, Tags tags, - String level, String spellType, Collection<String> traits, List<String> aliases, + String level, String spellType, Collection<QuteDataRef> traits, List<String> aliases, QuteDataDuration castDuration, List<String> components, String cost, String trigger, String requirements, QuteSpellTarget targeting, QuteSpellSave save, QuteSpellDuration duration, List<String> domains, List<String> traditions, List<String> spellLists, diff --git a/src/main/resources/templates/toolsPf2e/ability2md.txt b/src/main/resources/templates/toolsPf2e/ability2md.txt index 5b5a93a1c..ea5b04cae 100644 --- a/src/main/resources/templates/toolsPf2e/ability2md.txt +++ b/src/main/resources/templates/toolsPf2e/ability2md.txt @@ -26,9 +26,8 @@ aliases: ["{resource.name}"] - **Requirements**: {resource.requirements} {/if}{#if resource.prerequisites} - **Prerequisites**: {resource.prerequisites} -{/if}{#if resource.hasAttributes} +{/if}{#if resource.activity || resource.hasAttributes} -{/if}{#if resource.hasEffect} **Effect** {/if}{resource.text} {#if resource.special} diff --git a/src/main/resources/templates/toolsPf2e/inline-ability2md.txt b/src/main/resources/templates/toolsPf2e/inline-ability2md.txt index 1394f6edf..371486d8d 100644 --- a/src/main/resources/templates/toolsPf2e/inline-ability2md.txt +++ b/src/main/resources/templates/toolsPf2e/inline-ability2md.txt @@ -1,36 +1,57 @@ -```ad-embed-ability -{#if resource.hasDetails } -title: **{#if resource.reference}{resource.reference}{#else}{resource.name}{/if}** {resource.activity}{resource.components.join(", ").prefixSpace}{#if resource.traits} ({resource.bareTraitList}){/if} -{#if resource.note} +{#with resource} +{#let componentsTraits = (components.join(", ") + (components && traits ? " " : "") + (traits ? (str:format("(%s)", bareTraitList)) : ""))} +{#if asYamlStatblock} +- name: {reference.or(name).quoted} +{#if hasDetails && hasAttributes} + desc: >{#if activity || componentsTraits} + {activity.unicodeGlyphOrText ?: ""}{activity && componentsTraits ? " " : ""}{componentsTraits}{#if components && !traits};{/if}{/if}{#if range} + **Range** {range};{/if}{#if cost} + **Cost** {cost};{/if}{#if frequency} + **Frequency** {frequency};{/if}{#if trigger} + **Trigger** {trigger};{/if}{#if requirements} + **Requirements** {requirements};{/if}{#if prerequisites} + **Prerequisites** {prerequisites};{/if} + **Effect** {text.unfoldNewlines indent " "}{#if special}; **Special** {special}{/if} +{#else if hasDetails} + desc: > + {activity.unicodeGlyphOrText ?: ""}{activity && componentsTraits ? " " : ""}{#if activity || componentsTraits && text}{componentsTraits}{#if components && !traits};{/if} {/if}{text.unfoldNewlines indent " "} +{#else} + desc: "{activity.unicodeGlyphOrText ?: ""}{activity && componentsTraits ? " " : ""}{componentsTraits}{#if activity || componentsTraits && text}{componentsTraits}{#if components && !traits};{/if} {/if}{text}" +{/if} +{#else} +{#if hasDetails} +title: **{reference ?: name}**{#if activity} {activity}{/if}{componentsTraits.prefixSpace} +{#if note} > [!pf2-note] -> {resource.note} -{/if}{#if resource.range} -- **Range**: {resource.range} -{/if}{#if resource.cost} -- **Cost**: {resource.cost} -{/if}{#if resource.frequency} -- **Frequency**: {resource.frequency} -{/if}{#if resource.trigger} -- **Trigger**: {resource.trigger} -{/if}{#if resource.requirements} -- **Requirements**: {resource.requirements} -{/if}{#if resource.prerequisites} -- **Prerequisites**: {resource.prerequisites} -{/if}{#if resource.hasAttributes} +> {note} +{/if}{#if range} +- **Range**: {range} +{/if}{#if cost} +- **Cost**: {cost} +{/if}{#if frequency} +- **Frequency**: {frequency} +{/if}{#if trigger} +- **Trigger**: {trigger} +{/if}{#if requirements} +- **Requirements**: {requirements} +{/if}{#if prerequisites} +- **Prerequisites**: {prerequisites} +{/if}{#if hasAttributes} -{/if}{#if resource.hasEffect} -**Effect** {/if}{resource.text} -{#if resource.special} +**Effect** {/if}{text} +{#if special} -**Special**: {resource.special} -{/if}{#if resource.source || resource.tags} +**Special**: {special} +{/if}{#if source || tags} %% -{#if resource.source} -Source: {resource.source}* +{#if source} +Source: {source}* {/if} -{#each resource.tags} #{it} {/each} +{#each tags} #{it} {/each} %%{/if} {#else} -title: **{resource.name}** {resource.text} +title: **{reference ?: name}**{#if activity} {activity}{/if}{componentsTraits.prefixSpace}{#if componentsTraits};{/if} {text} {/if} ``` +{/if} +{/with} diff --git a/src/main/resources/templates/toolsPf2e/inline-affliction2md.txt b/src/main/resources/templates/toolsPf2e/inline-affliction2md.txt index 7d45d370a..dcbed20e7 100644 --- a/src/main/resources/templates/toolsPf2e/inline-affliction2md.txt +++ b/src/main/resources/templates/toolsPf2e/inline-affliction2md.txt @@ -1,34 +1,47 @@ +{#with resource} +{#if asYamlStatblock} +- name: {name.quoted} + desc: > + ({#each traits}{it.withoutTitle}{#if it_hasNext}, {/if}{/each}){#if notes} {notes.join(", ")};{/if}{#if savingThrow} + **Saving Throw** {savingThrow};{/if}{#if onset} + **Onset** {onset};{/if}{#if maxDuration} + **Maximum Duration** {maxDuration};{/if}{#if effect} + **Effect** {effect};{/if} + {#each stages}**{it.key}** {it.value.text}{#if it.value.duration} ({it.value.duration}){/if}{#if it_hasNext}; {/if}{/each} +{#else} ````ad-inline-affliction -{#if resource.name} -title: {resource.name}{#if resource.formattedLevel} _{resource.formattedLevel}_{/if} +{#if name} +title: {name}{#if formattedLevel} _{formattedLevel}_{/if} {/if} -{#if resource.traits} -{#each resource.traits}{it} {/each} -{/if}{#if resource.text} -{resource.text} +{#if traits} +{#each traits}{it} {/each} +{/if}{#if text} +{text} -{/if}{#if resource.notes} -{#each resource.notes}{it}{#if it_hasNext}, {/if}{/each} +{/if}{#if notes} +{#each notes}{it}{#if it_hasNext}, {/if}{/each} -{/if}{#if resource.savingThrow} -- **Saving Throws**: {resource.savingThrow} -{/if}{#if resource.onset} -- **Onset**: {resource.onset} -{/if}{#if resource.maxDuration} -- **Maximum Duration**: {resource.maxDuration} -{/if}{#if resource.effect} +{/if}{#if savingThrow} +- **Saving Throws**: {savingThrow} +{/if}{#if onset} +- **Onset**: {onset} +{/if}{#if maxDuration} +- **Maximum Duration**: {maxDuration} +{/if}{#if effect} -**Effect** {resource.effect} -{/if}{#if resource.stages} +**Effect** {effect} +{/if}{#if stages} ## Stages -{#each resource.stages} +{#each stages} **{it.key}** {it.value.text}{#if it.value.duration } ({it.value.duration}){/if} {/each} -{/if}{#if resource.source} -*Source: {resource.source}* +{/if}{#if source} +*Source: {source}* {/if} -{#if resource.tags}%% {#each resource.tags}#{it} {/each}%%{/if} +{#if tags}%% {#each tags}#{it} {/each}%%{/if} ```` +{/if} +{/with} diff --git a/src/main/resources/templates/toolsPf2e/inline-attack2md.txt b/src/main/resources/templates/toolsPf2e/inline-attack2md.txt index 1c5ad5b56..394c8c817 100644 --- a/src/main/resources/templates/toolsPf2e/inline-attack2md.txt +++ b/src/main/resources/templates/toolsPf2e/inline-attack2md.txt @@ -1,13 +1,28 @@ -{#let r=resource} -{#if r.multilineEffect} +{#with resource} +{#if asYamlStatblock} +- name: ___{rangeType}___ {activity.unicodeGlyphOrText} {name} + {#if traits} + desc: ({traits.formattedWithoutTitles}) + {/if} + bonus: {attackBonus} + {#if multilineEffect} + damage: > + {damage}; {multilineEffect.unfoldNewlines indent " "} + {#else} + damage: {damage.quoted} + {/if} +{#else} +{#if multilineEffect} ```ad-inline-attack -title: {r.rangeType} {r.activity} {r.name.capitalized}{r.bonus.prefixSpace}{r.formattedTraits.prefixSpace} -{#if r.damage} -**Damage** {r.damage} -{/if}{#if r.multilineEffect} -**Effect** {r.multilineEffect} +title: {rangeType} {activity} {name.capitalized}{bonus.prefixSpace}{#if traits} ({traits}){/if} +{#if damage} +**Damage** {damage} +{/if}{#if multilineEffect} +**Effect** {multilineEffect} {/if} ``` {#else} -- **{r.rangeType}** {r.activity} {r.name}{r.bonus.prefixSpace}{r.formattedTraits.prefixSpace}{#if r.damage}, **Damage** {r.damage}{/if} +- **{rangeType}** {activity} {name}{bonus.prefixSpace}{#if traits} ({traits}){/if}{#if damage}, **Damage** {damage}{/if} +{/if} {/if} +{/with} diff --git a/src/main/resources/templates/toolsPf2e/item2md.txt b/src/main/resources/templates/toolsPf2e/item2md.txt index 48b04cbd7..d6dc04531 100644 --- a/src/main/resources/templates/toolsPf2e/item2md.txt +++ b/src/main/resources/templates/toolsPf2e/item2md.txt @@ -34,7 +34,7 @@ aliases: ["{resource.name}"{#each resource.aliases}, "{it}"{/each}] {/if}{#if resource.shield } {resource.shield} {/if}{#if resource.armor } -{resource.armor} +- {resource.armor} {/if}{#if resource.weapons } {#each resource.weapons}{it} {/each}{/if}{#if resource.hands } diff --git a/src/test/java/dev/ebullient/convert/TestUtils.java b/src/test/java/dev/ebullient/convert/TestUtils.java index 1bf97ea44..0b64dfd4f 100644 --- a/src/test/java/dev/ebullient/convert/TestUtils.java +++ b/src/test/java/dev/ebullient/convert/TestUtils.java @@ -48,6 +48,9 @@ public class TestUtils { static String GENERATED_DOCS = PROJECT_PATH.resolve("docs/templates").normalize().toAbsolutePath().toString(); + private static final Pattern ASTERISK_START_PROP_PAT = Pattern.compile(":\\s*\\*"); + private static final Pattern SINGLE_ASTERISK_PAT = Pattern.compile("[^*]\\*[^*]"); + // Obnoxious regular expression because markdown links are complicated: // Matches: [link text](vaultPath "title") // - link text is optional, and may contain parentheses. Use a negative lookahead for ]( @@ -231,11 +234,16 @@ public static void commonTests(Path p, String l, List<String> errors) { errors.add(String.format("Found invalid dice roll in %s: %s", p, l)); } } + // Alarm is a basic spell. It should always be linked. If it isn't, // a reference has gone awry somewhere along the way if (p.toString().contains("list-spells-") && l.contains(" Alarm")) { errors.add(String.format("Missing link to Alarm spell in %s: %s", p, l)); } + + if (l.contains("NOT_FOUND")) { + errors.add(String.format("Found NOT_FOUND in %s: %s", p, l)); + } } /** @@ -353,6 +361,50 @@ static List<String> checkDirectoryContents(Path directory, Tui tui, return errors; } + public static List<String> yamlStatblockChecker(Path p, List<String> content) { + List<String> errors = new ArrayList<>(); + boolean found = false; + boolean yaml = false; + boolean index = false; + List<String> statblock = new ArrayList<>(); + + for (String l : content) { + if (l.startsWith("# Index ")) { + index = true; + } else if (l.equals("```statblock")) { + found = yaml = true; // start yaml block + } else if (l.equals("```")) { + yaml = false; // end yaml block + } else if (yaml) { + statblock.add(l); + // Asterisks at the start of values indicate aliases in YAML. If we find this, it's probably not intentional. + if (ASTERISK_START_PROP_PAT.matcher(l).matches()) { + errors.add(String.format("Found '*' property alias in %s: %s", p, l)); + } + // Sometimes statblock text uses asterisks. Double asterisks are usually intentional markdown, but single + // asterisks are suspect and may be asterisks which have snuck in from the data source, and won't be rendered + // literallywill be rendered. + if (SINGLE_ASTERISK_PAT.matcher(l).matches()) { + errors.add(String.format("Found '*' in %s: %s", p, l)); + } + if (l.contains("\"desc\": \"\"")) { + errors.add(String.format("Found empty description in %s: %s", p, l)); + } + } + TestUtils.commonTests(p, l, errors); + } + + try { + Tui.quotedYaml().load(String.join("\n", statblock)); + } catch (Exception e) { + errors.add(String.format("File %s contains invalid yaml: %s", p, e)); + } + if (!found && !index) { + errors.add(String.format("File %s did not contain a yaml statblock", p)); + } + return errors; + } + public static String dump(LaunchResult result) { return "\nSystem out:\n" + result.getOutput() + "\nSystem err:\n" + result.getErrorOutput(); diff --git a/src/test/java/dev/ebullient/convert/tools/TokenizerTest.java b/src/test/java/dev/ebullient/convert/tools/TokenizerTest.java index dbb1b3a3f..d108b2b54 100644 --- a/src/test/java/dev/ebullient/convert/tools/TokenizerTest.java +++ b/src/test/java/dev/ebullient/convert/tools/TokenizerTest.java @@ -42,8 +42,8 @@ public String linkify(IndexType type, String s) { } @Override - public String replaceText(String s) { - throw new UnsupportedOperationException("Unimplemented method 'replaceText'"); + public String replaceTokenText(String s, boolean nested) { + throw new UnsupportedOperationException("Unimplemented method 'replaceTokenText'"); } @Override diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java index 046c3ed50..cfa51124f 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java @@ -377,42 +377,7 @@ public void testMonsterYamlBody(Path outputPath) { Path undead = out.resolve(index.compendiumFilePath()).resolve(Tools5eQuteBase.monsterPath(false, "undead")); assertThat(undead).exists(); - TestUtils.assertDirectoryContents(undead, tui, (p, content) -> { - List<String> errors = new ArrayList<>(); - boolean found = false; - boolean yaml = false; - boolean index = false; - List<String> statblock = new ArrayList<>(); - - for (String l : content) { - if (l.startsWith("# Index ")) { - index = true; - } else if (l.equals("```statblock")) { - found = yaml = true; // start yaml block - } else if (l.equals("```")) { - yaml = false; // end yaml block - } else if (yaml) { - statblock.add(l); - if (l.contains("*")) { - errors.add(String.format("Found '*' in %s: %s", p, l)); - } - if (l.contains("\"desc\": \"\"")) { - errors.add(String.format("Found empty description in %s: %s", p, l)); - } - } - TestUtils.commonTests(p, l, errors); - } - - try { - Tui.quotedYaml().load(String.join("\n", statblock)); - } catch (Exception e) { - errors.add(String.format("File %s contains invalid yaml: %s", p, e)); - } - if (!found && !index) { - errors.add(String.format("File %s did not contain a yaml statblock", p)); - } - return errors; - }); + TestUtils.assertDirectoryContents(undead, tui, TestUtils::yamlStatblockChecker); } } diff --git a/src/test/java/dev/ebullient/convert/tools/pf2e/CommonDataTests.java b/src/test/java/dev/ebullient/convert/tools/pf2e/CommonDataTests.java index b4bfd2b16..4c0f79e73 100644 --- a/src/test/java/dev/ebullient/convert/tools/pf2e/CommonDataTests.java +++ b/src/test/java/dev/ebullient/convert/tools/pf2e/CommonDataTests.java @@ -37,6 +37,7 @@ public class CommonDataTests { enum TestInput { all, + yamlStatblocks, partial, none; } @@ -63,15 +64,16 @@ public CommonDataTests(TestInput variant) throws Exception { if (TestUtils.PATH_PF2E_TOOLS_DATA.toFile().exists()) { switch (variant) { - case all: - configurator.addSources(List.of("*")); - break; - case partial: - configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("pf2e.json")); - break; - case none: - // should be default (CRD) - break; + case all -> configurator.addSources(List.of("*")); + case yamlStatblocks -> configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("pf2e-yaml.json")); + case partial -> configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("pf2e.json")); + // should be default (CRD) + case none -> { + } + } + + if (variant != TestInput.yamlStatblocks) { + templates.setCustomTemplates(TtrpgConfig.getConfig()); } for (String x : List.of("books.json", diff --git a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataYamlStatblockTest.java b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataYamlStatblockTest.java new file mode 100644 index 000000000..327bd09c6 --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataYamlStatblockTest.java @@ -0,0 +1,26 @@ +package dev.ebullient.convert.tools.pf2e; + +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.pf2e.CommonDataTests.TestInput; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class Pf2eJsonDataYamlStatblockTest { + private static CommonDataTests commonTests; + + @BeforeAll + public static void setupDir() throws Exception { + commonTests = new CommonDataTests(TestInput.yamlStatblocks); + } + + @Test + public void testCreature_pf2e() { + Path dir = commonTests.generateNotesForType(Pf2eIndexType.creature); + + TestUtils.assertDirectoryContents(dir, commonTests.tui, TestUtils::yamlStatblockChecker); + } +} diff --git a/src/test/resources/pf2e-yaml.json b/src/test/resources/pf2e-yaml.json new file mode 100644 index 000000000..3d1bece9f --- /dev/null +++ b/src/test/resources/pf2e-yaml.json @@ -0,0 +1,8 @@ +{ + "from": ["*"], + "useDiceRoller": true, + "yamlStatblocks": true, + "template": { + "creature" : "examples/templates/pf2etools/creature2md-yamlStatblock.txt" + } +}