diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/libbible.cc | 262 | ||||
-rw-r--r-- | src/lib/libbible.h | 115 | ||||
-rw-r--r-- | src/lib/mods.cc | 233 | ||||
-rw-r--r-- | src/lib/settings.cc | 23 |
4 files changed, 633 insertions, 0 deletions
diff --git a/src/lib/libbible.cc b/src/lib/libbible.cc new file mode 100644 index 0000000..c9acb7d --- /dev/null +++ b/src/lib/libbible.cc @@ -0,0 +1,262 @@ +#include "libbible.h" +#include <sword/versekey.h> +#include <sword/markupfiltmgr.h> +#include <sword/swmodule.h> +#include <sword/swmgr.h> +#include <sword/osisfootnotes.h> +#include <algorithm> + +using namespace sword; +using namespace std; + +SWMgr library(new MarkupFilterMgr(FMT_XHTML)); +OSISFootnotes filter; + +vector<string> getBooks(SWModule *target) { + vector<string> books; + VerseKey *key = (VerseKey *) target->getKey(); + for(char t = 1; t <= key->getTestamentMax(); t++) { + key->setTestament(t); + for(char b = 1; b <= key->getBookMax(); b++) { + key->setBook(b); + // Bug (whose fault??) in JPS; they CLAIM to have two testaments, + // but they only have one, which causes repeats. + if(std::find(books.begin(), books.end(), key->getBookName()) != books.end()) { + continue; + } + // Another issue (maybe bug?) Some translations are NT only, + // but still report OT books/chapters. + if(string(target->renderText()).empty()) { + continue; + } + books.push_back(key->getBookName()); + } + } + return books; +} + +map<string, vector<string>> libbible::getModules() { + library.load(); + map<string, vector<string>> mods; + ModMap::iterator it; + for (it = library.getModules().begin(); it != library.getModules().end(); it++) { + string modName = (*it).second->getName(); + SWModule *target = library.getModule(modName.c_str()); + mods[modName] = getBooks(target); + } + return mods; +} + +vector<libbible::passage> libbible::getPassages(string modName, string book) { + vector<libbible::passage> passages; + SWModule *target = library.getModule(modName.c_str()); + if(target == nullptr) { + // Module doesn't exist + return passages; + } + target->setKey((book + " " + "1").c_str()); + VerseKey *key = (VerseKey *) target->getKey(); + int maxChapter = key->getChapterMax(); + for(int chapter = 1; chapter <= maxChapter; chapter++) { + string ref = book + ' ' + to_string(chapter); + target->setKey(ref.c_str()); + VerseKey *key = (VerseKey *) target->getKey(); + libbible::passage pass; + pass.modName = modName; + pass.book = string(key->getBookName()); + pass.bookShort = string(key->getBookAbbrev()); + pass.chapterStart = chapter; + pass.chapterEnd = chapter; + pass.verseStart = 1; + pass.verseEnd = key->getVerseMax(); + passages.push_back(pass); + } + return passages; +} + +libbible::text getEmptyText(VerseKey *key) { + libbible::text t; + t.chapter = key->getChapter(); + t.verse = key->getVerse(); + t.book = key->getBookName(); + t.bookShort = key->getBookAbbrev(); + return t; +} + +libbible::passage libbible::getPassage(string modName, string reference) { + libbible::passage pass; + pass.modName = modName; + SWModule *target = library.getModule(pass.modName.c_str()); + if(target == nullptr || reference.empty()) { + // Bad input + return pass; + } + vector<string> validBooks = getBooks(target); + //printf("Hey, I'm inferring missing parts!\n"); + // Let's use the target to help us + target->setKey(reference.c_str()); + VerseKey *key = (VerseKey *) target->getKey(); + pass.book = string(key->getBookName()); + // Hold on a moment, is this book even legal? + if(find(validBooks.begin(), validBooks.end(), pass.book) == validBooks.end()) { + key->setBookName(validBooks[0].c_str()); + pass.book = string(key->getBookName()); + } + pass.bookShort = string(key->getBookAbbrev()); + pass.chapterStart = key->getChapter(); + pass.verseStart = key->getVerse(); + //printf("Results so far: book: %s; chapterStart: %d; verseStart: %d\n", pass.book.c_str(), pass.chapterStart, pass.verseStart); + // And now we just need chapterEnd and verseEnd. Yippee. + string ref = string(reference); + ref.erase(remove(ref.begin(), ref.end(), ' '), ref.end()); + if(ref.find('-') == string::npos) { + // There's no range! + if(ref.find(':') == string::npos) { + // It's a full chapter reference + pass.chapterEnd = pass.chapterStart; + pass.verseEnd = key->getVerseMax(); + } else { + // It's a single verse reference + pass.chapterEnd = pass.chapterStart; + pass.verseEnd = pass.verseStart; + //printf("Hey, it's a single verse reference!\n"); + } + } else { + if(ref.find(':') == string::npos) { + // It's a multi-full-chapter reference + pass.chapterEnd = stoi(ref.substr(ref.find_last_of('-')+1)); + key->setChapter(pass.chapterEnd); + pass.verseEnd = key->getVerseMax(); + } else { + // It falls in categories c:v-v or c:v-c:v (or, technically, c-c:v) + string rangeEnd = ref.substr(ref.find_last_of('-')+1); + if(rangeEnd.find(':') == string::npos) { + // It's c:v-v + pass.verseEnd = stoi(rangeEnd); + pass.chapterEnd = pass.chapterStart; + } else { + // It's c:v-c:v (or c-c:v, but code is the same) + pass.chapterEnd = stoi(rangeEnd.substr(0, rangeEnd.find(':'))); + pass.verseEnd = stoi(rangeEnd.substr(rangeEnd.find(':')+1)); + } + } + } + return pass; +} + +vector<libbible::text> libbible::getText(libbible::passage pass) { + vector<libbible::text> texts; + SWModule *target = library.getModule(pass.modName.c_str()); + filter.setOptionValue("Off"); + target->addOptionFilter(&filter); + if(target == nullptr) { + // Module doesn't exist + return texts; + } + if(pass.book.empty()) { + pass.book = pass.bookShort; + } + target->setKey((pass.book + + " " + to_string(pass.chapterStart) + + ":" + to_string(pass.verseStart)).c_str()); + VerseKey *key = (VerseKey *) target->getKey(); + + bool endOfParagraph = false; + + string book = string(key->getBookName()); + + for(; string(key->getBookName()) == book && + (key->getChapter() < pass.chapterEnd + || (key->getChapter() == pass.chapterEnd && key->getVerse() <= pass.verseEnd)); + (*key)++) { + + string text = string(target->renderText()); + //printf("Working with: %s\n", text.c_str()); + + texts.push_back(getEmptyText(key)); + + if(key->getVerse() == 1 || endOfParagraph) { + if(find(texts.back().modifiers.begin(), texts.back().modifiers.end(), "paragraph") == texts.back().modifiers.end()) { + texts.back().modifiers.push_back("paragraph"); + } + endOfParagraph = false; + } + + // Variable to accumulate unterminated spans + std::vector<std::pair<std::string, std::string>> spans; + bool spansChanged = false; + bool hasAddedText = false; + // Iterate over text + for(auto i = text.begin(); i != text.end(); i++) { + if(*i != '<') { + if(spansChanged) { + spansChanged = false; + if(!texts.back().text.empty()) { + texts.push_back(getEmptyText(key)); + } + for(auto& [tag, modifier] : spans) { + if(find(texts.back().modifiers.begin(), texts.back().modifiers.end(), modifier) == texts.back().modifiers.end()) { + texts.back().modifiers.push_back(modifier); + } + } + } + if(*i == '\n') { + continue; // We add newlines with <br /> + } + if(! hasAddedText && (*i == ' ' || *i == '\t')) { + continue; + } + if(*i == "¶"[0] && i+1 != text.end() && *(i+1) == "¶"[1]) { + i++; + if(hasAddedText) { + texts.back().text += '\n'; + } else { + // Append \n to text in previous texts (if applicable) + if(texts.size() > 1) { + texts[texts.size()-2].text += '\n'; + } + texts.back().modifiers.push_back("paragraph"); + continue; + } + } + texts.back().text += *i; + hasAddedText = true; + } + else { + string span; + for(; i != text.end(); i++) { + span.push_back(*i); + if(*i == '>') { + // The end of the span will be "</tag>". + if(span[1] == '/') { + string tag = span.substr(2, span.size()-3); + for(auto rit = spans.rbegin(); rit != spans.rend(); rit++) { + if(rit->first == tag) { + spans.erase(rit.base()-1); + spansChanged = true; + break; + } + } + } else if(span.find("class=\"") != string::npos) { + // The span will be formatted "<tag class=\"NAME\">" + // We want just the NAME + string tag = span.substr(1, span.find(" ")-1); + size_t start = span.find("class=\"")+7; + size_t end = span.find("\"", start); + spans.push_back(std::pair<string, string>(tag, span.substr(start, end-start))); + spansChanged = true; + } else if(span.find("preverse") != string::npos) { + string tag = span.substr(1, span.find(" ")-1); + spans.push_back(std::pair<string, string>(tag, "preverse")); + } else if(span == "<br />" || span == "<br/>") { + texts.back().text += '\n'; + } + break; + } + } + } + } + endOfParagraph = (text[text.length()-1] == '\n'); + } + return texts; +} diff --git a/src/lib/libbible.h b/src/lib/libbible.h new file mode 100644 index 0000000..f77dc8c --- /dev/null +++ b/src/lib/libbible.h @@ -0,0 +1,115 @@ +#include <string> +#include <vector> +#include <map> + +namespace libbible { + + struct text { + int chapter; + int verse; + std::string book; + std::string bookShort; + std::string text; + std::vector<std::string> modifiers; // e.g., paragraph, line indent0, divineName, wordsOfJesus + }; + + struct passage { + std::string modName; + std::string book; + std::string bookShort; + int chapterStart; + int verseStart; + int chapterEnd; + int verseEnd; + }; + + /* + * @return Map of modName to supported books + */ + std::map<std::string, std::vector<std::string>> getModules(void); + + /* + * @return Vector of valid single full-chapter passages for a book + */ + std::vector<struct passage> getPassages(std::string modName, std::string book); + + /* + * @param modName the module to use for determining the passage + * @param reference a human-readable reference, e.g., "gen 1:26-27" + * @return the passage matching the reference + */ + passage getPassage(std::string modName, std::string reference); + + /* + * @return Text for a passage + */ + std::vector<struct text> getText(struct passage pass); + + /************************** + * Methods dealing with mods + ***************************/ + + class Status { + public: + virtual void update(unsigned long totalBytes, unsigned long completedBytes, std::string message) {} + }; + + /** + * @param status Status update method is called asynchronously as download progresses + */ + void setStatusReporter(Status& status); + + /** + * @return A mapping from language to bible version names + */ + std::map<std::string, std::vector<std::string>> downloadModsAvailable(); + + /** + * @return A mapping from language abbreviations to full language names + */ + std::map<std::string, std::string> getLanguageNames(); + + /** + * Cancel an in-progress download + */ + void terminateDownload(void); + + /** + * @param language The language of the mod to install as provided from downloadModsAvailable + * @param name The name of the bible version as provided from downloadModsAvailable + * @see downloadModsAvailable() + * @return true on success, false otherwise + */ + bool installModFromInternet(std::string language, std::string name); + + /** + * @param filename Path to the .zip compressed module to be installed + * @return true on success, false otherwise + */ + bool installModFromZip(std::string filename); + + /** + * @param modname The name of the module to be removed + */ + void uninstallMod(std::string modname); + + /****************************** + * Methods dealing with settings + *******************************/ + + /* + * From already established code, valid and useful values are: + * int fontsize: the last used size of the font + * string passage: the last looked-up passage + * string module: the last used module + */ + + void settingsWrite(std::string key, std::string value); + + std::string settingsRead(std::string key); + + void settingsWriteInt(std::string key, int value); + + int settingsReadInt(std::string key); + +} diff --git a/src/lib/mods.cc b/src/lib/mods.cc new file mode 100644 index 0000000..ab54e48 --- /dev/null +++ b/src/lib/mods.cc @@ -0,0 +1,233 @@ +#include "libbible.h" +#include <sword/swmgr.h> +#include <sword/swmodule.h> +#include <sword/installmgr.h> +#include <sword/filemgr.h> +#include <sword/remotetrans.h> +#include <unzip.h> +#include <filesystem> + +using namespace std; + +class myStatusReporter : public sword::StatusReporter { + public: + myStatusReporter(libbible::Status *status); + ~myStatusReporter(); + void preStatus(long totalBytes, long completedBytes, const char *message); + void update(unsigned long totalBytes, unsigned long completedBytes); + protected: + libbible::Status *status; + string message; +}; + +myStatusReporter::myStatusReporter(libbible::Status *s) { + status = s; +} + +myStatusReporter::~myStatusReporter() {}; + +//virtual void libbible::Status::update(unsigned long totalBytes, unsigned long completedBytes, string message) {} + +void myStatusReporter::preStatus(long totalBytes, long completedBytes, const char *msg) { + message = string(msg); + status->update((unsigned long) totalBytes, (unsigned long) completedBytes, message); + //printf("Got a status update: %ld / %ld, \"%s\"\n", completedBytes, totalBytes, message.c_str()); +} + +void myStatusReporter::update(unsigned long totalBytes, unsigned long completedBytes) { + status->update(totalBytes, completedBytes, message); + //printf("Got a status update: %ld / %ld, \"%s\"\n", completedBytes, totalBytes, message.c_str()); +} + +string basedir = (getenv("HOME")) + string("/.sword/"); +sword::InstallMgr *installMgr = new sword::InstallMgr((basedir + std::string("InstallMgr")).c_str(), nullptr); +map<string, vector<pair<string, sword::InstallSource *>>> installSources; +map<string, string> languageNames; // maps abbreviation to full name + +void libbible::setStatusReporter(libbible::Status& status) { + myStatusReporter *msr = new myStatusReporter(&status); + free(installMgr); + installMgr = new sword::InstallMgr((basedir + std::string("InstallMgr")).c_str(), msr); + installMgr->setUserDisclaimerConfirmed(true); +} + +map<string, vector<string>> libbible::downloadModsAvailable() { + installSources.clear(); + languageNames.clear(); + mkdir((basedir + std::string("mods.d/")).c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + mkdir((basedir + std::string("modules/")).c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + installMgr->setUserDisclaimerConfirmed(true); + string confpath = basedir + string("InstallMgr/InstallMgr.conf"); + if(! sword::FileMgr::existsFile(confpath.c_str())) { + // Lifted directly from xiphos + sword::FileMgr::createParent(confpath.c_str()); + sword::SWConfig config(confpath.c_str()); + sword::InstallSource is("FTP"); + is.caption = "CrossWire"; + is.source = "ftp.crosswire.org"; + is.directory = "/pub/sword/raw"; + config["General"]["PassiveFTP"] = "true"; + config["Sources"]["FTPSource"] = is.getConfEnt(); + config.save(); + installMgr->refreshRemoteSourceConfiguration(); + } + installMgr->readInstallConf(); + map<string, vector<string>> modsAvailable; + map<string, vector<string>> languagesToFull; + //printf("Getting langs...\n"); + for(auto src : installMgr->sources) { + if(src.second->getMgr()->Modules.empty()) { + //printf("Refreshing remote source: %s\n", src.second->getConfEnt().c_str()); + installMgr->refreshRemoteSource(src.second); + } + for(auto mod : src.second->getMgr()->Modules) { + auto *curMod = mod.second; + string type(curMod->getType()); + if(type == "Biblical Texts") { + string language(curMod->getLanguage()); + string fullLang; + if(curMod->getConfigEntry("LCSH")) { + // Split on periods, last field, strip + fullLang = string(curMod->getConfigEntry("LCSH")); + // If ends with ., remove + if(fullLang.ends_with('.')) fullLang = fullLang.substr(0, fullLang.size()-1); + if(fullLang.find('.') != string::npos) fullLang = fullLang.substr(fullLang.find_last_of('.')+1); + while(fullLang.starts_with(' ')) fullLang = fullLang.substr(1); + while(fullLang.ends_with(' ')) fullLang = fullLang.substr(0, fullLang.size()-1); + } + vector<string> newLangs; + languagesToFull.emplace(language, newLangs); + languagesToFull[language].push_back(fullLang); + vector<string> newMods; + vector<pair<string, sword::InstallSource *>> newSources; + // emplace only adds if key is unique + modsAvailable.emplace(language, newMods); + installSources.emplace(language, newSources); + modsAvailable[language].push_back(string(curMod->getName())); + pair<string, sword::InstallSource *> p(string(curMod->getName()), src.second); + installSources[language].push_back(p); + } + } + } + // Now use majority voting to move languagesToFull -> languageNames + for(const auto& [abbrev, fulls] : languagesToFull) { + std::map<string, int> majVote; + for(auto full : fulls) { + majVote.try_emplace(full, 0); + majVote[full]++; + } + string selected = fulls[0]; + for(auto full : fulls) { + if(majVote[full] > majVote[selected] or (majVote[full] == majVote[selected] and !full.empty() and full.size() < selected.size())) { + selected = full; + } + } + if(selected.empty()) languageNames[abbrev] = abbrev; + else languageNames[abbrev] = selected; + } + return modsAvailable; +} + +std::map<std::string, std::string> libbible::getLanguageNames() { + if(languageNames.empty()) { + downloadModsAvailable(); + } + return languageNames; +} + +void libbible::terminateDownload() { + installMgr->terminate(); +} + +bool libbible::installModFromInternet(string language, string name) { + // Searching through map<string, vector<pair<string, sword::InstallSource *>>> installSources; + if(installSources.empty()) { + downloadModsAvailable(); + } + for (pair<string, sword::InstallSource *> p : installSources[language]) { + if(p.first == name) { + sword::SWMgr mgr(basedir.c_str()); + if(installMgr->installModule(&mgr, 0, name.c_str(), p.second) == 0) { + printf("Installed from %s\n", p.second->getConfEnt().c_str()); + return true; + } + return false; + } + } + return false; +} + +#define READ_SIZE 8192 +#define delim '/' + +bool libbible::installModFromZip(string filename) { + // So... turns out it's a mite unsupported to install from a .zip + // Here's the deal. We do a syscall to unzip. We fancy like that. + // TODO: Use the ZipCompress module from SWORD instead. + /*string command = "unzip -o " + filename + " -d " + basedir + "&> /dev/null"; + if(system(command.c_str())) { + //Uh oh... + printf("Something bad happened when unpacking %s\n. Is unzip installed?", filename.c_str()); + }*/ + unzFile zipfile = unzOpen(filename.c_str()); + if(zipfile == NULL) { + return false; + } + unz_global_info global_info; + if(unzGetGlobalInfo(zipfile, &global_info) != UNZ_OK) { + unzClose(zipfile); + return false; + } + char read_buffer[READ_SIZE]; + ulong i; + for(i = 0; i < global_info.number_entry; i++) { + unz_file_info file_info; + if(unzGetCurrentFileInfo(zipfile, &file_info, read_buffer, READ_SIZE, NULL, 0, NULL, 0) != UNZ_OK) { + unzClose(zipfile); + return false; + } + string fname = basedir + string(read_buffer); + size_t pos = fname.find_last_of(delim); + if(pos != string::npos) { + string path = fname.substr(0, pos); + filesystem::create_directories(path); + } + if(unzOpenCurrentFile(zipfile) != UNZ_OK) { + unzCloseCurrentFile(zipfile); + unzClose(zipfile); + return false; + } + FILE *out = fopen(fname.c_str(), "wb"); + if(out == NULL) { + unzCloseCurrentFile(zipfile); + unzClose(zipfile); + return false; + } + int bytesRead; + do { + bytesRead = unzReadCurrentFile(zipfile, read_buffer, READ_SIZE); + if(bytesRead < 0) { + printf("error %d\n", bytesRead); + unzCloseCurrentFile(zipfile); + unzClose(zipfile); + return false; + } + if(bytesRead > 0) { + fwrite(read_buffer, bytesRead, 1, out); + } + } while(bytesRead > 0); + fclose(out); + unzCloseCurrentFile(zipfile); + unzGoToNextFile(zipfile); + } + unzClose(zipfile); + return true; +} + +void libbible::uninstallMod(string modname) { + sword::SWMgr mgr(basedir.c_str()); + sword::ModMap::iterator it = mgr.Modules.find(modname.c_str()); + if(it != mgr.Modules.end()) { + installMgr->removeModule(&mgr, it->second->getName()); + } +} diff --git a/src/lib/settings.cc b/src/lib/settings.cc new file mode 100644 index 0000000..848e22f --- /dev/null +++ b/src/lib/settings.cc @@ -0,0 +1,23 @@ +#include "libbible.h" +#include <sword/swconfig.h> + +std::string path = (std::getenv("HOME")) + std::string("/.sword/libbible.conf"); +sword::SWConfig config(path.c_str()); + +void libbible::settingsWrite(std::string key, std::string value) { + config["General"][key.c_str()] = sword::SWBuf(value.c_str()); + config.save(); +} + +std::string libbible::settingsRead(std::string key) { + return config["General"][key.c_str()].c_str(); +} + +void libbible::settingsWriteInt(std::string key, int value) { + config["General"][key.c_str()] = sword::SWBuf(std::to_string(value).c_str()); + config.save(); +} + +int libbible::settingsReadInt(std::string key) { + return atoi(config["General"][key.c_str()].c_str()); +} |