Skip to content

Commit

Permalink
✨ Generate CSS snippet for referenced fonts
Browse files Browse the repository at this point in the history
  • Loading branch information
ebullient committed Oct 17, 2023
1 parent 3621143 commit a826cab
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 3 deletions.
53 changes: 53 additions & 0 deletions src/main/java/dev/ebullient/convert/io/FontRef.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package dev.ebullient.convert.io;

public class FontRef {
/** Font family */
public final String fontFamily;
/** Path to font source (unresolved local or remote) */
public final String sourcePath;

boolean hasTextReference = false;

private FontRef(String fontFamily, String sourcePath) {
this.fontFamily = fontFamily;
this.sourcePath = sourcePath;
}

public void addTextReference() {
hasTextReference = true;
}

public boolean hasTextReference() {
return hasTextReference;
}

@Override
public String toString() {
return "FontRef [fontFamily=" + fontFamily + ", sourcePath=" + sourcePath + "]";
}

public static String fontFamily(String fontPath) {
fontPath = fontPath.trim();
int pos1 = fontPath.lastIndexOf('/');
int pos2 = fontPath.lastIndexOf('.');
if (pos1 > 0 && pos2 > 0) {
fontPath = fontPath.substring(pos1 + 1, pos2);
} else if (pos1 > 0) {
fontPath = fontPath.substring(pos1 + 1);
} else if (pos2 > 0) {
fontPath = fontPath.substring(0, pos2);
}
return fontPath;
}

public static FontRef of(String fontString) {
return of(fontFamily(fontString), fontString);
}

public static FontRef of(String fontFamily, String fontString) {
if (fontString == null || fontString.isEmpty()) {
return null;
}
return new FontRef(fontFamily, fontString);
}
}
21 changes: 21 additions & 0 deletions src/main/java/dev/ebullient/convert/io/Templates.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package dev.ebullient.convert.io;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Collection;

import jakarta.enterprise.context.ApplicationScoped;
Expand Down Expand Up @@ -103,4 +105,23 @@ public String renderIndex(String name, Collection<FileMap> resources) {
return "%% ERROR: " + message + " %%";
}
}

public String renderCss(FontRef fontRef, InputStream data) throws IOException {
Template tpl = customTemplateOrDefault("css-font.txt");
try {
String encoded = Base64.getEncoder().encodeToString(data.readAllBytes());
int extpos = fontRef.sourcePath.lastIndexOf(".");
String type = fontRef.sourcePath.substring(extpos + 1);
return tpl
.data("fontFamily", fontRef.fontFamily)
.data("type", type)
.data("encoded", encoded)
.render();
} catch (TemplateException tex) {
Throwable cause = tex.getCause();
String message = cause != null ? cause.toString() : tex.toString();
tui.error(tex, message);
return "%% ERROR: " + message + " %%";
}
}
}
37 changes: 37 additions & 0 deletions src/main/java/dev/ebullient/convert/io/Tui.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package dev.ebullient.convert.io;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand Down Expand Up @@ -55,6 +57,12 @@

