On Wed, 27 Sept 2023 at 19:46, Scott Kostyshak <skost...@lyx.org> wrote:

> On Wed, Sep 27, 2023 at 06:24:31PM +0200, Scott Kostyshak wrote:
> > commit bf241165dd9b30b26e45b5fcbab0e2bd0d143b4c
> > Author: Scott Kostyshak <skost...@lyx.org>
> > Date:   Wed Sep 27 13:42:40 2023 -0400
> >
> >     ctest: invert es/Intro_docbook5
> >
> >     This fails after recent changes to the document.
> > ---
>
> Hi Thibaut,
>
> The es/Intro_docbook5 test is failing after changes to the document. I
> know some of these issues are very hard to add support for, so my plan
> is to just invert them when they happen and CC you so that you can
> decide whether it's worth spending time on or not.
>

I'm attaching a patch that fixes the output from InsetInfo to match the
LaTeX output. This patch might be slightly controversial, since LyX 2.4 is
nearing the release; I made sure to touch the DocBook parts of the code
only to lower the risk of breakage.

Also, I'm adding two much smaller patches: one that improves code
readability, one that fixes a bug about formatting and change tracking.

I have manually checked that doc/es/Intro generates valid DocBook 5 (i.e. I
believe the test could be uninverted). Let me know if you find more issues!

Could someone please check this in the Git repository? Thanks!
From 0cef99615a371fb5b794091553dd29ec91a2de6f Mon Sep 17 00:00:00 2001
From: Thibaut Cuvelier <tcuvelier@lyx.org>
Date: Sun, 8 Oct 2023 01:20:14 +0200
Subject: [PATCH 1/3] DocBook: add support for InsetInfo.

A similar patch would be required for LyXHTML, but it will come later. The main impact is that some text isn't output in XHTML (like DocBook before this patch).

The code isn't as clean as it could be. I avoided touching anything not related to DocBook, as the release of 2.4 is nearing, while leaving comments for parts to improve for the next release cycle. Given that the code compiles, there are no risks for TeX or XHTML outputs; for DocBook, less content is skipped, which is a net improvement for users.
---
 autotests/export/docbook/insetinfo.lyx | 121 ++++++
 autotests/export/docbook/insetinfo.xml |   7 +
 src/insets/InsetInfo.cpp               | 545 +++++++++++++++++++++++++
 src/insets/InsetInfo.h                 |   2 +
 4 files changed, 675 insertions(+)
 create mode 100644 autotests/export/docbook/insetinfo.lyx
 create mode 100644 autotests/export/docbook/insetinfo.xml

