On Sun, 8 Oct 2023 at 17:05, Scott Kostyshak <skost...@lyx.org> wrote:

> On Sun, Oct 08, 2023 at 02:48:45PM +0200, Thibaut Cuvelier wrote:
> > Just a quick amendment to a previous patch (#1 in the series): there was
> a
> > quite big typo there... It should solve the largest issue with the newly
> > failing test, but not all. I'm having a look at this test to improve the
> > situation a bit more.
> >
> > Thibaut Cuvelier
>
> Sounds good. Riki OK to commit these to master?
>

I've taken some time to solve the issue with that test. I am adding one
more patch to the list to solve it, plus another one to fix a crash I
noticed while debugging this example.

(FYI, the patch #4 in the sequence is a cleanup I made while having a look
at https://www.lyx.org/trac/ticket/12891.)
From 8b5836d493afd689f327384c69f501b04f2c590d Mon Sep 17 00:00:00 2001
From: Thibaut Cuvelier <tcuvelier@lyx.org>
Date: Sun, 8 Oct 2023 21:06:46 +0200
Subject: [PATCH 5/6] DocBook: in InsetInfo, ensure that no db:date is inserted
 within a db:date.

---
 autotests/export/docbook/insetinfo.lyx | 11 +++++++++++
 autotests/export/docbook/insetinfo.xml |  3 +++
 src/Paragraph.cpp                      | 11 ++++++-----
 src/insets/InsetInfo.cpp               | 27 ++++++++++++++++++++++----
 4 files changed, 43 insertions(+), 9 deletions(-)

diff --git a/autotests/export/docbook/insetinfo.lyx b/autotests/export/docbook/insetinfo.lyx
index 1ae6c30760..a1b04a54f9 100644
--- a/autotests/export/docbook/insetinfo.lyx
+++ b/autotests/export/docbook/insetinfo.lyx
@@ -91,6 +91,17 @@ Test:
  InsetInfo
 \end_layout
 
+\begin_layout Date
+
+\lang japanese-cjk
+\begin_inset Info
+type  "moddate"
+arg   "long"
+\end_inset
+
+
+\end_layout
+
 \begin_layout Standard
 
 \lang spanish
diff --git a/autotests/export/docbook/insetinfo.xml b/autotests/export/docbook/insetinfo.xml
index 48ba1d802d..dd0dd6630c 100644
--- a/autotests/export/docbook/insetinfo.xml
+++ b/autotests/export/docbook/insetinfo.xml
@@ -2,6 +2,9 @@
 <!-- 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">
+<info>
 <title>Test: InsetInfo</title>
+<date>2023-10-08</date>
+</info>
 <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/Paragraph.cpp b/src/Paragraph.cpp
index 333aeb8862..5ac03fa474 100644
--- a/src/Paragraph.cpp
+++ b/src/Paragraph.cpp
@@ -3635,11 +3635,11 @@ std::tuple<vector<xml::FontTag>, vector<xml::EndFontTag>> computeDocBookFontSwit
 
 std::tuple<std::vector<docstring>, std::vector<docstring>, std::vector<docstring>>
     Paragraph::simpleDocBookOnePar(Buffer const & buf,
-                                                      OutputParams const & runparams,
-                                                      Font const & outerfont,
-                                                      pos_type initial,
-                                                      bool is_last_par,
-                                                      bool ignore_fonts) const
+                                   OutputParams const & runparams,
+                                   Font const & outerfont,
+                                   pos_type initial,
+                                   bool is_last_par,
+                                   bool ignore_fonts) const
 {
 	// Return values: segregation of the content of this paragraph.
 	std::vector<docstring> prependedParagraphs; // Anything that must be output before the main tag of this paragraph.
@@ -3669,6 +3669,7 @@ std::tuple<std::vector<docstring>, std::vector<docstring>, std::vector<docstring
             }
         }
     }
+	rp.lastid = id();
 
     // State variables for the main loop.
     auto xs = new XMLStream(os); // XMLStream has no copy constructor: to create a new object, the only solution
diff --git a/src/insets/InsetInfo.cpp b/src/insets/InsetInfo.cpp
index 1f50dfef5f..b08eb7b815 100644
--- a/src/insets/InsetInfo.cpp
+++ b/src/insets/InsetInfo.cpp
@@ -1726,9 +1726,20 @@ void InsetInfo::docbook(XMLStream & xs, OutputParams const & rp) const
 			break;
 		}
 
