package yuku.alkitabconverter.util; import yuku.alkitab.model.XrefEntry; import yuku.alkitab.util.Ari; import yuku.bintex.BintexWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; public class XrefDb { public static interface XrefProcessor { void process(XrefEntry xe, int ari, int entryIndex); } Map<Integer, List<XrefEntry>> map = new TreeMap<>(); public XrefDb() {} public XrefDb(final LinkedHashMap<Integer, XrefEntry> xrefEntries) { // make sure it's sorted for (final Map.Entry<Integer, XrefEntry> entry : new TreeMap<>(xrefEntries).entrySet()) { final int arif = entry.getKey(); final int ari = arif >> 8; List<XrefEntry> xes = map.get(ari); if (xes == null) { xes = new ArrayList<>(); map.put(ari, xes); } final XrefEntry xe = entry.getValue(); xes.add(xe); } } public LinkedHashMap<Integer, XrefEntry> toEntries() { final LinkedHashMap<Integer, XrefEntry> res = new LinkedHashMap<>(); processEach(new XrefProcessor() { @Override public void process(final XrefEntry xe, final int ari, final int entryIndex) { res.put(ari << 8 | (entryIndex + 1), xe); } }); return res; } /** * @return index of xref for this ari, starts from 0. */ public int addBegin(int ari) { List<XrefEntry> list = map.get(ari); if (list == null) { list = new ArrayList<>(); map.put(ari, list); } XrefEntry xe = new XrefEntry(); list.add(xe); return list.size() - 1; } /** must be after addBegin */ public void appendText(int ari, String text) { List<XrefEntry> list = map.get(ari); if (list == null) { throw new RuntimeException("Must be after addBegin (1)"); } XrefEntry xe = list.get(list.size() - 1); if (xe.content == null) { xe.content = text; } else { xe.content += text; } } /** * Combines addBegin and appendText into a single call. * @param content The complete xref between \x and \x*. e.g. "+ Joh 3:16; Joh 16:29" * @return index of xref for this ari, starts from 0. */ public int addComplete(final int ari, final String content) { final String target; // remove marker [a-zA-Z+-]<space> at the beginning if (content.matches("[a-zA-Z+-]\\s.*")) { // remove that first 2 characters target = content.substring(2); } else { target = content; } List<XrefEntry> list = map.get(ari); if (list == null) { list = new ArrayList<>(); map.put(ari, list); } XrefEntry xe = new XrefEntry(); list.add(xe); xe.content = target; return list.size() - 1; } public void dump() { for (Map.Entry<Integer, List<XrefEntry>> e: map.entrySet()) { List<XrefEntry> xes = e.getValue(); for (int i = 0; i < xes.size(); i++) { XrefEntry xe = xes.get(i); System.out.printf("xref 0x%06x(%d): [%s]%n", e.getKey(), i + 1, xe.content); } } } public void processEach(XrefProcessor processor) { for (Map.Entry<Integer, List<XrefEntry>> e: map.entrySet()) { List<XrefEntry> xes = e.getValue(); for (int i = 0; i < xes.size(); i++) { XrefEntry xe = xes.get(i); processor.process(xe, e.getKey(), i); } } } public static void writeXrefEntriesTo(final LinkedHashMap<Integer, XrefEntry> xrefEntries, final BintexWriter bw) throws IOException { // version bw.writeUint8(1); // entry_count bw.writeInt(xrefEntries.size()); // int arif[entry_count] for (final Map.Entry<Integer, XrefEntry> entry : xrefEntries.entrySet()) { bw.writeInt(entry.getKey()); } // try to calculate offset for each content. So we do the following ByteArrayOutputStream contents = new ByteArrayOutputStream(); BintexWriter contentsBw = new BintexWriter(contents); // int offsets[entry_count] for (final Map.Entry<Integer, XrefEntry> entry : xrefEntries.entrySet()) { bw.writeInt(contentsBw.getPos()); contentsBw.writeValueString(entry.getValue().content); } // value<string> xref_entry_contents[entry_count] bw.writeRaw(contents.toByteArray()); } public static XrefProcessor defaultShiftTbProcessor = new XrefDb.XrefProcessor() { @Override public void process(XrefEntry xe, int ari_location, int entryIndex) { final List<int[]> pairs = new ArrayList<>(); DesktopVerseFinder.findInText(xe.content, new DesktopVerseFinder.DetectorListener() { @Override public boolean onVerseDetected(int start, int end, String verse) { pairs.add(new int[] {start, end}); return true; } @Override public void onNoMoreDetected() { } }); String target = xe.content; for (int i = pairs.size() - 1; i >= 0; i--) { int[] pair = pairs.get(i); String verse = target.substring(pair[0], pair[1]); IntArrayList ariRanges = DesktopVerseParser.verseStringToAriWithShiftTb(verse); if (ariRanges == null || ariRanges.size() == 0) { throw new RuntimeException("verse cannot be parsed: " + verse); } { // we need to process 00 verses (entire chapter) to 1 for start and the last verse for end. boolean isStart = true; for (int j = 0; j < ariRanges.size(); j++, isStart = !isStart) { int ari2 = ariRanges.get(j); if (Ari.toVerse(ari2) == 0) { if (isStart) { ari2 = Ari.encodeWithBc(Ari.toBookChapter(ari2), 1); } else { int lastVerse_1 = KjvUtils.getVerseCount(Ari.toBook(ari2), Ari.toChapter(ari2)); ari2 = Ari.encodeWithBc(Ari.toBookChapter(ari2), lastVerse_1); } } ariRanges.set(j, ari2); } } { StringBuilder ariRanges_s = new StringBuilder(); boolean isStart = true; boolean endWritten = false; // this can be set to true in isstart portion if the end verse does not need to be written for (int j = 0; j < ariRanges.size(); j++, isStart = !isStart) { final int ari_target = ariRanges.get(j); { // just for checking int lid = KjvUtils.ariToLid(ari_target); if (lid <= 0) { throw new RuntimeException(String.format("invalid ari found 0x%06x", ari_target)); } } final String enc_value; if (isStart) { endWritten = false; final int ari_end = ariRanges.get(j + 1); enc_value = ariToString(ari_target); if (ari_end == ari_target) { endWritten = true; } } else { if (endWritten) { // already mentioned by start, no need to do anything enc_value = null; } else { enc_value = ariToString(ari_target); } } if (isStart) { if (ariRanges_s.length() != 0) { ariRanges_s.append(","); } ariRanges_s.append(enc_value); } else { if (enc_value != null) { ariRanges_s.append("-").append(enc_value); } } } target = target.substring(0, pair[0]) + "@<ta:" + ariRanges_s + "@>" + verse + "@/" + target.substring(pair[1]); } } xe.content = target; } private String ariToString(final int ari) { final String ari_target_hex = "0x" + Integer.toHexString(ari); final String ari_target_dec = Integer.toString(ari); return ari_target_hex.length() <= ari_target_dec.length()? ari_target_hex: ari_target_dec; } }; }