diff --git a/autotests/export/docbook/insetinfo.lyx b/autotests/export/docbook/insetinfo.lyx
new file mode 100644
index 0000000000..1ae6c30760
--- /dev/null
+++ b/autotests/export/docbook/insetinfo.lyx
@@ -0,0 +1,121 @@
+#LyX 2.4 created this file. For more info see https://www.lyx.org/
+\lyxformat 620
+\begin_document
+\begin_header
+\save_transient_properties true
+\origin unavailable
+\textclass article
+\use_default_options true
+\maintain_unincluded_children no
+\language american
+\language_package default
+\inputencoding utf8
+\fontencoding auto
+\font_roman "default" "default"
+\font_sans "default" "default"
+\font_typewriter "default" "default"
+\font_math "auto" "auto"
+\font_default_family default
+\use_non_tex_fonts false
+\font_sc false
+\font_roman_osf false
+\font_sans_osf false
+\font_typewriter_osf false
+\font_sf_scale 100 100
+\font_tt_scale 100 100
+\use_microtype false
+\use_dash_ligatures true
+\graphics default
+\default_output_format default
+\output_sync 0
+\bibtex_command default
+\index_command default
+\float_placement class
+\float_alignment class
+\paperfontsize default
+\use_hyperref false
+\papersize default
+\use_geometry false
+\use_package amsmath 1
+\use_package amssymb 1
+\use_package cancel 1
+\use_package esint 1
+\use_package mathdots 1
+\use_package mathtools 1
+\use_package mhchem 1
+\use_package stackrel 1
+\use_package stmaryrd 1
+\use_package undertilde 1
+\cite_engine basic
+\cite_engine_type default
+\use_bibtopic false
+\use_indices false
+\paperorientation portrait
+\suppress_date false
+\justification true
+\use_refstyle 1
+\use_formatted_ref 0
+\use_minted 0
+\use_lineno 0
+\index Index
+\shortcut idx
+\color #008000
+\end_index
+\secnumdepth 3
+\tocdepth 3
+\paragraph_separation indent
+\paragraph_indentation default
+\is_math_indent 0
+\math_numbering_side default
+\quotes_style english
+\dynamic_quotes 0
+\papercolumns 1
+\papersides 1
+\paperpagestyle default
+\tablestyle default
+\tracking_changes false
+\output_changes false
+\change_bars false
+\postpone_fragile_content true
+\html_math_output 0
+\html_css_as_file 0
+\html_be_strict false
+\docbook_table_output 0
+\docbook_mathml_prefix 1
+\end_header
+
+\begin_body
+
+\begin_layout Title
+Test:
+ InsetInfo
+\end_layout
+
+\begin_layout Standard
+
+\lang spanish
+Véase la 
+\emph on
+
+\begin_inset Info
+type  "l7n"
+arg   "User's Guide|U"
+\end_inset
+
+
+\emph default
+ o 
+\emph on
+
+\begin_inset Info
+type  "l7n"
+arg   "Additional Features|F"
+\end_inset
+
+
+\emph default
+ para más detalles.
+\end_layout
+
+\end_body
+\end_document
diff --git a/autotests/export/docbook/insetinfo.xml b/autotests/export/docbook/insetinfo.xml
new file mode 100644
index 0000000000..48ba1d802d
--- /dev/null
+++ b/autotests/export/docbook/insetinfo.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This DocBook file was created by LyX 2.4.0~RC1.devel
+  See https://www.lyx.org/ for more information -->
+<article xml:lang="en-US" xmlns="http://docbook.org/ns/docbook"; xmlns:xlink="http://www.w3.org/1999/xlink"; xmlns:m="http://www.w3.org/1998/Math/MathML"; xmlns:xi="http://www.w3.org/2001/XInclude"; version="5.2">
+<title>Test: InsetInfo</title>
+<para>Véase la <emphasis><phrase role="localized">User's Guide</phrase></emphasis> o <emphasis><phrase role="localized">Additional Features</phrase></emphasis> para más detalles.</para>
+</article>
\ No newline at end of file
diff --git a/src/insets/InsetInfo.cpp b/src/insets/InsetInfo.cpp
index 06851bb080..865c34ec3b 100644
--- a/src/insets/InsetInfo.cpp
+++ b/src/insets/InsetInfo.cpp
@@ -31,6 +31,7 @@
 #include "LyXRC.h"
 #include "LyXVC.h"
 #include "Lexer.h"
+#include "output_docbook.h"
 #include "Paragraph.h"
 #include "ParIterator.h"
 #include "ParagraphParameters.h"
@@ -54,6 +55,7 @@
 #include "support/Translator.h"
 
 #include <sstream>
+#include <tuple>
 
 #include <QtGui/QImage>
 #include <QDate>
