From aaa02c8cd34cd93beae50121762d47502abd275f Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 20 May 2026 16:52:21 +0200 Subject: [PATCH 1/5] Always use hyphen as the separator in the page header. --- docs/_includes/book-chapter-body.html | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/_includes/book-chapter-body.html b/docs/_includes/book-chapter-body.html index 6380f7c7..f34b5f21 100644 --- a/docs/_includes/book-chapter-body.html +++ b/docs/_includes/book-chapter-body.html @@ -151,7 +151,7 @@ overrides the class (front-matter, part-foreword), the override is used as-is and sub-page styling never kicks in. Otherwise sub-pages get the " sub-chapter" suffix and a compound running - header ("Module - Member" / "Class.Member"). + header ("Parent - Member"). {%- endcomment -%} {%- if include.article_class_override -%} {%- assign article_class = include.article_class_override -%} @@ -160,12 +160,7 @@ {%- assign article_class = 'page' -%} {%- if is_sub_page -%} {%- assign article_class = article_class | append: ' sub-chapter' -%} - {%- if current_index_kind == 'module' -%} - {%- assign compound_sep = ' - ' -%} - {%- else -%} - {%- assign compound_sep = '.' -%} - {%- endif -%} - {%- assign header_title = current_index_name | append: compound_sep | append: include.chapter.title -%} + {%- assign header_title = current_index_name | append: ' - ' | append: include.chapter.title -%} {%- else -%} {%- assign header_title = include.chapter.title -%} {%- endif -%} From 115e6249ec161af558fea2a2ecbe7c1f060ee86a Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 20 May 2026 16:54:44 +0200 Subject: [PATCH 2/5] Expand all FAQ entries and hide their markers. --- docs/_plugins/book-chapter-transform.rb | 41 +++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/_plugins/book-chapter-transform.rb b/docs/_plugins/book-chapter-transform.rb index c25f618f..e6419800 100644 --- a/docs/_plugins/book-chapter-transform.rb +++ b/docs/_plugins/book-chapter-transform.rb @@ -28,22 +28,26 @@ # === Approach === # # `book_chapter_transform(body, baseurl, heading_shift_n, chapter_anchor)` -# does all six passes in one method: +# does all seven passes in one method: # # * Step 1 uses a single literal `gsub!` keyed on the live # `site.baseurl` value (passed as the second filter argument so # the constant isn't baked into the plugin at load time). -# * Step 2 walks a frozen `WHITESPACE_PATTERNS` table of 12 +# * Step 2 strips `
`, `
`, ``, and +# `` tags so collapsible sections (FAQ) render as +# flat content in the PDF. +# * Step 3 walks a frozen `WHITESPACE_PATTERNS` table of 12 # literal `[search, replacement]` pairs and applies them in # longest-first order, matching the Liquid chain's order # exactly. Literal `gsub!` on each. -# * Steps 3-5 collapse into a single regex pass keyed on -# `heading_shift_n` (= 0, 1, 2, or 3 -- precomputed in Liquid -# from `skip_base_heading_shift`, `is_sub_page`, and -# `extra_heading_shift`). The N-pass cascade of the Liquid -# chain is equivalent to a one-pass regex that bumps each -# heading level by N, capping at `h7-stub` for levels above 6. -# * Step 6 replaces the 13 literal `replace` calls with one regex +# * Steps 4 collapses the three heading-shift cascades into a +# single regex pass keyed on `heading_shift_n` (= 0, 1, 2, or +# 3 -- precomputed in Liquid from `skip_base_heading_shift`, +# `is_sub_page`, and `extra_heading_shift`). The N-pass cascade +# of the Liquid chain is equivalent to a one-pass regex that +# bumps each heading level by N, capping at `h7-stub` for +# levels above 6. +# * Step 5 replaces the 13 literal `replace` calls with one regex # for heading-id injection (matches ` 0 result.gsub!(HEADING_SHIFT_RE) do @@ -153,7 +170,7 @@ def book_chapter_transform(body, baseurl, heading_shift_n, chapter_anchor) end end - # Step 4: anchor-id prefix on every heading id + intra-chapter href. + # Step 5: anchor-id prefix on every heading id + intra-chapter href. if chapter_anchor && !chapter_anchor.to_s.empty? prefix = "#{chapter_anchor}-" result.gsub!(HEADING_ID_RE) do From 71aea3dd18a79ba787e5a45d2488b8b323ce85a7 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 20 May 2026 17:12:20 +0200 Subject: [PATCH 3/5] Add the part name as a prefix to the page number. --- docs/lib/paged.browser.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/lib/paged.browser.js b/docs/lib/paged.browser.js index 07509811..869c6883 100644 --- a/docs/lib/paged.browser.js +++ b/docs/lib/paged.browser.js @@ -30450,9 +30450,10 @@ // Running page-number counter tracked in JS so the displayed value // survives aggressive-detach (perf/detach-pages.js), which removes // finalized pages from the DOM and breaks `counter(page)` accumulation. - // Set as `--page-num: "N"` on each page wrapper; print.css reads it - // via `content: var(--page-num)` in @bottom-right. + // Set as `--page-num: "Title - N"` on each page wrapper; print.css + // reads it via `content: var(--page-num)` in @bottom-right. this._displayPageCounter = 0; + this._displayPartTitle = ""; } onDeclaration(declaration, dItem, dList, rule) { @@ -30866,12 +30867,24 @@ const parsed = parseInt(reset.dataset.counterPageReset, 10); if (!isNaN(parsed)) pageResetValue = parsed; }); + // When a part divider resets the page counter, capture the part + // title from its h1 or .part-title-silent so subsequent pages + // display "Title - N" instead of a bare number. + let partDivider = pageElement.querySelector("article.part-divider"); + if (partDivider) { + let titleEl = partDivider.querySelector("h1") || partDivider.querySelector(".part-title-silent"); + if (titleEl) this._displayPartTitle = titleEl.textContent.trim(); + } + if (pageResetValue === null) { this._displayPageCounter += 1; } else { this._displayPageCounter = pageResetValue; } - pageElement.style.setProperty("--page-num", `"${this._displayPageCounter}"`); + let displayNum = this._displayPartTitle + ? `${this._displayPartTitle} - ${this._displayPageCounter}` + : `${this._displayPageCounter}`; + pageElement.style.setProperty("--page-num", `"${displayNum}"`); } } From c70b83d2984f645be14f2e8a90c0e57003c9e6fe Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 20 May 2026 17:36:00 +0200 Subject: [PATCH 4/5] Get the details of page headers/footers out of paged.js. --- docs/assets/css/print.css | 35 +++++++++++++++++++++++++---------- docs/book.html | 1 + docs/lib/paged.browser.js | 37 +++++++++++++++++-------------------- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/docs/assets/css/print.css b/docs/assets/css/print.css index 6cee56dd..3116188e 100644 --- a/docs/assets/css/print.css +++ b/docs/assets/css/print.css @@ -13,14 +13,15 @@ margin: 22mm 20mm 22mm 20mm; @bottom-right { - /* Reads a JS-tracked page number set on each .pagedjs_page wrapper - by the Counters handler in docs/lib/paged.browser.js. Switched off - `counter(page)` because the aggressive-detach render optimization - (perf/detach-pages.js) physically removes finalized pages from the - DOM, which breaks CSS counter accumulation. The Counters handler - honours the same part-divider counter-reset rules as the original - counter(page) did, so part-restarts continue to work. */ - content: var(--page-num); + /* Page number with part-title prefix. `string(part-title)` is set by + a hidden span on each part-divider article and persists across + pages until the next part. `var(--page-num)` is a JS-tracked + counter that survives aggressive-detach (perf/detach-pages.js) -- + CSS `counter(page)` breaks when finalized pages are removed from + the DOM, so the Counters handler polyfills it as a CSS variable. + Named @page overrides (front-matter, divider, :first) suppress or + simplify this for pages that shouldn't show the part prefix. */ + content: string(part-title) " - " var(--page-num); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 9pt; color: #555; @@ -61,7 +62,7 @@ article { + class-restricted selectors on h2/h3 left some top-level chapter pages with stale or empty running headers. For top-level chapters the span carries the chapter title; for sub-pages it carries the compound - "Parent.Sub" / "Parent - Sub" title (1.6c). */ + "Parent - Sub" title (1.6c). */ article.page > .header-string { string-set: chapter-title content(); position: absolute; @@ -71,6 +72,19 @@ article.page > .header-string { overflow: hidden; } +/* Part-title source for the @bottom-right page number prefix. Each part + divider emits a hidden carrying the + part's title. `string(part-title)` persists across pages until the + next part divider overrides it -- same mechanism as chapter-title. */ +article.part-divider > .part-title-string { + string-set: part-title content(); + position: absolute; + font-size: 0; + width: 0; + height: 0; + overflow: hidden; +} + /* ---- Title page (front matter, page 1) ------------------------------ Emitted by book.html as the first element in , so it lands on page 1 without any forced break. Chrome (running header + page number) @@ -191,7 +205,8 @@ article.front-matter { } @page front-matter { - @top-right { content: ""; } + @top-right { content: ""; } + @bottom-right { content: var(--page-num); } } diff --git a/docs/book.html b/docs/book.html index 9fa06048..2c465a38 100644 --- a/docs/book.html +++ b/docs/book.html @@ -100,6 +100,7 @@

