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) {
100 case CSwordModuleInfo::Unknown: // Fall thru
101 default:
103 }
104 }
105}
106
107inline CSwordModuleInfo::Features retrieveFeatures(
108 sword::SWModule const & module)
109{
110 // See https://wiki.crosswire.org/DevTools:conf_Files
111 CSwordModuleInfo::Features features;
112 for (auto featureEntries = module.getConfig().equal_range("Feature");
113 featureEntries.first != featureEntries.second;
114 ++featureEntries.first)
115 {
116 #define T(f) \
117 if (featureEntries.first->second == #f) { \
118 features |= CSwordModuleInfo::Feature ## f; \
119 }
120 T(StrongsNumbers)
121 else T(GreekDef)
122 else T(HebrewDef)
123 else T(GreekParse)
124 else T(HebrewParse)
125 else T(DailyDevotion)
126 else T(Glossary)
127 else T(Images)
128 else T(NoParagraphs)
129 #undef T
130 }
131 return features;
132}
133
134static const TCHAR * stop_words[] = { nullptr };
135
136class Analyzer : public lucene::analysis::standard::StandardAnalyzer {
137
138public: // Methods:
139
140 Analyzer() : lucene::analysis::standard::StandardAnalyzer(stop_words) {}
141
142};
143
144} // anonymous namespace
145
146char const *
148{ return value ? "On" : "Off"; }
149
150char const *
152 switch (value) {
153 case 0: return "Primary Reading";
154 case 1: return "Secondary Reading";
155 default: return "All Readings";
156 }
157}
158
160 "Footnotes",
161 "Footnotes",
162 QT_TRANSLATE_NOOP("QObject", "Footnotes")};
163
165 "Strong's Numbers",
166 "Strongs",
167 QT_TRANSLATE_NOOP("QObject", "Strong's numbers")};
168
170 "Headings",
171 "Headings",
172 QT_TRANSLATE_NOOP("QObject", "Headings")};
173
175 "Morphological Tags",
176 "Morph",
177 QT_TRANSLATE_NOOP("QObject", "Morphological tags")};
178
180 "Lemmas",
181 "Lemma",
182 QT_TRANSLATE_NOOP("QObject", "Lemmas")};
183
185 "Hebrew Vowel Points",
186 "HebrewPoints",
187 QT_TRANSLATE_NOOP("QObject", "Hebrew vowel points")};
188
190 "Hebrew Cantillation",
191 "Cantillation",
192 QT_TRANSLATE_NOOP("QObject", "Hebrew cantillation marks")};
193
195 "Greek Accents",
196 "GreekAccents",
197 QT_TRANSLATE_NOOP("QObject", "Greek accents")};
198
200 "Cross-references",
201 "Scripref",
202 QT_TRANSLATE_NOOP("QObject", "Scripture cross-references")};
203
205 "Words of Christ in Red",
206 "RedLetterWords",
207 QT_TRANSLATE_NOOP("QObject", "Red letter words")};
208
210 "Textual Variants",
211 "Variants",
212 QT_TRANSLATE_NOOP("QObject", "Textual variants"),
214
216 "Morph Segmentation",
217 "MorphSegmentation",
218 QT_TRANSLATE_NOOP("QObject", "Morph segmentation")};
219
220CSwordModuleInfo::CSwordModuleInfo(sword::SWModule & module,
221 CSwordBackend & backend,
222 ModuleType type)
223 : m_swordModule(module)
224 , m_backend(backend)
225 , m_type(type)
226 , m_cancelIndexing(false)
227 , m_cachedName(QString::fromUtf8(module.getName()))
228 , m_cachedFeatures(retrieveFeatures(module))
229 , m_cachedCategory(retrieveCategory(type, m_cachedFeatures, module))
230 , m_cachedLanguage(
231 Language::fromAbbrev(
232 util::tool::fixSwordBcp47(
233 m_cachedCategory == Glossary
234 /* Special handling for glossaries, we use the "from language" as
235 language for the module: */
236 ? config(GlossaryFrom)
237 : module.getLanguage())))
238 , m_cachedGlossaryTargetLanguage(
239 m_cachedCategory == Glossary
240 ? Language::fromAbbrev(
241 util::tool::fixSwordBcp47(module.getLanguage()))
242 : std::shared_ptr<Language const>())
243 , m_cachedHasVersion(
244 ((*m_backend.getConfig())[module.getName()]["Version"]).size() > 0)
245{
246 m_hidden = btConfig().value<QStringList>(
247 QStringLiteral("state/hiddenModules"))
248 .contains(m_cachedName);
249
251 && (minimumSwordVersion() > sword::SWVersion::currentVersion))
252 {
253 qWarning("The module \"%s\" requires a newer Sword library. Please "
254 "update to \"Sword %s\".",
255 m_cachedName.toUtf8().constData(),
256 minimumSwordVersion().getText());
257
258 /// \todo if this is the case, can we use the module at all?
259 }
260}
261
262bool CSwordModuleInfo::unlock(const QString & unlockKey) {
263 if (!isEncrypted())
264 return false;
265
266 bool unlocked = unlockKeyIsValid();
267
269
270 /// \todo remove this comment once it is no longer needed
271 /* There is currently a deficiency in sword 1.8.1 in that
272 backend->setCipherKey() does not work correctly for modules from which
273 data was already fetched. Therefore we have to reload the modules in
274 bibletime.cpp */
275 m_backend.raw().setCipherKey(m_swordModule.getName(),
276 unlockKey.toUtf8().constData());
277
278 /// \todo write to Sword config as well
279
280 if (unlockKeyIsValid() != unlocked)
281 Q_EMIT unlockedChanged(!unlocked);
282 return true;
283}
284
286 // still works, but the cipherkey is stored in BtConfig.
287 // Works because it is set in sword on program startup.
288
289 return isEncrypted() && !unlockKeyIsValid();
290}
291
293 /**
294 * If we have the CipherKey entry the module
295 * is encrypted but not necessarily locked
296 */
297
298 /* This code is still right, though we do no longer write to the module
299 config files any more. */
300 auto const & sections = m_backend.getConfig()->getSections();
301 auto const it(sections.find(m_cachedName.toUtf8().constData()));
302 if (it == sections.end())
303 return false;
304
305 const sword::ConfigEntMap & config = it->second;
306 return config.find("CipherKey") != config.end();
307}
308
310 sword::SWKey * const key = m_swordModule.getKey();
311 sword::VerseKey * const vk = dynamic_cast<sword::VerseKey *>(key);
312 if (vk)
313 vk->setIntros(false);
314 m_swordModule.setPosition(sword::TOP);
315
316 /* This needs to use ::fromLatin1 because if the text is still locked, a lot
317 of garbage will show up. It will also work with properly decrypted
318 Unicode text, because all non-ASCII Unicode chars consist of bytes >127
319 and therefore contain no control (nonprintable) characters, which are all
320 <127. */
321 const QString test(isUnicode()
322 ? QString::fromUtf8(m_swordModule.getRawEntry())
323 : QString::fromLatin1(m_swordModule.getRawEntry()));
324
325 if (test.isEmpty())
326 return false;
327
328 for (int i = 0; i < test.length() && i < 100; i++)
329 if (!test[i].isPrint() && !test[i].isNull())
330 return false;
331 return true;
332}
333
335 return m_swordModule.getConfigEntry("UnlockInfo");
336}
337
341
343 return QStringLiteral("%1/%2").arg(getGlobalBaseIndexLocation(),
344 m_cachedName.toLocal8Bit());
345}
346
348 // This for now returns the location of the main index
349 return getModuleBaseIndexLocation() + QStringLiteral("/standard");
350}
351
353 { // Is this a directory?
354 QFileInfo fi(getModuleStandardIndexLocation());
355 if (!fi.isDir())
356 return false;
357 }
358
359 // Are the index version and module version OK?
360 QSettings module_config(getModuleBaseIndexLocation()
361 + QStringLiteral("/bibletime-index.conf"),
362 QSettings::IniFormat);
363
365 && module_config.value(QStringLiteral("module-version")).toString()
367 {
368 return false;
369 }
370
371 if (module_config.value(QStringLiteral("index-version")).toUInt()
372 != INDEX_VERSION)
373 {
374 qDebug("%s: INDEX_VERSION is not compatible with this version of "
375 "BibleTime.",
376 m_cachedName.toUtf8().constData());
377 return false;
378 }
379
380 // Is the index there?
381 return lucene::index::IndexReader::indexExists(getModuleStandardIndexLocation()
382 .toLatin1().constData());
383}
384
395
402
404 auto cleanup =
405 qScopeGuard(
406 [this]() noexcept
407 { m_cancelIndexing.store(false, std::memory_order_relaxed); });
408#define CANCEL_INDEXING (m_cancelIndexing.load(std::memory_order_relaxed))
409
410 try {
411 // Without this we don't get strongs, lemmas, etc.
412 m_backend.setFilterOptions(btConfig().getFilterOptions());
413 /* Make sure we reset all important filter options which influcence the
414 plain filters. Turn on these options, they are needed for the
415 EntryAttributes population */
417
418 /* We don't want the following in the text, the do not carry searchable
419 information. */
423
424 // Do not use any stop words:
425 Analyzer analyzer;
426 const QString index(getModuleStandardIndexLocation());
427
428 QDir dir(QStringLiteral("/"));
429 dir.mkpath(getGlobalBaseIndexLocation());
430 dir.mkpath(getModuleBaseIndexLocation());
431 dir.mkpath(getModuleStandardIndexLocation());
432
433 if (lucene::index::IndexReader::indexExists(index.toLatin1().constData()))
434 if (lucene::index::IndexReader::isLocked(index.toLatin1().constData()))
435 lucene::index::IndexReader::unlock(index.toLatin1().constData());
436
437 // Always create a new index:
438 auto writer =
439 std::make_optional<lucene::index::IndexWriter>(
440 index.toLatin1().constData(),
441 &analyzer,
442 true);
443 writer->setMaxFieldLength(BT_MAX_LUCENE_FIELD_LENGTH);
444 writer->setUseCompoundFile(true); // Merge segments into a single file
445
446 CSwordBibleModuleInfo *bm = qobject_cast<CSwordBibleModuleInfo*>(this);
447
448 unsigned long verseLowIndex;
449 unsigned long verseHighIndex;
450
451 if(bm)
452 {
453 verseLowIndex = bm->lowerBound().index();
454 verseHighIndex = bm->upperBound().index();
455 }
456 else
457 {
458 m_swordModule.setPosition(sword::TOP);
459 verseLowIndex = m_swordModule.getIndex();
460 m_swordModule.setPosition(sword::BOTTOM);
461 verseHighIndex = m_swordModule.getIndex();
462 }
463
464 // verseLowIndex is not 0 in all cases (i.e. NT-only modules)
465 unsigned long verseIndex = verseLowIndex + 1;
466 unsigned long verseSpan = verseHighIndex - verseLowIndex;
467
468 // Index() is not implemented properly for lexicons, so work around it:
470 verseIndex = 0;
471 verseLowIndex = 0;
472 verseSpan = static_cast<CSwordLexiconModuleInfo *>(this)->entries().size();
473 }
474
475 Q_EMIT indexingProgress(0);
476
477 sword::SWKey * const key = m_swordModule.getKey();
478 sword::VerseKey * const vk = dynamic_cast<sword::VerseKey *>(key);
479
480 if (vk) {
481 /* We have to be sure to insert the english key into the index,
482 otherwise we'd be in trouble if the language changes. */
483 vk->setLocale("en_US");
484 /* If we have a verse based module, we want to include the pre-
485 chapter etc. headings in the search. */
486 vk->setIntros(true);
487 }
488
489 QByteArray textBuffer; // Holds UTF-8 data and is faster than QString.
490
491 // we start with the first module entry, key is automatically updated
492 // because key is a pointer to the modules key
493 m_swordModule.setSkipConsecutiveLinks(true);
494
495 auto const sPwcharBuffer =
496 std::make_unique<wchar_t[]>(BT_MAX_LUCENE_FIELD_LENGTH + 1);
497 wchar_t * const wcharBuffer = sPwcharBuffer.get();
498 BT_ASSERT(wcharBuffer);
499
500 if(bm && vk) // Implied that vk could be null due to cast above
501 vk->setIndex(bm->lowerBound().index());
502 else
503 m_swordModule.setPosition(sword::TOP);
504
505 bool importantFilterOption = hasImportantFilterOption();
506
507 while (!(m_swordModule.popError()) && !CANCEL_INDEXING) {
508
509 /* Also index Chapter 0 and Verse 0, because they might have
510 information in the entry attributes. We used to just put their
511 content into the textBuffer and continue to the next verse, but
512 with entry attributes this doesn't work any more. Hits in the
513 search dialog will show up as 1:1 (instead of 0). */
514
515 lucene::document::Document doc;
516
517 //index the key
518 lucene_utf8towcs(wcharBuffer, key->getText(), BT_MAX_LUCENE_FIELD_LENGTH);
519
520 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("key")),
521 static_cast<const TCHAR *>(wcharBuffer),
522 lucene::document::Field::STORE_YES
523 | lucene::document::Field::INDEX_NO)));
524
525 if (importantFilterOption) {
526 // Index text including strongs, morph, footnotes, and headings.
528 textBuffer.append(m_swordModule.stripText());
529 lucene_utf8towcs(wcharBuffer,
530 static_cast<const char *>(textBuffer),
532 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("content")),
533 static_cast<const TCHAR *>(wcharBuffer),
534 lucene::document::Field::STORE_NO
535 | lucene::document::Field::INDEX_TOKENIZED)));
536 textBuffer.clear();
537 }
538
539 // Index text without strongs, morph, footnotes, and headings.
541 textBuffer.append(m_swordModule.stripText());
542 lucene_utf8towcs(wcharBuffer,
543 static_cast<const char *>(textBuffer),
545 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("content")),
546 static_cast<const TCHAR *>(wcharBuffer),
547 lucene::document::Field::STORE_NO
548 | lucene::document::Field::INDEX_TOKENIZED)));
549 textBuffer.clear();
550
551 for (auto & vp : m_swordModule.getEntryAttributes()["Footnote"]) {
552 lucene_utf8towcs(wcharBuffer, vp.second["body"], BT_MAX_LUCENE_FIELD_LENGTH);
553 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("footnote")),
554 static_cast<const TCHAR *>(wcharBuffer),
555 lucene::document::Field::STORE_NO
556 | lucene::document::Field::INDEX_TOKENIZED)));
557 }
558
559 // Headings
560 for (auto & vp
561 : m_swordModule.getEntryAttributes()["Heading"]["Preverse"])
562 {
563 lucene_utf8towcs(wcharBuffer, vp.second, BT_MAX_LUCENE_FIELD_LENGTH);
564 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("heading")),
565 static_cast<const TCHAR *>(wcharBuffer),
566 lucene::document::Field::STORE_NO
567 | lucene::document::Field::INDEX_TOKENIZED)));
568 }
569
570 // Strongs/Morphs
571 for (auto const & vp : m_swordModule.getEntryAttributes()["Word"]) {
572 auto const & attrs = vp.second;
573 auto const partCountIter(attrs.find("PartCount"));
574 int partCount = (partCountIter != attrs.end())
575 ? QString(partCountIter->second).toInt()
576 : 0;
577 for (int i=0; i<partCount; i++) {
578
579 sword::SWBuf lemmaKey = "Lemma";
580 if (partCount > 1)
581 lemmaKey.appendFormatted(".%d", i+1);
582 auto const lemmaIter(attrs.find(lemmaKey));
583 if (lemmaIter != attrs.end()) {
584 lucene_utf8towcs(wcharBuffer, lemmaIter->second, BT_MAX_LUCENE_FIELD_LENGTH);
585 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("strong")),
586 static_cast<const TCHAR *>(wcharBuffer),
587 lucene::document::Field::STORE_NO
588 | lucene::document::Field::INDEX_TOKENIZED)));
589 }
590
591 }
592
593 auto const morphIter(attrs.find("Morph"));
594 if (morphIter != attrs.end()) {
595 lucene_utf8towcs(wcharBuffer, morphIter->second, BT_MAX_LUCENE_FIELD_LENGTH);
596 doc.add(*(new lucene::document::Field(static_cast<const TCHAR *>(_T("morph")),
597 static_cast<const TCHAR *>(wcharBuffer),
598 lucene::document::Field::STORE_NO
599 | lucene::document::Field::INDEX_TOKENIZED)));
600 }
601 }
602
603 writer->addDocument(&doc);
604 //Index() is not implemented properly for lexicons, so we use a
605 //workaround.
607 verseIndex++;
608 } else {
609 verseIndex = m_swordModule.getIndex();
610 }
611
612 if (verseIndex % 200 == 0) {
613 if (verseSpan == 0) { // Prevent division by zero
614 Q_EMIT indexingProgress(0);
615 } else {
616 Q_EMIT indexingProgress(
617 static_cast<int>(
618 (100 * (verseIndex - verseLowIndex))
619 / verseSpan));
620 }
621 }
622
623 m_swordModule.increment();
624 } // while (!(m_module.Error()) && !CANCEL_INDEXING)
625
626 if (!CANCEL_INDEXING)
627 writer->optimize();
628 writer->close();
629 writer.reset();
630
631 if (CANCEL_INDEXING) {
632 deleteIndex();
633 } else {
634 QSettings module_config(getModuleBaseIndexLocation()
635 + QStringLiteral("/bibletime-index.conf"),
636 QSettings::IniFormat);
638 module_config.setValue(QStringLiteral("module-version"),
640 module_config.setValue(QStringLiteral("index-version"),
642 Q_EMIT hasIndexChanged(true);
643 }
644 // } catch (CLuceneError & e) {
645 } catch (...) {
646 deleteIndex();
647 throw;
648 }
649}
650
655
656void CSwordModuleInfo::deleteIndexForModule(const QString & name) {
657 QDir(QStringLiteral("%1/%2").arg(getGlobalBaseIndexLocation(), name))
658 .removeRecursively();
659}
660
662 namespace DU = util::directory;
663 return DU::getDirSizeRecursive(getModuleBaseIndexLocation());
664}
665
667CSwordModuleInfo::searchIndexed(QString const & searchedText,
668 sword::ListKey const & scope) const
669{
670 auto const sPutfBuffer =
671 std::make_unique<char[]>(BT_MAX_LUCENE_FIELD_LENGTH + 1);
672 auto const sPwcharBuffer =
673 std::make_unique<wchar_t[]>(BT_MAX_LUCENE_FIELD_LENGTH + 1);
674 char * const utfBuffer = sPutfBuffer.get();
675 BT_ASSERT(utfBuffer);
676 wchar_t * const wcharBuffer = sPwcharBuffer.get();
677 BT_ASSERT(wcharBuffer);
678
679 // work around Swords thread insafety for Bibles and Commentaries
680 m_swordModule.setKey(createKey()->asSwordKey());
681
682 // do not use any stop words
683 Analyzer analyzer;
684 lucene::search::IndexSearcher searcher(getModuleStandardIndexLocation().toLatin1().constData());
685 lucene_utf8towcs(wcharBuffer, searchedText.toUtf8().constData(), BT_MAX_LUCENE_FIELD_LENGTH);
686 std::unique_ptr<lucene::search::Query> q(lucene::queryParser::QueryParser::parse(static_cast<const TCHAR *>(wcharBuffer),
687 static_cast<const TCHAR *>(_T("content")),
688 &analyzer));
689
690 std::unique_ptr<lucene::search::Hits> h(
691 searcher.search(q.get(), lucene::search::Sort::INDEXORDER()));
692
693 const bool useScope = (scope.getCount() > 0);
694
695 lucene::document::Document * doc = nullptr;
696 std::unique_ptr<sword::SWKey> swKey(m_swordModule.createKey());
697
698 sword::VerseKey * const vk = dynamic_cast<sword::VerseKey *>(swKey.get());
699 if (vk)
700 vk->setIntros(true);
701
703 for (size_t i = 0; i < h->length(); ++i) {
704 doc = &h->doc(i);
705 lucene_wcstoutf8(utfBuffer,
706 static_cast<const wchar_t *>(doc->get(static_cast<const TCHAR *>(_T("key")))),
708
709 swKey->setText(utfBuffer);
710
711 // Limit results based on scope:
712 if (useScope) {
713 for (int j = 0; j < scope.getCount(); j++) {
714 if (auto const * const vkey =
715 dynamic_cast<sword::VerseKey const *>(
716 scope.getElement(j)))
717 {
718 if (vkey->getLowerBound().compare(*swKey) <= 0
719 && vkey->getUpperBound().compare(*swKey) >= 0)
720 results.emplace_back(swKey->clone());
721 }
722 }
723 } else { // No scope, give me all buffers
724 results.emplace_back(swKey->clone());
725 }
726 }
727
728 return results;
729}
730
732 return sword::SWVersion(config(CSwordModuleInfo::MinimumSwordVersion)
733 .toUtf8().constData());
734}
735
737 switch (entry) {
738
739 case AboutInformation:
740 return getFormattedConfigEntry(QStringLiteral("About"));
741
742 case CipherKey: {
743 if (btConfig().getModuleEncryptionKey(m_cachedName).isNull()) {
744 return QString(m_swordModule.getConfigEntry("CipherKey")); // Fallback
745 } else {
747 }
748 }
749
750 case AbsoluteDataPath: {
751 auto path(getSimpleConfigEntry(QStringLiteral("AbsoluteDataPath")));
752 if (!path.endsWith('/')) /// \todo is this needed?
753 path.append('/');
754
755 return path;
756 }
757
758 case DataPath: {
759 QString path(getSimpleConfigEntry(QStringLiteral("DataPath")));
760
761 // Make sure we remove the dataFile part if it's a Lexicon
764 {
765 int pos = path.lastIndexOf('/'); // Last slash in the string
766 if (pos != -1)
767 path = path.left(pos + 1); // Include the slash
768 }
769 return path;
770 }
771
772 case Description:
773 return getFormattedConfigEntry(QStringLiteral("Description"));
774
775 case ModuleVersion: {
776 auto const version(getSimpleConfigEntry(QStringLiteral("Version")));
777 return version.isEmpty() ? QStringLiteral("1.0") : version;
778 }
779
780 case MinimumSwordVersion: {
781 auto const minimumVersion =
782 getSimpleConfigEntry(QStringLiteral("MinimumVersion"));
783 return minimumVersion.isEmpty()
784 ? QStringLiteral("0.0")
785 : minimumVersion;
786 }
787
788 case TextDir: {
789 auto const dir = getSimpleConfigEntry(QStringLiteral("Direction"));
790 return dir.isEmpty() ? QStringLiteral("LtoR") : dir;
791 }
792
793 case DisplayLevel: {
794 auto const level =
795 getSimpleConfigEntry(QStringLiteral("DisplayLevel"));
796 return level.isEmpty() ? QStringLiteral("1") : level;
797 }
798
799 case GlossaryFrom: {
801 return {};
802
803 return getSimpleConfigEntry(QStringLiteral("GlossaryFrom"));
804 }
805
806 case GlossaryTo: {
808 return {};
809
810 return getSimpleConfigEntry(QStringLiteral("GlossaryTo"));
811 }
812
813 case Markup: {
814 auto const markup =
815 getSimpleConfigEntry(QStringLiteral("SourceType"));
816 return markup.isEmpty() ? QStringLiteral("Unknown") : markup;
817 }
818
821 QStringLiteral("DistributionLicense"));
822
825 QStringLiteral("DistributionSource"));
826
828 return getFormattedConfigEntry(QStringLiteral("DistributionNotes"));
829
830 case TextSource:
831 return getFormattedConfigEntry(QStringLiteral("TextSource"));
832
833 case CopyrightNotes:
834 return getFormattedConfigEntry(QStringLiteral("CopyrightNotes"));
835
836 case CopyrightHolder:
837 return getFormattedConfigEntry(QStringLiteral("CopyrightHolder"));
838
839 case CopyrightDate:
840 return getFormattedConfigEntry(QStringLiteral("CopyrightDate"));
841
844 QStringLiteral("CopyrightContactName"));
845
848 QStringLiteral("CopyrightContactAddress"));
849
852 QStringLiteral("CopyrightContactEmail"));
853
854 default:
855 return {};
856 }
857}
858
860 /// \todo This is a BAD workaround to see if the filter is GBF, OSIS or ThML!
861 auto const & originalOptionName = option.configOptionName;
862 using namespace std::string_literals;
863 std::string const optionNames[] = {
864 "OSIS"s + originalOptionName,
865 "GBF"s + originalOptionName,
866 "ThML"s + originalOptionName,
867 "UTF8"s + originalOptionName,
868 originalOptionName
869 };
870 for (auto [it, end] =
871 m_swordModule.getConfig().equal_range("GlobalOptionFilter");
872 it != end;
873 ++it)
874 {
875 auto const & valueBuf = it->second;
876 std::string_view const value(valueBuf.c_str(), valueBuf.size());
877 for (auto const & optionName : optionNames)
878 if (value == optionName)
879 return true;
880 }
881 return false;
882}
883
885 return (config(TextDir) == QStringLiteral("RtoL"))
887 : LeftToRight;
888}
889
891{ return textDirection() == RightToLeft ? "rtl" : "ltr"; }
892
893void CSwordModuleInfo::write(CSwordKey * key, const QString & newText) {
894 m_swordModule.setKey(key->key().toUtf8().constData());
895
896 /* Don't store a pointer to the const char* value somewhere because QCString
897 doesn't keep the value of it. */
898 m_swordModule.setEntry(isUnicode()
899 ? newText.toUtf8().constData()
900 : newText.toLocal8Bit().constData());
901}
902
904 static auto const row(
905 QStringLiteral("<tr><td><b>%1</b></td><td>%2</td></tr>"));
906
907 auto text(QStringLiteral("<table>"));
908
909 text += row
910 .arg(tr("Version"))
912 ? config(CSwordModuleInfo::ModuleVersion).toHtmlEscaped()
913 : tr("unknown"));
914
915 {
916 const QString sourceType(m_swordModule.getConfigEntry("SourceType"));
917 text += row
918 .arg(tr("Markup"))
919 .arg(!sourceType.isEmpty()
920 ? sourceType.toHtmlEscaped()
921 : tr("unknown"));
922 }
923
924 text += row
925 .arg(tr("Location"))
926 .arg(config(CSwordModuleInfo::AbsoluteDataPath).toHtmlEscaped());
927
928 text += row
929 .arg(tr("Language"))
930 .arg(m_cachedLanguage->translatedName().toHtmlEscaped());
931
932 if (char const * const e = m_swordModule.getConfigEntry("Category"))
933 text += row.arg(tr("Category"))
934 .arg(QString{e}.toHtmlEscaped());
935
936 if (char const * const e = m_swordModule.getConfigEntry("LCSH"))
937 text += row.arg(tr("LCSH"))
938 .arg(QString{e}.toHtmlEscaped());
939
940 text += row
941 .arg(tr("Writable"))
942 .arg(isWritable() ? tr("yes") : tr("no"));
943
944 if (isEncrypted()) {
945 text += row
946 .arg(tr("Unlock key"))
947 .arg(config(CSwordModuleInfo::CipherKey).toHtmlEscaped());
948 if (char const * const e = m_swordModule.getConfigEntry("UnlockInfo"))
949 text += row.arg(tr("Unlock info")).arg(QString(e).toHtmlEscaped());
950 }
951
952 QString options;
953 static constexpr auto const allFilterOptions = {
954 &footnotes,
956 &headings,
957 &morphTags,
958 &lemmas,
966 };
967 for (auto const * const filterOption : allFilterOptions) {
968 if (has(*filterOption)) {
969 if (!options.isEmpty())
970 options += QStringLiteral(", ");
971 options += QObject::tr(filterOption->translatableOptionName);
972 }
973 }
974
975 if (!options.isEmpty())
976 text += row
977 .arg(tr("Features"))
978 .arg(options.toHtmlEscaped());
979
980 text += QStringLiteral("</table><hr>");
981
982 // Clearly say the module contains cult/questionable materials
983 if (m_cachedCategory == Cult)
984 text += QStringLiteral("<br/><b>%1</b><br/><br/>")
985 .arg(tr("Take care, this work contains cult / questionable "
986 "material!"));
987
988 text += QStringLiteral("<b>%1:</b><br/>%2")
989 .arg(tr("About"))
990 .arg(config(AboutInformation)); // May contain HTML, don't escape
991
992 text += QStringLiteral("<hr><table>");
993
994 struct Entry { ConfigEntry const type; char const * const text; };
995 auto const entries = {
996 Entry{DistributionLicense, QT_TR_NOOP("Distribution license")},
997 Entry{DistributionSource, QT_TR_NOOP("Distribution source")},
998 Entry{DistributionNotes, QT_TR_NOOP("Distribution notes")},
999 Entry{TextSource, QT_TR_NOOP("Text source")},
1000 Entry{CopyrightNotes, QT_TR_NOOP("Copyright notes")},
1001 Entry{CopyrightHolder, QT_TR_NOOP("Copyright holder")},
1002 Entry{CopyrightDate, QT_TR_NOOP("Copyright date")},
1003 Entry{CopyrightContactName, QT_TR_NOOP("Copyright contact name")},
1004 Entry{CopyrightContactAddress, QT_TR_NOOP("Copyright contact address")},
1005 Entry{CopyrightContactEmail, QT_TR_NOOP("Copyright contact email")},
1006 };
1007 for (auto const & entry : entries) {
1008 auto const value = config(entry.type);
1009 if (!value.isEmpty())
1010 text += row.arg(tr(entry.text).toHtmlEscaped())
1011 .arg(value.toHtmlEscaped());
1012 }
1013
1014 return text + QStringLiteral("</table>");
1015}
1016
1017bool CSwordModuleInfo::isUnicode() const noexcept
1018{ return m_swordModule.isUnicode(); }
1019
1020QIcon const & CSwordModuleInfo::moduleIcon(const CSwordModuleInfo & module) {
1022 switch (cat) {
1024 return module.isLocked()
1025 ? CResMgr::modules::bible::icon_locked()
1026 : CResMgr::modules::bible::icon_unlocked();
1027
1029 return module.isLocked()
1030 ? CResMgr::modules::commentary::icon_locked()
1031 : CResMgr::modules::commentary::icon_unlocked();
1032
1034 return module.isLocked()
1035 ? CResMgr::modules::lexicon::icon_locked()
1036 : CResMgr::modules::lexicon::icon_unlocked();
1037
1039 return module.isLocked()
1040 ? CResMgr::modules::book::icon_locked()
1041 : CResMgr::modules::book::icon_unlocked();
1042
1048 default:
1049 return categoryIcon(cat);
1050 }
1051}
1052
1054 switch (category) {
1056 return CResMgr::categories::bibles::icon();
1058 return CResMgr::categories::commentaries::icon();
1060 return CResMgr::categories::books::icon();
1062 return CResMgr::categories::cults::icon();
1064 return CResMgr::categories::images::icon();
1066 return CResMgr::categories::dailydevotional::icon();
1068 return CResMgr::categories::lexicons::icon();
1070 return CResMgr::categories::glossary::icon();
1072 default:
1074 }
1075}
1076
1078 switch (category) {
1080 return tr("Bibles");
1082 return tr("Commentaries");
1084 return tr("Books");
1086 return tr("Cults/Unorthodox");
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:228
void setModuleEncryptionKey(const QString &name, const QString &key)
Function to set a module decryption key.
Definition btconfig.cpp:221
static BtIcons & instance()
Definition bticons.h:44
QIcon const icon_null
Definition bticons.h:55
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)
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 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
static QIcon const & categoryIcon(CSwordModuleInfo::Category category)
QIcon moduleIcon() 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 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
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