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