BibleTime
thmltohtml.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 "thmltohtml.h"
14
15#include <QRegularExpression>
16#include <QRegularExpressionMatch>
17#include <QUrl>
18#include <utility>
19#include "../../util/btassert.h"
20#include "../config/btconfig.h"
21#include "../drivers/cswordmoduleinfo.h"
22#include "../managers/cswordbackend.h"
23#include "../managers/referencemanager.h"
24
25// Sword includes:
26#ifdef __GNUC__
27#pragma GCC diagnostic push
28#pragma GCC diagnostic ignored "-Wextra-semi"
29#pragma GCC diagnostic ignored "-Wsuggest-override"
30#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
31#endif
32#ifdef __clang__
33#pragma clang diagnostic push
34#pragma clang diagnostic ignored "-Wsuggest-destructor-override"
35#endif
36#include <swmodule.h>
37#include <utilstr.h>
38#include <utilxml.h>
39#include <versekey.h>
40#ifdef __clang__
41#pragma clang diagnostic pop
42#endif
43#ifdef __GNUC__
44#pragma GCC diagnostic pop
45#endif
46
47
48namespace Filters {
49
51 setEscapeStringCaseSensitive(true);
52 setPassThruUnknownEscapeString(true); //the HTML widget will render the HTML escape codes
53
54 setTokenStart("<");
55 setTokenEnd(">");
56 setTokenCaseSensitive(true);
57
58 addTokenSubstitute("/foreign", "</span>");
59
60 removeTokenSubstitute("note");
61 removeTokenSubstitute("/note");
62}
63
64char ThmlToHtml::processText(sword::SWBuf &buf, const sword::SWKey *key,
65 const sword::SWModule *module)
66{
67 sword::ThMLHTML::processText(buf, key, module);
68
69 if (auto * const m =
70 CSwordBackend::instance().findModuleByName(module->getName()))
71 {
72 // only parse if the module has strongs or lemmas:
73 if (!m->has(CSwordModuleInfo::lemmas)
75 return 1;
76 }
77
78 QStringList list;
79 {
80 auto t = QString::fromUtf8(buf.c_str());
81 {
82 static QRegularExpression const tag(
83 QStringLiteral(R"PCRE(([.,;]?<sync[^>]+(type|value)=)PCRE"
84 R"PCRE("([^"]+)"[^>]+(type|value)=)PCRE"
85 R"PCRE("([^"]+)"([^<]*)>)+)PCRE"));
86 QRegularExpressionMatch match;
87 auto pos = t.indexOf(tag, 0, &match);
88 if (pos == -1) //no strong or morph code found in this text
89 return 1; //WARNING: Return already here
90 do {
91 auto const partLength = pos + match.capturedLength();
92 list.append(t.left(partLength));
93 t.remove(0, partLength);
94 pos = t.indexOf(tag, 0, &match);
95 } while (pos != -1);
96 }
97
98 // Append the trailing text to the list:
99 if (!t.isEmpty())
100 list.append(std::move(t));
101 }
102
103 static QRegularExpression const tag(
104 QStringLiteral(R"PCRE(<sync[^>]+(type|value|class)="([^"]+)"[^>]+)PCRE"
105 R"PCRE((type|value|class)="([^"]+)"[^>]+)PCRE"
106 R"PCRE(((type|value|class)="([^"]+)")*([^<]*)>)PCRE"));
107 QString result;
108 for (auto & e : list) {
109
110 // pass text ahead of <sync> stright through
111 if (auto const pos = e.indexOf(tag); pos > 0) {
112 result.append(e.left(pos));
113 e.remove(0, pos);
114 }
115
116 // parse <sync> and change to <span>
117 bool hasLemmaAttr = false;
118 bool hasMorphAttr = false;
119
120 QRegularExpressionMatch match;
121 auto pos = e.indexOf(tag, 0, &match);
122 bool insertedTag = false;
123
124 while (pos != -1) {
125 bool isMorph = false;
126 bool isStrongs = false;
127 QString value;
128 QString valueClass;
129
130 // check 3 attribute/value pairs
131
132 for (int i = 1; i < 6; i += 2) {
133 if (i > 4)
134 i++;
135
136 auto const attrName = match.captured(i);
137 auto const attrValue = match.captured(i + 1);
138 if (attrName == QStringLiteral("type")) {
139 isMorph = (attrValue == QStringLiteral("morph"));
140 isStrongs = (attrValue == QStringLiteral("Strongs"));
141 } else if (attrName == QStringLiteral("value")) {
142 value = attrValue;
143 } else if (attrName == QStringLiteral("class")) {
144 valueClass = attrValue;
145 } else { // optional 3rd attribute pair is not present:
146 BT_ASSERT(attrName.isEmpty());
147 }
148 }
149
150 // prepend the class qualifier to the value
151 if (!valueClass.isEmpty())
152 value = QStringLiteral("%1:%2").arg(valueClass, value);
153
154 if (value.isEmpty()) {
155 break;
156 }
157
158 //insert the span
159 if (!insertedTag) {
160 e.replace(pos, match.capturedLength(), QStringLiteral("</span>"));
161 pos += 7;
162
163 auto rep = QStringLiteral("<span lemma=\"%1\">").arg(value);
164 int startPos = 0;
165 QChar c = e[startPos];
166
167 while ((startPos < pos) && (c.isSpace() || c.isPunct())) {
168 ++startPos;
169 c = e[startPos];
170 }
171
172 hasLemmaAttr = isStrongs;
173 hasMorphAttr = isMorph;
174
175 pos += rep.length();
176 e.insert(startPos, std::move(rep));
177 }
178 else { //add the attribute to the existing tag
179 e.remove(pos, match.capturedLength());
180
181 if ((!isMorph && hasLemmaAttr) || (isMorph && hasMorphAttr)) { //we append another attribute value, e.g. 3000 gets 3000|5000
182 //search the existing attribute start
183 auto const & attrRegExp =
184 [isMorph]{
185 if (isMorph) {
186 static QRegularExpression const re(
187 QStringLiteral("morph=\".+?(?=\")"));
188 return re;
189 } else {
190 static QRegularExpression const re(
191 QStringLiteral("lemma=\".+?(?=\")"));
192 return re;
193 }
194 }();
195 QRegularExpressionMatch match;
196 const int foundAttrPos = e.indexOf(attrRegExp, pos, &match);
197
198 if (foundAttrPos != -1) {
199 e.insert(foundAttrPos + match.capturedLength(),
200 QStringLiteral("|%1").arg(value));
201 pos += value.length() + 1;
202
203 hasLemmaAttr = !isMorph;
204 hasMorphAttr = isMorph;
205 }
206 }
207 else { //attribute was not yet inserted
208 static QRegularExpression const re(
209 QStringLiteral("morph=|lemma="));
210 const int attrPos = e.indexOf(re, 0);
211
212 if (attrPos >= 0) {
213 hasMorphAttr = isMorph;
214 hasLemmaAttr = !isMorph;
215
216 auto attr = QStringLiteral("%1=\"%2\" ")
217 .arg(isMorph
218 ? QStringLiteral("morph")
219 : QStringLiteral("lemma"),
220 value);
221 pos += attr.length();
222 e.insert(attrPos, std::move(attr)); /// \bug e.replace() instead?
223 }
224 }
225 }
226
227 insertedTag = true;
228 pos = e.indexOf(tag, pos, &match);
229 }
230
231 result.append(std::move(e));
232 }
233
234 if (!list.isEmpty())
235 buf = result.toUtf8();
236
237 return 1;
238}
239
240
241bool ThmlToHtml::handleToken(sword::SWBuf &buf, const char *token,
242 sword::BasicFilterUserData *userData)
243{
244 if (!substituteToken(buf, token) && !substituteEscapeString(buf, token)) {
245 sword::XMLTag const tag(token);
246 BT_ASSERT(dynamic_cast<UserData *>(userData));
247 UserData * const myUserData = static_cast<UserData *>(userData);
248 // Hack to be able to call stuff like Lang():
249 sword::SWModule const * const myModule =
250 const_cast<sword::SWModule *>(myUserData->module);
251 char const * const tagName = tag.getName();
252 if (!tagName) // unknown tag, pass through:
253 return sword::ThMLHTML::handleToken(buf, token, userData);
254 if (!sword::stricmp(tagName, "foreign")) {
255 // A text part in another language, we have to set the right font
256
257 if (const char * const tagLang = tag.getAttribute("lang"))
258 buf.append("<span class=\"foreign\" lang=\"")
259 .append(tagLang)
260 .append("\">");
261 } else if (!sword::stricmp(tagName, "sync")) {
262 // If Morph or Strong or Lemma:
263 if (const char * const tagType = tag.getAttribute("type"))
264 if (!sword::stricmp(tagType, "morph")
265 || !sword::stricmp(tagType, "Strongs")
266 || !sword::stricmp(tagType, "lemma"))
267 buf.append('<').append(token).append('>');
268 } else if (!sword::stricmp(tagName, "note")) { // <note> tag
269 if (!tag.isEmpty()) {
270 if (!tag.isEndTag()) {
271 buf.append(" <span class=\"footnote\" note=\"")
272 .append(myModule->getName())
273 .append('/')
274 .append(myUserData->key->getShortText())
275 .append('/')
276 .append(QString::number(myUserData->swordFootnote).toUtf8().constData())
277 .append("\">*</span> ");
278
279 myUserData->swordFootnote++;
280 myUserData->suspendTextPassThru = true;
281 myUserData->inFootnoteTag = true;
282 } else if (tag.isEndTag()) { // end tag
283 // buf += ")</span>";
284 myUserData->suspendTextPassThru = false;
285 myUserData->inFootnoteTag = false;
286 }
287 }
288 } else if (!sword::stricmp(tagName, "scripRef")) { // a scripRef
289 // scrip refs which are embeded in footnotes may not be displayed!
290
291 if (!myUserData->inFootnoteTag) {
292 if (tag.isEndTag()) {
293 if (myUserData->inscriptRef) { // like "<scripRef passage="John 3:16">See John 3:16</scripRef>"
294 buf.append("</a></span>");
295
296 myUserData->inscriptRef = false;
297 myUserData->suspendTextPassThru = false;
298 } else { // like "<scripRef>John 3:16</scripRef>"
299 if (CSwordModuleInfo const * const mod =
300 btConfig().getDefaultSwordModuleByType(
301 "standardBible"))
302 {
304 mod->name(),
305 // current module key:
306 QString::fromUtf8(myUserData->key->getText()),
307 myModule->getLanguage()};
308
309 //it's ok to split the reference, because to descriptive text is given
310 bool insertSemicolon = false;
311 buf.append("<span class=\"crossreference\">");
312 QStringList const refs(
313 QString::fromUtf8(
314 myUserData->lastTextNode.c_str()).split(
315 ';'));
316 QString oldRef; // the previous reference to use as a base for the next refs
317 for (auto const & ref : refs) {
318 if (!oldRef.isEmpty())
319 options.refBase = oldRef; // Use the last ref as a base, e.g. Rom 1,2-3, when the next ref is only 3:3-10
320
321 // Use the parsed result as the base for the next ref:
323 ref,
324 options);
325
326 // Prepend a ref divider if we're after the first one
327 if (insertSemicolon)
328 buf.append("; ");
329
330 buf.append("<a href=\"")
331 .append(
333 *mod,
334 oldRef
335 ).toUtf8().constData()
336 )
337 .append("\" crossrefs=\"")
338 .append(oldRef.toUtf8().constData())
339 .append("\">")
340 .append(ref.toUtf8().constData())
341 .append("</a>");
342 insertSemicolon = true;
343 }
344 buf.append("</span>"); //crossref end
345 }
346 myUserData->suspendTextPassThru = false;
347 }
348 } else if (tag.getAttribute("passage") ) {
349 // The passage was given as a parameter value
350 myUserData->inscriptRef = true;
351 myUserData->suspendTextPassThru = false;
352
354 QStringLiteral("standardBible"));
355 if (! mod)
357
358 if (mod) {
359 BT_ASSERT(tag.getAttribute("passage"));
360 QString const completeRef(
362 QString::fromUtf8(
363 tag.getAttribute("passage")),
365 mod->name(),
366 QString::fromUtf8(
367 myUserData->key->getText()),
368 myModule->getLanguage()}));
369 buf.append("<span class=\"crossreference\">")
370 .append("<a href=\"")
371 .append(
373 *mod,
374 completeRef
375 ).toUtf8().constData()
376 )
377 .append("\" crossrefs=\"")
378 .append(completeRef.toUtf8().constData())
379 .append("\">");
380 } else {
381 buf.append("<span><a>");
382 }
383 } else { // We're starting a scripRef like "<scripRef>John 3:16</scripRef>"
384 myUserData->inscriptRef = false;
385 /* Let's stop text from going to output, the text get's
386 added in the -tag handler: */
387 myUserData->suspendTextPassThru = true;
388 }
389 }
390 } else if (!sword::stricmp(tagName, "div")) {
391 if (tag.isEndTag()) {
392 buf.append("</div>");
393 } else if (char const * const tagClass = tag.getAttribute("class")){
394 if (!sword::stricmp(tagClass, "sechead") ) {
395 buf.append("<div class=\"sectiontitle\">");
396 } else if (!sword::stricmp(tagClass, "title")) {
397 buf.append("<div class=\"booktitle\">");
398 }
399 }
400 } else if (!sword::stricmp(tagName, "img") && tag.getAttribute("src")) {
401 const char * value = tag.getAttribute("src");
402
403 if (value[0] == '/')
404 value++; //strip the first /
405
406 if (!myUserData->absolutePath.has_value()) {
407 auto const * const absoluteDataPath =
408 myUserData->module->getConfigEntry("AbsoluteDataPath");
409 myUserData->absolutePath.emplace(
410 myUserData->module->isUnicode()
411 ? QString::fromUtf8(absoluteDataPath)
412 : QString::fromLatin1(absoluteDataPath));
413 }
414
415 buf.append("<img src=\"")
416 .append(
417 QUrl::fromLocalFile(
418 QStringLiteral("%1/%2").arg(
419 *myUserData->absolutePath,
420 QString::fromUtf8(value))
421 ).toString().toUtf8().constData())
422 .append("\" />");
423 } else { // Let unknown token pass thru:
424 return sword::ThMLHTML::handleToken(buf, token, userData);
425 }
426 }
427 return true;
428}
429
430} // namespace Filtes
#define BT_ASSERT(...)
Definition btassert.h:17
BtConfig & btConfig()
This is a shortchand for BtConfig::getInstance().
Definition btconfig.h:305
CSwordModuleInfo * getDefaultSwordModuleByType(const QString &moduleType)
Returns default sword module info class for a given module type.
Definition btconfig.cpp:497
CSwordModuleInfo * findFirstAvailableModule(CSwordModuleInfo::ModuleType type)
static CSwordBackend & instance() noexcept
static FilterOption const strongNumbers
static FilterOption const lemmas
bool handleToken(sword::SWBuf &buf, const char *token, sword::BasicFilterUserData *userData) override
char processText(sword::SWBuf &buf, const sword::SWKey *key, const sword::SWModule *module=nullptr) override
QString parseVerseReference(QString const &ref, ParseOptions const &options)
QString encodeHyperlink(CSwordModuleInfo const &module, QString const &key)