@@ -297,6 +299,7 @@ vector<pair<string,docstring>> InsetInfoParams::getArguments(Buffer const * buf,
 	case FIXDATE_INFO:
 	case DATE_INFO:
 	case MODDATE_INFO: {
+		// TODO: away from a release, use parseDate instead.
 		string const dt = split(name, '@');
 		QDate date;
 		if (itype == "moddate")
@@ -327,6 +330,7 @@ vector<pair<string,docstring>> InsetInfoParams::getArguments(Buffer const * buf,
 	case FIXTIME_INFO:
 	case TIME_INFO:
 	case MODTIME_INFO: {
+		// TODO: away from a release, use parseTime instead.
 		string const tt = split(name, '@');
 		QTime time;
 		if (itype == "modtime")
@@ -1002,6 +1006,7 @@ void InsetInfo::build()
 		break;
 	}
 	case InsetInfoParams::PACKAGE_INFO:
+		// TODO: away from a release, replace with getPackageInfo.
 		// only need to do this once.
 		if (initialized_)
 			break;
@@ -1029,6 +1034,7 @@ void InsetInfo::build()
 		break;
 
 	case InsetInfoParams::TEXTCLASS_INFO: {
+		// TODO: when away from a release, replace with getTextClassInfo.
 		// the TextClass can change
 		LayoutFileList const & list = LayoutFileList::get();
 		bool available = false;
@@ -1097,6 +1103,7 @@ void InsetInfo::build()
 		break;
 	}
 	case InsetInfoParams::L7N_INFO: {
+		// TODO: away from a release, use getNormalizedL7N instead.
 		docstring locstring = _(params_.name);
 		// Remove trailing colons
 		locstring = rtrim(locstring, ":");
@@ -1161,6 +1168,7 @@ void InsetInfo::build()
 		break;
 	}
 	case InsetInfoParams::BUFFER_INFO: {
+		// TODO: away from a release, replace by getBufferInfo.
 		// this could all change, so we will recalculate each time
 		if (params_.name == "name")
 			setText(from_utf8(buffer().fileName().onlyFileName()), params_.lang);
@@ -1173,6 +1181,7 @@ void InsetInfo::build()
 		break;
 	}
 	case InsetInfoParams::VCS_INFO: {
+		// TODO: away from a release, replace by getVCSInfo.
 		// this information could change, in principle, so we will 
 		// recalculate each time through
 		if (!buffer().lyxvc().inUse()) {
@@ -1214,6 +1223,7 @@ void InsetInfo::build()
 	case InsetInfoParams::DATE_INFO:
 	case InsetInfoParams::MODDATE_INFO:
 	case InsetInfoParams::FIXDATE_INFO: {
+		// TODO: away from a release, use parseDate instead.
 		string date_format = params_.name;
 		string const date_specifier = (params_.type == InsetInfoParams::FIXDATE_INFO
 					       && contains(params_.name, '@'))
@@ -1235,6 +1245,7 @@ void InsetInfo::build()
 	case InsetInfoParams::TIME_INFO:
 	case InsetInfoParams::MODTIME_INFO:
 	case InsetInfoParams::FIXTIME_INFO: {
+		// TODO: away from a release, use parseTime instead.
 		string time_format = params_.name;
 		string const time_specifier = (params_.type == InsetInfoParams::FIXTIME_INFO
 					       && contains(params_.name, '@'))
@@ -1280,5 +1291,539 @@ string InsetInfo::contextMenuName() const
 	return "context-info";
 }
 
+namespace {
+
+// TODO: away from a release, use these functions in InsetInfo::build and InsetInfoParams::getArguments.
+
+QDate 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();
+
+	if (params.type == InsetInfoParams::MODDATE_INFO)
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
+		return QDateTime::fromSecsSinceEpoch(buffer.fileName().lastModified()).date();
+#else
+		return 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();
+	} else {
+		if (params.type != InsetInfoParams::DATE_INFO && params.type != InsetInfoParams::FIXDATE_INFO)
+			lyxerr << "Unexpected InsetInfoParams::info_type in parseDate: " << params.type;
+		return QDate::currentDate();
+	}
+}
+
+QTime 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();
+
+	if (params.type == InsetInfoParams::MODTIME_INFO)
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
+		return QDateTime::fromSecsSinceEpoch(buffer.fileName().lastModified()).time();
+#else
+		return 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();
+	} else {
+		if (params.type != InsetInfoParams::TIME_INFO && params.type != InsetInfoParams::FIXTIME_INFO)
+			lyxerr << "Unexpected InsetInfoParams::info_type in parseTime: " << params.type;
+		return QTime::currentTime();
+	}
+}
+
+docstring getBufferInfo(Buffer const & buffer, const InsetInfoParams & params) {
+	if (params.name == "name")
+		return from_utf8(buffer.fileName().onlyFileName());
+	else if (params.name == "name-noext")
+		return from_utf8(buffer.fileName().onlyFileNameWithoutExt());
+	else if (params.name == "path")
+		return from_utf8(os::latex_path(buffer.filePath()));
+	else if (params.name == "class")
+		return from_utf8(buffer.params().documentClass().name());
+	else {
+		lyxerr << "Unexpected name for InsetInfoParams::BUFFER_INFO: " << params.name;
+		return from_ascii("");
+	}
+}
+
+docstring getVCSInfo(Buffer const & buffer, const InsetInfoParams & params) {
+	if (!buffer.lyxvc().inUse())
+		return _("No version control!");
+
+	LyXVC::RevisionInfo itype = LyXVC::Unknown;
+	if (params.name == "revision")
+		itype = LyXVC::File;
+	else if (params.name == "revision-abbrev")
+		itype = LyXVC::FileAbbrev;
+	else if (params.name == "tree-revision")
+		itype = LyXVC::Tree;
+	else if (params.name == "author")
+		itype = LyXVC::Author;
+	else if (params.name == "time")
+		itype = LyXVC::Time;
+	else if (params.name == "date")
+		itype = LyXVC::Date;
+
+	string binfo = buffer.lyxvc().revisionInfo(itype);
+	if (binfo.empty())
+		return from_ascii("VCS info unknown!");
+	else
+		return from_utf8(binfo);
+}
+
+docstring getPackageInfo(const InsetInfoParams & params) {
+	// check in packages.lst
+	bool available;
+	// we also allow version check with version separated by blank
+	if (contains(params.name, ' ')) {
+		string name;
+		string const version = split(params.name, name, ' ');
+		int const y = convert<int>(version.substr(0,4));
+		int const m = convert<int>(version.substr(4,2));
+		int const d = convert<int>(version.substr(6,2));
+		available = LaTeXFeatures::isAvailableAtLeastFrom(name, y, m, d);
+	} else
+		available = LaTeXFeatures::isAvailable(params.name);
+
+	return from_ascii(available ? "yes" : "no");
+}
+
+docstring getTextClassInfo(const InsetInfoParams & params) {
+	LayoutFileList const & list = LayoutFileList::get();
+	// params_.name is the class name
+	const bool available = list.haveClass(params.name) && list[params.name].isTeXClassAvailable();
+	return from_ascii(available ? "yes" : "no");
+}
+
+// With C++17, it would be better to have a std::string_view instead of const char *.
+const static std::map<char_type, const char *> keyToString {
+		{0x21b5, "Return[[Key]]"}, // Return
+		{0x21b9, "Tab[[Key]]"}, // Tab both directions (Win)
+		{0x21de, "PgUp"}, // Qt::Key_PageUp
+		{0x21df, "PgDown"}, // Qt::Key_PageDown
+		{0x21e4, "Backtab"}, // Qt::Key_Backtab
+		{0x21e5, "Tab"}, // Qt::Key_Tab
+		{0x21e7, "Shift"}, // Shift
+		{0x21ea, "CapsLock"}, // Qt::Key_CapsLock
+		{0x2303, "Control[[Key]]"}, // Control
+		{0x2318, "Command[[Key]]"}, // CMD
+		{0x2324, "Return[[Key]]"}, // Qt::Key_Enter
+		{0x2325, "Option[[Key]]"}, // Option key
+		{0x2326, "Delete[[Key]]"}, // Qt::Key_Delete
+		{0x232b, "Fn+Del"}, // Qt::Key_Backspace
+		{0x238b, "Esc"}, // Qt::Key_Escape
+};
+
+bool canTranslateKeySequence(const InsetInfoParams & params, const docstring & sequence,
+							 const docstring & seq_untranslated) {
+	bool is_translated = sequence != seq_untranslated;
+	std::string const lcode = params.lang->code();
+	docstring trans;
+
+	for (char_type const c : sequence) {
+		const auto keyMapping = keyToString.find(c);
+		if (keyMapping != keyToString.end()) {
+			is_translated = translateString(from_ascii(keyMapping->second), trans, lcode);
+		}
+	}
+
+	return is_translated;
+}
+
+void docbookShortcutInfo(XMLStream & xs, const InsetInfoParams & params) {
+	// Usually, a keyboard shortcut should be encoded as db:shortcut. However, this element doesn't accept text, hence
+	// the use of db:accel for error cases (not the right semantics, though).
+
+	std::string attr;
+	if (params.type == InsetInfoParams::SHORTCUTS_INFO)
+		attr = R"(role="shorcuts")";
+	else if (params.type == InsetInfoParams::SHORTCUT_INFO)
+		attr = R"(role="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, "accel", attr, "inline");
+		xs << _("Unknown action %1$s");
+		xml::closeTag(xs, "accel", "inline");
+		return;
+	}
+
+	KeyMap::Bindings bindings = theTopLevelKeymap().findBindings(func);
+	if (bindings.empty()) {
+		xml::openTag(xs, "accel", attr, "inline");
+		xs << _("undefined");
+		xml::closeTag(xs, "accel", "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") + "\"";
+	attr += " action=\"simul\"";
+	xml::openTag(xs, "shortcut", attr, "inline");
+	xml::openTag(xs, "keycombo", "", "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 (char_type const c : sequence) {
+		const auto keyMapping = keyToString.find(c);
+		if (keyMapping != keyToString.end()) {
+			translateString(from_ascii(keyMapping->second), trans, lcode);
+
+			// db:keysym: symbolic name (like Page Up), unlike what is printed on the key (like
+			// ⇞, ↑, ▲, PgUp, Page Up, etc.)
+			xml::openTag(xs, "keysym", "", "inline");
+			xs << trans;
+			xml::closeTag(xs, "keysym", "inline");
+		} else {
+			// db:keycap: this is not a special key, c is really what is printed on the key.
+			xml::openTag(xs, "keycap", "", "inline");
+			xs << c;
+			xml::closeTag(xs, "keycap", "inline");
+		}
+	}
+
+	xml::closeTag(xs, "keycombo", "inline");
+	xml::closeTag(xs, "shortcut", "inline");
+}
+
+docstring getLyxRCInfo(const InsetInfoParams & params) {
+	if (params.name.empty())
+		return _("undefined");
+
+	// this information could change, if the preferences are changed,
+	// so we will recalculate each time through.
+	// FIXME this uses the serialization mechanism to get the info
+	// we want, which i guess works but is a bit strange.
+	ostringstream oss;
+	lyxrc.write(oss, true, params.name);
+	string result = oss.str();
+	if (result.size() < 2) {
+		return _("undefined");
+	}
+
+	string::size_type loc = result.rfind('\n', result.size() - 2);
+	loc = loc == string::npos ? 0 : loc + 1;
+	if (result.size() < loc + params.name.size() + 1
+	    || result.substr(loc + 1, params.name.size()) != params.name) {
+		return _("undefined");
+	}
+
+	// remove leading comments and \\name and space
+	result = result.substr(loc + params.name.size() + 2);
+
+	// remove \n and ""
+	result = rtrim(result, "\n");
+	result = trim(result, "\"");
+
+	if (result.empty())
+		return from_ascii("not set");
+	else
+		return from_utf8(result);
+}
+
+void docbookMenuInfo(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, "guimenuitem", "", "inline");
+		xs << _("Unknown action %1$s");
+		xml::closeTag(xs, "guimenuitem", "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, "guimenuitem", "", "inline");
+		xs << _("Can't determine menu entry for action %1$s in batch mode");
+		xml::closeTag(xs, "guimenuitem", "inline");
+		return;
+	}
+
+	// and we will not keep trying if we fail
+	if (!theApp()->searchMenu(func, names)) {
+		xml::openTag(xs, "guimenuitem", "", "inline");
+		xs << _("No menu entry for action %1$s");
+		xml::closeTag(xs, "guimenuitem", "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") + "\"";
+
+	xml::openTag(xs, "menuchoice", attr, "inline"); // More of an inline tag in this case, as there is no db:shortcut to
+	// accompany the succession of menus.
+
+	for (int i = 0; i < names.size(); ++i) {
+	    docstring const & name = names[i];
+
+		std::string tag;
+		if (i == 0) {
+			tag = "guimenu";
+		} else if (i == names.size() - 1) {
+			tag = "guimenuitem";
+		} else {
+			tag = "guisubmenu";
+		}
+
+		xml::openTag(xs, tag, "", "inline");
+
+		//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;
+
+		xml::closeTag(xs, tag, "inline");
+	}
+
+	xml::closeTag(xs, "menuchoice", "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);
+	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, "guiicon", "", "inline");
+	inset->docbook(xs, rp);
+	xml::closeTag(xs, "guiicon", "inline");
+}
+
+docstring getLyXInfo(const InsetInfoParams & params) {
+	if (params.name == "version")
+		return from_ascii(lyx_version);
+	else if (params.name == "layoutformat")
+		return convert<docstring>(LAYOUT_FORMAT);
+	else {
+		lyxerr << "Unexpected name for InsetInfoParams::BUFFER_INFO: " << params.name;
+		return from_ascii("");
+	}
+}
+
+docstring getNormalizedL7N(const InsetInfoParams & params) {
+	docstring locstring = _(params.name);
+
+	// Remove trailing colons
+	locstring = rtrim(locstring, ":");
+
+	// Remove menu accelerators
+	if (contains(locstring, from_ascii("|"))) {
+		docstring nlocstring;
+		rsplit(locstring, nlocstring, '|');
+		locstring = nlocstring;
+	}
+
+	// Remove Qt accelerators, but keep literal ampersands
+	locstring = subst(locstring, from_ascii(" & "), from_ascii("</amp;>"));
+	locstring = subst(locstring, from_ascii("&"), docstring());
+	locstring = subst(locstring, from_ascii("</amp;>"), from_ascii(" & "));
+
+	return locstring;
+}
+
+} // 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.
+	switch (params_.type) {
+	case InsetInfoParams::DATE_INFO:
+	case InsetInfoParams::MODDATE_INFO:
+	case InsetInfoParams::FIXDATE_INFO: {
+		std::string role;
+		switch (params_.type) {
+		case InsetInfoParams::DATE_INFO:
+			role = "current-date";
+			break;
+		case InsetInfoParams::MODDATE_INFO:
+			role = "last-modification-date";
+			break;
+		case InsetInfoParams::FIXDATE_INFO:
+			role = "fix-date";
+			break;
+		default:
+			lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params().type;
+			break;
+		}
+
+		xml::openTag(xs, "date", "(role=\"" + role + "\"", "inline");
+		xs << qstring_to_ucs4(parseDate(buffer(), params_).toString(Qt::ISODate));
+		xml::closeTag(xs, "date", "inline");
+		break;
+	}
+
+	case InsetInfoParams::TIME_INFO:
+	case InsetInfoParams::MODTIME_INFO:
+	case InsetInfoParams::FIXTIME_INFO: {
+		std::string role;
+		switch (params_.type) {
+		case InsetInfoParams::TIME_INFO:
+			role = "current-time";
+			break;
+		case InsetInfoParams::MODTIME_INFO:
+			role = "last-modification-time";
+			break;
+		case InsetInfoParams::FIXTIME_INFO:
+			role = "fix-time";
+			break;
+		default:
+			lyxerr << "Assertion failed! InsetInfoParams::info_type: " << params().type;
+			break;
+		}
+
+		// DocBook has no specific element for time, so use a date.
+		xml::openTag(xs, "date", "(role=\"" + role + "\"", "inline");
+		xs << qstring_to_ucs4(parseTime(buffer(), params_).toString(Qt::ISODate));
+		xml::closeTag(xs, "date", "inline");
+		break;
+	}
+
+	case InsetInfoParams::BUFFER_INFO:
+		xml::openTag(xs, "phrase", "role=\"buffer-info " + params_.name + "\"", "inline");
+		xs << getBufferInfo(buffer(), params_);
+		xml::closeTag(xs, "phrase", "inline");
+		break;
+	case InsetInfoParams::VCS_INFO:
+		xml::openTag(xs, "phrase", "role=\"vcs-info " + params_.name + "\"", "inline");
+		xs << getVCSInfo(buffer(), params_);
+		xml::closeTag(xs, "phrase", "inline");
+		break;
+	case InsetInfoParams::PACKAGE_INFO:
+		xml::openTag(xs, "phrase", "role=\"package-availability " + params_.name + "\"", "inline");
+		xs << getPackageInfo(params_);
+		xml::closeTag(xs, "phrase", "inline");
+		break;
+	case InsetInfoParams::TEXTCLASS_INFO:
+		xml::openTag(xs, "phrase", "role=\"textclass-availability " + params_.name + "\"", "inline");
+		xs << getTextClassInfo(params_);
+		xml::closeTag(xs, "phrase", "inline");
+		break;
+
+	case InsetInfoParams::SHORTCUTS_INFO:
+	case InsetInfoParams::SHORTCUT_INFO:
+		docbookShortcutInfo(xs, params_);
+		break;
+		
+	case InsetInfoParams::LYXRC_INFO:
+		xml::openTag(xs, "phrase", "role=\"lyxrc-entry " + params_.name + "\"", "inline");
+		xs << getLyxRCInfo(params_);
+		xml::closeTag(xs, "phrase", "inline");
+		break;
+
+	case InsetInfoParams::MENU_INFO:
+		docbookMenuInfo(xs, buffer(), params_);
+		break;
+	case InsetInfoParams::ICON_INFO:
+		docbookIconInfo(xs, rp, buffer_, params_);
+		break;
+	case InsetInfoParams::LYX_INFO:
+		xml::openTag(xs, "phrase", "role=\"lyx-info " + params_.name + "\"", "inline");
+		xs << getLyXInfo(params_);
+		xml::closeTag(xs, "phrase", "inline");
+		break;
+
+	case InsetInfoParams::L7N_INFO:
+		// TODO: add "its:translate="no"" in the attributes if ITS is globally enabled for LyX (quite rare to have ITS
+		// for DocBook documents).
+		xml::openTag(xs, "phrase", R"(role="localized")", "inline");
+		xs << getNormalizedL7N(params_);
+		xml::closeTag(xs, "phrase", "inline");
+		break;
+
+	case InsetInfoParams::UNKNOWN_INFO:
+		xml::openTag(xs, "phrase", R"(role="unknown")", "inline");
+		xs << from_ascii("Unknown Info!");
+		xml::closeTag(xs, "phrase", "inline");
+		break;
+	default:
+		lyxerr << "Unrecognised InsetInfoParams::info_type: " << params().type;
+
+		xml::openTag(xs, "phrase", R"(role="unrecognized")", "inline");
+		xs << from_ascii("Unrecognized Info!");
+		xml::closeTag(xs, "phrase", "inline");
+		break;
+	}
+}
+
 
 } // namespace lyx
