From 531a37baee776e4a984820809d5ef4bcbe6f5047 Mon Sep 17 00:00:00 2001 From: Thibaut Cuvelier Date: Sun, 8 Oct 2023 22:09:59 +0200 Subject: [PATCH] XHTML: implement InsetInfo. --- src/insets/InsetInfo.cpp | 342 +++++++++++++++++++++++++++++++++++++-- src/insets/InsetInfo.h | 2 + 2 files changed, 330 insertions(+), 14 deletions(-) diff --git a/src/insets/InsetInfo.cpp b/src/insets/InsetInfo.cpp index b08eb7b815..4b9277490d 100644 --- a/src/insets/InsetInfo.cpp +++ b/src/insets/InsetInfo.cpp @@ -1295,48 +1295,54 @@ namespace { // TODO: away from a release, use these functions in InsetInfo::build and InsetInfoParams::getArguments. -QDate parseDate(Buffer const & buffer, const InsetInfoParams & params) { +std::pair parseDate(Buffer const & buffer, const InsetInfoParams & params) { std::string date_format = params.name; std::string const date_specifier = (params.type == InsetInfoParams::FIXDATE_INFO && contains(params.name, '@')) ? split(params.name, date_format, '@') : string(); + QDate date; if (params.type == InsetInfoParams::MODDATE_INFO) #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) - return QDateTime::fromSecsSinceEpoch(buffer.fileName().lastModified()).date(); + date = QDateTime::fromSecsSinceEpoch(buffer.fileName().lastModified()).date(); #else - return QDateTime::fromTime_t(buffer.fileName().lastModified()).date(); + date = QDateTime::fromTime_t(buffer.fileName().lastModified()).date(); #endif else if (params.type == InsetInfoParams::FIXDATE_INFO && !date_specifier.empty()) { QDate date = QDate::fromString(toqstr(date_specifier), Qt::ISODate); - return (date.isValid()) ? date : QDate::currentDate(); + date = (date.isValid()) ? date : QDate::currentDate(); } else { if (params.type != InsetInfoParams::DATE_INFO && params.type != InsetInfoParams::FIXDATE_INFO) lyxerr << "Unexpected InsetInfoParams::info_type in parseDate: " << params.type; - return QDate::currentDate(); + date = QDate::currentDate(); } + + return {date, date_format}; } -QTime parseTime(Buffer const & buffer, const InsetInfoParams & params) { +std::pair parseTime(Buffer const & buffer, const InsetInfoParams & params) { std::string time_format = params.name; std::string const date_specifier = (params.type == InsetInfoParams::FIXTIME_INFO && contains(params.name, '@')) ? split(params.name, time_format, '@') : string(); + QTime time; if (params.type == InsetInfoParams::MODTIME_INFO) #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) - return QDateTime::fromSecsSinceEpoch(buffer.fileName().lastModified()).time(); + time = QDateTime::fromSecsSinceEpoch(buffer.fileName().lastModified()).time(); #else - return QDateTime::fromTime_t(buffer.fileName().lastModified()).time(); + time = QDateTime::fromTime_t(buffer.fileName().lastModified()).time(); #endif else if (params.type == InsetInfoParams::FIXTIME_INFO && !date_specifier.empty()) { - QTime time = QTime::fromString(toqstr(date_specifier), Qt::ISODate); - return (time.isValid()) ? time : QTime::currentTime(); + time = QTime::fromString(toqstr(date_specifier), Qt::ISODate); + time = (time.isValid()) ? time : QTime::currentTime(); } else { if (params.type != InsetInfoParams::TIME_INFO && params.type != InsetInfoParams::FIXTIME_INFO) lyxerr << "Unexpected InsetInfoParams::info_type in parseTime: " << params.type; - return QTime::currentTime(); + time = QTime::currentTime(); } + + return {time, time_format}; } docstring getBufferInfo(Buffer const & buffer, const InsetInfoParams & params) { @@ -1444,7 +1450,7 @@ void docbookShortcutInfo(XMLStream & xs, const InsetInfoParams & params) { std::string attr; if (params.type == InsetInfoParams::SHORTCUTS_INFO) - attr = R"(role="shorcuts")"; + attr = R"(role="shortcuts")"; else if (params.type == InsetInfoParams::SHORTCUT_INFO) attr = R"(role="shortcut")"; else { @@ -1519,6 +1525,78 @@ void docbookShortcutInfo(XMLStream & xs, const InsetInfoParams & params) { xml::closeTag(xs, "shortcut", "inline"); } +void xhtmlShortcutInfo(XMLStream & xs, const InsetInfoParams & params) { + std::string attr; + if (params.type == InsetInfoParams::SHORTCUTS_INFO) + attr = R"(class="shortcuts")"; + else if (params.type == InsetInfoParams::SHORTCUT_INFO) + attr = R"(class="shortcut")"; + else { + // Only check for this assertion that exits this function. + lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params.type; + return; + } + + // shortcuts can change, so we need to re-do this each time + FuncRequest const func = lyxaction.lookupFunc(params.name); + if (func.action() == LFUN_UNKNOWN_ACTION) { + xml::openTag(xs, "span", attr, "inline"); + xs << _("Unknown action %1$s"); + xml::closeTag(xs, "span", "inline"); + return; + } + + KeyMap::Bindings bindings = theTopLevelKeymap().findBindings(func); + if (bindings.empty()) { + xml::openTag(xs, "span", attr, "inline"); + xs << _("undefined"); + xml::closeTag(xs, "span", "inline"); + return; + } + + docstring sequence; + docstring seq_untranslated; + if (params.type == InsetInfoParams::SHORTCUT_INFO) { + sequence = bindings.begin()->print(KeySequence::ForGui); + seq_untranslated = bindings.begin()->print(KeySequence::ForGui, true); + } else if (params.type == InsetInfoParams::SHORTCUTS_INFO) { + sequence = theTopLevelKeymap().printBindings(func, KeySequence::ForGui); + seq_untranslated = theTopLevelKeymap().printBindings(func, KeySequence::ForGui, true); + } + // No other possible case. + + Language const * tryguilang = languages.getFromCode(Messages::guiLanguage()); + // Some info insets use the language of the GUI (if available) + Language const * guilang = tryguilang ? tryguilang : params.lang; + const bool isTranslated = canTranslateKeySequence(params, sequence, seq_untranslated); + const bool isLtr = !isTranslated || (!guilang->rightToLeft() && !params.lang->rightToLeft()); + attr += std::string(" dir=\"") + (isLtr ? "ltr" : "rtl") + "\""; + // Use bdo instead of span to specify the text direction (dir is only allowed globally or on bdo). + xml::openTag(xs, "bdo", attr, "inline"); + + // QKeySequence returns special characters for keys on the mac + // Since these are not included in many fonts, we + // re-translate them to textual names (see #10641) + odocstringstream ods; + string const lcode = params.lang->code(); + docstring trans; + for (int i = 0; i < sequence.length(); ++i) { + char_type const c = sequence[i]; + const auto keyMapping = keyToString.find(c); + if (keyMapping != keyToString.end()) { + translateString(from_ascii(keyMapping->second), trans, lcode); + xs << trans; + } else { + xs << c; + } + + if (i > 0 && i + 1 < sequence.length()) + xs << from_ascii("+"); + } + + xml::closeTag(xs, "bdo", "inline"); +} + docstring getLyxRCInfo(const InsetInfoParams & params) { if (params.name.empty()) return _("undefined"); @@ -1625,6 +1703,67 @@ void docbookMenuInfo(XMLStream & xs, Buffer const & buffer, const InsetInfoParam xml::closeTag(xs, "menuchoice", "inline"); } + +void xhtmlMenuInfo(XMLStream & xs, Buffer const & buffer, const InsetInfoParams & params) { + docstring_list names; + FuncRequest func = lyxaction.lookupFunc(params.name); + if (func.action() == LFUN_UNKNOWN_ACTION) { + xml::openTag(xs, "span", "", "inline"); + xs << _("Unknown action %1$s"); + xml::closeTag(xs, "span", "inline"); + return; + } + + if (func.action() == LFUN_BUFFER_VIEW || func.action() == LFUN_BUFFER_UPDATE) { + // The default output format is in the menu without argument, + // so strip it here. + if (func.argument() == from_ascii(buffer.params().getDefaultOutputFormat())) + func = FuncRequest(func.action()); + } + + // iterate through the menubackend to find it + if (!theApp()) { + xml::openTag(xs, "span", "", "inline"); + xs << _("Can't determine menu entry for action %1$s in batch mode"); + xml::closeTag(xs, "span", "inline"); + return; + } + + // and we will not keep trying if we fail + if (!theApp()->searchMenu(func, names)) { + xml::openTag(xs, "span", "", "inline"); + xs << _("No menu entry for action %1$s"); + xml::closeTag(xs, "span", "inline"); + return; + } + + // if found, return its path. + Language const * tryguilang = languages.getFromCode(Messages::guiLanguage()); + // Some info insets use the language of the GUI (if available) + Language const * guilang = tryguilang ? tryguilang : params.lang; + const bool isLtr = !guilang->rightToLeft(); + const std::string attr = std::string("dir=\"") + (isLtr ? "ltr" : "rtl") + "\""; + // Use bdo instead of span to specify the text direction (dir is only allowed globally or on bdo). + xml::openTag(xs, "bdo", attr, "inline"); + + for (int i = 0; i < names.size(); ++i) { + docstring const & name = names[i]; + + //FIXME: add proper underlines here. This + // involves rewriting searchMenu used above to + // return a vector of menus. If we do not do + // that, we might as well use below + // Paragraph::insert on each string (JMarc) + // TODO: for DocBook, underlining corresponds to adding db:accel around the letter to underline. + xs << name; + + if (i > 0 && i + 1 < names.size()) + xs << "⇒"; // InsetSpecialChar::MENU_SEPARATOR + } + + xml::closeTag(xs, "bdo", "inline"); +} + void docbookIconInfo(XMLStream & xs, const OutputParams & rp, Buffer * buffer, const InsetInfoParams & params) { FuncRequest func = lyxaction.lookupFunc(params.name); docstring icon_name = frontend::Application::iconName(func, true); @@ -1669,6 +1808,50 @@ void docbookIconInfo(XMLStream & xs, const OutputParams & rp, Buffer * buffer, c xml::closeTag(xs, "guiicon", "inline"); } +void xhtmlIconInfo(XMLStream & xs, const OutputParams & rp, Buffer * buffer, const InsetInfoParams & params) { + FuncRequest func = lyxaction.lookupFunc(params.name); + docstring icon_name = frontend::Application::iconName(func, true); + FileName file(to_utf8(icon_name)); + if (file.onlyFileNameWithoutExt() == "unknown") { + std::string dir = "images"; + FileName file2(imageLibFileSearch(dir, params.name, "svgz,png")); + if (!file2.empty()) + file = file2; + } + + if (!file.exists()) + return; + + int percent_scale = 100; + if (use_gui) { + // Compute the scale factor for the icon such that its + // width on screen is equal to 1em in pixels. + // The scale factor is rounded to the integer nearest + // to the float value of the ratio 100*iconsize/imgsize. + int imgsize = QImage(toqstr(file.absFileName())).width(); + if (imgsize > 0) { + int iconsize = Length(1, Length::EM).inPixels(1); + percent_scale = (100 * iconsize + imgsize / 2) / imgsize; + } + } + + InsetGraphicsTight * inset = new InsetGraphicsTight(buffer); + InsetGraphicsParams igp; + igp.filename = file; + igp.lyxscale = percent_scale; + igp.scale = string(); + igp.width = Length(1, Length::EM); + if (contains(file.absoluteFilePath(), from_ascii("math")) + || contains(file.absoluteFilePath(), from_ascii("ert-insert")) + || suffixIs(file.onlyPath().absoluteFilePath(), from_ascii("ipa"))) + igp.darkModeSensitive = true; + inset->setParams(igp); + + xml::openTag(xs, "span", "class=\"guiicon\"", "inline"); + inset->xhtml(xs, rp); + xml::closeTag(xs, "span", "inline"); +} + docstring getLyXInfo(const InsetInfoParams & params) { if (params.name == "version") return from_ascii(lyx_version); @@ -1703,6 +1886,7 @@ docstring getNormalizedL7N(const InsetInfoParams & params) { } // namespace + void InsetInfo::docbook(XMLStream & xs, OutputParams const & rp) const { // TODO: away from a release, merge some of this code with InsetInfo::build and InsetInfoParams::getArguments. @@ -1737,7 +1921,7 @@ void InsetInfo::docbook(XMLStream & xs, OutputParams const & rp) const xml::openTag(xs, "date", "role=\"" + role + "\"", "inline"); else xs << XMLStream::ESCAPE_NONE << from_ascii(std::string(""); - xs << qstring_to_ucs4(parseDate(buffer(), params_).toString(Qt::ISODate)); + xs << qstring_to_ucs4(std::get<0>(parseDate(buffer(), params_)).toString(Qt::ISODate)); if (!isWithinDate) xml::closeTag(xs, "date", "inline"); break; @@ -1771,7 +1955,7 @@ void InsetInfo::docbook(XMLStream & xs, OutputParams const & rp) const xml::openTag(xs, "date", "role=\"" + role + "\"", "inline"); else xs << XMLStream::ESCAPE_NONE << from_ascii(std::string(""); - xs << qstring_to_ucs4(parseTime(buffer(), params_).toString(Qt::ISODate)); + xs << qstring_to_ucs4(std::get<0>(parseTime(buffer(), params_)).toString(Qt::ISODate)); if (!isWithinDate) xml::closeTag(xs, "date", "inline"); break; @@ -1845,4 +2029,134 @@ void InsetInfo::docbook(XMLStream & xs, OutputParams const & rp) const } +docstring InsetInfo::xhtml(XMLStream & xs, OutputParams const & rp) const +{ + // TODO: away from a release, merge some of this code with InsetInfo::build and InsetInfoParams::getArguments. + switch (params_.type) { + case InsetInfoParams::DATE_INFO: + case InsetInfoParams::MODDATE_INFO: + case InsetInfoParams::FIXDATE_INFO: { + std::string cssClass; + switch (params_.type) { + case InsetInfoParams::DATE_INFO: + cssClass = "current-date"; + break; + case InsetInfoParams::MODDATE_INFO: + cssClass = "last-modification-date"; + break; + case InsetInfoParams::FIXDATE_INFO: + cssClass = "fix-date"; + break; + default: + lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params().type; + break; + } + + QDate date; + std::string date_format; + std::tie(date, date_format) = parseDate(buffer(), params_); + + xml::openTag(xs, "span", std::string("class=\"infodate-") + cssClass + "\"", "inline"); + xs << params_.getDate(date_format, date); + xml::closeTag(xs, "span", "inline"); + break; + } + + case InsetInfoParams::TIME_INFO: + case InsetInfoParams::MODTIME_INFO: + case InsetInfoParams::FIXTIME_INFO: { + std::string cssClass; + switch (params_.type) { + case InsetInfoParams::TIME_INFO: + cssClass = "current-time"; + break; + case InsetInfoParams::MODTIME_INFO: + cssClass = "last-modification-time"; + break; + case InsetInfoParams::FIXTIME_INFO: + cssClass = "fix-time"; + break; + default: + lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params().type; + break; + } + + QTime time; + std::string time_format; + std::tie(time, time_format) = parseTime(buffer(), params_); + + xml::openTag(xs, "span", std::string("class=\"infotime-") + cssClass + "\"", "inline"); + xs << params_.getTime(time_format, time); + xml::closeTag(xs, "span", "inline"); + break; + } + + case InsetInfoParams::BUFFER_INFO: + xml::openTag(xs, "span", "class=\"buffer-info " + params_.name + "\"", "inline"); + xs << getBufferInfo(buffer(), params_); + xml::closeTag(xs, "span", "inline"); + break; + case InsetInfoParams::VCS_INFO: + xml::openTag(xs, "span", "class=\"vcs-info " + params_.name + "\"", "inline"); + xs << getVCSInfo(buffer(), params_); + xml::closeTag(xs, "span", "inline"); + break; + case InsetInfoParams::PACKAGE_INFO: + xml::openTag(xs, "span", "class=\"package-availability " + params_.name + "\"", "inline"); + xs << getPackageInfo(params_); + xml::closeTag(xs, "span", "inline"); + break; + case InsetInfoParams::TEXTCLASS_INFO: + xml::openTag(xs, "span", "class=\"textclass-availability " + params_.name + "\"", "inline"); + xs << getTextClassInfo(params_); + xml::closeTag(xs, "span", "inline"); + break; + + case InsetInfoParams::SHORTCUTS_INFO: + case InsetInfoParams::SHORTCUT_INFO: + xhtmlShortcutInfo(xs, params_); + break; + + case InsetInfoParams::LYXRC_INFO: + xml::openTag(xs, "span", "class=\"lyxrc-entry " + params_.name + "\"", "inline"); + xs << getLyxRCInfo(params_); + xml::closeTag(xs, "span", "inline"); + break; + + case InsetInfoParams::MENU_INFO: + xhtmlMenuInfo(xs, buffer(), params_); + break; + case InsetInfoParams::ICON_INFO: + xhtmlIconInfo(xs, rp, buffer_, params_); + break; + case InsetInfoParams::LYX_INFO: + xml::openTag(xs, "span", "class=\"lyx-info " + params_.name + "\"", "inline"); + xs << getLyXInfo(params_); + xml::closeTag(xs, "span", "inline"); + break; + + case InsetInfoParams::L7N_INFO: + xml::openTag(xs, "span", R"(class="localized" translate="no")", "inline"); + xs << getNormalizedL7N(params_); + xml::closeTag(xs, "span", "inline"); + break; + + case InsetInfoParams::UNKNOWN_INFO: + xml::openTag(xs, "span", R"(class="unknown")", "inline"); + xs << from_ascii("Unknown Info!"); + xml::closeTag(xs, "span", "inline"); + break; + default: + lyxerr << "Unrecognised InsetInfoParams::info_type: " << params().type; + + xml::openTag(xs, "span", R"(class="unrecognized")", "inline"); + xs << from_ascii("Unrecognized Info!"); + xml::closeTag(xs, "span", "inline"); + break; + } + + return from_ascii(""); +} + + } // namespace lyx diff --git a/src/insets/InsetInfo.h b/src/insets/InsetInfo.h index 95018379c7..a2ad046053 100644 --- a/src/insets/InsetInfo.h +++ b/src/insets/InsetInfo.h @@ -222,6 +222,8 @@ public: InsetInfoParams params() const { return params_; } /// Outputs the inset as DocBook, taking advantage of the metadata available in InsetInfoParams. void docbook(XMLStream &, OutputParams const &) const override; + /// Outputs the inset as XHTML, taking advantage of the metadata available in InsetInfoParams. + docstring xhtml(XMLStream &, OutputParams const &) const override; private: ///