[LyX/master] Support for front/main/backmatter (#12424)

Juergen Spitzmueller spitz at lyx.org
Mon Aug 3 09:30:41 UTC 2026


commit e569426b1583df3b7dd6c606cc0feadc37e5723b
Author: Juergen Spitzmueller <spitz at lyx.org>
Date:   Mon Aug 3 11:29:12 2026 +0200

    Support for front/main/backmatter (#12424)
    
    Based on a patch by Daniel Ramöller
    
    File and layout format change
---
 development/FORMAT            |   5 ++
 lib/doc/Customization.lyx     | 121 ++++++++++++++++++++++++++++++++++++++++++
 lib/doc/UserGuide.lyx         |  72 +++++++++++++++++++++++++
 lib/doc/de/Customization.lyx  |  76 ++++++++++++++++++++++++++
 lib/doc/de/UserGuide.lyx      |  76 ++++++++++++++++++++++++++
 lib/layouts/amsbook.layout    |   1 +
 lib/layouts/book.layout       |   4 +-
 lib/layouts/memoir.layout     |   1 +
 lib/layouts/recipebook.layout |   1 +
 lib/layouts/scrbook.layout    |   1 +
 lib/layouts/tufte-book.layout |   1 +
 lib/lyx2lyx/lyx_2_6.py        |  33 +++++++++++-
 lib/lyx2lyx/parser_tools.py   |   1 +
 lib/scripts/layout2layout.py  |   7 ++-
 lib/ui/stdmenus.inc           |   7 +++
 src/Buffer.cpp                |  11 +++-
 src/Counters.cpp              |   4 +-
 src/Counters.h                |   9 ++++
 src/FuncCode.h                |   1 +
 src/LayoutEnums.h             |  12 +++++
 src/LyXAction.cpp             |  10 ++++
 src/ParagraphParameters.cpp   |  46 ++++++++++++++++
 src/ParagraphParameters.h     |  12 +++++
 src/RowPainter.cpp            |  47 ++++++++++++----
 src/RowPainter.h              |   2 +-
 src/Text.cpp                  |  44 +++++++++++++++
 src/TextClass.cpp             |  11 +++-
 src/TextClass.h               |   4 ++
 src/TextMetrics.cpp           |   7 +--
 src/output_latex.cpp          |  15 ++++++
 src/tex2lyx/text.cpp          |  72 +++++++++++++++++++++++++
 src/version.h                 |   4 +-
 32 files changed, 692 insertions(+), 26 deletions(-)

diff --git a/development/FORMAT b/development/FORMAT
index 21bad579ed..fae968c761 100644
--- a/development/FORMAT
+++ b/development/FORMAT
@@ -7,6 +7,11 @@ changes happened in particular if possible. A good example would be
 
 -----------------------
 
+2026-08-03 Jürgen Spitzmüller <spitz at lyx.org>
+	* Format incremented to 657: Support for \fronmatter, \mainmatter,
+	  and \backmatter via paragraph parameter
+	  \start_of_matter {front,main,back}
+
 2026-08-02 Jürgen Spitzmüller <spitz at lyx.org>
 	* Format incremented to 656: Support for parfillskip
 	  New buffer param \parfillskip "<type>" "<glue length>"
diff --git a/lib/doc/Customization.lyx b/lib/doc/Customization.lyx
index c4e396b38f..fad374325d 100644
--- a/lib/doc/Customization.lyx
+++ b/lib/doc/Customization.lyx
@@ -11524,6 +11524,107 @@ nolink "false"
 \end_inset
 
  for the list of features.
+\change_inserted -712698321 1785747170
+
+\end_layout
+
+\begin_layout Description
+
+\change_inserted -712698321 1785747231
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\change_inserted -712698321 1785747178
+ProvidesMatters
+\end_layout
+
+\end_inset
+
+ [
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\change_inserted -712698321 1785747171
+
+\emph on
+0
+\end_layout
+
+\end_inset
+
+
+\begin_inset space \thinspace{}
+\end_inset
+
+
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\change_inserted -712698321 1785747171
+1
+\end_layout
+
+\end_inset
+
+] indicates whether the text class provides for 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\change_inserted -712698321 1785747217
+
+\backslash
+frontmatter
+\change_unchanged
+
+\end_layout
+
+\end_inset
+
+,
+ 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\change_inserted -712698321 1785747224
+
+\backslash
+mainmatter
+\change_unchanged
+
+\end_layout
+
+\end_inset
+
+,
+ and 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\change_inserted -712698321 1785747231
+
+\backslash
+backmatter
+\change_unchanged
+
+\end_layout
+
+\end_inset
+
+ document division.
+\change_unchanged
+
 \end_layout
 
 \begin_layout Description
@@ -12607,6 +12708,8 @@ status collapsed
 
 \change_inserted -712698321 1785664317
 ParfillskipNone
+\change_unchanged
+
 \end_layout
 
 \end_inset
@@ -12619,6 +12722,8 @@ status collapsed
 
 \change_inserted -712698321 1785664297
 string
+\change_unchanged
+
 \end_layout
 
 \end_inset
@@ -12647,6 +12752,8 @@ status collapsed
 
 \change_inserted -712698321 1785664387
 parfullskip=none
+\change_unchanged
+
 \end_layout
 
 \end_inset
@@ -12709,6 +12816,8 @@ status collapsed
 
 \change_inserted -712698321 1785664507
 ParskipFull parskip=full
+\change_unchanged
+
 \end_layout
 
 \end_inset
@@ -12728,6 +12837,8 @@ status collapsed
 
 \change_inserted -712698321 1785664326
 ParfillskipQuarter
+\change_unchanged
+
 \end_layout
 
 \end_inset
@@ -12740,6 +12851,8 @@ status collapsed
 
 \change_inserted -712698321 1785664297
 string
+\change_unchanged
+
 \end_layout
 
 \end_inset
@@ -12752,6 +12865,8 @@ status collapsed
 
 \change_inserted -712698321 1785664574
 ParfillskipNone
+\change_unchanged
+
 \end_layout
 
 \end_inset
@@ -12784,6 +12899,8 @@ status collapsed
 
 \change_inserted -712698321 1785664337
 ParfillskipThird
+\change_unchanged
+
 \end_layout
 
 \end_inset
@@ -12796,6 +12913,8 @@ status collapsed
 
 \change_inserted -712698321 1785664331
 string
+\change_unchanged
+
 \end_layout
 
 \end_inset
@@ -12808,6 +12927,8 @@ status collapsed
 
 \change_inserted -712698321 1785664602
 ParfillskipNone
+\change_unchanged
+
 \end_layout
 
 \end_inset
diff --git a/lib/doc/UserGuide.lyx b/lib/doc/UserGuide.lyx
index 227bc04b78..3f96a26068 100644
--- a/lib/doc/UserGuide.lyx
+++ b/lib/doc/UserGuide.lyx
@@ -33664,6 +33664,78 @@ Restore value of counter:
 The dialog allows you to choose which counter you wish to modify from all those that are available in the current document class.
 \end_layout
 
+\begin_layout Section
+
+\change_inserted -712698321 1785747694
+Structural Units
+\end_layout
+
+\begin_layout Standard
+
+\change_inserted -712698321 1785747937
+Some document classes,
+ particularly book classes,
+ support the division of documents in structural units,
+ named 
+\begin_inset Quotes eld
+\end_inset
+
+front matter
+\begin_inset Quotes erd
+\end_inset
+
+ (for the preliminary parts of the book such as titling,
+ table of contents,
+ preface),
+ 
+\begin_inset Quotes eld
+\end_inset
+
+main matter
+\begin_inset Quotes erd
+\end_inset
+
+ (for the main part),
+ and 
+\begin_inset Quotes eld
+\end_inset
+
+back matter
+\begin_inset Quotes erd
+\end_inset
+
+ (for appendices and the like).
+ Usually,
+ front matters are differently paginated (e.g.,
+ using roman numbering),
+ whereas main matters start with page 1.
+ Back matters often do not use section numbering.
+ The details how the three units are supposed to look are defined in the class.
+\end_layout
+
+\begin_layout Standard
+
+\change_inserted -712698321 1785748007
+In \SpecialChar LyX
+,
+ you can start these units via 
+\family sans
+Document\SpecialChar menuseparator
+Start
+\begin_inset space ~
+\end_inset
+
+Structural Unit
+\begin_inset space ~
+\end_inset
+
+Here
+\family default
+ with the classes that support these divisions.
+\change_unchanged
+
+\end_layout
+
 \begin_layout Section
 Appendices
 \begin_inset Index idx
diff --git a/lib/doc/de/Customization.lyx b/lib/doc/de/Customization.lyx
index 5efed2f20e..4ec0000336 100644
--- a/lib/doc/de/Customization.lyx
+++ b/lib/doc/de/Customization.lyx
@@ -9966,6 +9966,82 @@ nolink "false"
 \begin_inset Flex Code
 status collapsed
 
+\begin_layout Plain Layout
+ProvidesMatters
+\end_layout
+
+\end_inset
+
+ [
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\emph on
+0
+\end_layout
+
+\end_inset
+
+
+\begin_inset space \thinspace{}
+\end_inset
+
+
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+1
+\end_layout
+
+\end_inset
+
+] zeigt an ob die Klasse die Strukturierung des Dokuments mittels 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+frontmatter
+\end_layout
+
+\end_inset
+
+,
+ 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+mainmatter
+\end_layout
+
+\end_inset
+
+ und 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+backmatter
+\end_layout
+
+\end_inset
+
+ unterstützt.
+\end_layout
+
+\begin_layout Description
+\begin_inset Flex Code
+status collapsed
+
 \begin_layout Plain Layout
 ProvidesModule
 \end_layout
diff --git a/lib/doc/de/UserGuide.lyx b/lib/doc/de/UserGuide.lyx
index e5ab356c92..60e0a9ee0a 100644
--- a/lib/doc/de/UserGuide.lyx
+++ b/lib/doc/de/UserGuide.lyx
@@ -32850,6 +32850,82 @@ In diesem Dialog können Sie aus allen Zählern,
  den Sie ändern möchten.
 \end_layout
 
+\begin_layout Section
+Strukturelle Einheiten
+\end_layout
+
+\begin_layout Standard
+Manche Dokumentklassen,
+ besonders die für Bücher,
+ unterstützen die Einteilung von Dokumenten in strukturelle Einheiten,
+ nämlich Vorspann (engl.
+ 
+\begin_inset Quotes gld
+\end_inset
+
+
+\lang english
+front matter
+\lang ngerman
+
+\begin_inset Quotes grd
+\end_inset
+
+,
+ für die Titelei,
+ das Inhaltsverzeichnis,
+ Vorworte usw.),
+ Hauptteil (engl.
+ 
+\begin_inset Quotes gld
+\end_inset
+
+
+\lang english
+main matter
+\lang ngerman
+
+\begin_inset Quotes grd
+\end_inset
+
+) und Nachspann (engl.
+ 
+\begin_inset Quotes gld
+\end_inset
+
+
+\lang english
+back matter
+\lang ngerman
+
+\begin_inset Quotes grd
+\end_inset
+
+,
+ für Anhänge und Ähnliches).
+ Meistens ist der Vorspann anders paginiert (bspw.
+ mit römischen Seitenzahlen),
+ wobei der Hauptteil dann mit Seite
+\begin_inset space ~
+\end_inset
+
+1 beginnt.
+ Der Nachspann verwendet oft keine Abschnittsnummerierung.
+ Wie es genau ausschaut definiert aber die jeweilige Klasse.
+\end_layout
+
+\begin_layout Standard
+In \SpecialChar LyX
+ können Sie diese Einheiten über 
+\family sans
+Dokument\SpecialChar menuseparator
+Strukturelle Einheit hier beginnen 
+\family default
+im Fall der Klassen,
+ die solche Unterteilungen unterstützen,
+ markieren.
+\end_layout
+
 \begin_layout Section
 Anhänge
 \begin_inset CommandInset label
diff --git a/lib/layouts/amsbook.layout b/lib/layouts/amsbook.layout
index 5546c9dc3b..c82c372528 100644
--- a/lib/layouts/amsbook.layout
+++ b/lib/layouts/amsbook.layout
@@ -17,6 +17,7 @@ Format 119
 Columns                 1
 Sides                   2
 PageStyle               Headers
+ProvidesMatters         1
 DocBookRoot             book
 
 Provides amsmath         1
diff --git a/lib/layouts/book.layout b/lib/layouts/book.layout
index 3e56a6cdc2..e13894d6be 100644
--- a/lib/layouts/book.layout
+++ b/lib/layouts/book.layout
@@ -11,10 +11,10 @@ Format 119
 Input stdclass.inc
 Input numreport.inc
 
-
 Sides                 2
 PageStyle             Headings
-DocBookRoot		      book
+ProvidesMatters       1
+DocBookRoot	      book
 DocBookForceAbstract  true
 
 
diff --git a/lib/layouts/memoir.layout b/lib/layouts/memoir.layout
index 8d52a03e59..9f3502cbe0 100644
--- a/lib/layouts/memoir.layout
+++ b/lib/layouts/memoir.layout
@@ -12,6 +12,7 @@ Columns            1
 Sides              2
 SecNumDepth        1
 TocDepth           1
+ProvidesMatters    1
 DefaultStyle       Standard
 PageStyle          Headings
 PageSize           letter
diff --git a/lib/layouts/recipebook.layout b/lib/layouts/recipebook.layout
index 08ced25e8e..8295994c51 100644
--- a/lib/layouts/recipebook.layout
+++ b/lib/layouts/recipebook.layout
@@ -35,6 +35,7 @@ Input numreport.inc
 SecNumDepth		2
 TocDepth		3
 PageStyle		Headings
+ProvidesMatters         1
 DocBookRoot		book
 
 # There is no abstract environment in scrbook.cls
diff --git a/lib/layouts/scrbook.layout b/lib/layouts/scrbook.layout
index 4e5860c409..6ba036fa70 100644
--- a/lib/layouts/scrbook.layout
+++ b/lib/layouts/scrbook.layout
@@ -10,6 +10,7 @@ Input numreport.inc
 
 Sides			2
 PageStyle		Headings
+ProvidesMatters         1
 DocBookRoot		book
 
 NoStyle Abstract
diff --git a/lib/layouts/tufte-book.layout b/lib/layouts/tufte-book.layout
index fb683e4ff4..c91192f04c 100644
--- a/lib/layouts/tufte-book.layout
+++ b/lib/layouts/tufte-book.layout
@@ -12,6 +12,7 @@ Provides natbib-internal 1
 
 Sides		2
 PageStyle	Headings
+ProvidesMatters 1
 DocBookRoot	book
 
 ClassOptions
diff --git a/lib/lyx2lyx/lyx_2_6.py b/lib/lyx2lyx/lyx_2_6.py
index 5eed5cbe05..c123d1fb64 100644
--- a/lib/lyx2lyx/lyx_2_6.py
+++ b/lib/lyx2lyx/lyx_2_6.py
@@ -1600,6 +1600,35 @@ def revert_parfillskip(document):
     if preamble != "":
         add_to_preamble(document, preamble)
 
+
+def revert_matters(document):
+    """Revert \\frontmatter, \\mainmatter and \\backmatter to ERT"""
+
+    for matter in ["front", "main", "back"]:
+        i = 0
+        while True:
+            i = find_token(document.body, "\\start_of_matter " + matter, i)
+            if i == -1:
+                break
+            
+            # Add ERT
+            lay = get_containing_layout(document.body, i)
+            if lay == False:
+                document.warning("Matter has no layout!")
+                i += 1
+                continue
+            beglay = lay[1]
+            endlay = find_end_of_layout(document.body, beglay)
+            if endlay == False:
+                document.warning("Matter has no endlayout!")
+                i += 1
+                continue
+            
+            layb = document.body[beglay]
+            laye = document.body[endlay]
+            document.body[i : i + 1] = put_cmd_in_ert(["\\" + matter + "matter"]) + ["", laye, "", layb]
+            i += 1
+
 ##
 # Conversion hub
 #
@@ -1618,11 +1647,13 @@ convert = [
     [653, []],
     [654, []],
     [655, []],
-    [656, []]
+    [656, []],
+    [657, []]
 ]
 
 
 revert = [
+    [656, [revert_matters]],
     [655, [revert_parfillskip]],
     [654, [revert_hex32colors]],
     [653, [revert_mintedlangs]],
diff --git a/lib/lyx2lyx/parser_tools.py b/lib/lyx2lyx/parser_tools.py
index 839b922278..3e3de99125 100644
--- a/lib/lyx2lyx/parser_tools.py
+++ b/lib/lyx2lyx/parser_tools.py
@@ -684,6 +684,7 @@ def get_containing_layout(lines, i):
         "\\indent-toggle",
         "\\leftindent",
         "\\start_of_appendix",
+        "\\start_of_matter",
         "\\paragraph_spacing",
         "\\align",
         "\\labelwidthstring",
diff --git a/lib/scripts/layout2layout.py b/lib/scripts/layout2layout.py
index f506b5deb5..cbaa08904b 100644
--- a/lib/scripts/layout2layout.py
+++ b/lib/scripts/layout2layout.py
@@ -9,7 +9,7 @@
 # This script will update a .layout file to current format
 
 # The latest layout format is also defined in src/TextClass.cpp
-currentFormat = 119
+currentFormat = 120
 
 
 # Incremented to format 4, 6 April 2007, lasgouttes
@@ -404,6 +404,9 @@ currentFormat = 119
 # New class tags ParfillskipNone, ParfillskipQuarter
 # and ParfillskipThird
 
+# Incremented to format 120, 3 August 2026 by spitz
+# New class tag ProvidesMatters
+
 # Do not forget to document format change in Customization
 # Manual (section "Declaring a new text class").
 
@@ -743,7 +746,7 @@ def convert(lines, end_format):
                 i += 1
             continue
 
-        if 101 <= format <= 119:
+        if 101 <= format <= 120:
             # nothing to do.
             i += 1
             continue
diff --git a/lib/ui/stdmenus.inc b/lib/ui/stdmenus.inc
index cecf4616c3..edc736a62a 100644
--- a/lib/ui/stdmenus.inc
+++ b/lib/ui/stdmenus.inc
@@ -569,6 +569,7 @@ Menuset
 		Item "LaTeX Log|L" "dialog-show latexlog"
 		Item "Processing Errors|r" "errors-show"
 		Item "Start Appendix Here|x" "appendix"
+		OptSubmenu "Start Structural Unit Here" "matter"
 		Separator
 		ViewFormats
 		UpdateFormats
@@ -592,6 +593,12 @@ Menuset
 		Item "Show Changes in Output|S" "changes-output"
 	End
 
+	Menu "matter"
+		OptItem "Front Matter" "matter front"
+		OptItem "Main Matter" "matter main"
+		OptItem "Back Matter" "matter back"
+	End
+
 #
 # NAVIGATE MENU
 #
diff --git a/src/Buffer.cpp b/src/Buffer.cpp
index 85124d0f1f..e0dbb0659b 100644
--- a/src/Buffer.cpp
+++ b/src/Buffer.cpp
@@ -5405,6 +5405,11 @@ void Buffer::Impl::setLabel(ParIterator & it, UpdateType utype) const
 	}
 	par.params().appendix(counters.appendix());
 
+	if (par.params().startOfMatter() != MATTER_NONE) {
+		counters.mainMatter(par.params().startOfMatter() == MATTER_MAIN);
+	}
+	par.params().mainMatter(counters.mainMatter());
+
 	// Compute the item depth of the paragraph
 	par.itemdepth = getItemDepth(it);
 
@@ -5494,8 +5499,10 @@ void Buffer::Impl::setLabel(ParIterator & it, UpdateType utype) const
 		docstring const & lcounter = layout.counter;
 		if (!lcounter.empty()) {
 			if (layout.toclevel <= bp.secnumdepth
-						&& (layout.latextype != LATEX_ENVIRONMENT
-					|| it.text()->isFirstInSequence(it.pit()))) {
+					&& (layout.latextype != LATEX_ENVIRONMENT
+						|| it.text()->isFirstInSequence(it.pit()))
+					// No numbering for chapters outside of main matter.
+					&& (layout.toclevel != 0 || par.params().mainMatter())) {
 				if (counters.hasCounter(lcounter))
 					counters.step(lcounter, utype);
 				par.params().labelString(par.expandLabel(layout, bp));
diff --git a/src/Counters.cpp b/src/Counters.cpp
index a93274558a..12e6867187 100644
--- a/src/Counters.cpp
+++ b/src/Counters.cpp
@@ -250,7 +250,8 @@ Counter::StringMap & Counter::flatLabelStrings(bool in_appendix) const
 }
 
 
-Counters::Counters() : appendix_(false), subfloat_(false), longtable_(false)
+Counters::Counters() : appendix_(false), main_matter_(true), subfloat_(false),
+	longtable_(false)
 {
 	layout_stack_.push_back(nullptr);
 	counter_stack_.push_back(from_ascii(""));
@@ -444,6 +445,7 @@ docstring const & Counters::latexName(docstring const & cntr) const
 void Counters::reset()
 {
 	appendix_ = false;
+	main_matter_ = true;
 	subfloat_ = false;
 	current_float_.erase();
 	for (auto & ctr : counterList_)
diff --git a/src/Counters.h b/src/Counters.h
index d522a7b757..e1265b8ccb 100644
--- a/src/Counters.h
+++ b/src/Counters.h
@@ -16,6 +16,7 @@
 #define COUNTERS_H
 
 #include "OutputEnums.h"
+#include "LayoutEnums.h"
 
 #include "support/docstring.h"
 
@@ -149,6 +150,8 @@ public:
 	/// \return true on success
 	bool read(support::Lexer & lex, docstring const & name, bool makenew);
 	///
+	void setEnabled(docstring const & ctr, bool const enabled);
+	///
 	void set(docstring const & ctr, int val);
 	///
 	void addto(docstring const & ctr, int val);
@@ -210,6 +213,10 @@ public:
 	bool appendix() const { return appendix_; }
 	/// Set the state variable indicating whether we are in appendix.
 	void appendix(bool a) { appendix_ = a; }
+	/// Are we in main matter?
+	bool mainMatter() const { return main_matter_; }
+	/// Set the state variable indicating whether which matter we are in.
+	void mainMatter(bool m) { main_matter_ = m; }
 	/// Returns the current enclosing float.
 	std::string const & current_float() const { return current_float_; }
 	/// Sets the current enclosing float.
@@ -275,6 +282,8 @@ private:
 	CounterList counterList_;
 	/// Are we in an appendix?
 	bool appendix_;
+	/// Are we in the main matter?
+	bool main_matter_;
 	/// The current enclosing float.
 	std::string current_float_;
 	/// Are we in a subfloat?
diff --git a/src/FuncCode.h b/src/FuncCode.h
index 4f78596b9c..d8ad69e67b 100644
--- a/src/FuncCode.h
+++ b/src/FuncCode.h
@@ -516,6 +516,7 @@ enum FuncCode
 	LFUN_ERROR_NEXT,                // spitz 20200101,
 	/// 405
 	LFUN_BUFFER_UPDATE_EXTERNAL,    // spitz 20250406,
+	LFUN_MATTER,                    // spitz/daniel, 20260805
 	LFUN_LASTACTION                 // end of the table
 };
 
diff --git a/src/LayoutEnums.h b/src/LayoutEnums.h
index 48e849f1a4..3215311806 100644
--- a/src/LayoutEnums.h
+++ b/src/LayoutEnums.h
@@ -158,6 +158,18 @@ enum ArgPassThru {
 	PT_TRUE
 };
 
+///
+enum Matter {
+	///
+	MATTER_NONE,
+	///
+	MATTER_FRONT,
+	///
+	MATTER_MAIN,
+	///
+	MATTER_BACK
+};
+
 } // namespace lyx
 
 #endif
diff --git a/src/LyXAction.cpp b/src/LyXAction.cpp
index bd9c9df054..f7e174e1bf 100644
--- a/src/LyXAction.cpp
+++ b/src/LyXAction.cpp
@@ -298,6 +298,16 @@ void LyXAction::init()
  */
 		{ LFUN_APPENDIX, "appendix", Noop, Edit },
 
+/*!
+ * \var lyx::FuncCode lyx::LFUN_MATTER
+ * \li Action: Start (or unstart) matter on the given cursor position.
+ * \li Syntax: matter <type>
+ * \li Params: <type>: front, main, back.
+ * \li Origin: daniel/spitz, 3 Aug 2026
+ * \endvar
+ */
+		{ LFUN_MATTER, "matter", Noop, Edit },
+
 
 /*!
  * \var lyx::FuncCode lyx::LFUN_ARGUMENT_INSERT
diff --git a/src/ParagraphParameters.cpp b/src/ParagraphParameters.cpp
index 32dc44900b..ec50faafdf 100644
--- a/src/ParagraphParameters.cpp
+++ b/src/ParagraphParameters.cpp
@@ -41,6 +41,7 @@ static char const * const string_align[] = {
 ParagraphParameters::ParagraphParameters()
 	: noindent_(false),
 		start_of_appendix_(false), appendix_(false),
+		start_of_matter_(MATTER_NONE), main_matter_(true),
 	  align_(LYX_ALIGN_LAYOUT), depth_(0)
 {}
 
@@ -132,6 +133,30 @@ void ParagraphParameters::appendix(bool a)
 }
 
 
+Matter ParagraphParameters::startOfMatter() const
+{
+	return start_of_matter_;
+}
+
+
+void ParagraphParameters::startOfMatter(Matter som)
+{
+	start_of_matter_ = som;
+}
+
+
+bool ParagraphParameters::mainMatter() const
+{
+	return main_matter_;
+}
+
+
+void ParagraphParameters::mainMatter(bool m)
+{
+	main_matter_ = m;
+}
+
+
 docstring const & ParagraphParameters::labelString() const
 {
 	return labelstring_;
@@ -207,6 +232,15 @@ void ParagraphParameters::read(Lexer & lex, bool merge)
 			leftIndent(value);
 		} else if (token == "\\start_of_appendix") {
 			startOfAppendix(true);
+		} else if (token == "\\start_of_matter") {
+			lex.next();
+			string const tmp = rtrim(lex.getString());
+			if (tmp == "front")
+				startOfMatter(MATTER_FRONT);
+			else if (tmp == "main")
+				startOfMatter(MATTER_MAIN);
+			else if (tmp == "back")
+				startOfMatter(MATTER_BACK);
 		} else if (token == "\\paragraph_spacing") {
 			lex.next();
 			string const tmp = rtrim(lex.getString());
@@ -269,6 +303,18 @@ void ParagraphParameters::write(ostream & os) const
 	if (startOfAppendix())
 		os << "\\start_of_appendix\n";
 
+	// Start of front matter?
+	if (startOfMatter() != MATTER_NONE) {
+		string tmp;
+		if (startOfMatter() == MATTER_FRONT)
+			tmp = "front";
+		else if (startOfMatter() == MATTER_MAIN)
+			tmp = "main";
+		else if (startOfMatter() == MATTER_BACK)
+			tmp = "back";
+		os << "\\start_of_matter " << tmp << "\n";
+	}
+
 	// Noindent?
 	if (noindent())
 		os << "\\noindent\n";
diff --git a/src/ParagraphParameters.h b/src/ParagraphParameters.h
index 540ea95679..9ff42b0cf2 100644
--- a/src/ParagraphParameters.h
+++ b/src/ParagraphParameters.h
@@ -63,6 +63,14 @@ public:
 	///
 	void appendix(bool);
 	///
+	Matter startOfMatter() const;
+	///
+	void startOfMatter(Matter);
+	///
+	bool mainMatter() const;
+	///
+	void mainMatter(bool);
+	///
 	docstring const & labelString() const;
 	///
 	void labelString(docstring const &);
@@ -102,6 +110,10 @@ private:
 	///
 	bool appendix_;
 	///
+	Matter start_of_matter_;
+	///
+	bool main_matter_;
+	///
 	LyXAlignment align_;
 	///
 	depth_type depth_;
diff --git a/src/RowPainter.cpp b/src/RowPainter.cpp
index 6d2dd5ec65..8a6a5a6800 100644
--- a/src/RowPainter.cpp
+++ b/src/RowPainter.cpp
@@ -30,6 +30,7 @@
 #include "ParagraphList.h"
 #include "ParagraphParameters.h"
 #include "Text.h"
+#include "TextClass.h"
 #include "TextMetrics.h"
 
 #include "frontends/FontMetrics.h"
@@ -324,12 +325,14 @@ void RowPainter::paintChangeBar() const
 void RowPainter::paintAppendix() const
 {
 	// only draw the appendix frame once (for the main text)
-	if (!par_.params().appendix() || !text_.isMainText())
+	if ((!par_.params().appendix() && par_.params().mainMatter())
+	    || !text_.isMainText())
 		return;
 
 	int y = yo_ - row_.ascent();
 
-	if (par_.params().startOfAppendix())
+	if (par_.params().startOfAppendix()
+	    || par_.params().startOfMatter() != MATTER_NONE)
 		y += 2 * defaultRowHeight();
 
 	pi_.pain.line(1, y, 1, yo_ + row_.descent(), Color_appendix);
@@ -381,7 +384,7 @@ void RowPainter::paintDepthBar() const
 }
 
 
-void RowPainter::paintAppendixStart(int y) const
+void RowPainter::paintMatterStart(int y, docstring const & label) const
 {
 	FontInfo pb_font = sane_font;
 	pb_font.setColor(Color_appendix);
@@ -391,7 +394,6 @@ void RowPainter::paintAppendixStart(int y) const
 	int a = 0;
 	int d = 0;
 
-	docstring const label = _("Appendix");
 	theFontMetrics(pb_font).rectText(label, w, a, d);
 
 	int const text_start = int(xo_ + (tm_.width() - w) / 2);
@@ -423,21 +425,46 @@ void RowPainter::paintTooLargeMarks(bool const left, bool const right) const
 
 void RowPainter::paintFirst() const
 {
+	Layout const & TOCLayout = pi_.base.bv->buffer().params().documentClass().getTOCLayout();
 	Layout const & layout = par_.layout();
 
+	docstring label;
 	// start of appendix?
 	if (par_.params().startOfAppendix())
-	    paintAppendixStart(yo_ - row_.ascent() + 2 * defaultRowHeight());
+		label = _("Appendix");
+	// start of matter?
+	if (par_.params().startOfMatter() != MATTER_NONE) {
+		if (!label.empty())
+			label += " & ";
+		switch (par_.params().startOfMatter()) {
+			case MATTER_FRONT:
+				label += _("Front Matter");
+				break;
+			case MATTER_MAIN:
+				label += _("Main Matter");
+				break;
+			case MATTER_BACK:
+				label += _("Back Matter");
+				break;
+		}
+		// Matters are supported only by book (sectioning toplevel 0)
+		if (TOCLayout.toclevel != 0)
+			label += _(" (Senseless!!!)");
+	}
+	if (!label.empty())
+		paintMatterStart(yo_ - row_.ascent() + 2 * defaultRowHeight(), label);
 
 	bool const is_first =
 		text_.isFirstInSequence(row_.pit()) || !layout.isParagraphGroup();
 	//lyxerr << "paintFirst: " << par_.id() << " is_seq: " << is_seq << endl;
 
-	if (layout.labelIsInline()
-	    && (layout.labeltype != LABEL_STATIC || is_first))
-		paintLabel();
-	else if (is_first && layout.labelIsAbove())
-		paintTopLevelLabel();
+	if (par_.layout().toclevel != 0 || par_.params().mainMatter()) {
+		if (layout.labelIsInline()
+			&& (layout.labeltype != LABEL_STATIC || is_first))
+			paintLabel();
+		else if (is_first && layout.labelIsAbove())
+			paintTopLevelLabel();
+	}
 }
 
 
diff --git a/src/RowPainter.h b/src/RowPainter.h
index 918fc9f989..ce77554ac5 100644
--- a/src/RowPainter.h
+++ b/src/RowPainter.h
@@ -54,7 +54,7 @@ private:
 	void paintTextDecoration(Row::Element const & e) const;
 	void paintMisspelledMark(Row::Element const & e) const;
 	void paintChange(Row::Element const & e) const;
-	void paintAppendixStart(int y) const;
+	void paintMatterStart(int y, docstring const & label) const;
 	void paintInset(Row::Element const & e) const;
 
 	/// return the label font for this row (end label when \c end is true)
diff --git a/src/Text.cpp b/src/Text.cpp
index 22e1c2c87b..0a8be834fe 100644
--- a/src/Text.cpp
+++ b/src/Text.cpp
@@ -798,9 +798,11 @@ static void breakParagraph(Text & text, pit_type par_offset, pos_type pos,
 
 	if (!isempty) {
 		bool const soa = par.params().startOfAppendix();
+		Matter const som = par.params().startOfMatter();
 		par.params().clear();
 		// do not lose start of appendix marker (bug 4212)
 		par.params().startOfAppendix(soa);
+		par.params().startOfMatter(som);
 		par.setPlainOrDefaultLayout(bparams.documentClass());
 	}
 
@@ -3294,10 +3296,13 @@ bool Text::deleteEmptyParagraphMechanism(Cursor & cur,
 	               min(old.pit() + 1, old.lastpit()));
 	ParagraphList & plist = old.text()->paragraphs();
 	bool const soa = oldpar.params().startOfAppendix();
+	Matter const som = oldpar.params().startOfMatter();
 	plist.erase(plist.iterator_at(old.pit()));
 	// do not lose start of appendix marker (bug 4212)
 	if (soa && old.pit() < pit_type(plist.size()))
 		plist[old.pit()].params().startOfAppendix(true);
+	if (som != MATTER_NONE && old.pit() < pit_type(plist.size()))
+		plist[old.pit()].params().startOfMatter(som);
 
 	// see #warning (FIXME?) above
 	if (cur.depth() >= old.depth()) {
@@ -4135,6 +4140,27 @@ void Text::dispatch(Cursor & cur, FuncRequest & cmd)
 		break;
 	}
 
+	case LFUN_MATTER: {
+		Paragraph & par = cur.paragraph();
+		docstring const & arg = cmd.argument();
+		Matter start = MATTER_NONE;
+		if (arg == "front") {
+			start = par.params().startOfMatter() == MATTER_FRONT
+						   ? MATTER_NONE : MATTER_FRONT;
+		} else if (arg == "main") {
+			start = par.params().startOfMatter() == MATTER_MAIN
+						   ? MATTER_NONE : MATTER_MAIN;
+		} else if (arg == "back") {
+			start = par.params().startOfMatter() == MATTER_BACK
+						   ? MATTER_NONE : MATTER_BACK;
+		}
+		cur.recordUndo();
+		par.params().startOfMatter(start);
+		// we can set the refreshing parameters now
+		cur.forceBufferUpdate();
+		break;
+	}
+
 	case LFUN_WORD_DELETE_FORWARD:
 		if (cur.selection())
 			cutSelection(cur, false);
@@ -6396,6 +6422,24 @@ bool Text::getStatus(Cursor & cur, FuncRequest const & cmd,
 		status.setOnOff(cur.paragraph().params().startOfAppendix());
 		break;
 
+	case LFUN_MATTER:
+		if (!cur.buffer()->params().documentClass().hasMatters()) {
+			enable = false;
+			break;
+		}
+		// FIXME We really should not allow this to be put, e.g.,
+		// in a footnote, or in ERT. But it would make sense in a
+		// branch, so I'm not sure what to do.
+		if (cmd.argument() == "front")
+			status.setOnOff(cur.paragraph().params().startOfMatter() == MATTER_FRONT);
+		else if (cmd.argument() == "main")
+			status.setOnOff(cur.paragraph().params().startOfMatter() == MATTER_MAIN);
+		else if (cmd.argument() == "back")
+			status.setOnOff(cur.paragraph().params().startOfMatter() == MATTER_BACK);
+		else
+			enable = false;
+		break;
+
 	case LFUN_DIALOG_SHOW_NEW_INSET:
 		if (cmd.argument() == "bibitem")
 			code = BIBITEM_CODE;
diff --git a/src/TextClass.cpp b/src/TextClass.cpp
index d908473aff..c0477533e3 100644
--- a/src/TextClass.cpp
+++ b/src/TextClass.cpp
@@ -60,7 +60,7 @@ namespace lyx {
 // You should also run the development/tools/updatelayouts.py script,
 // to update the format of all of our layout files.
 //
-int const LAYOUT_FORMAT = 119; // spitz: ParfillSkip
+int const LAYOUT_FORMAT = 120; // spitz: ProvidesMatters
 
 
 // Layout format for the current lyx file format. Controls which format is
@@ -142,7 +142,7 @@ TextClass::TextClass()
 	  columns_(1), sides_(OneSide), secnumdepth_(3), tocdepth_(3), outputType_(LATEX),
 	  outputFormat_("latex"), has_output_format_(false), defaultfont_(sane_font), 
 	  titletype_(TITLE_COMMAND_AFTER), titlename_("maketitle"),
-	  min_toclevel_(0), max_toclevel_(0), maxcitenames_(2),
+	  min_toclevel_(0), max_toclevel_(0), has_matters_(false), maxcitenames_(2),
 	  cite_full_author_list_(true), bibintoc_(false)
 {}
 
@@ -185,6 +185,7 @@ enum TextClassTags {
 	TC_DEFAULTFONT,
 	TC_SECNUMDEPTH,
 	TC_TOCDEPTH,
+	TC_PROVIDESMATTERS,
 	TC_CLASSOPTIONS,
 	TC_SPECIALCHARS,
 	TC_PREAMBLE,
@@ -278,6 +279,7 @@ LexerKeyword textClassTags[] = {
 	{ "preamble",          TC_PREAMBLE },
 	{ "provideinsetlayout", TC_PROVIDEINSETLAYOUT },
 	{ "provides",          TC_PROVIDES },
+	{ "providesmatters",   TC_PROVIDESMATTERS },
 	{ "providesmodule",    TC_PROVIDESMODULE },
 	{ "providestyle",      TC_PROVIDESTYLE },
 	{ "requires",          TC_REQUIRES },
@@ -661,6 +663,11 @@ TextClass::ReturnValues TextClass::read(Lexer & lexrc, ReadType rt)
 			tocdepth_ = lexrc.getInteger();
 			break;
 
+		case TC_PROVIDESMATTERS:
+			if (lexrc.next())
+				has_matters_ = lexrc.getBool();
+			break;
+
 		// First step to support options
 		case TC_CLASSOPTIONS:
 			readClassOptions(lexrc);
diff --git a/src/TextClass.h b/src/TextClass.h
index f7673b10d8..e8bf0d768e 100644
--- a/src/TextClass.h
+++ b/src/TextClass.h
@@ -209,6 +209,8 @@ public:
 	std::string const & outputFormat() const { return outputFormat_; }
 	/// Does this class redefine the output format?
 	bool hasOutputFormat() const { return has_output_format_; }
+	/// Does this class provide \forntmatter, \mainmatter, and \backmatter?
+	bool hasMatters() const { return has_matters_; }
 	/// Return the non-localised names for the toc types.
 	std::map<std::string, docstring> const &
 		outlinerNames() const { return outliner_names_; }
@@ -368,6 +370,8 @@ protected:
 	int min_toclevel_;
 	/// The maximal TocLevel of sectioning layouts
 	int max_toclevel_;
+	/// Does this class provide \frontmatter, \mainmatter, and \backmatter?
+	bool has_matters_;
 	/// Citation formatting information
 	std::map<CiteEngineType, std::map<std::string, std::string> > cite_formats_;
 	/// Citation macros
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index fe4f775809..06e3fce2db 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1309,14 +1309,15 @@ int TextMetrics::parTopSpacing(pit_type const pit) const
 		        && pars[pit - 1].getDepth() == 0))) {
 		asc += bparams.getDefSkip().inPixels(*bv_);
 	}
-
-	if (par.params().startOfAppendix())
+	// extra row height for start of appendix and matters
+	if (par.params().startOfAppendix() || par.params().startOfMatter() != MATTER_NONE)
 		asc += int(3 * dh);
 
 	// special code for the top label
 	if (layout.labelIsAbove()
 	    && (!layout.isParagraphGroup() || text_->isFirstInSequence(pit))
-	    && !par.labelString().empty()) {
+	    && !par.labelString().empty()
+		&& (layout.toclevel != 0 || par.params().mainMatter())) {
 		FontInfo labelfont = text_->labelFont(par);
 		FontMetrics const & lfm = theFontMetrics(labelfont);
 		asc += int(lfm.maxHeight() * layout.spacing.getValue()
diff --git a/src/output_latex.cpp b/src/output_latex.cpp
index 94ed36b3f3..8aef8c75d0 100644
--- a/src/output_latex.cpp
+++ b/src/output_latex.cpp
@@ -1237,6 +1237,21 @@ void TeXOnePar(Buffer const & buf,
 		if (par.params().startOfAppendix()) {
 			os << "\n\\appendix\n";
 		}
+		if (bparams.documentClass().hasMatters()) {
+			switch (par.params().startOfMatter()) {
+				case MATTER_FRONT:
+					os << "\n\\frontmatter\n";
+					break;
+				case MATTER_MAIN:
+					os << "\n\\mainmatter\n";
+					break;
+				case MATTER_BACK:
+					os << "\n\\backmatter\n";
+					break;
+				default:
+					break;
+			}
+		}
 
 		// InTitle commands must use switches (not environments)
 		// inside the commands (see #9332)
diff --git a/src/tex2lyx/text.cpp b/src/tex2lyx/text.cpp
index a8ebbf9364..31b509ffcc 100644
--- a/src/tex2lyx/text.cpp
+++ b/src/tex2lyx/text.cpp
@@ -3945,6 +3945,78 @@ void parse_text(Parser & p, ostream & os, unsigned flags, bool outer,
 			continue;
 		}
 
+		if (t.cs() == "frontmatter") {
+			context.add_par_extra_stuff("\\start_of_matter front\n");
+			// We need to start a new paragraph. Otherwise the
+			// frontmatter in 'bla\frontmatter\chapter{' would start
+			// too late.
+			context.new_paragraph(os);
+			// We need to make sure that the paragraph is
+			// generated even if it is empty. Otherwise the
+			// frontmatter in '\par\frontmatter\par\chapter{' would
+			// start too late.
+			context.check_layout(os);
+			// FIXME: This is a hack to prevent paragraph
+			// deletion if it is empty. Handle this better!
+			output_comment(p, os,
+				"dummy comment inserted by tex2lyx to "
+				"ensure that this paragraph is not empty",
+				context);
+			// Both measures above may generate an additional
+			// empty paragraph, but that does not hurt, because
+			// whitespace does not matter here.
+			eat_whitespace(p, os, context, true);
+			continue;
+		}
+
+		if (t.cs() == "mainmatter") {
+			context.add_par_extra_stuff("\\start_of_matter main\n");
+			// We need to start a new paragraph. Otherwise the
+			// mainmatter in 'bla\mainmatter\chapter{' would start
+			// too late.
+			context.new_paragraph(os);
+			// We need to make sure that the paragraph is
+			// generated even if it is empty. Otherwise the
+			// mainmatter in '\par\mainmatter\par\chapter{' would
+			// start too late.
+			context.check_layout(os);
+			// FIXME: This is a hack to prevent paragraph
+			// deletion if it is empty. Handle this better!
+			output_comment(p, os,
+				"dummy comment inserted by tex2lyx to "
+				"ensure that this paragraph is not empty",
+				context);
+			// Both measures above may generate an additional
+			// empty paragraph, but that does not hurt, because
+			// whitespace does not matter here.
+			eat_whitespace(p, os, context, true);
+			continue;
+		}
+
+		if (t.cs() == "backmatter") {
+			context.add_par_extra_stuff("\\start_of_matter back\n");
+			// We need to start a new paragraph. Otherwise the
+			// mainmatter in 'bla\mainmatter\chapter{' would start
+			// too late.
+			context.new_paragraph(os);
+			// We need to make sure that the paragraph is
+			// generated even if it is empty. Otherwise the
+			// mainmatter in '\par\mainmatter\par\chapter{' would
+			// start too late.
+			context.check_layout(os);
+			// FIXME: This is a hack to prevent paragraph
+			// deletion if it is empty. Handle this better!
+			output_comment(p, os,
+				"dummy comment inserted by tex2lyx to "
+				"ensure that this paragraph is not empty",
+				context);
+			// Both measures above may generate an additional
+			// empty paragraph, but that does not hurt, because
+			// whitespace does not matter here.
+			eat_whitespace(p, os, context, true);
+			continue;
+		}
+
 		// Must catch empty dates before findLayout is called below
 		if (t.cs() == "date") {
 			eat_whitespace(p, os, context, false);
diff --git a/src/version.h b/src/version.h
index cd47b6268d..93eeff04a6 100644
--- a/src/version.h
+++ b/src/version.h
@@ -32,8 +32,8 @@ extern char const * const lyx_version_info;
 
 // Do not remove the comment below, so we get merge conflict in
 // independent branches. Instead add your own.
-#define LYX_FORMAT_LYX 656 // spitz: parfillskip
-#define LYX_FORMAT_TEX2LYX 656
+#define LYX_FORMAT_LYX 657 // spitz: matters
+#define LYX_FORMAT_TEX2LYX 657
 
 #if LYX_FORMAT_TEX2LYX != LYX_FORMAT_LYX
 #ifndef _MSC_VER


More information about the lyx-cvs mailing list