]> git.lyx.org Git - features.git/blob - src/insets/InsetIndex.cpp
DocBook: add a TODO for a newly discovered bug.
[features.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
15 #include "Buffer.h"
16 #include "BufferParams.h"
17 #include "BufferView.h"
18 #include "ColorSet.h"
19 #include "Cursor.h"
20 #include "DispatchResult.h"
21 #include "Encoding.h"
22 #include "FuncRequest.h"
23 #include "FuncStatus.h"
24 #include "IndicesList.h"
25 #include "Language.h"
26 #include "LaTeXFeatures.h"
27 #include "Lexer.h"
28 #include "output_latex.h"
29 #include "output_xhtml.h"
30 #include "xml.h"
31 #include "texstream.h"
32 #include "TextClass.h"
33 #include "TocBackend.h"
34
35 #include "support/debug.h"
36 #include "support/docstream.h"
37 #include "support/FileName.h"
38 #include "support/gettext.h"
39 #include "support/lstrings.h"
40
41 #include "frontends/alert.h"
42
43 #include <algorithm>
44 #include <set>
45 #include <ostream>
46
47 #include <QThreadStorage>
48
49 using namespace std;
50 using namespace lyx::support;
51
52 namespace lyx {
53
54 /////////////////////////////////////////////////////////////////////
55 //
56 // InsetIndex
57 //
58 ///////////////////////////////////////////////////////////////////////
59
60
61 InsetIndex::InsetIndex(Buffer * buf, InsetIndexParams const & params)
62         : InsetCollapsible(buf), params_(params)
63 {}
64
65
66 void InsetIndex::latex(otexstream & ios, OutputParams const & runparams_in) const
67 {
68         OutputParams runparams(runparams_in);
69         runparams.inIndexEntry = true;
70
71         otexstringstream os;
72
73         if (buffer().masterBuffer()->params().use_indices && !params_.index.empty()
74                 && params_.index != "idx") {
75                 os << "\\sindex[";
76                 os << escape(params_.index);
77                 os << "]{";
78         } else {
79                 os << "\\index";
80                 os << '{';
81         }
82
83         odocstringstream ourlatex;
84         otexstream ots(ourlatex);
85         InsetText::latex(ots, runparams);
86         if (runparams.for_searchAdv != OutputParams::NoSearch) {
87                 // No need for special handling, if we are only searching for some patterns
88                 os << ourlatex.str() << "}";
89                 return;
90         }
91         // get contents of InsetText as LaTeX and plaintext
92         odocstringstream ourplain;
93         InsetText::plaintext(ourplain, runparams);
94         // FIXME: do Tex/Row correspondence (I don't currently understand what is
95         // being generated from latexstr below)
96         docstring latexstr = ourlatex.str();
97         docstring plainstr = ourplain.str();
98
99         // this will get what follows | if anything does
100         docstring cmd;
101
102         // check for the | separator
103         // FIXME This would go wrong on an escaped "|", but
104         // how far do we want to go here?
105         size_t pos = latexstr.find(from_ascii("|"));
106         if (pos != docstring::npos) {
107                 // put the bit after "|" into cmd...
108                 cmd = latexstr.substr(pos + 1);
109                 // ...and erase that stuff from latexstr
110                 latexstr = latexstr.erase(pos);
111                 // ...and similarly from plainstr
112                 size_t ppos = plainstr.find(from_ascii("|"));
113                 if (ppos < plainstr.size())
114                         plainstr.erase(ppos);
115                 else
116                         LYXERR0("The `|' separator was not found in the plaintext version!");
117         }
118
119         // Separate the entries and subentries, i.e., split on "!"
120         // FIXME This would do the wrong thing with escaped ! characters
121         std::vector<docstring> const levels =
122                         getVectorFromString(latexstr, from_ascii("!"), true);
123         std::vector<docstring> const levels_plain =
124                         getVectorFromString(plainstr, from_ascii("!"), true);
125
126         vector<docstring>::const_iterator it = levels.begin();
127         vector<docstring>::const_iterator end = levels.end();
128         vector<docstring>::const_iterator it2 = levels_plain.begin();
129         bool first = true;
130         for (; it != end; ++it) {
131                 // write the separator except the first time
132                 if (!first)
133                         os << '!';
134                 else
135                         first = false;
136
137                 // correctly sort macros and formatted strings
138                 // if we do find a command, prepend a plain text
139                 // version of the content to get sorting right,
140                 // e.g. \index{LyX@\LyX}, \index{text@\textbf{text}}
141                 // Don't do that if the user entered '@' himself, though.
142                 if (contains(*it, '\\') && !contains(*it, '@')) {
143                         // Plaintext might return nothing (e.g. for ERTs)
144                         docstring const spart =
145                                         (it2 < levels_plain.end() && !(*it2).empty())
146                                         ? *it2 : *it;
147                         // Now we need to validate that all characters in
148                         // the sorting part are representable in the current
149                         // encoding. If not try the LaTeX macro which might
150                         // or might not be a good choice, and issue a warning.
151                         pair<docstring, docstring> spart_latexed =
152                                         runparams.encoding->latexString(spart, runparams.dryrun);
153                         if (!spart_latexed.second.empty())
154                                 LYXERR0("Uncodable character in index entry. Sorting might be wrong!");
155                         if (spart != spart_latexed.first && !runparams.dryrun) {
156                                 // FIXME: warning should be passed to the error dialog
157                                 frontend::Alert::warning(_("Index sorting failed"),
158                                                                                  bformat(_("LyX's automatic index sorting algorithm faced\n"
159                                                                                                                    "problems with the entry '%1$s'.\n"
160                                                                                                                    "Please specify the sorting of this entry manually, as\n"
161                                                                                                                    "explained in the User Guide."), spart));
162                         }
163                         // remove remaining \'s for the sorting part
164                         docstring const ppart =
165                                         subst(spart_latexed.first, from_ascii("\\"), docstring());
166                         os << ppart;
167                         os << '@';
168                 }
169                 docstring const tpart = *it;
170                 os << tpart;
171                 if (it2 < levels_plain.end())
172                         ++it2;
173         }
174         // write the bit that followed "|"
175         if (!cmd.empty()) {
176                 os << "|" << cmd;
177         }
178         os << '}';
179
180         // In macros with moving arguments, such as \section,
181         // we store the index and output it after the macro (#2154)
182         if (runparams_in.postpone_fragile_stuff)
183                 runparams_in.post_macro += os.str();
184         else
185                 ios << os.release();
186 }
187
188
189 void InsetIndex::docbook(XMLStream & xs, OutputParams const & runparams) const
190 {
191         // Get the content of the inset as LaTeX, as some things may be encoded as ERT (like {}).
192         // TODO: if there is an ERT within the index term, its conversion should be tried, in case it becomes useful;
193         //  otherwise, ERTs should become comments. For now, they are just copied as-is, which is barely satisfactory.
194         odocstringstream odss;
195         otexstream ots(odss);
196         InsetText::latex(ots, runparams);
197         docstring latexString = trim(odss.str());
198
199         // Check whether there are unsupported things. @ is supported, but only for sorting, without specific formatting.
200         if (latexString.find(from_utf8("@\\")) != lyx::docstring::npos) {
201                 docstring error = from_utf8("Unsupported feature: an index entry contains an @\\. "
202                                                                         "Complete entry: \"") + latexString + from_utf8("\"");
203                 LYXERR0(error);
204                 xs << XMLStream::ESCAPE_NONE << (from_utf8("<!-- Output Error: ") + error + from_utf8(" -->\n"));
205         }
206
207         // Handle several indices (indicated in the inset instead of the raw latexString).
208         docstring indexType = from_utf8("");
209         if (buffer().masterBuffer()->params().use_indices) {
210                 indexType += " type=\"" + params_.index + "\"";
211         }
212
213         // Split the string into its main constituents: terms, and command (see, see also, range).
214         size_t positionVerticalBar = latexString.find(from_ascii("|")); // What comes before | is (sub)(sub)entries.
215         docstring indexTerms = latexString.substr(0, positionVerticalBar);
216         docstring command;
217         if (positionVerticalBar != lyx::docstring::npos) {
218                 command =  latexString.substr(positionVerticalBar + 1);
219         }
220
221         // Handle sorting issues, with @.
222         vector<docstring> sortingElements = getVectorFromString(indexTerms, from_ascii("@"), false);
223         docstring sortAs;
224         if (sortingElements.size() == 2) {
225                 sortAs = sortingElements[0];
226                 indexTerms = sortingElements[1];
227         }
228
229         // Handle primary, secondary, and tertiary terms (entries, subentries, and subsubentries, for LaTeX).
230         vector<docstring> terms = getVectorFromString(indexTerms, from_ascii("!"), false);
231
232         // Handle ranges. Happily, (| and |) can only be at the end of the string!
233         bool hasStartRange = latexString.find(from_ascii("|(")) != lyx::docstring::npos;
234         bool hasEndRange = latexString.find(from_ascii("|)")) != lyx::docstring::npos;
235         if (hasStartRange || hasEndRange) {
236                 // Remove the ranges from the command if they do not appear at the beginning.
237                 size_t index = 0;
238                 while ((index = command.find(from_utf8("|("), index)) != std::string::npos)
239                         command.erase(index, 1);
240                 index = 0;
241                 while ((index = command.find(from_utf8("|)"), index)) != std::string::npos)
242                         command.erase(index, 1);
243
244                 // Remove the ranges when they are the only vertical bar in the complete string.
245                 if (command[0] == '(' || command[0] == ')')
246                         command.erase(0, 1);
247         }
248
249         // Handle see and seealso. As "see" is a prefix of "seealso", the order of the comparisons is important.
250         // Both commands are mutually exclusive!
251         docstring see = from_utf8("");
252         vector<docstring> seeAlsoes;
253         if (command.substr(0, 3) == "see") {
254                 // Unescape brackets.
255                 size_t index = 0;
256                 while ((index = command.find(from_utf8("\\{"), index)) != std::string::npos)
257                         command.erase(index, 1);
258                 index = 0;
259                 while ((index = command.find(from_utf8("\\}"), index)) != std::string::npos)
260                         command.erase(index, 1);
261
262                 // Retrieve the part between brackets, and remove the complete seealso.
263                 size_t positionOpeningBracket = command.find(from_ascii("{"));
264                 size_t positionClosingBracket = command.find(from_ascii("}"));
265                 docstring list = command.substr(positionOpeningBracket + 1, positionClosingBracket - positionOpeningBracket - 1);
266
267                 // Parse the list of referenced entries (or a single one for see).
268                 if (command.substr(0, 7) == "seealso") {
269                         seeAlsoes = getVectorFromString(list, from_ascii(","), false);
270                 } else {
271                         see = list;
272
273                         if (see.find(from_ascii(",")) != std::string::npos) {
274                                 docstring error = from_utf8("Several index terms found as \"see\"! Only one is acceptable. "
275                                                                                         "Complete entry: \"") + latexString + from_utf8("\"");
276                                 LYXERR0(error);
277                                 xs << XMLStream::ESCAPE_NONE << (from_utf8("<!-- Output Error: ") + error + from_utf8(" -->\n"));
278                         }
279                 }
280
281                 // Remove the complete see/seealso from the commands, in case there is something else to parse.
282                 command = command.substr(positionClosingBracket + 1);
283         }
284
285         // Some parts of the strings are not parsed, as they do not have anything matching in DocBook: things like
286         // formatting the entry or the page number, other strings for sorting. https://wiki.lyx.org/Tips/Indexing
287         // If there are such things in the index entry, then this code may miserably fail. For example, for "Peter|(textbf",
288         // no range will be detected.
289         // TODO: Could handle formatting as significance="preferred"?
290         if (!command.empty()) {
291                 docstring error = from_utf8("Unsupported feature: an index entry contains a | with an unsupported command, ")
292                                           + command + from_utf8(". ") + from_utf8("Complete entry: \"") + latexString + from_utf8("\"");
293                 LYXERR0(error);
294                 xs << XMLStream::ESCAPE_NONE << (from_utf8("<!-- Output Error: ") + error + from_utf8(" -->\n"));
295         }
296
297     // Write all of this down.
298         if (terms.empty() && !hasEndRange) {
299                 docstring error = from_utf8("No index term found! Complete entry: \"") + latexString + from_utf8("\"");
300                 LYXERR0(error);
301                 xs << XMLStream::ESCAPE_NONE << (from_utf8("<!-- Output Error: ") + error + from_utf8(" -->\n"));
302         } else {
303                 // Generate the attributes for ranges. It is based on the terms that are indexed, but the ID must be unique
304                 // to this indexing area (xml::cleanID does not guarantee this: for each call with the same arguments,
305                 // the same legal ID is produced; here, as the input would be the same, the output must be, by design).
306                 // Hence the thread-local storage, as the numbers must strictly be unique, and thus cannot be shared across
307                 // a paragraph (making the solution used for HTML worthless). This solution is very similar to the one used in
308                 // xml::cleanID.
309                 // indexType can only be used for singular and startofrange types!
310                 docstring attrs;
311                 if (!hasStartRange && !hasEndRange) {
312                         attrs = indexType;
313                 } else {
314                         // Append an ID if uniqueness is not guaranteed across the document.
315                         static QThreadStorage<set<docstring>> tKnownTermLists;
316                         static QThreadStorage<int> tID;
317
318                         set<docstring> &knownTermLists = tKnownTermLists.localData();
319                         int &ID = tID.localData();
320
321                         if (!tID.hasLocalData()) {
322                                 tID.localData() = 0;
323                         }
324
325                         // Modify the index terms to add the unique ID if needed.
326                         docstring newIndexTerms = indexTerms;
327                         if (knownTermLists.find(indexTerms) != knownTermLists.end()) {
328                                 newIndexTerms += from_ascii(string("-") + to_string(ID));
329
330                                 // Only increment for the end of range, so that the same number is used for the start of range.
331                                 if (hasEndRange) {
332                                         ID++;
333                                 }
334                         }
335
336                         // Term list not yet known: add it to the set AFTER the end of range. After
337                         if (knownTermLists.find(indexTerms) == knownTermLists.end() && hasEndRange) {
338                                 knownTermLists.insert(indexTerms);
339                         }
340
341                         // Generate the attributes.
342                         docstring id = xml::cleanID(newIndexTerms);
343                         if (hasStartRange) {
344                                 attrs = indexType + " class=\"startofrange\" xml:id=\"" + id + "\"";
345                         } else {
346                                 attrs = " class=\"endofrange\" startref=\"" + id + "\"";
347                         }
348                 }
349
350                 // Handle the index terms (including the specific index for this entry).
351                 if (hasEndRange) {
352                         xs << xml::CompTag("indexterm", attrs);
353                 } else {
354                         xs << xml::StartTag("indexterm", attrs);
355                         if (!terms.empty()) { // hasEndRange has no content.
356                                 docstring attr;
357                                 if (!sortAs.empty()) {
358                                         attr = from_utf8("sortas='") + sortAs + from_utf8("'");
359                                 }
360
361                                 xs << xml::StartTag("primary", attr);
362                                 xs << terms[0];
363                                 xs << xml::EndTag("primary");
364                         }
365                         if (terms.size() > 1) {
366                                 xs << xml::StartTag("secondary");
367                                 xs << terms[1];
368                                 xs << xml::EndTag("secondary");
369                         }
370                         if (terms.size() > 2) {
371                                 xs << xml::StartTag("tertiary");
372                                 xs << terms[2];
373                                 xs << xml::EndTag("tertiary");
374                         }
375
376                         // Handle see and see also.
377                         if (!see.empty()) {
378                                 xs << xml::StartTag("see");
379                                 xs << see;
380                                 xs << xml::EndTag("see");
381                         }
382
383                         if (!seeAlsoes.empty()) {
384                                 for (auto &entry : seeAlsoes) {
385                                         xs << xml::StartTag("seealso");
386                                         xs << entry;
387                                         xs << xml::EndTag("seealso");
388                                 }
389                         }
390
391                         // Close the entry.
392                         xs << xml::EndTag("indexterm");
393                 }
394         }
395 }
396
397
398 docstring InsetIndex::xhtml(XMLStream & xs, OutputParams const &) const
399 {
400         // we just print an anchor, taking the paragraph ID from
401         // our own interior paragraph, which doesn't get printed
402         std::string const magic = paragraphs().front().magicLabel();
403         std::string const attr = "id='" + magic + "'";
404         xs << xml::CompTag("a", attr);
405         return docstring();
406 }
407
408
409 bool InsetIndex::showInsetDialog(BufferView * bv) const
410 {
411         bv->showDialog("index", params2string(params_),
412                         const_cast<InsetIndex *>(this));
413         return true;
414 }
415
416
417 void InsetIndex::doDispatch(Cursor & cur, FuncRequest & cmd)
418 {
419         switch (cmd.action()) {
420
421         case LFUN_INSET_MODIFY: {
422                 if (cmd.getArg(0) == "changetype") {
423                         cur.recordUndoInset(this);
424                         params_.index = from_utf8(cmd.getArg(1));
425                         break;
426                 }
427                 InsetIndexParams params;
428                 InsetIndex::string2params(to_utf8(cmd.argument()), params);
429                 cur.recordUndoInset(this);
430                 params_.index = params.index;
431                 // what we really want here is a TOC update, but that means
432                 // a full buffer update
433                 cur.forceBufferUpdate();
434                 break;
435         }
436
437         case LFUN_INSET_DIALOG_UPDATE:
438                 cur.bv().updateDialog("index", params2string(params_));
439                 break;
440
441         default:
442                 InsetCollapsible::doDispatch(cur, cmd);
443                 break;
444         }
445 }
446
447
448 bool InsetIndex::getStatus(Cursor & cur, FuncRequest const & cmd,
449                 FuncStatus & flag) const
450 {
451         switch (cmd.action()) {
452
453         case LFUN_INSET_MODIFY:
454                 if (cmd.getArg(0) == "changetype") {
455                         docstring const newtype = from_utf8(cmd.getArg(1));
456                         Buffer const & realbuffer = *buffer().masterBuffer();
457                         IndicesList const & indiceslist = realbuffer.params().indiceslist();
458                         Index const * index = indiceslist.findShortcut(newtype);
459                         flag.setEnabled(index != 0);
460                         flag.setOnOff(
461                                 from_utf8(cmd.getArg(1)) == params_.index);
462                         return true;
463                 }
464                 return InsetCollapsible::getStatus(cur, cmd, flag);
465
466         case LFUN_INSET_DIALOG_UPDATE: {
467                 Buffer const & realbuffer = *buffer().masterBuffer();
468                 flag.setEnabled(realbuffer.params().use_indices);
469                 return true;
470         }
471
472         default:
473                 return InsetCollapsible::getStatus(cur, cmd, flag);
474         }
475 }
476
477
478 ColorCode InsetIndex::labelColor() const
479 {
480         if (params_.index.empty() || params_.index == from_ascii("idx"))
481                 return InsetCollapsible::labelColor();
482         // FIXME UNICODE
483         ColorCode c = lcolor.getFromLyXName(to_utf8(params_.index)
484                                             + "@" + buffer().fileName().absFileName());
485         if (c == Color_none)
486                 c = InsetCollapsible::labelColor();
487         return c;
488 }
489
490
491 docstring InsetIndex::toolTip(BufferView const &, int, int) const
492 {
493         docstring tip = _("Index Entry");
494         if (buffer().params().use_indices && !params_.index.empty()) {
495                 Buffer const & realbuffer = *buffer().masterBuffer();
496                 IndicesList const & indiceslist = realbuffer.params().indiceslist();
497                 tip += " (";
498                 Index const * index = indiceslist.findShortcut(params_.index);
499                 if (!index)
500                         tip += _("unknown type!");
501                 else
502                         tip += index->index();
503                 tip += ")";
504         }
505         tip += ": ";
506         return toolTipText(tip);
507 }
508
509
510 docstring const InsetIndex::buttonLabel(BufferView const & bv) const
511 {
512         InsetLayout const & il = getLayout();
513         docstring label = translateIfPossible(il.labelstring());
514
515         if (buffer().params().use_indices && !params_.index.empty()) {
516                 Buffer const & realbuffer = *buffer().masterBuffer();
517                 IndicesList const & indiceslist = realbuffer.params().indiceslist();
518                 label += " (";
519                 Index const * index = indiceslist.findShortcut(params_.index);
520                 if (!index)
521                         label += _("unknown type!");
522                 else
523                         label += index->index();
524                 label += ")";
525         }
526
527         if (!il.contentaslabel() || geometry(bv) != ButtonOnly)
528                 return label;
529         return getNewLabel(label);
530 }
531
532
533 void InsetIndex::write(ostream & os) const
534 {
535         os << to_utf8(layoutName());
536         params_.write(os);
537         InsetCollapsible::write(os);
538 }
539
540
541 void InsetIndex::read(Lexer & lex)
542 {
543         params_.read(lex);
544         InsetCollapsible::read(lex);
545 }
546
547
548 string InsetIndex::params2string(InsetIndexParams const & params)
549 {
550         ostringstream data;
551         data << "index";
552         params.write(data);
553         return data.str();
554 }
555
556
557 void InsetIndex::string2params(string const & in, InsetIndexParams & params)
558 {
559         params = InsetIndexParams();
560         if (in.empty())
561                 return;
562
563         istringstream data(in);
564         Lexer lex;
565         lex.setStream(data);
566         lex.setContext("InsetIndex::string2params");
567         lex >> "index";
568         params.read(lex);
569 }
570
571
572 void InsetIndex::addToToc(DocIterator const & cpit, bool output_active,
573                                                   UpdateType utype, TocBackend & backend) const
574 {
575         DocIterator pit = cpit;
576         pit.push_back(CursorSlice(const_cast<InsetIndex &>(*this)));
577         docstring str;
578         string type = "index";
579         if (buffer().masterBuffer()->params().use_indices)
580                 type += ":" + to_utf8(params_.index);
581         // this is unlikely to be terribly long
582         text().forOutliner(str, INT_MAX);
583         TocBuilder & b = backend.builder(type);
584         b.pushItem(pit, str, output_active);
585         // Proceed with the rest of the inset.
586         InsetCollapsible::addToToc(cpit, output_active, utype, backend);
587         b.pop();
588 }
589
590
591 void InsetIndex::validate(LaTeXFeatures & features) const
592 {
593         if (buffer().masterBuffer()->params().use_indices
594             && !params_.index.empty()
595             && params_.index != "idx")
596                 features.require("splitidx");
597         InsetCollapsible::validate(features);
598 }
599
600
601 string InsetIndex::contextMenuName() const
602 {
603         return "context-index";
604 }
605
606
607 bool InsetIndex::hasSettings() const
608 {
609         return buffer().masterBuffer()->params().use_indices;
610 }
611
612
613
614
615 /////////////////////////////////////////////////////////////////////
616 //
617 // InsetIndexParams
618 //
619 ///////////////////////////////////////////////////////////////////////
620
621
622 void InsetIndexParams::write(ostream & os) const
623 {
624         os << ' ';
625         if (!index.empty())
626                 os << to_utf8(index);
627         else
628                 os << "idx";
629         os << '\n';
630 }
631
632
633 void InsetIndexParams::read(Lexer & lex)
634 {
635         if (lex.eatLine())
636                 index = lex.getDocString();
637         else
638                 index = from_ascii("idx");
639 }
640
641
642 /////////////////////////////////////////////////////////////////////
643 //
644 // InsetPrintIndex
645 //
646 ///////////////////////////////////////////////////////////////////////
647
648 InsetPrintIndex::InsetPrintIndex(Buffer * buf, InsetCommandParams const & p)
649         : InsetCommand(buf, p)
650 {}
651
652
653 ParamInfo const & InsetPrintIndex::findInfo(string const & /* cmdName */)
654 {
655         static ParamInfo param_info_;
656         if (param_info_.empty()) {
657                 param_info_.add("type", ParamInfo::LATEX_OPTIONAL,
658                                 ParamInfo::HANDLING_ESCAPE);
659                 param_info_.add("name", ParamInfo::LATEX_OPTIONAL,
660                                 ParamInfo::HANDLING_LATEXIFY);
661                 param_info_.add("literal", ParamInfo::LYX_INTERNAL);
662         }
663         return param_info_;
664 }
665
666
667 docstring InsetPrintIndex::screenLabel() const
668 {
669         bool const printall = suffixIs(getCmdName(), '*');
670         bool const multind = buffer().masterBuffer()->params().use_indices;
671         if ((!multind
672              && getParam("type") == from_ascii("idx"))
673             || (getParam("type").empty() && !printall))
674                 return _("Index");
675         Buffer const & realbuffer = *buffer().masterBuffer();
676         IndicesList const & indiceslist = realbuffer.params().indiceslist();
677         Index const * index = indiceslist.findShortcut(getParam("type"));
678         if (!index && !printall)
679                 return _("Unknown index type!");
680         docstring res = printall ? _("All indexes") : index->index();
681         if (!multind)
682                 res += " (" + _("non-active") + ")";
683         else if (contains(getCmdName(), "printsubindex"))
684                 res += " (" + _("subindex") + ")";
685         return res;
686 }
687
688
689 bool InsetPrintIndex::isCompatibleCommand(string const & s)
690 {
691         return s == "printindex" || s == "printsubindex"
692                 || s == "printindex*" || s == "printsubindex*";
693 }
694
695
696 void InsetPrintIndex::doDispatch(Cursor & cur, FuncRequest & cmd)
697 {
698         switch (cmd.action()) {
699
700         case LFUN_INSET_MODIFY: {
701                 if (cmd.argument() == from_ascii("toggle-subindex")) {
702                         string scmd = getCmdName();
703                         if (contains(scmd, "printindex"))
704                                 scmd = subst(scmd, "printindex", "printsubindex");
705                         else
706                                 scmd = subst(scmd, "printsubindex", "printindex");
707                         cur.recordUndo();
708                         setCmdName(scmd);
709                         break;
710                 } else if (cmd.argument() == from_ascii("check-printindex*")) {
711                         string scmd = getCmdName();
712                         if (suffixIs(scmd, '*'))
713                                 break;
714                         scmd += '*';
715                         cur.recordUndo();
716                         setParam("type", docstring());
717                         setCmdName(scmd);
718                         break;
719                 }
720                 InsetCommandParams p(INDEX_PRINT_CODE);
721                 // FIXME UNICODE
722                 InsetCommand::string2params(to_utf8(cmd.argument()), p);
723                 if (p.getCmdName().empty()) {
724                         cur.noScreenUpdate();
725                         break;
726                 }
727                 cur.recordUndo();
728                 setParams(p);
729                 break;
730         }
731
732         default:
733                 InsetCommand::doDispatch(cur, cmd);
734                 break;
735         }
736 }
737
738
739 bool InsetPrintIndex::getStatus(Cursor & cur, FuncRequest const & cmd,
740         FuncStatus & status) const
741 {
742         switch (cmd.action()) {
743
744         case LFUN_INSET_MODIFY: {
745                 if (cmd.argument() == from_ascii("toggle-subindex")) {
746                         status.setEnabled(buffer().masterBuffer()->params().use_indices);
747                         status.setOnOff(contains(getCmdName(), "printsubindex"));
748                         return true;
749                 } else if (cmd.argument() == from_ascii("check-printindex*")) {
750                         status.setEnabled(buffer().masterBuffer()->params().use_indices);
751                         status.setOnOff(suffixIs(getCmdName(), '*'));
752                         return true;
753                 } if (cmd.getArg(0) == "index_print"
754                     && cmd.getArg(1) == "CommandInset") {
755                         InsetCommandParams p(INDEX_PRINT_CODE);
756                         InsetCommand::string2params(to_utf8(cmd.argument()), p);
757                         if (suffixIs(p.getCmdName(), '*')) {
758                                 status.setEnabled(true);
759                                 status.setOnOff(false);
760                                 return true;
761                         }
762                         Buffer const & realbuffer = *buffer().masterBuffer();
763                         IndicesList const & indiceslist =
764                                 realbuffer.params().indiceslist();
765                         Index const * index = indiceslist.findShortcut(p["type"]);
766                         status.setEnabled(index != 0);
767                         status.setOnOff(p["type"] == getParam("type"));
768                         return true;
769                 } else
770                         return InsetCommand::getStatus(cur, cmd, status);
771         }
772
773         case LFUN_INSET_DIALOG_UPDATE: {
774                 status.setEnabled(buffer().masterBuffer()->params().use_indices);
775                 return true;
776         }
777
778         default:
779                 return InsetCommand::getStatus(cur, cmd, status);
780         }
781 }
782
783
784 void InsetPrintIndex::updateBuffer(ParIterator const &, UpdateType, bool const /*deleted*/)
785 {
786         Index const * index =
787                 buffer().masterParams().indiceslist().findShortcut(getParam("type"));
788         if (index)
789                 setParam("name", index->index());
790 }
791
792
793 void InsetPrintIndex::latex(otexstream & os, OutputParams const & runparams_in) const
794 {
795         if (!buffer().masterBuffer()->params().use_indices) {
796                 if (getParam("type") == from_ascii("idx"))
797                         os << "\\printindex" << termcmd;
798                 return;
799         }
800         OutputParams runparams = runparams_in;
801         os << getCommand(runparams);
802 }
803
804
805 void InsetPrintIndex::validate(LaTeXFeatures & features) const
806 {
807         features.require("makeidx");
808         if (buffer().masterBuffer()->params().use_indices)
809                 features.require("splitidx");
810         InsetCommand::validate(features);
811 }
812
813
814 string InsetPrintIndex::contextMenuName() const
815 {
816         return buffer().masterBuffer()->params().use_indices ?
817                 "context-indexprint" : string();
818 }
819
820
821 bool InsetPrintIndex::hasSettings() const
822 {
823         return buffer().masterBuffer()->params().use_indices;
824 }
825
826
827 namespace {
828
829 void parseItem(docstring & s, bool for_output)
830 {
831         // this does not yet check for escaped things
832         size_type loc = s.find(from_ascii("@"));
833         if (loc != string::npos) {
834                 if (for_output)
835                         s.erase(0, loc + 1);
836                 else
837                         s.erase(loc);
838         }
839         loc = s.find(from_ascii("|"));
840         if (loc != string::npos)
841                 s.erase(loc);
842 }
843
844
845 void extractSubentries(docstring const & entry, docstring & main,
846                 docstring & sub1, docstring & sub2)
847 {
848         if (entry.empty())
849                 return;
850         size_type const loc = entry.find(from_ascii(" ! "));
851         if (loc == string::npos)
852                 main = entry;
853         else {
854                 main = trim(entry.substr(0, loc));
855                 size_t const locend = loc + 3;
856                 size_type const loc2 = entry.find(from_ascii(" ! "), locend);
857                 if (loc2 == string::npos) {
858                         sub1 = trim(entry.substr(locend));
859                 } else {
860                         sub1 = trim(entry.substr(locend, loc2 - locend));
861                         sub2 = trim(entry.substr(loc2 + 3));
862                 }
863         }
864 }
865
866
867 struct IndexEntry
868 {
869         IndexEntry()
870         {}
871
872         IndexEntry(docstring const & s, DocIterator const & d)
873                         : dit(d)
874         {
875                 extractSubentries(s, main, sub, subsub);
876                 parseItem(main, false);
877                 parseItem(sub, false);
878                 parseItem(subsub, false);
879         }
880
881         bool equal(IndexEntry const & rhs) const
882         {
883                 return main == rhs.main && sub == rhs.sub && subsub == rhs.subsub;
884         }
885
886         bool same_sub(IndexEntry const & rhs) const
887         {
888                 return main == rhs.main && sub == rhs.sub;
889         }
890
891         bool same_main(IndexEntry const & rhs) const
892         {
893                 return main == rhs.main;
894         }
895
896         docstring main;
897         docstring sub;
898         docstring subsub;
899         DocIterator dit;
900 };
901
902 bool operator<(IndexEntry const & lhs, IndexEntry const & rhs)
903 {
904         int comp = compare_no_case(lhs.main, rhs.main);
905         if (comp == 0)
906                 comp = compare_no_case(lhs.sub, rhs.sub);
907         if (comp == 0)
908                 comp = compare_no_case(lhs.subsub, rhs.subsub);
909         return (comp < 0);
910 }
911
912 } // namespace
913
914
915 docstring InsetPrintIndex::xhtml(XMLStream &, OutputParams const & op) const
916 {
917         BufferParams const & bp = buffer().masterBuffer()->params();
918
919         // we do not presently support multiple indices, so we refuse to print
920         // anything but the main index, so as not to generate multiple indices.
921         // NOTE Multiple index support would require some work. The reason
922         // is that the TOC does not know about multiple indices. Either it would
923         // need to be told about them (not a bad idea), or else the index entries
924         // would need to be collected differently, say, during validation.
925         if (bp.use_indices && getParam("type") != from_ascii("idx"))
926                 return docstring();
927
928         shared_ptr<Toc const> toc = buffer().tocBackend().toc("index");
929         if (toc->empty())
930                 return docstring();
931
932         // Collect the index entries in a form we can use them.
933         Toc::const_iterator it = toc->begin();
934         Toc::const_iterator const en = toc->end();
935         vector<IndexEntry> entries;
936         for (; it != en; ++it)
937                 if (it->isOutput())
938                         entries.push_back(IndexEntry(it->str(), it->dit()));
939
940         if (entries.empty())
941                 // not very likely that all the index entries are in notes or
942                 // whatever, but....
943                 return docstring();
944
945         stable_sort(entries.begin(), entries.end());
946
947         Layout const & lay = bp.documentClass().htmlTOCLayout();
948         string const & tocclass = lay.defaultCSSClass();
949         string const tocattr = "class='index " + tocclass + "'";
950
951         // we'll use our own stream, because we are going to defer everything.
952         // that's how we deal with the fact that we're probably inside a standard
953         // paragraph, and we don't want to be.
954         odocstringstream ods;
955         XMLStream xs(ods);
956
957         xs << xml::StartTag("div", tocattr);
958         xs << xml::StartTag(lay.htmltag(), lay.htmlattr())
959                  << translateIfPossible(from_ascii("Index"),
960                                   op.local_font->language()->lang())
961                  << xml::EndTag(lay.htmltag());
962         xs << xml::StartTag("ul", "class='main'");
963         Font const dummy;
964
965         vector<IndexEntry>::const_iterator eit = entries.begin();
966         vector<IndexEntry>::const_iterator const een = entries.end();
967         // tracks whether we are already inside a main entry (1),
968         // a sub-entry (2), or a sub-sub-entry (3). see below for the
969         // details.
970         int level = 1;
971         // the last one we saw
972         IndexEntry last;
973         int entry_number = -1;
974         for (; eit != een; ++eit) {
975                 Paragraph const & par = eit->dit.innerParagraph();
976                 if (entry_number == -1 || !eit->equal(last)) {
977                         if (entry_number != -1) {
978                                 // not the first time through the loop, so
979                                 // close last entry or entries, depending.
980                                 if (level == 3) {
981                                         // close this sub-sub-entry
982                                         xs << xml::EndTag("li") << xml::CR();
983                                         // is this another sub-sub-entry within the same sub-entry?
984                                         if (!eit->same_sub(last)) {
985                                                 // close this level
986                                                 xs << xml::EndTag("ul") << xml::CR();
987                                                 level = 2;
988                                         }
989                                 }
990                                 // the point of the second test here is that we might get
991                                 // here two ways: (i) by falling through from above; (ii) because,
992                                 // though the sub-entry hasn't changed, the sub-sub-entry has,
993                                 // which means that it is the first sub-sub-entry within this
994                                 // sub-entry. In that case, we do not want to close anything.
995                                 if (level == 2 && !eit->same_sub(last)) {
996                                         // close sub-entry
997                                         xs << xml::EndTag("li") << xml::CR();
998                                         // is this another sub-entry with the same main entry?
999                                         if (!eit->same_main(last)) {
1000                                                 // close this level
1001                                                 xs << xml::EndTag("ul") << xml::CR();
1002                                                 level = 1;
1003                                         }
1004                                 }
1005                                 // again, we can get here two ways: from above, or because we have
1006                                 // found the first sub-entry. in the latter case, we do not want to
1007                                 // close the entry.
1008                                 if (level == 1 && !eit->same_main(last)) {
1009                                         // close entry
1010                                         xs << xml::EndTag("li") << xml::CR();
1011                                 }
1012                         }
1013
1014                         // we'll be starting new entries
1015                         entry_number = 0;
1016
1017                         // We need to use our own stream, since we will have to
1018                         // modify what we get back.
1019                         odocstringstream ent;
1020                         XMLStream entstream(ent);
1021                         OutputParams ours = op;
1022                         ours.for_toc = true;
1023                         par.simpleLyXHTMLOnePar(buffer(), entstream, ours, dummy);
1024
1025                         // these will contain XHTML versions of the main entry, etc
1026                         // remember that everything will already have been escaped,
1027                         // so we'll need to use NextRaw() during output.
1028                         docstring main;
1029                         docstring sub;
1030                         docstring subsub;
1031                         extractSubentries(ent.str(), main, sub, subsub);
1032                         parseItem(main, true);
1033                         parseItem(sub, true);
1034                         parseItem(subsub, true);
1035
1036                         if (level == 3) {
1037                                 // another subsubentry
1038                                 xs << xml::StartTag("li", "class='subsubentry'")
1039                                    << XMLStream::ESCAPE_NONE << subsub;
1040                         } else if (level == 2) {
1041                                 // there are two ways we can be here:
1042                                 // (i) we can actually be inside a sub-entry already and be about
1043                                 //     to output the first sub-sub-entry. in this case, our sub
1044                                 //     and the last sub will be the same.
1045                                 // (ii) we can just have closed a sub-entry, possibly after also
1046                                 //     closing a list of sub-sub-entries. here our sub and the last
1047                                 //     sub are different.
1048                                 // only in the latter case do we need to output the new sub-entry.
1049                                 // note that in this case, too, though, the sub-entry might already
1050                                 // have a sub-sub-entry.
1051                                 if (eit->sub != last.sub)
1052                                         xs << xml::StartTag("li", "class='subentry'")
1053                                            << XMLStream::ESCAPE_NONE << sub;
1054                                 if (!subsub.empty()) {
1055                                         // it's actually a subsubentry, so we need to start that list
1056                                         xs << xml::CR()
1057                                            << xml::StartTag("ul", "class='subsubentry'")
1058                                            << xml::StartTag("li", "class='subsubentry'")
1059                                            << XMLStream::ESCAPE_NONE << subsub;
1060                                         level = 3;
1061                                 }
1062                         } else {
1063                                 // there are also two ways we can be here:
1064                                 // (i) we can actually be inside an entry already and be about
1065                                 //     to output the first sub-entry. in this case, our main
1066                                 //     and the last main will be the same.
1067                                 // (ii) we can just have closed an entry, possibly after also
1068                                 //     closing a list of sub-entries. here our main and the last
1069                                 //     main are different.
1070                                 // only in the latter case do we need to output the new main entry.
1071                                 // note that in this case, too, though, the main entry might already
1072                                 // have a sub-entry, or even a sub-sub-entry.
1073                                 if (eit->main != last.main)
1074                                         xs << xml::StartTag("li", "class='main'") << main;
1075                                 if (!sub.empty()) {
1076                                         // there's a sub-entry, too
1077                                         xs << xml::CR()
1078                                            << xml::StartTag("ul", "class='subentry'")
1079                                            << xml::StartTag("li", "class='subentry'")
1080                                            << XMLStream::ESCAPE_NONE << sub;
1081                                         level = 2;
1082                                         if (!subsub.empty()) {
1083                                                 // and a sub-sub-entry
1084                                                 xs << xml::CR()
1085                                                    << xml::StartTag("ul", "class='subsubentry'")
1086                                                    << xml::StartTag("li", "class='subsubentry'")
1087                                                    << XMLStream::ESCAPE_NONE << subsub;
1088                                                 level = 3;
1089                                         }
1090                                 }
1091                         }
1092                 }
1093                 // finally, then, we can output the index link itself
1094                 string const parattr = "href='#" + par.magicLabel() + "'";
1095                 xs << (entry_number == 0 ? ":" : ",");
1096                 xs << " " << xml::StartTag("a", parattr)
1097                    << ++entry_number << xml::EndTag("a");
1098                 last = *eit;
1099         }
1100         // now we have to close all the open levels
1101         while (level > 0) {
1102                 xs << xml::EndTag("li") << xml::EndTag("ul") << xml::CR();
1103                 --level;
1104         }
1105         xs << xml::EndTag("div") << xml::CR();
1106         return ods.str();
1107 }
1108
1109 } // namespace lyx