BibleTime
cswordmoduleinfo.cpp
Go to the documentation of this file.
1/*********
2*
3* In the name of the Father, and of the Son, and of the Holy Spirit.
4*
5* This file is part of BibleTime's source code, https://bibletime.info/
6*
7* Copyright 1999-2025 by the BibleTime developers.
8* The BibleTime source code is licensed under the GNU General Public License
9* version 2.0.
10*
11**********/
12
13#include "cswordmoduleinfo.h"
14
15#include <memory>
16#include <cassert>
17#include <CLucene.h>
18#include <optional>
19#include <QByteArray>
20#include <QCoreApplication>
21#include <QDebug>
22#include <QDir>
23#include <QFileInfo>
24#include <QLocale>
25#include <QScopeGuard>
26#include <QSettings>
27#include <QTextDocument>
28#include <string>
29#include <string_view>
30#include <type_traits>
31#include "../../util/btassert.h"
32#include "../../util/cresmgr.h"
33#include "../../util/directory.h"
34#include "../../util/tool.h"
35#include "../config/btconfig.h"
36#include "../keys/cswordkey.h"
37#include "../managers/cswordbackend.h"
38#include "../cswordmodulesearch.h"
41
42// Sword includes:
43#pragma GCC diagnostic push
44#pragma GCC diagnostic ignored "-Wsuggest-override"
45#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
46#include <listkey.h>
47#include <swbuf.h>
48#include <swconfig.h>
49#include <swkey.h>
50#include <swmodule.h>
51#include <swversion.h>
52#include <rtfhtml.h>
53#include <versekey.h>
54#pragma GCC diagnostic pop
55
56
57//Increment this, if the index format changes
58//Then indices on the user's systems will be rebuilt
59constexpr static unsigned const INDEX_VERSION = 7;
60
61//Maximum index entry size, 1MiB for now
62//Lucene default is too small
63constexpr static unsigned long const BT_MAX_LUCENE_FIELD_LENGTH = 1024 * 1024;
64
65namespace {
66
69 CSwordModuleInfo::Features const features,
70 sword::SWModule & module)
71{
72 /// \todo Maybe we can use raw string comparsion instead of QString?
73 QString const cat(module.getConfigEntry("Category"));
74
75 // Category has to be checked before type:
76 if (cat == QStringLiteral("Cults / Unorthodox / Questionable Material")) {
78 } else if (cat == QStringLiteral("Daily Devotional")
79 || features.testFlag(CSwordModuleInfo::FeatureDailyDevotion))
80 {
82 } else if (cat == QStringLiteral("Glossaries")
83 || features.testFlag(CSwordModuleInfo::FeatureGlossary))
84 {
86 } else if (cat == QStringLiteral("Images")
87 || cat == QStringLiteral("Maps"))
88 {
90 } else {
91 switch (type) {
98 default:
101 }
102 }
103}
104
105inline CSwordModuleInfo::Features retrieveFeatures(
106 sword::SWModule const & module)
107{
108 // See https://wiki.crosswire.org/DevTools:conf_Files
109 CSwordModuleInfo::Features features;
110 for (auto featureEntries = module.getConfig().equal_range("Feature");
111 featureEntries.first != featureEntries.second;
112 ++featureEntries.first)
113 {
114 #define T(f) \
115 if (featureEntries.first->second == #f) { \
116 features |= CSwordModuleInfo::Feature ## f; \
117 }
118 T(StrongsNumbers)
119 else T(GreekDef)
120 else T(HebrewDef)
121 else T(GreekParse)
122 else T(HebrewParse)
123 else T(DailyDevotion)
124 else T(Glossary)
125 else T(Images)
126 else T(NoParagraphs)
127 #undef T
128 }
129 return features;
130}
131
132static const TCHAR * stop_words[] = { nullptr };
133
134class Analyzer : public lucene::analysis::standard::StandardAnalyzer {
135
136public: // Methods:
137
138 Analyzer() : lucene::analysis::standard::StandardAnalyzer(stop_words) {}
139
140};
141
142} // anonymous namespace
143
144char const *
146{ return value ? "On" : "Off"; }
147
148char const *
150 switch (value) {
151 case 0: return "Primary Reading";
152 case 1: return "Secondary Reading";
153 default: return "All Readings";
154 }
155}
156
158 "Footnotes",
159 "Footnotes",
160 QT_TRANSLATE_NOOP("QObject", "Footnotes")};
161
163 "Strong's Numbers",
164 "Strongs",
165 QT_TRANSLATE_NOOP("QObject", "Strong's numbers")};
166
168 "Headings",
169 "Headings",
170 QT_TRANSLATE_NOOP("QObject", "Headings")};
171
173 "Morphological Tags",
174 "Morph",
175 QT_TRANSLATE_NOOP("QObject", "Morphological tags")};
176
178 "Lemmas",
179 "Lemma",
180 QT_TRANSLATE_NOOP("QObject", "Lemmas")};
181
183 "Hebrew Vowel Points",
184 "HebrewPoints",
185 QT_TRANSLATE_NOOP("QObject", "Hebrew vowel points")};
186
188 "Hebrew Cantillation",
189 "Cantillation",
190 QT_TRANSLATE_NOOP("QObject", "Hebrew cantillation marks")};
191
193 "Greek Accents",
194 "GreekAccents",
195 QT_TRANSLATE_NOOP("QObject", "Greek accents")};
196
198 "Cross-references",
199 "Scripref",
200 QT_TRANSLATE_NOOP("QObject", "Scripture cross-references")};
201
203 "Words of Christ in Red",
204 "RedLetterWords",
205 QT_TRANSLATE_NOOP("QObject", "Red letter words")};
206
208 "Textual Variants",
209 "Variants",
210 QT_TRANSLATE_NOOP("QObject", "Textual variants"),
212
214 "Morph Segmentation",
215 "MorphSegmentation",
216 QT_TRANSLATE_NOOP("QObject", "Morph segmentation")};
217
218CSwordModuleInfo::CSwordModuleInfo(sword::SWModule & module,
219 CSwordBackend & backend,
220 ModuleType type)
221 : m_swordModule(module)
222 , m_backend(backend)
223 , m_type(type)
224 , m_cancelIndexing(false)
225 , m_cachedName(QString::fromUtf8(module.getName()))
226 , m_cachedFeatures(retrieveFeatures(module))
227 , m_cachedCategory(retrieveCategory(type, m_cachedFeatures, module))
228 , m_cachedLanguage(
229 Language::fromAbbrev(
230 util::tool::fixSwordBcp47(
231 m_cachedCategory == Category::Glossaries
232 /* Special handling for glossaries, we use the "from language" as
233 language for the module: */
234 ? config(GlossaryFrom)
235 : module.getLanguage())))
236 , m_cachedGlossaryTargetLanguage(
237 m_cachedCategory == Category::Glossaries
238 ? Language::fromAbbrev(
239 util::tool::fixSwordBcp47(module.getLanguage()))
240 : std::shared_ptr<Language const>())
241 , m_cachedHasVersion(
242 ((*m_backend.getConfig())[module.getName()]["Version"]).size() > 0)
243{
244 m_hidden = btConfig().value<QStringList>(
245 QStringLiteral("state/hiddenModules"))
246 .contains(m_cachedName);
247
249 && (minimumSwordVersion() > sword::SWVersion::currentVersion))
250 {
251 qWarning("The module \"%s\" requires a newer Sword library. Please "
252 "update to \"Sword %s\".",
253 m_cachedName.toUtf8().constData(),
254 minimumSwordVersion().getText());
255
256 /// \todo if this is the case, can we use the module at all?
257 }
258}
259
260bool CSwordModuleInfo::unlock(const QString & unlockKey) {
261 if (!isEncrypted())
262 return false;
263
264 bool unlocked = unlockKeyIsValid();
265
267
268 /// \todo remove this comment once it is no longer needed
269 /* There is currently a deficiency in sword 1.8.1 in that
270 backend->setCipherKey() does not work correctly for modules from which
271 data was already fetched. Therefore we have to reload the modules in
272 bibletime.cpp */
273 m_backend.raw().setCipherKey(m_swordModule.getName(),
274 unlockKey.toUtf8().constData());
275
276 /// \todo write to Sword config as well
277
278 if (unlockKeyIsValid() != unlocked)
279 Q_EMIT unlockedChanged(!unlocked);
280 return true;
281}
282
284 // still works, but the cipherkey is stored in BtConfig.
285 // Works because it is set in sword on program startup.
286
287 return isEncrypted() && !unlockKeyIsValid();
288}
289
291 /**
292 * If we have the CipherKey entry the module
293 * is encrypted but not necessarily locked
294 */
295
296 /* This code is still right, though we do no longer write to the module
297 config files any more. */
298 auto const & sections = m_backend.getConfig()->getSections();
299 auto const it(sections.find(m_cachedName.toUtf8().constData()));
300 if (it == sections.end())
301 return false;
302
303 const sword::ConfigEntMap & config = it->second;
304 return config.find("CipherKey") != config.end();
305}
306
308 sword::SWKey * const key = m_swordModule.getKey();
309 sword::VerseKey * const vk = dynamic_cast<sword::VerseKey *>(key);
310 if (vk)
311 vk->setIntros(false);
312 m_swordModule.setPosition(sword::TOP);
313
314 /* This needs to use ::fromLatin1 because if the text is still locked, a lot
315 of garbage will show up. It will also work with properly decrypted
316 Unicode text, because all non-ASCII Unicode chars consist of bytes >127
317 and therefore contain no control (nonprintable) characters, which are all
318 <127. */
319 const QString test(isUnicode()
320 ? QString::fromUtf8(m_swordModule.getRawEntry())
321 : QString::fromLatin1(m_swordModule.getRawEntry()));
322
323 if (test.isEmpty())
324 return false;
325
326 for (int i = 0; i < test.length() && i < 100; i++)
327 if (!test[i].isPrint() && !test[i].isNull())
328 return false;
329 return true;
330}
331
333 return m_swordModule.getConfigEntry("UnlockInfo");
334}
335
339
341 return QStringLiteral("%1/%2").arg(getGlobalBaseIndexLocation(),
342 m_cachedName.toLocal8Bit());
343}
344
346 // This for now returns the location of the main index
347 return getModuleBaseIndexLocation() + QStringLiteral("/standard");
348}
349
351 { // Is this a directory?
352 QFileInfo fi(getModuleStandardIndexLocation());
353 if (!fi.isDir())
354 return false;
355 }
356
357 // Are the index version and module version OK?
358 QSettings module_config(getModuleBaseIndexLocation()
359 + QStringLiteral("/bibletime-index.conf"),
360 QSettings::IniFormat);
361
363 && module_config.value(QStringLiteral("module-version")).toString()
365 {
366 return false;
367 }
368
369 if (module_config.value(QStringLiteral("index-version")).toUInt()
370 != INDEX_VERSION)
371 {
372 qDebug("%s: INDEX_VERSION is not compatible with this version of "
373 "BibleTime.",
374 m_cachedName.toUtf8().constData());
375 return false;
376 }
377
378 // Is the index there?
379 return lucene::index::IndexReader::indexExists(getModuleStandardIndexLocation()
380 .toLatin1().constData());
381}
382
393
400
402 auto cleanup =
403 qScopeGuard(
404 [this]() noexcept
405 { m_cancelIndexing.store(false, std::memory_order_relaxed); });
406#define CANCEL_INDEXING (m_cancelIndexing.load(std::memory_order_relaxed))
407
408 try {
409 // Without this we don't get strongs, lemmas, etc.
410 m_backend.setFilterOptions(btConfig().getFilterOptions());
411 /* Make sure we reset all important filter options which influcence the
412 plain filters. Turn on these options, they are needed for the
413 EntryAttributes population */
415
416 /* We don't want the following in the text, the do not carry searchable
417 information. */
421
422 // Do not use any stop words:
423 Analyzer analyzer;
424 const QString index(getModuleStandardIndexLocation());
425
426 QDir dir(QStringLiteral("/"));
427 dir.mkpath(getGlobalBaseIndexLocation());
428 dir.mkpath(getModuleBaseIndexLocation());
429 dir.mkpath(getModuleStandardIndexLocation());
430
431 if (lucene::index::IndexReader::indexExists(index.toLatin1().constData()))
432 if (lucene::index::IndexReader::isLocked(index.toLatin1().constData()))
433 lucene::index::IndexReader::unlock(index.toLatin1().constData());
434
435 // Always create a new index:
436 auto writer =
437 std::make_optional<lucene::index::IndexWriter>(
438 index.toLatin1().constData(),
439 &analyzer,
440 true);
441 writer->setMaxFieldLength(BT_MAX_LUCENE_FIELD_LENGTH);
442 writer->setUseCompoundFile(true); // Merge segments into a single file
443
444 CSwordBibleModuleInfo *bm = qobject_cast<CSwordBibleModuleInfo*>(this);
445
446 unsigned long verseLowIndex;
447 unsigned long verseHighIndex;
448
449 if(bm)
450 {
451 verseLowIndex = bm->lowerBound().index();
452 verseHighIndex = bm->upperBound().index();
453 }
454 else
455 {
456 m_swordModule.setPosition(sword::TOP);
457 verseLowIndex = m_swordModule.getIndex();
458 m_swordModule.setPosition(sword::BOTTOM);
459 verseHighIndex = m_swordModule.getIndex();
460 }
461
462 // verseLowIndex is not 0 in all cases (i.e. NT-only modules)
463 unsigned long verseIndex = verseLowIndex + 1;
464 unsigned long verseSpan = verseHighIndex - verseLowIndex;
465
466 // Index() is not implemented properly for lexicons, so work around it:
468 verseIndex = 0;
469 verseLowIndex = 0;
470 verseSpan = static_cast<CSwordLexiconModuleInfo *>(this)->entries().size();
471 }
472
473 Q_EMIT indexingProgress(0);
474
475 sword::SWKey * const key = m_swordModule.getKey();
476 sword::VerseKey * const vk = dynamic_cast<sword::VerseKey *>(key);
477
478 if (vk) {
479 /* We have to be sure to insert the english key into the index,
480 otherwise we'd be in trouble if the language changes. */
481 vk->setLocale("en_US");
482 /* If we have a verse based module, we want to include the pre-
483 chapter etc. headings in the search. */
484 vk->setIntros(true);
485 }
486
487 QByteArray textBuffer; // Holds UTF-8 data and is faster than QString.
488
489 // we start with the first module entry, key is automatically updated
490 // because key is a pointer to the modules key
491 m_swordModule.setSkipConsecutiveLinks(true);
492
493 auto const sPwcharBuffer =
494 std::make_unique<wchar_t[]>(BT_MAX_LUCENE_FIELD_LENGTH + 1);
495 wchar_t * const wcharBuffer = sPwcharBuffer.get();
496 BT_ASSERT(wcharBuffer);
497
498 if(bm && vk) // Implied that vk could be null due to cast above
499 vk->setIndex(bm->lowerBound().index());
500 else
501 m_swordModule.setPosition(sword::TOP);
502
503 bool importantFilterOption = hasImportantFilterOption();
504
505 while (!(m_swordModule.popError()) && !CANCEL_INDEXING) {
506
507 /* Also index Chapter 0 and Verse 0, because they might have
508 information in the entry attributes. We used to just put their
509 content into the textBuffer and continue to the next verse, but
510 with entry attributes this doesn't work any more. Hits in the
511 search dialog will show up as 1:1 (instead of 0). */
512
513 lucene::document::Document doc;
514
515 //index the key
516 lucene_utf8towcs(wcharBuffer, key->getText(), BT_MAX_LUCENE_FIELD_LENGTH);
517
518 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("key")),
519 static_cast<const TCHAR *>(wcharBuffer),
520 lucene::document::Field::STORE_YES
521 | lucene::document::Field::INDEX_NO)));
522
523 if (importantFilterOption) {
524 // Index text including strongs, morph, footnotes, and headings.
526 textBuffer.append(m_swordModule.stripText());
527 lucene_utf8towcs(wcharBuffer,
528 static_cast<const char *>(textBuffer),
530 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("content")),
531 static_cast<const TCHAR *>(wcharBuffer),
532 lucene::document::Field::STORE_NO
533 | lucene::document::Field::INDEX_TOKENIZED)));
534 textBuffer.clear();
535 }
536
537 // Index text without strongs, morph, footnotes, and headings.
539 textBuffer.append(m_swordModule.stripText());
540 lucene_utf8towcs(wcharBuffer,
541 static_cast<const char *>(textBuffer),
543 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("content")),
544 static_cast<const TCHAR *>(wcharBuffer),
545 lucene::document::Field::STORE_NO
546 | lucene::document::Field::INDEX_TOKENIZED)));
547 textBuffer.clear();
548
549 for (auto & vp : m_swordModule.getEntryAttributes()["Footnote"]) {
550 lucene_utf8towcs(wcharBuffer, vp.second["body"], BT_MAX_LUCENE_FIELD_LENGTH);
551 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("footnote")),
552 static_cast<const TCHAR *>(wcharBuffer),
553 lucene::document::Field::STORE_NO
554 | lucene::document::Field::INDEX_TOKENIZED)));
555 }
556
557 // Headings
558 for (auto & vp
559 : m_swordModule.getEntryAttributes()["Heading"]["Preverse"])
560 {
561 lucene_utf8towcs(wcharBuffer, vp.second, BT_MAX_LUCENE_FIELD_LENGTH);
562 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("heading")),
563 static_cast<const TCHAR *>(wcharBuffer),
564 lucene::document::Field::STORE_NO
565 | lucene::document::Field::INDEX_TOKENIZED)));
566 }
567
568 // Strongs/Morphs
569 for (auto const & vp : m_swordModule.getEntryAttributes()["Word"]) {
570 auto const & attrs = vp.second;
571 auto const partCountIter(attrs.find("PartCount"));
572 int partCount = (partCountIter != attrs.end())
573 ? QString(partCountIter->second).toInt()
574 : 0;
575 for (int i=0; i<partCount; i++) {
576
577 sword::SWBuf lemmaKey = "Lemma";
578 if (partCount > 1)
579 lemmaKey.appendFormatted(".%d", i+1);
580 auto const lemmaIter(attrs.find(lemmaKey));
581 if (lemmaIter != attrs.end()) {
582 lucene_utf8towcs(wcharBuffer, lemmaIter->second, BT_MAX_LUCENE_FIELD_LENGTH);
583 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("strong")),
584 static_cast<const TCHAR *>(wcharBuffer),
585 lucene::document::Field::STORE_NO
586 | lucene::document::Field::INDEX_TOKENIZED)));
587 }
588
589 }
590
591 auto const morphIter(attrs.find("Morph"));
592 if (morphIter != attrs.end()) {
593 lucene_utf8towcs(wcharBuffer, morphIter->second, BT_MAX_LUCENE_FIELD_LENGTH);
594 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("morph")),
595 static_cast<const TCHAR *>(wcharBuffer),
596 lucene::document::Field::STORE_NO
597 | lucene::document::Field::INDEX_TOKENIZED)));
598 }
599 }
600
601 writer->addDocument(&doc);
602 //Index() is not implemented properly for lexicons, so we use a
603 //workaround.
605 verseIndex++;
606 } else {
607 verseIndex = m_swordModule.getIndex();
608 }
609
610 if (verseIndex % 200 == 0) {
611 if (verseSpan == 0) { // Prevent division by zero
612 Q_EMIT indexingProgress(0);
613 } else {
614 Q_EMIT indexingProgress(
615 static_cast<int>(
616 (100 * (verseIndex - verseLowIndex))
617 / verseSpan));
618 }
619 }
620
621 m_swordModule.increment();
622 } // while (!(m_module.Error()) && !CANCEL_INDEXING)
623
624 if (!CANCEL_INDEXING)
625 writer->optimize();
626 writer->close();
627 writer.reset();
628
629 if (CANCEL_INDEXING) {
630 deleteIndex();
631 } else {
632 QSettings module_config(getModuleBaseIndexLocation()
633 + QStringLiteral("/bibletime-index.conf"),
634 QSettings::IniFormat);
636 module_config.setValue(QStringLiteral("module-version"),
638 module_config.setValue(QStringLiteral("index-version"),
640 Q_EMIT hasIndexChanged(true);
641 }
642 // } catch (CLuceneError & e) {
643 } catch (...) {
644 deleteIndex();
645 throw;
646 }
647}
648
653
654void CSwordModuleInfo::deleteIndexForModule(const QString & name) {
655 QDir(QStringLiteral("%1/%2").arg(getGlobalBaseIndexLocation(), name))
656 .removeRecursively();
657}
658
660 namespace DU = util::directory;
661 return DU::getDirSizeRecursive(getModuleBaseIndexLocation());
662}
663
665CSwordModuleInfo::searchIndexed(QString const & searchedText,
666 sword::ListKey const & scope) const
667{
668 auto const sPutfBuffer =
669 std::make_unique<char[]>(BT_MAX_LUCENE_FIELD_LENGTH + 1);
670 auto const sPwcharBuffer =
671 std::make_unique<wchar_t[]>(BT_MAX_LUCENE_FIELD_LENGTH + 1);
672 char * const utfBuffer = sPutfBuffer.get();
673 BT_ASSERT(utfBuffer);
674 wchar_t * const wcharBuffer = sPwcharBuffer.get();
675 BT_ASSERT(wcharBuffer);
676
677 // work around Swords thread insafety for Bibles and Commentaries
678 m_swordModule.setKey(createKey()->asSwordKey());
679
680 // do not use any stop words
681 Analyzer analyzer;
682 lucene::search::IndexSearcher searcher(getModuleStandardIndexLocation().toLatin1().constData());
683 lucene_utf8towcs(wcharBuffer, searchedText.toUtf8().constData(), BT_MAX_LUCENE_FIELD_LENGTH);
684 std::unique_ptr<lucene::search::Query> q(lucene::queryParser::QueryParser::parse(static_cast<const TCHAR *>(wcharBuffer),
685 static_cast<const TCHAR *>(_T("content")),
686 &analyzer));
687
688 std::unique_ptr<lucene::search::Hits> h(
689 searcher.search(q.get(), lucene::search::Sort::INDEXORDER()));
690
691 const bool useScope = (scope.getCount() > 0);
692
693 lucene::document::Document * doc = nullptr;
694 std::unique_ptr<sword::SWKey> swKey(m_swordModule.createKey());
695
696 sword::VerseKey * const vk = dynamic_cast<sword::VerseKey *>(swKey.get());
697 if (vk)
698 vk->setIntros(true);
699
701 for (size_t i = 0; i < h->length(); ++i) {
702 doc = &h->doc(i);
703 lucene_wcstoutf8(utfBuffer,
704 static_cast<const wchar_t *>(doc->get(static_cast<const TCHAR *>(_T("key")))),
706
707 swKey->setText(utfBuffer);
708
709 // Limit results based on scope:
710 if (useScope) {
711 for (int j = 0; j < scope.getCount(); j++) {
712 if (auto const * const vkey =
713 dynamic_cast<sword::VerseKey const *>(
714 scope.getElement(j)))
715 {
716 if (vkey->getLowerBound().compare(*swKey) <= 0
717 && vkey->getUpperBound().compare(*swKey) >= 0)
718 results.emplace_back(swKey->clone());
719 }
720 }
721 } else { // No scope, give me all buffers
722 results.emplace_back(swKey->clone());
723 }
724 }
725
726 return results;
727}
728
730 return sword::SWVersion(config(CSwordModuleInfo::MinimumSwordVersion)
731 .toUtf8().constData());
732}
733
735 switch (entry) {
736
737 case AboutInformation:
738 return getFormattedConfigEntry(QStringLiteral("About"));
739
740 case CipherKey: {
741 if (btConfig().getModuleEncryptionKey(m_cachedName).isNull()) {
742 return QString(m_swordModule.getConfigEntry("CipherKey")); // Fallback
743 } else {
745 }
746 }
747
748 case AbsoluteDataPath: {
749 auto path(getSimpleConfigEntry(QStringLiteral("AbsoluteDataPath")));
750 if (!path.endsWith('/')) /// \todo is this needed?
751 path.append('/');
752
753 return path;
754 }
755
756 case DataPath: {
757 QString path(getSimpleConfigEntry(QStringLiteral("DataPath")));
758
759 // Make sure we remove the dataFile part if it's a Lexicon
762 {
763 int pos = path.lastIndexOf('/'); // Last slash in the string
764 if (pos != -1)
765 path = path.left(pos + 1); // Include the slash
766 }
767 return path;
768 }
769
770 case Description:
771 return getFormattedConfigEntry(QStringLiteral("Description"));
772
773 case ModuleVersion: {
774 auto const version(getSimpleConfigEntry(QStringLiteral("Version")));
775 return version.isEmpty() ? QStringLiteral("1.0") : version;
776 }
777
778 case MinimumSwordVersion: {
779 auto const minimumVersion =
780 getSimpleConfigEntry(QStringLiteral("MinimumVersion"));
781 return minimumVersion.isEmpty()
782 ? QStringLiteral("0.0")
783 : minimumVersion;
784 }
785
786 case TextDir: {
787 auto const dir = getSimpleConfigEntry(QStringLiteral("Direction"));
788 return dir.isEmpty() ? QStringLiteral("LtoR") : dir;
789 }
790
791 case DisplayLevel: {
792 auto const level =
793 getSimpleConfigEntry(QStringLiteral("DisplayLevel"));
794 return level.isEmpty() ? QStringLiteral("1") : level;
795 }
796
797 case GlossaryFrom: {
799 return {};
800
801 return getSimpleConfigEntry(QStringLiteral("GlossaryFrom"));
802 }
803
804 case GlossaryTo: {
806 return {};
807
808 return getSimpleConfigEntry(QStringLiteral("GlossaryTo"));
809 }
810
811 case Markup: {
812 auto const markup =
813 getSimpleConfigEntry(QStringLiteral("SourceType"));
814 return markup.isEmpty() ? QStringLiteral("Unknown") : markup;
815 }
816
819 QStringLiteral("DistributionLicense"));
820
823 QStringLiteral("DistributionSource"));
824
826 return getFormattedConfigEntry(QStringLiteral("DistributionNotes"));
827
828 case TextSource:
829 return getFormattedConfigEntry(QStringLiteral("TextSource"));
830
831 case CopyrightNotes:
832 return getFormattedConfigEntry(QStringLiteral("CopyrightNotes"));
833
834 case CopyrightHolder:
835 return getFormattedConfigEntry(QStringLiteral("CopyrightHolder"));
836
837 case CopyrightDate:
838 return getFormattedConfigEntry(QStringLiteral("CopyrightDate"));
839
842 QStringLiteral("CopyrightContactName"));
843
846 QStringLiteral("CopyrightContactAddress"));
847
850 QStringLiteral("CopyrightContactEmail"));
851
852 default:
853 return {};
854 }
855}
856
858 /// \todo This is a BAD workaround to see if the filter is GBF, OSIS or ThML!
859 auto const & originalOptionName = option.configOptionName;
860 using namespace std::string_literals;
861 std::string const optionNames[] = {
862 "OSIS"s + originalOptionName,
863 "GBF"s + originalOptionName,
864 "ThML"s + originalOptionName,
865 "UTF8"s + originalOptionName,
866 originalOptionName
867 };
868 for (auto [it, end] =
869 m_swordModule.getConfig().equal_range("GlobalOptionFilter");
870 it != end;
871 ++it)
872 {
873 auto const & valueBuf = it->second;
874 std::string_view const value(valueBuf.c_str(), valueBuf.size());
875 for (auto const & optionName : optionNames)
876 if (value == optionName)
877 return true;
878 }
879 return false;
880}
881
883 return (config(TextDir) == QStringLiteral("RtoL"))
885 : LeftToRight;
886}
887
889{ return textDirection() == RightToLeft ? "rtl" : "ltr"; }
890
891void CSwordModuleInfo::write(CSwordKey * key, const QString & newText) {
892 m_swordModule.setKey(key->key().toUtf8().constData());
893
894 /* Don't store a pointer to the const char* value somewhere because QCString
895 doesn't keep the value of it. */
896 m_swordModule.setEntry(isUnicode()
897 ? newText.toUtf8().constData()
898 : newText.toLocal8Bit().constData());
899}
900
902 static auto const row(
903 QStringLiteral("<tr><td><b>%1</b></td><td>%2</td></tr>"));
904
905 auto text(QStringLiteral("<table>"));
906
907 text += row
908 .arg(tr("Version"))
910 ? config(CSwordModuleInfo::ModuleVersion).toHtmlEscaped()
911 : tr("unknown"));
912
913 {
914 const QString sourceType(m_swordModule.getConfigEntry("SourceType"));
915 text += row
916 .arg(tr("Markup"))
917 .arg(!sourceType.isEmpty()
918 ? sourceType.toHtmlEscaped()
919 : tr("unknown"));
920 }
921
922 text += row
923 .arg(tr("Location"))
924 .arg(config(CSwordModuleInfo::AbsoluteDataPath).toHtmlEscaped());
925
926 text += row
927 .arg(tr("Language"))
928 .arg(m_cachedLanguage->translatedName().toHtmlEscaped());
929
930 if (char const * const e = m_swordModule.getConfigEntry("Category"))
931 text += row.arg(tr("Category"))
932 .arg(QString{e}.toHtmlEscaped());
933
934 if (char const * const e = m_swordModule.getConfigEntry("LCSH"))
935 text += row.arg(tr("LCSH"))
936 .arg(QString{e}.toHtmlEscaped());
937
938 text += row
939 .arg(tr("Writable"))
940 .arg(isWritable() ? tr("yes") : tr("no"));
941
942 if (isEncrypted()) {
943 text += row
944 .arg(tr("Unlock key"))
945 .arg(config(CSwordModuleInfo::CipherKey).toHtmlEscaped());
946 if (char const * const e = m_swordModule.getConfigEntry("UnlockInfo"))
947 text += row.arg(tr("Unlock info")).arg(QString(e).toHtmlEscaped());
948 }
949
950 QString options;
951 static constexpr auto const allFilterOptions = {
952 &footnotes,
954 &headings,
955 &morphTags,
956 &lemmas,
964 };
965 for (auto const * const filterOption : allFilterOptions) {
966 if (has(*filterOption)) {
967 if (!options.isEmpty())
968 options += QStringLiteral(", ");
969 options += QObject::tr(filterOption->translatableOptionName);
970 }
971 }
972
973 if (!options.isEmpty())
974 text += row
975 .arg(tr("Features"))
976 .arg(options.toHtmlEscaped());
977
978 text += QStringLiteral("</table><hr>");
979
980 // Clearly say the module contains cult/questionable materials
982 text += QStringLiteral("<br/><b>%1</b><br/><br/>")
983 .arg(tr("Take care, this work contains cult / questionable "
984 "material!"));
985
986 text += QStringLiteral("<b>%1:</b><br/>%2")
987 .arg(tr("About"))
988 .arg(config(AboutInformation)); // May contain HTML, don't escape
989
990 text += QStringLiteral("<hr><table>");
991
992 struct Entry { ConfigEntry const type; char const * const text; };
993 auto const entries = {
994 Entry{DistributionLicense, QT_TR_NOOP("Distribution license")},
995 Entry{DistributionSource, QT_TR_NOOP("Distribution source")},
996 Entry{DistributionNotes, QT_TR_NOOP("Distribution notes")},
997 Entry{TextSource, QT_TR_NOOP("Text source")},
998 Entry{CopyrightNotes, QT_TR_NOOP("Copyright notes")},
999 Entry{CopyrightHolder, QT_TR_NOOP("Copyright holder")},
1000 Entry{CopyrightDate, QT_TR_NOOP("Copyright date")},
1001 Entry{CopyrightContactName, QT_TR_NOOP("Copyright contact name")},
1002 Entry{CopyrightContactAddress, QT_TR_NOOP("Copyright contact address")},
1003 Entry{CopyrightContactEmail, QT_TR_NOOP("Copyright contact email")},
1004 };
1005 for (auto const & entry : entries) {
1006 auto const value = config(entry.type);
1007 if (!value.isEmpty())
1008 text += row.arg(tr(entry.text).toHtmlEscaped())
1009 .arg(value.toHtmlEscaped());
1010 }
1011
1012 return text + QStringLiteral("</table>");
1013}
1014
1015bool CSwordModuleInfo::isUnicode() const noexcept
1016{ return m_swordModule.isUnicode(); }
1017
1023
1024#define BT_CAT_ICON(Icon,icon) \
1025 QIcon const & \
1026 CSwordModuleInfo::category ## Icon( \
1027 CSwordModuleInfo::Category const category) \
1028 { \
1029 switch (category) { \
1030 case CSwordModuleInfo::Category::Bibles: \
1031 return CResMgr::categories::bibles::icon(); \
1032 case CSwordModuleInfo::Category::Commentaries: \
1033 return CResMgr::categories::commentaries::icon(); \
1034 case CSwordModuleInfo::Category::Books: \
1035 return CResMgr::categories::books::icon(); \
1036 case CSwordModuleInfo::Category::Lexicons: \
1037 return CResMgr::categories::lexicons::icon(); \
1038 case CSwordModuleInfo::Category::Glossaries: \
1039 return CResMgr::categories::glossaries::icon(); \
1040 case CSwordModuleInfo::Category::DailyDevotionals: \
1041 return CResMgr::categories::dailyDevotionals::icon(); \
1042 case CSwordModuleInfo::Category::MapsAndImages: \
1043 return CResMgr::categories::mapsAndImages::icon(); \
1044 default: \
1045 BT_ASSERT(category == \
1046 CSwordModuleInfo::Category::Questionable); \
1047 return CResMgr::categories::questionable::icon(); \
1048 } \
1049 }
1050BT_CAT_ICON(Icon, icon)
1051BT_CAT_ICON(IconAdd, icon_add)
1052BT_CAT_ICON(IconLocked, icon_locked)
1053#undef BT_CAT_ICON
1054
1055QIcon const &
1069
1071 switch (category) {
1073 return tr("Bibles");
1075 return tr("Commentaries");
1077 return tr("Books");
1079 return tr("Questionable");
1081 return tr("Maps and Images");
1083 return tr("Daily Devotionals");
1085 return tr("Lexicons and Dictionaries");
1087 return tr("Glossaries");
1088 default:
1089 return tr("Unknown");
1090 }
1091}
1092
1093QString CSwordModuleInfo::getSimpleConfigEntry(const QString & name) const {
1094 auto const * const value =
1095 m_swordModule.getConfigEntry(name.toUtf8().constData());
1096 return isUnicode() ? QString::fromUtf8(value) : QString::fromLatin1(value);
1097}
1098
1099/// \note See http://www.crosswire.org/wiki/DevTools:conf_Files#Localization
1100QString CSwordModuleInfo::getFormattedConfigEntry(const QString & name) const {
1101 auto const localeNames(
1102 QLocale(CSwordBackend::instance().booknameLanguage())
1103 .uiLanguages());
1104 for (int i = localeNames.size() - 1; i >= -1; --i) {
1105 sword::SWBuf RTF_Buffer;
1106 if (i < 0) {
1107 RTF_Buffer =
1108 m_swordModule.getConfigEntry(name.toUtf8().constData());
1109 } else {
1110 RTF_Buffer =
1111 m_swordModule.getConfigEntry(
1112 QStringLiteral("%1_%2")
1113 .arg(name, localeNames[i])
1114 .toUtf8().constData());
1115 }
1116 if (RTF_Buffer.length() > 0) {
1117 sword::RTFHTML RTF_Filter;
1118 RTF_Filter.processText(RTF_Buffer, nullptr, nullptr);
1119 return isUnicode()
1120 ? QString::fromUtf8(RTF_Buffer.c_str())
1121 : QString::fromLatin1(RTF_Buffer.c_str());
1122 }
1123 }
1124 return {};
1125}
1126
1128 if (m_hidden == hide)
1129 return false;
1130
1131 m_hidden = hide;
1132 static auto const configKey = QStringLiteral("state/hiddenModules");
1133 QStringList hiddenModules(btConfig().value<QStringList>(configKey));
1134 BT_ASSERT(hiddenModules.contains(m_cachedName) != hide);
1135 if (hide) {
1136 hiddenModules.append(m_cachedName);
1137 } else {
1138 hiddenModules.removeOne(m_cachedName);
1139 }
1140 btConfig().setValue(configKey, hiddenModules);
1141 Q_EMIT hiddenChanged(hide);
1142 return true;
1143}
#define BT_ASSERT(...)
Definition btassert.h:17
BtConfig & btConfig()
This is a shortchand for BtConfig::getInstance().
Definition btconfig.h:305
T value(QString const &key, T const &defaultValue=T()) const
Returns the settings value for the given global key.
void setValue(QString const &key, T const &value)
Sets a value for a key.
QString getModuleEncryptionKey(const QString &name)
Function to get a module decryption key.
Definition btconfig.cpp:227
void setModuleEncryptionKey(const QString &name, const QString &key)
Function to set a module decryption key.
Definition btconfig.cpp:220
The backend layer main class, a backend implementation of Sword.
void setOption(CSwordModuleInfo::FilterOption const &type, const int state)
Sets the state of the given filter option.
sword::SWMgr & raw()
static CSwordBackend & instance() noexcept
void setFilterOptions(const FilterOptions &options)
sword::SWConfig * getConfig() const
Implementation for Sword Bibles.
CSwordVerseKey const & lowerBound() const
CSwordVerseKey const & upperBound() const
virtual QString key() const =0
bool has(CSwordModuleInfo::Feature const feature) const noexcept
CSwordModuleSearch::ModuleResultList searchIndexed(QString const &searchedText, sword::ListKey const &scope) const
sword::SWVersion minimumSwordVersion() const
void write(CSwordKey *key, const QString &newText)
QString config(const CSwordModuleInfo::ConfigEntry entry) const
bool isUnicode() const noexcept
static FilterOption const redLetterWords
static FilterOption const headings
virtual CSwordKey * createKey() const =0
static FilterOption const textualVariants
static void deleteIndexForModule(const QString &name)
static QIcon const & categoryIconAdd(CSwordModuleInfo::Category category)
bool unlock(const QString &unlockKey)
static FilterOption const hebrewPoints
static QString getGlobalBaseIndexLocation()
CSwordModuleInfo::Category const m_cachedCategory
ModuleType const m_type
static QString categoryName(const CSwordModuleInfo::Category &category)
static FilterOption const strongNumbers
bool unlockKeyIsValid() const
static FilterOption const morphTags
static QIcon const & categoryIconLocked(CSwordModuleInfo::Category category)
static FilterOption const scriptureReferences
void setImportantFilterOptions(bool enable)
QString getFormattedConfigEntry(const QString &name) const
QString const m_cachedName
QString getModuleBaseIndexLocation() const
static FilterOption const hebrewCantillation
CSwordModuleInfo::TextDirection textDirection() const
bool const m_cachedHasVersion
ModuleType type() const
QString const & name() const
void hasIndexChanged(bool hasIndex)
bool hasImportantFilterOption() const
QString aboutText() const
static FilterOption const morphSegmentation
QString getModuleStandardIndexLocation() const
char const * textDirectionAsHtml() const
sword::SWModule & m_swordModule
void hiddenChanged(bool hidden)
void indexingProgress(int)
bool setHidden(bool hide)
virtual bool isWritable() const
static FilterOption const greekAccents
QString getSimpleConfigEntry(const QString &name) const
std::atomic< bool > m_cancelIndexing
static QIcon const & categoryIcon(CSwordModuleInfo::Category category)
static FilterOption const footnotes
void unlockedChanged(bool unlocked)
std::shared_ptr< Language const > const m_cachedLanguage
CSwordBackend & m_backend
::qint64 indexSize() const
CSwordModuleInfo::Category category() const
CSwordModuleInfo(CSwordModuleInfo &&)=delete
static FilterOption const lemmas
long index() const
#define BT_CAT_ICON(Icon, icon)
static constexpr unsigned long const BT_MAX_LUCENE_FIELD_LENGTH
#define T(f)
static constexpr unsigned const INDEX_VERSION
#define CANCEL_INDEXING
size_t lucene_utf8towcs(wchar_t *, const char *, size_t maxslen)
size_t lucene_wcstoutf8(char *, const wchar_t *, size_t maxslen)
std::vector< std::shared_ptr< sword::SWKey const > > ModuleResultList
CSwordModuleInfo::Category retrieveCategory(CSwordModuleInfo::ModuleType const type, CSwordModuleInfo::Features const features, sword::SWModule &module)
CSwordModuleInfo::Features retrieveFeatures(sword::SWModule const &module)
const QDir & getUserIndexDir()
static char const * valueToReadings(int value) noexcept
static char const * valueToOnOff(int value) noexcept