1 /******************************************************************************
2  *
3  *  osislatex.cpp -	Render filter for LaTeX of an OSIS module
4  *
5  * $Id: osislatex.cpp 3548 2017-12-10 05:11:38Z scribe $
6  *
7  * Copyright 2011-2014 CrossWire Bible Society (http://www.crosswire.org)
8  *	CrossWire Bible Society
9  *	P. O. Box 2528
10  *	Tempe, AZ  85280-2528
11  *
12  * This program is free software; you can redistribute it and/or modify it
13  * under the terms of the GNU General Public License as published by the
14  * Free Software Foundation version 2.
15  *
16  * This program is distributed in the hope that it will be useful, but
17  * WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19  * General Public License for more details.
20  *
21  */
22 
23 #include <stdlib.h>
24 #include <ctype.h>
25 #include <osislatex.h>
26 #include <utilxml.h>
27 #include <utilstr.h>
28 #include <versekey.h>
29 #include <swmodule.h>
30 #include <url.h>
31 #include <stringmgr.h>
32 #include <stack>
33 
34 SWORD_NAMESPACE_START
35 
getHeader() const36 const char *OSISLaTeX::getHeader() const {
37 // can be used to return static start-up info, like packages to load. Not sure yet if I want to retain it.
38 
39 	const static char *header = "\
40 		\\LoadClass[11pt,a4paper,twoside,headinclude=true,footinclude=true,BCOR=0mm,DIV=calc]{scrbook}\n\
41 		\\LoadClass[11pt,a4paper,twoside,headinclude=true,footinclude=true,BCOR=0mm,DIV=calc]{scrbook}\n\
42 		\\NeedsTeXFormat{LaTeX2e}\n\
43 		\\ProvidesClass{sword}[2015/03/29 CrossWire LaTeX class for Biblical texts]\n\
44 		%\\sworddiclink{%s}{%s}{\n\
45 		%\\sworddictref{%s}{%s}{\n\
46 		%\\sworddict{%s}{\n\
47 		%\\sworddivinename}{%s}{\n\
48 		%\\swordfont{\n\
49 		%\\swordfootnote[%c]{%s}{%s}{%s}{%s}{\n\
50 		%\\swordfootnote{%s}{%s}{%s}{\n\
51 		%\\swordfootnote{%s}{%s}{%s}{%s}{\n\
52 		%\\swordmorph{\n\
53 		%\\swordmorph[Greek]{%s}\n\
54 		%\\swordmorph[lemma]{%s}\n\
55 		%\\swordmorph{%s}\n\
56 		%\\swordpoetryline{\n\
57 		%\\swordquote{\n\
58 		%\\swordref{%s}{%s}{\n\
59 		%\\swordsection{\n\
60 		%\\swordsection{}{\n\
61 		%\\swordsection{book}{\n\
62 		%\\swordsection{sechead}{\n\
63 		%\\swordstrong[Greek]{\n\
64 		%\\swordstrong[Greektense]{\n\
65 		%\\swordstrong[Hebrew]{\n\
66 		%\\swordstrong[Hebrewtense]{\n\
67 		%\\swordstrong[%s]{%s}{\n\
68 		%\\swordstrong{%s}{%s}\n\
69 		%\\swordtitle{\n\
70 		%\\swordtranschange{supplied}{\n\
71 		%\\swordtranschange{tense}{\n\
72 		%\\swordwoj{\n\
73 		%\\swordxref{\n\
74 		%\\swordxref{%s}{\n\
75 		%\\swordxref{%s}{%s}{\n\
76 	";
77 	return header;
78 }
79 
80 
81 namespace {
82 
83 // though this might be slightly slower, possibly causing an extra bool check, this is a renderFilter
84 // so speed isn't the absolute highest priority, and this is a very minor possible hit
outText(const char * t,SWBuf & o,BasicFilterUserData * u)85 static inline void outText(const char *t, SWBuf &o, BasicFilterUserData *u) { if (!u->suspendTextPassThru) o += t; else u->lastSuspendSegment += t; }
outText(char t,SWBuf & o,BasicFilterUserData * u)86 static inline void outText(char t, SWBuf &o, BasicFilterUserData *u) { if (!u->suspendTextPassThru) o += t; else u->lastSuspendSegment += t; }
87 
processLemma(bool suspendTextPassThru,XMLTag & tag,SWBuf & buf)88 void processLemma(bool suspendTextPassThru, XMLTag &tag, SWBuf &buf) {
89 	const char *attrib;
90 	const char *val;
91 	if ((attrib = tag.getAttribute("lemma"))) {
92 		int count = tag.getAttributePartCount("lemma", ' ');
93 		int i = (count > 1) ? 0 : -1;		// -1 for whole value cuz it's faster, but does the same thing as 0
94 		do {
95 			attrib = tag.getAttribute("lemma", i, ' ');
96 			if (i < 0) i = 0;	// to handle our -1 condition
97 			val = strchr(attrib, ':');
98 			val = (val) ? (val + 1) : attrib;
99 			SWBuf gh;
100 			if(*val == 'G')
101 				gh = "Greek";
102 			if(*val == 'H')
103 				gh = "Hebrew";
104 			const char *val2 = val;
105 			if ((strchr("GH", *val)) && (isdigit(val[1])))
106 				val2++;
107 			//if ((!strcmp(val2, "3588")) && (lastText.length() < 1))
108 			//	show = false;
109 			//else {
110 				if (!suspendTextPassThru) {
111 					buf.appendFormatted("\\swordstrong{%s}{%s}",
112 							(gh.length()) ? gh.c_str() : "",
113 							val2);
114 				}
115 			//}
116 
117 		} while (++i < count);
118 	}
119 }
120 
121 
122 
processMorph(bool suspendTextPassThru,XMLTag & tag,SWBuf & buf)123 void processMorph(bool suspendTextPassThru, XMLTag &tag, SWBuf &buf) {
124 	const char * attrib;
125 	const char *val;
126 	if ((attrib = tag.getAttribute("morph"))) { // && (show)) {
127 		SWBuf savelemma = tag.getAttribute("savlm");
128 		//if ((strstr(savelemma.c_str(), "3588")) && (lastText.length() < 1))
129 		//	show = false;
130 		//if (show) {
131 			int count = tag.getAttributePartCount("morph", ' ');
132 			int i = (count > 1) ? 0 : -1;		// -1 for whole value cuz it's faster, but does the same thing as 0
133 			do {
134 				attrib = tag.getAttribute("morph", i, ' ');
135 				if (i < 0) i = 0;	// to handle our -1 condition
136 				val = strchr(attrib, ':');
137 				val = (val) ? (val + 1) : attrib;
138 				const char *val2 = val;
139 				if ((*val == 'T') && (strchr("GH", val[1])) && (isdigit(val[2])))
140 					val2+=2;
141 				if (!suspendTextPassThru) {
142 					buf.appendFormatted("\\swordmorph{%s}",
143 							tag.getAttribute("morph")
144 							);
145 				}
146 			} while (++i < count);
147 		//}
148 	}
149 }
150 
151 
152 }	// end anonymous namespace
153 
createUserData(const SWModule * module,const SWKey * key)154 BasicFilterUserData *OSISLaTeX::createUserData(const SWModule *module, const SWKey *key) {
155 	return new MyUserData(module, key);
156 }
157 
158 
OSISLaTeX()159 OSISLaTeX::OSISLaTeX() {
160 	setTokenStart("<");
161 	setTokenEnd(">");
162 
163 	setEscapeStart("&");
164 	setEscapeEnd(";");
165 
166 	setEscapeStringCaseSensitive(true);
167 	setPassThruNumericEscapeString(true);
168 
169 	addAllowedEscapeString("quot");
170 	addAllowedEscapeString("apos");
171 	addAllowedEscapeString("amp");
172 	addAllowedEscapeString("lt");
173 	addAllowedEscapeString("gt");
174 
175 	setTokenCaseSensitive(true);
176 
177 	//	addTokenSubstitute("lg",  "<br />");
178 	//	addTokenSubstitute("/lg", "<br />");
179 
180 	morphFirst = false;
181 	renderNoteNumbers = false;
182 }
183 
184 class OSISLaTeX::TagStack : public std::stack<SWBuf> {
185 };
186 
MyUserData(const SWModule * module,const SWKey * key)187 OSISLaTeX::MyUserData::MyUserData(const SWModule *module, const SWKey *key) : BasicFilterUserData(module, key), quoteStack(new TagStack()), hiStack(new TagStack()), titleStack(new TagStack()), lineStack(new TagStack()) {
188 	inXRefNote    = false;
189 	suspendLevel = 0;
190 	divLevel = "module";
191 	wordsOfChristStart = "\\swordwoj{";
192 	wordsOfChristEnd   = "}";
193 	consecutiveNewlines = 0;
194 	firstCell = false;
195 }
196 
~MyUserData()197 OSISLaTeX::MyUserData::~MyUserData() {
198 	delete quoteStack;
199 	delete hiStack;
200 	delete titleStack;
201 	delete lineStack;
202 }
203 
outputNewline(SWBuf & buf)204 void OSISLaTeX::MyUserData::outputNewline(SWBuf &buf) {
205 	if (++consecutiveNewlines <= 2) {
206 		outText("//\n", buf, this);
207 		supressAdjacentWhitespace = true;
208 	}
209 }
handleToken(SWBuf & buf,const char * token,BasicFilterUserData * userData)210 bool OSISLaTeX::handleToken(SWBuf &buf, const char *token, BasicFilterUserData *userData) {
211 	MyUserData *u = (MyUserData *)userData;
212 	SWBuf scratch;
213 
214 	bool sub = (u->suspendTextPassThru) ? substituteToken(scratch, token) : substituteToken(buf, token);
215 	if (!sub) {
216   // manually process if it wasn't a simple substitution
217 		XMLTag tag(token);
218 
219 		// <w> tag
220 		if (!strcmp(tag.getName(), "w")) {
221 
222 			// start <w> tag
223 			if ((!tag.isEmpty()) && (!tag.isEndTag())) {
224 				u->w = token;
225 			}
226 
227 			// end or empty <w> tag
228 			else {
229 				bool endTag = tag.isEndTag();
230 				SWBuf lastText;
231 				//bool show = true;	// to handle unplaced article in kjv2003-- temporary till combined
232 
233 				if (endTag) {
234 					tag = u->w.c_str();
235 					lastText = u->lastTextNode.c_str();
236 				}
237 				else lastText = "stuff";
238 
239 				const char *attrib;
240 				const char *val;
241 				if ((attrib = tag.getAttribute("xlit"))) {
242 					val = strchr(attrib, ':');
243 					val = (val) ? (val + 1) : attrib;
244 					outText(" ", buf, u);
245 					outText(val, buf, u);
246 				}
247 				if ((attrib = tag.getAttribute("gloss"))) {
248 					// I'm sure this is not the cleanest way to do it, but it gets the job done
249 					// for rendering ruby chars properly ^_^
250 					buf -= lastText.length();
251 
252 					outText("\\ruby{", buf, u);
253 					outText(lastText, buf, u);
254 					outText("}{", buf, u);
255 					outText(attrib, buf, u);
256 					outText("}", buf, u);
257 				}
258 				if (!morphFirst) {
259 					processLemma(u->suspendTextPassThru, tag, buf);
260 					processMorph(u->suspendTextPassThru, tag, buf);
261 				}
262 				else {
263 					processMorph(u->suspendTextPassThru, tag, buf);
264 					processLemma(u->suspendTextPassThru, tag, buf);
265 				}
266 				if ((attrib = tag.getAttribute("POS"))) {
267 					val = strchr(attrib, ':');
268 					val = (val) ? (val + 1) : attrib;
269 					outText(" ", buf, u);
270 					outText(val, buf, u);
271 				}
272 
273 
274 			}
275 		}
276 
277 		// <note> tag
278 
279 		else if (!strcmp(tag.getName(), "note")) {
280 			if (!tag.isEndTag()) {
281 				SWBuf type = tag.getAttribute("type");
282 				bool strongsMarkup = (type == "x-strongsMarkup" || type == "strongsMarkup");	// the latter is deprecated
283 				if (strongsMarkup) {
284 					tag.setEmpty(false);	// handle bug in KJV2003 module where some note open tags were <note ... />
285 				}
286 
287 				if (!tag.isEmpty()) {
288 
289 					if (!strongsMarkup) {	// leave strong's markup notes out, in the future we'll probably have different option filters to turn different note types on or off
290 						SWBuf footnoteNumber = tag.getAttribute("swordFootnote");
291 						SWBuf footnoteBody = "";
292 						if (u->module){
293 							footnoteBody += u->module->getEntryAttributes()["Footnote"][footnoteNumber]["body"];
294 						}
295 						SWBuf noteName = tag.getAttribute("n");
296 
297 						u->inXRefNote = true; // Why this change? Ben Morgan: Any note can have references in, so we need to set this to true for all notes
298 //						u->inXRefNote = (ch == 'x');
299 
300 						if (u->vkey) {
301 							//printf("URL = %s\n",URL::encode(u->vkey->getText()).c_str());
302 							buf.appendFormatted("\\swordfootnote{%s}{%s}{%s}{%s}{%s}{",
303 
304 								footnoteNumber.c_str(),
305 								u->version.c_str(),
306 								u->vkey->getText(),
307 								tag.getAttribute("type"),
308 								(renderNoteNumbers ? noteName.c_str() : ""));
309 							if (u->module) {
310 								outText( u->module->renderText(footnoteBody).c_str(), buf, u);
311 							}
312 						}
313 						else {
314 							buf.appendFormatted("\\swordfootnote{%s}{%s}{%s}{%s}{%s}{",
315 								footnoteNumber.c_str(),
316 								u->version.c_str(),
317 								u->key->getText(),
318 								tag.getAttribute("type"),
319 								(renderNoteNumbers ? noteName.c_str() : ""));
320 							if (u->module) {
321 								outText( u->module->renderText(footnoteBody).c_str(), buf, u);
322 							}
323 						}
324 					}
325 				}
326 				u->suspendTextPassThru = (++u->suspendLevel);
327 			}
328 			if (tag.isEndTag()) {
329 				u->suspendTextPassThru = (--u->suspendLevel);
330 				u->inXRefNote = false;
331 				u->lastSuspendSegment = ""; // fix/work-around for nasb divineName in note bug
332 				outText("}", buf, u);
333 			}
334 		}
335 
336 		// <p> paragraph and <lg> linegroup tags
337 		else if (!strcmp(tag.getName(), "p") || !strcmp(tag.getName(), "lg")) {
338 			if ((!tag.isEndTag()) && (!tag.isEmpty())) {	// non-empty start tag
339 				u->outputNewline(buf);
340 			}
341 			else if (tag.isEndTag()) {	// end tag
342 				u->outputNewline(buf);
343 			}
344 			else {					// empty paragraph break marker
345 				u->outputNewline(buf);
346 			}
347 		}
348 
349 		// Milestoned paragraphs, created by osis2mod
350 		// <div type="paragraph" sID.../>
351 		// <div type="paragraph" eID.../>
352 		else if (tag.isEmpty() && !strcmp(tag.getName(), "div") && tag.getAttribute("type") && (!strcmp(tag.getAttribute("type"), "x-p") || !strcmp(tag.getAttribute("type"), "paragraph"))) {
353 			// <div type="paragraph"  sID... />
354 			if (tag.getAttribute("sID")) {	// non-empty start tag
355 				u->outputNewline(buf);
356 			}
357 			// <div type="paragraph"  eID... />
358 			else if (tag.getAttribute("eID")) {
359 				u->outputNewline(buf);
360 			}
361 		}
362 
363 		// <reference> tag
364 		else if (!strcmp(tag.getName(), "reference")) {
365 			if (!u->inXRefNote) {	// only show these if we're not in an xref note
366 				if (!tag.isEndTag()) {
367 					SWBuf target;
368 					SWBuf work;
369 					SWBuf ref;
370 					bool is_scripRef = false;
371 
372 					target = tag.getAttribute("osisRef");
373 					const char* the_ref = strchr(target, ':');
374 
375 					if(!the_ref) {
376 						// No work
377 						ref = target;
378 						is_scripRef = true;
379 					}
380 					else {
381 						// Compensate for starting :
382 						ref = the_ref + 1;
383 
384 						int size = (int)(target.size() - ref.size() - 1);
385 						work.setSize(size);
386 						strncpy(work.getRawData(), target, size);
387 
388 						// For Bible:Gen.3.15 or Bible.vulgate:Gen.3.15
389 						if(!strncmp(work, "Bible", 5))
390 							is_scripRef = true;
391 					}
392 
393 					if(is_scripRef)
394 					{
395 						buf.appendFormatted("\\swordxref{%s}{",
396 							ref.c_str()
397 //							(work.size()) ? URL::encode(work.c_str()).c_str() : "")
398 							);
399 					}
400 					else
401 					{
402 						// Dictionary link, or something
403 						buf.appendFormatted("\\sworddiclink{%s}{%s}{", // work, entry
404 							work.c_str(),
405 							ref.c_str()
406 							);
407 					}
408 				}
409 				else {
410 					outText("}", buf, u);
411 				}
412 			}
413 		}
414 
415 		// <l> poetry, etc
416 		else if (!strcmp(tag.getName(), "l")) {
417 			// start line marker
418 			if (tag.getAttribute("sID") || (!tag.isEndTag() && !tag.isEmpty())) {
419 				// nested lines plus if the line itself has an x-indent type attribute value
420 				outText("\\swordpoetryline{", buf, u);
421 				u->lineStack->push(tag.toString());
422 			}
423 			// end line marker
424 			else if (tag.getAttribute("eID") || tag.isEndTag()) {
425 				outText("}", buf, u);
426 				u->outputNewline(buf);
427 				if (u->lineStack->size()) u->lineStack->pop();
428 			}
429 			// <l/> without eID or sID
430 			// Note: this is improper osis. This should be <lb/>
431 			else if (tag.isEmpty() && !tag.getAttribute("sID")) {
432 				u->outputNewline(buf);
433 			}
434 		}
435 
436 		// <lb.../>
437 		else if (!strcmp(tag.getName(), "lb") && (!tag.getAttribute("type") || strcmp(tag.getAttribute("type"), "x-optional"))) {
438 				u->outputNewline(buf);
439 		}
440 		// <milestone type="line"/>
441 		// <milestone type="x-p"/>
442 		// <milestone type="cQuote" marker="x"/>
443 		else if ((!strcmp(tag.getName(), "milestone")) && (tag.getAttribute("type"))) {
444 			if (!strcmp(tag.getAttribute("type"), "line")) {
445 				u->outputNewline(buf);
446 				if (tag.getAttribute("subType") && !strcmp(tag.getAttribute("subType"), "x-PM")) {
447 					u->outputNewline(buf);
448 				}
449 			}
450 			else if (!strcmp(tag.getAttribute("type"),"x-p"))  {
451 				if (tag.getAttribute("marker"))
452 					outText(tag.getAttribute("marker"), buf, u);
453 				else outText("<!p>", buf, u);
454 			}
455 			else if (!strcmp(tag.getAttribute("type"), "cQuote")) {
456 				const char *tmp = tag.getAttribute("marker");
457 				bool hasMark    = tmp;
458 				SWBuf mark      = tmp;
459 				tmp             = tag.getAttribute("level");
460 				int level       = (tmp) ? atoi(tmp) : 1;
461 
462 				// first check to see if we've been given an explicit mark
463 				if (hasMark)
464 					outText(mark, buf, u);
465 				// finally, alternate " and ', if config says we should supply a mark
466 				else if (u->osisQToTick)
467 					outText((level % 2) ? '\"' : '\'', buf, u);
468 			}
469 		}
470 
471 		// <title>
472 
473 		else if (!strcmp(tag.getName(), "title")) {
474 			if ((!tag.isEndTag()) && (!tag.isEmpty())) {
475 				const char *tmp = tag.getAttribute("type");
476 				bool hasType    = tmp;
477 				SWBuf type      = tmp;
478 
479 				outText("\n\\swordtitle{", buf, u);
480 				outText(u->divLevel, buf, u);
481 				outText("}{", buf, u);
482 
483 				if (hasType) outText(type, buf, u);
484 				else outText("", buf, u);
485 
486 				outText("}{", buf, u);
487 			}
488 			else if (tag.isEndTag()) {
489 				outText( "}", buf, u);
490 				++u->consecutiveNewlines;
491 				u->supressAdjacentWhitespace = true;
492 			}
493 		}
494 
495 		// <list>
496 		else if (!strcmp(tag.getName(), "list")) {
497 			if((!tag.isEndTag()) && (!tag.isEmpty())) {
498 				outText("\n\\begin{itemize}", buf, u);
499 			}
500 			else if (tag.isEndTag()) {
501 				outText("\n\\end{itemize}", buf, u);
502 				++u->consecutiveNewlines;
503 				u->supressAdjacentWhitespace = true;
504 			}
505 		}
506 
507 		// <item>
508 		else if (!strcmp(tag.getName(), "item")) {
509 			if((!tag.isEndTag()) && (!tag.isEmpty())) {
510 				outText("\n\\item ", buf, u);
511 			}
512 			else if (tag.isEndTag()) {
513 				++u->consecutiveNewlines;
514 				u->supressAdjacentWhitespace = true;
515 			}
516 		}
517 		// <catchWord> & <rdg> tags (italicize)
518 		else if (!strcmp(tag.getName(), "rdg") || !strcmp(tag.getName(), "catchWord")) {
519 			if ((!tag.isEndTag()) && (!tag.isEmpty())) {
520 				outText("\\emph{", buf, u);
521 			}
522 			else if (tag.isEndTag()) {
523 				outText("}", buf, u);
524 			}
525 		}
526 
527 		// divineName
528 		else if (!strcmp(tag.getName(), "divineName")) {
529 			if ((!tag.isEndTag()) && (!tag.isEmpty())) {
530 				outText( "\\sworddivinename{", buf, u);
531 				u->suspendTextPassThru = (++u->suspendLevel);
532 			}
533 			else if (tag.isEndTag()) {
534 				SWBuf lastText = u->lastSuspendSegment.c_str();
535 				u->suspendTextPassThru = (--u->suspendLevel);
536 				if (lastText.size()) {
537 					scratch.setFormatted("%s}", lastText.c_str());
538 					outText(scratch.c_str(), buf, u);
539 				}
540 			}
541 		}
542 
543 		// <hi> text highlighting
544 		else if (!strcmp(tag.getName(), "hi")) {
545 			SWBuf type = tag.getAttribute("type");
546 
547 			// handle tei rend attribute if type doesn't exist
548 			if (!type.length()) type = tag.getAttribute("rend");
549 
550 			if ((!tag.isEndTag()) && (!tag.isEmpty())) {
551 				if (type == "bold" || type == "b" || type == "x-b") {
552 					outText("\\textbold{", buf, u);
553 				}
554 
555 				// there is no officially supported OSIS overline attribute,
556 				// thus either TEI overline or OSIS x-overline would be best,
557 				// but we have used "ol" in the past, as well.  Once a valid
558 				// OSIS overline attribute is made available, these should all
559 				// eventually be deprecated and never documented that they are supported.
560 				else if (type == "ol" || type == "overline" || type == "x-overline") {
561 					outText("\\textoverline{", buf, u);
562 				}
563 
564 				else if (type == "super") {
565 					outText("\\textsuperscript{", buf, u);
566 				}
567 				else if (type == "sub") {
568 					outText("\\textsubscript{", buf, u);
569 				}
570 				else {	// all other types
571 					outText("\\emph {", buf, u);
572 				}
573 				u->hiStack->push(tag.toString());
574 			}
575 			else if (tag.isEndTag()) {
576 				outText("}", buf, u);
577 			}
578 		}
579 
580 		// <q> quote
581 		// Rules for a quote element:
582 		// If the tag is empty with an sID or an eID then use whatever it specifies for quoting.
583 		//    Note: empty elements without sID or eID are ignored.
584 		// If the tag is <q> then use it's specifications and push it onto a stack for </q>
585 		// If the tag is </q> then use the pushed <q> for specification
586 		// If there is a marker attribute, possibly empty, this overrides osisQToTick.
587 		// If osisQToTick, then output the marker, using level to determine the type of mark.
588 		else if (!strcmp(tag.getName(), "q")) {
589 			SWBuf type      = tag.getAttribute("type");
590 			SWBuf who       = tag.getAttribute("who");
591 			const char *tmp = tag.getAttribute("level");
592 			int level       = (tmp) ? atoi(tmp) : 1;
593 			tmp             = tag.getAttribute("marker");
594 			bool hasMark    = tmp;
595 			SWBuf mark      = tmp;
596 
597 			// open <q> or <q sID... />
598 			if ((!tag.isEmpty() && !tag.isEndTag()) || (tag.isEmpty() && tag.getAttribute("sID"))) {
599 				// if <q> then remember it for the </q>
600 				if (!tag.isEmpty()) {
601 					u->quoteStack->push(tag.toString());
602 				}
603 
604 				// Do this first so quote marks are included as WoC
605 				if (who == "Jesus")
606 					outText(u->wordsOfChristStart, buf, u);
607 
608 				// first check to see if we've been given an explicit mark
609 				if (hasMark)
610 					outText(mark, buf, u);
611 				//alternate " and '
612 				else if (u->osisQToTick)
613 					outText((level % 2) ? '\"' : '\'', buf, u);
614 			}
615 			// close </q> or <q eID... />
616 			else if ((tag.isEndTag()) || (tag.isEmpty() && tag.getAttribute("eID"))) {
617 				// if it is </q> then pop the stack for the attributes
618 				if (tag.isEndTag() && !u->quoteStack->empty()) {
619 					XMLTag qTag(u->quoteStack->top());
620 					if (u->quoteStack->size()) u->quoteStack->pop();
621 
622 					type    = qTag.getAttribute("type");
623 					who     = qTag.getAttribute("who");
624 					tmp     = qTag.getAttribute("level");
625 					level   = (tmp) ? atoi(tmp) : 1;
626 					tmp     = qTag.getAttribute("marker");
627 					hasMark = tmp;
628 					mark    = tmp;
629 				}
630 
631 				// first check to see if we've been given an explicit mark
632 				if (hasMark)
633 					outText(mark, buf, u);
634 				// finally, alternate " and ', if config says we should supply a mark
635 				else if (u->osisQToTick)
636 					outText((level % 2) ? '\"' : '\'', buf, u);
637 
638 				// Do this last so quote marks are included as WoC
639 				if (who == "Jesus")
640 					outText(u->wordsOfChristEnd, buf, u);
641 			}
642 		}
643 
644 		// <transChange>
645 		else if (!strcmp(tag.getName(), "transChange")) {
646 			if ((!tag.isEndTag()) && (!tag.isEmpty())) {
647 				SWBuf type = tag.getAttribute("type");
648 				u->lastTransChange = type;
649 
650 				// just do all transChange tags this way for now
651 				if ((type == "added") || (type == "supplied"))
652 					outText("\\swordtranschange{supplied}{", buf, u);
653 				else if (type == "tenseChange")
654 					outText( "\\swordtranschange{tense}{", buf, u);
655 			}
656 			else if (tag.isEndTag()) {
657 				outText("}", buf, u);
658 			}
659 			else {	// empty transChange marker?
660 			}
661 		}
662 
663 		// image
664 		else if (!strcmp(tag.getName(), "figure")) {
665 			const char *src = tag.getAttribute("src");
666 			if (src) {		// assert we have a src attribute
667 				SWBuf filepath;
668 				if (userData->module) {
669 					filepath = userData->module->getConfigEntry("AbsoluteDataPath");
670 					if ((filepath.size()) && (filepath[filepath.size()-1] != '/') && (src[0] != '/'))
671 						filepath += '/';
672 				}
673 				filepath += src;
674 
675 				outText("\\figure{", buf, u);
676 				outText("\\includegraphics{", buf, u);
677 				outText(filepath.c_str(), buf, u);
678 				outText("}}", buf, u);
679 
680 			}
681 		}
682 
683 		// ok to leave these in
684 		else if (!strcmp(tag.getName(), "div")) {
685 			SWBuf type = tag.getAttribute("type");
686 			if (type == "module") {
687 				u->divLevel = type;
688 				outText("\n", buf, u);
689 			}
690 			else if (type == "testament") {
691 				u->divLevel = type;
692 				outText("\n", buf, u);
693 			}
694 			else if (type == "bookGroup") {
695 				u->divLevel = type;
696 				outText("\n", buf, u);
697 			}
698 			else if (type == "book") {
699 				u->divLevel = type;
700 				outText("\n", buf, u);
701 			}
702 			else if (type == "majorSection") {
703 				u->divLevel = type;
704 				outText("\n", buf, u);
705 			}
706 			else if (type == "section") {
707 				u->divLevel = type;
708 				outText("\n", buf, u);
709 			}
710 			else if (type == "paragraph") {
711 				u->divLevel = type;
712 				outText("\n", buf, u);
713 			}
714 		}
715 		else if (!strcmp(tag.getName(), "span")) {
716 			outText( "", buf, u);
717 		}
718 		else if (!strcmp(tag.getName(), "br")) {
719 			outText( "\\", buf, u);
720 		}
721 		else if (!strcmp(tag.getName(), "table")) {
722 			if ((!tag.isEndTag()) && (!tag.isEmpty())) {
723 				outText( "\n\\begin{tabular}", buf, u);
724 			}
725 			else if (tag.isEndTag()) {
726 				outText( "\n\\end{tabular}", buf, u);
727 				++u->consecutiveNewlines;
728 				u->supressAdjacentWhitespace = true;
729 			}
730 
731 		}
732 		else if (!strcmp(tag.getName(), "row")) {
733 			if ((!tag.isEndTag()) && (!tag.isEmpty())) {
734 				outText( "\n", buf, u);
735 				u->firstCell = true;
736 			}
737 			else if (tag.isEndTag()) {
738 				outText( "//", buf, u);
739 				u->firstCell = false;
740 			}
741 
742 		}
743 		else if (!strcmp(tag.getName(), "cell")) {
744 			if ((!tag.isEndTag()) && (!tag.isEmpty())) {
745 				if (u->firstCell == false) {
746 					outText( " & ", buf, u);
747 				}
748 				else {
749 					u->firstCell = false;
750 				}
751 			}
752 			else if (tag.isEndTag()) {
753 				outText( "", buf, u);
754 			}
755 		}
756 		else {
757 			if (!u->supressAdjacentWhitespace) u->consecutiveNewlines = 0;
758 			return false;  // we still didn't handle token
759 		}
760 	}
761 	if (!u->supressAdjacentWhitespace) u->consecutiveNewlines = 0;
762 	return true;
763 }
764 
765 
766 SWORD_NAMESPACE_END
767