X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;ds=sidebyside;f=src%2Foutput_docbook.cpp;h=c80a7cdb91b7e4dab05591cc308c23e63ac205ae;hb=b198a36a363bb6a084407d476942d68ef5fb5e86;hp=458d8389c005c1ff45bcde09b1af2ce62196a667;hpb=0ba9b7c0c87b3ef08deadc50a48ec01ac8e70bd3;p=lyx.git diff --git a/src/output_docbook.cpp b/src/output_docbook.cpp index 458d8389c0..c80a7cdb91 100644 --- a/src/output_docbook.cpp +++ b/src/output_docbook.cpp @@ -11,12 +11,13 @@ #include +#include "output_docbook.h" + #include "Buffer.h" #include "buffer_funcs.h" #include "BufferParams.h" #include "Font.h" #include "InsetList.h" -#include "output_docbook.h" #include "Paragraph.h" #include "ParagraphList.h" #include "ParagraphParameters.h" @@ -27,8 +28,10 @@ #include "insets/InsetBibtex.h" #include "insets/InsetBibitem.h" #include "insets/InsetLabel.h" +#include "mathed/InsetMath.h" #include "insets/InsetNote.h" +#include "support/debug.h" #include "support/lassert.h" #include "support/textutils.h" @@ -158,141 +161,34 @@ string fontToAttribute(xml::FontTypes type) { // If there is a role (i.e. nonstandard use of a tag), output the attribute. Otherwise, the sheer tag is sufficient // for the font. string role = fontToRole(type); - if (!role.empty()) { + if (!role.empty()) return "role='" + role + "'"; - } else { - return ""; - } -} - - -// Convenience functions to open and close tags. First, very low-level ones to ensure a consistent new-line behaviour. -// Block style: -// Content before -// -// Contents of the block. -// -// Content after -// Paragraph style: -// Content before -// Contents of the paragraph. -// Content after -// Inline style: -// Content beforeContents of the paragraph.Content after - -void openInlineTag(XMLStream & xs, const std::string & tag, const std::string & attr) -{ - xs << xml::StartTag(tag, attr); -} - - -void closeInlineTag(XMLStream & xs, const std::string & tag) -{ - xs << xml::EndTag(tag); -} - - -void openParTag(XMLStream & xs, const std::string & tag, const std::string & attr) -{ - if (!xs.isLastTagCR()) - xs << xml::CR(); - xs << xml::StartTag(tag, attr); -} - - -void closeParTag(XMLStream & xs, const std::string & tag) -{ - xs << xml::EndTag(tag); - xs << xml::CR(); -} - - -void openBlockTag(XMLStream & xs, const std::string & tag, const std::string & attr) -{ - if (!xs.isLastTagCR()) - xs << xml::CR(); - xs << xml::StartTag(tag, attr); - xs << xml::CR(); -} - - -void closeBlockTag(XMLStream & xs, const std::string & tag) -{ - if (!xs.isLastTagCR()) - xs << xml::CR(); - xs << xml::EndTag(tag); - xs << xml::CR(); -} - - -void openTag(XMLStream & xs, const std::string & tag, const std::string & attr, const std::string & tagtype) -{ - if (tag.empty() || tag == "NONE") // Common check to be performed elsewhere, if it was not here. - return; - - if (tag == "para" || tagtype == "paragraph") // Special case for : always considered as a paragraph. - openParTag(xs, tag, attr); - else if (tagtype == "block") - openBlockTag(xs, tag, attr); - else if (tagtype == "inline") - openInlineTag(xs, tag, attr); - else - xs.writeError("Unrecognised tag type '" + tagtype + "' for '" + tag + " " + attr + "'"); -} - - -void closeTag(XMLStream & xs, const std::string & tag, const std::string & tagtype) -{ - if (tag.empty() || tag == "NONE") - return; - - if (tag == "para" || tagtype == "paragraph") // Special case for : always considered as a paragraph. - closeParTag(xs, tag); - else if (tagtype == "block") - closeBlockTag(xs, tag); - else if (tagtype == "inline") - closeInlineTag(xs, tag); else - xs.writeError("Unrecognised tag type '" + tagtype + "' for '" + tag + "'"); -} - - -void compTag(XMLStream & xs, const std::string & tag, const std::string & attr, const std::string & tagtype) -{ - if (tag.empty() || tag == "NONE") - return; - - // Special case for : always considered as a paragraph. - if (tag == "para" || tagtype == "paragraph" || tagtype == "block") { - if (!xs.isLastTagCR()) - xs << xml::CR(); - xs << xml::CompTag(tag, attr); - xs << xml::CR(); - } else if (tagtype == "inline") { - xs << xml::CompTag(tag, attr); - } else { - xs.writeError("Unrecognised tag type '" + tagtype + "' for '" + tag + "'"); - } + return ""; } // Higher-level convenience functions. -void openParTag(XMLStream & xs, const Paragraph * par, const Paragraph * prevpar) +void openParTag(XMLStream & xs, const Paragraph * par, const Paragraph * prevpar, const OutputParams & runparams) { - Layout const & lay = par->layout(); - if (par == prevpar) prevpar = nullptr; + // If the previous paragraph is empty, don't consider it when opening wrappers. + if (prevpar && prevpar->empty() && !prevpar->allowEmpty()) + prevpar = nullptr; + // When should the wrapper be opened here? Only if the previous paragraph has the SAME wrapper tag // (usually, they won't have the same layout) and the CURRENT one allows merging. // The main use case is author information in several paragraphs: if the name of the author is the // first paragraph of an author, then merging with the previous tag does not make sense. Say the // next paragraph is the affiliation, then it should be output in the same tag (different // layout, same wrapper tag). - bool openWrapper = lay.docbookwrappertag() != "NONE"; - if (prevpar != nullptr) { + Layout const & lay = par->layout(); + bool openWrapper = lay.docbookwrappertag() != "NONE" && !runparams.docbook_ignore_wrapper; + + if (prevpar != nullptr && !runparams.docbook_ignore_wrapper) { Layout const & prevlay = prevpar->layout(); if (prevlay.docbookwrappertag() != "NONE") { if (prevlay.docbookwrappertag() == lay.docbookwrappertag() && @@ -304,8 +200,17 @@ void openParTag(XMLStream & xs, const Paragraph * par, const Paragraph * prevpar } // Main logic. - if (openWrapper) - openTag(xs, lay.docbookwrappertag(), lay.docbookwrapperattr(), lay.docbookwrappertagtype()); + if (openWrapper) { + xml::openTag(xs, lay.docbookwrappertag(), lay.docbookwrapperattr(), lay.docbookwrappertagtype()); + + if (lay.docbookgeneratetitle()) { + docstring const label = par->params().labelString(); + + xml::openTag(xs, "title", "", "paragraph"); + xs << (!label.empty() ? label : from_ascii("No title")); + xml::closeTag(xs, "title", "paragraph"); + } + } const string & tag = lay.docbooktag(); if (tag != "NONE") { @@ -313,25 +218,31 @@ void openParTag(XMLStream & xs, const Paragraph * par, const Paragraph * prevpar if (!xs.isTagOpen(xmltag, 1)) { // Don't nest a paragraph directly in a paragraph. // TODO: required or not? // TODO: avoid creating a ParTag object just for this query... - openTag(xs, lay.docbooktag(), lay.docbookattr(), lay.docbooktagtype()); - openTag(xs, lay.docbookinnertag(), lay.docbookinnerattr(), lay.docbookinnertagtype()); + xml::openTag(xs, lay.docbooktag(), lay.docbookattr(), lay.docbooktagtype()); + xml::openTag(xs, lay.docbookinnertag(), lay.docbookinnerattr(), lay.docbookinnertagtype()); } } - openTag(xs, lay.docbookitemtag(), lay.docbookitemattr(), lay.docbookitemtagtype()); - openTag(xs, lay.docbookiteminnertag(), lay.docbookiteminnerattr(), lay.docbookiteminnertagtype()); + xml::openTag(xs, lay.docbookitemwrappertag(), lay.docbookitemwrapperattr(), lay.docbookitemwrappertagtype()); + xml::openTag(xs, lay.docbookitemtag(), lay.docbookitemattr(), lay.docbookitemtagtype()); + xml::openTag(xs, lay.docbookiteminnertag(), lay.docbookiteminnerattr(), lay.docbookiteminnertagtype()); } -void closeParTag(XMLStream & xs, Paragraph const * par, Paragraph const * nextpar) +void closeParTag(XMLStream & xs, Paragraph const * par, Paragraph const * nextpar, const OutputParams & runparams) { if (par == nextpar) nextpar = nullptr; + // If the next paragraph is empty, don't consider it when closing wrappers. + if (nextpar && nextpar->empty() && !nextpar->allowEmpty()) + nextpar = nullptr; + // See comment in openParTag. Layout const & lay = par->layout(); - bool closeWrapper = lay.docbookwrappertag() != "NONE"; - if (nextpar != nullptr) { + bool closeWrapper = lay.docbookwrappertag() != "NONE" && !runparams.docbook_ignore_wrapper; + + if (nextpar != nullptr && !runparams.docbook_ignore_wrapper) { Layout const & nextlay = nextpar->layout(); if (nextlay.docbookwrappertag() != "NONE") { if (nextlay.docbookwrappertag() == lay.docbookwrappertag() && @@ -343,12 +254,13 @@ void closeParTag(XMLStream & xs, Paragraph const * par, Paragraph const * nextpa } // Main logic. - closeTag(xs, lay.docbookiteminnertag(), lay.docbookiteminnertagtype()); - closeTag(xs, lay.docbookitemtag(), lay.docbookitemtagtype()); - closeTag(xs, lay.docbookinnertag(), lay.docbookinnertagtype()); - closeTag(xs, lay.docbooktag(), lay.docbooktagtype()); + xml::closeTag(xs, lay.docbookiteminnertag(), lay.docbookiteminnertagtype()); + xml::closeTag(xs, lay.docbookitemtag(), lay.docbookitemtagtype()); + xml::closeTag(xs, lay.docbookitemwrappertag(), lay.docbookitemwrappertagtype()); + xml::closeTag(xs, lay.docbookinnertag(), lay.docbookinnertagtype()); + xml::closeTag(xs, lay.docbooktag(), lay.docbooktagtype()); if (closeWrapper) - closeTag(xs, lay.docbookwrappertag(), lay.docbookwrappertagtype()); + xml::closeTag(xs, lay.docbookwrappertag(), lay.docbookwrappertagtype()); } @@ -374,7 +286,8 @@ void makeBibliography( if (!ip) continue; if (const auto * bibitem = dynamic_cast(ip)) { - attr = from_utf8("xml:id='") + bibitem->getParam("key") + from_utf8("'"); + auto id = xml::cleanID(bibitem->getParam("key")); + attr = from_utf8("xml:id='") + id + from_utf8("'"); break; } } @@ -382,9 +295,17 @@ void makeBibliography( // Generate the entry. Concatenate the different parts of the paragraph if any. auto const begin = text.paragraphs().begin(); - auto pars = par->simpleDocBookOnePar(buf, runparams, text.outerFont(std::distance(begin, par)), 0); + std::vector pars_prepend; + std::vector pars; + std::vector pars_append; + tie(pars_prepend, pars, pars_append) = par->simpleDocBookOnePar(buf, runparams, text.outerFont(std::distance(begin, par)), 0); + + for (auto & parXML : pars_prepend) + xs << XMLStream::ESCAPE_NONE << parXML; for (auto & parXML : pars) xs << XMLStream::ESCAPE_NONE << parXML; + for (auto & parXML : pars_append) + xs << XMLStream::ESCAPE_NONE << parXML; // End the precooked bibliography entry. xs << xml::EndTag("bibliomixed"); @@ -410,6 +331,7 @@ void makeParagraph( OutputParams const & runparams, ParagraphList::const_iterator const & par) { + // Useful variables. auto const begin = text.paragraphs().begin(); auto const end = text.paragraphs().end(); auto prevpar = text.paragraphs().getParagraphBefore(par); @@ -430,7 +352,7 @@ void makeParagraph( // We do not really want to wrap that whole thing in a
...
. bool special_case = false; Inset const *specinset = par->size() == 1 ? par->getInset(0) : nullptr; - if (specinset && !specinset->getLayout().htmlisblock()) { // TODO: Convert htmlisblock to a DocBook parameter? + if (specinset && !specinset->getLayout().htmlisblock()) { // TODO: Convert htmlisblock to a DocBook parameter? docbooknotinpara should be enough in most cases. Layout const &style = par->layout(); FontInfo const first_font = style.labeltype == LABEL_MANUAL ? style.labelfont : style.font; @@ -443,38 +365,80 @@ void makeParagraph( } size_t nInsets = std::distance(par->insetList().begin(), par->insetList().end()); + auto parSize = (size_t) par->size(); // Plain layouts must be ignored. special_case |= buf.params().documentClass().isPlainLayout(par->layout()) && !runparams.docbook_force_pars; - // Equations do not deserve their own paragraph (DocBook allows them outside paragraphs). - special_case |= nInsets == (size_t) par->size() && std::all_of(par->insetList().begin(), par->insetList().end(), [](InsetList::Element inset) { - return inset.inset && inset.inset->asInsetMath(); - }); - // 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. - if (!special_case && par->size() == 1 && par->getInset(0)) { - Inset const * firstInset = par->getInset(0); - - // Floats cannot be in paragraphs. - special_case = to_utf8(firstInset->layoutName()).substr(0, 6) == "Float:"; - - // Bibliographies cannot be in paragraphs. - if (!special_case && firstInset->asInsetCommand()) - special_case = firstInset->asInsetCommand()->params().getCmdName() == "bibtex"; + // Equations do not deserve their own paragraph (DocBook allows them outside paragraphs). + // Exception: any case that generates an must still get a paragraph to be valid. + auto isEquationSpecialCase = [](InsetList::Element inset) { + return inset.inset && inset.inset->asInsetMath() && inset.inset->asInsetMath()->getType() != hullSimple; + }; + special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), isEquationSpecialCase); + + // Things that should not get into their own paragraph. (Only valid for DocBook.) + static std::set lyxCodeSpecialCases = { + TABULAR_CODE, + FLOAT_CODE, + BIBTEX_CODE, // Bibliographies cannot be in paragraphs. Bibitems should still be handled as paragraphs, + // though (see makeParagraphBibliography). + ERT_CODE, // ERTs are in comments, not paragraphs. + LISTINGS_CODE, + BOX_CODE, + INCLUDE_CODE, + NOMENCL_PRINT_CODE, + TOC_CODE, // To be ignored in DocBook, the processor afterwards should deal with ToCs. + NOTE_CODE // Notes do not produce any output. + }; + auto isLyxCodeSpecialCase = [](InsetList::Element inset) { + return lyxCodeSpecialCases.find(inset.inset->lyxCode()) != lyxCodeSpecialCases.end(); + }; + special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), isLyxCodeSpecialCase); + + // Flex elements (InsetLayout) have their own parameter to control the special case. + auto isFlexSpecialCase = [](InsetList::Element inset) { + if (inset.inset->lyxCode() != FLEX_CODE) + return false; - // ERTs are in comments, not paragraphs. - if (!special_case && firstInset->lyxCode() == lyx::ERT_CODE) - special_case = true; + // Standard condition: check the parameter. + if (inset.inset->getLayout().docbooknotinpara()) + return true; + + // If the parameter is not set, maybe the flex inset only contains things that should match the standard + // condition. In this case, isLyxCodeSpecialCase must also check for bibitems... + auto isLyxCodeSpecialCase = [](InsetList::Element inset) { + return lyxCodeSpecialCases.find(inset.inset->lyxCode()) != lyxCodeSpecialCases.end() || + inset.inset->lyxCode() == BIBITEM_CODE; + }; + if (InsetText * text = inset.inset->asInsetText()) { + for (auto const & par : text->paragraphs()) { + size_t nInsets = std::distance(par.insetList().begin(), par.insetList().end()); + auto parSize = (size_t) par.size(); + + if (nInsets == 1 && par.insetList().begin()->inset->lyxCode() == BIBITEM_CODE) + return true; + if (nInsets != parSize) + return false; + if (!std::all_of(par.insetList().begin(), par.insetList().end(), isLyxCodeSpecialCase)) + return false; + } + return true; + } - // Listings should not get into their own paragraph. - if (!special_case && firstInset->lyxCode() == lyx::LISTINGS_CODE) - special_case = true; + // No case matched: give up. + return false; + }; + special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), isFlexSpecialCase); - // Boxes cannot get into their own paragraph. - if (!special_case && firstInset->lyxCode() == lyx::BOX_CODE) - special_case = true; - } + // If the insets should be rendered as images, enter the special case. + auto isRenderedAsImageSpecialCase = [](InsetList::Element inset) { + return inset.inset && inset.inset->getLayout().docbookrenderasimage(); + }; + special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), isRenderedAsImageSpecialCase); + // Open a paragraph if it is allowed, we are not already within a paragraph, and the insets in the paragraph do + // not forbid paragraphs (aka special cases). bool const open_par = runparams.docbook_make_pars && !runparams.docbook_in_par && !special_case; @@ -484,7 +448,7 @@ void makeParagraph( // or we're not in the last paragraph, anyway. // (ii) We didn't open it and docbook_in_par is true, // but we are in the first par, and there is a next par. - bool const close_par = open_par && (!runparams.docbook_in_par); + bool const close_par = open_par && !runparams.docbook_in_par; // Determine if this paragraph has some real content. Things like new pages are not caught // by Paragraph::empty(), even though they do not generate anything useful in DocBook. @@ -493,18 +457,28 @@ void makeParagraph( // Open and close tags around each contained paragraph. auto nextpar = par; ++nextpar; - auto pars = par->simpleDocBookOnePar(buf, runparams, text.outerFont(distance(begin, par)), 0, nextpar == end, special_case); + + std::vector pars_prepend; + std::vector pars; + std::vector pars_append; + tie(pars_prepend, pars, pars_append) = par->simpleDocBookOnePar(buf, runparams, text.outerFont(distance(begin, par)), 0, nextpar == end, special_case); + + for (docstring const & parXML : pars_prepend) + xs << XMLStream::ESCAPE_NONE << parXML; for (docstring const & parXML : pars) { - if (xml::isNotOnlySpace(parXML)) { - if (open_par) - openParTag(xs, &*par, prevpar); + if (!xml::isNotOnlySpace(parXML)) + continue; - xs << XMLStream::ESCAPE_NONE << parXML; + if (open_par) + openParTag(xs, &*par, prevpar, runparams); - if (close_par) - closeParTag(xs, &*par, (nextpar != end) ? &*nextpar : nullptr); - } + xs << XMLStream::ESCAPE_NONE << parXML; + + if (close_par) + closeParTag(xs, &*par, (nextpar != end) ? &*nextpar : nullptr, runparams); } + for (docstring const & parXML : pars_append) + xs << XMLStream::ESCAPE_NONE << parXML; } @@ -514,6 +488,7 @@ void makeEnvironment(Text const &text, OutputParams const &runparams, ParagraphList::const_iterator const & par) { + // Useful variables. auto const end = text.paragraphs().end(); auto nextpar = par; ++nextpar; @@ -531,7 +506,7 @@ void makeEnvironment(Text const &text, // Output the opening tag for this environment, but only if it has not been previously opened (condition // implemented in openParTag). auto prevpar = text.paragraphs().getParagraphBefore(par); - openParTag(xs, &*par, prevpar); // TODO: switch in layout for par/block? + openParTag(xs, &*par, prevpar, runparams); // Generate the contents of this environment. There is a special case if this is like some environment. Layout const & style = par->layout(); @@ -539,32 +514,45 @@ void makeEnvironment(Text const &text, // Nothing to do (otherwise, infinite loops). } else if (style.latextype == LATEX_ENVIRONMENT) { // Generate the paragraph, if need be. - auto pars = par->simpleDocBookOnePar(buf, runparams, text.outerFont(std::distance(text.paragraphs().begin(), par)), 0, false, ignoreFonts); - + std::vector pars_prepend; + std::vector pars; + std::vector pars_append; + tie(pars_prepend, pars, pars_append) = + par->simpleDocBookOnePar(buf, runparams, text.outerFont(std::distance(text.paragraphs().begin(), par)), + 0, false, ignoreFonts); + + for (docstring const & parXML : pars_prepend) + xs << XMLStream::ESCAPE_NONE << parXML; if (mimicListing) { auto p = pars.begin(); while (p != pars.end()) { - openTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(), par->layout().docbookiteminnertagtype()); + xml::openTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(), + par->layout().docbookiteminnertagtype()); xs << XMLStream::ESCAPE_NONE << *p; - closeTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnertagtype()); + xml::closeTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnertagtype()); ++p; + // Insert a new line after each "paragraph" (i.e. line in the listing), except for the last one. + // Otherwise, there would one more new line in the output than in the LyX document. if (p != pars.end()) xs << xml::CR(); } } else { for (auto const & p : pars) { - openTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(), par->layout().docbookiteminnertagtype()); + xml::openTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(), + par->layout().docbookiteminnertagtype()); xs << XMLStream::ESCAPE_NONE << p; - closeTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnertagtype()); + xml::closeTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnertagtype()); } } + for (docstring const & parXML : pars_append) + xs << XMLStream::ESCAPE_NONE << parXML; } else { makeAny(text, buf, xs, runparams, par); } // Close the environment. - closeParTag(xs, &*par, (nextpar != end) ? &*nextpar : nullptr); // TODO: switch in layout for par/block? + closeParTag(xs, &*par, (nextpar != end) ? &*nextpar : nullptr, runparams); } @@ -608,21 +596,28 @@ ParagraphList::const_iterator makeListEnvironment(Text const &text, OutputParams const &runparams, ParagraphList::const_iterator const & begin) { + // Useful variables. auto par = begin; auto const end = text.paragraphs().end(); auto const envend = findEndOfEnvironment(par, end); // Output the opening tag for this environment. Layout const & envstyle = par->layout(); - openTag(xs, envstyle.docbookwrappertag(), envstyle.docbookwrapperattr(), envstyle.docbookwrappertagtype()); - openTag(xs, envstyle.docbooktag(), envstyle.docbookattr(), envstyle.docbooktagtype()); + xml::openTag(xs, envstyle.docbookwrappertag(), envstyle.docbookwrapperattr(), envstyle.docbookwrappertagtype()); + xml::openTag(xs, envstyle.docbooktag(), envstyle.docbookattr(), envstyle.docbooktagtype()); // Handle the content of the list environment, item by item. while (par != envend) { - Layout const & style = par->layout(); + // Skip this paragraph if it is both empty and the last one (otherwise, there may be deeper paragraphs after). + auto nextpar = par; + ++nextpar; + if (par->empty() && nextpar == envend) + break; // Open the item wrapper. - openTag(xs, style.docbookitemwrappertag(), style.docbookitemwrapperattr(), style.docbookitemwrappertagtype()); + Layout const & style = par->layout(); + xml::openTag(xs, style.docbookitemwrappertag(), style.docbookitemwrapperattr(), + style.docbookitemwrappertagtype()); // Generate the label, if need be. If it is taken from the text, sep != 0 and corresponds to the first // character after the label. @@ -630,38 +625,47 @@ ParagraphList::const_iterator makeListEnvironment(Text const &text, if (style.labeltype != LABEL_NO_LABEL && style.docbookitemlabeltag() != "NONE") { if (style.labeltype == LABEL_MANUAL) { // Only variablelist gets here (or similar items defined as an extension in the layout). - openTag(xs, style.docbookitemlabeltag(), style.docbookitemlabelattr(), style.docbookitemlabeltagtype()); + xml::openTag(xs, style.docbookitemlabeltag(), style.docbookitemlabelattr(), + style.docbookitemlabeltagtype()); sep = 1 + par->firstWordDocBook(xs, runparams); - closeTag(xs, style.docbookitemlabeltag(), style.docbookitemlabeltagtype()); + xml::closeTag(xs, style.docbookitemlabeltag(), style.docbookitemlabeltagtype()); } else { // Usual cases: maybe there is something specified at the layout level. Highly unlikely, though. docstring const lbl = par->params().labelString(); if (!lbl.empty()) { - openTag(xs, style.docbookitemlabeltag(), style.docbookitemlabelattr(), style.docbookitemlabeltagtype()); + xml::openTag(xs, style.docbookitemlabeltag(), style.docbookitemlabelattr(), + style.docbookitemlabeltagtype()); xs << lbl; - closeTag(xs, style.docbookitemlabeltag(), style.docbookitemlabeltagtype()); + xml::closeTag(xs, style.docbookitemlabeltag(), style.docbookitemlabeltagtype()); } } } // Open the item (after the wrapper and the label). - openTag(xs, style.docbookitemtag(), style.docbookitemattr(), style.docbookitemtagtype()); + xml::openTag(xs, style.docbookitemtag(), style.docbookitemattr(), style.docbookitemtagtype()); // Generate the content of the item. if (sep < par->size()) { - auto pars = par->simpleDocBookOnePar(buf, runparams, + std::vector pars_prepend; + std::vector pars; + std::vector pars_append; + tie(pars_prepend, pars, pars_append) = par->simpleDocBookOnePar(buf, runparams, text.outerFont(std::distance(text.paragraphs().begin(), par)), sep); + for (docstring const & parXML : pars_prepend) + xs << XMLStream::ESCAPE_NONE << parXML; for (auto &p : pars) { - openTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(), - par->layout().docbookiteminnertagtype()); + xml::openTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(), + par->layout().docbookiteminnertagtype()); xs << XMLStream::ESCAPE_NONE << p; - closeTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnertagtype()); + xml::closeTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnertagtype()); } + for (docstring const & parXML : pars_append) + xs << XMLStream::ESCAPE_NONE << parXML; } else { // DocBook doesn't like emptiness. - compTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(), - par->layout().docbookiteminnertagtype()); + xml::compTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(), + par->layout().docbookiteminnertagtype()); } // If the next item is deeper, it must go entirely within this item (do it recursively). @@ -674,13 +678,13 @@ ParagraphList::const_iterator makeListEnvironment(Text const &text, // a list, and another paragraph; or an item with two types of list (itemise then enumerate, for instance). // Close the item. - closeTag(xs, style.docbookitemtag(), style.docbookitemtagtype()); - closeTag(xs, style.docbookitemwrappertag(), style.docbookitemwrappertagtype()); + xml::closeTag(xs, style.docbookitemtag(), style.docbookitemtagtype()); + xml::closeTag(xs, style.docbookitemwrappertag(), style.docbookitemwrappertagtype()); } // Close this environment in exactly the same way as it was opened. - closeTag(xs, envstyle.docbooktag(), envstyle.docbooktagtype()); - closeTag(xs, envstyle.docbookwrappertag(), envstyle.docbookwrappertagtype()); + xml::closeTag(xs, envstyle.docbooktag(), envstyle.docbooktagtype()); + xml::closeTag(xs, envstyle.docbookwrappertag(), envstyle.docbookwrappertagtype()); return envend; } @@ -693,6 +697,7 @@ void makeCommand( OutputParams const & runparams, ParagraphList::const_iterator const & par) { + // Useful variables. // Unlike XHTML, no need for labels, as they are handled by DocBook tags. auto const begin = text.paragraphs().begin(); auto const end = text.paragraphs().end(); @@ -701,20 +706,39 @@ void makeCommand( // Generate this command. auto prevpar = text.paragraphs().getParagraphBefore(par); - openParTag(xs, &*par, prevpar); - auto pars = par->simpleDocBookOnePar(buf, runparams,text.outerFont(distance(begin, par))); + std::vector pars_prepend; + std::vector pars; + std::vector pars_append; + tie(pars_prepend, pars, pars_append) = par->simpleDocBookOnePar(buf, runparams,text.outerFont(distance(begin, par))); + + for (docstring const & parXML : pars_prepend) + xs << XMLStream::ESCAPE_NONE << parXML; + + openParTag(xs, &*par, prevpar, runparams); for (auto & parXML : pars) // TODO: decide what to do with openParTag/closeParTag in new lines. xs << XMLStream::ESCAPE_NONE << parXML; + closeParTag(xs, &*par, (nextpar != end) ? &*nextpar : nullptr, runparams); - closeParTag(xs, &*par, (nextpar != end) ? &*nextpar : nullptr); + for (docstring const & parXML : pars_append) + xs << XMLStream::ESCAPE_NONE << parXML; } bool isLayoutSectioning(Layout const & lay) { - return lay.category() == from_utf8("Sectioning"); + if (lay.docbooksection()) // Special case: some DocBook styles must be handled as sections. + return true; + else if (lay.category() == from_utf8("Sectioning") || lay.docbooktag() == "section") // Generic case. + return lay.toclevel != Layout::NOT_IN_TOC; + return false; +} + + +bool isLayoutSectioningOrSimilar(Layout const & lay) +{ + return isLayoutSectioning(lay) || lay.docbooktag() == "bridgehead"; } @@ -741,8 +765,10 @@ DocBookDocumentSectioning hasDocumentSectioning(ParagraphList const ¶graphs, bool documentHasSections = false; while (bpit < epit) { + LASSERT(static_cast(bpit) < paragraphs.size(), return make_tuple(documentHasSections, bpit)); + Layout const &style = paragraphs[bpit].layout(); - documentHasSections |= isLayoutSectioning(style); + documentHasSections |= isLayoutSectioningOrSimilar(style); if (documentHasSections) break; @@ -762,20 +788,32 @@ bool hasOnlyNotes(Paragraph const & par) // return false. if (!par.isInset(i) || par.getInset(i)->lyxCode() != NOTE_CODE) return false; + + // An empty paragraph may still require some output. + if (par.layout().docbooksection()) + return false; + + // There should be really no content here. return true; } DocBookInfoTag getParagraphsWithInfo(ParagraphList const ¶graphs, pit_type bpit, pit_type const epit, - // Typically, bpit is the beginning of the document and epit the end *or* the first section. - bool documentHasSections) { + // Typically, bpit is the beginning of the document and epit the end of the + // document *or* the first section. + bool documentHasSections, + bool detectUnlayoutedAbstract + // Whether paragraphs with no specific layout should be detected as abstracts. + // For inner sections, an abstract should only be detected if it has a specific + // layout. For others, anything that might look like an abstract should be sought. + ) { set shouldBeInInfo; set mustBeInInfo; set abstractWithLayout; set abstractNoLayout; - // Find the first non empty paragraph by mutating bpit. + // Find the first nonempty paragraph by mutating bpit. while (bpit < epit) { Paragraph const &par = paragraphs[bpit]; if (par.empty() || hasOnlyNotes(par)) @@ -786,36 +824,57 @@ DocBookInfoTag getParagraphsWithInfo(ParagraphList const ¶graphs, // Traverse everything that might belong to . bool hasAbstractLayout = false; + static depth_type INVALID_DEPTH = 100000; + depth_type abstractDepth = INVALID_DEPTH; pit_type cpit = bpit; for (; cpit < epit; ++cpit) { // Skip paragraphs that don't generate anything in DocBook. Paragraph const & par = paragraphs[cpit]; + Layout const &style = par.layout(); if (hasOnlyNotes(par)) continue; - // There should never be any section here. (Just a sanity check: if this fails, this function could end up - // processing the whole document.) - if (isLayoutSectioning(par.layout())) { - LYXERR0("Assertion failed: section found in potential paragraphs."); + // There should never be any section here, except for the first paragraph (a title can be part of ). + // (Just a sanity check: if this fails, this function could end up processing the whole document.) + if (cpit != bpit && isLayoutSectioningOrSimilar(par.layout())) { + LYXERR(Debug::OUTFILE, "Assertion failed: section found in potential paragraphs."); break; } // If this is marked as an abstract by the layout, put it in the right set. - if (par.layout().docbookabstract()) { + if (style.docbookabstract()) { hasAbstractLayout = true; + abstractDepth = par.getDepth(); abstractWithLayout.emplace(cpit); continue; } + // Deeper paragraphs following the abstract must still be considered as part of the abstract. + // For instance, this includes lists. There should not be any other kind of paragraph in between. + if (abstractDepth != INVALID_DEPTH && style.docbookininfo() == "never") { + if (par.getDepth() > abstractDepth) { + abstractWithLayout.emplace(cpit); + continue; + } + if (par.getDepth() == abstractDepth) { + // This is not an abstract paragraph and it should not either be considered as part + // of it. It breaks the rule that abstract paragraphs must follow each other. + abstractDepth = INVALID_DEPTH; + break; + } + } + // Based on layout information, store this paragraph in one set: should be in , must be, // or abstract (either because of layout or of position). - Layout const &style = par.layout(); - if (style.docbookininfo() == "always") mustBeInInfo.emplace(cpit); else if (style.docbookininfo() == "maybe") shouldBeInInfo.emplace(cpit); - else if (documentHasSections && !hasAbstractLayout) + else if (documentHasSections && !hasAbstractLayout && detectUnlayoutedAbstract && + (style.docbooktag() == "NONE" || style.docbooktag() == "para") && + style.docbookwrappertag() == "NONE") + // In this case, it is very likely that style.docbookininfo() == "never"! Be extra careful + // about anything that gets caught here. For instance, don't ake into account abstractNoLayout.emplace(cpit); else // This should definitely not be in . break; @@ -831,30 +890,79 @@ DocBookInfoTag getParagraphsWithInfo(ParagraphList const ¶graphs, } // end anonymous namespace +std::set gatherInfo(ParagraphList::const_iterator par) +{ + // This function has a structure highly similar to makeAny and its friends. It's only made to be called on what + // should become the document's . + std::set values; + + // If this kind of layout should be ignored, already leave. + if (par->layout().docbooktag() == "IGNORE") + return values; + + // If this should go in info, mark it as such. Dive deep into the abstract, as it may hide many things that + // DocBook doesn't want to be inside the abstract. + for (pos_type i = 0; i < par->size(); ++i) { + if (par->getInset(i) && par->getInset(i)->asInsetText()) { + InsetText const *inset = par->getInset(i)->asInsetText(); + + if (inset->getLayout().docbookininfo() != "never") { + values.insert(inset); + } else { + auto subpar = inset->paragraphs().begin(); + while (subpar != inset->paragraphs().end()) { + auto subinfos = gatherInfo(subpar); + for (auto & subinfo: subinfos) + values.insert(subinfo); + ++subpar; + } + } + } + } + + return values; +} + + ParagraphList::const_iterator makeAny(Text const &text, Buffer const &buf, XMLStream &xs, OutputParams const &runparams, ParagraphList::const_iterator par) { - switch (par->layout().latextype) { - case LATEX_COMMAND: - makeCommand(text, buf, xs, runparams, par); - break; - case LATEX_ENVIRONMENT: - makeEnvironment(text, buf, xs, runparams, par); - break; - case LATEX_LIST_ENVIRONMENT: - case LATEX_ITEM_ENVIRONMENT: - // Only case when makeAny() might consume more than one paragraph. - return makeListEnvironment(text, buf, xs, runparams, par); - case LATEX_PARAGRAPH: - makeParagraph(text, buf, xs, runparams, par); - break; - case LATEX_BIB_ENVIRONMENT: - makeBibliography(text, buf, xs, runparams, par); - break; + bool ignoreParagraph = false; + + // If this kind of layout should be ignored, already leave. + ignoreParagraph |= par->layout().docbooktag() == "IGNORE"; + + // For things that should go into , check the variable rp.docbook_generate_info. This does not apply to the + // abstract itself. + bool isAbstract = par->layout().docbookabstract() || par->layout().docbooktag() == "abstract"; + ignoreParagraph |= !isAbstract && par->layout().docbookininfo() != "never" && !runparams.docbook_generate_info; + + // Switch on the type of paragraph to call the right handler. + if (!ignoreParagraph) { + switch (par->layout().latextype) { + case LATEX_COMMAND: + makeCommand(text, buf, xs, runparams, par); + break; + case LATEX_ENVIRONMENT: + makeEnvironment(text, buf, xs, runparams, par); + break; + case LATEX_LIST_ENVIRONMENT: + case LATEX_ITEM_ENVIRONMENT: + // Only case when makeAny() might consume more than one paragraph. + return makeListEnvironment(text, buf, xs, runparams, par); + case LATEX_PARAGRAPH: + makeParagraph(text, buf, xs, runparams, par); + break; + case LATEX_BIB_ENVIRONMENT: + makeBibliography(text, buf, xs, runparams, par); + break; + } } + + // For cases that are not lists, the next paragraph to handle is the next one. ++par; return par; } @@ -886,19 +994,57 @@ void outputDocBookInfo( // This check must be performed *before* a decision on whether or not to output is made. bool hasAbstract = !info.abstract.empty(); docstring abstract; + set infoInsets; // Paragraphs that should go into , but are hidden in an + // paragraph. (This happens for quite a few layouts, unfortunately.) + if (hasAbstract) { // Generate the abstract XML into a string before further checks. + // Usually, makeAny only generates one paragraph at a time. However, for the specific case of lists, it might + // generate more than one paragraph, as indicated in the return value. odocstringstream os2; XMLStream xs2(os2); - for (auto const & p : info.abstract) - makeAny(text, buf, xs2, runparams, paragraphs.iterator_at(p)); - // Actually output the abstract if there is something to do. Don't count line feeds or spaces in this, - // even though they must be properly output if there is some abstract. + auto rp = runparams; + rp.docbook_generate_info = false; + rp.docbook_ignore_wrapper = true; + + set doneParas; // Paragraphs that have already been converted (mostly to deal with lists). + for (auto const & p : info.abstract) { + if (doneParas.find(p) == doneParas.end()) { + auto oldPar = paragraphs.iterator_at(p); + auto newPar = makeAny(text, buf, xs2, rp, oldPar); + + // Find insets that should go outside the abstract. + auto subinfos = gatherInfo(oldPar); + for (auto & subinfo: subinfos) + infoInsets.insert(subinfo); + + // Insert the indices of all the paragraphs that were just generated (typically, one). + // **Make the hypothesis that, when an abstract has a list, all its items are consecutive.** + // Otherwise, makeAny and makeListEnvironment would have to be adapted too. + pit_type id = p; + while (oldPar != newPar) { + doneParas.emplace(id); + ++oldPar; + ++id; + } + } + } + + // Actually output the abstract if there is something to do. Don't count line feeds, spaces, or comments + // in this -- even though line feeds and spaces must be properly output if there is some abstract. abstract = os2.str(); docstring cleaned = abstract; cleaned.erase(std::remove_if(cleaned.begin(), cleaned.end(), lyx::isSpace), cleaned.end()); + size_t beginComment; + size_t endComment; + while ((beginComment = cleaned.find(from_ascii(""), beginComment)) != lyx::docstring::npos) { + cleaned.erase(cleaned.begin() + beginComment, cleaned.begin() + endComment + 3); + } + } + // Nothing? Then there is no abstract! if (cleaned.empty()) hasAbstract = false; @@ -914,42 +1060,41 @@ void outputDocBookInfo( xs << xml::CR(); } - // Output the elements that should go in , before and after the abstract. + // Output the elements that should go in . + // - First, the title. for (auto pit : info.shouldBeInInfo) // Typically, the title: these elements are so important and ubiquitous // that mandating a wrapper like would repel users. Thus, generate them first. makeAny(text, buf, xs, runparams, paragraphs.iterator_at(pit)); - for (auto pit : info.mustBeInInfo) - makeAny(text, buf, xs, runparams, paragraphs.iterator_at(pit)); - // If there is no title, generate one (required for the document to be valid). // This code is called for the main document, for table cells, etc., so be precise in this condition. - if (text.isMainText() && info.shouldBeInInfo.empty()) { + if (text.isMainText() && info.shouldBeInInfo.empty() && !runparams.inInclude) { xs << xml::StartTag("title"); xs << "Untitled Document"; xs << xml::EndTag("title"); xs << xml::CR(); } - // Always output the abstract as the last item of the , as it requires special treatment (especially if - // it contains several paragraphs that are empty). - if (hasAbstract) { - if (info.abstractLayout) { - xs << XMLStream::ESCAPE_NONE << abstract; - xs << xml::CR(); - } else { - string tag = paragraphs[*info.abstract.begin()].layout().docbookforceabstracttag(); - if (tag == "NONE") - tag = "abstract"; + // - Then, other metadata. + for (auto pit : info.mustBeInInfo) + makeAny(text, buf, xs, runparams, paragraphs.iterator_at(pit)); + for (auto const * inset : infoInsets) + inset->docbook(xs, runparams); - if (!xs.isLastTagCR()) - xs << xml::CR(); + // - Finally, always output the abstract as the last item of the , as it requires special treatment + // (especially if it contains several paragraphs that are empty). + if (hasAbstract) { + string tag = paragraphs[*info.abstract.begin()].layout().docbookforceabstracttag(); + if (tag == "NONE") + tag = "abstract"; - xs << xml::StartTag(tag); - xs << xml::CR(); - xs << XMLStream::ESCAPE_NONE << abstract; - xs << xml::EndTag(tag); + if (!xs.isLastTagCR()) xs << xml::CR(); - } + + xs << xml::StartTag(tag); + xs << xml::CR(); + xs << XMLStream::ESCAPE_NONE << abstract; + xs << xml::EndTag(tag); + xs << xml::CR(); } // End the tag if it was started. @@ -977,7 +1122,7 @@ void docbookSimpleAllParagraphs( ParagraphList const ¶graphs = text.paragraphs(); // First, the tag. - DocBookInfoTag info = getParagraphsWithInfo(paragraphs, bpit, epit, false); + DocBookInfoTag info = getParagraphsWithInfo(paragraphs, bpit, epit, false, true); outputDocBookInfo(text, buf, xs, runparams, paragraphs, info); // Then, the content. It starts where the ends. @@ -1009,9 +1154,6 @@ void docbookParagraphs(Text const &text, return; }); - std::stack> headerLevels; // Used to determine when to open/close sections: store the depth - // of the section and the tag that was used to open it. - // Detect whether the document contains sections. If there are no sections, treatment is largely simplified. // In particular, there can't be an abstract, unless it is manually marked. bool documentHasSections; @@ -1025,35 +1167,94 @@ void docbookParagraphs(Text const &text, } // Output the first tag (or just the title). - DocBookInfoTag info = getParagraphsWithInfo(paragraphs, bpit, eppit, true); + DocBookInfoTag info = getParagraphsWithInfo(paragraphs, bpit, eppit, true, true); outputDocBookInfo(text, buf, xs, runparams, paragraphs, info); bpit = info.epit; - // Then, iterate through the paragraphs of this document. - bool currentlyInAppendix = false; + // In the specific case of books, there must be parts or chapters. In some cases, star sections are used at the + // beginning for many things like acknowledgements or licenses. DocBook has tags for many of these cases, but not + // the LyX layouts... Gather everything in a , that's the closest in meaning. + // This is only useful if the things after the tag are not already parts or chapters! + if (buf.params().documentClass().docbookroot() == "book") { + // Check the condition on the first few elements. + bool hasPreface = false; + pit_type pref_bpit = bpit; + pit_type pref_epit = bpit; + + static const std::set allowedElements = { + // List from https://tdg.docbook.org/tdg/5.2/book.html + "acknowledgements", "appendix", "article", "bibliography", "chapter", "colophon", "dedication", + "glossary", "index", "part", "preface", "reference", "toc" + }; + + for (; pref_epit < epit; ++pref_epit) { + auto par = text.paragraphs().iterator_at(pref_epit); + if (allowedElements.find(par->layout().docbooktag()) != allowedElements.end() || + allowedElements.find(par->layout().docbooksectiontag()) != allowedElements.end()) + break; + + hasPreface = true; + } + + // Output a preface if required. A title is needed for the document to be valid... + if (hasPreface) { + xs << xml::StartTag("preface"); + xs << xml::CR(); + + xs << xml::StartTag("title"); + xs << "Preface"; + xs << xml::EndTag("title"); + xs << xml::CR(); + + auto pref_par = text.paragraphs().iterator_at(pref_bpit); + auto pref_end = text.paragraphs().iterator_at(pref_epit); + while (pref_par != pref_end) { + // Skip paragraphs not producing any output. + if (hasOnlyNotes(*pref_par)) { + ++pref_par; + continue; + } + + // TODO: must sections be handled here? If so, it might be useful to extract the corresponding loop + // in the rest of this function to use the same here (and avoid copy-paste mistakes). + pref_par = makeAny(text, buf, xs, runparams, pref_par); + } + + xs << xml::EndTag("preface"); + xs << xml::CR(); + + // Skip what has just been generated in the preface. + bpit = pref_epit; + } + } + + std::stack> headerLevels; // Used to determine when to open/close sections: store the depth + // of the section and the tag that was used to open it. + // Then, iterate through the paragraphs of this document. auto par = text.paragraphs().iterator_at(bpit); auto end = text.paragraphs().iterator_at(epit); while (par != end) { - OutputParams ourparams = runparams; - - if (par->params().startOfAppendix()) - currentlyInAppendix = true; + // Skip paragraphs not producing any output. if (hasOnlyNotes(*par)) { ++par; continue; } + OutputParams ourparams = runparams; Layout const &style = par->layout(); // Think about adding
and/or
s. - if (isLayoutSectioning(style)) { + if (isLayoutSectioning(style) || par->params().startOfAppendix()) { int level = style.toclevel; // Need to close a previous section if it has the same level or a higher one (close
if opening a //

after a

,

,

,

or
). More examples: // - current: h2; back: h1; do not close any
// - current: h1; back: h2; close two
(first the

, then the

, so a new

can come) + // Some layouts require that Layout::NOT_IN_TOC sections still cause closing of previous sections. This is + // mostly to ensure that the section is positioned at a DocBook-compatible level (acknowledgements: cannot + // be under a section!). while (!headerLevels.empty() && level <= headerLevels.top().first) { // Output the tag only if it corresponds to a legit section. int stackLevel = headerLevels.top().first; @@ -1065,8 +1266,7 @@ void docbookParagraphs(Text const &text, } // Open the new section: first push it onto the stack, then output it in DocBook. - string sectionTag = (currentlyInAppendix && style.docbooksectiontag() == "chapter") ? - "appendix" : style.docbooksectiontag(); + string sectionTag = (par->params().startOfAppendix()) ? "appendix" : style.docbooksectiontag(); headerLevels.push(std::make_pair(level, sectionTag)); // Some sectioning-like elements should not be output (such as FrontMatter). @@ -1083,7 +1283,8 @@ void docbookParagraphs(Text const &text, // Don't output the ID as a DocBook . ourparams.docbook_anchors_to_ignore.emplace(label->screenLabel()); - // Cannot have multiple IDs per tag. + // Cannot have multiple IDs per tag. If there is another ID inset in the document, it will + // be output as a DocBook anchor. break; } } @@ -1104,11 +1305,16 @@ void docbookParagraphs(Text const &text, Inset const *firstInset = par->getInset(0); if (firstInset && (firstInset->lyxCode() == BIBITEM_CODE || firstInset->lyxCode() == BIBTEX_CODE)) { while (!headerLevels.empty()) { + // Don't close appendices before bibliographies. + if (headerLevels.top().second == "appendix") + break; + + // Pop the section from the stack. int level = headerLevels.top().first; docstring tag = from_utf8(""); headerLevels.pop(); - // Output the tag only if it corresponds to a legit section. + // Output the tag only if it corresponds to a legit section, as the rest of the code. if (level != Layout::NOT_IN_TOC) { xs << XMLStream::ESCAPE_NONE << tag; xs << xml::CR(); @@ -1117,30 +1323,84 @@ void docbookParagraphs(Text const &text, } } - // Generate this paragraph. - par = makeAny(text, buf, xs, ourparams, par); - - // Some special sections may require abstracts (mostly parts, in books). + // Generate the tag if a section was just opened. + // Some sections may require abstracts (mostly parts, in books: DocBookForceAbstractTag will not be NONE), + // others can still have an abstract (it must be detected so that it can be output at the right place). // 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 and , so it's probably not worth the effort. - if (isLayoutSectioning(style) && style.docbookforceabstracttag() != "NONE") { + if (isLayoutSectioning(style)) { // This abstract may be found between the next paragraph and the next title. pit_type cpit = std::distance(text.paragraphs().begin(), par); - pit_type ppit = std::get<1>(hasDocumentSectioning(paragraphs, cpit, epit)); + pit_type ppit = std::get<1>(hasDocumentSectioning(paragraphs, cpit + 1L, epit)); // Generate this abstract (this code corresponds to parts of outputDocBookInfo). - DocBookInfoTag secInfo = getParagraphsWithInfo(paragraphs, cpit, ppit, true); + DocBookInfoTag secInfo = getParagraphsWithInfo(paragraphs, cpit, ppit, true, + style.docbookforceabstracttag() != "NONE"); + + if (!secInfo.mustBeInInfo.empty() || !secInfo.shouldBeInInfo.empty() || !secInfo.abstract.empty()) { + // Generate the , if required. If DocBookForceAbstractTag != NONE, this abstract will not be in + // , unlike other ("standard") abstracts. + bool hasStandardAbstract = !secInfo.abstract.empty() && style.docbookforceabstracttag() == "NONE"; + bool needInfo = !secInfo.mustBeInInfo.empty() || hasStandardAbstract; + + if (needInfo) { + xs.startDivision(false); + xs << xml::StartTag("info"); + xs << xml::CR(); + } - if (!secInfo.abstract.empty()) { - xs << xml::StartTag(style.docbookforceabstracttag()); - xs << xml::CR(); - for (auto const &p : secInfo.abstract) - makeAny(text, buf, xs, runparams, paragraphs.iterator_at(p)); - xs << xml::EndTag(style.docbookforceabstracttag()); - xs << xml::CR(); - } + // Output the elements that should go in , before and after the abstract. + for (auto pit : secInfo.shouldBeInInfo) // Typically, the title: these elements are so important and ubiquitous + // that mandating a wrapper like would repel users. Thus, generate them first. + makeAny(text, buf, xs, ourparams, paragraphs.iterator_at(pit)); + for (auto pit : secInfo.mustBeInInfo) + makeAny(text, buf, xs, ourparams, paragraphs.iterator_at(pit)); + + // Deal with the abstract in if it is standard (i.e. its tag is ). + if (!secInfo.abstract.empty() && hasStandardAbstract) { + if (!secInfo.abstractLayout) { + xs << xml::StartTag("abstract"); + xs << xml::CR(); + } + + for (auto const &p : secInfo.abstract) + makeAny(text, buf, xs, ourparams, paragraphs.iterator_at(p)); + + if (!secInfo.abstractLayout) { + xs << xml::EndTag("abstract"); + xs << xml::CR(); + } + } - // Skip all the text that just has been generated. - par = paragraphs.iterator_at(ppit); + // End the tag if it was started. + if (needInfo) { + if (!xs.isLastTagCR()) + xs << xml::CR(); + + xs << xml::EndTag("info"); + xs << xml::CR(); + xs.endDivision(); + } + + // Deal with the abstract outside if it is not standard (i.e. its tag is layout-defined). + if (!secInfo.abstract.empty() && !hasStandardAbstract) { + // Assert: style.docbookforceabstracttag() != NONE. + xs << xml::StartTag(style.docbookforceabstracttag()); + xs << xml::CR(); + for (auto const &p : secInfo.abstract) + makeAny(text, buf, xs, ourparams, paragraphs.iterator_at(p)); + xs << xml::EndTag(style.docbookforceabstracttag()); + xs << xml::CR(); + } + + // Skip all the text that has just been generated. + par = paragraphs.iterator_at(secInfo.epit); + } else { + // No tag to generate, proceed as for normal paragraphs. + par = makeAny(text, buf, xs, ourparams, par); + } + } else { + // Generate this paragraph, as it has nothing special. + par = makeAny(text, buf, xs, ourparams, par); } }