Skip to content

Commit d5fb93e

Browse files
authored
Merge pull request #150 from KubaO/staging
Book formatting fixes.
2 parents ea2ea3d + f7ba0a9 commit d5fb93e

7 files changed

Lines changed: 83 additions & 48 deletions

File tree

BOOKPLAN.md

Lines changed: 11 additions & 14 deletions
Large diffs are not rendered by default.

WIP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ Ranked by estimated wall-clock saving on the current Windows machine:
499499
| `Liquid::Variable#render` total | 10.05 s | 8.96 s | -1.09 s |
500500

501501
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/`).
502-
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.
502+
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 `<details>`/`<summary>` 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.
503503

504504
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.
505505

docs/_includes/book-chapter-body.html

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
overrides the class (front-matter, part-foreword), the override
152152
is used as-is and sub-page styling never kicks in. Otherwise
153153
sub-pages get the " sub-chapter" suffix and a compound running
154-
header ("Module - Member" / "Class.Member").
154+
header ("Parent - Member").
155155
{%- endcomment -%}
156156
{%- if include.article_class_override -%}
157157
{%- assign article_class = include.article_class_override -%}
@@ -160,12 +160,7 @@
160160
{%- assign article_class = 'page' -%}
161161
{%- if is_sub_page -%}
162162
{%- assign article_class = article_class | append: ' sub-chapter' -%}
163-
{%- if current_index_kind == 'module' -%}
164-
{%- assign compound_sep = ' - ' -%}
165-
{%- else -%}
166-
{%- assign compound_sep = '.' -%}
167-
{%- endif -%}
168-
{%- assign header_title = current_index_name | append: compound_sep | append: include.chapter.title -%}
163+
{%- assign header_title = current_index_name | append: ' - ' | append: include.chapter.title -%}
169164
{%- else -%}
170165
{%- assign header_title = include.chapter.title -%}
171166
{%- endif -%}

docs/_plugins/book-chapter-transform.rb

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,26 @@
2828
# === Approach ===
2929
#
3030
# `book_chapter_transform(body, baseurl, heading_shift_n, chapter_anchor)`
31-
# does all six passes in one method:
31+
# does all seven passes in one method:
3232
#
3333
# * Step 1 uses a single literal `gsub!` keyed on the live
3434
# `site.baseurl` value (passed as the second filter argument so
3535
# the constant isn't baked into the plugin at load time).
36-
# * Step 2 walks a frozen `WHITESPACE_PATTERNS` table of 12
36+
# * Step 2 strips `<details>`, `</details>`, `<summary>`, and
37+
# `</summary>` tags so collapsible sections (FAQ) render as
38+
# flat content in the PDF.
39+
# * Step 3 walks a frozen `WHITESPACE_PATTERNS` table of 12
3740
# literal `[search, replacement]` pairs and applies them in
3841
# longest-first order, matching the Liquid chain's order
3942
# exactly. Literal `gsub!` on each.
40-
# * Steps 3-5 collapse into a single regex pass keyed on
41-
# `heading_shift_n` (= 0, 1, 2, or 3 -- precomputed in Liquid
42-
# from `skip_base_heading_shift`, `is_sub_page`, and
43-
# `extra_heading_shift`). The N-pass cascade of the Liquid
44-
# chain is equivalent to a one-pass regex that bumps each
45-
# heading level by N, capping at `h7-stub` for levels above 6.
46-
# * Step 6 replaces the 13 literal `replace` calls with one regex
43+
# * Steps 4 collapses the three heading-shift cascades into a
44+
# single regex pass keyed on `heading_shift_n` (= 0, 1, 2, or
45+
# 3 -- precomputed in Liquid from `skip_base_heading_shift`,
46+
# `is_sub_page`, and `extra_heading_shift`). The N-pass cascade
47+
# of the Liquid chain is equivalent to a one-pass regex that
48+
# bumps each heading level by N, capping at `h7-stub` for
49+
# levels above 6.
50+
# * Step 5 replaces the 13 literal `replace` calls with one regex
4751
# for heading-id injection (matches `<h[2-6]` and `<h7-stub`,
4852
# with and without `class="no_toc"`) and one literal `gsub!`
4953
# for `href="#`.
@@ -121,6 +125,14 @@ module BookChapterTransform
121125
# Heading-shift regex. Captures the optional `/` for closing
122126
# tags and the level digit (1..6). The `\b` after the digit
123127
# prevents accidental matches on hypothetical `<h12...>`.
128+
# <details>/<summary> unwrapping regexes. The FAQ (and potentially
129+
# other pages) uses collapsible sections that must read as flat
130+
# content in the PDF -- Chromium's internal <details> mechanism
131+
# can't be overridden with CSS alone.
132+
DETAILS_OPEN_RE = %r{<details[^>]*>\n?}i.freeze
133+
DETAILS_CLOSE_RE = %r{</details>\n?}i.freeze
134+
SUMMARY_RE = %r{<summary[^>]*>|</summary>\n?}i.freeze
135+
124136
HEADING_SHIFT_RE = /<(\/?)h([1-6])\b/.freeze
125137