@ApplicationScoped
public class Tui {
static Tui instance;

public static Tui instance() {
return instance;
}

public final static TypeReference<List<String>> LIST_STRING = new TypeReference<>() {
};
public final static TypeReference<List<Integer>> LIST_INT = new TypeReference<>() {
Expand Down Expand Up @@ -176,6 +184,8 @@ public Tui() {
this.err = new PrintWriter(System.err);
this.debug = false;
this.verbose = true;

Tui.instance = this;
}

public void init(CommandSpec spec, boolean debug, boolean verbose) {
Expand Down Expand Up @@ -304,6 +314,33 @@ public Optional<Path> resolvePath(Path path) {
.findFirst();
}

public void copyFonts(Collection<FontRef> fonts, Map<String, String> fallbackPaths) {
for (FontRef fontRef : fonts) {
Path targetPath = output.resolve(Path.of("css-snippets", slugify(fontRef.fontFamily) + ".css"));
targetPath.getParent().toFile().mkdirs();

printlnf("⏱️ Generating CSS snippet for %s", fontRef.sourcePath);
if (fontRef.sourcePath.startsWith("http")) {
try (InputStream is = URI.create(fontRef.sourcePath.replace(" ", "%20")).toURL().openStream()) {
Files.writeString(targetPath, templates.renderCss(fontRef, is));
} catch (IOException e) {
errorf(e, "Unable to copy font from %s to %s", fontRef.sourcePath, targetPath);
}
} else {
Optional<Path> resolvedSource = resolvePath(Path.of(fontRef.sourcePath));
if (resolvedSource.isEmpty()) {
errorf("Unable to find font '%s'", fontRef.sourcePath);
continue;
}
try (BufferedInputStream is = new BufferedInputStream(Files.newInputStream(resolvedSource.get()))) {
Files.writeString(targetPath, templates.renderCss(fontRef, is));
} catch (IOException e) {
errorf(e, "Unable to copy font from %s to %s", fontRef.sourcePath, targetPath);
}
}
}
}

public void copyImages(Collection<ImageRef> images, Map<String, String> fallbackPaths) {
for (ImageRef image : images) {
Path targetPath = output.resolve(image.targetFilePath());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public interface JsonTextReplacement extends JsonTextConverter<Tools5eIndexType>
Pattern dicePattern = Pattern.compile("\\{@(dice|damage) ([^{}]+)}");

Pattern chancePattern = Pattern.compile("\\{@chance ([^}]+)}");
Pattern fontPattern = Pattern.compile("\\{@font ([^}]+)}");
Pattern homebrewPattern = Pattern.compile("\\{@homebrew ([^}]+)}");
Pattern quickRefPattern = Pattern.compile("\\{@quickref ([^}]+)}");
Pattern notePattern = Pattern.compile("\\{@note (\\*|Note:)?\\s?([^}]+)}");
Expand Down Expand Up @@ -186,6 +187,17 @@ default String _replaceTokenText(String input, boolean nested) {
result = linkifyPattern.matcher(result)
.replaceAll(this::linkify);

result = fontPattern.matcher(result)
.replaceAll((match) -> {
String[] parts = match.group(1).split("\\|");
String fontFamily = Tools5eSources.getFontReference(parts[1]);
if (fontFamily != null) {
return String.format("<span style=\"font-family: %s\">%s</span>",
fontFamily, parts[0]);
}
return parts[0];
});

try {
result = result
.replace("{@hitYourSpellAttack}", "the summoner's spell attack modifier")
Expand Down Expand Up @@ -216,8 +228,8 @@ default String _replaceTokenText(String input, boolean nested) {
.replaceAll("\\{@cult ([^|}]+)}", "$1")
.replaceAll("\\{@language ([^|}]+)\\|?[^}]*}", "$1")
.replaceAll("\\{@book ([^}|]+)\\|?[^}]*}", "\"$1\"")
.replaceAll("\\{@hit ([+-][^}<]+)}", "$1")
.replaceAll("\\{@hit ([^}<]+)}", "+$1")
.replaceAll("\\{@(hit|h) ([+-][^}<]+)}", "$2")
.replaceAll("\\{@(hit|h) ([^}<]+)}", "+$2")
.replaceAll("\\{@h}", "*Hit:* ")
.replaceAll("\\{@m}", "*Miss:* ")
.replaceAll("\\{@atk a}", "*Area Attack:*")
Expand All @@ -235,6 +247,7 @@ default String _replaceTokenText(String input, boolean nested) {
.replaceAll("\\{@atk ms}", "*Melee Spell Attack:*")
.replaceAll("\\{@atk rs}", "*Ranged Spell Attack:*")
.replaceAll("\\{@atk ms,rs}", "*Melee or Ranged Spell Attack:*")
.replaceAll("\\{@spell\\s*}", "") // error in homebrew
.replaceAll("\\{@color ([^|}]+)\\|?[^}]*}", "$1")
.replaceAll("\\{@style ([^|}]+)\\|?[^}]*}", "$1")
.replaceAll("\\{@b ([^}]+?)}", "**$1**")
Expand Down Expand Up @@ -269,6 +282,7 @@ default String _replaceTokenText(String input, boolean nested) {
String[] parts = match.group(1).split("\\|");
if (parts[0].contains("<sup>")) {
// This already assumes what the footnote name will be
// TODO: Note content is lost on this path at the moment
return String.format("%s", parts[0]);
}
if (parts.length > 2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ private void indexTypes(String filename, JsonNode node) {
Tools5eIndexType.spellFluff.withArrayFrom(node, this::addToIndex);
Tools5eIndexType.vehicleFluff.withArrayFrom(node, this::addToIndex);

Tools5eIndexType.language.withArrayFrom(node, this::addToIndex);

Tools5eIndexType.itemEntry.withArrayFrom(node, this::addToIndex);
Tools5eIndexType.itemTypeAdditionalEntries.withArrayFrom(node, this::addToIndex);
Tools5eIndexType.magicvariant.withArrayFrom(node, this::addToIndex);
Expand Down Expand Up @@ -220,11 +222,12 @@ private boolean addHomebrewSourcesIfPresent(String filename, JsonNode node) {
}
}

JsonNode fonts = SourceField._meta.getFieldFrom(node, HomebrewFields.fonts);
JsonNode featureTypes = SourceField._meta.getFieldFrom(node, HomebrewFields.optionalFeatureTypes);
JsonNode spellSchools = SourceField._meta.getFieldFrom(node, HomebrewFields.spellSchools);
JsonNode psionicTypes = SourceField._meta.getFieldFrom(node, HomebrewFields.psionicTypes);
JsonNode skillTypes = HomebrewFields.skill.getFrom(node);
if (featureTypes != null || spellSchools != null || psionicTypes != null || skillTypes != null) {
if (fonts != null || featureTypes != null || spellSchools != null || psionicTypes != null || skillTypes != null) {
for (Entry<String, JsonNode> entry : iterableFields(featureTypes)) {
metaTypes.setOptionalFeatureType(entry.getKey(), entry.getValue().asText());
}
Expand All @@ -245,6 +248,17 @@ private boolean addHomebrewSourcesIfPresent(String filename, JsonNode node) {
}
metaTypes.setSkillType(skillName, skill);
}
if (fonts != null) {
if (fonts.isArray()) {
for (JsonNode font : iterableElements(fonts)) {
Tools5eSources.addFont(font.asText());
}
} else {
for (Entry<String, JsonNode> font : iterableFields(fonts)) {
Tools5eSources.addFont(font.getKey(), font.getValue().asText());
}
}
}
}
return true;
}
Expand Down Expand Up @@ -307,6 +321,15 @@ void addToIndex(Tools5eIndexType type, JsonNode node) {
SourceAndPage sp = new SourceAndPage(node);
tableIndex.computeIfAbsent(sp, k -> new ArrayList<>()).add(node);
}
if (type == Tools5eIndexType.language && HomebrewFields.fonts.existsIn(node)) {
// Make a note of all fonts
JsonNode fonts = HomebrewFields.fonts.getFrom(node);
if (fonts.isArray()) {
for (JsonNode font : iterableElements(fonts)) {
Tools5eSources.addFont(font.asText());
}
}
}

if (node.has("srd")) {
srdKeys.add(key);
Expand Down Expand Up @@ -1172,6 +1195,7 @@ public void setItemProperty(String key, CustomItemProperty value) {

enum HomebrewFields implements JsonNodeReader {
abbreviation,
fonts,
full,
json,
optionalFeatureTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader {
itemType,
itemTypeAdditionalEntries,
itemProperty,
language,
legendaryGroup,
magicvariant,
monster,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public Tools5eMarkdownConverter writeNotesAndTables() {

public Tools5eMarkdownConverter writeImages() {
index.tui().copyImages(Tools5eSources.getImages(), fallbackPaths);
index.tui().copyFonts(Tools5eSources.getFonts(), fallbackPaths);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.fasterxml.jackson.databind.JsonNode;

import dev.ebullient.convert.config.TtrpgConfig;
import dev.ebullient.convert.io.FontRef;
import dev.ebullient.convert.io.Tui;
import dev.ebullient.convert.qute.ImageRef;
import dev.ebullient.convert.qute.QuteBase;
import dev.ebullient.convert.tools.CompendiumSources;
Expand All @@ -27,6 +29,7 @@ public class Tools5eSources extends CompendiumSources {

private static final Map<String, Tools5eSources> keyToSources = new HashMap<>();
private static final Map<Path, ImageRef> imageSourceToRef = new HashMap<>();
private static final Map<String, FontRef> fontSourceToRef = new HashMap<>();
private static final Map<String, List<QuteBase>> keyToInlineNotes = new HashMap<>();

public static Tools5eSources findSources(String key) {
Expand Down Expand Up @@ -86,6 +89,39 @@ public void addInlineNote(QuteBase note) {
keyToInlineNotes.computeIfAbsent(this.key, k -> new ArrayList<>()).add(note);
}

public static Collection<FontRef> getFonts() {
return fontSourceToRef.values().stream()
.filter(FontRef::hasTextReference)
.toList();
}

public static void addFont(String fontFamily, String fontString) {
FontRef ref = FontRef.of(fontFamily, fontString);
if (ref == null) {
Tui.instance().warnf("Font '%s' is invalid, empty, or not found", fontString);
} else {
FontRef previous = fontSourceToRef.putIfAbsent(fontFamily, ref);
if (previous != null) {
Tui.instance().warnf("Font '%s' is already defined as '%s'", fontString, previous);
}
}
}

public static void addFont(String fontString) {
String fontFamily = FontRef.fontFamily(fontString);
addFont(fontFamily, fontString);
}

public static String getFontReference(String fontString) {
String fontFamily = FontRef.fontFamily(fontString);
FontRef ref = fontSourceToRef.get(fontFamily);
if (ref == null) {
return null;
}
ref.addTextReference();
return fontFamily;
}

final boolean srd;
final boolean basicRules;
final Tools5eIndexType type;
Expand Down
7 changes: 7 additions & 0 deletions src/main/resources/templates/tools5e/css-font.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@font-face {
font-family: "{fontFamily}";
font-style: normal;
font-display: swap;
font-weight: 400;
src: url("data:font/{type};charset=utf8;base64,{encoded}");
}
1 change: 1 addition & 0 deletions src/test/java/dev/ebullient/convert/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ static List<String> checkDirectoryContents(Path directory, Tui tui,
if (!p.toString().endsWith(".md")) {
if (!p.toString().endsWith(".png")
&& !p.toString().endsWith(".jpg")
&& !p.toString().endsWith(".css")
&& !p.toString().endsWith(".svg")
&& !p.toString().endsWith(".webp")
&& !p.toString().endsWith(".json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ void testLiveData_5eHomebrew(QuarkusMainLauncher launcher) {
TestUtils.TEST_RESOURCES.resolve("psion.json").toString(),
TestUtils.TEST_RESOURCES.resolve("ermis-bg.json").toString(),
TestUtils.HOMEBREW_PATH_5E.resolve("adventure/Anthony Joyce; The Blood Hunter Adventure.json").toString(),
TestUtils.HOMEBREW_PATH_5E.resolve("adventure/JVC Parry; Call from the Deep.json").toString(),
TestUtils.HOMEBREW_PATH_5E.resolve("adventure/Kobold Press; Book of Lairs.json").toString(),
TestUtils.HOMEBREW_PATH_5E.resolve("background/D&D Wiki; Featured Quality Backgrounds.json").toString(),
TestUtils.HOMEBREW_PATH_5E.resolve("book/Ghostfire Gaming; Grim Hollow Campaign Guide.json").toString(),
Expand All @@ -167,6 +168,8 @@ void testLiveData_5eHomebrew(QuarkusMainLauncher launcher) {
TestUtils.HOMEBREW_PATH_5E
.resolve("collection/MCDM Productions; The Talent and Psionics Open Playtest Round 2.json")
.toString(),
TestUtils.HOMEBREW_PATH_5E.resolve("creature/Kobold Press; Creature Codex.json")
.toString(),
TestUtils.HOMEBREW_PATH_5E.resolve("creature/MCDM Productions; Flee, Mortals!.json").toString(),
TestUtils.HOMEBREW_PATH_5E.resolve("creature/Nerzugal Role-Playing; Nerzugal's Extended Bestiary.json")
.toString(),
Expand Down
2 changes: 2 additions & 0 deletions src/test/resources/sources-homebrew.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"B:P",
"BH2022",
"BookofLairs",
"CallfromtheDeep",
"CCodex",
"DM14",
"DM:BFS",
"DMG",
Expand Down

0 comments on commit a826cab

Please sign in to comment.