]> git.lyx.org Git - features.git/blob - src/output_docbook.cpp
DocBook: refactor font handling.
[features.git] / src / output_docbook.cpp
1 /**
2  * \file output_docbook.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Lars Gullik Bjønnes
7  * \author José Matos
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "Buffer.h"
15 #include "buffer_funcs.h"
16 #include "BufferParams.h"
17 #include "Font.h"
18 #include "InsetList.h"
19 #include "Layout.h"
20 #include "OutputParams.h"
21 #include "Paragraph.h"
22 #include "ParagraphList.h"
23 #include "ParagraphParameters.h"
24 #include "xml.h"
25 #include "Text.h"
26 #include "TextClass.h"
27
28 #include "insets/InsetBibtex.h"
29 #include "insets/InsetBibitem.h"
30 #include "insets/InsetLabel.h"
31 #include "insets/InsetNote.h"
32
33 #include "support/convert.h"
34 #include "support/debug.h"
35 #include "support/lassert.h"
36 #include "support/lstrings.h"
37 #include "support/textutils.h"
38
39 #include <stack>
40 #include <iostream>
41 #include <algorithm>
42
43 using namespace std;
44 using namespace lyx::support;
45
46 namespace lyx {
47
48 namespace {
49
50 std::string const fontToDocBookTag(xml::FontTypes type)
51 {
52         switch (type) {
53         case xml::FontTypes::FT_EMPH:
54         case xml::FontTypes::FT_BOLD:
55                 return "emphasis";
56         case xml::FontTypes::FT_NOUN:
57                 return "person";
58         case xml::FontTypes::FT_UBAR:
59         case xml::FontTypes::FT_WAVE:
60         case xml::FontTypes::FT_DBAR:
61         case xml::FontTypes::FT_SOUT:
62         case xml::FontTypes::FT_XOUT:
63         case xml::FontTypes::FT_ITALIC:
64         case xml::FontTypes::FT_UPRIGHT:
65         case xml::FontTypes::FT_SLANTED:
66         case xml::FontTypes::FT_SMALLCAPS:
67         case xml::FontTypes::FT_ROMAN:
68         case xml::FontTypes::FT_SANS:
69                 return "emphasis";
70         case xml::FontTypes::FT_TYPE:
71                 return "code";
72         case xml::FontTypes::FT_SIZE_TINY:
73         case xml::FontTypes::FT_SIZE_SCRIPT:
74         case xml::FontTypes::FT_SIZE_FOOTNOTE:
75         case xml::FontTypes::FT_SIZE_SMALL:
76         case xml::FontTypes::FT_SIZE_NORMAL:
77         case xml::FontTypes::FT_SIZE_LARGE:
78         case xml::FontTypes::FT_SIZE_LARGER:
79         case xml::FontTypes::FT_SIZE_LARGEST:
80         case xml::FontTypes::FT_SIZE_HUGE:
81         case xml::FontTypes::FT_SIZE_HUGER:
82         case xml::FontTypes::FT_SIZE_INCREASE:
83         case xml::FontTypes::FT_SIZE_DECREASE:
84                 return "emphasis";
85         default:
86                 return "";
87         }
88 }
89
90 string fontToRole(xml::FontTypes type)
91 {
92         // Specific fonts are achieved with roles. The only common ones are "" for basic emphasis,
93         // and "bold"/"strong" for bold. With some specific options, other roles are copied into
94         // HTML output (via the DocBook XSLT sheets); otherwise, if not recognised, they are just ignored.
95         // Hence, it is not a problem to have many roles by default here.
96         // See https://www.sourceware.org/ml/docbook/2003-05/msg00269.html
97         switch (type) {
98         case xml::FontTypes::FT_ITALIC:
99         case xml::FontTypes::FT_EMPH:
100                 return "";
101         case xml::FontTypes::FT_BOLD:
102                 return "bold";
103         case xml::FontTypes::FT_NOUN:
104                 return ""; // Outputs a <person>
105         case xml::FontTypes::FT_TYPE:
106                 return ""; // Outputs a <code>
107         case xml::FontTypes::FT_UBAR:
108                 return "underline";
109
110                 // All other roles are non-standard for DocBook.
111
112         case xml::FontTypes::FT_WAVE:
113                 return "wave";
114         case xml::FontTypes::FT_DBAR:
115                 return "dbar";
116         case xml::FontTypes::FT_SOUT:
117                 return "sout";
118         case xml::FontTypes::FT_XOUT:
119                 return "xout";
120         case xml::FontTypes::FT_UPRIGHT:
121                 return "upright";
122         case xml::FontTypes::FT_SLANTED:
123                 return "slanted";
124         case xml::FontTypes::FT_SMALLCAPS:
125                 return "smallcaps";
126         case xml::FontTypes::FT_ROMAN:
127                 return "roman";
128         case xml::FontTypes::FT_SANS:
129                 return "sans";
130         case xml::FontTypes::FT_SIZE_TINY:
131                 return "tiny";
132         case xml::FontTypes::FT_SIZE_SCRIPT:
133                 return "size_script";
134         case xml::FontTypes::FT_SIZE_FOOTNOTE:
135                 return "size_footnote";
136         case xml::FontTypes::FT_SIZE_SMALL:
137                 return "size_small";
138         case xml::FontTypes::FT_SIZE_NORMAL:
139                 return "size_normal";
140         case xml::FontTypes::FT_SIZE_LARGE:
141                 return "size_large";
142         case xml::FontTypes::FT_SIZE_LARGER:
143                 return "size_larger";
144         case xml::FontTypes::FT_SIZE_LARGEST:
145                 return "size_largest";
146         case xml::FontTypes::FT_SIZE_HUGE:
147                 return "size_huge";
148         case xml::FontTypes::FT_SIZE_HUGER:
149                 return "size_huger";
150         case xml::FontTypes::FT_SIZE_INCREASE:
151                 return "size_increase";
152         case xml::FontTypes::FT_SIZE_DECREASE:
153                 return "size_decrease";
154         default:
155                 return "";
156         }
157 }
158
159 string fontToAttribute(xml::FontTypes type) {
160         // If there is a role (i.e. nonstandard use of a tag), output the attribute. Otherwise, the sheer tag is sufficient
161         // for the font.
162         string role = fontToRole(type);
163         if (!role.empty()) {
164                 return "role='" + role + "'";
165         } else {
166                 return "";
167         }
168 }
169
170 } // end anonymous namespace
171
172
173 xml::FontTag docbookStartFontTag(xml::FontTypes type)
174 {
175         return xml::FontTag(from_utf8(fontToDocBookTag(type)), from_utf8(fontToAttribute(type)), type);
176 }
177
178
179 xml::EndFontTag docbookEndFontTag(xml::FontTypes type)
180 {
181         return xml::EndFontTag(from_utf8(fontToDocBookTag(type)), type);
182 }
183
184
185 namespace {
186
187 // convenience functions
188
189 void openParTag(XMLStream &xs, Layout const &lay)
190 {
191         if (lay.docbookwrappertag() != "NONE") {
192                 xs << xml::StartTag(lay.docbookwrappertag(), lay.docbookwrapperattr());
193         }
194
195         string tag = lay.docbooktag();
196         if (tag == "Plain Layout")
197                 tag = "para";
198
199         xs << xml::ParTag(tag, lay.docbookattr());
200 }
201
202
203 void closeTag(XMLStream &xs, Layout const &lay)
204 {
205         string tag = lay.docbooktag();
206         if (tag == "Plain Layout")
207                 tag = "para";
208
209         xs << xml::EndTag(tag);
210         if (lay.docbookwrappertag() != "NONE")
211                 xs << xml::EndTag(lay.docbookwrappertag());
212 }
213
214
215 void openLabelTag(XMLStream & xs, Layout const & lay) // Mostly for definition lists.
216 {
217         xs << xml::StartTag(lay.docbookitemlabeltag(), lay.docbookitemlabelattr());
218 }
219
220
221 void closeLabelTag(XMLStream & xs, Layout const & lay)
222 {
223         xs << xml::EndTag(lay.docbookitemlabeltag());
224         xs << xml::CR();
225 }
226
227
228 void openItemTag(XMLStream &xs, Layout const &lay)
229 {
230         xs << xml::StartTag(lay.docbookitemtag(), lay.docbookitemattr());
231 }
232
233
234 // Return true when new elements are output in a paragraph, false otherwise.
235 bool openInnerItemTag(XMLStream &xs, Layout const &lay)
236 {
237         if (lay.docbookiteminnertag() != "NONE") {
238                 xs << xml::CR();
239                 xs << xml::ParTag(lay.docbookiteminnertag(), lay.docbookiteminnerattr());
240
241                 if (lay.docbookiteminnertag() == "para") {
242                         return true;
243                 }
244         }
245         return false;
246 }
247
248
249 void closeInnerItemTag(XMLStream &xs, Layout const &lay)
250 {
251         if (lay.docbookiteminnertag()!= "NONE") {
252                 xs << xml::EndTag(lay.docbookiteminnertag());
253                 xs << xml::CR();
254         }
255 }
256
257
258 inline void closeItemTag(XMLStream &xs, Layout const &lay)
259 {
260         xs << xml::EndTag(lay.docbookitemtag());
261         xs << xml::CR();
262 }
263
264 // end of convenience functions
265
266 ParagraphList::const_iterator findLastParagraph(
267                 ParagraphList::const_iterator p,
268                 ParagraphList::const_iterator const & pend) {
269         for (++p; p != pend && p->layout().latextype == LATEX_PARAGRAPH; ++p);
270
271         return p;
272 }
273
274
275 ParagraphList::const_iterator findEndOfEnvironment(
276                 ParagraphList::const_iterator const & pstart,
277                 ParagraphList::const_iterator const & pend)
278 {
279         ParagraphList::const_iterator p = pstart;
280         Layout const &bstyle = p->layout();
281         size_t const depth = p->params().depth();
282         for (++p; p != pend; ++p) {
283                 Layout const &style = p->layout();
284                 // It shouldn't happen that e.g. a section command occurs inside
285                 // a quotation environment, at a higher depth, but as of 6/2009,
286                 // it can happen. We pretend that it's just at lowest depth.
287                 if (style.latextype == LATEX_COMMAND)
288                         return p;
289
290                 // If depth is down, we're done
291                 if (p->params().depth() < depth)
292                         return p;
293
294                 // If depth is up, we're not done
295                 if (p->params().depth() > depth)
296                         continue;
297
298                 // FIXME I am not sure about the first check.
299                 // Surely we *could* have different layouts that count as
300                 // LATEX_PARAGRAPH, right?
301                 if (style.latextype == LATEX_PARAGRAPH || style != bstyle)
302                         return p;
303         }
304         return pend;
305 }
306
307
308 ParagraphList::const_iterator makeParagraphBibliography(
309                 Buffer const &buf,
310                 XMLStream &xs,
311                 OutputParams const &runparams,
312                 Text const &text,
313                 ParagraphList::const_iterator const & pbegin,
314                 ParagraphList::const_iterator const & pend)
315 {
316         auto const begin = text.paragraphs().begin();
317         auto const end = text.paragraphs().end();
318
319         // Find the paragraph *before* pbegin.
320         ParagraphList::const_iterator pbegin_before = begin;
321         if (pbegin != begin) {
322                 ParagraphList::const_iterator pbegin_before_next = begin;
323                 ++pbegin_before_next;
324
325                 while (pbegin_before_next != pbegin) {
326                         ++pbegin_before;
327                         ++pbegin_before_next;
328                 }
329         }
330
331         ParagraphList::const_iterator par = pbegin;
332
333         // If this is the first paragraph in a bibliography, open the bibliography tag.
334         if (pbegin != begin && pbegin_before->layout().latextype != LATEX_BIB_ENVIRONMENT) {
335                 xs << xml::StartTag("bibliography");
336                 xs << xml::CR();
337         }
338
339         // Generate the required paragraphs.
340         for (; par != pend; ++par) {
341                 // Start the precooked bibliography entry. This is very much like opening a paragraph tag.
342                 // Don't forget the citation ID!
343                 docstring attr;
344                 for (auto i = 0; i < par->size(); ++i) {
345                         if (par->getInset(0)->lyxCode() == BIBITEM_CODE) {
346                                 const auto * bibitem = dynamic_cast<const InsetBibitem*>(par->getInset(i));
347                                 attr = from_utf8("xml:id='") + bibitem->bibLabel() + from_utf8("'");
348                                 break;
349                         }
350                 }
351                 xs << xml::StartTag(from_utf8("bibliomixed"), attr);
352
353                 // Generate the entry.
354                 par->simpleDocBookOnePar(buf, xs, runparams, text.outerFont(distance(begin, par)), true, true, 0);
355
356                 // End the precooked bibliography entry.
357                 xs << xml::EndTag("bibliomixed");
358                 xs << xml::CR();
359         }
360
361         // If this is the last paragraph in a bibliography, close the bibliography tag.
362         if (par == end || par->layout().latextype != LATEX_BIB_ENVIRONMENT) {
363                 xs << xml::EndTag("bibliography");
364                 xs << xml::CR();
365         }
366
367         return pend;
368 }
369
370
371 ParagraphList::const_iterator makeParagraphs(
372                 Buffer const &buf,
373                 XMLStream &xs,
374                 OutputParams const &runparams,
375                 Text const &text,
376                 ParagraphList::const_iterator const & pbegin,
377                 ParagraphList::const_iterator const & pend)
378 {
379         ParagraphList::const_iterator const begin = text.paragraphs().begin();
380         ParagraphList::const_iterator par = pbegin;
381         for (; par != pend; ++par) {
382                 Layout const &lay = par->layout();
383
384                 // We want to open the paragraph tag if:
385                 //   (i) the current layout permits multiple paragraphs
386                 //  (ii) we are either not already inside a paragraph (HTMLIsBlock) OR
387                 //         we are, but this is not the first paragraph
388                 //
389                 // But there is also a special case, and we first see whether we are in it.
390                 // We do not want to open the paragraph tag if this paragraph contains
391                 // only one item, and that item is "inline", i.e., not HTMLIsBlock (such
392                 // as a branch). On the other hand, if that single item has a font change
393                 // applied to it, then we still do need to open the paragraph.
394                 //
395                 // Obviously, this is very fragile. The main reason we need to do this is
396                 // because of branches, e.g., a branch that contains an entire new section.
397                 // We do not really want to wrap that whole thing in a <div>...</div>.
398                 bool special_case = false;
399                 Inset const *specinset = par->size() == 1 ? par->getInset(0) : 0;
400                 if (specinset && !specinset->getLayout().htmlisblock()) { // TODO: Convert htmlisblock to a DocBook parameter?
401                         Layout const &style = par->layout();
402                         FontInfo const first_font = style.labeltype == LABEL_MANUAL ?
403                                                                                 style.labelfont : style.font;
404                         FontInfo const our_font =
405                                         par->getFont(buf.masterBuffer()->params(), 0,
406                                                                  text.outerFont(distance(begin, par))).fontInfo();
407
408                         if (first_font == our_font)
409                                 special_case = true;
410                 }
411
412                 // Plain layouts must be ignored.
413                 if (!special_case && buf.params().documentClass().isPlainLayout(lay) && !runparams.docbook_force_pars)
414                         special_case = true;
415                 // TODO: Could get rid of this with a DocBook equivalent to htmlisblock?
416                 if (!special_case && par->size() == 1 && par->getInset(0)) {
417                         Inset const * firstInset = par->getInset(0);
418
419                         // Floats cannot be in paragraphs.
420                         special_case = to_ascii(firstInset->layoutName()).substr(0, 6) == "Float:";
421
422                         // Bibliographies cannot be in paragraphs.
423                         if (!special_case && firstInset->asInsetCommand())
424                                 special_case = firstInset->asInsetCommand()->params().getCmdName() == "bibtex";
425
426                         // Equations do not deserve their own paragraph (DocBook allows them outside paragraphs).
427                         if (!special_case && firstInset->asInsetMath())
428                                 special_case = true;
429
430                         // ERTs are in comments, not paragraphs.
431                         if (!special_case && firstInset->lyxCode() == lyx::ERT_CODE)
432                                 special_case = true;
433
434                         // Listings should not get into their own paragraph.
435                         if (!special_case && firstInset->lyxCode() == lyx::LISTINGS_CODE)
436                                 special_case = true;
437                 }
438
439                 bool const open_par = runparams.docbook_make_pars
440                                                           && (!runparams.docbook_in_par || par != pbegin)
441                                                           && !special_case;
442
443                 // We want to issue the closing tag if either:
444                 //   (i)  We opened it, and either docbook_in_par is false,
445                 //              or we're not in the last paragraph, anyway.
446                 //   (ii) We didn't open it and docbook_in_par is true,
447                 //              but we are in the first par, and there is a next par.
448                 ParagraphList::const_iterator nextpar = par;
449                 ++nextpar;
450                 bool const close_par =
451                                 ((open_par && (!runparams.docbook_in_par || nextpar != pend))
452                                 || (!open_par && runparams.docbook_in_par && par == pbegin && nextpar != pend));
453
454                 if (open_par) {
455                         openParTag(xs, lay);
456                 }
457
458                 par->simpleDocBookOnePar(buf, xs, runparams, text.outerFont(distance(begin, par)), open_par, close_par, 0);
459
460                 if (close_par) {
461                         closeTag(xs, lay);
462                         xs << xml::CR();
463                 }
464         }
465         return pend;
466 }
467
468
469 bool isNormalEnv(Layout const &lay)
470 {
471         return lay.latextype == LATEX_ENVIRONMENT
472                    || lay.latextype == LATEX_BIB_ENVIRONMENT;
473 }
474
475
476 ParagraphList::const_iterator makeEnvironment(
477                 Buffer const &buf,
478                 XMLStream &xs,
479                 OutputParams const &runparams,
480                 Text const &text,
481                 ParagraphList::const_iterator const & pbegin,
482                 ParagraphList::const_iterator const & pend)
483 {
484         ParagraphList::const_iterator const begin = text.paragraphs().begin();
485         ParagraphList::const_iterator par = pbegin;
486         Layout const &bstyle = par->layout();
487         depth_type const origdepth = pbegin->params().depth();
488
489         // open tag for this environment
490         openParTag(xs, bstyle);
491         xs << xml::CR();
492
493         // we will on occasion need to remember a layout from before.
494         Layout const *lastlay = nullptr;
495
496         while (par != pend) {
497                 Layout const & style = par->layout();
498                 ParagraphList::const_iterator send;
499
500                 // Actual content of this paragraph.
501                 switch (style.latextype) {
502                 case LATEX_ENVIRONMENT:
503                 case LATEX_LIST_ENVIRONMENT:
504                 case LATEX_ITEM_ENVIRONMENT: {
505                         // There are two possibilities in this case.
506                         // One is that we are still in the environment in which we
507                         // started---which we will be if the depth is the same.
508                         if (par->params().depth() == origdepth) {
509                                 LATTEST(bstyle == style);
510                                 if (lastlay != nullptr) {
511                                         closeItemTag(xs, *lastlay);
512                                         if (lastlay->docbookitemwrappertag() != "NONE") {
513                                                 xs << xml::EndTag(lastlay->docbookitemwrappertag());
514                                                 xs << xml::CR();
515                                         }
516                                         lastlay = nullptr;
517                                 }
518
519                                 // this will be positive if we want to skip the
520                                 // initial word (if it's been taken for the label).
521                                 pos_type sep = 0;
522
523                                 // Open a wrapper tag if needed.
524                                 if (style.docbookitemwrappertag() != "NONE") {
525                                         xs << xml::StartTag(style.docbookitemwrappertag(), style.docbookitemwrapperattr());
526                                         xs << xml::CR();
527                                 }
528
529                                 // label output
530                                 if (style.labeltype != LABEL_NO_LABEL &&
531                                                 style.docbookitemlabeltag() != "NONE") {
532
533                                         if (isNormalEnv(style)) {
534                                                 // in this case, we print the label only for the first
535                                                 // paragraph (as in a theorem or an abstract).
536                                                 if (par == pbegin) {
537                                                         docstring const lbl = pbegin->params().labelString();
538                                                         if (!lbl.empty()) {
539                                                                 openLabelTag(xs, style);
540                                                                 xs << lbl;
541                                                                 closeLabelTag(xs, style);
542                                                         } else {
543                                                                 // No new line after closeLabelTag.
544                                                                 xs << xml::CR();
545                                                         }
546                                                 }
547                                         } else { // some kind of list
548                                                 if (style.labeltype == LABEL_MANUAL) {
549                                                         // Only variablelist gets here.
550
551                                                         openLabelTag(xs, style);
552                                                         sep = par->firstWordDocBook(xs, runparams);
553                                                         closeLabelTag(xs, style);
554                                                 } else {
555                                                         openLabelTag(xs, style);
556                                                         xs << par->params().labelString();
557                                                         closeLabelTag(xs, style);
558                                                 }
559                                         }
560                                 } // end label output
561
562                                 // Start generating the item.
563                                 bool wasInParagraph = runparams.docbook_in_par;
564                                 openItemTag(xs, style);
565                                 bool getsIntoParagraph = openInnerItemTag(xs, style);
566                                 OutputParams rp = runparams;
567                                 rp.docbook_in_par = wasInParagraph | getsIntoParagraph;
568
569                                 // Maybe the item is completely empty, i.e. if the first word ends at the end of the current paragraph
570                                 // AND if the next paragraph doesn't have the same depth (if there is such a paragraph).
571                                 // Common case: there is only the first word on the line, but there is a nested list instead
572                                 // of more text.
573                                 bool emptyItem = false;
574                                 if (sep == par->size()) {
575                                         auto next_par = par;
576                                         ++next_par;
577                                         if (next_par == text.paragraphs().end()) // There is no next paragraph.
578                                                 emptyItem = true;
579                                         else // There is a next paragraph: check depth.
580                                                 emptyItem = par->params().depth() >= next_par->params().depth();
581                                 }
582
583                                 if (emptyItem) {
584                                         // Avoid having an empty item, this is not valid DocBook. A single character is enough to force
585                                         // generation of a full <para>.
586                                         xs << ' ';
587                                 } else {
588                                         // Generate the rest of the paragraph, if need be.
589                                         par->simpleDocBookOnePar(buf, xs, rp, text.outerFont(distance(begin, par)), true, true, sep);
590                                 }
591
592                                 ++par;
593                                 if (getsIntoParagraph)
594                                         closeInnerItemTag(xs, style);
595
596                                 // We may not want to close the tag yet, in particular:
597                                 // If we're not at the end of the item...
598                                 if (par != pend
599                                         //  and are doing items...
600                                         && !isNormalEnv(style)
601                                         // and if the depth has changed...
602                                         && par->params().depth() != origdepth) {
603                                         // then we'll save this layout for later, and close it when
604                                         // we get another item.
605                                         lastlay = &style;
606                                 } else {
607                                         closeItemTag(xs, style);
608
609                                         // Eventually, close the item wrapper.
610                                         if (style.docbookitemwrappertag() != "NONE") {
611                                                 xs << xml::EndTag(style.docbookitemwrappertag());
612                                                 xs << xml::CR();
613                                         }
614                                 }
615                         }
616                         // The other possibility is that the depth has increased.
617                         else {
618                                 send = findEndOfEnvironment(par, pend);
619                                 par = makeEnvironment(buf, xs, runparams, text, par, send);
620                         }
621                         break;
622                 }
623                 case LATEX_PARAGRAPH:
624                         send = findLastParagraph(par, pend);
625                         par = makeParagraphs(buf, xs, runparams, text, par, send);
626                         break;
627                 case LATEX_BIB_ENVIRONMENT:
628                         send = findLastParagraph(par, pend);
629                         par = makeParagraphBibliography(buf, xs, runparams, text, par, send);
630                         break;
631                 case LATEX_COMMAND:
632                         ++par;
633                         break;
634                 }
635         }
636
637         if (lastlay != nullptr) {
638                 closeItemTag(xs, *lastlay);
639                 if (lastlay->docbookitemwrappertag() != "NONE") {
640                         xs << xml::EndTag(lastlay->docbookitemwrappertag());
641                         xs << xml::CR();
642                 }
643         }
644         closeTag(xs, bstyle);
645         xs << xml::CR();
646         return pend;
647 }
648
649
650 void makeCommand(
651                 Buffer const & buf,
652                 XMLStream & xs,
653                 OutputParams const & runparams,
654                 Text const & text,
655                 ParagraphList::const_iterator const & pbegin)
656 {
657         Layout const &style = pbegin->layout();
658
659         // No need for labels, as they are handled by DocBook tags.
660
661         openParTag(xs, style);
662
663         ParagraphList::const_iterator const begin = text.paragraphs().begin();
664         pbegin->simpleDocBookOnePar(buf, xs, runparams,
665                                                                 text.outerFont(distance(begin, pbegin)));
666         closeTag(xs, style);
667         xs << xml::CR();
668 }
669
670 pair<ParagraphList::const_iterator, ParagraphList::const_iterator> makeAny(
671                 Text const &text,
672                 Buffer const &buf,
673                 XMLStream &xs,
674                 OutputParams const &ourparams,
675                 ParagraphList::const_iterator par,
676                 ParagraphList::const_iterator send,
677                 ParagraphList::const_iterator pend)
678 {
679         Layout const & style = par->layout();
680
681         switch (style.latextype) {
682                 case LATEX_COMMAND: {
683                         // The files with which we are working never have more than
684                         // one paragraph in a command structure.
685                         // FIXME
686                         // if (ourparams.docbook_in_par)
687                         //   fix it so we don't get sections inside standard, e.g.
688                         // note that we may then need to make runparams not const, so we
689                         // can communicate that back.
690                         // FIXME Maybe this fix should be in the routines themselves, in case
691                         // they are called from elsewhere.
692                         makeCommand(buf, xs, ourparams, text, par);
693                         ++par;
694                         break;
695                 }
696                 case LATEX_ENVIRONMENT:
697                 case LATEX_LIST_ENVIRONMENT:
698                 case LATEX_ITEM_ENVIRONMENT: {
699                         // FIXME Same fix here.
700                         send = findEndOfEnvironment(par, pend);
701                         par = makeEnvironment(buf, xs, ourparams, text, par, send);
702                         break;
703                 }
704                 case LATEX_BIB_ENVIRONMENT: {
705                         send = findLastParagraph(par, pend);
706                         par = makeParagraphBibliography(buf, xs, ourparams, text, par, send);
707                         break;
708                 }
709                 case LATEX_PARAGRAPH: {
710                         send = findLastParagraph(par, pend);
711                         par = makeParagraphs(buf, xs, ourparams, text, par, send);
712                         break;
713                 }
714         }
715
716         return make_pair(par, send);
717 }
718
719 } // end anonymous namespace
720
721
722 using DocBookDocumentSectioning = tuple<bool, pit_type>;
723 using DocBookInfoTag = tuple<set<pit_type>, set<pit_type>, pit_type, pit_type>;
724
725
726 DocBookDocumentSectioning hasDocumentSectioning(ParagraphList const &paragraphs, pit_type bpit, pit_type const epit) {
727         bool documentHasSections = false;
728
729         while (bpit < epit) {
730                 Layout const &style = paragraphs[bpit].layout();
731                 documentHasSections |= style.category() == from_utf8("Sectioning");
732
733                 if (documentHasSections) {
734                         break;
735                 }
736                 bpit += 1;
737         }
738         // Paragraphs before the first section: [ runparams.par_begin ; eppit )
739
740         return make_tuple(documentHasSections, bpit);
741 }
742
743
744 DocBookInfoTag getParagraphsWithInfo(ParagraphList const &paragraphs, pit_type const bpit, pit_type const epit) {
745         set<pit_type> shouldBeInInfo;
746         set<pit_type> mustBeInInfo;
747
748         pit_type cpit = bpit;
749         while (cpit < epit) {
750                 // Skip paragraphs only containing one note.
751                 Paragraph const &par = paragraphs[cpit];
752                 if (par.size() == 1 && dynamic_cast<InsetNote*>(paragraphs[cpit].insetList().get(0))) {
753                         cpit += 1;
754                         continue;
755                 }
756
757                 // Based on layout information, store this paragraph in one set: should be in <info>, must be.
758                 Layout const &style = par.layout();
759
760                 if (style.docbookininfo() == "always") {
761                         mustBeInInfo.emplace(cpit);
762                 } else if (style.docbookininfo() == "maybe") {
763                         shouldBeInInfo.emplace(cpit);
764                 } else {
765                         // Hypothesis: the <info> parts should be grouped together near the beginning bpit.
766                         break;
767                 }
768                 cpit += 1;
769         }
770         // Now, cpit points to the last paragraph that has things that could go in <info>.
771         // bpit is still the beginning of the <info> part.
772
773         return make_tuple(shouldBeInInfo, mustBeInInfo, bpit, cpit);
774 }
775
776
777 bool hasAbstractBetween(ParagraphList const &paragraphs, pit_type const bpitAbstract, pit_type const epitAbstract)
778 {
779         // Hypothesis: the paragraphs between bpitAbstract and epitAbstract can be considered an abstract because they
780         // are just after a document or part title.
781         if (epitAbstract - bpitAbstract <= 0)
782                 return false;
783
784         // If there is something between these paragraphs, check if it's compatible with an abstract (i.e. some text).
785         pit_type bpit = bpitAbstract;
786         while (bpit < epitAbstract) {
787                 const Paragraph &p = paragraphs.at(bpit);
788
789                 if (p.layout().name() == from_ascii("Abstract"))
790                         return true;
791
792                 if (!p.insetList().empty()) {
793                         for (const auto &i : p.insetList()) {
794                                 if (i.inset->getText(0) != nullptr) {
795                                         return true;
796                                 }
797                         }
798                 }
799                 bpit++;
800         }
801         return false;
802 }
803
804
805 pit_type generateDocBookParagraphWithoutSectioning(
806                 Text const & text,
807                 Buffer const & buf,
808                 XMLStream & xs,
809                 OutputParams const & runparams,
810                 ParagraphList const & paragraphs,
811                 pit_type bpit,
812                 pit_type epit)
813 {
814         auto par = paragraphs.iterator_at(bpit);
815         auto lastStartedPar = par;
816         ParagraphList::const_iterator send;
817         auto const pend =
818                         (epit == (int) paragraphs.size()) ?
819                         paragraphs.end() : paragraphs.iterator_at(epit);
820
821         while (bpit < epit) {
822                 tie(par, send) = makeAny(text, buf, xs, runparams, par, send, pend);
823                 bpit += distance(lastStartedPar, par);
824                 lastStartedPar = par;
825         }
826
827         return bpit;
828 }
829
830
831 void outputDocBookInfo(
832                 Text const & text,
833                 Buffer const & buf,
834                 XMLStream & xs,
835                 OutputParams const & runparams,
836                 ParagraphList const & paragraphs,
837                 DocBookInfoTag const & info,
838                 pit_type bpitAbstract,
839                 pit_type const epitAbstract)
840 {
841         // Consider everything between bpitAbstract and epitAbstract (excluded) as paragraphs for the abstract.
842         // Use bpitAbstract >= epitAbstract to indicate there is no abstract.
843
844         set<pit_type> shouldBeInInfo;
845         set<pit_type> mustBeInInfo;
846         pit_type bpitInfo;
847         pit_type epitInfo;
848         tie(shouldBeInInfo, mustBeInInfo, bpitInfo, epitInfo) = info;
849
850         // The abstract must go in <info>.
851         bool hasAbstract = hasAbstractBetween(paragraphs, bpitAbstract, epitAbstract);
852         bool needInfo = !mustBeInInfo.empty() || hasAbstract;
853
854         // Start the <info> tag if required.
855         if (needInfo) {
856                 xs.startDivision(false);
857                 xs << xml::StartTag("info");
858                 xs << xml::CR();
859         }
860
861         // Output the elements that should go in <info>.
862         generateDocBookParagraphWithoutSectioning(text, buf, xs, runparams, paragraphs, bpitInfo, epitInfo);
863
864         if (hasAbstract) {
865                 string tag = paragraphs[bpitAbstract].layout().docbookforceabstracttag();
866                 if (tag == "NONE")
867                         tag = "abstract";
868
869                 xs << xml::StartTag(tag);
870                 xs << xml::CR();
871                 xs.startDivision(false);
872                 generateDocBookParagraphWithoutSectioning(text, buf, xs, runparams, paragraphs, bpitAbstract, epitAbstract);
873                 xs.endDivision();
874                 xs << xml::EndTag(tag);
875                 xs << xml::CR();
876         }
877
878         // End the <info> tag if it was started.
879         if (needInfo) {
880                 xs << xml::EndTag("info");
881                 xs << xml::CR();
882                 xs.endDivision();
883         }
884 }
885
886
887 void docbookFirstParagraphs(
888                 Text const &text,
889                 Buffer const &buf,
890                 XMLStream &xs,
891                 OutputParams const &runparams,
892                 pit_type epit)
893 {
894         // Handle the beginning of the document, supposing it has sections.
895         // Major role: output the first <info> tag.
896
897         ParagraphList const &paragraphs = text.paragraphs();
898         pit_type bpit = runparams.par_begin;
899         DocBookInfoTag info = getParagraphsWithInfo(paragraphs, bpit, epit);
900         outputDocBookInfo(text, buf, xs, runparams, paragraphs, info, get<3>(info), epit);
901 }
902
903
904 bool isParagraphEmpty(const Paragraph &par)
905 {
906         InsetList const &insets = par.insetList();
907         size_t insetsLength = distance(insets.begin(), insets.end());
908         bool hasParagraphOnlyNote = insetsLength == 1 && insets.get(0) && insets.get(0)->asInsetCollapsible() &&
909                                                                 dynamic_cast<InsetNote *>(insets.get(0));
910         return hasParagraphOnlyNote;
911 }
912
913
914 void docbookSimpleAllParagraphs(
915                 Text const & text,
916                 Buffer const & buf,
917                 XMLStream & xs,
918                 OutputParams const & runparams)
919 {
920         // Handle the document, supposing it has no sections (i.e. a "simple" document).
921
922         // First, the <info> tag.
923         ParagraphList const &paragraphs = text.paragraphs();
924         pit_type bpit = runparams.par_begin;
925         pit_type const epit = runparams.par_end;
926         DocBookInfoTag info = getParagraphsWithInfo(paragraphs, bpit, epit);
927         outputDocBookInfo(text, buf, xs, runparams, paragraphs, info, 0, 0);
928         bpit = get<3>(info); // Generate the content starting from the end of the <info> part.
929
930         // Then, the content.
931         ParagraphList::const_iterator const pend =
932                         (epit == (int) paragraphs.size()) ?
933                         paragraphs.end() : paragraphs.iterator_at(epit);
934
935         while (bpit < epit) {
936                 auto par = paragraphs.iterator_at(bpit);
937                 ParagraphList::const_iterator const lastStartedPar = par;
938                 ParagraphList::const_iterator send;
939
940                 if (isParagraphEmpty(*par)) {
941                         ++par;
942                         bpit += distance(lastStartedPar, par);
943                         continue;
944                 }
945
946                 // Generate this paragraph.
947                 tie(par, send) = makeAny(text, buf, xs, runparams, par, send, pend);
948                 bpit += distance(lastStartedPar, par);
949         }
950 }
951
952
953 void docbookParagraphs(Text const &text,
954                                            Buffer const &buf,
955                                            XMLStream &xs,
956                                            OutputParams const &runparams) {
957         ParagraphList const &paragraphs = text.paragraphs();
958         if (runparams.par_begin == runparams.par_end) {
959                 runparams.par_begin = 0;
960                 runparams.par_end = paragraphs.size();
961         }
962         pit_type bpit = runparams.par_begin;
963         pit_type const epit = runparams.par_end;
964         LASSERT(bpit < epit,
965                         {
966                                 xs << XMLStream::ESCAPE_NONE << "<!-- DocBook output error! -->\n";
967                                 return;
968                         });
969
970         ParagraphList::const_iterator const pend =
971                         (epit == (int) paragraphs.size()) ?
972                         paragraphs.end() : paragraphs.iterator_at(epit);
973         std::stack<std::pair<int, string>> headerLevels; // Used to determine when to open/close sections: store the depth
974         // of the section and the tag that was used to open it.
975
976         // Detect whether the document contains sections. If there are no sections, there can be no automatically
977         // discovered abstract.
978         bool documentHasSections;
979         pit_type eppit;
980         tie(documentHasSections, eppit) = hasDocumentSectioning(paragraphs, bpit, epit);
981
982         if (documentHasSections) {
983                 docbookFirstParagraphs(text, buf, xs, runparams, eppit);
984                 bpit = eppit;
985         } else {
986                 docbookSimpleAllParagraphs(text, buf, xs, runparams);
987                 return;
988         }
989
990         bool currentlyInAppendix = false;
991
992         while (bpit < epit) {
993                 OutputParams ourparams = runparams;
994
995                 auto par = paragraphs.iterator_at(bpit);
996                 if (par->params().startOfAppendix())
997                         currentlyInAppendix = true;
998                 Layout const &style = par->layout();
999                 ParagraphList::const_iterator const lastStartedPar = par;
1000                 ParagraphList::const_iterator send;
1001
1002                 if (isParagraphEmpty(*par)) {
1003                         ++par;
1004                         bpit += distance(lastStartedPar, par);
1005                         continue;
1006                 }
1007
1008                 // Think about adding <section> and/or </section>s.
1009                 const bool isLayoutSectioning = style.category() == from_utf8("Sectioning");
1010                 if (isLayoutSectioning) {
1011                         int level = style.toclevel;
1012
1013                         // Need to close a previous section if it has the same level or a higher one (close <section> if opening a <h2>
1014                         // after a <h2>, <h3>, <h4>, <h5> or <h6>). More examples:
1015                         //   - current: h2; back: h1; do not close any <section>
1016                         //   - current: h1; back: h2; close two <section> (first the <h2>, then the <h1>, so a new <h1> can come)
1017                         while (!headerLevels.empty() && level <= headerLevels.top().first) {
1018                                 int stackLevel = headerLevels.top().first;
1019                                 docstring stackTag = from_utf8("</" + headerLevels.top().second + ">");
1020                                 headerLevels.pop();
1021
1022                                 // Output the tag only if it corresponds to a legit section.
1023                                 if (stackLevel != Layout::NOT_IN_TOC)
1024                                         xs << XMLStream::ESCAPE_NONE << stackTag << xml::CR();
1025                         }
1026
1027                         // Open the new section: first push it onto the stack, then output it in DocBook.
1028                         string sectionTag = (currentlyInAppendix && style.docbooksectiontag() == "chapter") ?
1029                                                                 "appendix" : style.docbooksectiontag();
1030                         headerLevels.push(std::make_pair(level, sectionTag));
1031
1032                         // Some sectioning-like elements should not be output (such as FrontMatter).
1033                         if (level != Layout::NOT_IN_TOC) {
1034                                 // Look for a label in the title, i.e. a InsetLabel as a child.
1035                                 docstring id = docstring();
1036                                 for (pos_type i = 0; i < par->size(); ++i) {
1037                                         Inset const *inset = par->getInset(i);
1038                                         if (inset) {
1039                                                 if (auto label = dynamic_cast<InsetLabel const *>(inset)) {
1040                                                         // Generate the attributes for the section if need be.
1041                                                         id += "xml:id=\"" + xml::cleanID(label->screenLabel()) + "\"";
1042
1043                                                         // Don't output the ID as a DocBook <anchor>.
1044                                                         ourparams.docbook_anchors_to_ignore.emplace(label->screenLabel());
1045
1046                                                         // Cannot have multiple IDs per tag.
1047                                                         break;
1048                                                 }
1049                                         }
1050                                 }
1051
1052                                 // Write the open tag for this section.
1053                                 docstring tag = from_utf8("<" + sectionTag);
1054                                 if (!id.empty())
1055                                         tag += from_utf8(" ") + id;
1056                                 tag += from_utf8(">");
1057                                 xs << XMLStream::ESCAPE_NONE << tag;
1058                                 xs << xml::CR();
1059                         }
1060                 }
1061
1062                 // Close all sections before the bibliography.
1063                 // TODO: Only close all when the bibliography is at the end of the document? Or force to output the bibliography at the end of the document? Or don't care (as allowed by DocBook)?
1064                 auto insetsLength = distance(par->insetList().begin(), par->insetList().end());
1065                 if (insetsLength > 0) {
1066                         Inset const *firstInset = par->getInset(0);
1067                         if (firstInset && dynamic_cast<InsetBibtex const *>(firstInset)) {
1068                                 while (!headerLevels.empty()) {
1069                                         int level = headerLevels.top().first;
1070                                         docstring tag = from_utf8("</" + headerLevels.top().second + ">");
1071                                         headerLevels.pop();
1072
1073                                         // Output the tag only if it corresponds to a legit section.
1074                                         if (level != Layout::NOT_IN_TOC) {
1075                                                 xs << XMLStream::ESCAPE_NONE << tag;
1076                                                 xs << xml::CR();
1077                                         }
1078                                 }
1079                         }
1080                 }
1081
1082                 // Generate this paragraph.
1083                 tie(par, send) = makeAny(text, buf, xs, ourparams, par, send, pend);
1084                 bpit += distance(lastStartedPar, par);
1085         }
1086
1087         // If need be, close <section>s, but only at the end of the document (otherwise, dealt with at the beginning
1088         // of the loop).
1089         while (!headerLevels.empty() && headerLevels.top().first > Layout::NOT_IN_TOC) {
1090                 docstring tag = from_utf8("</" + headerLevels.top().second + ">");
1091                 headerLevels.pop();
1092                 xs << XMLStream::ESCAPE_NONE << tag;
1093                 xs << xml::CR();
1094         }
1095 }
1096
1097 } // namespace lyx