BibleTime
osistohtml.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 "osistohtml.h"
14
15#include <QString>
16#include <string_view>
17#include "../config/btconfig.h"
18#include "../drivers/cswordmoduleinfo.h"
19#include "../managers/cswordbackend.h"
20#include "../managers/referencemanager.h"
21
22// Sword includes:
23#ifdef __GNUC__
24#pragma GCC diagnostic push
25#pragma GCC diagnostic ignored "-Wsuggest-override"
26#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
27#endif
28#include <swbuf.h>
29#include <swmodule.h>
30#include <utilxml.h>
31#ifdef __GNUC__
32#pragma GCC diagnostic pop
33#endif
34
35
36namespace {
37
38template <typename UserData>
39void renderReference(char const * const osisRef,
40 sword::SWBuf & buf,
41 sword::SWModule const & myModule,
42 UserData const & myUserData)
43{
44 QString const ref(osisRef);
45 //BT_ASSERT(!ref.isEmpty()); checked later
46
47 if (!ref.isEmpty()) {
48 /* find out the mod, using the current module makes sense if it's a
49 bible or commentary because the refs link into a bible by default.
50 If the osisRef is something like "ModuleID:key comes here" then the
51 modulename is given, so we'll use that one. */
52
53 auto const * mod =
55 //BT_ASSERT(mod); checked later
56 if (!mod || (mod->type() != CSwordModuleInfo::Bible
57 && mod->type() != CSwordModuleInfo::Commentary))
58 {
60 QStringLiteral("standardBible"));
61 if (!mod)
64 }
65
66 // BT_ASSERT(mod); There's no necessarily a module or standard Bible
67
68 //if the osisRef like "GerLut:key" contains a module, use that
69 auto const pos = ref.indexOf(':');
70
71 QString hrefRef;
72 if ((pos >= 0)
73 && ref.at(pos - 1).isLetter()
74 && ref.at(pos + 1).isLetter())
75 {
76 auto const newModuleName(ref.left(pos));
77 hrefRef = ref.mid(pos + 1);
78
79 if (auto const * const moduleByName =
80 CSwordBackend::instance().findModuleByName(newModuleName))
81 mod = moduleByName;
82 } else {
83 hrefRef = ref;
84 }
85
86 if (mod) {
87 using namespace ReferenceManager;
88 ParseOptions const options{
89 mod->name(),
90 QString::fromUtf8(myUserData.key->getText()),
91 myModule.getLanguage()};
92
93 auto const hyperlink( // Hyperlink with key and mod
94 encodeHyperlink(
95 *mod,
96 parseVerseReference(hrefRef, options)).toUtf8());
97
98 // Ref must contain the osisRef module marker if there was any:
99 auto const moduleMarker(parseVerseReference(ref, options).toUtf8());
100
101 buf.append("<a href=\"")
102 .append(hyperlink.constData())
103 .append("\" crossrefs=\"")
104 .append(moduleMarker.constData())
105 .append("\">");
106 }
107 /** \todo Should we add something if there were no referenced module
108 available? */
109 }
110} // renderReference()
111
112} // anonymous namespace
113
115 setPassThruUnknownEscapeString(true); //the HTML widget will render the HTML escape codes
116
117 addTokenSubstitute("inscription", "<span class=\"inscription\">");
118 addTokenSubstitute("/inscription", "</span>");
119
120 addTokenSubstitute("mentioned", "<span class=\"mentioned\">");
121 addTokenSubstitute("/mentioned", "</span>");
122
123// addTokenSubstitute("divineName", "<span class=\"name\"><span class=\"divine\">");
124// addTokenSubstitute("/divineName", "</span></span>");
125
126 /// \todo Move that down to the real tag handling, segs without the type morph would generate incorrect markup, as the end span is always inserted
127// addTokenSubstitute("seg type=\"morph\"", "<span class=\"morphSegmentation\">");
128// addTokenSubstitute("/seg", "</span>");
129
130 // OSIS tables
131 addTokenSubstitute("table", "<table>");
132 addTokenSubstitute("/table", "</table>");
133 addTokenSubstitute("row", "<tr>");
134 addTokenSubstitute("/row", "</tr>");
135 addTokenSubstitute("cell", "<td>");
136 addTokenSubstitute("/cell", "</td>");
137
138}
139
140bool Filters::OsisToHtml::handleToken(sword::SWBuf &buf, const char *token, sword::BasicFilterUserData *userData) {
141 using namespace std::literals;
142 // manually process if it wasn't a simple substitution
143
144 if (!substituteToken(buf, token)) {
145 UserData* myUserData = static_cast<UserData*>(userData);
146 sword::SWModule* myModule = const_cast<sword::SWModule*>(myUserData->module); //hack
147
148 sword::XMLTag const tag(token);
149 // qWarning("found %s", token);
150 auto const osisQToTickEntry =
151 userData->module->getConfigEntry("OSISqToTick");
152 bool const osisQToTick =
153 !osisQToTickEntry || osisQToTickEntry != "false"sv;
154
155 std::string_view const tagName(tag.getName());
156
157 if (tagName == "div"sv) {
158 if (tag.isEndTag()) {
159 buf.append("</div>");
160 } else {
161 sword::SWBuf type( tag.getAttribute("type") );
162 if (type == "introduction") {
163 if (!tag.isEmpty())
164 buf.append("<div class=\"introduction\">");
165 } else if (type == "chapter") {
166 if (!tag.isEmpty())
167 buf.append("<div class=\"chapter\" ></div>"); //don't open a div here, that would lead to a broken XML structure
168 } else if (type == "x-p") {
169 buf.append("<br/>");
170 } else if (type == "paragraph") {
171 if (tag.getAttribute("sID"))
172 buf.append("<p>");
173 else if (tag.getAttribute("eID"))
174 buf.append("</p>");
175 } else {
176 buf.append("<div>");
177 }
178 }
179 }
180 else if (tagName == "w"sv) {
181 if ((!tag.isEmpty()) && (!tag.isEndTag())) { //start tag
182 const char *attrib;
183 const char *val;
184
185 sword::XMLTag outTag("span");
186 sword::SWBuf attrValue;
187
188 if ((attrib = tag.getAttribute("xlit"))) {
189 val = strchr(attrib, ':');
190 val = (val) ? (val + 1) : attrib;
191 outTag.setAttribute("xlit", val);
192 }
193
194 if ((attrib = tag.getAttribute("gloss"))) {
195 val = strchr(attrib, ':');
196 val = (val) ? (val + 1) : attrib;
197 outTag.setAttribute("gloss", val);
198 }
199
200 if ((attrib = tag.getAttribute("lemma"))) {
201 char splitChar = '|';
202 const int countSplit1 = tag.getAttributePartCount("lemma", '|');
203 const int countSplit2 = tag.getAttributePartCount("lemma", ' '); /// \todo not allowed, remove soon
204 int count = 0;
205
206 if (countSplit1 > countSplit2) { //| split char
207 splitChar = '|'; /// \todo not allowed, remove soon
208 count = countSplit1;
209 }
210 else {
211 splitChar = ' ';
212 count = countSplit2;
213 }
214
215 int i = (count > 1) ? 0 : -1; // -1 for whole value cuz it's faster, but does the same thing as 0
216 attrValue = "";
217
218 do {
219 if (attrValue.length()) {
220 attrValue.append( '|' );
221 }
222
223 attrib = tag.getAttribute("lemma", i, splitChar);
224
225 if (i < 0) { // to handle our -1 condition
226 i = 0;
227 }
228
229 val = strchr(attrib, ':');
230 val = (val) ? (val + 1) : attrib;
231
232 attrValue.append(val);
233 }
234 while (++i < count);
235
236 if (attrValue.length()) {
237 outTag.setAttribute("lemma", attrValue.c_str());
238 }
239 }
240
241 if ((attrib = tag.getAttribute("morph"))) {
242 char splitChar = '|';
243 const int countSplit1 = tag.getAttributePartCount("morph", '|');
244 const int countSplit2 = tag.getAttributePartCount("morph", ' '); /// \todo not allowed, remove soon
245 int count = 0;
246
247 if (countSplit1 > countSplit2) { //| split char
248 splitChar = '|';
249 count = countSplit1;
250 }
251 else {
252 splitChar = ' ';
253 count = countSplit2;
254 }
255
256 int i = (count > 1) ? 0 : -1; // -1 for whole value cuz it's faster, but does the same thing as 0
257
258 attrValue = "";
259
260 do {
261 if (attrValue.length()) {
262 attrValue.append('|');
263 }
264
265 attrib = tag.getAttribute("morph", i, splitChar);
266
267 if (i < 0) {
268 i = 0; // to handle our -1 condition
269 }
270
271 val = strchr(attrib, ':');
272
273 if (val) { //the prefix gives the modulename
274 //check the prefix
275 if (!strncmp("robinson:", attrib, 9)) { //robinson
276 attrValue.append( "Robinson:" ); //work is not the same as Sword's module name
277 attrValue.append( val + 1 );
278 }
279 //strongs is handled by BibleTime
280 /*else if (!strncmp("strongs", attrib, val-atrrib)) {
281 attrValue.append( !strncmp(attrib, "x-", 2) ? attrib+2 : attrib );
282 }*/
283 else {
284 attrValue.append( !strncmp(attrib, "x-", 2) ? attrib + 2 : attrib );
285 }
286 }
287 else { //no prefix given
288 val = attrib;
289 const bool skipFirst = ((val[0] == 'T') && ((val[1] == 'H') || (val[1] == 'G')));
290 attrValue.append( skipFirst ? val + 1 : val );
291 }
292 }
293 while (++i < count);
294
295 if (attrValue.length()) {
296 outTag.setAttribute("morph", attrValue.c_str());
297 }
298 }
299
300 if ((attrib = tag.getAttribute("POS"))) {
301 val = strchr(attrib, ':');
302 val = (val) ? (val + 1) : attrib;
303 outTag.setAttribute("pos", val);
304 }
305
306 buf.append( outTag.toString() );
307 }
308 else if (tag.isEndTag()) { // end or empty <w> tag
309 buf.append("</span>");
310 }
311 }
312 else if (tagName == "note"sv) {
313 if (!tag.isEndTag()) { //start tag
314 const sword::SWBuf type( tag.getAttribute("type") );
315
316 if (type == "crossReference") { //note containing cross references
317 myUserData->inCrossrefNote = true;
318 myUserData->noteTypes.emplace_back(UserData::CrossReference);
319
320 /*
321 * Do not count crossrefs as footnotes if they are displayed in the text. This will cause problems
322 * with footnote numbering when crossrefs are turned on/off.
323 * When accessing footnotes, crossrefs must be turned off in the filter so that they are not in the entry
324 * attributes of Sword.
325 *
326 * //myUserData->swordFootnote++; // cross refs count as notes, too
327 */
328
329 buf.append("<span class=\"crossreference\">");
330 sword::SWBuf footnoteNumber = tag.getAttribute("swordFootnote");
331 sword::SWBuf footnoteBody = myUserData->entryAttributes["Footnote"][footnoteNumber]["body"];
332 buf += myModule->renderText(footnoteBody);
333 }
334
335 /* else if (type == "explanation") {
336 }
337 */
338 else if ((type == "strongsMarkup") || (type == "x-strongsMarkup")) {
339 /**
340 * leave strong's markup notes out, in the future we'll probably have
341 * different option filters to turn different note types on or off
342 */
343
344 myUserData->suspendTextPassThru = true;
345 myUserData->noteTypes.emplace_back(UserData::StrongsMarkup);
346 }
347
348 else {
349 // qWarning("found note in %s", myUserData->key->getShortText());
350 buf.append(" <span class=\"footnote\" note=\"");
351 buf.append(myModule->getName());
352 buf.append('/');
353 buf.append(myUserData->key->getShortText());
354 buf.append('/');
355 buf.append( QString::number(myUserData->swordFootnote++).toUtf8().constData() ); //inefficient
356
357 const sword::SWBuf n = tag.getAttribute("n");
358
359 buf.append("\">");
360 buf.append( (n.length() > 0) ? QString::fromUtf8(n.c_str()).toHtmlEscaped().toUtf8().constData() : "*" );
361 buf.append("</span> ");
362
363 myUserData->noteTypes.emplace_back(UserData::Footnote);
364 myUserData->suspendTextPassThru = true;
365 }
366 }
367 else if (/* tag.isEndTag() && */ !myUserData->noteTypes.empty()) {
368 if (myUserData->noteTypes.back() == UserData::CrossReference) {
369 buf.append("</span> ");
370// myUserData->suspendTextPassThru = false;
371 myUserData->inCrossrefNote = false;
372 }
373
374 myUserData->noteTypes.pop_back();
375 myUserData->suspendTextPassThru = false;
376 }
377 }
378 else if (tagName == "reference"sv) {
379 if (!tag.isEndTag() && !tag.isEmpty()) {
380 renderReference(tag.getAttribute("osisRef"),
381 buf,
382 *myModule,
383 *myUserData);
384 }
385 else if (tag.isEndTag()) {
386 buf.append("</a>");
387 }
388 else { // empty reference marker
389 // -- what should we do? nothing for now.
390 }
391 }
392 else if (tagName == "title"sv) {
393 if (!tag.isEndTag() && !tag.isEmpty()) {
394 buf.append("<div class=\"sectiontitle\">");
395 }
396 else if (tag.isEndTag()) {
397 buf.append("</div>");
398 }
399 else { // empty title marker
400 // what to do? is this even valid?
401 buf.append("<br/>");
402 }
403 }
404 else if (tagName == "hi"sv) { // <hi> highlighted text
405 const sword::SWBuf type = tag.getAttribute("type");
406
407 if ((!tag.isEndTag()) && (!tag.isEmpty())) {
408 if (type == "bold") {
409 buf.append("<span class=\"bold\">");
410 }
411 else if (type == "illuminated") {
412 buf.append("<span class=\"illuminated\">");
413 }
414 else if (type == "italic") {
415 buf.append("<span class=\"italic\">");
416 }
417 else if (type == "line-through") {
418 buf.append("<span class=\"line-through\">");
419 }
420 else if (type == "normal") {
421 buf.append("<span class=\"normal\">");
422 }
423 else if (type == "small-caps") {
424 buf.append("<span class=\"small-caps\">");
425 }
426 else if (type == "underline") {
427 buf.append("<span class=\"underline\">");
428 }
429 else {
430 buf.append("<span>"); //don't break markup, </span> is inserted later
431 }
432 }
433 else if (tag.isEndTag()) { //all hi replacements are html spans
434 buf.append("</span>");
435 }
436 }
437 else if (tagName == "name"sv) {
438 const sword::SWBuf type = tag.getAttribute("type");
439
440 if ((!tag.isEndTag()) && (!tag.isEmpty())) {
441 if (type == "geographic") {
442 buf.append("<span class=\"name\"><span class=\"geographic\">");
443 }
444 else if (type == "holiday") {
445 buf.append("<span class=\"name\"><span class=\"holiday\">");
446 }
447 else if (type == "nonhuman") {
448 buf.append("<span class=\"name\"><span class=\"nonhuman\">");
449 }
450 else if (type == "person") {
451 buf.append("<span class=\"name\"><span class=\"person\">");
452 }
453 else if (type == "ritual") {
454 buf.append("<span class=\"name\"><span class=\"ritual\">");
455 }
456 else {
457 buf.append("<span class=\"name\"><span>");
458 }
459 }
460 else if (tag.isEndTag()) { //all hi replacements are html spans
461 buf.append("</span></span> ");
462 }
463 }
464 else if (tagName == "transChange"sv) {
465 sword::SWBuf type( tag.getAttribute("type") );
466
467 if ( !type.length() ) {
468 type = tag.getAttribute("changeType");
469 }
470
471 if ((!tag.isEndTag()) && (!tag.isEmpty())) {
472 if (type == "added") {
473 buf.append("<span class=\"transchange\" title=\"");
474 buf.append(QObject::tr("Added text").toUtf8().constData());
475 buf.append("\"><span class=\"added\">");
476 }
477 else if (type == "amplified") {
478 buf.append("<span class=\"transchange\"><span class=\"amplified\">");
479 }
480 else if (type == "changed") {
481 buf.append("<span class=\"transchange\"><span class=\"changed\">");
482 }
483 else if (type == "deleted") {
484 buf.append("<span class=\"transchange\"><span class=\"deleted\">");
485 }
486 else if (type == "moved") {
487 buf.append("<span class=\"transchange\"><span class=\"moved\">");
488 }
489 else if (type == "tenseChange") {
490 buf.append("<span class=\"transchange\" title=\"");
491 buf.append(QObject::tr("Verb tense changed").toUtf8().constData());
492 buf.append("\"><span class=\"tenseChange\">");
493 }
494 else {
495 buf.append("<span class=\"transchange\"><span>");
496 }
497 }
498 else if (tag.isEndTag()) { //all hi replacements are html spans
499 buf.append("</span></span>");
500 }
501 }
502 else if (tagName == "p"sv) {
503 if (tag.isEndTag())
504 buf.append("</p>");
505 else
506 buf.append("<p>");
507
508 }
509 else if (tagName == "q"sv) { // <q> quote
510 //sword::SWBuf type = tag.getAttribute("type");
511 sword::SWBuf who = tag.getAttribute("who");
512 const char *lev = tag.getAttribute("level");
513 int level = (lev) ? atoi(lev) : 1;
514 sword::SWBuf quoteMarker = tag.getAttribute("marker");
515
516 if ((!tag.isEndTag())) {
517 if (!tag.isEmpty()) {
518 myUserData->quote.who = who;
519 }
520
521 if (quoteMarker.size() > 0) {
522 buf.append(quoteMarker);
523 }
524 else if (osisQToTick) //alternate " and '
525 buf.append((level % 2) ? '\"' : '\'');
526
527 if (who == "Jesus") {
528 buf.append("<span class=\"jesuswords\">");
529 }
530 }
531 else if (tag.isEndTag()) {
532 if (myUserData->quote.who == "Jesus") {
533 buf.append("</span>");
534 }
535 if (quoteMarker.size() > 0) {
536 buf.append(quoteMarker);
537 }
538 else if (osisQToTick) { //alternate " and '
539 buf.append((level % 2) ? '\"' : '\'');
540 }
541
542 myUserData->quote.who = "";
543 }
544 }
545 else if (tagName == "abbr"sv) {
546 if (!tag.isEndTag() && !tag.isEmpty()) {
547 const sword::SWBuf expansion = tag.getAttribute("expansion");
548
549 buf.append("<span class=\"abbreviation\" expansion=\"");
550 buf.append(expansion);
551 buf.append("\">");
552 }
553 else if (tag.isEndTag()) {
554 buf.append("</span>");
555 }
556 }
557 else if (tagName == "milestone"sv) {
558 const sword::SWBuf type = tag.getAttribute("type");
559
560 if ((type == "screen") || (type == "line")) {//line break
561 buf.append("<br/>");
562 userData->supressAdjacentWhitespace = true;
563 }
564 else if (type == "x-p") { //e.g. occurs in the KJV2006 module
565 //buf.append("<br/>");
566 const sword::SWBuf marker = tag.getAttribute("marker");
567 if (marker.length() > 0) {
568 buf.append(marker);
569 }
570 }
571 }
572 else if (tagName == "seg"sv) {
573 if (!tag.isEndTag() && !tag.isEmpty()) {
574
575 const sword::SWBuf type = tag.getAttribute("type");
576
577 if (type == "morph") {//line break
578 //This code is for WLC and MORPH (WHI)
579 sword::XMLTag outTag("span");
580 outTag.setAttribute("class", "morphSegmentation");
581 const char* attrValue;
582 //Transfer the values to the span
583 //Problem: the data is in hebrew/aramaic, how to encode in HTML/BibleTime?
584 if ((attrValue = tag.getAttribute("lemma"))) outTag.setAttribute("lemma", attrValue);
585 if ((attrValue = tag.getAttribute("morph"))) outTag.setAttribute("morph", attrValue);
586 if ((attrValue = tag.getAttribute("homonym"))) outTag.setAttribute("homonym", attrValue);
587
588 buf.append(outTag.toString());
589 //buf.append("<span class=\"morphSegmentation\">");
590 }
591 else {
592 buf.append("<span>");
593 }
594 }
595 else { // seg end tag
596 buf.append("</span>");
597 }
598 //qWarning(QString("handled <seg> token. result: %1").arg(buf.c_str()).latin1());
599 }
600 //divine name, don't use simple tag replacing because it may have attributes
601 else if (tagName == "divineName"sv) {
602 if (!tag.isEndTag()) {
603 buf.append("<span class=\"name\"><span class=\"divine\">");
604 }
605 else { //all hi replacements are html spans
606 buf.append("</span></span>");
607 }
608 }
609 else { //all tokens handled by OSISHTMLHref will run through the filter now
610 return sword::OSISHTMLHREF::handleToken(buf, token, userData);
611 }
612 }
613
614 return false;
615}
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
CSwordModuleInfo * findSwordModuleByPointer(const sword::SWModule *const swmodule) const
Searches for a module with the given sword module as module().
unsigned short int swordFootnote
Definition osistohtml.h:56
sword::AttributeTypeList entryAttributes
Definition osistohtml.h:58
std::vector< NoteType > noteTypes
Definition osistohtml.h:66
struct Filters::OsisToHtml::UserData::@0 quote
bool handleToken(sword::SWBuf &buf, const char *token, sword::BasicFilterUserData *userData) override
void renderReference(char const *const osisRef, sword::SWBuf &buf, sword::SWModule const &myModule, UserData const &myUserData)