]> git.lyx.org Git - features.git/blobdiff - src/insets/InsetIndex.cpp
Handle empty index subentries (#7820)
[features.git] / src / insets / InsetIndex.cpp
index bcae16cd5c6835d55a2f479e8ddc877c1faeb2c1..b3024bf74cfd05f55070089d11a16e25d2d7d61e 100644 (file)
@@ -29,6 +29,7 @@
 #include "LaTeX.h"
 #include "LaTeXFeatures.h"
 #include "Lexer.h"
+#include "LyX.h"
 #include "output_latex.h"
 #include "output_xhtml.h"
 #include "xml.h"
@@ -54,6 +55,9 @@
 using namespace std;
 using namespace lyx::support;
 
+// Uncomment to enable InsetIndex-specific debugging mode: the tree for the index will be printed to std::cout.
+// #define LYX_INSET_INDEX_DEBUG
+
 namespace lyx {
 
 namespace {
@@ -156,11 +160,15 @@ void InsetIndex::latex(otexstream & ios, OutputParams const & runparams_in) cons
                getSortkey(os, runparams);
                os << "@";
                os << ourlatex.str();
-               getSubentries(os, runparams);
+               getSubentries(os, runparams, ourlatex.str());
                if (hasSeeRef()) {
                        os << "|";
                        os << insetindexpagerangetranslator_latex().find(params_.range);
                        getSeeRefs(os, runparams);
+               } else if (!params_.pagefmt.empty() && params_.pagefmt != "default") {
+                       os << "|";
+                       os << insetindexpagerangetranslator_latex().find(params_.range);
+                       os << from_utf8(params_.pagefmt);
                }
        } else {
                // We check whether we need a sort key.
@@ -206,7 +214,7 @@ void InsetIndex::latex(otexstream & ios, OutputParams const & runparams_in) cons
 
                odocstringstream subentries;
                otexstream otsub(subentries);
-               getSubentries(otsub, runparams);
+               getSubentries(otsub, runparams, ourlatex.str());
                if (subentries.str().empty()) {
                        // Separate the entries and subentries, i.e., split on "!".
                        // This goes wrong on an escaped "!", but as the escape
@@ -222,6 +230,12 @@ void InsetIndex::latex(otexstream & ios, OutputParams const & runparams_in) cons
                        vector<docstring>::const_iterator it2 = levels_plain.begin();
                        bool first = true;
                        for (; it != end; ++it) {
+                               if ((*it).empty()) {
+                                       emptySubentriesWarning(ourlatex.str());
+                                       if (it2 < levels_plain.end())
+                                               ++it2;
+                                       continue;
+                               }
                                // The separator needs to be put back when
                                // writing the levels, except for the first level
                                if (!first)
@@ -320,14 +334,6 @@ void InsetIndex::docbook(XMLStream & xs, OutputParams const & runparams) const
        InsetText::latex(ots, runparams);
        docstring latexString = trim(odss.str());
 
-       // Check whether there are unsupported things. @ is supported, but only for sorting, without specific formatting.
-       if (latexString.find(from_utf8("@\\")) != lyx::docstring::npos) {
-               docstring error = from_utf8("Unsupported feature: an index entry contains an @\\. "
-                                                                       "Complete entry: \"") + latexString + from_utf8("\"");
-               LYXERR0(error);
-               xs << XMLStream::ESCAPE_NONE << (from_utf8("<!-- Output Error: ") + error + from_utf8(" -->\n"));
-       }
-
        // Handle several indices (indicated in the inset instead of the raw latexString).
        docstring indexType = from_utf8("");
        if (buffer().masterBuffer()->params().use_indices) {
@@ -368,10 +374,10 @@ void InsetIndex::docbook(XMLStream & xs, OutputParams const & runparams) const
        }
 
        // Handle ranges. Happily, in the raw LaTeX mode, (| and |) can only be at the end of the string!
-       bool hasInsetRange = params_.range != InsetIndexParams::PageRange::None;
-       bool hasStartRange = params_.range == InsetIndexParams::PageRange::Start ||
+       const bool hasInsetRange = params_.range != InsetIndexParams::PageRange::None;
+       const bool hasStartRange = params_.range == InsetIndexParams::PageRange::Start ||
                        latexString.find(from_ascii("|(")) != lyx::docstring::npos;
-       bool hasEndRange = params_.range == InsetIndexParams::PageRange::End ||
+       const bool hasEndRange = params_.range == InsetIndexParams::PageRange::End ||
                        latexString.find(from_ascii("|)")) != lyx::docstring::npos;
 
        if (hasInsetRange) {
@@ -437,7 +443,7 @@ void InsetIndex::docbook(XMLStream & xs, OutputParams const & runparams) const
                xs << XMLStream::ESCAPE_NONE << (from_utf8("<!-- Output Error: ") + error + from_utf8(" -->\n"));
        }
 
-    // Write all of this down.
+       // Write all of this down.
        if (terms.empty() && !hasEndRange) {
                docstring error = from_utf8("No index term found! Complete entry: \"") + latexString + from_utf8("\"");
                LYXERR0(error);
@@ -583,6 +589,14 @@ void InsetIndex::doDispatch(Cursor & cur, FuncRequest & cmd)
                cur.bv().updateDialog("index", params2string(params_));
                break;
 
+       case LFUN_PARAGRAPH_BREAK: {
+               // Since this inset in single-par anyway, let's use
+               // return to enter subentries
+               FuncRequest fr(LFUN_INDEXMACRO_INSERT, "subentry");
+               lyx::dispatch(fr);
+               break;
+       }
+
        default:
                InsetCollapsible::doDispatch(cur, cmd);
                break;
@@ -614,6 +628,9 @@ bool InsetIndex::getStatus(Cursor & cur, FuncRequest const & cmd,
                return true;
        }
        
+       case LFUN_PARAGRAPH_BREAK:
+               return macrosPossible("subentry");
+       
        case LFUN_INDEXMACRO_INSERT:
                return macrosPossible(cmd.getArg(0));
 
@@ -657,7 +674,22 @@ docstring InsetIndex::getSortkeyAsText(OutputParams const & runparams) const
 }
 
 
-void InsetIndex::getSubentries(otexstream & os, OutputParams const & runparams) const
+void InsetIndex::emptySubentriesWarning(docstring const & mainentry) const
+{
+       // Empty subentries crash makeindex. So warn and ignore this.
+       TeXErrors terr;
+       ErrorList & errorList = buffer().errorList("Export");
+       docstring const s = bformat(_("There is an empty index subentry in the entry '%1$s'.\n"
+                                     "It will be ignored in the output."), mainentry);
+       Paragraph const & par = buffer().paragraphs().front();
+       errorList.push_back(ErrorItem(_("Empty index subentry!"), s,
+                                     {par.id(), 0}, {par.id(), -1}));
+       buffer().bufferErrors(terr, errorList);
+}
+
+
+void InsetIndex::getSubentries(otexstream & os, OutputParams const & runparams,
+                              docstring const & mainentry) const
 {
        Paragraph const & par = paragraphs().front();
        InsetList::const_iterator it = par.insetList().begin();
@@ -667,7 +699,11 @@ void InsetIndex::getSubentries(otexstream & os, OutputParams const & runparams)
                if (inset.lyxCode() == INDEXMACRO_CODE) {
                        InsetIndexMacro const & iim =
                                static_cast<InsetIndexMacro const &>(inset);
-                       if (iim.params().type == InsetIndexMacroParams::Subindex) {
+                       if (iim.params().type == InsetIndexMacroParams::Subentry) {
+                               if (iim.hasNoContent()) {
+                                       emptySubentriesWarning(mainentry);
+                                       continue;
+                               }
                                ++i;
                                if (i > 2)
                                        return;
@@ -679,7 +715,8 @@ void InsetIndex::getSubentries(otexstream & os, OutputParams const & runparams)
 }
 
 
-std::vector<docstring> InsetIndex::getSubentriesAsText(OutputParams const & runparams) const
+std::vector<docstring> InsetIndex::getSubentriesAsText(OutputParams const & runparams,
+                                                      bool const asLabel) const
 {
        std::vector<docstring> subentries;
 
@@ -691,14 +728,19 @@ std::vector<docstring> InsetIndex::getSubentriesAsText(OutputParams const & runp
                if (inset.lyxCode() == INDEXMACRO_CODE) {
                        InsetIndexMacro const & iim =
                                static_cast<InsetIndexMacro const &>(inset);
-                       if (iim.params().type == InsetIndexMacroParams::Subindex) {
+                       if (iim.params().type == InsetIndexMacroParams::Subentry) {
                                ++i;
                                if (i > 2)
                                        break;
-
-                               otexstringstream os;
-                               iim.getLatex(os, runparams);
-                               subentries.emplace_back(os.str());
+                               if (asLabel) {
+                                       docstring const l;
+                                       docstring const sl = iim.getNewLabel(l);
+                                       subentries.emplace_back(sl);
+                               } else {
+                                       otexstringstream os;
+                                       iim.getLatex(os, runparams);
+                                       subentries.emplace_back(os.str());
+                               }
                        }
                }
        }
@@ -805,7 +847,7 @@ bool hasInsetWithCode(const InsetIndex * const inset_index, const InsetCode code
 
 bool InsetIndex::hasSubentries() const
 {
-       return hasInsetWithCode(this, INDEXMACRO_CODE, {InsetIndexMacroParams::Subindex});
+       return hasInsetWithCode(this, INDEXMACRO_CODE, {InsetIndexMacroParams::Subentry});
 }
 
 
@@ -824,7 +866,7 @@ bool InsetIndex::hasSortKey() const
 bool InsetIndex::macrosPossible(string const type) const
 {
        if (type != "see" && type != "seealso"
-           && type != "sortkey" && type != "subindex")
+           && type != "sortkey" && type != "subentry")
                return false;
 
        Paragraph const & par = paragraphs().front();
@@ -840,8 +882,8 @@ bool InsetIndex::macrosPossible(string const type) const
                             && (iim.params().type == InsetIndexMacroParams::See
                                 || iim.params().type == InsetIndexMacroParams::Seealso))
                                return false;
-                       if (type == "subindex"
-                            && iim.params().type == InsetIndexMacroParams::Subindex) {
+                       if (type == "subentry"
+                            && iim.params().type == InsetIndexMacroParams::Subentry) {
                                ++subidxs;
                                if (subidxs > 1)
                                        return false;
@@ -918,8 +960,15 @@ docstring const InsetIndex::buttonLabel(BufferView const & bv) const
        docstring res;
        if (!il.contentaslabel() || geometry(bv) != ButtonOnly)
                res = label;
-       else
+       else {
                res = getNewLabel(label);
+               OutputParams const rp(0);
+               vector<docstring> sublbls = getSubentriesAsText(rp, true);
+               for (auto const & sublbl : sublbls) {
+                       res += " " + docstring(1, char_type(0x2023));// TRIANGULAR BULLET
+                       res += " " + sublbl;
+               }
+       }
        if (!insetindexpagerangetranslator_latex().find(params_.range).empty())
                res += " " + from_ascii(insetindexpagerangetranslator_latex().find(params_.range));
        return res;
@@ -971,11 +1020,22 @@ void InsetIndex::addToToc(DocIterator const & cpit, bool output_active,
        DocIterator pit = cpit;
        pit.push_back(CursorSlice(const_cast<InsetIndex &>(*this)));
        docstring str;
+       InsetLayout const & il = getLayout();
+       docstring label = translateIfPossible(il.labelstring());
+       if (!il.contentaslabel())
+               str = label;
+       else {
+               str = getNewLabel(label);
+               OutputParams const rp(0);
+               vector<docstring> sublbls = getSubentriesAsText(rp, true);
+               for (auto const & sublbl : sublbls) {
+                       str += " " + docstring(1, char_type(0x2023));// TRIANGULAR BULLET
+                       str += " " + sublbl;
+               }
+       }
        string type = "index";
        if (buffer().masterBuffer()->params().use_indices)
                type += ":" + to_utf8(params_.index);
-       // this is unlikely to be terribly long
-       text().forOutliner(str, INT_MAX);
        TocBuilder & b = backend.builder(type);
        b.pushItem(pit, str, output_active);
        // Proceed with the rest of the inset.
@@ -1484,7 +1544,7 @@ bool operator<(IndexEntry const & lhs, IndexEntry const & rhs)
        if (lhs.terms_.empty())
                return false;
 
-       for (int i = 0; i < min(rhs.terms_.size(), lhs.terms_.size()); ++i) {
+       for (unsigned i = 0; i < min(rhs.terms_.size(), lhs.terms_.size()); ++i) {
                int comp = compare_no_case(lhs.terms_[i], rhs.terms_[i]);
                if (comp != 0)
                        return comp < 0;
@@ -1574,7 +1634,7 @@ IndexNode* buildIndexTree(vector<IndexEntry>& entries)
        return index_root;
 }
 
-void outputIndexPage(XMLStream & xs, const IndexNode* root_node, unsigned depth = 0)
+void outputIndexPage(XMLStream & xs, const IndexNode* root_node, unsigned depth = 0) // NOLINT(misc-no-recursion)
 {
        LASSERT(root_node->entries.size() + root_node->children.size() > 0, return);
 
@@ -1584,16 +1644,49 @@ void outputIndexPage(XMLStream & xs, const IndexNode* root_node, unsigned depth
        // By tree assumption, all the entries at this node have the same set of terms.
 
        if (!root_node->entries.empty()) {
-               xs << XMLStream::ESCAPE_NONE << " &#8212; ";
+               xs << XMLStream::ESCAPE_NONE << " &#8212; "; // Em dash, i.e. long (---).
                unsigned entry_number = 1;
 
-               for (unsigned i = 0; i < root_node->entries.size(); ++i) {
-                       const IndexEntry &entry = root_node->entries[i];
-
+               auto writeLinkToEntry = [&xs](const IndexEntry &entry, unsigned entry_number) {
                        std::string const link_attr = "href='#" + entry.inset()->paragraphs()[0].magicLabel() + "'";
                        xs << xml::StartTag("a", link_attr);
                        xs << from_ascii(std::to_string(entry_number));
                        xs << xml::EndTag("a");
+               };
+
+               for (unsigned i = 0; i < root_node->entries.size(); ++i) {
+                       const IndexEntry &entry = root_node->entries[i];
+
+                       switch (entry.inset()->params().range) {
+                               case InsetIndexParams::PageRange::None:
+                                       writeLinkToEntry(entry, entry_number);
+                                       break;
+                               case InsetIndexParams::PageRange::Start: {
+                                       // Try to find the end of the range, if it is just after. Otherwise, the output will be slightly
+                                       // scrambled, but understandable. Doing better would mean implementing more of the indexing logic here
+                                       // and more complex indexing here (skipping the end is not just incrementing i). Worst case output:
+                                       //     1--, 2, --3
+                                       const bool nextEntryIsEnd = i + 1 < root_node->entries.size() &&
+                                                                   root_node->entries[i + 1].inset()->params().range ==
+                                                                   InsetIndexParams::PageRange::End;
+                                       // No need to check if both entries are for the same terms: they are in the same IndexNode.
+
+                                       writeLinkToEntry(entry, entry_number);
+                                       xs << XMLStream::ESCAPE_NONE << " &#8211; "; // En dash, i.e. semi-long (--).
+
+                                       if (nextEntryIsEnd) {
+                                               // Skip the next entry in the loop, write it right now, after the dash.
+                                               entry_number += 1;
+                                               i += 1;
+                                               writeLinkToEntry(root_node->entries[i], entry_number);
+                                       }
+                               }
+                                       break;
+                               case InsetIndexParams::PageRange::End:
+                                       // This range end was not caught by the range start, do it now to avoid losing content.
+                                       xs << XMLStream::ESCAPE_NONE << " &#8211; "; // En dash, i.e. semi-long (--).
+                                       writeLinkToEntry(root_node->entries[i], entry_number);
+                       }
 
                        if (i < root_node->entries.size() - 1) {
                                xs << ", ";
@@ -1622,7 +1715,7 @@ void outputIndexPage(XMLStream & xs, const IndexNode* root_node, unsigned depth
        xs << xml::CR();
 }
 
-// Only useful for debugging.
+#ifdef LYX_INSET_INDEX_DEBUG
 void printTree(const IndexNode* root_node, unsigned depth = 0)
 {
        static const std::string pattern = "    ";
@@ -1657,6 +1750,7 @@ void printTree(const IndexNode* root_node, unsigned depth = 0)
                printTree(node, depth + 1);
        }
 }
+#endif // LYX_INSET_INDEX_DEBUG
 }
 
 
@@ -1664,24 +1758,17 @@ docstring InsetPrintIndex::xhtml(XMLStream &, OutputParams const & op) const
 {
        BufferParams const & bp = buffer().masterBuffer()->params();
 
-       // we do not presently support multiple indices, so we refuse to print
-       // anything but the main index, so as not to generate multiple indices.
-       // NOTE Multiple index support would require some work. The reason
-       // is that the TOC does not know about multiple indices. Either it would
-       // need to be told about them (not a bad idea), or else the index entries
-       // would need to be collected differently, say, during validation.
-       if (bp.use_indices && getParam("type") != from_ascii("idx"))
-               return docstring();
-
        shared_ptr<Toc const> toc = buffer().tocBackend().toc("index");
        if (toc->empty())
                return docstring();
 
        // Collect the index entries in a form we can use them.
        vector<IndexEntry> entries;
+       const docstring & indexType = params().getParamOr("type", from_ascii("idx"));
        for (const TocItem& item : *toc) {
-               if (item.isOutput())
-                       entries.emplace_back(IndexEntry{static_cast<const InsetIndex*>(&(item.dit().inset())), &op});
+               const auto* inset = static_cast<const InsetIndex*>(&(item.dit().inset()));
+               if (item.isOutput() && inset->params().index == indexType)
+                       entries.emplace_back(IndexEntry{inset, &op});
        }
 
        // If all the index entries are in notes or not displayed, get out sooner.
@@ -1689,7 +1776,7 @@ docstring InsetPrintIndex::xhtml(XMLStream &, OutputParams const & op) const
                return docstring();
 
        const IndexNode* index_root = buildIndexTree(entries);
-#if 0
+#ifdef LYX_INSET_INDEX_DEBUG
        printTree(index_root);
 #endif
 
@@ -1697,6 +1784,7 @@ docstring InsetPrintIndex::xhtml(XMLStream &, OutputParams const & op) const
        Layout const & lay = bp.documentClass().htmlTOCLayout();
        string const & tocclass = lay.defaultCSSClass();
        string const tocattr = "class='index " + tocclass + "'";
+       docstring const indexName = params().getParamOr("name", from_ascii("Index"));
 
        // we'll use our own stream, because we are going to defer everything.
        // that's how we deal with the fact that we're probably inside a standard
@@ -1707,7 +1795,7 @@ docstring InsetPrintIndex::xhtml(XMLStream &, OutputParams const & op) const
        xs << xml::StartTag("div", tocattr);
        xs << xml::CR();
        xs << xml::StartTag(lay.htmltag(), lay.htmlattr());
-       xs << translateIfPossible(from_ascii("Index"), op.local_font->language()->lang());
+       xs << translateIfPossible(indexName, op.local_font->language()->lang());
        xs << xml::EndTag(lay.htmltag());
        xs << xml::CR();
        xs << xml::StartTag("ul", "class='main'");