-		xml::openTag(xs, "date", "role=\"" + role + "\"", "inline");
+		// A db:date cannot be nested within a db:date. This case typically happens when the document class defines a
+		// Date layout. In this case, avoid outputting a new db:date. This means that InsetInfo cannot add a role on top
+		// of the previous db:date, hence add it as a comment. (Another solution would be an XML processing instruction,
+		// but this case is not common enough.) Adding the role to the already output tag might have consequences for
+		// some document classes where the layout already has a role or uses the same role for another purpose.
+		const bool isWithinDate = buffer().getParFromID(rp.lastid).top().paragraph().layout().docbooktag() == "date";
+
+		if (!isWithinDate)
+			xml::openTag(xs, "date", "role=\"" + role + "\"", "inline");
+		else
+			xs << XMLStream::ESCAPE_NONE << from_ascii(std::string("<!-- ") + role + " -->");
 		xs << qstring_to_ucs4(parseDate(buffer(), params_).toString(Qt::ISODate));
-		xml::closeTag(xs, "date", "inline");
+		if (!isWithinDate)
+			xml::closeTag(xs, "date", "inline");
 		break;
 	}
 
@@ -1752,9 +1763,17 @@ void InsetInfo::docbook(XMLStream & xs, OutputParams const & rp) const
 		}
 
 		// DocBook has no specific element for time, so use a date.
-		xml::openTag(xs, "date", "(role=\"" + role + "\"", "inline");
+		// See the discussion above (DATE_INFO, MODDATE_INFO, and FIXDATE_INFO) for a discussion about the choices that
+		// have been made.
+		const bool isWithinDate = buffer().getParFromID(rp.lastid).top().paragraph().layout().docbooktag() == "date";
+
+		if (!isWithinDate)
+			xml::openTag(xs, "date", "role=\"" + role + "\"", "inline");
+		else
+			xs << XMLStream::ESCAPE_NONE << from_ascii(std::string("<!-- ") + role + " -->");
 		xs << qstring_to_ucs4(parseTime(buffer(), params_).toString(Qt::ISODate));
-		xml::closeTag(xs, "date", "inline");
+		if (!isWithinDate)
+			xml::closeTag(xs, "date", "inline");
 		break;
 	}
 
-- 
2.41.0.windows.1

From e0815bfe4f51a573bfb2e90c0fbb70d68c497e32 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/6] 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 0c99fec2a77f71ddce2d9ece1757a2a66f27b0a2 Mon Sep 17 00:00:00 2001
From: Thibaut Cuvelier <tcuvelier@lyx.org>
Date: Sun, 8 Oct 2023 21:11:17 +0200
Subject: [PATCH 6/6] DocBook: fix a crash in docbookSimpleAllParagraphs.

When having the code preview pane open with examples/Language_Support/Mixing_Japanese_with_other_Languages_(with_CJKutf8), clicking at random on paragraphs sometimes yielded a crash: *par when par is the end of the iterator. LyX could output the whole document as DocBook without this patch and still generates the same output with it (i.e. no text is missing).
---
 src/output_docbook.cpp | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/output_docbook.cpp b/src/output_docbook.cpp
index 19f6de43af..983d4e7bef 100644
--- a/src/output_docbook.cpp
+++ b/src/output_docbook.cpp
@@ -1127,8 +1127,9 @@ void docbookSimpleAllParagraphs(
 
 	// Then, the content. It starts where the <info> ends.
 	auto par = paragraphs.iterator_at(info.epit);
-	auto end = paragraphs.iterator_at(epit);
-	while (par != end) {
+	auto par_epit = paragraphs.iterator_at(epit);
+	auto par_end = paragraphs.end();
+	while (par != par_epit && par != par_end) {
 		if (!hasOnlyNotes(*par))
 			par = makeAny(text, buf, xs, runparams, par);
 		else
-- 
2.41.0.windows.1

From 74125e666be1ff472c8183e082ab3c4ad7ed8671 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/6] 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

From 198549907647861ddc055067c0fd59298ae925f8 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/6] 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..1f50dfef5f 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

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

Reply via email to