twinBASIC Documentation

{%- for part in site.data.book.parts -%}
+ {{ part.title }}

Part {{ roman[forloop.index0] }}

{%- if part.no_outline_entry %}

{{ part.title }}

diff --git a/docs/lib/paged.browser.js b/docs/lib/paged.browser.js index 869c6883..c22727a2 100644 --- a/docs/lib/paged.browser.js +++ b/docs/lib/paged.browser.js @@ -30450,10 +30450,9 @@ // Running page-number counter tracked in JS so the displayed value // survives aggressive-detach (perf/detach-pages.js), which removes // finalized pages from the DOM and breaks `counter(page)` accumulation. - // Set as `--page-num: "Title - N"` on each page wrapper; print.css - // reads it via `content: var(--page-num)` in @bottom-right. + // Set as `--page-num: "N"` on each page wrapper; print.css reads it + // via `content: var(--page-num)` in @bottom-right. this._displayPageCounter = 0; - this._displayPartTitle = ""; } onDeclaration(declaration, dItem, dList, rule) { @@ -30867,24 +30866,12 @@ const parsed = parseInt(reset.dataset.counterPageReset, 10); if (!isNaN(parsed)) pageResetValue = parsed; }); - // When a part divider resets the page counter, capture the part - // title from its h1 or .part-title-silent so subsequent pages - // display "Title - N" instead of a bare number. - let partDivider = pageElement.querySelector("article.part-divider"); - if (partDivider) { - let titleEl = partDivider.querySelector("h1") || partDivider.querySelector(".part-title-silent"); - if (titleEl) this._displayPartTitle = titleEl.textContent.trim(); - } - if (pageResetValue === null) { this._displayPageCounter += 1; } else { this._displayPageCounter = pageResetValue; } - let displayNum = this._displayPartTitle - ? `${this._displayPartTitle} - ${this._displayPageCounter}` - : `${this._displayPageCounter}`; - pageElement.style.setProperty("--page-num", `"${displayNum}"`); + pageElement.style.setProperty("--page-num", `"${this._displayPageCounter}"`); } } @@ -32084,10 +32071,20 @@ } - fragment.style.setProperty(`--pagedjs-string-first-${name}`, `"${cleanPseudoContent(varFirst)}`); - fragment.style.setProperty(`--pagedjs-string-last-${name}`, `"${cleanPseudoContent(varLast)}`); - fragment.style.setProperty(`--pagedjs-string-start-${name}`, `"${cleanPseudoContent(varStart)}`); - fragment.style.setProperty(`--pagedjs-string-first-except-${name}`, `"${cleanPseudoContent(varFirstExcept)}`); + // Local patch: trailing `"` on each value. Upstream pagedjs + // writes `"${...}` (no closing quote); CSS auto-closes + // unterminated strings at the declaration boundary, so + // `content: var(--pagedjs-string-first-X)` alone works. + // But mixing `string()` with other values (e.g. + // `content: string(X) " - " var(--page-num)`) breaks -- + // the substituted `"value` swallows the literal `" - "` + // as part of its unterminated string and the browser + // drops the declaration. Closing the quote here makes + // `string()` composable with sibling content values. + fragment.style.setProperty(`--pagedjs-string-first-${name}`, `"${cleanPseudoContent(varFirst)}"`); + fragment.style.setProperty(`--pagedjs-string-last-${name}`, `"${cleanPseudoContent(varLast)}"`); + fragment.style.setProperty(`--pagedjs-string-start-${name}`, `"${cleanPseudoContent(varStart)}"`); + fragment.style.setProperty(`--pagedjs-string-first-except-${name}`, `"${cleanPseudoContent(varFirstExcept)}"`); } From f7ba0a9bb7b8305ed37b16cd948de273e5d1c6fb Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 20 May 2026 17:43:17 +0200 Subject: [PATCH 5/5] Document the recent book process updates. --- BOOKPLAN.md | 25 +++++++++++-------------- WIP.md | 2 +- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/BOOKPLAN.md b/BOOKPLAN.md index 9518eb65..c9c8c683 100644 --- a/BOOKPLAN.md +++ b/BOOKPLAN.md @@ -17,12 +17,12 @@ or `build.bat` then `book.bat`. One Jekyll invocation produces three trees in pa Touch points and what each one already exposes: -- [docs/book.html](docs/book.html) — iterator that concatenates every chapter into one HTML document. Permalink `/book.html`, layout `book-combined`. Contains: the Roman numerals array; the title-page section (1.3); the front-matter loop (1.7) that emits `site.data.book.front_matter` entries inline between the title page and Part I; the per-part loop. Each part can be **flat** (page selectors directly on the part, plus an optional `landing_page:`) or **chaptered** (1.9; a `foreword_page:` and/or `landing_page:` plus a nested `chapters:` list, each chapter carrying its own selectors and divider page). Each chapter-loop caller reads its pre-resolved page list from `entry._chapters`, populated once at `:site, :pre_render` by `_plugins/book-resolve-chapters.rb` (so the selector schema stays in one place). Per-chapter body rendering is delegated to `_includes/book-chapter-body.html`, which in turn calls the `book_chapter_transform` Liquid filter (`_plugins/book-chapter-transform.rb`) for whitespace fix, heading-depth shift, heading-id rewrite, and intra-chapter href-anchor rewrite. Insertion points for new front matter go **after** the title-page section and **before** the `{%- for part in site.data.book.parts -%}` opener. +- [docs/book.html](docs/book.html) — iterator that concatenates every chapter into one HTML document. Permalink `/book.html`, layout `book-combined`. Contains: the Roman numerals array; the title-page section (1.3); the front-matter loop (1.7) that emits `site.data.book.front_matter` entries inline between the title page and Part I; the per-part loop. Each part can be **flat** (page selectors directly on the part, plus an optional `landing_page:`) or **chaptered** (1.9; a `foreword_page:` and/or `landing_page:` plus a nested `chapters:` list, each chapter carrying its own selectors and divider page). Each chapter-loop caller reads its pre-resolved page list from `entry._chapters`, populated once at `:site, :pre_render` by `_plugins/book-resolve-chapters.rb` (so the selector schema stays in one place). Each part divider emits a `` carrying the part title; CSS captures it via `string-set: part-title` and uses it in the `@bottom-right` page-number prefix. Per-chapter body rendering is delegated to `_includes/book-chapter-body.html`, which in turn calls the `book_chapter_transform` Liquid filter (`_plugins/book-chapter-transform.rb`) for `
`/`` unwrapping, whitespace fix, heading-depth shift, heading-id rewrite, and intra-chapter href-anchor rewrite. Insertion points for new front matter go **after** the title-page section and **before** the `{%- for part in site.data.book.parts -%}` opener. - [docs/_includes/book-chapter-body.html](docs/_includes/book-chapter-body.html) — per-chapter body processing, called via `{% include book-chapter-body.html chapter=... %}` from each of book.html's chapter-loop callers. Handles sub-page detection (1.6a, opt-out via `skip_sub_page_detection`), compound running header (1.6c), and emits the final `
` block. The heavier rewrites — markdownify, the pagedjs whitespace fix (1.5/2.1), the 1.5a heading-depth shift (+ the 1.6b sub-page and 1.9 chaptered-part additional shifts when applicable), the 1.5b heading-id prefix, and the intra-chapter `href="#..."` anchor prefix — are batched into one Ruby pass via the `body | book_chapter_transform: site.baseurl, heading_shift_n, chapter_anchor` filter call. Take-it-or-leave-it parameters cover the cases that don't fit the default: `article_class_override` (front-matter and part-foreword), `chapter_anchor_override` (root URL `/` fallback to `ch-introduction`), `skip_sub_page_detection` (front-matter entries and part landings don't share an index hierarchy with following chapters), `skip_base_heading_shift` (skips the 1.5a `+1` shift; paired with the part's `no_heading_shift` flag), `extra_heading_shift` (adds the 1.9 chaptered-part `+1` shift on top of 1.5a so class / module indexes nest under their chapter divider in the outline). The three `_*_heading_shift` parameters and `skip_base_heading_shift` combine into a single `heading_shift_n` integer the include passes to the filter; the filter then bumps each heading by exactly N levels in one regex pass (capping at h7-stub above source-h6), rather than running 0-3 cascading shift chains. - [docs/_plugins/book-resolve-chapters.rb](docs/_plugins/book-resolve-chapters.rb) — `:site, :pre_render` generator that walks `_data/book.yml` (`front_matter:`, each flat part, each part's optional `foreword_page:`/`landing_page:`, and each chapter inside a chaptered part) and stashes the resolved page array on `entry._chapters` for `book.html` to iterate. Recognises four selector keys on the entry — `page:` (single URL), `pages:` (list of URLs), `nav_page:` (single nav-path), `nav_pages:` (list of nav-paths) — and one modifier, `no_descent:`, that flips every match from the default `contains` (starts-with) semantics to exact-equality. `landing_page:` and `foreword_page:` are not resolved here; their first-emission / divider-styling semantics live in `book.html`'s caller. Replaces the earlier per-render Liquid include `_includes/book-collect-matches.html` -- the `where_exp` / `where` / `concat` / `sort_by_nav_order` chains were running 37 times per build for ~1.5 s of Liquid expression-interpreter time; precomputing once at site:pre_render is free. -- [docs/_plugins/book-chapter-transform.rb](docs/_plugins/book-chapter-transform.rb) — registers the `book_chapter_transform` Liquid filter that takes a chapter body and applies, in one Ruby pass: the pagedjs inter-span whitespace fix (longest-first regex over `WHITESPACE_PATTERNS`), the N-level heading shift (1.5a + 1.6b + 1.9, where N is precomputed by the include from `skip_base_heading_shift` / `is_sub_page` / `extra_heading_shift`), the 1.5b `id="..."` prefix per chapter, and the corresponding `href="#..."` prefix for intra-chapter anchors. One filter call replaces a chain of ~36 `| replace:` invocations plus a 12-pattern whitespace span wrap from the prior in-template implementation (~3 cascading heading-shift passes × 12 replaces, plus the anchor-id 13-replace pass). +- [docs/_plugins/book-chapter-transform.rb](docs/_plugins/book-chapter-transform.rb) — registers the `book_chapter_transform` Liquid filter that takes a chapter body and applies, in one Ruby pass: `
`/`` tag stripping (so collapsible sections like the FAQ render as flat content in the PDF), the pagedjs inter-span whitespace fix (longest-first regex over `WHITESPACE_PATTERNS`), the N-level heading shift (1.5a + 1.6b + 1.9, where N is precomputed by the include from `skip_base_heading_shift` / `is_sub_page` / `extra_heading_shift`), the 1.5b `id="..."` prefix per chapter, and the corresponding `href="#..."` prefix for intra-chapter anchors. One filter call replaces a chain of ~36 `| replace:` invocations plus a 12-pattern whitespace span wrap from the prior in-template implementation (~3 cascading heading-shift passes × 12 replaces, plus the anchor-id 13-replace pass). - [docs/_layouts/book-combined.html](docs/_layouts/book-combined.html) — minimal wrapper: `` + `{{ site.title }}` + `rouge.css` + `print.css` + `{{ content }}`. No nav, no JS, no chrome. Pagedjs runs on the rendered output of this layout. The only layout the PDF pipeline uses; the older per-source-page `book` layout was retired alongside `_config-pdf.yml`. -- [docs/assets/css/print.css](docs/assets/css/print.css) — the book's design. Existing structural rules: `@page` (A4, 22mm margins, running header in `@top-right` via `string(chapter-title)`, page number in `@bottom-right`); `@page :first` (suppresses both — used by the title page); `@page divider` (suppresses both, used by part dividers via `page: divider`); `@page front-matter` (suppresses both, used by `article.front-matter` for 1.7 Introduction-style sections); `@page part-foreword` + `@page chapter-divider` (suppresses both, used by the 1.9 part foreword and per-chapter title pages); `article { break-before: page }`; per-chapter `string-set: chapter-title` on `article.page > .header-string`; the top-level vs sub-chapter heading-size split (`article.page:not(.sub-chapter) > h2:first-of-type` vs `article.page.sub-chapter > h3:first-of-type`); chapter-divider H2 typography (`article.chapter-divider h2` — 24pt centered, no border) plus its subtitle (`.chapter-subtitle` — 13pt italic). +- [docs/assets/css/print.css](docs/assets/css/print.css) — the book's design. Existing structural rules: `@page` (A4, 22mm margins, running header in `@top-right` via `string(chapter-title)`, page number with part prefix in `@bottom-right` via `string(part-title) " - " var(--page-num)`); `@page :first` (suppresses both --- used by the title page); `@page divider` (suppresses both, used by part dividers via `page: divider`); `@page front-matter` (suppresses running header and uses bare page number without part prefix, used by `article.front-matter` for 1.7 Introduction-style sections); `@page part-foreword` + `@page chapter-divider` (suppresses both, used by the 1.9 part foreword and per-chapter title pages); `article { break-before: page }`; per-chapter `string-set: chapter-title` on `article.page > .header-string`; per-part `string-set: part-title` on `article.part-divider > .part-title-string`; the top-level vs sub-chapter heading-size split (`article.page:not(.sub-chapter) > h2:first-of-type` vs `article.page.sub-chapter > h3:first-of-type`); chapter-divider H2 typography (`article.chapter-divider h2` — 24pt centered, no border) plus its subtitle (`.chapter-subtitle` — 13pt italic). - [docs/_data/book.yml](docs/_data/book.yml) — the manifest book.html iterates over. Schema: `parts:` is an ordered list of numbered parts. A **flat part** carries page-selectors directly (`page:` / `pages:` / `nav_page:` / `nav_pages:`) plus an optional `landing_page:`; a **chaptered part** (1.9) replaces the selectors with a `chapters:` list of per-chapter entries `{ title, subtitle, landing_page, page/pages/nav_page/nav_pages, ... }` and may carry a `foreword_page:` and/or a `landing_page:` on the part itself. Each chaptered chapter emits a full-page `
` title page followed by its landing page (with the source H1 stripped by the plugin) and the selector-matched content in `sort_by_nav_order` order. `front_matter:` is a sibling list of front-matter sections (1.7), same selector shape as a flat part. The selector keys: `pages:` is a list of URL substrings matched via `contains` (multiple entries can map to one Part / chapter — used for the Reference Section in 1.8 and for the VBA chapter's landing at `/tB/Packages/VBA` + members under `/tB/Modules/...`); `page:` is the singular alias. `nav_pages:` is the same shape against `page.data["nav_path"]` (populated by `_plugins/nav-path.rb`) — used when a section is most naturally expressed as a nav-tree branch rather than a URL prefix; `nav_page:` is its singular alias. A `no_descent: true` modifier on the entry switches every selector to exact-equality so a single index page can be picked up without sweeping its sub-pages. Additional control flags: `no_outline_entry:` suppresses the part-divider H1 / chapter-divider H2 (so the section's first content heading becomes the bookmark target); `no_heading_shift:` skips the 1.5a base shift for the part's entries (used when the source pages are already authored at H2-and-deeper). Available in Liquid as `site.data.book.parts` and `site.data.book.front_matter`. - [docs/_data/build.yml](#) — **not committed**. Build provenance lives in `site.data.build` (populated in memory by the plugin), so the YAML file is never written. The fields exposed are `site.data.build.commit` (short hash) and `site.data.build.commit_date` (ISO date, `%cs`), or `'unknown'` when git is unavailable. - [docs/_config.yml](docs/_config.yml) — the regular-site config. Reads `site.title` ("twinBASIC Documentation") and `site.footer_content` (the canonical copyright string, reused by the title page and colophon). Also exposes the two combined-build toggles the post-write plugins consult: `also_build_offline: true` (offlinify) and `also_build_pdf: true` (pdfify). Both default to true in the committed config; flip either to `false` to skip that output without touching `_site/`. @@ -63,6 +63,7 @@ Cumulative discoveries from earlier phases. Read before starting a new task — - **`article:first-of-type { break-before: avoid }` was a pre-title-page hack.** It prevented a blank page 0 when the first content was the Part I divider. With a title page now sitting on page 1 and the first article (Part I divider) wanting to break to page 2, the rule must be **removed**, not kept — it collides with `section.title-page { break-after: page }` and `article { break-before: page }`. - **Whitespace inside `
` is fragile across page breaks.** The `book.html` `p1..p4` and `p4i4..p4i16` replacement chains exist to wrap every inter-token text node in `` so pagedjs doesn't drop the whitespace when it splits a code block. New code that emits `
` content should expect this treatment (or render its code through the same Liquid pipeline).
 - **Adjacent forced breaks collapse to one.** `section.title-page { break-after: page }` plus the next article's `break-before: page` produces a single page break, not two. No blank-page mitigation needed.
+- **Pagedjs `StringSets` handler writes unterminated CSS string values.** The `afterPageLayout` method in `StringSets` sets `--pagedjs-string-first-` etc. with a leading `"` but no closing `"`. CSS auto-closes unterminated strings at the declaration boundary, so `content: var(--pagedjs-string-first-X)` alone works — the browser sees `"value` and closes it to `"value"`. But mixing `string()` with other values (e.g. `content: string(X) " - " var(--page-num)`) breaks: the substituted `"value` swallows the literal `" - "` as part of its unterminated string, producing an invalid declaration the browser drops. Fixed locally in `paged.browser.js` by adding the closing `"` to all four `setProperty` calls.
 
 ### Jekyll plugin patterns
 