diff --git a/src/insets/InsetInfo.h b/src/insets/InsetInfo.h
index 3531b41681..95018379c7 100644
--- a/src/insets/InsetInfo.h
+++ b/src/insets/InsetInfo.h
@@ -220,6 +220,8 @@ public:
 	void validate(LaTeXFeatures & features) const override;
 	///
 	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;
 
 private:
 	///
-- 
2.41.0.windows.1

From 773a40bfe6b739d8ec5ccc1fb40352d6fa03c419 Mon Sep 17 00:00:00 2001
From: Thibaut Cuvelier <tcuvelier@lyx.org>
Date: Sun, 8 Oct 2023 01:38:27 +0200
Subject: [PATCH 2/3] DocBook: fix formatting of TODOs.

The lines were very long; this commit truncates them to 120 characters.
---
 src/output_docbook.cpp | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/output_docbook.cpp b/src/output_docbook.cpp
index c80a7cdb91..19f6de43af 100644
--- a/src/output_docbook.cpp
+++ b/src/output_docbook.cpp
@@ -1300,7 +1300,8 @@ void docbookParagraphs(Text const &text,
 		}
 
 		// Close all sections before the bibliography.
-		// TODO: Only close all when the bibliography is at the end of the document? Or force to output the bibliography at the end of the document? Or don't care (as allowed by DocBook)?
+		// TODO: Only close all when the bibliography is at the end of the document? Or force to output the bibliography
+		// at the end of the document? Or don't care (as allowed by DocBook)?
 		if (!par->insetList().empty()) {
 			Inset const *firstInset = par->getInset(0);
 			if (firstInset && (firstInset->lyxCode() == BIBITEM_CODE || firstInset->lyxCode() == BIBTEX_CODE)) {
@@ -1326,7 +1327,8 @@ void docbookParagraphs(Text const &text,
 		// Generate the <info> tag if a section was just opened.
 		// Some sections may require abstracts (mostly parts, in books: DocBookForceAbstractTag will not be NONE),
 		// others can still have an abstract (it must be detected so that it can be output at the right place).
-		// TODO: docbookforceabstracttag is a bit contrived here, but it does the job. Having another field just for this would be cleaner, but that's just for <part> and <partintro>, so it's probably not worth the effort.
+		// TODO: docbookforceabstracttag is a bit contrived here, but it does the job. Having another field just for
+		// this would be cleaner, but that's just for <part> and <partintro>, so it's probably not worth the effort.
 		if (isLayoutSectioning(style)) {
 			// This abstract may be found between the next paragraph and the next title.
 			pit_type cpit = std::distance(text.paragraphs().begin(), par);
-- 
2.41.0.windows.1

From 267a79eb415fc04e473a692249644d3fc4b0344c Mon Sep 17 00:00:00 2001
From: Thibaut Cuvelier <tcuvelier@lyx.org>
Date: Sun, 8 Oct 2023 01:39:45 +0200
Subject: [PATCH 3/3] DocBook: fix closing formatting after deleted text.

Previously, when closing font tags, only the previous character's font was used. However, if that character is deleted, it had no change of having the right font tags opened/closed. Hence, look further to compare the font of the current character to output with the font of the previously output character.
---
 .../export/docbook/tracking_formatting.lyx    | 111 ++++++++++++++++++
 .../export/docbook/tracking_formatting.xml    |   7 ++
 src/Paragraph.cpp                             |  10 +-
 3 files changed, 126 insertions(+), 2 deletions(-)
 create mode 100644 autotests/export/docbook/tracking_formatting.lyx
 create mode 100644 autotests/export/docbook/tracking_formatting.xml

diff --git a/autotests/export/docbook/tracking_formatting.lyx b/autotests/export/docbook/tracking_formatting.lyx
new file mode 100644
index 0000000000..44802ff48e
--- /dev/null
+++ b/autotests/export/docbook/tracking_formatting.lyx
@@ -0,0 +1,111 @@
+#LyX 2.4 created this file. For more info see https://www.lyx.org/
+\lyxformat 620
+\begin_document
+\begin_header
+\save_transient_properties true
+\origin unavailable
+\textclass article
+\use_default_options true
+\maintain_unincluded_children no
+\language american
+\language_package default
+\inputencoding utf8
+\fontencoding auto
+\font_roman "default" "default"
+\font_sans "default" "default"
+\font_typewriter "default" "default"
+\font_math "auto" "auto"
+\font_default_family default
+\use_non_tex_fonts false
+\font_sc false
+\font_roman_osf false
+\font_sans_osf false
+\font_typewriter_osf false
+\font_sf_scale 100 100
+\font_tt_scale 100 100
+\use_microtype false
+\use_dash_ligatures true
+\graphics default
+\default_output_format default
+\output_sync 0
+\bibtex_command default
+\index_command default
+\float_placement class
+\float_alignment class
+\paperfontsize default
+\use_hyperref false
+\papersize default
+\use_geometry false
+\use_package amsmath 1
+\use_package amssymb 1
+\use_package cancel 1
+\use_package esint 1
+\use_package mathdots 1
+\use_package mathtools 1
+\use_package mhchem 1
+\use_package stackrel 1
+\use_package stmaryrd 1
+\use_package undertilde 1
+\cite_engine basic
+\cite_engine_type default
+\use_bibtopic false
+\use_indices false
+\paperorientation portrait
+\suppress_date false
+\justification true
+\use_refstyle 1
+\use_formatted_ref 0
+\use_minted 0
+\use_lineno 0
+\index Index
+\shortcut idx
+\color #008000
+\end_index
+\secnumdepth 3
+\tocdepth 3
+\paragraph_separation indent
+\paragraph_indentation default
+\is_math_indent 0
+\math_numbering_side default
+\quotes_style english
+\dynamic_quotes 0
+\papercolumns 1
+\papersides 1
+\paperpagestyle default
+\tablestyle default
+\tracking_changes true
+\output_changes false
+\change_bars false
+\postpone_fragile_content true
+\html_math_output 0
+\html_css_as_file 0
+\html_be_strict false
+\docbook_table_output 0
+\docbook_mathml_prefix 1
+\author 1075283030 "Thibaut"
+\end_header
+
+\begin_body
+
+\begin_layout Title
+Test:
+ deleted text just after the end of formatted text
+\end_layout
+
+\begin_layout Standard
+
+\lang spanish
+incluye el archivo 
+\family typewriter
+config.log
+\change_deleted 1075283030 1696721001
+
+\family default
+,
+
+\change_unchanged
+ y menciona el compilador que usas.
+\end_layout
+
+\end_body
+\end_document
diff --git a/autotests/export/docbook/tracking_formatting.xml b/autotests/export/docbook/tracking_formatting.xml
new file mode 100644
index 0000000000..fb245c1476
--- /dev/null
+++ b/autotests/export/docbook/tracking_formatting.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This DocBook file was created by LyX 2.4.0~RC1.devel
+  See https://www.lyx.org/ for more information -->
+<article xml:lang="en-US" xmlns="http://docbook.org/ns/docbook"; xmlns:xlink="http://www.w3.org/1999/xlink"; xmlns:m="http://www.w3.org/1998/Math/MathML"; xmlns:xi="http://www.w3.org/2001/XInclude"; version="5.2">
+<title>Test: deleted text just after the end of formatted text</title>
+<para>incluye el archivo <code>config.log</code> y menciona el compilador que usas.</para>
+</article>
\ No newline at end of file
diff --git a/src/Paragraph.cpp b/src/Paragraph.cpp
index 3718bd4a98..333aeb8862 100644
--- a/src/Paragraph.cpp
+++ b/src/Paragraph.cpp
@@ -3721,10 +3721,16 @@ std::tuple<std::vector<docstring>, std::vector<docstring>, std::vector<docstring
 			}
 		}
 
-		// Determine which tags should be opened or closed regarding fonts.
+		// Determine which tags should be opened or closed regarding fonts. Consider the last output character (i.e. not
+		// deleted).
+		int last_output_char = (i == 0) ? 0 : i - 1;
+		if (i > 0) {
+			while (last_output_char > 0 && isDeleted(last_output_char))
+				--last_output_char;
+		}
 		FontInfo const font_old = (i == 0 ?
 				(style.labeltype == LABEL_MANUAL ? style.labelfont : style.font) :
-				getFont(buf.masterBuffer()->params(), i - 1, outerfont).fontInfo());
+				getFont(buf.masterBuffer()->params(), last_output_char, outerfont).fontInfo());
 		Font const font = getFont(buf.masterBuffer()->params(), i, outerfont);
         tie(tagsToOpen, tagsToClose) = computeDocBookFontSwitch(
 				font_old, font, buf.masterBuffer()->params().fonts_default_family, fs);
-- 
2.41.0.windows.1

-- 
lyx-devel mailing list
lyx-devel@lists.lyx.org
http://lists.lyx.org/mailman/listinfo/lyx-devel

Reply via email to