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