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