From 0cd7922e2593f53918e8e2235268e23e13ea3023 Mon Sep 17 00:00:00 2001 From: Michael Schierl Date: Fri, 5 Apr 2024 23:31:16 +0200 Subject: [PATCH] Add Laridian Book Builder export format This has been tested with Laridian Book Builder 2.1.0.624. Note that the resulting Bibles cannot be opened in the current stable version of Laridian Pocket Bible for Windows yet; to test on Windows you need to remove the `pb_biblebooks` meta tag and convert for an older Laridian version. Also, while interlinear bible export has been tested (in the sense that the Book Builder does not show any errors), the resulting Bible file cannot (yet) be tested in Laridian Pocket Bible for Windows (no Interlinear books supported yet). Therefore, treat this code with a grain of salt. I will re-test this once the new Pocket Bible for Windows version has been released. --- README.md | 1 + .../logos/LogosModuleRegistry.java | 1 + ...aridianPocketBibleExtendedInterlinear.java | 60 ++ .../MainModuleRegistry.java | 1 + .../format/LaridianPocketBible.java | 735 ++++++++++++++++++ 5 files changed, 798 insertions(+) create mode 100644 biblemulticonverter-logos/src/main/java/biblemulticonverter/logos/format/LaridianPocketBibleExtendedInterlinear.java create mode 100644 biblemulticonverter/src/main/java/biblemulticonverter/format/LaridianPocketBible.java diff --git a/README.md b/README.md index ca2242d..3816dba 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ In addition, the following other formats are supported, with varying accuracy: - **[MySword](https://www.mysword.info/)**: import and export - **[Obsidian](https://obsidian.md/)**: export only - **[Beblia XML](https://beblia.com/)**: import and export +- **[Laridian Book Builder](https://www.laridian.com/): export only In combination with third party tools, other export formats are available: diff --git a/biblemulticonverter-logos/src/main/java/biblemulticonverter/logos/LogosModuleRegistry.java b/biblemulticonverter-logos/src/main/java/biblemulticonverter/logos/LogosModuleRegistry.java index 81c72a7..7e2f4d4 100644 --- a/biblemulticonverter-logos/src/main/java/biblemulticonverter/logos/LogosModuleRegistry.java +++ b/biblemulticonverter-logos/src/main/java/biblemulticonverter/logos/LogosModuleRegistry.java @@ -24,6 +24,7 @@ public Collection> getExportFormats() { result.add(new Module("LogosRenumberedDiffable", "Renumber named verses for Logos before exporting as Diffable.", LogosRenumberedDiffable.HELP_TEXT, LogosRenumberedDiffable.class)); result.add(new Module("AugmentLogosLinks", "Add values of Logos links to Extra Attributes of a Bible", AugmentLogosLinks.HELP_TEXT, AugmentLogosLinks.class)); result.add(new Module("LogosHTML", "HTML Export format for Logos Bible Software", LogosHTML.HELP_TEXT, LogosHTML.class)); + result.add(new Module("LaridianPocketBibleExtendedInterlinear", "Export to Laridian Pocket Bible with Interlinear fields from Logos links", LaridianPocketBibleExtendedInterlinear.HELP_TEXT, LaridianPocketBibleExtendedInterlinear.class)); return result; } diff --git a/biblemulticonverter-logos/src/main/java/biblemulticonverter/logos/format/LaridianPocketBibleExtendedInterlinear.java b/biblemulticonverter-logos/src/main/java/biblemulticonverter/logos/format/LaridianPocketBibleExtendedInterlinear.java new file mode 100644 index 0000000..13c3d3a --- /dev/null +++ b/biblemulticonverter-logos/src/main/java/biblemulticonverter/logos/format/LaridianPocketBibleExtendedInterlinear.java @@ -0,0 +1,60 @@ +package biblemulticonverter.logos.format; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import biblemulticonverter.data.Versification.Reference; +import biblemulticonverter.format.LaridianPocketBible; + +public class LaridianPocketBibleExtendedInterlinear extends LaridianPocketBible { + + public static final String[] HELP_TEXT = new String[LaridianPocketBible.HELP_TEXT.length + 2]; + + static { + HELP_TEXT[0] = "Export to Laridian Pocket Bible with Interlinear fields from Logos links"; + for (int i = 1; i < LaridianPocketBible.HELP_TEXT.length; i++) { + HELP_TEXT[i] = LaridianPocketBible.HELP_TEXT[i]; + } + HELP_TEXT[HELP_TEXT.length - 2] = "Additionally, any Logos link prefix can be used as interlinear type, by prefixing it with 'LogosLink:'."; + HELP_TEXT[HELP_TEXT.length - 1] = "You will need to edit the generate meta tag in the head, though."; + }; + + @Override + protected InterlinearType parseInterlinearType(int index, String type) { + if (type.startsWith("LogosLink:")) { + return new LogosLinkInterlinearType(index, type.substring(10)); + } + return super.parseInterlinearType(index, type); + } + + public static class LogosLinkInterlinearType extends InterlinearType> { + + private static final InterlinearTypePrecalculator> LOGOS_LINK_PRECALCULATOR = new InterlinearTypePrecalculator>() { + private final LogosLinksGenerator generator; + { + try { + generator = new LogosLinksGenerator(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + public List precalculate(boolean nt, Reference verseReference, char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices) { + return generator.generateLinks(nt, verseReference, strongsPrefixes, strongs, rmac, sourceIndices); + } + }; + + private final String linkPrefix; + + protected LogosLinkInterlinearType(int index, String linkPrefix) { + super(LOGOS_LINK_PRECALCULATOR, "extra" + (index + 1), "Name for LogosLink:" + linkPrefix + " (edit me), , , , , , sync:\\\\, yes"); + this.linkPrefix = linkPrefix; + } + + @Override + protected List determineValues(List links, boolean nt, Reference verseReference, char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices) { + return links.stream().filter(l -> l.startsWith(linkPrefix)).map(l -> l.substring(linkPrefix.length())).collect(Collectors.toList()); + } + } +} diff --git a/biblemulticonverter/src/main/java/biblemulticonverter/MainModuleRegistry.java b/biblemulticonverter/src/main/java/biblemulticonverter/MainModuleRegistry.java index 564009d..1f9fa5a 100644 --- a/biblemulticonverter/src/main/java/biblemulticonverter/MainModuleRegistry.java +++ b/biblemulticonverter/src/main/java/biblemulticonverter/MainModuleRegistry.java @@ -45,6 +45,7 @@ public Collection> getExportFormats() { result.add(new Module("SwordSearcher", "Export format for SwordSearcher", SwordSearcher.HELP_TEXT, SwordSearcher.class)); result.add(new Module("Obsidian", "Export to Markdown for Obsidian (one chapter per file)", Obsidian.HELP_TEXT, Obsidian.class)); result.add(new Module("AugmentGrammar", "Analyze used Grammar information in one bible and augment another one from that data.", AugmentGrammar.HELP_TEXT, AugmentGrammar.class)); + result.add(new Module("LaridianPocketBible", "Export to Laridian Pocket Bible", LaridianPocketBible.HELP_TEXT, LaridianPocketBible.class)); return result; } diff --git a/biblemulticonverter/src/main/java/biblemulticonverter/format/LaridianPocketBible.java b/biblemulticonverter/src/main/java/biblemulticonverter/format/LaridianPocketBible.java new file mode 100644 index 0000000..ec9f2c6 --- /dev/null +++ b/biblemulticonverter/src/main/java/biblemulticonverter/format/LaridianPocketBible.java @@ -0,0 +1,735 @@ +package biblemulticonverter.format; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.EnumMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import biblemulticonverter.data.Bible; +import biblemulticonverter.data.Book; +import biblemulticonverter.data.BookID; +import biblemulticonverter.data.Chapter; +import biblemulticonverter.data.FormattedText.ExtraAttributePriority; +import biblemulticonverter.data.FormattedText.FormattingInstructionKind; +import biblemulticonverter.data.FormattedText.LineBreakKind; +import biblemulticonverter.data.FormattedText.RawHTMLMode; +import biblemulticonverter.data.FormattedText.Visitor; +import biblemulticonverter.data.FormattedText.VisitorAdapter; +import biblemulticonverter.data.Utils; +import biblemulticonverter.data.Verse; +import biblemulticonverter.data.Versification; +import biblemulticonverter.data.Versification.Reference; + +public class LaridianPocketBible implements ExportFormat { + + public static final String[] HELP_TEXT = { + "Export to Laridian Pocket Bible", + "", + "Export: LaridianPocketBible .html [,] [-inline | ]", + "", + "THe resulting HTML file can be used with Laridian Book Builder (tested version 2.1.0.624) to a LBK file.", + "", + "When two versifications are given, one is used for OT and one for NT.", + "Versifications can be overridden per book in the meta tags after conversion.", + "", + " can be any subset of 'Bible,Book,Chapter', or 'None'", + " can be a comma-separated list of 'SG', 'SH', 'RMAC', 'WIVU', 'IDX'." + }; + + private static final Map BOOK_MAP = new EnumMap<>(BookID.class); + + static { + BOOK_MAP.put(BookID.INTRODUCTION, "FntM"); + + BOOK_MAP.put(BookID.BOOK_Gen, "Gn"); + BOOK_MAP.put(BookID.BOOK_Exod, "Ex"); + BOOK_MAP.put(BookID.BOOK_Lev, "Lv"); + BOOK_MAP.put(BookID.BOOK_Num, "Nu"); + BOOK_MAP.put(BookID.BOOK_Deut, "Dt"); + BOOK_MAP.put(BookID.BOOK_Josh, "Josh"); + BOOK_MAP.put(BookID.BOOK_Judg, "Jdg"); + BOOK_MAP.put(BookID.BOOK_Ruth, "Ru"); + BOOK_MAP.put(BookID.BOOK_1Sam, "1S"); + BOOK_MAP.put(BookID.BOOK_2Sam, "2S"); + BOOK_MAP.put(BookID.BOOK_1Kgs, "1K"); + BOOK_MAP.put(BookID.BOOK_2Kgs, "2K"); + BOOK_MAP.put(BookID.BOOK_1Chr, "1Ch"); + BOOK_MAP.put(BookID.BOOK_2Chr, "2Ch"); + BOOK_MAP.put(BookID.BOOK_Ezra, "Ezr"); + BOOK_MAP.put(BookID.BOOK_Neh, "Ne"); + BOOK_MAP.put(BookID.BOOK_Esth, "Es"); + BOOK_MAP.put(BookID.BOOK_Job, "Job"); + BOOK_MAP.put(BookID.BOOK_Ps, "Ps"); + BOOK_MAP.put(BookID.BOOK_Prov, "Pr"); + BOOK_MAP.put(BookID.BOOK_Eccl, "Ec"); + BOOK_MAP.put(BookID.BOOK_Song, "Song"); + BOOK_MAP.put(BookID.BOOK_Isa, "Isa"); + BOOK_MAP.put(BookID.BOOK_Jer, "Je"); + BOOK_MAP.put(BookID.BOOK_Lam, "La"); + BOOK_MAP.put(BookID.BOOK_Ezek, "Ezk"); + BOOK_MAP.put(BookID.BOOK_Dan, "Da"); + BOOK_MAP.put(BookID.BOOK_Hos, "Ho"); + BOOK_MAP.put(BookID.BOOK_Joel, "Joe"); + BOOK_MAP.put(BookID.BOOK_Amos, "Am"); + BOOK_MAP.put(BookID.BOOK_Obad, "Ob"); + BOOK_MAP.put(BookID.BOOK_Jonah, "Jnh"); + BOOK_MAP.put(BookID.BOOK_Mic, "Mi"); + BOOK_MAP.put(BookID.BOOK_Nah, "Na"); + BOOK_MAP.put(BookID.BOOK_Hab, "Hab"); + BOOK_MAP.put(BookID.BOOK_Zeph, "Zeph"); + BOOK_MAP.put(BookID.BOOK_Hag, "Hag"); + BOOK_MAP.put(BookID.BOOK_Zech, "Zec"); + BOOK_MAP.put(BookID.BOOK_Mal, "Mal"); + + BOOK_MAP.put(BookID.BOOK_Matt, "Mt"); + BOOK_MAP.put(BookID.BOOK_Mark, "Mk"); + BOOK_MAP.put(BookID.BOOK_Luke, "Lk"); + BOOK_MAP.put(BookID.BOOK_John, "Jn"); + BOOK_MAP.put(BookID.BOOK_Acts, "Ac"); + BOOK_MAP.put(BookID.BOOK_Rom, "Rm"); + BOOK_MAP.put(BookID.BOOK_1Cor, "1Co"); + BOOK_MAP.put(BookID.BOOK_2Cor, "2Co"); + BOOK_MAP.put(BookID.BOOK_Gal, "Ga"); + BOOK_MAP.put(BookID.BOOK_Eph, "Ep"); + BOOK_MAP.put(BookID.BOOK_Phil, "Php"); + BOOK_MAP.put(BookID.BOOK_Col, "Col"); + BOOK_MAP.put(BookID.BOOK_1Thess, "1Th"); + BOOK_MAP.put(BookID.BOOK_2Thess, "2Th"); + BOOK_MAP.put(BookID.BOOK_1Tim, "1Ti"); + BOOK_MAP.put(BookID.BOOK_2Tim, "2Ti"); + BOOK_MAP.put(BookID.BOOK_Titus, "Tit"); + BOOK_MAP.put(BookID.BOOK_Phlm, "Phm"); + BOOK_MAP.put(BookID.BOOK_Heb, "Heb"); + BOOK_MAP.put(BookID.BOOK_Jas, "Ja"); + BOOK_MAP.put(BookID.BOOK_1Pet, "1P"); + BOOK_MAP.put(BookID.BOOK_2Pet, "2P"); + BOOK_MAP.put(BookID.BOOK_1John, "1Jn"); + BOOK_MAP.put(BookID.BOOK_2John, "2Jn"); + BOOK_MAP.put(BookID.BOOK_3John, "3Jn"); + BOOK_MAP.put(BookID.BOOK_Jude, "Jde"); + BOOK_MAP.put(BookID.BOOK_Rev, "Rev"); + + BOOK_MAP.put(BookID.BOOK_Jdt, "Jdt"); + BOOK_MAP.put(BookID.BOOK_Wis, "Wis"); + BOOK_MAP.put(BookID.BOOK_Tob, "Tob"); + BOOK_MAP.put(BookID.BOOK_Sir, "Sir"); + BOOK_MAP.put(BookID.BOOK_Bar, "Bar"); + BOOK_MAP.put(BookID.BOOK_1Macc, "1M"); + BOOK_MAP.put(BookID.BOOK_2Macc, "2M"); + BOOK_MAP.put(BookID.BOOK_AddEsth, "AddEst"); + BOOK_MAP.put(BookID.BOOK_PrMan, "Man"); + BOOK_MAP.put(BookID.BOOK_3Macc, "3M"); + BOOK_MAP.put(BookID.BOOK_4Macc, "4M"); + BOOK_MAP.put(BookID.BOOK_EpJer, "LetJer"); + BOOK_MAP.put(BookID.BOOK_1Esd, "1E"); + BOOK_MAP.put(BookID.BOOK_2Esd, "2E"); + BOOK_MAP.put(BookID.BOOK_Sus, "Sus"); + BOOK_MAP.put(BookID.BOOK_Bel, "Bel"); + BOOK_MAP.put(BookID.BOOK_PrAzar, "Aza"); + BOOK_MAP.put(BookID.BOOK_EsthGr, "Est (Gk)"); + + BOOK_MAP.put(BookID.APPENDIX, "BckM"); + + // BOOK_MAP.put(BookID.BOOK_AddDan,""); + // BOOK_MAP.put(BookID.BOOK_Odes,""); + // BOOK_MAP.put(BookID.BOOK_PssSol,""); + // BOOK_MAP.put(BookID.BOOK_EpLao,""); + // BOOK_MAP.put(BookID.BOOK_1En,""); + // BOOK_MAP.put(BookID.BOOK_AddPs,""); + // BOOK_MAP.put(BookID.BOOK_DanGr,""); + // BOOK_MAP.put(BookID.BOOK_Jub,""); + // BOOK_MAP.put(BookID.BOOK_4Ezra,""); + // BOOK_MAP.put(BookID.BOOK_5Ezra,""); + // BOOK_MAP.put(BookID.BOOK_6Ezra,""); + // BOOK_MAP.put(BookID.BOOK_5ApocSyrPss,""); + // BOOK_MAP.put(BookID.BOOK_2Bar,""); + // BOOK_MAP.put(BookID.BOOK_4Bar,""); + // BOOK_MAP.put(BookID.BOOK_EpBar,""); + // BOOK_MAP.put(BookID.BOOK_1Meq,""); + // BOOK_MAP.put(BookID.BOOK_2Meq,""); + // BOOK_MAP.put(BookID.BOOK_3Meq,""); + // BOOK_MAP.put(BookID.BOOK_Rep,""); + + } + + @Override + public void doExport(Bible bible, String... exportArgs) throws Exception { + String outfile = exportArgs[0]; + String versificationOT = exportArgs[1], versificationNT = versificationOT; + if (versificationOT.contains(",")) { + String[] parts = versificationOT.split(",", 2); + versificationOT = parts[0]; + versificationNT = parts[1]; + } + EnumMap headingTypes = new EnumMap<>(HeadingType.class); + if (!exportArgs[2].equals("None")) { + for (String part : exportArgs[2].split(",")) + headingTypes.put(HeadingType.valueOf(part), -1); + } + int firstHeading = 1; + for (HeadingType ht : Arrays.asList(HeadingType.Bible, HeadingType.Book, HeadingType.Chapter)) { + if (headingTypes.containsKey(ht)) { + headingTypes.put(ht, firstHeading); + firstHeading++; + } + } + boolean inline = false, hasChapterPrologs = false, hasStrongsGreek = false, hasStrongsHebrew = false; + InterlinearType[] interlinearTypes = null; + if (exportArgs.length > 3) { + if (exportArgs[3].equals("-inline")) { + inline = true; + } else { + String[] parts = exportArgs[3].split(","); + interlinearTypes = new InterlinearType[parts.length]; + for (int i = 0; i < interlinearTypes.length; i++) { + interlinearTypes[i] = parseInterlinearType(i, parts[i]); + if (interlinearTypes[i] == null) { + throw new RuntimeException("Unsupported interlinear type: " + parts[i]); + } + } + } + } + StringBuilder bookList = new StringBuilder(); + for (Book bk : bible.getBooks()) { + if (!BOOK_MAP.containsKey(bk.getId())) + continue; + if (bookList.length() > 0) + bookList.append(", "); + bookList.append(BOOK_MAP.get(bk.getId()) + "(" + (bk.getId().isNT() ? versificationNT : versificationOT) + ")"); + final boolean nt = bk.getId().isNT(); + List chapters = bk.getChapters(); + for (int cn = 0; cn < chapters.size(); cn++) { + Chapter ch = chapters.get(cn); + if (cn > 0 && ch.getProlog() != null) { + hasChapterPrologs = true; + } + if (interlinearTypes == null) { + for (Verse vv : ch.getVerses()) { + String elementTypes = vv.getElementTypes(Integer.MAX_VALUE); + if (elementTypes.contains("g")) { + boolean[] strongsFlags = new boolean[2]; + vv.accept(new VisitorAdapter(null) { + @Override + protected Visitor wrapChildVisitor(Visitor childVisitor) throws RuntimeException { + return this; + } + + @Override + public Visitor visitGrammarInformation(char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices) throws RuntimeException { + if (strongs != null) { + for (int i = 0; i < strongs.length; i++) { + String formatted = Utils.formatStrongs(nt, i, strongsPrefixes, strongs); + if (formatted.startsWith("G")) + strongsFlags[0] = true; + else if (formatted.startsWith("H")) + strongsFlags[1] = true; + } + } + return super.visitGrammarInformation(strongsPrefixes, strongs, rmac, sourceIndices); + } + }); + hasStrongsGreek |= strongsFlags[0]; + hasStrongsHebrew |= strongsFlags[1]; + } + } + } + } + } + try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outfile), StandardCharsets.UTF_8))) { + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + bw.write("\n"); + if (interlinearTypes != null) { + for (int i = 0; i < interlinearTypes.length; i++) { + bw.write("\n"); + } + } else { + if (hasStrongsHebrew) + bw.write("\n"); + if (hasStrongsGreek) + bw.write("\n"); + } + bw.write("\n"); + bw.write("\n"); + String pendingLine = null; + for (Book book : bible.getBooks()) { + String abbr = BOOK_MAP.get(book.getId()); + if (abbr == null) { + System.out.println("WARNING: Skipping unsupported book " + book.getAbbr()); + continue; + } + int chapterNumber = 0; + for (Chapter ch : book.getChapters()) { + chapterNumber++; + boolean firstVerse = true; + for (Verse v : ch.getVerses()) { + LaridianVisitor vv = new LaridianVisitor(firstHeading, abbr + " " + chapterNumber + ":" + v.getNumber(), new Versification.Reference(book.getId(), chapterNumber, v.getNumber()), interlinearTypes); + if (firstVerse) { + if (headingTypes.containsKey(HeadingType.Bible)) { + vv.addHeadline(headingTypes.get(HeadingType.Bible), h(bible.getName())); + headingTypes.remove(HeadingType.Bible); + } + if (headingTypes.containsKey(HeadingType.Book) && chapterNumber == 1) { + vv.addHeadline(headingTypes.get(HeadingType.Book), h(book.getLongName())); + } + if (headingTypes.containsKey(HeadingType.Chapter) && hasChapterPrologs) { + vv.addHeadline(headingTypes.get(HeadingType.Chapter), "" + h(book.getAbbr()) + " " + chapterNumber + ""); + } + if (ch.getProlog() != null) { + vv.sb.append("\n"); + vv.startProlog(); + ch.getProlog().accept(vv); + vv.sb.append("\n"); // ensure paragraphs are not merged! + } + if (headingTypes.containsKey(HeadingType.Chapter) && !hasChapterPrologs) { + vv.addHeadline(headingTypes.get(HeadingType.Chapter), "" + h(book.getAbbr()) + " " + chapterNumber + ""); + } + firstVerse = false; + } + v.accept(vv); + for (String line : vv.getLines()) { + if (line.isEmpty()) + continue; + if (inline) { + if (pendingLine != null && pendingLine.endsWith("

") && line.startsWith("

")) { + pendingLine = pendingLine.substring(0, pendingLine.length() - 4) + " " + line.substring(3); + } else { + if (pendingLine != null) { + bw.write(pendingLine + "\n"); + } + pendingLine = line; + } + } else { + if (interlinearTypes != null && line.contains("", " class=\"pb_interlinear\">"); + } + bw.write(line + "\n"); + } + } + } + } + } + if (pendingLine != null) { + bw.write(pendingLine + "\n"); + } + bw.write("\n"); + } + } + + private static String h(String text) { + return text.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); + } + + protected InterlinearType parseInterlinearType(int index, String type) { + try { + return BuiltinInterlinearType.valueOf(type).it; + } catch (IllegalArgumentException ex) { + return null; + } + } + + public static enum HeadingType { + Bible, Book, Chapter; + } + + protected static abstract class InterlinearType { + + public static InterlinearTypePrecalculator NO_PRECALCULATOR = new InterlinearTypePrecalculator() { + @Override + public Void precalculate(boolean nt, Reference verseReference, char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices) { + return null; + } + }; + + public final String name, metaValue; + public final InterlinearTypePrecalculator precalculator; + + protected InterlinearType(InterlinearTypePrecalculator precalculator, String name, String metaValue) { + this.precalculator = precalculator; + this.name = name; + this.metaValue = metaValue; + } + + protected abstract List determineValues(C precalculatedValue, boolean nt, Versification.Reference verseReference, char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices); + } + + protected static interface InterlinearTypePrecalculator { + public abstract C precalculate(boolean nt, Versification.Reference verseReference, char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices); + } + + private static class StrongsInterlinearType extends InterlinearType> { + + private static final InterlinearTypePrecalculator> STRONGS_PRECALCULATOR = new InterlinearTypePrecalculator>() { + public List precalculate(boolean nt, Reference verseReference, char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices) { + List result = new ArrayList<>(); + if (strongs != null) { + for (int i = 0; i < strongs.length; i++) { + result.add(Utils.formatStrongs(nt, i, strongsPrefixes, strongs)); + } + } + return result; + } + }; + + private final boolean greek; + + private StrongsInterlinearType(boolean greek) { + super(STRONGS_PRECALCULATOR, greek ? "strongsgreek" : "strongshebrew", // + (greek ? "Strong's Greek Number, G, G, G, nt" : "Strong's Hebrew Number, H, H, H, ot") + ", , sync:\\\\, yes"); + this.greek = greek; + } + + @Override + protected List determineValues(List formattedStrongs, boolean nt, Reference verseReference, char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices) { + return formattedStrongs.stream().filter(s -> s.startsWith(greek ? "G" : "H")).map(s -> s.substring(1)).collect(Collectors.toList()); + } + } + + private static class MorphInterlinearType extends InterlinearType { + private final String regex; + + private MorphInterlinearType(boolean greek) { + super(NO_PRECALCULATOR, greek ? "rmacmorph" : "wivumorph", // + (greek ? "Greek RMAC Morphology, , , , nt" : "Hebrew WIVU morphology, , , , ot") + ", , sync:\\\\\\\\, yes"); + this.regex = greek ? Utils.RMAC_REGEX : Utils.WIVU_REGEX; + } + + @Override + protected List determineValues(Void unused, boolean nt, Reference verseReference, char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices) { + List result = new ArrayList<>(); + if (rmac != null) { + for (int i = 0; i < rmac.length; i++) { + if (rmac[i].matches(regex)) + result.add(rmac[i]); + } + } + return result; + } + } + + private static class SourceIndexInterlinearType extends InterlinearType { + public SourceIndexInterlinearType() { + super(NO_PRECALCULATOR, "sourceindex", "Source Index, , , , , , , no"); + } + + @Override + protected List determineValues(Void unused, boolean nt, Reference verseReference, char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices) { + List result = new ArrayList<>(); + if (sourceIndices != null) { + for (int i = 0; i < sourceIndices.length; i++) { + result.add("" + sourceIndices[i]); + } + } + return result; + } + } + + private static enum BuiltinInterlinearType { + SG(new StrongsInterlinearType(true)), SH(new StrongsInterlinearType(false)), // + RMAC(new MorphInterlinearType(true)), WIVU(new MorphInterlinearType(false)), // + IDX(new SourceIndexInterlinearType()); + + private InterlinearType it; + + private BuiltinInterlinearType(InterlinearType it) { + this.it = it; + } + } + + private static class LaridianVisitor extends AbstractNoCSSVisitor { + + private final List suffixStack = new ArrayList<>(); + private final StringBuilder sb = new StringBuilder(); + private final int firstHeading; + private final String verseReference; + private final Versification.Reference reference; + private final InterlinearType[] interlinearTypes; + + private boolean afterHeadline, inVerse, inProlog; + + private LaridianVisitor(int firstHeading, String verseReference, Versification.Reference reference, InterlinearType[] interlinearTypes) { + this.firstHeading = firstHeading; + this.verseReference = verseReference; + this.reference = reference; + this.interlinearTypes = interlinearTypes; + suffixStack.add(""); + } + + public void addHeadline(int depth, String text) { + visitHeadline(depth - firstHeading + 1); + visitStart(); + sb.append(text); + visitEnd(); + } + + public String[] getLines() { + if (suffixStack.size() > 0) + throw new IllegalStateException("Unused suffixes"); + return sb.toString().split("\n"); + } + + private void beforeAddHeadline() { + if (suffixStack.size() != 1) + return; + if (!afterHeadline) { + if (!inVerse && !inProlog) { + sb.append("\n"); + } else { + sb.append("

\n"); + } + afterHeadline = true; + } + } + + public void startProlog() { + if (suffixStack.size() != 1 || inVerse || inProlog) + throw new IllegalStateException("Unable to start prolog"); + beforeAddHeadline(); + inProlog = true; + } + + private void ensureInParagraph() { + if (suffixStack.size() != 1) + return; + if (!inVerse && !inProlog) + throw new IllegalStateException("Neither verse nor prolog was started"); + if (afterHeadline) { + sb.append("

"); + afterHeadline = false; + } + } + + @Override + public int visitElementTypes(String elementTypes) throws RuntimeException { + return 0; + } + + @Override + public Visitor visitHeadline(int depth) throws RuntimeException { + if (suffixStack.size() != 1) { + System.out.println("WARNING: Skipped headline not at toplevel"); + suffixStack.add(""); + return this; + } + beforeAddHeadline(); + int tagNum = depth + firstHeading - 1; + if (tagNum > 9) + tagNum = 9; + if (tagNum < 1) + throw new IllegalStateException("Invalid headline tag h" + tagNum); + sb.append(""); + suffixStack.add("\n"); + return this; + } + + @Override + public void visitStart() throws RuntimeException { + if (suffixStack.size() != 1 || inProlog) + return; + if (inVerse) + throw new IllegalStateException("Verse already started"); + sb.append("

"); + afterHeadline = false; + inVerse = true; + } + + @Override + public void visitText(String text) throws RuntimeException { + ensureInParagraph(); + if (suffixStack.get(suffixStack.size() - 1).endsWith("")) { + char[] chars = text.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char ch = chars[i]; + if (ch >= 'a' && ch <= 'z' && ch != 'q' && ch != 'x') { + chars[i] = "ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘ-ʀꜱᴛᴜᴠᴡ-ʏᴢ".charAt(ch - 'a'); + } else if (Character.isLowerCase(ch)) { + System.out.println("WARNING: Unable to create DIVINE_NAME character for " + ch); + } + } + text = new String(chars); + } + sb.append(h(text)); + } + + @Override + public Visitor visitFootnote() throws RuntimeException { + ensureInParagraph(); + sb.append(""); + suffixStack.add(""); + return this; + } + + @Override + public Visitor visitCrossReference(String bookAbbr, BookID book, int firstChapter, String firstVerse, int lastChapter, String lastVerse) throws RuntimeException { + ensureInParagraph(); + String abbr = BOOK_MAP.get(book); + if (abbr == null) { + System.out.println("WARNING: Skipping cross reference to unsupported book " + book.getOsisID()); + suffixStack.add(""); + } else { + sb.append(""); + suffixStack.add(""); + } + return this; + } + + @Override + public Visitor visitFormattingInstruction(FormattingInstructionKind kind) throws RuntimeException { + ensureInParagraph(); + String startTag, endTag; + + switch (kind) { + case BOLD: + case ITALIC: + case UNDERLINE: + case SUBSCRIPT: + case SUPERSCRIPT: + startTag = "<" + kind.getHtmlTag() + ">"; + endTag = ""; + break; + case DIVINE_NAME: + startTag = ""; + endTag = ""; + break; + case FOOTNOTE_LINK: + case LINK: + startTag = ""; + endTag = ""; + break; + case STRIKE_THROUGH: + startTag = ""; + endTag = ""; + break; + case WORDS_OF_JESUS: + startTag = ""; + endTag = ""; + break; + default: + throw new IllegalArgumentException("Unsupported formatting " + kind); + } + sb.append(startTag); + suffixStack.add(endTag); + return this; + } + + @Override + public Visitor visitCSSFormatting(String css) throws RuntimeException { + ensureInParagraph(); + return super.visitCSSFormatting(css); + } + + @Override + protected Visitor visitChangedCSSFormatting(String remainingCSS, Visitor resultingVisitor, int replacements) { + if (!remainingCSS.isEmpty()) + System.out.println("WARNING: Skipping unsupported CSS formatting"); + fixupSuffixStack(replacements, suffixStack); + return resultingVisitor; + } + + @Override + public void visitVerseSeparator() throws RuntimeException { + ensureInParagraph(); + sb.append("/"); + } + + @Override + public void visitLineBreak(LineBreakKind kind) throws RuntimeException { + ensureInParagraph(); + if (kind == LineBreakKind.PARAGRAPH) + sb.append("

\n

"); + else + sb.append("
"); + } + + @Override + public Visitor visitGrammarInformation(char[] strongsPrefixes, int[] strongs, String[] rmac, int[] sourceIndices) throws RuntimeException { + ensureInParagraph(); + StringBuilder suffixBuilder = new StringBuilder(); + if (interlinearTypes != null) { + Map precalculatedCache = new IdentityHashMap<>(); + for (InterlinearType it : interlinearTypes) { + if (!precalculatedCache.containsKey(it.precalculator)) + precalculatedCache.put(it.precalculator, it.precalculator.precalculate(reference.getBook().isNT(), reference, strongsPrefixes, strongs, rmac, sourceIndices)); + for (String value : ((InterlinearType) it).determineValues(precalculatedCache.get(it.precalculator), reference.getBook().isNT(), reference, strongsPrefixes, strongs, rmac, sourceIndices)) { + suffixBuilder.append(""); + } + } + } else if (strongs != null) { + for (int i = 0; i < strongs.length; i++) { + String formatted = Utils.formatStrongs(reference.getBook().isNT(), i, strongsPrefixes, strongs); + String name = null; + if (formatted.startsWith("G")) + name = "strongsgreek"; + else if (formatted.startsWith("H")) + name = "strongshebrew"; + if (name != null) + suffixBuilder.append(""); + } + } + suffixStack.add(suffixBuilder.toString()); + return this; + } + + @Override + public Visitor visitDictionaryEntry(String dictionary, String entry) throws RuntimeException { + ensureInParagraph(); + System.out.println("WARNING: Skipping unsupported dictionary entry"); + suffixStack.add(""); + return this; + } + + @Override + public void visitRawHTML(RawHTMLMode mode, String raw) throws RuntimeException { + ensureInParagraph(); + System.out.println("WARNING: Skipping unsupported Raw HTML"); + } + + @Override + public Visitor visitVariationText(String[] variations) throws RuntimeException { + throw new UnsupportedOperationException("Variation text not supported"); + } + + @Override + public Visitor visitExtraAttribute(ExtraAttributePriority prio, String category, String key, String value) throws RuntimeException { + Visitor next = prio.handleVisitor(category, this); + if (next != null) + suffixStack.add(""); + return next; + } + + @Override + public boolean visitEnd() throws RuntimeException { + if (suffixStack.size() == 1) { + beforeAddHeadline(); + if (inProlog) { + inProlog = false; + return false; + } + if (!inVerse) { + throw new IllegalStateException("Verse was not started"); + } + } + sb.append(suffixStack.remove(suffixStack.size() - 1)); + return false; + } + } +}