Skip to content

Commit a1c6187

Browse files
committed
Update the book render plan.
1 parent 335674e commit a1c6187

1 file changed

Lines changed: 8 additions & 4 deletions

File tree

BOOKPLAN.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ or `build.bat` then `book.bat`. One Jekyll invocation produces three trees in pa
1717

1818
Touch points and what each one already exposes:
1919

20-
- [docs/book.html](docs/book.html) — iterator that concatenates every chapter into one HTML document. Permalink `/book.html`, layout `book-combined`. Contains: whitespace-pattern primitives (p1..p4, indent variants) for the pagedjs whitespace fix; 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). Page selection across all three call sites (front-matter, flat part, chaptered chapter) is delegated to `_includes/book-collect-matches.html` so the selector schema stays in one place. Per-chapter body rendering — whitespace fix, heading depth shift, sub-page detection, id uniqueness, header-string emission — is factored out into `_includes/book-chapter-body.html` so the chapter-loop callers share one implementation. Insertion points for new front matter go **after** the title-page section and **before** the `{%- for part in site.data.book.parts -%}` opener.
21-
- [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 markdownify, the pagedjs whitespace fix (consumes the `p1..p4` patterns from the outer scope), heading depth shift (1.5a + sub-page 1.6b), sub-page detection (1.6a, opt-out via `skip_sub_page_detection`), heading-id uniqueness (1.5b), compound running header (1.6c), and emits the final `<article>` block. 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).
22-
- [docs/_includes/book-collect-matches.html](docs/_includes/book-collect-matches.html) — shared selector logic for gathering pages that match a manifest entry. Called from each of book.html's chapter-loop callers via `{% include book-collect-matches.html entry=... %}` after the caller initialises an outer-scope `collected = "" | split: ""` array; the include appends every matching page to `collected` in selector order. 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 handled here; their first-emission / divider-styling semantics live in the caller.
20+
- [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.
21+
- [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 `<article>` 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.
22+
- [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.
23+
- [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).
2324
- [docs/_layouts/book-combined.html](docs/_layouts/book-combined.html) — minimal wrapper: `<html><head>` + `<title>{{ site.title }}</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`.
2425
- [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).
2526
- [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 `<article class="chapter-divider">` 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`.
@@ -45,7 +46,8 @@ Concretely for the PDF book:
4546
- Git-derived build info (commit hash, commit date) → Jekyll plugin (`_plugins/build-info.rb`) that populates `site.data.build` on `:site, :post_read`. Not a pre-build Python step writing `_data/build.yml`.
4647
- Chapter manifest → `_data/book.yml` (committed source of truth, hand-edited).
4748
- Title page, colophon, TOC content → Liquid in `book.html` and the layouts.
48-
- Heading rewrites → Liquid (existing approach in `book.html`). Per-chapter, one-shot, fast.
49+
- Chapter selector resolution (`page` / `pages` / `nav_page` / `nav_pages` / `no_descent`, the `sort_by_nav_order` ordering, and `foreword_page`/`landing_page` resolution) → Jekyll plugin (`_plugins/book-resolve-chapters.rb`) running at `:site, :pre_render`. The Liquid implementation (formerly in `_includes/book-collect-matches.html`) was running ~37 `where_exp` invocations per build for ~1.5 s of Liquid expression-interpreter time; resolving once into `entry._chapters` is free.
50+
- Per-chapter body rewrites (pagedjs whitespace fix, heading-depth shift, heading-id prefix, intra-chapter href anchor prefix) → Jekyll plugin (`_plugins/book-chapter-transform.rb`), exposed as the `book_chapter_transform` Liquid filter that `_includes/book-chapter-body.html` calls once per chapter. The Liquid version was a chain of ~36 `| replace:` invocations plus a 12-pattern whitespace span wrap per chapter; the filter does the same passes in C-implemented regex over the body string, with the heading-shift cascade collapsed to a single bump-by-N regex.
4951
- Cross-reference href rewrites → Jekyll plugin (`_plugins/book-href-rewrite.rb`), running on `:pages, :post_render`. The first cut was inline Liquid; the per-(chapter × permalink) loop burned ~21 s of render even after pre-computing per-permalink search/replace strings and gating each permalink on a common-prefix `contains`, vs ~50 ms in Ruby. Rule of thumb: use Liquid for per-chapter shaping; reach for a plugin when the work is N × M with large N and M.
5052

5153
The carve-out in WIP.md for `_plugins/offlinify.rb` is the same shape: build-time concerns tightly coupled to Jekyll's internal model belong in `_plugins/`, not in an external script.
@@ -190,6 +192,8 @@ Intra-chapter local links must be rewritten in lock-step. Patterns like `[**Coun
190192

191193
Both rewrites are mechanical text substitutions over the chapter body string, no parsing required.
192194

195+
**Implementation.** Landed first as the Liquid replace-chain shown in 1.5a plus a sibling pair for the `id=` / `href="#..."` prefixes, all inside `_includes/book-chapter-body.html`. Folded into the single Ruby filter `book_chapter_transform` (`_plugins/book-chapter-transform.rb`) once the per-chapter Liquid-replace dispatch became visible in the profile — ~36 `| replace:` invocations across the heading-shift cascade (12 source levels × 3 cascading passes) plus the 13-replace id/anchor prefix chain plus the 12-pattern whitespace span wrap, ~3.5 s of `Liquid::StandardFilters#replace` per build. The filter does the same passes in C-implemented regex with the cascade collapsed to a single bump-by-N pass; ~0.14 s for 718 chapter calls.
196+
193197
#### print.css updates
194198

195199
- `string-set: chapter-title content()` moves from `h1:first-of-type` to `h2:first-of-type`.

0 commit comments

Comments
 (0)