]> git.lyx.org Git - features.git/blobdiff - src/output_docbook.cpp
Fix broken Apple speller interface
[features.git] / src / output_docbook.cpp
index 519d70e5975dcd99af783fb3ad8c47fdc303eb67..5e1f6d12cf2829fc281a1cd95268d9967249270f 100644 (file)
@@ -175,6 +175,10 @@ void openParTag(XMLStream & xs, const Paragraph * par, const Paragraph * prevpar
        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
@@ -221,6 +225,10 @@ void closeParTag(XMLStream & xs, Paragraph const * par, Paragraph const * nextpa
        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" && !runparams.docbook_ignore_wrapper;
@@ -278,9 +286,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<docstring> pars_prepend;
+       std::vector<docstring> pars;
+       std::vector<docstring> 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");
@@ -327,7 +343,7 @@ void makeParagraph(
        // We do not really want to wrap that whole thing in a <div>...</div>.
        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;
@@ -342,56 +358,78 @@ void makeParagraph(
        size_t nInsets = std::distance(par->insetList().begin(), par->insetList().end());
        auto parSize = (size_t) par->size();
 
-       // If this LyX code does not produce any output, it can be safely ignored in the following checks: if this thing
-       // is present in the paragraph, it has no impact on the definition of the special case (i.e. whether or not
-       // a <para> tag should be output).
-       auto isLyxCodeToIgnore = [](InsetCode x) { return x == TOC_CODE || x == NOTE_CODE; };
-
-       // TODO: if a paragraph *only* contains floats, listings, bibliographies, etc., should this be considered as a
-       //  special case? If so, the code could be largely simplifies (all the calls to all_of, basically) and optimised
-       //  at the compilation stage.
-
        // 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).
        // Exception: any case that generates an <inlineequation> must still get a paragraph to be valid.
-       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), [](InsetList::Element inset) {
+       auto isEquationSpecialCase = [](InsetList::Element inset) {
                return inset.inset && inset.inset->asInsetMath() && inset.inset->asInsetMath()->getType() != hullSimple;
-       });
-       // Tables do not deserve their own paragraphs (DocBook allows them outside paragraphs).
-       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), [isLyxCodeToIgnore](InsetList::Element inset) {
-               return inset.inset->lyxCode() == TABULAR_CODE || isLyxCodeToIgnore(inset.inset->lyxCode());
-       });
-       // Floats cannot be in paragraphs.
-       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), [isLyxCodeToIgnore](InsetList::Element inset) {
-               return inset.inset->lyxCode() == FLOAT_CODE || isLyxCodeToIgnore(inset.inset->lyxCode());
-       });
-       // Bibliographies cannot be in paragraphs. Bibitems should still be handled as paragraphs, though
-       // (see makeParagraphBibliography).
-       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), [isLyxCodeToIgnore](InsetList::Element inset) {
-               return inset.inset->lyxCode() == BIBTEX_CODE || isLyxCodeToIgnore(inset.inset->lyxCode());
-       });
-       // ERTs are in comments, not paragraphs.
-       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), [isLyxCodeToIgnore](InsetList::Element inset) {
-               return inset.inset->lyxCode() == ERT_CODE || isLyxCodeToIgnore(inset.inset->lyxCode());
-       });
-       // Listings should not get into their own paragraph.
-       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), [isLyxCodeToIgnore](InsetList::Element inset) {
-               return inset.inset->lyxCode() == LISTINGS_CODE || isLyxCodeToIgnore(inset.inset->lyxCode());
-       });
-       // Boxes cannot get into their own paragraph.
-       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), [isLyxCodeToIgnore](InsetList::Element inset) {
-               return inset.inset->lyxCode() == BOX_CODE || isLyxCodeToIgnore(inset.inset->lyxCode());
-       });
-       // Includes should not have a paragraph.
-       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), [isLyxCodeToIgnore](InsetList::Element inset) {
-               return inset.inset->lyxCode() == INCLUDE_CODE || isLyxCodeToIgnore(inset.inset->lyxCode());
-       });
-       // Glossaries should not have a paragraph.
-       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), [isLyxCodeToIgnore](InsetList::Element inset) {
-               return inset.inset->lyxCode() == NOMENCL_PRINT_CODE || isLyxCodeToIgnore(inset.inset->lyxCode());
-       });
+       };
+       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<InsetCode> 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;
+
+               // 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;
+               }
+
+               // No case matched: give up.
+               return false;
+       };
+       special_case |= nInsets == parSize && std::all_of(par->insetList().begin(), par->insetList().end(), isFlexSpecialCase);
+
+       // 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;
@@ -410,7 +448,14 @@ 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<docstring> pars_prepend;
+       std::vector<docstring> pars;
+       std::vector<docstring> 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))
                        continue;