@@ -216,7 +217,7 @@ Both rewrites are mechanical text substitutions over the chapter body string, no
 When a folder has an `index.md` plus sibling `.md` files (e.g. `Reference/VBA/Collection/index.md` plus `Add.md`, `Clear.md`, `Count.md`, `Item.md`, …), the siblings are sub-pages of the index. In the rendered book they should:
 
 - Nest under their index in the PDF outline so the bookmark sidebar shows Collection → Add / Clear / Count, not Collection and Add at the same level.
-- Carry a compound running header — `Collection.Add` when the parent index is a class, `Compilation - CompilerVersion` when the parent index is a module.
+- Carry a compound running header — `Collection - Add` when the parent index is a class, `Compilation - CompilerVersion` when the parent index is a module.
 
 This phase pulls naturally from the heading-shift machinery already in 1.5 and shares the per-chapter iteration loop in book.html.
 
@@ -266,17 +267,13 @@ With `--outline-tags h1,h2,h3,h4` on `pagedjs-cli` (extended from the current `h
 
 Sub-pages need a compound running header. The simple-header approach used today (`string-set: chapter-title content()` on the chapter title h2) doesn't compose, so we need a separate string source.
 
-Determine the parent kind and name from the sub-page's `parent:` frontmatter, which by project convention reads ` class`, ` Module`, or ` module`:
-
-- `parent: Collection class` → kind = class, name = `Collection`, separator = `.`.
-- `parent: Interaction Module` → kind = module, name = `Interaction`, separator = ` - `.
-- Anything else → no compound; emit just the sub-page title (defensive fallback for unexpected frontmatter).
+Determine the parent name from the sub-page's `parent:` frontmatter, which by project convention reads ` class`, ` Module`, or ` module`. The parent kind suffix is stripped; all sub-pages use ` - ` as the compound separator regardless of whether the parent is a class or a module.
 
 Emit the compound string in a hidden span immediately inside the sub-page article, before the visible chapter heading:
 
 ```html
 
- Collection.Add + Collection - Add

Add

...
@@ -313,7 +310,7 @@ Internal sub-page section headings (h4 and below) inherit the existing in-chapte - Open the rendered PDF outline. Inside "VBA Runtime" (Part II) → "Collection class", confirm nested entries Add, Clear, Count, Item, Items, Keys, Remove. - Inside "VBRUN Package" → "Compilation module", confirm nested entries CompilerVersion, BuildConfiguration, … under it. - Click "Add" in the outline — jumps to its sub-page. -- On the Add sub-page, the running header at the top-right reads `Collection.Add`. +- On the Add sub-page, the running header at the top-right reads `Collection - Add`. - On a `Compilation/CompilerVersion` sub-page, the running header reads `Compilation - CompilerVersion`. - The visible chapter heading inside the sub-page article still reads just `Add` (or `CompilerVersion`) — the parent isn't repeated visually. - Cross-references from other chapters' See Also lists still resolve correctly (heading-id uniqueness from 1.5 stays intact, and the additional shift in 1.6b doesn't change the `id="ch-..."` prefix scheme). @@ -321,9 +318,9 @@ Internal sub-page section headings (h4 and below) inherit the existing in-chapte #### Tradeoffs / open questions - **Sub-page detection relies on an index.md being present.** If a folder has sibling `.md` files but no `index.md`, those siblings won't be detected as sub-pages — they'll inherit the previous unrelated index in iteration order, then either match it by URL prefix (wrong) or fall through to standalone (acceptable). Audit during implementation: list folders under `docs/Reference/` that have multiple `.md` siblings and no `index.md`. -- **`parent:` frontmatter is the source of truth for class/module distinction.** This is already a project convention enforced across the docs; the WIP.md style guide describes it. If any sub-page is missing `parent:`, the running header falls back to just the sub-title — flag during verification. +- **`parent:` frontmatter is the source of truth for the parent name.** This is already a project convention enforced across the docs; the WIP.md style guide describes it. If any sub-page is missing `parent:`, the running header falls back to just the sub-title — flag during verification. - **Outline tag list grows to `h1,h2,h3,h4`.** Combined with `h7-stub` (which is excluded), the outline gets one extra level. Total entries climb from ~2700 to ~3500. Still acceptable; the `h1,h2` narrow-outline fallback noted in 1.5 is also available if needed. -- **Deeper nesting (sub-sub-pages).** As of 1.7, three-deep folder structures exist (`Features/index.md` → `Features/Compiler-IDE/index.md` → `Features/Compiler-IDE/CodeLens.md`). The single-slot state machine handles them by treating each subfolder's `index.md` as a fresh top-level chapter within the part, with its leaves as direct sub-pages of that subfolder index. The compound running header therefore shows only the closest two levels ("Compiler-IDE - CodeLens"), not the full part path ("Features > Compiler-IDE > CodeLens"). Acceptable; a true stack-based state machine would be needed to recover the full path, and the part divider gives the reader the missing top-level context. +- **Deeper nesting (sub-sub-pages).** As of 1.7, three-deep folder structures exist (`Features/index.md` → `Features/Compiler-IDE/index.md` → `Features/Compiler-IDE/CodeLens.md`). The single-slot state machine handles them by treating each subfolder's `index.md` as a fresh top-level chapter within the part, with its leaves as direct sub-pages of that subfolder index. The compound running header therefore shows only the closest two levels ("Compiler-IDE - CodeLens"), not the full part path ("Features > Compiler-IDE > CodeLens"). The part-title prefix on page numbers provides the missing top-level context. Acceptable; a true stack-based state machine would be needed to recover the full path, and the part divider gives the reader the missing top-level context. ### 1.7 Beyond Reference: front matter and supplementary parts @@ -346,7 +343,7 @@ Pages without explicit `permalink:` frontmatter render at `/X.html`, while pages #### Front-matter CSS -`article.front-matter { page: front-matter; break-before: page; }` plus `@page front-matter { @top-right { content: ""; } }` suppresses the running header on Introduction pages, matching the title-page chrome convention. The named-page selector works here because the article has `break-before: page` (per the pagedjs gotcha that first-of-body never gets a named page applied; front matter is never the first element thanks to the preceding `section.title-page`). +`article.front-matter { page: front-matter; break-before: page; }` plus `@page front-matter { @top-right { content: ""; } @bottom-right { content: var(--page-num); } }` suppresses the running header and shows a bare page number (no part-title prefix) on Introduction pages, matching the title-page chrome convention. The named-page selector works here because the article has `break-before: page` (per the pagedjs gotcha that first-of-body never gets a named page applied; front matter is never the first element thanks to the preceding `section.title-page`). #### Known limitations diff --git a/WIP.md b/WIP.md index 7c067713..3bd29986 100644 --- a/WIP.md +++ b/WIP.md @@ -499,7 +499,7 @@ Ranked by estimated wall-clock saving on the current Windows machine: | `Liquid::Variable#render` total | 10.05 s | 8.96 s | -1.09 s | The `BlockBody#render` / `Context#stack` / `Variable#render` drops reflect the eliminated `{%- assign -%}` / `{%- if -%}` blocks in head_seo.html (dropped from ~85 lines of Liquid logic to ~20 lines of straight output). The 128 remaining `markdownify` calls come from `book.html`'s part subtitle/intro (~24) and `book-chapter-body.html`'s per-chapter `chapter.content | markdownify` (~100 chapters whose content doesn't start with `<`); both candidates for a follow-up pass (see #3). New `Jekyll::SeoPrecompute#absolute_url` adds 0.44 s for 846 calls, replacing 1,675 filter calls that totalled 0.40 s -- essentially flat, but the absolute_url filter had its own per-build cache, so the swap is a wash on this axis. Output byte-identical to baseline (`diff -rq` clean on all three of `_site/`, `_site-offline/`, `_site-pdf/`). -3. **`book-chapter-body.html` heading-shift + anchor-prefix `replace` chain → Ruby pass. [LANDED]** Replaced the per-chapter chain of 0-3 heading-shift cascades (12 replaces each), the 12-pattern whitespace span wrapping, and the 13-replace anchor-id prefix pass with a single Liquid filter `book_chapter_transform` (`_plugins/book-chapter-transform.rb`). The filter takes the body, the site baseurl, a precomputed `heading_shift_n` (0-3, derived in Liquid from `skip_base_heading_shift` / `is_sub_page` / `extra_heading_shift`), and the chapter anchor; does all six passes in one method with no intermediate string allocations beyond what the regex engine produces internally. The dead `p1_search` / `p1_replace` / ... whitespace-pattern declarations were also removed from `book.html`'s prologue. +3. **`book-chapter-body.html` heading-shift + anchor-prefix `replace` chain → Ruby pass. [LANDED]** Replaced the per-chapter chain of 0-3 heading-shift cascades (12 replaces each), the 12-pattern whitespace span wrapping, and the 13-replace anchor-id prefix pass with a single Liquid filter `book_chapter_transform` (`_plugins/book-chapter-transform.rb`). The filter takes the body, the site baseurl, a precomputed `heading_shift_n` (0-3, derived in Liquid from `skip_base_heading_shift` / `is_sub_page` / `extra_heading_shift`), and the chapter anchor; does all seven passes in one method with no intermediate string allocations beyond what the regex engine produces internally (the seventh pass, added later, strips `
`/`` tags so collapsible sections like the FAQ render as flat content in the PDF). The dead `p1_search` / `p1_replace` / ... whitespace-pattern declarations were also removed from `book.html`'s prologue. The single-pass heading shift (one regex bumping each level by N, capping at h7-stub for source levels above 6) is equivalent to N applications of the bottom-up cascade chain -- each source heading lands at `level + N` or `h7-stub` regardless of how many sequential passes the chain ran, since the cascade structure was an artifact of Liquid not having a bump-by-N primitive, not a semantic requirement.