126138
# Heading-id prefix regex. Matches both `<h[2-6]` and
@@ -137,12 +149,17 @@ def book_chapter_transform(body, baseurl, heading_shift_n, chapter_anchor)
137149
strip = %(src="#{baseurl}/)
138150
result.gsub!(strip, %(src=")) if result.include?(strip)
139151

140-
# Step 2: whitespace span wrapping.
152+
# Step 2: unwrap <details>/<summary> for print layout.
153+
result.gsub!(DETAILS_OPEN_RE, "")
154+
result.gsub!(DETAILS_CLOSE_RE, "")
155+
result.gsub!(SUMMARY_RE, "")
156+
157+
# Step 3: whitespace span wrapping.
141158
WHITESPACE_PATTERNS.each do |search, replacement|
142159
result.gsub!(search, replacement)
143160
end
144161

145-
# Step 3: heading shift cascade by N levels (0..3).
162+
# Step 4: heading shift cascade by N levels (0..3).
146163
n = heading_shift_n.to_i
147164
if n > 0
148165
result.gsub!(HEADING_SHIFT_RE) do
@@ -153,7 +170,7 @@ def book_chapter_transform(body, baseurl, heading_shift_n, chapter_anchor)
153170
end
154171
end
155172

156-
# Step 4: anchor-id prefix on every heading id + intra-chapter href.
173+
# Step 5: anchor-id prefix on every heading id + intra-chapter href.
157174
if chapter_anchor && !chapter_anchor.to_s.empty?
158175
prefix = "#{chapter_anchor}-"
159176
result.gsub!(HEADING_ID_RE) do

docs/assets/css/print.css

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
margin: 22mm 20mm 22mm 20mm;
1414

1515
@bottom-right {
16-
/* Reads a JS-tracked page number set on each .pagedjs_page wrapper
17-
by the Counters handler in docs/lib/paged.browser.js. Switched off
18-
`counter(page)` because the aggressive-detach render optimization
19-
(perf/detach-pages.js) physically removes finalized pages from the
20-
DOM, which breaks CSS counter accumulation. The Counters handler
21-
honours the same part-divider counter-reset rules as the original
22-
counter(page) did, so part-restarts continue to work. */
23-
content: var(--page-num);
16+
/* Page number with part-title prefix. `string(part-title)` is set by
17+
a hidden span on each part-divider article and persists across
18+
pages until the next part. `var(--page-num)` is a JS-tracked
19+
counter that survives aggressive-detach (perf/detach-pages.js) --
20+
CSS `counter(page)` breaks when finalized pages are removed from
21+
the DOM, so the Counters handler polyfills it as a CSS variable.
22+
Named @page overrides (front-matter, divider, :first) suppress or
23+
simplify this for pages that shouldn't show the part prefix. */
24+
content: string(part-title) " - " var(--page-num);
2425
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
2526
font-size: 9pt;
2627
color: #555;
@@ -61,7 +62,7 @@ article {
6162
+ class-restricted selectors on h2/h3 left some top-level chapter
6263
pages with stale or empty running headers. For top-level chapters the
6364
span carries the chapter title; for sub-pages it carries the compound
64-
"Parent.Sub" / "Parent - Sub" title (1.6c). */
65+
"Parent - Sub" title (1.6c). */
6566
article.page > .header-string {
6667
string-set: chapter-title content();
6768
position: absolute;
@@ -71,6 +72,19 @@ article.page > .header-string {
7172
overflow: hidden;
7273
}
7374

75+
/* Part-title source for the @bottom-right page number prefix. Each part
76+
divider emits a hidden <span class="part-title-string"> carrying the
77+
part's title. `string(part-title)` persists across pages until the
78+
next part divider overrides it -- same mechanism as chapter-title. */
79+
article.part-divider > .part-title-string {
80+
string-set: part-title content();
81+
position: absolute;
82+
font-size: 0;
83+
width: 0;
84+
height: 0;
85+
overflow: hidden;
86+
}
87+
7488
/* ---- Title page (front matter, page 1) ------------------------------
7589
Emitted by book.html as the first element in <body>, so it lands on
7690
page 1 without any forced break. Chrome (running header + page number)
@@ -191,7 +205,8 @@ article.front-matter {
191205
}
192206

193207
@page front-matter {
194-
@top-right { content: ""; }
208+
@top-right { content: ""; }
209+
@bottom-right { content: var(--page-num); }
195210
}
196211

197212

docs/book.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ <h1 class="book-title">twinBASIC Documentation</h1>
100100

101101
{%- for part in site.data.book.parts -%}
102102
<article class="part-divider{% if part.no_outline_entry %} silent{% endif %}" id="pt-{{ forloop.index }}">
103+
<span class="part-title-string">{{ part.title }}</span>
103104
<p class="part-number">Part {{ roman[forloop.index0] }}</p>
104105
{%- if part.no_outline_entry %}
105106
<p class="part-title-silent">{{ part.title }}</p>

docs/lib/paged.browser.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32071,10 +32071,20 @@
3207132071

3207232072
}
3207332073

32074-
fragment.style.setProperty(`--pagedjs-string-first-${name}`, `"${cleanPseudoContent(varFirst)}`);
32075-
fragment.style.setProperty(`--pagedjs-string-last-${name}`, `"${cleanPseudoContent(varLast)}`);
32076-
fragment.style.setProperty(`--pagedjs-string-start-${name}`, `"${cleanPseudoContent(varStart)}`);
32077-
fragment.style.setProperty(`--pagedjs-string-first-except-${name}`, `"${cleanPseudoContent(varFirstExcept)}`);
32074+
// Local patch: trailing `"` on each value. Upstream pagedjs
32075+
// writes `"${...}` (no closing quote); CSS auto-closes
32076+
// unterminated strings at the declaration boundary, so
32077+
// `content: var(--pagedjs-string-first-X)` alone works.
32078+
// But mixing `string()` with other values (e.g.
32079+
// `content: string(X) " - " var(--page-num)`) breaks --
32080+
// the substituted `"value` swallows the literal `" - "`
32081+
// as part of its unterminated string and the browser
32082+
// drops the declaration. Closing the quote here makes
32083+
// `string()` composable with sibling content values.
32084+
fragment.style.setProperty(`--pagedjs-string-first-${name}`, `"${cleanPseudoContent(varFirst)}"`);
32085+
fragment.style.setProperty(`--pagedjs-string-last-${name}`, `"${cleanPseudoContent(varLast)}"`);
32086+
fragment.style.setProperty(`--pagedjs-string-start-${name}`, `"${cleanPseudoContent(varStart)}"`);
32087+
fragment.style.setProperty(`--pagedjs-string-first-except-${name}`, `"${cleanPseudoContent(varFirstExcept)}"`);
3207832088

3207932089

3208032090
}

0 commit comments

Comments
 (0)