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; + } + } +}