@@ -423,6 +468,8 @@ void makeParagraph(
                if (close_par)
                        closeParTag(xs, &*par, (nextpar != end) ? &*nextpar : nullptr, runparams);
        }
+       for (docstring const & parXML : pars_append)
+           xs << XMLStream::ESCAPE_NONE << parXML;
 }
 
 
@@ -458,8 +505,13 @@ 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<docstring> pars_prepend;
+        std::vector<docstring> pars;
+        std::vector<docstring> 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()) {
@@ -482,6 +534,8 @@ void makeEnvironment(Text const &text,
                                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);
        }
@@ -582,14 +636,21 @@ ParagraphList::const_iterator makeListEnvironment(Text const &text,
 
                // Generate the content of the item.
                if (sep < par->size()) {
-                       auto pars = par->simpleDocBookOnePar(buf, runparams,
+            std::vector<docstring> pars_prepend;
+            std::vector<docstring> pars;
+            std::vector<docstring> 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) {
                                xml::openTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(),
                                             par->layout().docbookiteminnertagtype());
                                xs << XMLStream::ESCAPE_NONE << p;
                                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.
                        xml::compTag(xs, par->layout().docbookiteminnertag(), par->layout().docbookiteminnerattr(),
@@ -634,14 +695,23 @@ void makeCommand(
 
        // Generate this command.
        auto prevpar = text.paragraphs().getParagraphBefore(par);
-       openParTag(xs, &*par, prevpar, runparams);
 
-       auto pars = par->simpleDocBookOnePar(buf, runparams,text.outerFont(distance(begin, par)));
+    std::vector<docstring> pars_prepend;
+    std::vector<docstring> pars;
+    std::vector<docstring> 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, runparams);
+    for (docstring const & parXML : pars_append)
+        xs << XMLStream::ESCAPE_NONE << parXML;
 }
 
 
@@ -684,6 +754,8 @@ DocBookDocumentSectioning hasDocumentSectioning(ParagraphList const &paragraphs,
        bool documentHasSections = false;
 
        while (bpit < epit) {
+               LASSERT(static_cast<size_t>(bpit) < paragraphs.size(), return make_tuple(documentHasSections, bpit));
+
                Layout const &style = paragraphs[bpit].layout();
                documentHasSections |= isLayoutSectioningOrSimilar(style);
 
@@ -730,7 +802,7 @@ DocBookInfoTag getParagraphsWithInfo(ParagraphList const &paragraphs,
        set<pit_type> abstractWithLayout;
        set<pit_type> 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))
@@ -754,7 +826,7 @@ DocBookInfoTag getParagraphsWithInfo(ParagraphList const &paragraphs,
                // There should never be any section here, except for the first paragraph (a title can be part of <info>).
                // (Just a sanity check: if this fails, this function could end up processing the whole document.)
                if (cpit != bpit && isLayoutSectioningOrSimilar(par.layout())) {
-                       LYXERR0("Assertion failed: section found in potential <info> paragraphs.");
+                       LYXERR(Debug::OUTFILE, "Assertion failed: section found in potential <info> paragraphs.");
                        break;
                }
 
@@ -948,12 +1020,20 @@ void outputDocBookInfo(
                        }
                }
 
-               // 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.
+               // 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("<!--"))) != lyx::docstring::npos) {
+                       if ((endComment = 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;
@@ -1063,9 +1143,6 @@ void docbookParagraphs(Text const &text,
                                return;
                        });
 
-       std::stack<std::pair<int, string>> 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;
@@ -1083,21 +1160,77 @@ void docbookParagraphs(Text const &text,
        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 <preface>, that's the closest in meaning.
+       // This is only useful if the things after the <info> 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<std::string> 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<std::pair<int, string>> 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 <section> and/or </section>s.