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