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