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