]> git.lyx.org Git - lyx.git/blob - src/insets/InsetIndex.cpp
Update Win installer for new dictionary links. Untested.
[lyx.git] / src / insets / InsetIndex.cpp
1 /**
2  * \file InsetIndex.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Lars Gullik Bjønnes
7  * \author Jürgen Spitzmüller
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11 #include <config.h>
12
13 #include "InsetIndex.h"
14 #include "InsetIndexMacro.h"
15
16 #include "Buffer.h"
17 #include "BufferParams.h"
18 #include "BufferView.h"
19 #include "ColorSet.h"
20 #include "Cursor.h"
21 #include "CutAndPaste.h"
22 #include "DispatchResult.h"
23 #include "Encoding.h"
24 #include "ErrorList.h"
25 #include "FuncRequest.h"
26 #include "FuncStatus.h"
27 #include "IndicesList.h"
28 #include "InsetList.h"
29 #include "Language.h"
30 #include "Paragraph.h"
31 #include "LaTeX.h"
32 #include "LaTeXFeatures.h"
33 #include "Lexer.h"
34 #include "LyX.h"
35 #include "output_latex.h"
36 #include "output_xhtml.h"
37 #include "xml.h"
38 #include "texstream.h"
39 #include "TextClass.h"
40 #include "TocBackend.h"
41
42 #include "support/debug.h"
43 #include "support/docstream.h"
44 #include "support/FileName.h"
45 #include "support/gettext.h"
46 #include "support/lstrings.h"
47 #include "support/Translator.h"
48
49 #include "frontends/alert.h"
50
51 #include <algorithm>
52 #include <set>
53 #include <iostream>
54
55 #include <QThreadStorage>
56
57 using namespace std;
58 using namespace lyx::support;
59
60 // Uncomment to enable InsetIndex-specific debugging mode: the tree for the index will be printed to std::cout.
61 // #define LYX_INSET_INDEX_DEBUG
62
63 namespace lyx {
64
65 namespace {
66
67 typedef Translator<string, InsetIndexParams::PageRange> PageRangeTranslator;
68 typedef Translator<docstring, InsetIndexParams::PageRange> PageRangeTranslatorLoc;
69
70 PageRangeTranslator const init_insetindexpagerangetranslator()
71 {
72         PageRangeTranslator translator("none", InsetIndexParams::None);
73         translator.addPair("start", InsetIndexParams::Start);
74         translator.addPair("end", InsetIndexParams::End);
75         return translator;
76 }
77
78 PageRangeTranslator const init_insetindexpagerangetranslator_latex()
79 {
80         PageRangeTranslator translator("", InsetIndexParams::None);
81         translator.addPair("(", InsetIndexParams::Start);
82         translator.addPair(")", InsetIndexParams::End);
83         return translator;
84 }
85
86
87 PageRangeTranslatorLoc const init_insetindexpagerangetranslator_loc()
88 {
89         PageRangeTranslatorLoc translator(docstring(), InsetIndexParams::None);
90         translator.addPair(_("Starts page range"), InsetIndexParams::Start);
91         translator.addPair(_("Ends page range"), InsetIndexParams::End);
92         return translator;
93 }
94
95
96 PageRangeTranslator const & insetindexpagerangetranslator()
97 {
98         static PageRangeTranslator const prtranslator =
99                         init_insetindexpagerangetranslator();
100         return prtranslator;
101 }
102
103
104 PageRangeTranslatorLoc const & insetindexpagerangetranslator_loc()
105 {
106         static PageRangeTranslatorLoc const translator =
107                         init_insetindexpagerangetranslator_loc();
108         return translator;
109 }
110
111
112 PageRangeTranslator const & insetindexpagerangetranslator_latex()
113 {
114         static PageRangeTranslator const lttranslator =
115                         init_insetindexpagerangetranslator_latex();
116         return lttranslator;
117 }
118
119 } // namespace anon
120
121 /////////////////////////////////////////////////////////////////////
122 //
123 // InsetIndex
124 //
125 ///////////////////////////////////////////////////////////////////////
126
127
128 InsetIndex::InsetIndex(Buffer * buf, InsetIndexParams const & params)
129         : InsetCollapsible(buf), params_(params)
130 {}
131
132
133 void InsetIndex::latex(otexstream & ios, OutputParams const & runparams_in) const
134 {
135         OutputParams runparams(runparams_in);
136         runparams.inIndexEntry = true;
137         if (runparams_in.postpone_fragile_stuff)
138                 // This is not needed and would impact sorting
139                 runparams.moving_arg = false;
140
141         otexstringstream os;
142
143         if (buffer().masterBuffer()->params().use_indices && !params_.index.empty()
144                 && params_.index != "idx") {
145                 os << "\\sindex[";
146                 os << escape(params_.index);
147                 os << "]{";
148         } else {
149                 os << "\\index";
150                 os << '{';
151         }
152
153         // Get the LaTeX output from InsetText. We need to deconstruct this later
154         // in order to check if we need to generate a sorting key
155         odocstringstream ourlatex;
156         otexstream ots(ourlatex);
157         InsetText::latex(ots, runparams);
158         if (runparams.find_effective()) {
159                 // No need for special handling, if we are only searching for some patterns
160                 os << ourlatex.str() << "}";
161                 return;
162         }
163
164         if (hasSortKey()) {
165                 getSortkey(os, runparams);
166                 os << "@";
167                 os << ourlatex.str();
168                 getSubentries(os, runparams, ourlatex.str());
169                 if (hasSeeRef()) {
170                         os << "|";
171                         os << insetindexpagerangetranslator_latex().find(params_.range);
172                         getSeeRefs(os, runparams);
173                 } else if (!params_.pagefmt.empty() && params_.pagefmt != "default") {
174                         os << "|";
175                         os << insetindexpagerangetranslator_latex().find(params_.range);
176                         os << from_utf8(params_.pagefmt);
177                 } else if (params_.range != InsetIndexParams::PageRange::None) {
178                         os << "|";
179                         os << insetindexpagerangetranslator_latex().find(params_.range);
180                 }
181         } else {
182                 // We check whether we need a sort key.
183                 // If so, we use the plaintext version
184                 odocstringstream ourplain;
185                 InsetText::plaintext(ourplain, runparams);
186
187                 // These are the LaTeX and plaintext representations
188                 docstring latexstr = ourlatex.str();
189                 docstring plainstr = ourplain.str();
190         
191                 // This will get what follows | if anything does,
192                 // the command (e.g., see, textbf) for pagination
193                 // formatting
194                 docstring cmd;
195
196                 if (hasSeeRef()) {
197                         odocstringstream seeref;
198                         otexstream otsee(seeref);
199                         getSeeRefs(otsee, runparams);
200                         cmd = seeref.str();
201                 } else if (!params_.pagefmt.empty() && params_.pagefmt != "default") {
202                         cmd = from_utf8(params_.pagefmt);
203                 } else {
204                         // Check for the | separator to strip the cmd.
205                         // This goes wrong on an escaped "|", but as the escape
206                         // character can be changed in style files, we cannot
207                         // prevent that.
208                         size_t pos = latexstr.find(from_ascii("|"));
209                         if (pos != docstring::npos) {
210                                 // Put the bit after "|" into cmd...
211                                 cmd = latexstr.substr(pos + 1);
212                                 // ...and erase that stuff from latexstr
213                                 latexstr = latexstr.erase(pos);
214                                 // ...as well as from plainstr
215                                 size_t ppos = plainstr.find(from_ascii("|"));
216                                 if (ppos < plainstr.size())
217                                         plainstr.erase(ppos);
218                                 else
219                                         LYXERR0("The `|' separator was not found in the plaintext version!");
220                         }
221                 }
222
223                 odocstringstream subentries;
224                 otexstream otsub(subentries);
225                 getSubentries(otsub, runparams, ourlatex.str());
226                 if (subentries.str().empty()) {
227                         // Separate the entries and subentries, i.e., split on "!".
228                         // This goes wrong on an escaped "!", but as the escape
229                         // character can be changed in style files, we cannot
230                         // prevent that.
231                         std::vector<docstring> const levels =
232                                         getVectorFromString(latexstr, from_ascii("!"), true);
233                         std::vector<docstring> const levels_plain =
234                                         getVectorFromString(plainstr, from_ascii("!"), true);
235                 
236                         vector<docstring>::const_iterator it = levels.begin();
237                         vector<docstring>::const_iterator end = levels.end();
238                         vector<docstring>::const_iterator it2 = levels_plain.begin();
239                         bool first = true;
240                         for (; it != end; ++it) {
241                                 if ((*it).empty()) {
242                                         emptySubentriesWarning(ourlatex.str());
243                                         if (it2 < levels_plain.end())
244                                                 ++it2;
245                                         continue;
246                                 }
247                                 // The separator needs to be put back when
248                                 // writing the levels, except for the first level
249                                 if (!first)
250                                         os << '!';
251                                 else
252                                         first = false;
253                 
254                                 // Now here comes the reason for this whole procedure:
255                                 // We try to correctly sort macros and formatted strings.
256                                 // If we find a command, prepend a plain text
257                                 // version of the content to get sorting right,
258                                 // e.g. \index{LyX@\LyX}, \index{text@\textbf{text}}.
259                                 // We do this on all levels.
260                                 // We don't do it if the level already contains a '@', though.
261                                 // Plaintext might return nothing (e.g. for ERTs).
262                                 // In that case, we use LaTeX.
263                                 docstring const spart = (levels_plain.empty() || (*it2).empty()) ? *it : *it2;
264                                 processLatexSorting(os, runparams, *it, spart);
265                                 if (it2 < levels_plain.end())
266                                         ++it2;
267                         }
268                 } else {
269                         processLatexSorting(os, runparams, latexstr, plainstr);
270                         os << subentries.str();
271                 }
272
273                 // At last, re-insert the command, separated by "|"
274                 if (!cmd.empty()) {
275                         os << "|"
276                            << insetindexpagerangetranslator_latex().find(params_.range)
277                            << cmd;
278                 } else if (params_.range != InsetIndexParams::PageRange::None) {
279                         os << "|";
280                         os << insetindexpagerangetranslator_latex().find(params_.range);
281                 }
282         }
283         os << '}';
284
285         // In macros with moving arguments, such as \section,
286         // we store the index and output it after the macro (#2154)
287         if (runparams_in.postpone_fragile_stuff)
288                 runparams_in.post_macro += os.str();
289         else
290                 ios << os.release();
291 }
292
293
294 void InsetIndex::processLatexSorting(otexstream & os, OutputParams const & runparams,
295                                 docstring const & latex, docstring const & spart) const
296 {
297         if (contains(latex, '\\') && !contains(latex, '@')) {
298                 // Now we need to validate that all characters in
299                 // the sorting part are representable in the current
300                 // encoding. If not try the LaTeX macro which might
301                 // or might not be a good choice, and issue a warning.
302                 pair<docstring, docstring> spart_latexed =
303                                 runparams.encoding->latexString(spart, runparams.dryrun);
304                 if (!spart_latexed.second.empty())
305                         LYXERR0("Uncodable character in index entry. Sorting might be wrong!");
306                 if (spart != spart_latexed.first && !runparams.dryrun) {
307                         TeXErrors terr;
308                         ErrorList & errorList = buffer().errorList("Export");
309                         docstring const s = bformat(_("LyX's automatic index sorting algorithm faced "
310                                                       "problems with the entry '%1$s'.\n"
311                                                       "Please specify the sorting of this entry manually, as "
312                                                       "explained in the User Guide."), spart);
313                         Paragraph const & par = buffer().paragraphs().front();
314                         errorList.push_back(ErrorItem(_("Index sorting failed"), s,
315                                                       {par.id(), 0}, {par.id(), -1}));
316                         buffer().bufferErrors(terr, errorList);
317                 }
318                 // Remove remaining \'s from the sort key
319                 docstring ppart = subst(spart_latexed.first, from_ascii("\\"), docstring());
320                 // Plain quotes need to be escaped, however (#10649), as this
321                 // is the default escape character
322                 ppart = subst(ppart, from_ascii("\""), from_ascii("\\\""));
323
324                 // Now insert the sortkey, separated by '@'.
325                 os << ppart;
326                 os << '@';
327         }
328         // Insert the actual level text
329         os << latex;
330 }
331
332
333 void InsetIndex::docbook(XMLStream & xs, OutputParams const & runparams) const
334 {
335         // Two ways of processing this inset are implemented:
336         // - the legacy one, based on parsing the raw LaTeX (before LyX 2.4) -- unlikely to be deprecated
337         // - the modern one, based on precise insets for indexing features
338         // Like the LaTeX implementation, consider the user chooses either of those options.
339
340         // Get the content of the inset as LaTeX, as some things may be encoded as ERT (like {}).
341         // TODO: if there is an ERT within the index term, its conversion should be tried, in case it becomes useful;
342         //  otherwise, ERTs should become comments. For now, they are just copied as-is, which is barely satisfactory.
343         odocstringstream odss;
344         otexstream ots(odss);
345         InsetText::latex(ots, runparams);
346         docstring latexString = trim(odss.str());
347
348         // Handle several indices (indicated in the inset instead of the raw latexString).
349         docstring indexType = from_utf8("");
350         if (buffer().masterBuffer()->params().use_indices) {
351                 indexType += " type=\"" + params_.index + "\"";
352         }
353
354         // Split the string into its main constituents: terms, and command (see, see also, range).
355         size_t positionVerticalBar = latexString.find(from_ascii("|")); // What comes before | is (sub)(sub)entries.
356         docstring indexTerms = latexString.substr(0, positionVerticalBar);
357         docstring command;
358         if (positionVerticalBar != lyx::docstring::npos) {
359                 command = latexString.substr(positionVerticalBar + 1);
360         }
361
362         // Handle sorting issues, with @.
363         docstring sortAs;
364         if (hasSortKey()) {
365                 sortAs = getSortkeyAsText(runparams);
366                 // indexTerms may contain a sort key if the user has both the inset and the manual key.
367         } else {
368                 vector<docstring> sortingElements = getVectorFromString(indexTerms, from_ascii("@"), false);
369                 if (sortingElements.size() == 2) {
370                         sortAs = sortingElements[0];
371                         indexTerms = sortingElements[1];
372                 }
373         }
374
375         // Handle primary, secondary, and tertiary terms (entries, subentries, and subsubentries, for LaTeX).
376         vector<docstring> terms;
377         const vector<docstring> potential_terms = getSubentriesAsText(runparams);
378         if (!potential_terms.empty()) {
379                 terms = potential_terms;
380                 // The main term is not present in the vector, as it's not a subentry. The main index term is inserted raw in
381                 // the index inset. Considering that the user either uses the new or the legacy mechanism, the main term is the
382                 // full string within this inset (i.e. without the subinsets).
383                 terms.insert(terms.begin(), latexString);
384         } else {
385                 terms = getVectorFromString(indexTerms, from_ascii("!"), false);
386         }
387
388         // Handle ranges. Happily, in the raw LaTeX mode, (| and |) can only be at the end of the string!
389         // Handle both modern ranges (params_.range) and legacy ones (with a suffix |( or |) as in pure LaTeX).
390         const bool hasInsetRange = params_.range != InsetIndexParams::PageRange::None ||
391                         latexString.find(from_ascii("|(")) != lyx::docstring::npos ||
392                         latexString.find(from_ascii("|)")) != lyx::docstring::npos;
393         const bool hasStartRange = params_.range == InsetIndexParams::PageRange::Start ||
394                         latexString.find(from_ascii("|(")) != lyx::docstring::npos;
395         const bool hasEndRange = params_.range == InsetIndexParams::PageRange::End ||
396                         latexString.find(from_ascii("|)")) != lyx::docstring::npos;
397
398         if (hasInsetRange) {
399                 // Remove the ranges from the command if they do not appear at the beginning.
400                 size_t index = 0;
401                 while ((index = command.find(from_utf8("|("), index)) != std::string::npos)
402                         command.erase(index, 1);
403                 index = 0;
404                 while ((index = command.find(from_utf8("|)"), index)) != std::string::npos)
405                         command.erase(index, 1);
406
407                 // Remove the ranges when they are the only vertical bar in the complete string.
408                 if (command[0] == '(' || command[0] == ')')
409                         command.erase(0, 1);
410         }
411
412         // Handle see and seealso. As "see" is a prefix of "seealso", the order of the comparisons is important.
413         // Both commands are mutually exclusive!
414         docstring see = getSeeAsText(runparams);
415         vector<docstring> seeAlsoes = getSeeAlsoesAsText(runparams);
416
417         if (see.empty() && seeAlsoes.empty() && command.substr(0, 3) == "see") {
418                 // Unescape brackets.
419                 size_t index = 0;
420                 while ((index = command.find(from_utf8("\\{"), index)) != std::string::npos)
421                         command.erase(index, 1);
422                 index = 0;
423                 while ((index = command.find(from_utf8("\\}"), index)) != std::string::npos)
424                         command.erase(index, 1);
425
426                 // Retrieve the part between brackets, and remove the complete seealso.
427                 size_t positionOpeningBracket = command.find(from_ascii("{"));
428                 size_t positionClosingBracket = command.find(from_ascii("}"));
429                 docstring list = command.substr(positionOpeningBracket + 1, positionClosingBracket - positionOpeningBracket - 1);
430
431                 // Parse the list of referenced entries (or a single one for see).
432                 if (command.substr(0, 7) == "seealso") {
433                         seeAlsoes = getVectorFromString(list, from_ascii(","), false);
434                 } else {
435                         see = list;
436
437                         if (see.find(from_ascii(",")) != std::string::npos) {
438                                 docstring error = from_utf8("Several index terms found as \"see\"! Only one is acceptable. "
439                                                                                         "Complete entry: \"") + latexString + from_utf8("\"");
440                                 LYXERR0(error);
441                                 xs << XMLStream::ESCAPE_NONE << (from_utf8("<!-- Output Error: ") + error + from_utf8(" -->\n"));
442                         }
443                 }
444
445                 // Remove the complete see/seealso from the commands, in case there is something else to parse.
446                 command = command.substr(positionClosingBracket + 1);
447         }
448
449         // Some parts of the strings are not parsed, as they do not have anything matching in DocBook: things like
450         // formatting the entry or the page number, other strings for sorting. https://wiki.lyx.org/Tips/Indexing
451         // If there are such things in the index entry, then this code may miserably fail. For example, for "Peter|(textbf",
452         // no range will be detected.
453         // TODO: Could handle formatting as significance="preferred"?
454         if (!command.empty()) {
455                 docstring error = from_utf8("Unsupported feature: an index entry contains a | with an unsupported command, ")
456                                           + command + from_utf8(". Complete entry: \"") + latexString + from_utf8("\"");
457                 LYXERR0(error);
458                 xs << XMLStream::ESCAPE_NONE << (from_utf8("<!-- Output Error: ") + error + from_utf8(" -->\n"));
459         }
460
461         // Write all of this down.
462         if (terms.empty() && !hasEndRange) {
463                 docstring error = from_utf8("No index term found! Complete entry: \"") + latexString + from_utf8("\"");
464                 LYXERR0(error);
465                 xs << XMLStream::ESCAPE_NONE << (from_utf8("<!-- Output Error: ") + error + from_utf8(" -->\n"));
466         } else {
467                 // Generate the attributes for ranges. It is based on the terms that are indexed, but the ID must be unique
468                 // to this indexing area (xml::cleanID does not guarantee this: for each call with the same arguments,
469                 // the same legal ID is produced; here, as the input would be the same, the output must be, by design).
470                 // Hence the thread-local storage, as the numbers must strictly be unique, and thus cannot be shared across
471                 // a paragraph (making the solution used for HTML worthless). This solution is very similar to the one used in
472                 // xml::cleanID.
473                 // indexType can only be used for singular and startofrange types!
474                 docstring attrs;
475                 if (!hasStartRange && !hasEndRange) {
476                         attrs = indexType;
477                 } else {
478                         // Append an ID if uniqueness is not guaranteed across the document.
479                         static QThreadStorage<set<docstring>> tKnownTermLists;
480                         static QThreadStorage<int> tID;
481
482                         set<docstring> &knownTermLists = tKnownTermLists.localData();
483                         int &ID = tID.localData();
484
485                         if (!tID.hasLocalData()) {
486                                 tID.localData() = 0;
487                         }
488
489                         // Modify the index terms to add the unique ID if needed.
490                         docstring newIndexTerms = indexTerms;
491                         if (knownTermLists.find(indexTerms) != knownTermLists.end()) {
492                                 newIndexTerms += from_ascii(string("-") + to_string(ID));
493
494                                 // Only increment for the end of range, so that the same number is used for the start of range.
495                                 if (hasEndRange) {
496                                         ID++;
497                                 }
498                         }
499
500                         // Term list not yet known: add it to the set AFTER the end of range. After
501                         if (knownTermLists.find(indexTerms) == knownTermLists.end() && hasEndRange) {
502                                 knownTermLists.insert(indexTerms);
503                         }
504
505                         // Generate the attributes.
506                         docstring id = xml::cleanID(newIndexTerms);
507                         if (hasStartRange) {
508                                 attrs = indexType + R"( class="startofrange" xml:id=")" + id + "\"";
509                         } else {
510                                 attrs = R"( class="endofrange" startref=")" + id + "\"";
511                         }
512                 }
513
514                 // Handle the index terms (including the specific index for this entry).
515                 if (hasEndRange) {
516                         xs << xml::CompTag("indexterm", attrs);
517                 } else {
518                         xs << xml::StartTag("indexterm", attrs);
519                         if (!terms.empty()) { // hasEndRange has no content.
520                                 docstring attr;
521                                 if (!sortAs.empty()) {
522                                         attr = from_utf8("sortas='") + sortAs + from_utf8("'");
523                                 }
524
525                                 xs << xml::StartTag("primary", attr);
526                                 xs << terms[0];
527                                 xs << xml::EndTag("primary");
528                         }
529                         if (terms.size() > 1) {
530                                 xs << xml::StartTag("secondary");
531                                 xs << terms[1];
532                                 xs << xml::EndTag("secondary");
533                         }
534                         if (terms.size() > 2) {
535                                 xs << xml::StartTag("tertiary");
536                                 xs << terms[2];
537                                 xs << xml::EndTag("tertiary");
538                         }
539
540                         // Handle see and see also.
541                         if (!see.empty()) {
542                                 xs << xml::StartTag("see");
543                                 xs << see;
544                                 xs << xml::EndTag("see");
545                         }
546
547                         if (!seeAlsoes.empty()) {
548                                 for (auto &entry : seeAlsoes) {
549                                         xs << xml::StartTag("seealso");
550                                         xs << entry;
551                                         xs << xml::EndTag("seealso");
552                                 }
553                         }
554
555                         // Close the entry.
556                         xs << xml::EndTag("indexterm");
557                 }
558         }
559 }
560
561
562 docstring InsetIndex::xhtml(XMLStream & xs, OutputParams const &) const
563 {
564         // we just print an anchor, taking the paragraph ID from
565         // our own interior paragraph, which doesn't get printed
566         std::string const magic = paragraphs().front().magicLabel();
567         std::string const attr = "id='" + magic + "'";
568         xs << xml::CompTag("a", attr);
569         return docstring();
570 }
571
572
573 bool InsetIndex::showInsetDialog(BufferView * bv) const
574 {
575         bv->showDialog("index", params2string(params_),
576                         const_cast<InsetIndex *>(this));
577         return true;
578 }
579
580
581 void InsetIndex::doDispatch(Cursor & cur, FuncRequest & cmd)
582 {
583         switch (cmd.action()) {
584
585         case LFUN_INSET_MODIFY: {
586                 if (cmd.getArg(0) == "changetype") {
587                         cur.recordUndoInset(this);
588                         params_.index = from_utf8(cmd.getArg(1));
589                         break;
590                 }
591                 if (cmd.getArg(0) == "changeparam") {
592                         string const p = cmd.getArg(1);
593                         string const v = cmd.getArg(2);
594                         cur.recordUndoInset(this);
595                         if (p == "range")
596                                 params_.range = insetindexpagerangetranslator().find(v);
597                         if (p == "pagefmt") {
598                                 if (v == "default" || v == "textbf"
599                                     || v == "textit" || v == "emph")
600                                         params_.pagefmt = v;
601                                 else
602                                         lyx::dispatch(FuncRequest(LFUN_INSET_SETTINGS, "index"));
603                         }
604                         break;
605                 }
606                 InsetIndexParams params;
607                 InsetIndex::string2params(to_utf8(cmd.argument()), params);
608                 cur.recordUndoInset(this);
609                 params_.index = params.index;
610                 params_.range = params.range;
611                 params_.pagefmt = params.pagefmt;
612                 // what we really want here is a TOC update, but that means
613                 // a full buffer update
614                 cur.forceBufferUpdate();
615                 break;
616         }
617
618         case LFUN_INSET_DIALOG_UPDATE:
619                 cur.bv().updateDialog("index", params2string(params_));
620                 break;
621
622         case LFUN_PARAGRAPH_BREAK: {
623                 // Since this inset in single-par anyway, let's use
624                 // return to enter subentries
625                 FuncRequest fr(LFUN_INDEXMACRO_INSERT, "subentry");
626                 lyx::dispatch(fr);
627                 break;
628         }
629
630         case LFUN_INSET_INSERT_COPY: {
631                 Cursor & bvcur = cur.bv().cursor();
632                 if (cmd.origin() == FuncRequest::TOC && bvcur.inTexted()) {
633                         cap::copyInsetToTemp(cur, clone());
634                         cap::pasteFromTemp(bvcur, bvcur.buffer()->errorList("Paste"));
635                 } else
636                         cur.undispatched();
637                 break;
638         }
639
640         default:
641                 InsetCollapsible::doDispatch(cur, cmd);
642                 break;
643         }
644 }
645
646
647 bool InsetIndex::getStatus(Cursor & cur, FuncRequest const & cmd,
648                 FuncStatus & flag) const
649 {
650         switch (cmd.action()) {
651
652         case LFUN_INSET_MODIFY:
653                 if (cmd.getArg(0) == "changetype") {
654                         docstring const newtype = from_utf8(cmd.getArg(1));
655                         Buffer const & realbuffer = *buffer().masterBuffer();
656                         IndicesList const & indiceslist = realbuffer.params().indiceslist();
657                         Index const * index = indiceslist.findShortcut(newtype);
658                         flag.setEnabled(index != 0);
659                         flag.setOnOff(
660                                 from_utf8(cmd.getArg(1)) == params_.index);
661                         return true;
662                 }
663                 if (cmd.getArg(0) == "changeparam") {
664                         string const p = cmd.getArg(1);
665                         string const v = cmd.getArg(2);
666                         if (p == "range") {
667                                 flag.setEnabled(v == "none" || v == "start" || v == "end");
668                                 flag.setOnOff(params_.range == insetindexpagerangetranslator().find(v));
669                         }
670                         if (p == "pagefmt") {
671                                 flag.setEnabled(!v.empty());
672                                 if (params_.pagefmt == "default" || params_.pagefmt == "textbf"
673                                     || params_.pagefmt == "textit" || params_.pagefmt == "emph")
674                                         flag.setOnOff(params_.pagefmt == v);
675                                 else
676                                         flag.setOnOff(v == "custom");
677                         }
678                         return true;
679                 }
680                 return InsetCollapsible::getStatus(cur, cmd, flag);
681
682         case LFUN_INSET_DIALOG_UPDATE: {
683                 Buffer const & realbuffer = *buffer().masterBuffer();
684                 flag.setEnabled(realbuffer.params().use_indices);
685                 return true;
686         }
687
688         case LFUN_INSET_INSERT_COPY:
689                 // This can only be invoked by ToC widget
690                 flag.setEnabled(cmd.origin() == FuncRequest::TOC
691                                 && cur.bv().cursor().inset().insetAllowed(lyxCode()));
692                 return true;
693
694         case LFUN_PARAGRAPH_BREAK:
695                 return macrosPossible("subentry");
696
697         case LFUN_INDEXMACRO_INSERT:
698                 return macrosPossible(cmd.getArg(0));
699
700         case LFUN_INDEX_TAG_ALL: {
701                 if (cur.pos() == 0)
702                         // nothing to tag
703                         return false;
704                 // move backwards into preceding word
705                 // skip over other index insets
706                 DocIterator dit(cur);
707                 dit.backwardPosIgnoreCollapsed();
708                 while (true) {
709                         if (dit.inset().lyxCode() == INDEX_CODE)
710                                 dit.pop_back();
711                         else if (dit.prevInset() && dit.prevInset()->lyxCode() == INDEX_CODE)
712                                 dit.backwardPosIgnoreCollapsed();
713                         else
714                                 break;
715                 }
716                 if (!dit.inTexted())
717                         // action not possible
718                         return false;
719                 // Check if we actually have a word to tag
720                 FontSpan tw = dit.locateWord(WHOLE_WORD);
721
722                 // action possible if we have a word of at least one char
723                 return (tw.size() > 0);
724         }
725
726         default:
727                 return InsetCollapsible::getStatus(cur, cmd, flag);
728         }
729 }
730
731
732 void InsetIndex::getSortkey(otexstream & os, OutputParams const & runparams) const
733 {
734         Paragraph const & par = paragraphs().front();
735         InsetList::const_iterator it = par.insetList().begin();
736         for (; it != par.insetList().end(); ++it) {
737                 Inset & inset = *it->inset;
738                 if (inset.lyxCode() == INDEXMACRO_SORTKEY_CODE) {
739                         InsetIndexMacro const & iim =
740                                 static_cast<InsetIndexMacro const &>(inset);
741                         iim.getLatex(os, runparams);
742                         return;
743                 }
744         }
745 }
746
747
748 docstring InsetIndex::getSortkeyAsText(OutputParams const & runparams) const
749 {
750         Paragraph const & par = paragraphs().front();
751         InsetList::const_iterator it = par.insetList().begin();
752         for (; it != par.insetList().end(); ++it) {
753                 Inset & inset = *it->inset;
754                 if (inset.lyxCode() == INDEXMACRO_SORTKEY_CODE) {
755                         otexstringstream os;
756                         InsetIndexMacro const & iim =
757                                 static_cast<InsetIndexMacro const &>(inset);
758                         iim.getLatex(os, runparams);
759                         return os.str();
760                 }
761         }
762         return from_ascii("");
763 }
764
765
766 void InsetIndex::emptySubentriesWarning(docstring const & mainentry) const
767 {
768         // Empty subentries crash makeindex. So warn and ignore this.
769         TeXErrors terr;
770         ErrorList & errorList = buffer().errorList("Export");
771         docstring const s = bformat(_("There is an empty index subentry in the entry '%1$s'.\n"
772                                       "It will be ignored in the output."), mainentry);
773         Paragraph const & par = buffer().paragraphs().front();
774         errorList.push_back(ErrorItem(_("Empty index subentry!"), s,
775                                       {par.id(), 0}, {par.id(), -1}));
776         buffer().bufferErrors(terr, errorList);
777 }
778
779
780 void InsetIndex::getSubentries(otexstream & os, OutputParams const & runparams,
781                                docstring const & mainentry) const
782 {
783         Paragraph const & par = paragraphs().front();
784         InsetList::const_iterator it = par.insetList().begin();
785         int i = 0;
786         for (; it != par.insetList().end(); ++it) {
787                 Inset & inset = *it->inset;
788                 if (inset.lyxCode() == INDEXMACRO_CODE) {
789                         InsetIndexMacro const & iim =
790                                 static_cast<InsetIndexMacro const &>(inset);
791                         if (iim.params().type == InsetIndexMacroParams::Subentry) {
792                                 if (iim.hasNoContent()) {
793                                         emptySubentriesWarning(mainentry);
794                                         continue;
795                                 }
796                                 ++i;
797                                 if (i > 2)
798                                         return;
799                                 os << "!";
800                                 iim.getLatex(os, runparams);
801                         }
802                 }
803         }
804 }
805
806
807 std::vector<docstring> InsetIndex::getSubentriesAsText(OutputParams const & runparams,
808                                                        bool const asLabel) const
809 {
810         std::vector<docstring> subentries;
811
812         Paragraph const & par = paragraphs().front();
813         InsetList::const_iterator it = par.insetList().begin();
814         int i = 0;
815         for (; it != par.insetList().end(); ++it) {
816                 Inset & inset = *it->inset;
817                 if (inset.lyxCode() == INDEXMACRO_CODE) {
818                         InsetIndexMacro const & iim =
819                                 static_cast<InsetIndexMacro const &>(inset);
820                         if (iim.params().type == InsetIndexMacroParams::Subentry) {
821                                 ++i;
822                                 if (i > 2)
823                                         break;
824                                 if (asLabel) {
825                                         docstring const l;
826                                         docstring const sl = iim.getNewLabel(l);
827                                         subentries.emplace_back(sl);
828                                 } else {
829                                         otexstringstream os;
830                                         iim.getLatex(os, runparams);
831                                         subentries.emplace_back(os.str());
832                                 }
833                         }
834                 }
835         }
836
837         return subentries;
838 }
839
840
841 docstring InsetIndex::getMainSubentryAsText(OutputParams const & runparams) const
842 {
843         otexstringstream os;
844         InsetText::latex(os, runparams);
845         return os.str();
846 }
847
848
849 void InsetIndex::getSeeRefs(otexstream & os, OutputParams const & runparams) const
850 {
851         Paragraph const & par = paragraphs().front();
852         InsetList::const_iterator it = par.insetList().begin();
853         for (; it != par.insetList().end(); ++it) {
854                 Inset & inset = *it->inset;
855                 if (inset.lyxCode() == INDEXMACRO_CODE) {
856                         InsetIndexMacro const & iim =
857                                 static_cast<InsetIndexMacro const &>(inset);
858                         if (iim.params().type == InsetIndexMacroParams::See
859                             || iim.params().type == InsetIndexMacroParams::Seealso) {
860                                 iim.getLatex(os, runparams);
861                                 return;
862                         }
863                 }
864         }
865 }
866
867
868 docstring InsetIndex::getSeeAsText(OutputParams const & runparams,
869                                    bool const asLabel) const
870 {
871         Paragraph const & par = paragraphs().front();
872         InsetList::const_iterator it = par.insetList().begin();
873         for (; it != par.insetList().end(); ++it) {
874                 Inset & inset = *it->inset;
875                 if (inset.lyxCode() == INDEXMACRO_CODE) {
876                         InsetIndexMacro const & iim =
877                                 static_cast<InsetIndexMacro const &>(inset);
878                         if (iim.params().type == InsetIndexMacroParams::See) {
879                                 if (asLabel) {
880                                         docstring const l;
881                                         return iim.getNewLabel(l);
882                                 } else {
883                                         otexstringstream os;
884                                         iim.getLatex(os, runparams);
885                                         return os.str();
886                                 }
887                         }
888                 }
889         }
890         return from_ascii("");
891 }
892
893
894 std::vector<docstring> InsetIndex::getSeeAlsoesAsText(OutputParams const & runparams,
895                                                       bool const asLabel) const
896 {
897         std::vector<docstring> seeAlsoes;
898
899         Paragraph const & par = paragraphs().front();
900         InsetList::const_iterator it = par.insetList().begin();
901         for (; it != par.insetList().end(); ++it) {
902                 Inset & inset = *it->inset;
903                 if (inset.lyxCode() == INDEXMACRO_CODE) {
904                         InsetIndexMacro const & iim =
905                                 static_cast<InsetIndexMacro const &>(inset);
906                         if (iim.params().type == InsetIndexMacroParams::Seealso) {
907                                 if (asLabel) {
908                                         docstring const l;
909                                         seeAlsoes.emplace_back(iim.getNewLabel(l));
910                                 } else {
911                                         otexstringstream os;
912                                         iim.getLatex(os, runparams);
913                                         seeAlsoes.emplace_back(os.str());
914                                 }
915                         }
916                 }
917         }
918
919         return seeAlsoes;
920 }
921
922
923 namespace {
924
925 bool hasInsetWithCode(const InsetIndex * const inset_index, const InsetCode code,
926                                           const std::set<InsetIndexMacroParams::Type> types = {})
927 {
928         Paragraph const & par = inset_index->paragraphs().front();
929         InsetList::const_iterator it = par.insetList().begin();
930         for (; it != par.insetList().end(); ++it) {
931                 Inset & inset = *it->inset;
932                 if (inset.lyxCode() == code) {
933                         if (types.empty())
934                                 return true;
935
936                         LASSERT(code == INDEXMACRO_CODE, return false);
937                         InsetIndexMacro const & iim =
938                                         static_cast<InsetIndexMacro const &>(inset);
939                         if (types.find(iim.params().type) != types.end())
940                                 return true;
941                 }
942         }
943         return false;
944 }
945
946 } // namespace
947
948
949 bool InsetIndex::hasSubentries() const
950 {
951         return hasInsetWithCode(this, INDEXMACRO_CODE, {InsetIndexMacroParams::Subentry});
952 }
953
954
955 bool InsetIndex::hasSeeRef() const
956 {
957         return hasInsetWithCode(this, INDEXMACRO_CODE, {InsetIndexMacroParams::See, InsetIndexMacroParams::Seealso});
958 }
959
960
961 bool InsetIndex::hasSortKey() const
962 {
963         return hasInsetWithCode(this, INDEXMACRO_SORTKEY_CODE);
964 }
965
966
967 bool InsetIndex::macrosPossible(string const type) const
968 {
969         if (type != "see" && type != "seealso"
970             && type != "sortkey" && type != "subentry")
971                 return false;
972
973         Paragraph const & par = paragraphs().front();
974         InsetList::const_iterator it = par.insetList().begin();
975         int subidxs = 0;
976         for (; it != par.insetList().end(); ++it) {
977                 Inset & inset = *it->inset;
978                 if (type == "sortkey" && inset.lyxCode() == INDEXMACRO_SORTKEY_CODE)
979                         return false;
980                 if (inset.lyxCode() == INDEXMACRO_CODE) {
981                         InsetIndexMacro const & iim = static_cast<InsetIndexMacro const &>(inset);
982                         if ((type == "see" || type == "seealso")
983                              && (iim.params().type == InsetIndexMacroParams::See
984                                  || iim.params().type == InsetIndexMacroParams::Seealso))
985                                 return false;
986                         if (type == "subentry"
987                              && iim.params().type == InsetIndexMacroParams::Subentry) {
988                                 ++subidxs;
989                                 if (subidxs > 1)
990                                         return false;
991                         }
992                 }
993         }
994         return true;
995 }
996
997
998 ColorCode InsetIndex::labelColor() const
999 {
1000         if (params_.index.empty() || params_.index == from_ascii("idx"))
1001                 return InsetCollapsible::labelColor();
1002         // FIXME UNICODE
1003         ColorCode c = lcolor.getFromLyXName(to_utf8(params_.index)
1004                                             + "@" + buffer().fileName().absFileName());
1005         if (c == Color_none)
1006                 c = InsetCollapsible::labelColor();
1007         return c;
1008 }
1009
1010
1011 docstring InsetIndex::toolTip(BufferView const &, int, int) const
1012 {
1013         docstring tip = _("Index Entry");
1014         if (buffer().params().use_indices && !params_.index.empty()) {
1015                 Buffer const & realbuffer = *buffer().masterBuffer();
1016                 IndicesList const & indiceslist = realbuffer.params().indiceslist();
1017                 tip += " (";
1018                 Index const * index = indiceslist.findShortcut(params_.index);
1019                 if (!index)
1020                         tip += _("unknown type!");
1021                 else
1022                         tip += index->index();
1023                 tip += ")";
1024         }
1025         tip += ": ";
1026         docstring res = toolTipText(tip);
1027         if (!insetindexpagerangetranslator_loc().find(params_.range).empty())
1028                 res += "\n" + insetindexpagerangetranslator_loc().find(params_.range);
1029         if (!params_.pagefmt.empty() && params_.pagefmt != "default") {
1030                 res += "\n" + _("Pagination format:") + " ";
1031                 if (params_.pagefmt == "textbf")
1032                         res += _("bold");
1033                 else if (params_.pagefmt == "textit")
1034                         res += _("italic");
1035                 else if (params_.pagefmt == "emph")
1036                         res += _("emphasized");
1037                 else
1038                         res += from_utf8(params_.pagefmt);
1039         }
1040         return res;
1041 }
1042
1043
1044 docstring const InsetIndex::buttonLabel(BufferView const & bv) const
1045 {
1046         InsetLayout const & il = getLayout();
1047         docstring label = translateIfPossible(il.labelstring());
1048
1049         if (buffer().params().use_indices && !params_.index.empty()) {
1050                 Buffer const & realbuffer = *buffer().masterBuffer();
1051                 IndicesList const & indiceslist = realbuffer.params().indiceslist();
1052                 label += " (";
1053                 Index const * index = indiceslist.findShortcut(params_.index);
1054                 if (!index)
1055                         label += _("unknown type!");
1056                 else
1057                         label += index->index();
1058                 label += ")";
1059         }
1060
1061         docstring res;
1062         if (!il.contentaslabel() || geometry(bv) != ButtonOnly)
1063                 res = label;
1064         else {
1065                 res = getNewLabel(label);
1066                 OutputParams const rp(0);
1067                 vector<docstring> sublbls = getSubentriesAsText(rp, true);
1068                 for (auto const & sublbl : sublbls) {
1069                         res += " " + docstring(1, char_type(0x2023));// TRIANGULAR BULLET
1070                         res += " " + sublbl;
1071                 }
1072                 docstring see = getSeeAsText(rp, true);
1073                 if (see.empty() && !getSeeAlsoesAsText(rp, true).empty())
1074                         see = getSeeAlsoesAsText(rp, true).front();
1075                 if (!see.empty()) {
1076                         res += " " + docstring(1, char_type(0x261e));// WHITE RIGHT POINTING INDEX
1077                         res += " " + see;
1078                 }
1079         }
1080         if (!insetindexpagerangetranslator_latex().find(params_.range).empty())
1081                 res += " " + from_ascii(insetindexpagerangetranslator_latex().find(params_.range));
1082         return res;
1083 }
1084
1085
1086 void InsetIndex::write(ostream & os) const
1087 {
1088         os << to_utf8(layoutName());
1089         params_.write(os);
1090         InsetCollapsible::write(os);
1091 }
1092
1093
1094 void InsetIndex::read(Lexer & lex)
1095 {
1096         params_.read(lex);
1097         InsetCollapsible::read(lex);
1098 }
1099
1100
1101 string InsetIndex::params2string(InsetIndexParams const & params)
1102 {
1103         ostringstream data;
1104         data << "index";
1105         params.write(data);
1106         return data.str();
1107 }
1108
1109
1110 void InsetIndex::string2params(string const & in, InsetIndexParams & params)
1111 {
1112         params = InsetIndexParams();
1113         if (in.empty())
1114                 return;
1115
1116         istringstream data(in);
1117         Lexer lex;
1118         lex.setStream(data);
1119         lex.setContext("InsetIndex::string2params");
1120         lex >> "index";
1121         params.read(lex);
1122 }
1123
1124
1125 void InsetIndex::addToToc(DocIterator const & cpit, bool output_active,
1126                                                   UpdateType utype, TocBackend & backend) const
1127 {
1128         DocIterator pit = cpit;
1129         pit.push_back(CursorSlice(const_cast<InsetIndex &>(*this)));
1130         docstring str;
1131         InsetLayout const & il = getLayout();
1132         docstring label = translateIfPossible(il.labelstring());
1133         if (!il.contentaslabel())
1134                 str = label;
1135         else {
1136                 str = getNewLabel(label);
1137                 OutputParams const rp(0);
1138                 vector<docstring> sublbls = getSubentriesAsText(rp, true);
1139                 for (auto const & sublbl : sublbls) {
1140                         str += " " + docstring(1, char_type(0x2023));// TRIANGULAR BULLET
1141                         str += " " + sublbl;
1142                 }
1143                 docstring see = getSeeAsText(rp, true);
1144                 if (see.empty() && !getSeeAlsoesAsText(rp, true).empty())
1145                         see = getSeeAlsoesAsText(rp, true).front();
1146                 if (!see.empty()) {
1147                         str += " " + docstring(1, char_type(0x261e));// WHITE RIGHT POINTING INDEX
1148                         str += " " + see;
1149                 }
1150         }
1151         string type = "index";
1152         if (buffer().masterBuffer()->params().use_indices)
1153                 type += ":" + to_utf8(params_.index);
1154         TocBuilder & b = backend.builder(type);
1155         b.pushItem(pit, str, output_active);
1156         // Proceed with the rest of the inset.
1157         InsetCollapsible::addToToc(cpit, output_active, utype, backend);
1158         b.pop();
1159 }
1160
1161
1162 void InsetIndex::validate(LaTeXFeatures & features) const
1163 {
1164         if (buffer().masterBuffer()->params().use_indices
1165             && !params_.index.empty()
1166             && params_.index != "idx")
1167                 features.require("splitidx");
1168         InsetCollapsible::validate(features);
1169 }
1170
1171
1172 string InsetIndex::contextMenuName() const
1173 {
1174         return "context-index";
1175 }
1176
1177
1178 string InsetIndex::contextMenu(BufferView const & bv, int x, int y) const
1179 {
1180         // We override the implementation of InsetCollapsible,
1181         // because we have extra entries.
1182         string owncm = "context-edit-index;";
1183         return owncm + InsetCollapsible::contextMenu(bv, x, y);
1184 }
1185
1186
1187 bool InsetIndex::hasSettings() const
1188 {
1189         return true;
1190 }
1191
1192
1193 bool InsetIndex::insetAllowed(InsetCode code) const
1194 {
1195         switch (code) {
1196         case INDEXMACRO_CODE:
1197         case INDEXMACRO_SORTKEY_CODE:
1198                 return true;
1199         case INDEX_CODE:
1200                 return false;
1201         default:
1202                 return InsetCollapsible::insetAllowed(code);
1203         }
1204 }
1205
1206
1207 /////////////////////////////////////////////////////////////////////
1208 //
1209 // InsetIndexParams
1210 //
1211 ///////////////////////////////////////////////////////////////////////
1212
1213
1214 void InsetIndexParams::write(ostream & os) const
1215 {
1216         os << ' ';
1217         if (!index.empty())
1218                 os << to_utf8(index);
1219         else
1220                 os << "idx";
1221         os << '\n';
1222         os << "range "
1223            << insetindexpagerangetranslator().find(range)
1224            << '\n';
1225         os << "pageformat "
1226            << pagefmt
1227            << '\n';
1228 }
1229
1230
1231 void InsetIndexParams::read(Lexer & lex)
1232 {
1233         if (lex.eatLine())
1234                 index = lex.getDocString();
1235         else
1236                 index = from_ascii("idx");
1237         if (lex.checkFor("range")) {
1238                 string st = lex.getString();
1239                 if (lex.eatLine()) {
1240                         st = lex.getString();
1241                         range = insetindexpagerangetranslator().find(lex.getString());
1242                 }
1243         }
1244         if (lex.checkFor("pageformat") && lex.eatLine()) {
1245                 pagefmt = lex.getString();
1246         }
1247 }
1248
1249
1250 /////////////////////////////////////////////////////////////////////
1251 //
1252 // InsetPrintIndex
1253 //
1254 ///////////////////////////////////////////////////////////////////////
1255
1256 InsetPrintIndex::InsetPrintIndex(Buffer * buf, InsetCommandParams const & p)
1257         : InsetCommand(buf, p)
1258 {}
1259
1260
1261 ParamInfo const & InsetPrintIndex::findInfo(string const & /* cmdName */)
1262 {
1263         static ParamInfo param_info_;
1264         if (param_info_.empty()) {
1265                 param_info_.add("type", ParamInfo::LATEX_OPTIONAL,
1266                                 ParamInfo::HANDLING_ESCAPE);
1267                 param_info_.add("name", ParamInfo::LATEX_OPTIONAL,
1268                                 ParamInfo::HANDLING_LATEXIFY);
1269                 param_info_.add("literal", ParamInfo::LYX_INTERNAL);
1270         }
1271         return param_info_;
1272 }
1273
1274
1275 docstring InsetPrintIndex::screenLabel() const
1276 {
1277         bool const printall = suffixIs(getCmdName(), '*');
1278         bool const multind = buffer().masterBuffer()->params().use_indices;
1279         if ((!multind
1280              && getParam("type") == from_ascii("idx"))
1281             || (getParam("type").empty() && !printall))
1282                 return _("Index");
1283         Buffer const & realbuffer = *buffer().masterBuffer();
1284         IndicesList const & indiceslist = realbuffer.params().indiceslist();
1285         Index const * index = indiceslist.findShortcut(getParam("type"));
1286         if (!index && !printall)
1287                 return _("Unknown index type!");
1288         docstring res = printall ? _("All indexes") : index->index();
1289         if (!multind)
1290                 res += " (" + _("non-active") + ")";
1291         else if (contains(getCmdName(), "printsubindex"))
1292                 res += " (" + _("subindex") + ")";
1293         return res;
1294 }
1295
1296
1297 bool InsetPrintIndex::isCompatibleCommand(string const & s)
1298 {
1299         return s == "printindex" || s == "printsubindex"
1300                 || s == "printindex*" || s == "printsubindex*";
1301 }
1302
1303
1304 void InsetPrintIndex::doDispatch(Cursor & cur, FuncRequest & cmd)
1305 {
1306         switch (cmd.action()) {
1307
1308         case LFUN_INSET_MODIFY: {
1309                 if (cmd.argument() == from_ascii("toggle-subindex")) {
1310                         string scmd = getCmdName();
1311                         if (contains(scmd, "printindex"))
1312                                 scmd = subst(scmd, "printindex", "printsubindex");
1313                         else
1314                                 scmd = subst(scmd, "printsubindex", "printindex");
1315                         cur.recordUndo();
1316                         setCmdName(scmd);
1317                         break;
1318                 } else if (cmd.argument() == from_ascii("check-printindex*")) {
1319                         string scmd = getCmdName();
1320                         if (suffixIs(scmd, '*'))
1321                                 break;
1322                         scmd += '*';
1323                         cur.recordUndo();
1324                         setParam("type", docstring());
1325                         setCmdName(scmd);
1326                         break;
1327                 }
1328                 InsetCommandParams p(INDEX_PRINT_CODE);
1329                 // FIXME UNICODE
1330                 InsetCommand::string2params(to_utf8(cmd.argument()), p);
1331                 if (p.getCmdName().empty()) {
1332                         cur.noScreenUpdate();
1333                         break;
1334                 }
1335                 cur.recordUndo();
1336                 setParams(p);
1337                 break;
1338         }
1339
1340         default:
1341                 InsetCommand::doDispatch(cur, cmd);
1342                 break;
1343         }
1344 }
1345
1346
1347 bool InsetPrintIndex::getStatus(Cursor & cur, FuncRequest const & cmd,
1348         FuncStatus & status) const
1349 {
1350         switch (cmd.action()) {
1351
1352         case LFUN_INSET_MODIFY: {
1353                 if (cmd.argument() == from_ascii("toggle-subindex")) {
1354                         status.setEnabled(buffer().masterBuffer()->params().use_indices);
1355                         status.setOnOff(contains(getCmdName(), "printsubindex"));
1356                         return true;
1357                 } else if (cmd.argument() == from_ascii("check-printindex*")) {
1358                         status.setEnabled(buffer().masterBuffer()->params().use_indices);
1359                         status.setOnOff(suffixIs(getCmdName(), '*'));
1360                         return true;
1361                 } if (cmd.getArg(0) == "index_print"
1362                     && cmd.getArg(1) == "CommandInset") {
1363                         InsetCommandParams p(INDEX_PRINT_CODE);
1364                         InsetCommand::string2params(to_utf8(cmd.argument()), p);
1365                         if (suffixIs(p.getCmdName(), '*')) {
1366                                 status.setEnabled(true);
1367                                 status.setOnOff(false);
1368                                 return true;
1369                         }
1370                         Buffer const & realbuffer = *buffer().masterBuffer();
1371                         IndicesList const & indiceslist =
1372                                 realbuffer.params().indiceslist();
1373                         Index const * index = indiceslist.findShortcut(p["type"]);
1374                         status.setEnabled(index != 0);
1375                         status.setOnOff(p["type"] == getParam("type"));
1376                         return true;
1377                 } else
1378                         return InsetCommand::getStatus(cur, cmd, status);
1379         }
1380
1381         case LFUN_INSET_DIALOG_UPDATE: {
1382                 status.setEnabled(buffer().masterBuffer()->params().use_indices);
1383                 return true;
1384         }
1385
1386         default:
1387                 return InsetCommand::getStatus(cur, cmd, status);
1388         }
1389 }
1390
1391
1392 void InsetPrintIndex::updateBuffer(ParIterator const &, UpdateType, bool const /*deleted*/)
1393 {
1394         Index const * index =
1395                 buffer().masterParams().indiceslist().findShortcut(getParam("type"));
1396         if (index)
1397                 setParam("name", index->index());
1398 }
1399
1400
1401 void InsetPrintIndex::latex(otexstream & os, OutputParams const & runparams_in) const
1402 {
1403         if (!buffer().masterBuffer()->params().use_indices) {
1404                 if (getParam("type") == from_ascii("idx"))
1405                         os << "\\printindex" << termcmd;
1406                 return;
1407         }
1408         OutputParams runparams = runparams_in;
1409         os << getCommand(runparams);
1410 }
1411
1412
1413 void InsetPrintIndex::validate(LaTeXFeatures & features) const
1414 {
1415         features.require("makeidx");
1416         if (buffer().masterBuffer()->params().use_indices)
1417                 features.require("splitidx");
1418         InsetCommand::validate(features);
1419 }
1420
1421
1422 string InsetPrintIndex::contextMenuName() const
1423 {
1424         return buffer().masterBuffer()->params().use_indices ?
1425                 "context-indexprint" : string();
1426 }
1427
1428
1429 bool InsetPrintIndex::hasSettings() const
1430 {
1431         return buffer().masterBuffer()->params().use_indices;
1432 }
1433
1434
1435 class IndexEntry
1436 {
1437 public:
1438         /// Builds an entry for the index.
1439         IndexEntry(const InsetIndex * inset, OutputParams const * runparams) : inset_(inset), runparams_(runparams)
1440         {
1441                 LASSERT(runparams, return);
1442
1443                 // Convert the inset as text. The resulting text usually only contains an XHTML anchor (<a id='...'/>) and text.
1444                 odocstringstream entry;
1445                 OutputParams ours = *runparams;
1446                 ours.for_toc = false;
1447                 inset_->plaintext(entry, ours);
1448                 entry_ = entry.str();
1449
1450                 // Determine in which index this entry belongs to.
1451                 if (inset_->buffer().masterBuffer()->params().use_indices) {
1452                         index_ = inset_->params_.index;
1453                 }
1454
1455                 // Attempt parsing the inset.
1456                 if (isModern())
1457                         parseAsModern();
1458                 else
1459                         parseAsLegacy();
1460         }
1461
1462
1463 private:
1464         bool isModern()
1465         {
1466 #ifdef LYX_INSET_INDEX_DEBUG
1467                 std::cout << to_utf8(entry_) << std::endl;
1468 #endif // LYX_INSET_INDEX_DEBUG
1469
1470                 // If a modern parameter is present, this is definitely a modern index inset. Similarly, if it contains the
1471                 // usual LaTeX symbols (!|@), then it is definitely a legacy index inset. Otherwise, if it has features of
1472                 // neither, it is both: consider this is a modern inset, to trigger the least complex code. Mixing both types
1473                 // is not allowed (i.e. behaviour is undefined).
1474                 const bool is_definitely_modern = inset_->hasSortKey() || inset_->hasSeeRef() || inset_->hasSubentries()
1475                                             || inset_->params_.range != InsetIndexParams::PageRange::None;
1476                 const bool is_definitely_legacy = entry_.find('@') != std::string::npos
1477                                 || entry_.find('|') != std::string::npos || entry_.find('!') != std::string::npos;
1478
1479                 if (is_definitely_legacy && is_definitely_modern)
1480                         output_error_ += from_utf8("Mix of index properties and raw LaTeX index commands is unsupported. ");
1481
1482                 // Truth table:
1483                 // - is_definitely_modern == true:
1484                 //   - is_definitely_legacy == true: error (return whatever)
1485                 //   - is_definitely_legacy == false: return modern
1486                 // - is_definitely_modern == false:
1487                 //   - is_definitely_legacy == true: return legacy
1488                 //   - is_definitely_legacy == false: return modern
1489                 return !is_definitely_legacy;
1490         }
1491
1492         void parseAsModern()
1493         {
1494                 LASSERT(runparams_, return);
1495
1496                 if (inset_->hasSortKey()) {
1497                         sort_as_ = inset_->getSortkeyAsText(*runparams_);
1498                 }
1499
1500                 terms_ = inset_->getSubentriesAsText(*runparams_);
1501                 // The main term is not present in the vector, as it's not a subentry. The main index term is inserted raw in
1502                 // the index inset. Considering that the user either uses the new or the legacy mechanism, the main term is the
1503                 // full string within this inset (i.e. without the subinsets).
1504                 terms_.insert(terms_.begin(), inset_->getMainSubentryAsText(*runparams_));
1505
1506                 has_start_range_ = inset_->params_.range == InsetIndexParams::PageRange::Start;
1507                 has_end_range_ = inset_->params_.range == InsetIndexParams::PageRange::End;
1508
1509                 see_ = inset_->getSeeAsText(*runparams_);
1510                 see_alsoes_ = inset_->getSeeAlsoesAsText(*runparams_);
1511         }
1512
1513         void parseAsLegacy() {
1514                 // Determine if some features are known not to be supported. For now, this is only formatting like
1515                 // \index{alpha@\textbf{alpha}} or \index{alpha@$\alpha$}.
1516                 // @ is supported, but only for sorting, without specific formatting.
1517                 if (entry_.find(from_utf8("@\\")) != lyx::docstring::npos) {
1518                         output_error_ += from_utf8("Unsupported feature: an index entry contains an @\\. "
1519                                                    "Complete entry: \"") + entry_ + from_utf8("\". ");
1520                 }
1521                 if (entry_.find(from_utf8("@$")) != lyx::docstring::npos) {
1522                         output_error_ += from_utf8("Unsupported feature: an index entry contains an @$. "
1523                                                    "Complete entry: \"") + entry_ + from_utf8("\". ");
1524                 }
1525
1526                 // Split the string into its main constituents: terms, and command (see, see also, range).
1527                 size_t positionVerticalBar = entry_.find(from_ascii("|")); // What comes before | is (sub)(sub)entries.
1528                 docstring indexTerms = entry_.substr(0, positionVerticalBar);
1529                 docstring command;
1530                 if (positionVerticalBar != lyx::docstring::npos) {
1531                         command = entry_.substr(positionVerticalBar + 1);
1532                 }
1533
1534                 // Handle sorting issues, with @.
1535                 vector<docstring> sortingElements = getVectorFromString(indexTerms, from_ascii("@"), false);
1536                 if (sortingElements.size() == 2) {
1537                         sort_as_ = sortingElements[0];
1538                         indexTerms = sortingElements[1];
1539                 }
1540
1541                 // Handle entries, subentries, and subsubentries.
1542                 terms_ = getVectorFromString(indexTerms, from_ascii("!"), false);
1543
1544                 // Handle ranges. Happily, (| and |) can only be at the end of the string!
1545                 has_start_range_ = entry_.find(from_ascii("|(")) != lyx::docstring::npos;
1546                 has_end_range_ = entry_.find(from_ascii("|)")) != lyx::docstring::npos;
1547
1548                 // - Remove the ranges from the command if they do not appear at the beginning.
1549                 size_t range_index = 0;
1550                 while ((range_index = command.find(from_utf8("|("), range_index)) != std::string::npos)
1551                         command.erase(range_index, 1);
1552                 range_index = 0;
1553                 while ((range_index = command.find(from_utf8("|)"), range_index)) != std::string::npos)
1554                         command.erase(range_index, 1);
1555
1556                 // - Remove the ranges when they are the only vertical bar in the complete string.
1557                 if (command[0] == '(' || command[0] == ')')
1558                         command.erase(0, 1);
1559
1560                 // Handle see and seealso. As "see" is a prefix of "seealso", the order of the comparisons is important.
1561                 // Both commands are mutually exclusive!
1562                 if (command.substr(0, 3) == "see") {
1563                         // Unescape brackets.
1564                         size_t index_argument_begin = 0;
1565                         while ((index_argument_begin = command.find(from_utf8("\\{"), index_argument_begin)) != std::string::npos)
1566                                 command.erase(index_argument_begin, 1);
1567                         size_t index_argument_end = 0;
1568                         while ((index_argument_end = command.find(from_utf8("\\}"), index_argument_end)) != std::string::npos)
1569                                 command.erase(index_argument_end, 1);
1570
1571                         // Retrieve the part between brackets, and remove the complete seealso.
1572                         size_t position_opening_bracket = command.find(from_ascii("{"));
1573                         size_t position_closing_bracket = command.find(from_ascii("}"));
1574                         docstring argument = command.substr(position_opening_bracket + 1,
1575                                                                                                 position_closing_bracket - position_opening_bracket - 1);
1576
1577                         // Parse the argument of referenced entries (or a single one for see).
1578                         if (command.substr(0, 7) == "seealso") {
1579                                 see_alsoes_ = getVectorFromString(argument, from_ascii(","), false);
1580                         } else {
1581                                 see_ = argument;
1582
1583                                 if (see_.find(from_ascii(",")) != std::string::npos) {
1584                                         output_error_ += from_utf8("Several index_argument_end terms found as \"see\"! Only one is "
1585                                                                    "acceptable. Complete entry: \"") + entry_ + from_utf8("\". ");
1586                                 }
1587                         }
1588
1589                         // Remove the complete see/seealso from the commands, in case there is something else to parse.
1590                         command = command.substr(position_closing_bracket + 1);
1591                 }
1592
1593                 // Some parts of the strings are not parsed, as they do not have anything matching in DocBook or XHTML:
1594                 // things like formatting the entry or the page number, other strings for sorting.
1595                 // https://wiki.lyx.org/Tips/Indexing
1596                 // If there are such things in the index entry, then this code may miserably fail. For example, for
1597                 // "Peter|(textbf", no range will be detected.
1598                 if (!command.empty()) {
1599                         output_error_ += from_utf8("Unsupported feature: an index entry contains a | with an unsupported command, ")
1600                                          + command + from_utf8(". Complete entry: \"") + entry_ + from_utf8("\". ");
1601                 }
1602         }
1603
1604 public:
1605         int level() const {
1606                 return terms_.size();
1607         }
1608
1609         const std::vector<docstring>& terms() const {
1610                 return terms_;
1611         }
1612
1613         std::vector<docstring>& terms() {
1614                 return terms_;
1615         }
1616
1617         const InsetIndex* inset() const {
1618                 return inset_;
1619         }
1620
1621 private:
1622         // Input inset. These should only be used when parsing the inset (either parseAsModern or parseAsLegacy, called in
1623         // the constructor).
1624         const InsetIndex * inset_;
1625         OutputParams const * runparams_;
1626         docstring entry_;
1627         docstring index_; // Useful when there are multiple indices in the same document.
1628
1629         // Errors, concatenated as a single string, available as soon as parsing is done, const afterwards (i.e. once
1630         // constructor is done).
1631         docstring output_error_;
1632
1633         // Parsed index entry.
1634         std::vector<docstring> terms_; // Up to three entries, in general.
1635         docstring sort_as_;
1636         docstring command_;
1637         bool has_start_range_ = false;
1638         bool has_end_range_ = false;
1639         docstring see_;
1640         vector<docstring> see_alsoes_;
1641
1642         // Operators used for sorting entries (alphabetical order).
1643         friend bool operator<(IndexEntry const & lhs, IndexEntry const & rhs);
1644 };
1645
1646 bool operator<(IndexEntry const & lhs, IndexEntry const & rhs)
1647 {
1648         if (lhs.terms_.empty())
1649                 return false;
1650
1651         for (unsigned i = 0; i < min(rhs.terms_.size(), lhs.terms_.size()); ++i) {
1652                 int comp = compare_no_case(lhs.terms_[i], rhs.terms_[i]);
1653                 if (comp != 0)
1654                         return comp < 0;
1655         }
1656         return false;
1657 }
1658
1659
1660 namespace {
1661 std::string generateCssClassAtDepth(unsigned depth) {
1662         std::string css_class = "entry";
1663
1664         while (depth > 0) {
1665                 depth -= 1;
1666                 css_class.insert(0, "sub");
1667         }
1668
1669         return css_class;
1670 }
1671
1672 struct IndexNode {
1673         std::vector<IndexEntry> entries;
1674         std::vector<IndexNode*> children;
1675
1676         ~IndexNode() {
1677                 for (IndexNode * child : children) {
1678                         if (!child)
1679                                 continue;
1680                         delete child;
1681                 }
1682         }
1683 };
1684
1685 docstring termAtLevel(const IndexNode* node, unsigned depth)
1686 {
1687         // The typical entry has a depth of 1 to 3: the call stack would then be at most 4 (due to the root node). This
1688         // function could be made constant time by copying the term in each node, but that would make data duplication that
1689         // may fall out of sync; the performance benefit would probably be negligible.
1690         if (!node->entries.empty()) {
1691                 LASSERT(node->entries.begin()->terms().size() >= depth + 1, return from_ascii(""));
1692                 return node->entries.begin()->terms()[depth];
1693         }
1694
1695         if (!node->children.empty()) {
1696                 return termAtLevel(*node->children.begin(), depth);
1697         }
1698
1699         LASSERT(false, return from_ascii(""));
1700 }
1701
1702 void insertIntoNode(const IndexEntry& entry, IndexNode* node, unsigned depth = 0)
1703 {
1704         // Do not insert empty entries.
1705         if (entry.terms().empty())
1706                 return;
1707
1708         // depth == 0 is for the root, not yet the index, hence the increase when going to vector size.
1709         for (IndexNode* child : node->children) {
1710                 if (entry.terms()[depth] == termAtLevel(child, depth)) {
1711                         if (depth + 1 == entry.terms().size()) { // == child.entries.begin()->terms().size()
1712                                 // All term entries match: it's an entry.
1713                                 if (!entry.terms()[depth].empty())
1714                                         child->entries.emplace_back(entry);
1715                                 return;
1716                         } else {
1717                                 insertIntoNode(entry, child, depth + 1);
1718                                 return;
1719                         }
1720                 }
1721         }
1722
1723         // Out of the loop: no matching child found, create a new (possibly nested) child for this entry. Due to the
1724         // possibility of nestedness, only insert the current entry when the right level is reached. This is needed if the
1725         // first entry for a word has several levels that never appeared.
1726         // In particular, this case is called for the first entry.
1727         IndexNode* new_node = node;
1728         do {
1729                 new_node->children.emplace_back(new IndexNode{{}, {}});
1730                 new_node = new_node->children.back();
1731                 depth += 1;
1732         } while (depth + 1 <= entry.terms().size()); // depth == 0: root node, no text associated.
1733         new_node->entries.emplace_back(entry);
1734 }
1735
1736 IndexNode* buildIndexTree(vector<IndexEntry>& entries)
1737 {
1738         // Sort the entries, first on the main entry, then the subentry, then the subsubentry,
1739         // thanks to the implementation of operator<.
1740         // If this operation is not performed, the algorithm below is no more correct (and ensuring that it works with
1741         // unsorted entries would make its complexity blow up).
1742         stable_sort(entries.begin(), entries.end());
1743
1744         // Cook the index into a nice tree data structure: entries at a given level in the index as a node, with subentries
1745         // as children.
1746         auto* index_root = new IndexNode{{}, {}};
1747         for (const IndexEntry& entry : entries) {
1748                 insertIntoNode(entry, index_root);
1749         }
1750
1751         return index_root;
1752 }
1753
1754 void outputIndexPage(XMLStream & xs, const IndexNode* root_node, unsigned depth = 0) // NOLINT(misc-no-recursion)
1755 {
1756         LASSERT(root_node->entries.size() + root_node->children.size() > 0, return);
1757
1758         xs << xml::StartTag("li", "class='" + generateCssClassAtDepth(depth) + "'");
1759         xs << xml::CR();
1760         xs << termAtLevel(root_node, depth);
1761         // By tree assumption, all the entries at this node have the same set of terms.
1762
1763         if (!root_node->entries.empty()) {
1764                 xs << XMLStream::ESCAPE_NONE << " &#8212; "; // Em dash, i.e. long (---).
1765                 unsigned entry_number = 1;
1766
1767                 auto writeLinkToEntry = [&xs](const IndexEntry &entry, unsigned entry_number) {
1768                         std::string const link_attr = "href='#" + entry.inset()->paragraphs()[0].magicLabel() + "'";
1769                         xs << xml::StartTag("a", link_attr);
1770                         xs << from_ascii(std::to_string(entry_number));
1771                         xs << xml::EndTag("a");
1772                 };
1773
1774                 for (unsigned i = 0; i < root_node->entries.size(); ++i) {
1775                         const IndexEntry &entry = root_node->entries[i];
1776
1777                         switch (entry.inset()->params().range) {
1778                                 case InsetIndexParams::PageRange::None:
1779                                         writeLinkToEntry(entry, entry_number);
1780                                         break;
1781                                 case InsetIndexParams::PageRange::Start: {
1782                                         // Try to find the end of the range, if it is just after. Otherwise, the output will be slightly
1783                                         // scrambled, but understandable. Doing better would mean implementing more of the indexing logic here
1784                                         // and more complex indexing here (skipping the end is not just incrementing i). Worst case output:
1785                                         //     1--, 2, --3
1786                                         const bool nextEntryIsEnd = i + 1 < root_node->entries.size() &&
1787                                                                     root_node->entries[i + 1].inset()->params().range ==
1788                                                                     InsetIndexParams::PageRange::End;
1789                                         // No need to check if both entries are for the same terms: they are in the same IndexNode.
1790
1791                                         writeLinkToEntry(entry, entry_number);
1792                                         xs << XMLStream::ESCAPE_NONE << " &#8211; "; // En dash, i.e. semi-long (--).
1793
1794                                         if (nextEntryIsEnd) {
1795                                                 // Skip the next entry in the loop, write it right now, after the dash.
1796                                                 entry_number += 1;
1797                                                 i += 1;
1798                                                 writeLinkToEntry(root_node->entries[i], entry_number);
1799                                         }
1800                                 }
1801                                         break;
1802                                 case InsetIndexParams::PageRange::End:
1803                                         // This range end was not caught by the range start, do it now to avoid losing content.
1804                                         xs << XMLStream::ESCAPE_NONE << " &#8211; "; // En dash, i.e. semi-long (--).
1805                                         writeLinkToEntry(root_node->entries[i], entry_number);
1806                         }
1807
1808                         if (i < root_node->entries.size() - 1) {
1809                                 xs << ", ";
1810                         }
1811                         entry_number += 1;
1812                 }
1813                 xs << xml::CR();
1814         }
1815
1816         if (!root_node->entries.empty() && !root_node->children.empty()) {
1817                 xs << xml::CR();
1818         }
1819
1820         if (!root_node->children.empty()) {
1821                 xs << xml::StartTag("ul", "class='" + generateCssClassAtDepth(depth) + "'");
1822                 xs << xml::CR();
1823
1824                 for (const IndexNode* child : root_node->children) {
1825                         outputIndexPage(xs, child, depth + 1);
1826                 }
1827
1828                 xs << xml::EndTag("ul");
1829                 xs << xml::CR();
1830         }
1831
1832         xs << xml::EndTag("li");
1833         xs << xml::CR();
1834 }
1835
1836 #ifdef LYX_INSET_INDEX_DEBUG
1837 void printTree(const IndexNode* root_node, unsigned depth = 0)
1838 {
1839         static const std::string pattern = "    ";
1840         std::string prefix;
1841         for (unsigned i = 0; i < depth; ++i) {
1842                 prefix += pattern;
1843         }
1844         const std::string prefix_long = prefix + pattern + pattern;
1845
1846         docstring term_at_level;
1847         if (depth == 0) {
1848                 // The root has no term.
1849                 std::cout << "<ROOT>" << std::endl;
1850         } else {
1851                 LASSERT(depth - 1 <= 10, return); // Check for overflows.
1852                 term_at_level = termAtLevel(root_node, depth - 1);
1853                 std::cout << prefix << to_utf8(term_at_level) << " (x " << std::to_string(root_node->entries.size()) << ")"
1854                           << std::endl;
1855         }
1856
1857         for (const IndexEntry& entry : root_node->entries) {
1858                 if (entry.terms().size() != depth) {
1859                         std::cout << prefix_long << "ERROR: an entry doesn't have the same number of terms" << std::endl;
1860                 }
1861                 if (depth > 0 && entry.terms()[depth - 1] != term_at_level) {
1862                         std::cout << prefix_long << "ERROR: an entry doesn't have the right term at depth " << std::to_string(depth)
1863                                 << std::endl;
1864                 }
1865         }
1866
1867         for (const IndexNode* node : root_node->children) {
1868                 printTree(node, depth + 1);
1869         }
1870 }
1871 #endif // LYX_INSET_INDEX_DEBUG
1872 }
1873
1874
1875 docstring InsetPrintIndex::xhtml(XMLStream &, OutputParams const & op) const
1876 {
1877         BufferParams const & bp = buffer().masterBuffer()->params();
1878
1879         shared_ptr<Toc const> toc = buffer().tocBackend().toc("index");
1880         if (toc->empty())
1881                 return docstring();
1882
1883         // Collect the index entries in a form we can use them.
1884         vector<IndexEntry> entries;
1885         const docstring defaultIndexType = from_ascii("idx");
1886         const docstring & indexType = params().getParamOr("type", defaultIndexType);
1887         for (const TocItem& item : *toc) {
1888                 const auto* inset = static_cast<const InsetIndex*>(&(item.dit().inset()));
1889                 if (item.isOutput() && inset && inset->params().index == indexType)
1890                         entries.emplace_back(IndexEntry{inset, &op});
1891         }
1892
1893         // If all the index entries are in notes or not displayed, get out sooner.
1894         if (entries.empty())
1895                 return docstring();
1896
1897         const IndexNode* index_root = buildIndexTree(entries);
1898 #ifdef LYX_INSET_INDEX_DEBUG
1899         printTree(index_root);
1900 #endif
1901
1902         // Start generating the XHTML index.
1903         Layout const & lay = bp.documentClass().htmlTOCLayout();
1904         string const & tocclass = lay.defaultCSSClass();
1905         string const tocattr = "class='index " + tocclass + "'";
1906         docstring const indexName = params().getParamOr("name", from_ascii("Index"));
1907
1908         // we'll use our own stream, because we are going to defer everything.
1909         // that's how we deal with the fact that we're probably inside a standard
1910         // paragraph, and we don't want to be.
1911         odocstringstream ods;
1912         XMLStream xs(ods);
1913
1914         xs << xml::StartTag("div", tocattr);
1915         xs << xml::CR();
1916         xs << xml::StartTag(lay.htmltag(), lay.htmlGetAttrString());
1917         xs << translateIfPossible(indexName, getLocalOrDefaultLang(op)->lang());
1918         xs << xml::EndTag(lay.htmltag());
1919         xs << xml::CR();
1920         xs << xml::StartTag("ul", "class='main'");
1921         xs << xml::CR();
1922
1923         LASSERT(index_root->entries.empty(), return docstring()); // No index entry should have zero terms.
1924         for (const IndexNode* node : index_root->children) {
1925                 outputIndexPage(xs, node);
1926         }
1927         delete index_root;
1928
1929         xs << xml::EndTag("ul");
1930         xs << xml::CR();
1931         xs << xml::EndTag("div");
1932
1933         return ods.str();
1934 }
1935
1936 } // namespace lyx