3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
7 * \author Jürgen Spitzmüller
9 * Full author contact details are available in file CREDITS.
13 #include "InsetInfo.h"
16 #include "BufferParams.h"
17 #include "BufferView.h"
20 #include "CutAndPaste.h"
22 #include "FuncRequest.h"
23 #include "FuncStatus.h"
24 #include "InsetGraphics.h"
25 #include "InsetSpecialChar.h"
27 #include "LaTeXFeatures.h"
29 #include "LayoutFile.h"
30 #include "LyXAction.h"
34 #include "output_docbook.h"
35 #include "Paragraph.h"
36 #include "ParIterator.h"
37 #include "ParagraphParameters.h"
40 #include "frontends/Application.h"
42 #include "support/Changer.h"
43 #include "support/convert.h"
44 #include "support/debug.h"
45 #include "support/docstream.h"
46 #include "support/docstring_list.h"
47 #include "support/ExceptionMessage.h"
48 #include "support/FileName.h"
49 #include "support/filetools.h"
50 #include "support/gettext.h"
51 #include "support/Length.h"
52 #include "support/Messages.h"
53 #include "support/lstrings.h"
54 #include "support/qstring_helpers.h"
55 #include "support/Translator.h"
60 #include <QtGui/QImage>
65 using namespace lyx::support;
71 typedef Translator<InsetInfoParams::info_type, string> NameTranslator;
73 NameTranslator const initTranslator()
75 NameTranslator translator(InsetInfoParams::UNKNOWN_INFO, "unknown");
77 translator.addPair(InsetInfoParams::SHORTCUTS_INFO, "shortcuts");
78 translator.addPair(InsetInfoParams::SHORTCUT_INFO, "shortcut");
79 translator.addPair(InsetInfoParams::LYXRC_INFO, "lyxrc");
80 translator.addPair(InsetInfoParams::PACKAGE_INFO, "package");
81 translator.addPair(InsetInfoParams::TEXTCLASS_INFO, "textclass");
82 translator.addPair(InsetInfoParams::MENU_INFO, "menu");
83 translator.addPair(InsetInfoParams::L7N_INFO, "l7n");
84 translator.addPair(InsetInfoParams::ICON_INFO, "icon");
85 translator.addPair(InsetInfoParams::BUFFER_INFO, "buffer");
86 translator.addPair(InsetInfoParams::LYX_INFO, "lyxinfo");
87 translator.addPair(InsetInfoParams::VCS_INFO, "vcs");
88 translator.addPair(InsetInfoParams::DATE_INFO, "date");
89 translator.addPair(InsetInfoParams::MODDATE_INFO, "moddate");
90 translator.addPair(InsetInfoParams::FIXDATE_INFO, "fixdate");
91 translator.addPair(InsetInfoParams::TIME_INFO, "time");
92 translator.addPair(InsetInfoParams::MODTIME_INFO, "modtime");
93 translator.addPair(InsetInfoParams::FIXTIME_INFO, "fixtime");
98 /// The translator between the information type enum and corresponding string.
99 NameTranslator const & nameTranslator()
101 static NameTranslator const translator = initTranslator();
106 typedef Translator<InsetInfoParams::info_type, string> DefaultValueTranslator;
108 DefaultValueTranslator const initDVTranslator()
110 DefaultValueTranslator translator(InsetInfoParams::UNKNOWN_INFO, "");
112 translator.addPair(InsetInfoParams::SHORTCUTS_INFO, "info-insert");
113 translator.addPair(InsetInfoParams::SHORTCUT_INFO, "info-insert");
114 translator.addPair(InsetInfoParams::LYXRC_INFO, "user_name");
115 translator.addPair(InsetInfoParams::PACKAGE_INFO, "graphics");
116 translator.addPair(InsetInfoParams::TEXTCLASS_INFO, "article");
117 translator.addPair(InsetInfoParams::MENU_INFO, "info-insert");
118 translator.addPair(InsetInfoParams::L7N_INFO, "");
119 translator.addPair(InsetInfoParams::ICON_INFO, "info-insert");
120 translator.addPair(InsetInfoParams::BUFFER_INFO, "name-noext");
121 translator.addPair(InsetInfoParams::LYX_INFO, "version");
122 translator.addPair(InsetInfoParams::VCS_INFO, "revision");
123 translator.addPair(InsetInfoParams::DATE_INFO, "loclong");
124 translator.addPair(InsetInfoParams::MODDATE_INFO, "loclong");
125 translator.addPair(InsetInfoParams::FIXDATE_INFO, "loclong");
126 translator.addPair(InsetInfoParams::TIME_INFO, "long");
127 translator.addPair(InsetInfoParams::MODTIME_INFO, "long");
128 translator.addPair(InsetInfoParams::FIXTIME_INFO, "long");
133 /// The translator between the information type enum and some sensible default value.
134 DefaultValueTranslator const & defaultValueTranslator()
136 static DefaultValueTranslator const translator = initDVTranslator();
143 /////////////////////////////////////////////////////////////////////
147 ///////////////////////////////////////////////////////////////////////
149 InsetInfoParams infoparams;
152 set<string> getTexFileList(string const & filename)
155 FileName const file = libFileSearch(string(), filename);
160 vector<docstring> doclist =
161 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
163 // Normalise paths like /foo//bar ==> /foo/bar
164 // No "auto const &" because doc is modified later
165 // coverity[auto_causes_copy]
166 for (auto doc : doclist) {
167 doc = subst(doc, from_ascii("\r"), docstring());
168 while (contains(doc, from_ascii("//")))
169 doc = subst(doc, from_ascii("//"), from_ascii("/"));
171 list.insert(removeExtension(onlyFileName(to_utf8(doc))));
178 bool translateString(docstring const & in, docstring & out, string const & lcode)
180 out = translateIfPossible(in, lcode);
186 docstring InsetInfoParams::getDate(string const & iname, QDate const date) const
190 loc = QLocale(toqstr(lang->code()));
192 return qstring_to_ucs4(loc.toString(date, QLocale::LongFormat));
193 else if (iname == "short")
194 return qstring_to_ucs4(loc.toString(date, QLocale::ShortFormat));
195 else if (iname == "ISO")
196 return qstring_to_ucs4(date.toString(Qt::ISODate));
197 else if (iname == "loclong")
198 return lang ? qstring_to_ucs4(loc.toString(date, toqstr(lang->dateFormat(0))))
199 : _("No long date format (language unknown)!");
200 else if (iname == "locmedium")
201 return lang ? qstring_to_ucs4(loc.toString(date, toqstr(lang->dateFormat(1))))
202 : _("No medium date format (language unknown)!");
203 else if (iname == "locshort")
204 return lang ? qstring_to_ucs4(loc.toString(date, toqstr(lang->dateFormat(2))))
205 : _("No short date format (language unknown)!");
207 return qstring_to_ucs4(loc.toString(date, toqstr(iname)));
211 docstring InsetInfoParams::getTime(string const & iname, QTime const time) const
215 loc = QLocale(toqstr(lang->code()));
217 return qstring_to_ucs4(loc.toString(time, QLocale::LongFormat));
218 else if (iname == "short")
219 return qstring_to_ucs4(loc.toString(time, QLocale::ShortFormat));
220 else if (iname == "ISO")
221 return qstring_to_ucs4(time.toString(Qt::ISODate));
223 return qstring_to_ucs4(loc.toString(time, toqstr(iname)));
227 vector<pair<string,docstring>> InsetInfoParams::getArguments(Buffer const * buf,
228 string const & itype) const
230 vector<pair<string,docstring>> result;
232 switch (nameTranslator().find(itype)) {
234 result.push_back(make_pair("invalid", _("Please select a valid type!")));
241 result.push_back(make_pair("custom", _("Custom")));
242 for (auto const & name_code : lyxaction) {
243 string const lfun = name_code.first;
245 result.push_back(make_pair(lfun, from_ascii(lfun)));
251 result.push_back(make_pair("custom", _("Custom")));
255 result.push_back(make_pair("custom", _("Custom")));
256 set<string> rcs = lyxrc.getRCs();
257 for (auto const & rc : rcs)
258 result.push_back(make_pair(rc, from_ascii(rc)));
263 case TEXTCLASS_INFO: {
264 result.push_back(make_pair("custom", _("Custom")));
265 string const filename = (itype == "package") ? "styFiles.lst"
267 set<string> flist = getTexFileList(filename);
268 for (auto const & f : flist)
269 result.push_back(make_pair(f, from_utf8(f)));
274 result.push_back(make_pair("name", _("File name (with extension)")));
275 result.push_back(make_pair("name-noext", _("File name (without extension)")));
276 result.push_back(make_pair("path", _("File path")));
277 result.push_back(make_pair("class", _("Used text class")));
281 if (!buf->lyxvc().inUse()) {
282 result.push_back(make_pair("invalid", _("No version control!")));
285 result.push_back(make_pair("revision", _("Revision[[Version Control]]")));
286 result.push_back(make_pair("revision-abbrev", _("Abbreviated revision[[Version Control]]")));
287 result.push_back(make_pair("tree-revision", _("Tree revision")));
288 result.push_back(make_pair("author", _("Author")));
289 result.push_back(make_pair("date", _("Date")));
290 result.push_back(make_pair("time", _("Time[[of day]]")));
295 result.push_back(make_pair("version", _("LyX version")));
296 result.push_back(make_pair("layoutformat", _("LyX layout format")));
302 // TODO: away from a release, use parseDate instead.
303 string const dt = split(name, '@');
305 if (itype == "moddate")
306 #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
307 date = QDateTime::fromSecsSinceEpoch(buf->fileName().lastModified()).date();
309 date = QDateTime::fromTime_t(buf->fileName().lastModified()).date();
311 else if (itype == "fixdate" && !dt.empty()) {
312 QDate const gdate = QDate::fromString(toqstr(dt), Qt::ISODate);
313 date = (gdate.isValid()) ? gdate : QDate::currentDate();
315 date = QDate::currentDate();
316 result.push_back(make_pair("long",getDate("long", date)));
317 result.push_back(make_pair("short", getDate("short", date)));
318 result.push_back(make_pair("loclong", getDate("loclong", date)));
319 result.push_back(make_pair("locmedium", getDate("locmedium", date)));
320 result.push_back(make_pair("locshort", getDate("locshort", date)));
321 result.push_back(make_pair("ISO", getDate("ISO", date)));
322 result.push_back(make_pair("yyyy", getDate("yyyy", date)));
323 result.push_back(make_pair("MMMM", getDate("MMMM", date)));
324 result.push_back(make_pair("MMM", getDate("MMM", date)));
325 result.push_back(make_pair("dddd", getDate("dddd", date)));
326 result.push_back(make_pair("ddd", getDate("ddd", date)));
327 result.push_back(make_pair("custom", _("Custom")));
333 // TODO: away from a release, use parseTime instead.
334 string const tt = split(name, '@');
336 if (itype == "modtime")
337 #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
338 time = QDateTime::fromSecsSinceEpoch(buf->fileName().lastModified()).time();
340 time = QDateTime::fromTime_t(buf->fileName().lastModified()).time();
342 else if (itype == "fixtime" && !tt.empty()) {
343 QTime const gtime = QTime::fromString(toqstr(tt), Qt::ISODate);
344 time = (gtime.isValid()) ? gtime : QTime::currentTime();
346 time = QTime::currentTime();
347 result.push_back(make_pair("long",getTime("long", time)));
348 result.push_back(make_pair("short", getTime("short", time)));
349 result.push_back(make_pair("ISO", getTime("ISO", time)));
350 result.push_back(make_pair("custom", _("Custom")));
359 bool InsetInfoParams::validateArgument(Buffer const * buf, docstring const & arg,
360 bool const usedefaults) const
363 string name = trim(split(to_utf8(arg), type, ' '));
364 if (name.empty() && usedefaults)
365 name = defaultValueTranslator().find(type);
367 switch (nameTranslator().find(type)) {
374 FuncRequest func = lyxaction.lookupFunc(name);
375 return func.action() != LFUN_UNKNOWN_ACTION;
379 return !name.empty();
382 FuncCode const action = lyxaction.lookupFunc(name).action();
383 if (action == LFUN_UNKNOWN_ACTION) {
384 string dir = "images";
385 return !imageLibFileSearch(dir, name, "svgz,png").empty();
391 set<string> rcs = lyxrc.getRCs();
392 return rcs.find(name) != rcs.end();
400 return (name == "name" || name == "name-noext"
401 || name == "path" || name == "class");
404 if (name == "revision" || name == "revision-abbrev" || name == "tree-revision"
405 || name == "author" || name == "date" || name == "time")
406 return buf->lyxvc().inUse();
410 return name == "version" || name == "layoutformat";
415 date = split(name, piece, '@');
416 if (!date.empty() && !QDate::fromString(toqstr(date), Qt::ISODate).isValid())
424 if (name == "long" || name == "short" || name == "ISO")
427 QDate date = QDate::currentDate();
428 return !date.toString(toqstr(name)).isEmpty();
434 time = split(name, piece, '@');
435 if (!time.empty() && !QTime::fromString(toqstr(time), Qt::ISODate).isValid())
443 if (name == "long" || name == "short" || name == "ISO")
446 QTime time = QTime::currentTime();
447 return !time.toString(toqstr(name)).isEmpty();
458 string InsetInfoParams::infoType() const
460 return nameTranslator().find(type);
465 /////////////////////////////////////////////////////////////////////////
469 /////////////////////////////////////////////////////////////////////////
474 class InsetGraphicsTight : public InsetGraphics
478 explicit InsetGraphicsTight(Buffer * buf) : InsetGraphics(buf) {}
481 int leftOffset(BufferView const *) const override { return 0; }
483 int rightOffset(BufferView const *) const override { return 0; }
489 InsetInfo::InsetInfo(Buffer * buf, string const & info)
490 : InsetCollapsible(buf), initialized_(false)
492 params_.type = InsetInfoParams::UNKNOWN_INFO;
493 params_.force_ltr = false;
499 Inset * InsetInfo::editXY(Cursor & cur, int x, int y)
501 // do not allow the cursor to be set in this Inset
502 return Inset::editXY(cur, x, y);
506 docstring InsetInfo::layoutName() const
508 return from_ascii("Info:" + params_.infoType());
512 docstring InsetInfo::toolTip(BufferView const &, int, int) const
515 switch (nameTranslator().find(params_.infoType())) {
516 case InsetInfoParams::UNKNOWN_INFO:
517 result = _("Invalid information inset");
519 case InsetInfoParams::SHORTCUT_INFO:
520 result = bformat(_("The keybard shortcut for the function '%1$s'"),
521 from_utf8(params_.name));
523 case InsetInfoParams::SHORTCUTS_INFO:
524 result = bformat(_("The keybard shortcuts for the function '%1$s'"),
525 from_utf8(params_.name));
527 case InsetInfoParams::MENU_INFO:
528 result = bformat(_("The menu location for the function '%1$s'"),
529 from_utf8(params_.name));
531 case InsetInfoParams::L7N_INFO:
532 result = bformat(_("The localization for the string '%1$s'"),
533 from_utf8(params_.name));
535 case InsetInfoParams::ICON_INFO:
536 result = bformat(_("The toolbar icon for the function '%1$s'"),
537 from_utf8(params_.name));
539 case InsetInfoParams::LYXRC_INFO:
540 result = bformat(_("The preference setting for the preference key '%1$s'"),
541 from_utf8(params_.name));
543 case InsetInfoParams::PACKAGE_INFO:
544 result = bformat(_("Availability of the LaTeX package '%1$s'"),
545 from_utf8(params_.name));
547 case InsetInfoParams::TEXTCLASS_INFO:
548 result = bformat(_("Availability of the LaTeX class '%1$s'"),
549 from_utf8(params_.name));
551 case InsetInfoParams::BUFFER_INFO:
552 if (params_.name == "name")
553 result = _("The name of this file (incl. extension)");
554 else if (params_.name == "name-noext")
555 result = _("The name of this file (without extension)");
556 else if (params_.name == "path")
557 result = _("The path where this file is saved");
558 else if (params_.name == "class")
559 result = _("The class this document uses");
561 case InsetInfoParams::VCS_INFO:
562 if (params_.name == "revision")
563 result = _("Version control revision");
564 else if (params_.name == "revision-abbrev")
565 result = _("Version control abbreviated revision");
566 else if (params_.name == "tree-revision")
567 result = _("Version control tree revision");
568 else if (params_.name == "author")
569 result = _("Version control author");
570 else if (params_.name == "date")
571 result = _("Version control date");
572 else if (params_.name == "time")
573 result = _("Version control time");
575 case InsetInfoParams::LYX_INFO:
576 if (params_.name == "version")
577 result = _("The current LyX version");
578 else if (params_.name == "layoutformat")
579 result = _("The current LyX layout format");
581 case InsetInfoParams::DATE_INFO:
582 result = _("The current date");
584 case InsetInfoParams::MODDATE_INFO:
585 result = _("The date of last save");
587 case InsetInfoParams::FIXDATE_INFO:
588 result = _("A static date");
590 case InsetInfoParams::TIME_INFO:
591 result = _("The current time");
593 case InsetInfoParams::MODTIME_INFO:
594 result = _("The time of last save");
596 case InsetInfoParams::FIXTIME_INFO:
597 result = _("A static time");
605 void InsetInfo::read(Lexer & lex)
610 token = lex.getString();
611 if (token == "type") {
613 token = lex.getString();
614 params_.type = nameTranslator().find(token);
615 } else if (token == "arg") {
617 params_.name = lex.getString();
618 } else if (token == "\\end_inset")
621 if (token != "\\end_inset") {
622 lex.printError("Missing \\end_inset at this point");
623 throw ExceptionMessage(WarningException,
624 _("Missing \\end_inset at this point."),
630 void InsetInfo::write(ostream & os) const
632 os << "Info\ntype \"" << params_.infoType()
633 << "\"\narg " << Lexer::quoteString(params_.name);
637 bool InsetInfo::showInsetDialog(BufferView * bv) const
639 bv->showDialog("info");
644 bool InsetInfo::getStatus(Cursor & cur, FuncRequest const & cmd,
645 FuncStatus & flag) const
647 switch (cmd.action()) {
648 case LFUN_INSET_SETTINGS:
649 return InsetCollapsible::getStatus(cur, cmd, flag);
651 case LFUN_INSET_DIALOG_UPDATE:
652 case LFUN_INSET_COPY_AS:
653 case LFUN_INSET_DISSOLVE:
654 flag.setEnabled(true);
657 case LFUN_INSET_MODIFY:
658 if (nameTranslator().find(cmd.getArg(0)) == InsetInfoParams::UNKNOWN_INFO)
659 return Inset::getStatus(cur, cmd, flag);
660 if (params_.validateArgument(&buffer(), cmd.argument())) {
661 flag.setEnabled(true);
663 string name = trim(split(to_utf8(cmd.argument()), typestr, ' '));
664 InsetInfoParams::info_type type = nameTranslator().find(typestr);
665 string origname = params_.name;
666 if (type == InsetInfoParams::FIXDATE_INFO
667 || type == InsetInfoParams::FIXTIME_INFO)
668 split(params_.name, origname, '@');
669 flag.setOnOff(type == params_.type && name == origname);
680 void InsetInfo::doDispatch(Cursor & cur, FuncRequest & cmd)
682 switch (cmd.action()) {
683 case LFUN_INSET_MODIFY:
684 if (nameTranslator().find(cmd.getArg(0)) == InsetInfoParams::UNKNOWN_INFO) {
689 setInfo(to_utf8(cmd.argument()));
690 cur.forceBufferUpdate();
691 initialized_ = false;
694 case LFUN_INSET_COPY_AS: {
695 cap::clearSelection();
697 copy.pushBackward(*this);
701 copy.pit() = copy.lastpit();
702 copy.pos() = copy.lastpos();
704 cap::copySelection(copy);
709 InsetCollapsible::doDispatch(cur, cmd);
715 void InsetInfo::setInfo(string const & info)
720 string saved_date_specifier;
721 // Store old date specifier for potential re-use
722 if (!params_.name.empty())
723 saved_date_specifier = split(params_.name, '@');
726 params_.name = trim(split(info, type, ' '));
727 params_.type = nameTranslator().find(type);
728 if (params_.name.empty())
729 params_.name = defaultValueTranslator().find(params_.type);
730 if (params_.type == InsetInfoParams::FIXDATE_INFO) {
731 string const date_specifier = split(params_.name, '@');
732 // If an explicit new fix date is specified, use that
733 // Otherwise, use the old one or, if there is none,
735 if (date_specifier.empty()) {
736 if (saved_date_specifier.empty())
737 params_.name += "@" + fromqstr(QDate::currentDate().toString(Qt::ISODate));
739 params_.name += "@" + saved_date_specifier;
742 else if (params_.type == InsetInfoParams::FIXTIME_INFO) {
743 string const time_specifier = split(params_.name, '@');
744 // If an explicit new fix time is specified, use that
745 // Otherwise, use the old one or, if there is none,
747 if (time_specifier.empty()) {
748 if (saved_date_specifier.empty())
749 params_.name += "@" + fromqstr(QTime::currentTime().toString(Qt::ISODate));
751 params_.name += "@" + saved_date_specifier;
757 void InsetInfo::error(docstring const & err, Language const * lang)
759 docstring const res = translateIfPossible(err, lang->code());
760 bool const translated = res != err;
761 // If the string is not translated, we use default lang (English)
762 Font const f = translated ? Font(inherit_font, lang) : Font(inherit_font);
763 setText(bformat(res, from_utf8(params_.name)), f, false);
767 void InsetInfo::info(docstring const & err, Language const * lang)
769 docstring const res = translateIfPossible(err, lang->code());
770 bool const translated = res != err;
771 // If the string is not translated, we use default lang (English)
772 Font const f = translated ? Font(inherit_font, lang) : Font(inherit_font);
773 setText(translateIfPossible(err, lang->code()), f, false);
777 void InsetInfo::setText(docstring const & str, Language const * lang)
779 setText(str, Font(inherit_font, lang), false);
783 bool InsetInfo::forceLTR(OutputParams const &) const
785 return params_.force_ltr;
789 bool InsetInfo::forceLocalFontSwitch() const
791 return params_.type == InsetInfoParams::MENU_INFO
792 || params_.type == InsetInfoParams::SHORTCUTS_INFO
793 || params_.type == InsetInfoParams::SHORTCUT_INFO
794 || params_.type == InsetInfoParams::L7N_INFO;
798 void InsetInfo::metrics(MetricsInfo & mi, Dimension & dim) const
800 const_cast<InsetInfo *>(this)->build();
801 InsetCollapsible::metrics(mi, dim);
805 void InsetInfo::draw(PainterInfo & pi, int x, int y) const
807 Changer chg = changeVar(lyxrc.mark_foreign_language, false);
808 InsetCollapsible::draw(pi, x, y);
811 void InsetInfo::updateBuffer(ParIterator const & it, UpdateType utype, bool const deleted)
814 // If the Buffer is a clone, then we neither need nor want to do any
815 // of what follows. We want, rather, just to inherit how things were
816 // in the original Buffer. This is especially important for VCS.
817 // Otherwise, we could in principle have different settings here
818 // than in the Buffer we were exporting.
819 // However, we need to check whether the inset is in an intitle
821 if (buffer().isClone()) {
822 InsetText::checkIntitleContext(it);
825 BufferParams const & bp = buffer().params();
826 params_.lang = it.paragraph().getFontSettings(bp, it.pos()).language();
827 InsetCollapsible::updateBuffer(it, utype, deleted);
831 void InsetInfo::build()
833 // If the Buffer is a clone, then we neither need nor want to do any
834 // of what follows. We want, rather, just to inherit how things were
835 // in the original Buffer. This is especially important for VCS.
836 // Otherwise, we could in principle have different settings here
837 // than in the Buffer we were exporting.
838 if (buffer().isClone())
841 Language const * tryguilang = languages.getFromCode(Messages::guiLanguage());
842 // Some info insets use the language of the GUI (if available)
843 Language const * guilang = tryguilang ? tryguilang : params_.lang;
845 params_.force_ltr = !params_.lang->rightToLeft();
846 // This is just to get the string into the po files
848 switch (params_.type) {
849 case InsetInfoParams::UNKNOWN_INFO:
850 gui = _("Unknown Info!");
851 info(from_ascii("Unknown Info!"), params_.lang);
852 initialized_ = false;
854 case InsetInfoParams::SHORTCUT_INFO:
855 case InsetInfoParams::SHORTCUTS_INFO: {
856 // shortcuts can change, so we need to re-do this each time
857 FuncRequest const func = lyxaction.lookupFunc(params_.name);
858 if (func.action() == LFUN_UNKNOWN_ACTION) {
859 gui = _("Unknown action %1$s");
860 error(from_ascii("Unknown action %1$s"), params_.lang);
863 KeyMap::Bindings bindings = theTopLevelKeymap().findBindings(func);
864 if (bindings.empty()) {
865 gui = _("undefined");
866 info(from_ascii("undefined"), params_.lang);
870 docstring seq_untranslated;
871 if (params_.type == InsetInfoParams::SHORTCUT_INFO) {
872 sequence = bindings.begin()->print(KeySequence::ForGui);
873 seq_untranslated = bindings.begin()->print(KeySequence::ForGui, true);
875 sequence = theTopLevelKeymap().printBindings(func, KeySequence::ForGui);
876 seq_untranslated = theTopLevelKeymap().printBindings(func, KeySequence::ForGui, true);
878 // QKeySequence returns special characters for keys on the mac
879 // Since these are not included in many fonts, we
880 // re-translate them to textual names (see #10641)
881 odocstringstream ods;
882 string const lcode = params_.lang->code();
884 bool is_translated = sequence != seq_untranslated;
885 for (char_type const c : sequence) {
888 gui = _("Return[[Key]]");
889 is_translated = translateString(from_ascii("Return[[Key]]"), trans, lcode);
892 case 0x21b9://Tab both directions (Win)
893 gui = _("Tab[[Key]]");
894 is_translated = translateString(from_ascii("Tab[[Key]]"), trans, lcode);
897 case 0x21de://Qt::Key_PageUp
899 is_translated = translateString(from_ascii("PgUp"), trans, lcode);
902 case 0x21df://Qt::Key_PageDown
904 is_translated = translateString(from_ascii("PgDown"), trans, lcode);
907 case 0x21e4://Qt::Key_Backtab
909 is_translated = translateString(from_ascii("Backtab"), trans, lcode);
912 case 0x21e5://Qt::Key_Tab
914 is_translated = translateString(from_ascii("Tab"), trans, lcode);
919 is_translated = translateString(from_ascii("Shift"), trans, lcode);
922 case 0x21ea://Qt::Key_CapsLock
924 is_translated = translateString(from_ascii("CapsLock"), trans, lcode);
927 case 0x2303://Control
928 gui = _("Control[[Key]]");
929 is_translated = translateString(from_ascii("Control[[Key]]"), trans, lcode);
933 gui = _("Command[[Key]]");
934 is_translated = translateString(from_ascii("Command[[Key]]"), trans, lcode);
937 case 0x2324://Qt::Key_Enter
938 gui = _("Return[[Key]]");
939 is_translated = translateString(from_ascii("Return[[Key]]"), trans, lcode);
942 case 0x2325://Option key
943 gui = _("Option[[Key]]");
944 is_translated = translateString(from_ascii("Option[[Key]]"), trans, lcode);
947 case 0x2326://Qt::Key_Delete
948 gui = _("Delete[[Key]]");
949 is_translated = translateString(from_ascii("Delete[[Key]]"), trans, lcode);
952 case 0x232b://Qt::Key_Backspace
954 is_translated = translateString(from_ascii("Fn+Del"), trans, lcode);
957 case 0x238b://Qt::Key_Escape
959 is_translated = translateString(from_ascii("Esc"), trans, lcode);
966 setText(ods.str(), is_translated ? guilang : nullptr);
967 params_.force_ltr = !is_translated || (!guilang->rightToLeft() && !params_.lang->rightToLeft());
970 case InsetInfoParams::LYXRC_INFO: {
971 // this information could change, if the preferences are changed,
972 // so we will recalculate each time through.
974 if (params_.name.empty()) {
975 gui = _("undefined");
976 info(from_ascii("undefined"), params_.lang);
979 // FIXME this uses the serialization mechanism to get the info
980 // we want, which i guess works but is a bit strange.
981 lyxrc.write(oss, true, params_.name);
982 string result = oss.str();
983 if (result.size() < 2) {
984 gui = _("undefined");
985 info(from_ascii("undefined"), params_.lang);
988 string::size_type loc = result.rfind("\n", result.size() - 2);
989 loc = loc == string::npos ? 0 : loc + 1;
990 if (result.size() < loc + params_.name.size() + 1
991 || result.substr(loc + 1, params_.name.size()) != params_.name) {
992 gui = _("undefined");
993 info(from_ascii("undefined"), params_.lang);
996 // remove leading comments and \\name and space
997 result = result.substr(loc + params_.name.size() + 2);
1000 result = rtrim(result, "\n");
1001 result = trim(result, "\"");
1005 setText(from_utf8(result), params_.lang);
1008 case InsetInfoParams::PACKAGE_INFO:
1009 // TODO: away from a release, replace with getPackageInfo.
1010 // only need to do this once.
1013 // check in packages.lst
1015 // we also allow version check with version separated by blank
1016 if (contains(params_.name, ' ')) {
1018 string const version = split(params_.name, name, ' ');
1019 int const y = convert<int>(version.substr(0,4));
1020 int const m = convert<int>(version.substr(4,2));
1021 int const d = convert<int>(version.substr(6,2));
1022 available = LaTeXFeatures::isAvailableAtLeastFrom(name, y, m, d);
1024 available = LaTeXFeatures::isAvailable(params_.name);
1028 info(from_ascii("yes"), params_.lang);
1031 info(from_ascii("no"), params_.lang);
1033 initialized_ = true;
1036 case InsetInfoParams::TEXTCLASS_INFO: {
1037 // TODO: when away from a release, replace with getTextClassInfo.
1038 // the TextClass can change
1039 LayoutFileList const & list = LayoutFileList::get();
1040 bool available = false;
1041 // params_.name is the class name
1042 if (list.haveClass(params_.name))
1043 available = list[params_.name].isTeXClassAvailable();
1046 info(from_ascii("yes"), params_.lang);
1049 info(from_ascii("no"), params_.lang);
1053 case InsetInfoParams::MENU_INFO: {
1054 // only need to do this once.
1057 docstring_list names;
1058 FuncRequest func = lyxaction.lookupFunc(params_.name);
1059 if (func.action() == LFUN_UNKNOWN_ACTION) {
1060 gui = _("Unknown action %1$s");
1061 error(from_ascii("Unknown action %1$s"), params_.lang);
1064 if (func.action() == LFUN_BUFFER_VIEW || func.action() == LFUN_BUFFER_UPDATE)
1065 // The default output format is in the menu without argument,
1066 // so strip it here.
1067 if (func.argument() == from_ascii(buffer().params().getDefaultOutputFormat()))
1068 func = FuncRequest(func.action());
1069 // iterate through the menubackend to find it
1071 gui = _("Can't determine menu entry for action %1$s in batch mode");
1072 error(from_ascii("Can't determine menu entry for action %1$s in batch mode"), params_.lang);
1073 initialized_ = true;
1076 // and we will not keep trying if we fail
1077 initialized_ = theApp()->hasBufferView();
1078 if (!theApp()->searchMenu(func, names)) {
1079 gui = _("No menu entry for action %1$s");
1080 error(from_ascii("No menu entry for action %1$s"), params_.lang);
1083 // if found, return its path.
1085 Paragraph & par = paragraphs().front();
1086 Font const f(inherit_font, guilang);
1087 params_.force_ltr = !guilang->rightToLeft();
1089 //fu.fontInfo().setUnderbar(FONT_ON);
1090 for (docstring const & name : names) {
1091 // do not insert > for the top level menu item
1092 if (&name != &names.front())
1093 par.insertInset(par.size(), new InsetSpecialChar(InsetSpecialChar::MENU_SEPARATOR),
1094 f, Change(Change::UNCHANGED));
1095 //FIXME: add proper underlines here. This
1096 // involves rewriting searchMenu used above to
1097 // return a vector of menus. If we do not do
1098 // that, we might as well use below
1099 // Paragraph::insert on each string (JMarc)
1100 for (char_type c : name)
1101 par.insertChar(par.size(), c, f, Change(Change::UNCHANGED));
1105 case InsetInfoParams::L7N_INFO: {
1106 // TODO: away from a release, use getNormalizedL7N instead.
1107 docstring locstring = _(params_.name);
1108 // Remove trailing colons
1109 locstring = rtrim(locstring, ":");
1110 // Remove menu accelerators
1111 if (contains(locstring, from_ascii("|"))) {
1112 docstring nlocstring;
1113 rsplit(locstring, nlocstring, '|');
1114 locstring = nlocstring;
1116 // Remove Qt accelerators, but keep literal ampersands
1117 locstring = subst(locstring, from_ascii(" & "), from_ascii("</amp;>"));
1118 locstring = subst(locstring, from_ascii("&"), docstring());
1119 locstring = subst(locstring, from_ascii("</amp;>"), from_ascii(" & "));
1120 setText(locstring, guilang);
1121 params_.force_ltr = !guilang->rightToLeft() && !params_.lang->rightToLeft();
1124 case InsetInfoParams::ICON_INFO: {
1125 // only need to do this once.
1128 // and we will not keep trying if we fail
1129 initialized_ = true;
1130 FuncRequest func = lyxaction.lookupFunc(params_.name);
1131 docstring icon_name = frontend::Application::iconName(func, true);
1132 FileName file(to_utf8(icon_name));
1133 if (file.onlyFileNameWithoutExt() == "unknown") {
1134 string dir = "images";
1135 FileName file2(imageLibFileSearch(dir, params_.name, "svgz,png"));
1141 int percent_scale = 100;
1143 // Compute the scale factor for the icon such that its
1144 // width on screen is equal to 1em in pixels.
1145 // The scale factor is rounded to the integer nearest
1146 // to the float value of the ratio 100*iconsize/imgsize.
1147 int imgsize = QImage(toqstr(file.absFileName())).width();
1149 int iconsize = Length(1, Length::EM).inPixels(1);
1150 percent_scale = (100 * iconsize + imgsize / 2)/imgsize;
1153 InsetGraphicsTight * inset = new InsetGraphicsTight(buffer_);
1154 InsetGraphicsParams igp;
1155 igp.filename = file;
1156 igp.lyxscale = percent_scale;
1157 igp.scale = string();
1158 igp.width = Length(1, Length::EM);
1159 if (contains(file.absoluteFilePath(), from_ascii("math"))
1160 || contains(file.absoluteFilePath(), from_ascii("ert-insert"))
1161 || suffixIs(file.onlyPath().absoluteFilePath(), from_ascii("ipa")))
1162 igp.darkModeSensitive = true;
1163 inset->setParams(igp);
1165 Font const f(inherit_font, params_.lang);
1166 paragraphs().front().insertInset(0, inset, f,
1167 Change(Change::UNCHANGED));
1170 case InsetInfoParams::BUFFER_INFO: {
1171 // TODO: away from a release, replace by getBufferInfo.
1172 // this could all change, so we will recalculate each time
1173 if (params_.name == "name")
1174 setText(from_utf8(buffer().fileName().onlyFileName()), params_.lang);
1175 else if (params_.name == "name-noext")
1176 setText(from_utf8(buffer().fileName().onlyFileNameWithoutExt()), params_.lang);
1177 else if (params_.name == "path")
1178 setText(from_utf8(os::latex_path(buffer().filePath())), params_.lang);
1179 else if (params_.name == "class")
1180 setText(from_utf8(buffer().params().documentClass().name()), params_.lang);
1183 case InsetInfoParams::VCS_INFO: {
1184 // TODO: away from a release, replace by getVCSInfo.
1185 // this information could change, in principle, so we will
1186 // recalculate each time through
1187 if (!buffer().lyxvc().inUse()) {
1188 gui = _("No version control!");
1189 info(from_ascii("No version control!"), params_.lang);
1192 LyXVC::RevisionInfo itype = LyXVC::Unknown;
1193 if (params_.name == "revision")
1194 itype = LyXVC::File;
1195 else if (params_.name == "revision-abbrev")
1196 itype = LyXVC::FileAbbrev;
1197 else if (params_.name == "tree-revision")
1198 itype = LyXVC::Tree;
1199 else if (params_.name == "author")
1200 itype = LyXVC::Author;
1201 else if (params_.name == "time")
1202 itype = LyXVC::Time;
1203 else if (params_.name == "date")
1204 itype = LyXVC::Date;
1205 string binfo = buffer().lyxvc().revisionInfo(itype);
1206 if (binfo.empty()) {
1207 gui = _("%1$s[[vcs data]] unknown");
1208 error(from_ascii("%1$s[[vcs data]] unknown"), params_.lang);
1210 setText(from_utf8(binfo), params_.lang);
1213 case InsetInfoParams::LYX_INFO:
1214 // only need to do this once.
1217 if (params_.name == "version")
1218 setText(from_ascii(lyx_version), params_.lang);
1219 else if (params_.name == "layoutformat")
1220 setText(convert<docstring>(LAYOUT_FORMAT), params_.lang);
1221 initialized_ = true;
1223 case InsetInfoParams::DATE_INFO:
1224 case InsetInfoParams::MODDATE_INFO:
1225 case InsetInfoParams::FIXDATE_INFO: {
1226 // TODO: away from a release, use parseDate instead.
1227 string date_format = params_.name;
1228 string const date_specifier = (params_.type == InsetInfoParams::FIXDATE_INFO
1229 && contains(params_.name, '@'))
1230 ? split(params_.name, date_format, '@') : string();
1232 if (params_.type == InsetInfoParams::MODDATE_INFO)
1233 #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
1234 date = QDateTime::fromSecsSinceEpoch(buffer().fileName().lastModified()).date();
1236 date = QDateTime::fromTime_t(buffer().fileName().lastModified()).date();
1238 else if (params_.type == InsetInfoParams::FIXDATE_INFO && !date_specifier.empty())
1239 date = QDate::fromString(toqstr(date_specifier), Qt::ISODate);
1241 date = QDate::currentDate();
1242 setText(params_.getDate(date_format, date), params_.lang);
1245 case InsetInfoParams::TIME_INFO:
1246 case InsetInfoParams::MODTIME_INFO:
1247 case InsetInfoParams::FIXTIME_INFO: {
1248 // TODO: away from a release, use parseTime instead.
1249 string time_format = params_.name;
1250 string const time_specifier = (params_.type == InsetInfoParams::FIXTIME_INFO
1251 && contains(params_.name, '@'))
1252 ? split(params_.name, time_format, '@') : string();
1254 if (params_.type == InsetInfoParams::MODTIME_INFO)
1255 #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
1256 time = QDateTime::fromSecsSinceEpoch(buffer().fileName().lastModified()).time();
1258 time = QDateTime::fromTime_t(buffer().fileName().lastModified()).time();
1260 else if (params_.type == InsetInfoParams::FIXTIME_INFO && !time_specifier.empty())
1261 time = QTime::fromString(toqstr(time_specifier), Qt::ISODate);
1263 time = QTime::currentTime();
1264 setText(params_.getTime(time_format, time), params_.lang);
1269 // Just to do something with that string
1270 LYXERR(Debug::INFO, "info inset text: " << gui);
1274 void InsetInfo::validate(LaTeXFeatures & features) const
1276 const_cast<InsetInfo *>(this)->build();
1277 InsetCollapsible::validate(features);
1281 string InsetInfo::contextMenu(BufferView const &, int, int) const
1283 //FIXME: We override the implementation of InsetCollapsible,
1284 //because this inset is not a collapsible inset.
1285 return contextMenuName();
1289 string InsetInfo::contextMenuName() const
1291 return "context-info";
1296 // TODO: away from a release, use these functions in InsetInfo::build and InsetInfoParams::getArguments.
1298 std::pair<QDate, std::string> parseDate(Buffer const & buffer, const InsetInfoParams & params) {
1299 std::string date_format = params.name;
1300 std::string const date_specifier = (params.type == InsetInfoParams::FIXDATE_INFO
1301 && contains(params.name, '@'))
1302 ? split(params.name, date_format, '@') : string();
1305 if (params.type == InsetInfoParams::MODDATE_INFO)
1306 #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
1307 date = QDateTime::fromSecsSinceEpoch(buffer.fileName().lastModified()).date();
1309 date = QDateTime::fromTime_t(buffer.fileName().lastModified()).date();
1311 else if (params.type == InsetInfoParams::FIXDATE_INFO && !date_specifier.empty()) {
1312 QDate date = QDate::fromString(toqstr(date_specifier), Qt::ISODate);
1313 date = (date.isValid()) ? date : QDate::currentDate();
1315 if (params.type != InsetInfoParams::DATE_INFO && params.type != InsetInfoParams::FIXDATE_INFO)
1316 lyxerr << "Unexpected InsetInfoParams::info_type in parseDate: " << params.type;
1317 date = QDate::currentDate();
1320 return {date, date_format};
1323 std::pair<QTime, std::string> parseTime(Buffer const & buffer, const InsetInfoParams & params) {
1324 std::string time_format = params.name;
1325 std::string const date_specifier = (params.type == InsetInfoParams::FIXTIME_INFO
1326 && contains(params.name, '@'))
1327 ? split(params.name, time_format, '@') : string();
1330 if (params.type == InsetInfoParams::MODTIME_INFO)
1331 #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
1332 time = QDateTime::fromSecsSinceEpoch(buffer.fileName().lastModified()).time();
1334 time = QDateTime::fromTime_t(buffer.fileName().lastModified()).time();
1336 else if (params.type == InsetInfoParams::FIXTIME_INFO && !date_specifier.empty()) {
1337 time = QTime::fromString(toqstr(date_specifier), Qt::ISODate);
1338 time = (time.isValid()) ? time : QTime::currentTime();
1340 if (params.type != InsetInfoParams::TIME_INFO && params.type != InsetInfoParams::FIXTIME_INFO)
1341 lyxerr << "Unexpected InsetInfoParams::info_type in parseTime: " << params.type;
1342 time = QTime::currentTime();
1345 return {time, time_format};
1348 docstring getBufferInfo(Buffer const & buffer, const InsetInfoParams & params) {
1349 if (params.name == "name")
1350 return from_utf8(buffer.fileName().onlyFileName());
1351 else if (params.name == "name-noext")
1352 return from_utf8(buffer.fileName().onlyFileNameWithoutExt());
1353 else if (params.name == "path")
1354 return from_utf8(os::latex_path(buffer.filePath()));
1355 else if (params.name == "class")
1356 return from_utf8(buffer.params().documentClass().name());
1358 lyxerr << "Unexpected name for InsetInfoParams::BUFFER_INFO: " << params.name;
1359 return from_ascii("");
1363 docstring getVCSInfo(Buffer const & buffer, const InsetInfoParams & params) {
1364 if (!buffer.lyxvc().inUse())
1365 return _("No version control!");
1367 LyXVC::RevisionInfo itype = LyXVC::Unknown;
1368 if (params.name == "revision")
1369 itype = LyXVC::File;
1370 else if (params.name == "revision-abbrev")
1371 itype = LyXVC::FileAbbrev;
1372 else if (params.name == "tree-revision")
1373 itype = LyXVC::Tree;
1374 else if (params.name == "author")
1375 itype = LyXVC::Author;
1376 else if (params.name == "time")
1377 itype = LyXVC::Time;
1378 else if (params.name == "date")
1379 itype = LyXVC::Date;
1381 string binfo = buffer.lyxvc().revisionInfo(itype);
1383 return from_ascii("VCS info unknown!");
1385 return from_utf8(binfo);
1388 docstring getPackageInfo(const InsetInfoParams & params) {
1389 // check in packages.lst
1391 // we also allow version check with version separated by blank
1392 if (contains(params.name, ' ')) {
1394 string const version = split(params.name, name, ' ');
1395 int const y = convert<int>(version.substr(0,4));
1396 int const m = convert<int>(version.substr(4,2));
1397 int const d = convert<int>(version.substr(6,2));
1398 available = LaTeXFeatures::isAvailableAtLeastFrom(name, y, m, d);
1400 available = LaTeXFeatures::isAvailable(params.name);
1402 return from_ascii(available ? "yes" : "no");
1405 docstring getTextClassInfo(const InsetInfoParams & params) {
1406 LayoutFileList const & list = LayoutFileList::get();
1407 // params_.name is the class name
1408 const bool available = list.haveClass(params.name) && list[params.name].isTeXClassAvailable();
1409 return from_ascii(available ? "yes" : "no");
1412 // With C++17, it would be better to have a std::string_view instead of const char *.
1413 const static std::map<char_type, const char *> keyToString {
1414 {0x21b5, "Return[[Key]]"}, // Return
1415 {0x21b9, "Tab[[Key]]"}, // Tab both directions (Win)
1416 {0x21de, "PgUp"}, // Qt::Key_PageUp
1417 {0x21df, "PgDown"}, // Qt::Key_PageDown
1418 {0x21e4, "Backtab"}, // Qt::Key_Backtab
1419 {0x21e5, "Tab"}, // Qt::Key_Tab
1420 {0x21e7, "Shift"}, // Shift
1421 {0x21ea, "CapsLock"}, // Qt::Key_CapsLock
1422 {0x2303, "Control[[Key]]"}, // Control
1423 {0x2318, "Command[[Key]]"}, // CMD
1424 {0x2324, "Return[[Key]]"}, // Qt::Key_Enter
1425 {0x2325, "Option[[Key]]"}, // Option key
1426 {0x2326, "Delete[[Key]]"}, // Qt::Key_Delete
1427 {0x232b, "Fn+Del"}, // Qt::Key_Backspace
1428 {0x238b, "Esc"}, // Qt::Key_Escape
1431 bool canTranslateKeySequence(const InsetInfoParams & params, const docstring & sequence,
1432 const docstring & seq_untranslated) {
1433 bool is_translated = sequence != seq_untranslated;
1434 std::string const lcode = params.lang->code();
1437 for (char_type const c : sequence) {
1438 const auto keyMapping = keyToString.find(c);
1439 if (keyMapping != keyToString.end()) {
1440 is_translated = translateString(from_ascii(keyMapping->second), trans, lcode);
1444 return is_translated;
1447 void docbookShortcutInfo(XMLStream & xs, const InsetInfoParams & params) {
1448 // Usually, a keyboard shortcut should be encoded as db:shortcut. However, this element doesn't accept text, hence
1449 // the use of db:accel for error cases (not the right semantics, though).
1452 if (params.type == InsetInfoParams::SHORTCUTS_INFO)
1453 attr = R"(role="shortcuts")";
1454 else if (params.type == InsetInfoParams::SHORTCUT_INFO)
1455 attr = R"(role="shortcut")";
1457 // Only check for this assertion that exits this function.
1458 lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params.type;
1462 // shortcuts can change, so we need to re-do this each time
1463 FuncRequest const func = lyxaction.lookupFunc(params.name);
1464 if (func.action() == LFUN_UNKNOWN_ACTION) {
1465 xml::openTag(xs, "accel", attr, "inline");
1466 xs << _("Unknown action %1$s");
1467 xml::closeTag(xs, "accel", "inline");
1471 KeyMap::Bindings bindings = theTopLevelKeymap().findBindings(func);
1472 if (bindings.empty()) {
1473 xml::openTag(xs, "accel", attr, "inline");
1474 xs << _("undefined");
1475 xml::closeTag(xs, "accel", "inline");
1480 docstring seq_untranslated;
1481 if (params.type == InsetInfoParams::SHORTCUT_INFO) {
1482 sequence = bindings.begin()->print(KeySequence::ForGui);
1483 seq_untranslated = bindings.begin()->print(KeySequence::ForGui, true);
1484 } else if (params.type == InsetInfoParams::SHORTCUTS_INFO) {
1485 sequence = theTopLevelKeymap().printBindings(func, KeySequence::ForGui);
1486 seq_untranslated = theTopLevelKeymap().printBindings(func, KeySequence::ForGui, true);
1488 // No other possible case.
1490 Language const * tryguilang = languages.getFromCode(Messages::guiLanguage());
1491 // Some info insets use the language of the GUI (if available)
1492 Language const * guilang = tryguilang ? tryguilang : params.lang;
1493 const bool isTranslated = canTranslateKeySequence(params, sequence, seq_untranslated);
1494 const bool isLtr = !isTranslated || (!guilang->rightToLeft() && !params.lang->rightToLeft());
1495 attr += std::string(" dir=\"") + (isLtr ? "ltr" : "rtl") + "\"";
1496 attr += " action=\"simul\"";
1497 xml::openTag(xs, "shortcut", attr, "inline");
1498 xml::openTag(xs, "keycombo", "", "inline");
1500 // QKeySequence returns special characters for keys on the mac
1501 // Since these are not included in many fonts, we
1502 // re-translate them to textual names (see #10641)
1503 odocstringstream ods;
1504 string const lcode = params.lang->code();
1506 for (char_type const c : sequence) {
1507 const auto keyMapping = keyToString.find(c);
1508 if (keyMapping != keyToString.end()) {
1509 translateString(from_ascii(keyMapping->second), trans, lcode);
1511 // db:keysym: symbolic name (like Page Up), unlike what is printed on the key (like
1512 // ⇞, ↑, ▲, PgUp, Page Up, etc.)
1513 xml::openTag(xs, "keysym", "", "inline");
1515 xml::closeTag(xs, "keysym", "inline");
1517 // db:keycap: this is not a special key, c is really what is printed on the key.
1518 xml::openTag(xs, "keycap", "", "inline");
1520 xml::closeTag(xs, "keycap", "inline");
1524 xml::closeTag(xs, "keycombo", "inline");
1525 xml::closeTag(xs, "shortcut", "inline");
1528 void xhtmlShortcutInfo(XMLStream & xs, const InsetInfoParams & params) {
1530 if (params.type == InsetInfoParams::SHORTCUTS_INFO)
1531 attr = R"(class="shortcuts")";
1532 else if (params.type == InsetInfoParams::SHORTCUT_INFO)
1533 attr = R"(class="shortcut")";
1535 // Only check for this assertion that exits this function.
1536 lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params.type;
1540 // shortcuts can change, so we need to re-do this each time
1541 FuncRequest const func = lyxaction.lookupFunc(params.name);
1542 if (func.action() == LFUN_UNKNOWN_ACTION) {
1543 xml::openTag(xs, "span", attr, "inline");
1544 xs << _("Unknown action %1$s");
1545 xml::closeTag(xs, "span", "inline");
1549 KeyMap::Bindings bindings = theTopLevelKeymap().findBindings(func);
1550 if (bindings.empty()) {
1551 xml::openTag(xs, "span", attr, "inline");
1552 xs << _("undefined");
1553 xml::closeTag(xs, "span", "inline");
1558 docstring seq_untranslated;
1559 if (params.type == InsetInfoParams::SHORTCUT_INFO) {
1560 sequence = bindings.begin()->print(KeySequence::ForGui);
1561 seq_untranslated = bindings.begin()->print(KeySequence::ForGui, true);
1562 } else if (params.type == InsetInfoParams::SHORTCUTS_INFO) {
1563 sequence = theTopLevelKeymap().printBindings(func, KeySequence::ForGui);
1564 seq_untranslated = theTopLevelKeymap().printBindings(func, KeySequence::ForGui, true);
1566 // No other possible case.
1568 Language const * tryguilang = languages.getFromCode(Messages::guiLanguage());
1569 // Some info insets use the language of the GUI (if available)
1570 Language const * guilang = tryguilang ? tryguilang : params.lang;
1571 const bool isTranslated = canTranslateKeySequence(params, sequence, seq_untranslated);
1572 const bool isLtr = !isTranslated || (!guilang->rightToLeft() && !params.lang->rightToLeft());
1573 attr += std::string(" dir=\"") + (isLtr ? "ltr" : "rtl") + "\"";
1574 // Use bdo instead of span to specify the text direction (dir is only allowed globally or on bdo).
1575 xml::openTag(xs, "bdo", attr, "inline");
1577 // QKeySequence returns special characters for keys on the mac
1578 // Since these are not included in many fonts, we
1579 // re-translate them to textual names (see #10641)
1580 odocstringstream ods;
1581 string const lcode = params.lang->code();
1583 for (size_t i = 0; i < sequence.length(); ++i) {
1584 char_type const c = sequence[i];
1585 const auto keyMapping = keyToString.find(c);
1586 if (keyMapping != keyToString.end()) {
1587 translateString(from_ascii(keyMapping->second), trans, lcode);
1593 if (i > 0 && i + 1 < sequence.length())
1594 xs << from_ascii("+");
1597 xml::closeTag(xs, "bdo", "inline");
1600 docstring getLyxRCInfo(const InsetInfoParams & params) {
1601 if (params.name.empty())
1602 return _("undefined");
1604 // this information could change, if the preferences are changed,
1605 // so we will recalculate each time through.
1606 // FIXME this uses the serialization mechanism to get the info
1607 // we want, which i guess works but is a bit strange.
1609 lyxrc.write(oss, true, params.name);
1610 string result = oss.str();
1611 if (result.size() < 2) {
1612 return _("undefined");
1615 string::size_type loc = result.rfind('\n', result.size() - 2);
1616 loc = loc == string::npos ? 0 : loc + 1;
1617 if (result.size() < loc + params.name.size() + 1
1618 || result.substr(loc + 1, params.name.size()) != params.name) {
1619 return _("undefined");
1622 // remove leading comments and \\name and space
1623 result = result.substr(loc + params.name.size() + 2);
1626 result = rtrim(result, "\n");
1627 result = trim(result, "\"");
1630 return from_ascii("not set");
1632 return from_utf8(result);
1635 void docbookMenuInfo(XMLStream & xs, Buffer const & buffer, const InsetInfoParams & params) {
1636 docstring_list names;
1637 FuncRequest func = lyxaction.lookupFunc(params.name);
1638 if (func.action() == LFUN_UNKNOWN_ACTION) {
1639 xml::openTag(xs, "guimenuitem", "", "inline");
1640 xs << _("Unknown action %1$s");
1641 xml::closeTag(xs, "guimenuitem", "inline");
1645 if (func.action() == LFUN_BUFFER_VIEW || func.action() == LFUN_BUFFER_UPDATE) {
1646 // The default output format is in the menu without argument,
1647 // so strip it here.
1648 if (func.argument() == from_ascii(buffer.params().getDefaultOutputFormat()))
1649 func = FuncRequest(func.action());
1652 // iterate through the menubackend to find it
1654 xml::openTag(xs, "guimenuitem", "", "inline");
1655 xs << _("Can't determine menu entry for action %1$s in batch mode");
1656 xml::closeTag(xs, "guimenuitem", "inline");
1660 // and we will not keep trying if we fail
1661 if (!theApp()->searchMenu(func, names)) {
1662 xml::openTag(xs, "guimenuitem", "", "inline");
1663 xs << _("No menu entry for action %1$s");
1664 xml::closeTag(xs, "guimenuitem", "inline");
1668 // if found, return its path.
1669 Language const * tryguilang = languages.getFromCode(Messages::guiLanguage());
1670 // Some info insets use the language of the GUI (if available)
1671 Language const * guilang = tryguilang ? tryguilang : params.lang;
1672 const bool isLtr = !guilang->rightToLeft();
1673 const std::string attr = std::string("dir=\"") + (isLtr ? "ltr" : "rtl") + "\"";
1675 xml::openTag(xs, "menuchoice", attr, "inline"); // More of an inline tag in this case, as there is no db:shortcut to
1676 // accompany the succession of menus.
1678 for (size_t i = 0; i < names.size(); ++i) {
1679 docstring const & name = names[i];
1684 } else if (i == names.size() - 1) {
1685 tag = "guimenuitem";
1690 xml::openTag(xs, tag, "", "inline");
1692 //FIXME: add proper underlines here. This
1693 // involves rewriting searchMenu used above to
1694 // return a vector of menus. If we do not do
1695 // that, we might as well use below
1696 // Paragraph::insert on each string (JMarc)
1697 // TODO: for DocBook, underlining corresponds to adding db:accel around the letter to underline.
1700 xml::closeTag(xs, tag, "inline");
1703 xml::closeTag(xs, "menuchoice", "inline");
1707 void xhtmlMenuInfo(XMLStream & xs, Buffer const & buffer, const InsetInfoParams & params) {
1708 docstring_list names;
1709 FuncRequest func = lyxaction.lookupFunc(params.name);
1710 if (func.action() == LFUN_UNKNOWN_ACTION) {
1711 xml::openTag(xs, "span", "", "inline");
1712 xs << _("Unknown action %1$s");
1713 xml::closeTag(xs, "span", "inline");
1717 if (func.action() == LFUN_BUFFER_VIEW || func.action() == LFUN_BUFFER_UPDATE) {
1718 // The default output format is in the menu without argument,
1719 // so strip it here.
1720 if (func.argument() == from_ascii(buffer.params().getDefaultOutputFormat()))
1721 func = FuncRequest(func.action());
1724 // iterate through the menubackend to find it
1726 xml::openTag(xs, "span", "", "inline");
1727 xs << _("Can't determine menu entry for action %1$s in batch mode");
1728 xml::closeTag(xs, "span", "inline");
1732 // and we will not keep trying if we fail
1733 if (!theApp()->searchMenu(func, names)) {
1734 xml::openTag(xs, "span", "", "inline");
1735 xs << _("No menu entry for action %1$s");
1736 xml::closeTag(xs, "span", "inline");
1740 // if found, return its path.
1741 Language const * tryguilang = languages.getFromCode(Messages::guiLanguage());
1742 // Some info insets use the language of the GUI (if available)
1743 Language const * guilang = tryguilang ? tryguilang : params.lang;
1744 const bool isLtr = !guilang->rightToLeft();
1745 const std::string attr = std::string("dir=\"") + (isLtr ? "ltr" : "rtl") + "\"";
1746 // Use bdo instead of span to specify the text direction (dir is only allowed globally or on bdo).
1747 xml::openTag(xs, "bdo", attr, "inline");
1749 for (size_t i = 0; i < names.size(); ++i) {
1750 docstring const & name = names[i];
1752 //FIXME: add proper underlines here. This
1753 // involves rewriting searchMenu used above to
1754 // return a vector of menus. If we do not do
1755 // that, we might as well use below
1756 // Paragraph::insert on each string (JMarc)
1757 // TODO: for DocBook, underlining corresponds to adding db:accel around the letter to underline.
1760 if (i > 0 && i + 1 < names.size())
1761 xs << "⇒"; // InsetSpecialChar::MENU_SEPARATOR
1764 xml::closeTag(xs, "bdo", "inline");
1767 void docbookIconInfo(XMLStream & xs, const OutputParams & rp, Buffer * buffer, const InsetInfoParams & params) {
1768 FuncRequest func = lyxaction.lookupFunc(params.name);
1769 docstring icon_name = frontend::Application::iconName(func, true);
1770 FileName file(to_utf8(icon_name));
1771 if (file.onlyFileNameWithoutExt() == "unknown") {
1772 std::string dir = "images";
1773 FileName file2(imageLibFileSearch(dir, params.name, "svgz,png"));
1781 int percent_scale = 100;
1783 // Compute the scale factor for the icon such that its
1784 // width on screen is equal to 1em in pixels.
1785 // The scale factor is rounded to the integer nearest
1786 // to the float value of the ratio 100*iconsize/imgsize.
1787 int imgsize = QImage(toqstr(file.absFileName())).width();
1789 int iconsize = Length(1, Length::EM).inPixels(1);
1790 percent_scale = (100 * iconsize + imgsize / 2) / imgsize;
1794 InsetGraphicsTight * inset = new InsetGraphicsTight(buffer);
1795 InsetGraphicsParams igp;
1796 igp.filename = file;
1797 igp.lyxscale = percent_scale;
1798 igp.scale = string();
1799 igp.width = Length(1, Length::EM);
1800 if (contains(file.absoluteFilePath(), from_ascii("math"))
1801 || contains(file.absoluteFilePath(), from_ascii("ert-insert"))
1802 || suffixIs(file.onlyPath().absoluteFilePath(), from_ascii("ipa")))
1803 igp.darkModeSensitive = true;
1804 inset->setParams(igp);
1806 xml::openTag(xs, "guiicon", "", "inline");
1807 inset->docbook(xs, rp);
1808 xml::closeTag(xs, "guiicon", "inline");
1811 void xhtmlIconInfo(XMLStream & xs, const OutputParams & rp, Buffer * buffer, const InsetInfoParams & params) {
1812 FuncRequest func = lyxaction.lookupFunc(params.name);
1813 docstring icon_name = frontend::Application::iconName(func, true);
1814 FileName file(to_utf8(icon_name));
1815 if (file.onlyFileNameWithoutExt() == "unknown") {
1816 std::string dir = "images";
1817 FileName file2(imageLibFileSearch(dir, params.name, "svgz,png"));
1825 int percent_scale = 100;
1827 // Compute the scale factor for the icon such that its
1828 // width on screen is equal to 1em in pixels.
1829 // The scale factor is rounded to the integer nearest
1830 // to the float value of the ratio 100*iconsize/imgsize.
1831 int imgsize = QImage(toqstr(file.absFileName())).width();
1833 int iconsize = Length(1, Length::EM).inPixels(1);
1834 percent_scale = (100 * iconsize + imgsize / 2) / imgsize;
1838 InsetGraphicsTight * inset = new InsetGraphicsTight(buffer);
1839 InsetGraphicsParams igp;
1840 igp.filename = file;
1841 igp.lyxscale = percent_scale;
1842 igp.scale = string();
1843 igp.width = Length(1, Length::EM);
1844 if (contains(file.absoluteFilePath(), from_ascii("math"))
1845 || contains(file.absoluteFilePath(), from_ascii("ert-insert"))
1846 || suffixIs(file.onlyPath().absoluteFilePath(), from_ascii("ipa")))
1847 igp.darkModeSensitive = true;
1848 inset->setParams(igp);
1850 xml::openTag(xs, "span", "class=\"guiicon\"", "inline");
1851 inset->xhtml(xs, rp);
1852 xml::closeTag(xs, "span", "inline");
1855 docstring getLyXInfo(const InsetInfoParams & params) {
1856 if (params.name == "version")
1857 return from_ascii(lyx_version);
1858 else if (params.name == "layoutformat")
1859 return convert<docstring>(LAYOUT_FORMAT);
1861 lyxerr << "Unexpected name for InsetInfoParams::BUFFER_INFO: " << params.name;
1862 return from_ascii("");
1866 docstring getNormalizedL7N(const InsetInfoParams & params) {
1867 docstring locstring = _(params.name);
1869 // Remove trailing colons
1870 locstring = rtrim(locstring, ":");
1872 // Remove menu accelerators
1873 if (contains(locstring, from_ascii("|"))) {
1874 docstring nlocstring;
1875 rsplit(locstring, nlocstring, '|');
1876 locstring = nlocstring;
1879 // Remove Qt accelerators, but keep literal ampersands
1880 locstring = subst(locstring, from_ascii(" & "), from_ascii("</amp;>"));
1881 locstring = subst(locstring, from_ascii("&"), docstring());
1882 locstring = subst(locstring, from_ascii("</amp;>"), from_ascii(" & "));
1890 void InsetInfo::docbook(XMLStream & xs, OutputParams const & rp) const
1892 // TODO: away from a release, merge some of this code with InsetInfo::build and InsetInfoParams::getArguments.
1893 switch (params_.type) {
1894 case InsetInfoParams::DATE_INFO:
1895 case InsetInfoParams::MODDATE_INFO:
1896 case InsetInfoParams::FIXDATE_INFO: {
1898 switch (params_.type) {
1899 case InsetInfoParams::DATE_INFO:
1900 role = "current-date";
1902 case InsetInfoParams::MODDATE_INFO:
1903 role = "last-modification-date";
1905 case InsetInfoParams::FIXDATE_INFO:
1909 lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params().type;
1913 // A db:date cannot be nested within a db:date. This case typically happens when the document class defines a
1914 // Date layout. In this case, avoid outputting a new db:date. This means that InsetInfo cannot add a role on top
1915 // of the previous db:date, hence add it as a comment. (Another solution would be an XML processing instruction,
1916 // but this case is not common enough.) Adding the role to the already output tag might have consequences for
1917 // some document classes where the layout already has a role or uses the same role for another purpose.
1918 const bool isWithinDate = buffer().getParFromID(rp.lastid).top().paragraph().layout().docbooktag() == "date";
1921 xml::openTag(xs, "date", "role=\"" + role + "\"", "inline");
1923 xs << XMLStream::ESCAPE_NONE << from_ascii(std::string("<!-- ") + role + " -->");
1924 xs << qstring_to_ucs4(std::get<0>(parseDate(buffer(), params_)).toString(Qt::ISODate));
1926 xml::closeTag(xs, "date", "inline");
1930 case InsetInfoParams::TIME_INFO:
1931 case InsetInfoParams::MODTIME_INFO:
1932 case InsetInfoParams::FIXTIME_INFO: {
1934 switch (params_.type) {
1935 case InsetInfoParams::TIME_INFO:
1936 role = "current-time";
1938 case InsetInfoParams::MODTIME_INFO:
1939 role = "last-modification-time";
1941 case InsetInfoParams::FIXTIME_INFO:
1945 lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params().type;
1949 // DocBook has no specific element for time, so use a date.
1950 // See the discussion above (DATE_INFO, MODDATE_INFO, and FIXDATE_INFO) for a discussion about the choices that
1952 const bool isWithinDate = buffer().getParFromID(rp.lastid).top().paragraph().layout().docbooktag() == "date";
1955 xml::openTag(xs, "date", "role=\"" + role + "\"", "inline");
1957 xs << XMLStream::ESCAPE_NONE << from_ascii(std::string("<!-- ") + role + " -->");
1958 xs << qstring_to_ucs4(std::get<0>(parseTime(buffer(), params_)).toString(Qt::ISODate));
1960 xml::closeTag(xs, "date", "inline");
1964 case InsetInfoParams::BUFFER_INFO:
1965 xml::openTag(xs, "phrase", "role=\"buffer-info " + params_.name + "\"", "inline");
1966 xs << getBufferInfo(buffer(), params_);
1967 xml::closeTag(xs, "phrase", "inline");
1969 case InsetInfoParams::VCS_INFO:
1970 xml::openTag(xs, "phrase", "role=\"vcs-info " + params_.name + "\"", "inline");
1971 xs << getVCSInfo(buffer(), params_);
1972 xml::closeTag(xs, "phrase", "inline");
1974 case InsetInfoParams::PACKAGE_INFO:
1975 xml::openTag(xs, "phrase", "role=\"package-availability " + params_.name + "\"", "inline");
1976 xs << getPackageInfo(params_);
1977 xml::closeTag(xs, "phrase", "inline");
1979 case InsetInfoParams::TEXTCLASS_INFO:
1980 xml::openTag(xs, "phrase", "role=\"textclass-availability " + params_.name + "\"", "inline");
1981 xs << getTextClassInfo(params_);
1982 xml::closeTag(xs, "phrase", "inline");
1985 case InsetInfoParams::SHORTCUTS_INFO:
1986 case InsetInfoParams::SHORTCUT_INFO:
1987 docbookShortcutInfo(xs, params_);
1990 case InsetInfoParams::LYXRC_INFO:
1991 xml::openTag(xs, "phrase", "role=\"lyxrc-entry " + params_.name + "\"", "inline");
1992 xs << getLyxRCInfo(params_);
1993 xml::closeTag(xs, "phrase", "inline");
1996 case InsetInfoParams::MENU_INFO:
1997 docbookMenuInfo(xs, buffer(), params_);
1999 case InsetInfoParams::ICON_INFO:
2000 docbookIconInfo(xs, rp, buffer_, params_);
2002 case InsetInfoParams::LYX_INFO:
2003 xml::openTag(xs, "phrase", "role=\"lyx-info " + params_.name + "\"", "inline");
2004 xs << getLyXInfo(params_);
2005 xml::closeTag(xs, "phrase", "inline");
2008 case InsetInfoParams::L7N_INFO:
2009 // TODO: add "its:translate="no"" in the attributes if ITS is globally enabled for LyX (quite rare to have ITS
2010 // for DocBook documents).
2011 xml::openTag(xs, "phrase", R"(role="localized")", "inline");
2012 xs << getNormalizedL7N(params_);
2013 xml::closeTag(xs, "phrase", "inline");
2016 case InsetInfoParams::UNKNOWN_INFO:
2017 xml::openTag(xs, "phrase", R"(role="unknown")", "inline");
2018 xs << from_ascii("Unknown Info!");
2019 xml::closeTag(xs, "phrase", "inline");
2022 lyxerr << "Unrecognised InsetInfoParams::info_type: " << params().type;
2024 xml::openTag(xs, "phrase", R"(role="unrecognized")", "inline");
2025 xs << from_ascii("Unrecognized Info!");
2026 xml::closeTag(xs, "phrase", "inline");
2032 docstring InsetInfo::xhtml(XMLStream & xs, OutputParams const & rp) const
2034 // TODO: away from a release, merge some of this code with InsetInfo::build and InsetInfoParams::getArguments.
2035 switch (params_.type) {
2036 case InsetInfoParams::DATE_INFO:
2037 case InsetInfoParams::MODDATE_INFO:
2038 case InsetInfoParams::FIXDATE_INFO: {
2039 std::string cssClass;
2040 switch (params_.type) {
2041 case InsetInfoParams::DATE_INFO:
2042 cssClass = "current-date";
2044 case InsetInfoParams::MODDATE_INFO:
2045 cssClass = "last-modification-date";
2047 case InsetInfoParams::FIXDATE_INFO:
2048 cssClass = "fix-date";
2051 lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params().type;
2056 std::string date_format;
2057 std::tie(date, date_format) = parseDate(buffer(), params_);
2059 xml::openTag(xs, "span", std::string("class=\"infodate-") + cssClass + "\"", "inline");
2060 xs << params_.getDate(date_format, date);
2061 xml::closeTag(xs, "span", "inline");
2065 case InsetInfoParams::TIME_INFO:
2066 case InsetInfoParams::MODTIME_INFO:
2067 case InsetInfoParams::FIXTIME_INFO: {
2068 std::string cssClass;
2069 switch (params_.type) {
2070 case InsetInfoParams::TIME_INFO:
2071 cssClass = "current-time";
2073 case InsetInfoParams::MODTIME_INFO:
2074 cssClass = "last-modification-time";
2076 case InsetInfoParams::FIXTIME_INFO:
2077 cssClass = "fix-time";
2080 lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params().type;
2085 std::string time_format;
2086 std::tie(time, time_format) = parseTime(buffer(), params_);
2088 xml::openTag(xs, "span", std::string("class=\"infotime-") + cssClass + "\"", "inline");
2089 xs << params_.getTime(time_format, time);
2090 xml::closeTag(xs, "span", "inline");
2094 case InsetInfoParams::BUFFER_INFO:
2095 xml::openTag(xs, "span", "class=\"buffer-info " + params_.name + "\"", "inline");
2096 xs << getBufferInfo(buffer(), params_);
2097 xml::closeTag(xs, "span", "inline");
2099 case InsetInfoParams::VCS_INFO:
2100 xml::openTag(xs, "span", "class=\"vcs-info " + params_.name + "\"", "inline");
2101 xs << getVCSInfo(buffer(), params_);
2102 xml::closeTag(xs, "span", "inline");
2104 case InsetInfoParams::PACKAGE_INFO:
2105 xml::openTag(xs, "span", "class=\"package-availability " + params_.name + "\"", "inline");
2106 xs << getPackageInfo(params_);
2107 xml::closeTag(xs, "span", "inline");
2109 case InsetInfoParams::TEXTCLASS_INFO:
2110 xml::openTag(xs, "span", "class=\"textclass-availability " + params_.name + "\"", "inline");
2111 xs << getTextClassInfo(params_);
2112 xml::closeTag(xs, "span", "inline");
2115 case InsetInfoParams::SHORTCUTS_INFO:
2116 case InsetInfoParams::SHORTCUT_INFO:
2117 xhtmlShortcutInfo(xs, params_);
2120 case InsetInfoParams::LYXRC_INFO:
2121 xml::openTag(xs, "span", "class=\"lyxrc-entry " + params_.name + "\"", "inline");
2122 xs << getLyxRCInfo(params_);
2123 xml::closeTag(xs, "span", "inline");
2126 case InsetInfoParams::MENU_INFO:
2127 xhtmlMenuInfo(xs, buffer(), params_);
2129 case InsetInfoParams::ICON_INFO:
2130 xhtmlIconInfo(xs, rp, buffer_, params_);
2132 case InsetInfoParams::LYX_INFO:
2133 xml::openTag(xs, "span", "class=\"lyx-info " + params_.name + "\"", "inline");
2134 xs << getLyXInfo(params_);
2135 xml::closeTag(xs, "span", "inline");
2138 case InsetInfoParams::L7N_INFO:
2139 xml::openTag(xs, "span", R"(class="localized" translate="no")", "inline");
2140 xs << getNormalizedL7N(params_);
2141 xml::closeTag(xs, "span", "inline");
2144 case InsetInfoParams::UNKNOWN_INFO:
2145 xml::openTag(xs, "span", R"(class="unknown")", "inline");
2146 xs << from_ascii("Unknown Info!");
2147 xml::closeTag(xs, "span", "inline");
2150 lyxerr << "Unrecognised InsetInfoParams::info_type: " << params().type;
2152 xml::openTag(xs, "span", R"(class="unrecognized")", "inline");
2153 xs << from_ascii("Unrecognized Info!");
2154 xml::closeTag(xs, "span", "inline");
2158 return from_ascii("");