]> git.lyx.org Git - features.git/blob - src/output_docbook.cpp
DocBook: fix issues with nested labeling lists.
[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                                                         }
543                                                         xs << xml::CR();
544                                                 }
545                                         } else { // some kind of list
546                                                 if (style.labeltype == LABEL_MANUAL) {
547                                                         // Only variablelist gets here.
548
549                                                         openLabelTag(xs, style);
550                                                         sep = par->firstWordDocBook(xs, runparams);
551                                                         closeLabelTag(xs, style);
552                                                         xs << xml::CR();
553                                                 } else {
554                                                         openLabelTag(xs, style);
555                                                         xs << par->params().labelString();
556                                                         closeLabelTag(xs, style);
557                                                         xs << xml::CR();
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                                 bool emptyItem = false;
573                                 if (sep == par->size()) {
574                                         auto next_par = par;
575                                         ++next_par;
576                                         if (next_par == text.paragraphs().end()) // There is no next paragraph.
577                                                 emptyItem = true;
578                                         else // There is a next paragraph: check depth.
579                                                 emptyItem = par->params().depth() > next_par->params().depth();
580                                 }
581
582                                 if (emptyItem) {
583                                         // Avoid having an empty item, this is not valid DocBook. A single character is enough to force
584                                         // generation of a full <para>.
585                                         xs << ' ';
586                                 } else {
587                                         // Generate the rest of the paragraph, if need be.
588                                         par->simpleDocBookOnePar(buf, xs, rp, text.outerFont(distance(begin, par)), true, true, sep);
589                                 }
590
591                                 ++par;
592                                 if (getsIntoParagraph)
593                                         closeInnerItemTag(xs, style);
594
595                                 // We may not want to close the tag yet, in particular:
596                                 // If we're not at the end of the item...
597                                 if (par != pend
598                                         //  and are doing items...
599                                         && !isNormalEnv(style)
600                                         // and if the depth has changed...
601                                         && par->params().depth() != origdepth) {
602                                         // then we'll save this layout for later, and close it when
603                                         // we get another item.
604                                         lastlay = &style;
605                                 } else {
606                                         closeItemTag(xs, style);
607
608                                         // Eventually, close the item wrapper.
609                                         if (style.docbookitemwrappertag() != "NONE") {
610                                                 xs << xml::EndTag(style.docbookitemwrappertag());
611                                                 xs << xml::CR();
612                                         }
613                                 }
614                         }
615                         // The other possibility is that the depth has increased.
616                         else {
617                                 send = findEndOfEnvironment(par, pend);
618                                 par = makeEnvironment(buf, xs, runparams, text, par, send);
619                         }
620                         break;
621                 }
622                 case LATEX_PARAGRAPH:
623                         send = findLastParagraph(par, pend);
624                         par = makeParagraphs(buf, xs, runparams, text, par, send);
625                         break;
626                 case LATEX_BIB_ENVIRONMENT:
627                         send = findLastParagraph(par, pend);
628                         par = makeParagraphBibliography(buf, xs, runparams, text, par, send);
629                         break;
630                 case LATEX_COMMAND:
631                         ++par;
632                         break;
633                 }
634         }
635
636         if (lastlay != nullptr) {
637                 closeItemTag(xs, *lastlay);
638                 if (lastlay->docbookitemwrappertag() != "NONE") {
639                         xs << xml::EndTag(lastlay->docbookitemwrappertag());
640                         xs << xml::CR();
641                 }
642         }
643         closeTag(xs, bstyle);
644         xs << xml::CR();
645         return pend;
646 }
647
648
649 void makeCommand(
650                 Buffer const & buf,
651                 XMLStream & xs,
652                 OutputParams const & runparams,
653                 Text const & text,
654                 ParagraphList::const_iterator const & pbegin)
655 {
656         Layout const &style = pbegin->layout();
657
658         // No need for labels, as they are handled by DocBook tags.
659
660         openParTag(xs, style);
661
662         ParagraphList::const_iterator const begin = text.paragraphs().begin();
663         pbegin->simpleDocBookOnePar(buf, xs, runparams,
664                                                                 text.outerFont(distance(begin, pbegin)));
665         closeTag(xs, style);
666         xs << xml::CR();
667 }
668
669 pair<ParagraphList::const_iterator, ParagraphList::const_iterator> makeAny(
670                 Text const &text,
671                 Buffer const &buf,
672                 XMLStream &xs,
673                 OutputParams const &ourparams,
674                 ParagraphList::const_iterator par,
675                 ParagraphList::const_iterator send,
676                 ParagraphList::const_iterator pend)
677 {
678         Layout const & style = par->layout();
679
680         switch (style.latextype) {
681                 case LATEX_COMMAND: {
682                         // The files with which we are working never have more than
683                         // one paragraph in a command structure.
684                         // FIXME
685                         // if (ourparams.docbook_in_par)
686                         //   fix it so we don't get sections inside standard, e.g.
687                         // note that we may then need to make runparams not const, so we
688                         // can communicate that back.
689                         // FIXME Maybe this fix should be in the routines themselves, in case
690                         // they are called from elsewhere.
691                         makeCommand(buf, xs, ourparams, text, par);
692                         ++par;
693                         break;
694                 }
695                 case LATEX_ENVIRONMENT:
696                 case LATEX_LIST_ENVIRONMENT:
697                 case LATEX_ITEM_ENVIRONMENT: {
698                         // FIXME Same fix here.
699                         send = findEndOfEnvironment(par, pend);
700                         par = makeEnvironment(buf, xs, ourparams, text, par, send);
701                         break;
702                 }
703                 case LATEX_BIB_ENVIRONMENT: {
704                         send = findLastParagraph(par, pend);
705                         par = makeParagraphBibliography(buf, xs, ourparams, text, par, send);
706                         break;
707                 }
708                 case LATEX_PARAGRAPH: {
709                         send = findLastParagraph(par, pend);
710                         par = makeParagraphs(buf, xs, ourparams, text, par, send);
711                         break;
712                 }
713         }
714
715         return make_pair(par, send);
716 }
717
718 } // end anonymous namespace
719
720
721 using DocBookDocumentSectioning = tuple<bool, pit_type>;
722 using DocBookInfoTag = tuple<set<pit_type>, set<pit_type>, pit_type, pit_type>;
723
724
725 DocBookDocumentSectioning hasDocumentSectioning(ParagraphList const &paragraphs, pit_type bpit, pit_type const epit) {
726         bool documentHasSections = false;
727
728         while (bpit < epit) {
729                 Layout const &style = paragraphs[bpit].layout();
730                 documentHasSections |= style.category() == from_utf8("Sectioning");
731
732                 if (documentHasSections) {
733                         break;
734                 }
735                 bpit += 1;
736         }
737         // Paragraphs before the first section: [ runparams.par_begin ; eppit )
738
739         return make_tuple(documentHasSections, bpit);
740 }
741
742
743 DocBookInfoTag getParagraphsWithInfo(ParagraphList const &paragraphs, pit_type const bpit, pit_type const epit) {
744         set<pit_type> shouldBeInInfo;
745         set<pit_type> mustBeInInfo;
746
747         pit_type cpit = bpit;
748         while (cpit < epit) {
749                 // Skip paragraphs only containing one note.
750                 Paragraph const &par = paragraphs[cpit];
751                 if (par.size() == 1 && dynamic_cast<InsetNote*>(paragraphs[cpit].insetList().get(0))) {
752                         cpit += 1;
753                         continue;
754                 }
755
756                 // Based on layout information, store this paragraph in one set: should be in <info>, must be.
757                 Layout const &style = par.layout();
758
759                 if (style.docbookininfo() == "always") {
760                         mustBeInInfo.emplace(cpit);
761                 } else if (style.docbookininfo() == "maybe") {
762                         shouldBeInInfo.emplace(cpit);
763                 } else {
764                         // Hypothesis: the <info> parts should be grouped together near the beginning bpit.
765                         break;
766                 }
767                 cpit += 1;
768         }
769         // Now, cpit points to the last paragraph that has things that could go in <info>.
770         // bpit is still the beginning of the <info> part.
771
772         return make_tuple(shouldBeInInfo, mustBeInInfo, bpit, cpit);
773 }
774
775
776 bool hasAbstractBetween(ParagraphList const &paragraphs, pit_type const bpitAbstract, pit_type const epitAbstract)
777 {
778         // Hypothesis: the paragraphs between bpitAbstract and epitAbstract can be considered an abstract because they
779         // are just after a document or part title.
780         if (epitAbstract - bpitAbstract <= 0)
781                 return false;
782
783         // If there is something between these paragraphs, check if it's compatible with an abstract (i.e. some text).
784         pit_type bpit = bpitAbstract;
785         while (bpit < epitAbstract) {
786                 const Paragraph &p = paragraphs.at(bpit);
787
788                 if (p.layout().name() == from_ascii("Abstract"))
789                         return true;
790
791                 if (!p.insetList().empty()) {
792                         for (const auto &i : p.insetList()) {
793                                 if (i.inset->getText(0) != nullptr) {
794                                         return true;
795                                 }
796                         }
797                 }
798                 bpit++;
799         }
800         return false;
801 }
802
803
804 pit_type generateDocBookParagraphWithoutSectioning(
805                 Text const & text,
806                 Buffer const & buf,
807                 XMLStream & xs,
808                 OutputParams const & runparams,
809                 ParagraphList const & paragraphs,
810                 pit_type bpit,
811                 pit_type epit)
812 {
813         auto par = paragraphs.iterator_at(bpit);
814         auto lastStartedPar = par;
815         ParagraphList::const_iterator send;
816         auto const pend =
817                         (epit == (int) paragraphs.size()) ?
818                         paragraphs.end() : paragraphs.iterator_at(epit);
819
820         while (bpit < epit) {
821                 tie(par, send) = makeAny(text, buf, xs, runparams, par, send, pend);
822                 bpit += distance(lastStartedPar, par);
823                 lastStartedPar = par;
824         }
825
826         return bpit;
827 }
828
829
830 void outputDocBookInfo(
831                 Text const & text,
832                 Buffer const & buf,
833                 XMLStream & xs,
834                 OutputParams const & runparams,
835                 ParagraphList const & paragraphs,
836                 DocBookInfoTag const & info,
837                 pit_type bpitAbstract,
838                 pit_type const epitAbstract)
839 {
840         // Consider everything between bpitAbstract and epitAbstract (excluded) as paragraphs for the abstract.
841         // Use bpitAbstract >= epitAbstract to indicate there is no abstract.
842
843         set<pit_type> shouldBeInInfo;
844         set<pit_type> mustBeInInfo;
845         pit_type bpitInfo;
846         pit_type epitInfo;
847         tie(shouldBeInInfo, mustBeInInfo, bpitInfo, epitInfo) = info;
848
849         // The abstract must go in <info>.
850         bool hasAbstract = hasAbstractBetween(paragraphs, bpitAbstract, epitAbstract);
851         bool needInfo = !mustBeInInfo.empty() || hasAbstract;
852
853         // Start the <info> tag if required.
854         if (needInfo) {
855                 xs.startDivision(false);
856                 xs << xml::StartTag("info");
857                 xs << xml::CR();
858         }
859
860         // Output the elements that should go in <info>.
861         generateDocBookParagraphWithoutSectioning(text, buf, xs, runparams, paragraphs, bpitInfo, epitInfo);
862
863         if (hasAbstract) {
864                 string tag = paragraphs[bpitAbstract].layout().docbookforceabstracttag();
865                 if (tag == "NONE")
866                         tag = "abstract";
867
868                 xs << xml::StartTag(tag);
869                 xs << xml::CR();
870                 xs.startDivision(false);
871                 generateDocBookParagraphWithoutSectioning(text, buf, xs, runparams, paragraphs, bpitAbstract, epitAbstract);
872                 xs.endDivision();
873                 xs << xml::EndTag(tag);
874                 xs << xml::CR();
875         }
876
877         // End the <info> tag if it was started.
878         if (needInfo) {
879                 xs << xml::EndTag("info");
880                 xs << xml::CR();
881                 xs.endDivision();
882         }
883 }
884
885
886 void docbookFirstParagraphs(
887                 Text const &text,
888                 Buffer const &buf,
889                 XMLStream &xs,
890                 OutputParams const &runparams,
891                 pit_type epit)
892 {
893         // Handle the beginning of the document, supposing it has sections.
894         // Major role: output the first <info> tag.
895
896         ParagraphList const &paragraphs = text.paragraphs();
897         pit_type bpit = runparams.par_begin;
898         DocBookInfoTag info = getParagraphsWithInfo(paragraphs, bpit, epit);
899         outputDocBookInfo(text, buf, xs, runparams, paragraphs, info, get<3>(info), epit);
900 }
901
902
903 bool isParagraphEmpty(const Paragraph &par)
904 {
905         InsetList const &insets = par.insetList();
906         size_t insetsLength = distance(insets.begin(), insets.end());
907         bool hasParagraphOnlyNote = insetsLength == 1 && insets.get(0) && insets.get(0)->asInsetCollapsible() &&
908                                                                 dynamic_cast<InsetNote *>(insets.get(0));
909         return hasParagraphOnlyNote;
910 }
911
912
913 void docbookSimpleAllParagraphs(
914                 Text const & text,
915                 Buffer const & buf,
916                 XMLStream & xs,
917                 OutputParams const & runparams)
918 {
919         // Handle the document, supposing it has no sections (i.e. a "simple" document).
920
921         // First, the <info> tag.
922         ParagraphList const &paragraphs = text.paragraphs();
923         pit_type bpit = runparams.par_begin;
924         pit_type const epit = runparams.par_end;
925         DocBookInfoTag info = getParagraphsWithInfo(paragraphs, bpit, epit);
926         outputDocBookInfo(text, buf, xs, runparams, paragraphs, info, 0, 0);
927         bpit = get<3>(info); // Generate the content starting from the end of the <info> part.
928
929         // Then, the content.
930         ParagraphList::const_iterator const pend =
931                         (epit == (int) paragraphs.size()) ?
932                         paragraphs.end() : paragraphs.iterator_at(epit);
933
934         while (bpit < epit) {
935                 auto par = paragraphs.iterator_at(bpit);
936                 ParagraphList::const_iterator const lastStartedPar = par;
937                 ParagraphList::const_iterator send;
938
939                 if (isParagraphEmpty(*par)) {
940                         ++par;
941                         bpit += distance(lastStartedPar, par);
942                         continue;
943                 }
944
945                 // Generate this paragraph.
946                 tie(par, send) = makeAny(text, buf, xs, runparams, par, send, pend);
947                 bpit += distance(lastStartedPar, par);
948         }
949 }
950
951
952 void docbookParagraphs(Text const &text,
953                                            Buffer const &buf,
954                                            XMLStream &xs,
955                                            OutputParams const &runparams) {
956         ParagraphList const &paragraphs = text.paragraphs();
957         if (runparams.par_begin == runparams.par_end) {
958                 runparams.par_begin = 0;
959                 runparams.par_end = paragraphs.size();
960         }
961         pit_type bpit = runparams.par_begin;
962         pit_type const epit = runparams.par_end;
963         LASSERT(bpit < epit,
964                         {
965                                 xs << XMLStream::ESCAPE_NONE << "<!-- DocBook output error! -->\n";
966                                 return;
967                         });
968
969         ParagraphList::const_iterator const pend =
970                         (epit == (int) paragraphs.size()) ?
971                         paragraphs.end() : paragraphs.iterator_at(epit);
972         std::stack<std::pair<int, string>> headerLevels; // Used to determine when to open/close sections: store the depth
973         // of the section and the tag that was used to open it.
974
975         // Detect whether the document contains sections. If there are no sections, there can be no automatically
976         // discovered abstract.
977         bool documentHasSections;
978         pit_type eppit;
979         tie(documentHasSections, eppit) = hasDocumentSectioning(paragraphs, bpit, epit);
980
981         if (documentHasSections) {
982                 docbookFirstParagraphs(text, buf, xs, runparams, eppit);
983                 bpit = eppit;
984         } else {
985                 docbookSimpleAllParagraphs(text, buf, xs, runparams);
986                 return;
987         }
988
989         bool currentlyInAppendix = false;
990
991         while (bpit < epit) {
992                 OutputParams ourparams = runparams;
993
994                 auto par = paragraphs.iterator_at(bpit);
995                 if (par->params().startOfAppendix())
996                         currentlyInAppendix = true;
997                 Layout const &style = par->layout();
998                 ParagraphList::const_iterator const lastStartedPar = par;
999                 ParagraphList::const_iterator send;
1000
1001                 if (isParagraphEmpty(*par)) {
1002                         ++par;
1003                         bpit += distance(lastStartedPar, par);
1004                         continue;
1005                 }
1006
1007                 // Think about adding <section> and/or </section>s.
1008                 const bool isLayoutSectioning = style.category() == from_utf8("Sectioning");
1009                 if (isLayoutSectioning) {
1010                         int level = style.toclevel;
1011
1012                         // Need to close a previous section if it has the same level or a higher one (close <section> if opening a <h2>
1013                         // after a <h2>, <h3>, <h4>, <h5> or <h6>). More examples:
1014                         //   - current: h2; back: h1; do not close any <section>
1015                         //   - current: h1; back: h2; close two <section> (first the <h2>, then the <h1>, so a new <h1> can come)
1016                         while (!headerLevels.empty() && level <= headerLevels.top().first) {
1017                                 int stackLevel = headerLevels.top().first;
1018                                 docstring stackTag = from_utf8("</" + headerLevels.top().second + ">");
1019                                 headerLevels.pop();
1020
1021                                 // Output the tag only if it corresponds to a legit section.
1022                                 if (stackLevel != Layout::NOT_IN_TOC)
1023                                         xs << XMLStream::ESCAPE_NONE << stackTag << xml::CR();
1024                         }
1025
1026                         // Open the new section: first push it onto the stack, then output it in DocBook.
1027                         string sectionTag = (currentlyInAppendix && style.docbooksectiontag() == "chapter") ?
1028                                                                 "appendix" : style.docbooksectiontag();
1029                         headerLevels.push(std::make_pair(level, sectionTag));
1030
1031                         // Some sectioning-like elements should not be output (such as FrontMatter).
1032                         if (level != Layout::NOT_IN_TOC) {
1033                                 // Look for a label in the title, i.e. a InsetLabel as a child.
1034                                 docstring id = docstring();
1035                                 for (pos_type i = 0; i < par->size(); ++i) {
1036                                         Inset const *inset = par->getInset(i);
1037                                         if (inset) {
1038                                                 if (auto label = dynamic_cast<InsetLabel const *>(inset)) {
1039                                                         // Generate the attributes for the section if need be.
1040                                                         id += "xml:id=\"" + xml::cleanID(label->screenLabel()) + "\"";
1041
1042                                                         // Don't output the ID as a DocBook <anchor>.
1043                                                         ourparams.docbook_anchors_to_ignore.emplace(label->screenLabel());
1044
1045                                                         // Cannot have multiple IDs per tag.
1046                                                         break;
1047                                                 }
1048                                         }
1049                                 }
1050
1051                                 // Write the open tag for this section.
1052                                 docstring tag = from_utf8("<" + sectionTag);
1053                                 if (!id.empty())
1054                                         tag += from_utf8(" ") + id;
1055                                 tag += from_utf8(">");
1056                                 xs << XMLStream::ESCAPE_NONE << tag;
1057                                 xs << xml::CR();
1058                         }
1059                 }
1060
1061                 // Close all sections before the bibliography.
1062                 // 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)?
1063                 auto insetsLength = distance(par->insetList().begin(), par->insetList().end());
1064                 if (insetsLength > 0) {
1065                         Inset const *firstInset = par->getInset(0);
1066                         if (firstInset && dynamic_cast<InsetBibtex const *>(firstInset)) {
1067                                 while (!headerLevels.empty()) {
1068                                         int level = headerLevels.top().first;
1069                                         docstring tag = from_utf8("</" + headerLevels.top().second + ">");
1070                                         headerLevels.pop();
1071
1072                                         // Output the tag only if it corresponds to a legit section.
1073                                         if (level != Layout::NOT_IN_TOC) {
1074                                                 xs << XMLStream::ESCAPE_NONE << tag;
1075                                                 xs << xml::CR();
1076                                         }
1077                                 }
1078                         }
1079                 }
1080
1081                 // Generate this paragraph.
1082                 tie(par, send) = makeAny(text, buf, xs, ourparams, par, send, pend);
1083                 bpit += distance(lastStartedPar, par);
1084         }
1085
1086         // If need be, close <section>s, but only at the end of the document (otherwise, dealt with at the beginning
1087         // of the loop).
1088         while (!headerLevels.empty() && headerLevels.top().first > Layout::NOT_IN_TOC) {
1089                 docstring tag = from_utf8("</" + headerLevels.top().second + ">");
1090                 headerLevels.pop();
1091                 xs << XMLStream::ESCAPE_NONE << tag;
1092                 xs << xml::CR();
1093         }
1094 }
1095
1096 } // namespace lyx