From 210cb289eda04528875c3ed1c5012bcd39597be0 Mon Sep 17 00:00:00 2001 From: Omikhleia Date: Tue, 14 Jan 2025 02:08:11 +0100 Subject: [PATCH] feat(inputters): Support front/main/back matter divisions in master documents --- examples/manual-masterdoc/bookdivisions.dj | 84 ++++++++ examples/manual-masterdoc/masterdoc.dj | 11 +- examples/sile-resilient-manual.silm | 1 + inputters/silm.lua | 237 ++++++++++++++------- 4 files changed, 250 insertions(+), 83 deletions(-) create mode 100644 examples/manual-masterdoc/bookdivisions.dj diff --git a/examples/manual-masterdoc/bookdivisions.dj b/examples/manual-masterdoc/bookdivisions.dj new file mode 100644 index 0000000..d894dff --- /dev/null +++ b/examples/manual-masterdoc/bookdivisions.dj @@ -0,0 +1,84 @@ +# Book divisions + +In §[](#masterdoc-content-basic), we explored how to declare a simple book consisting of chapters or parts. +For books with chapters only: + + +```yaml +content: + chapters: + ⟨...⟩ +``` + +Or for books with parts: + +```yaml +content: + parts: + ⟨...⟩ +``` + +Actually, this is equivalent to the following full syntax: + +```yaml +content: + mainmatter: + ⟨parts or chapters⟩ +``` + +That is, your content is assumed to be part of the main matter by default. +Some books, however, require a more complex structure, dividing content into front matter, main matter, and back matter. +This is particularly common in academic works, where the front matter might include a preface, dedication, and table of contents. + +## Front, main and back divisions + +Each division begins on an odd-numbered page. +Divisions influence both the format and numbering of pages: + + - The front matter uses roman numerals for page numbers by default. + - The main and back matter use arabic numerals for page numbers by default. + +These defaults can be overridden to suit specific requirements, with adequate styles. +Page numbering resets whenever the format changes between divisions (e.g., front matter from i--viii, main matter from 1--148). + +Divisions also influence the numbering of parts and chapters: + + - In the front matter and back matter, parts and chapters are unnumbered by definition. + - In the main matter, parts and chapters are numbered by default, though this behavior can be customized with styles. + +All default styles can be overridden to suit specific requirements, but we will cover this in a later section. +All divisions can include parts. +However, it is up to you to maintain consistency with your structural expectations. + +```yaml +content: + frontmatter: + ⟨parts or chapters⟩ + mainmatter: + ⟨parts or chapters⟩ + backmatter: + ⟨parts or chapters⟩ +``` + +Below is an example of how to declare a book that incorporates these divisions. + +```yaml +content: + frontmatter: + chapters: + - preface.dj + - dedication.dj + mainmatter: + parts: + - caption: Part 1 + content: + - chapt1-1.dj + - chapt1-2.dj + - caption: Part 2 + content: + - chapt2-1.dj + - chapt2-2.dj + backmatter: + chapters: + - postface.dj +``` diff --git a/examples/manual-masterdoc/masterdoc.dj b/examples/manual-masterdoc/masterdoc.dj index d488018..8115296 100644 --- a/examples/manual-masterdoc/masterdoc.dj +++ b/examples/manual-masterdoc/masterdoc.dj @@ -55,11 +55,11 @@ sile: textsubsuper.fake: false typesetter.italicCorrection: true packages: - - dropcaps - - couyards -chapters: - - chap1.dj - - chap2.dj + - resilient.poetry +content: + chapters: + - chap1.dj + - chap2.dj ``` The `masterfile` entry is a number corresponding to a version, for compatibility purposes.[^masterdoc-compat] @@ -104,6 +104,7 @@ The `sile` object provides intructions for processing the master document with S - The `packages` object lists extra packages that SILE must use. It can be used to load add-on packages needed by your content but not already provided by the document class and its supporting packages. +{#masterdoc-content-basic} ## Up to the content Last but not least, the `chapters` array (or `parts`, see further below) lists the files to include in the work. diff --git a/examples/sile-resilient-manual.silm b/examples/sile-resilient-manual.silm index 9d4af7c..86934ad 100644 --- a/examples/sile-resilient-manual.silm +++ b/examples/sile-resilient-manual.silm @@ -53,6 +53,7 @@ parts: content: - manual-masterdoc/masterdoc.dj - manual-masterdoc/bookmatters.dj + - manual-masterdoc/bookdivisions.dj - manual-masterdoc/bibliographies.dj - file: manual-parts/part-layouts.dj content: diff --git a/inputters/silm.lua b/inputters/silm.lua index b7bdbae..f2419d4 100644 --- a/inputters/silm.lua +++ b/inputters/silm.lua @@ -1,6 +1,6 @@ --- Some YAML master file inputter for SILE -- --- @copyright License: MIT (c) 2023 Omikhleia, Didier Willis +-- @copyright License: MIT (c) 2023, 2025 Omikhleia, Didier Willis -- @module inputters.silm -- local ast = require("silex.ast") @@ -134,99 +134,146 @@ local BibliographySchema = { } } -local MasterSchema = { +local MetaDataSchema = { type = "object", - additionalProperties = true, -- Allow unknown property for extensibility properties = { - masterfile = { - type = "number", + title = { type = "string" }, + subtitle = { type = "string" }, + subject = { type = "string" }, + keywords = { + type = { -- oneOf + { type = "string" }, + { type = "array", items = { type = "string" } }, + }, + }, + authors = { + type = { -- oneOf + { type = "string" }, + { type = "array", items = { type = "string" } }, + }, }, - metadata = { + translators = { -- oneOf + type = "string", + items = { type = "string" } + }, + publisher = { type = "string" }, + pubdate = { type = "object", + -- tinyyaml converts dates to osdate objects + -- We don't care about hours, minutes, etc. + additionalProperties = true, properties = { - title = { type = "string" }, - subtitle = { type = "string" }, - subject = { type = "string" }, - keywords = { - type = { -- oneOf - { type = "string" }, - { type = "array", items = { type = "string" } }, - }, - }, - authors = { - type = { -- oneOf - { type = "string" }, - { type = "array", items = { type = "string" } }, - }, - }, - translators = { -- oneOf - type = "string", - items = { type = "string" } - }, - publisher = { type = "string" }, - pubdate = { - type = "object", - -- tinyyaml converts dates to osdate objects - -- We don't care about hours, minutes, etc. - additionalProperties = true, - properties = { - year = { type = "number" }, - month = { type = "number" }, - day = { type = "number" } - } - }, - isbn = { type = "string" }, - url = { type = "string" }, - copyright = { type = "string" }, - legal = { type = "string" }, + year = { type = "number" }, + month = { type = "number" }, + day = { type = "number" } } }, - font = { + isbn = { type = "string" }, + url = { type = "string" }, + copyright = { type = "string" }, + legal = { type = "string" }, + } +} + +local FontSchema = { + type = "object", + properties = { + family = { + type = { -- oneOf + { type = "string" }, + { type = "array", items = { type = "string" } }, + }, + }, + size = { type = "string" } + } +} + +local SileConfigurationSchema = { + type = "object", + properties = { + options = { type = "object", properties = { - family = { - type = { -- oneOf - { type = "string" }, - { type = "array", items = { type = "string" } }, - }, - }, - size = { type = "string" } + class = { type = "string" }, + papersize = { type = "string" }, + layout = { type = "string" }, + resolution = { type = "number" }, + headers = { type = "string" }, + offset = { type = "string" }, } }, - language = { type = "string" }, - sile = { + settings = { type = "object", + -- Allow any setting without validation + additionalProperties = true, properties = { - options = { - type = "object", - properties = { - class = { type = "string" }, - papersize = { type = "string" }, - layout = { type = "string" }, - resolution = { type = "number" }, - headers = { type = "string" }, - offset = { type = "string" }, - } - }, - settings = { - type = "object", - -- Allow any setting without validation - additionalProperties = true, - properties = { - } - }, - packages = { - type = "array", - items = { type = "string" } - } } }, + packages = { + type = "array", + items = { type = "string" } + } + } +} + +local PartPropertiesSchema = { + type = "object", + properties = { + parts = ContentSchema, + } +} + +local ChapterPropertiesSchema = { + type = "object", + properties = { + chapters = ContentSchema, + } +} + +local PartOrChapterSchema = { + type = { -- oneOf + PartPropertiesSchema, + ChapterPropertiesSchema, + } +} + +local StructuredContentSchema = { + type = { -- oneOf + PartPropertiesSchema, + ChapterPropertiesSchema, + { type = "object", + properties = { + frontmatter = PartOrChapterSchema, + mainmatter = PartOrChapterSchema, + backmatter = PartOrChapterSchema, + } + }, + } +} + +local MasterSchema = { + type = "object", + additionalProperties = true, -- Allow unknown property for extensibility + properties = { + masterfile = { + type = "number", + }, + metadata = MetaDataSchema, + font = FontSchema, + language = { type = "string" }, + sile = SileConfigurationSchema, book = BookSchema, bibliography = BibliographySchema, - -- parts and chapters are exclusive, but we don't enforce it here. + -- Legacy compatibility: + -- We can have parts and chapters at the root level. + -- parts, chapters and content are mutually exclusive, but we don't enforce it here. -- We will check it later in the inputter parts = ContentSchema, - chapters = ContentSchema + chapters = ContentSchema, + -- New recommended way: + -- content can have parts, chapters or a structured content (frontmatter, mainmatter, backmatter + -- divisons) + content = StructuredContentSchema, } } @@ -394,6 +441,29 @@ local function doLevel (content, entries, shiftHeadings, metaopts) end end +local function doDivisionContent (content, entry, shiftHeadings, metaopts) + if entry.parts then + doLevel(content, entry.parts, shiftHeadings - 1, metaopts) + elseif entry.chapters then + doLevel(content, entry.chapters, shiftHeadings, metaopts) + end +end + +local function doDivision (content, entry, shiftHeadings, metaopts) + if entry.frontmatter then + content[#content+1] = createCommand("frontmatter") + doDivisionContent(content, entry.frontmatter, shiftHeadings, metaopts) + end + if entry.mainmatter then + content[#content+1] = createCommand("mainmatter") + doDivisionContent(content, entry.mainmatter, shiftHeadings, metaopts) + end + if entry.backmatter then + content[#content+1] = createCommand("backmatter") + doDivisionContent(content, entry.backmatter, shiftHeadings, metaopts) + end +end + --- Process back cover content (text included in the back cover) ---@param entry table|string Back cover content entry ---@param metaopts table Metadata options @@ -492,6 +562,9 @@ function inputter:parse (doc) }) end end + -- TODO QUESTION: if not a root document, should we wrap the includes + -- in a language group? Mixing documents with different languages + -- is probably not a good idea, anyhow... local sile = master.sile or {} local metadata = master.metadata or {} @@ -591,13 +664,21 @@ function inputter:parse (doc) end -- Document content - -- TODO QUESTION: if not a root document, should we wrap the includes - -- in a language group? Mixing documents with different languages - -- is probably not a good idea, anyhow... + if master.parts then doLevel(content, master.parts, baseShiftHeadings - 1, metadataOptions) elseif master.chapters then doLevel(content, master.chapters, baseShiftHeadings, metadataOptions) + elseif master.content then + if master.content.parts then + doLevel(content, master.content.parts, baseShiftHeadings - 1, metadataOptions) + elseif master.content.chapters then + doLevel(content, master.content.chapters, baseShiftHeadings, metadataOptions) + elseif master.content.frontmatter or master.content.mainmatter or master.content.backmatter then + doDivision(content, master.content, baseShiftHeadings, metadataOptions) + else + SU.error("Invalid master document (no division, parts or chapters)") + end end if enabledBook then