Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions BOOKPLAN.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<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.

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.

Expand Down
9 changes: 2 additions & 7 deletions docs/_includes/book-chapter-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 -%}
Expand All @@ -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 -%}
Expand Down
41 changes: 29 additions & 12 deletions docs/_plugins/book-chapter-transform.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<details>`, `</details>`, `<summary>`, and
# `</summary>` 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 `<h[2-6]` and `<h7-stub`,
# with and without `class="no_toc"`) and one literal `gsub!`
# for `href="#`.
Expand Down Expand Up @@ -121,6 +125,14 @@ module BookChapterTransform
# Heading-shift regex. Captures the optional `/` for closing
# tags and the level digit (1..6). The `\b` after the digit
# prevents accidental matches on hypothetical `<h12...>`.
# <details>/<summary> unwrapping regexes. The FAQ (and potentially
# other pages) uses collapsible sections that must read as flat
# content in the PDF -- Chromium's internal <details> mechanism
# can't be overridden with CSS alone.
DETAILS_OPEN_RE = %r{<details[^>]*>\n?}i.freeze
DETAILS_CLOSE_RE = %r{</details>\n?}i.freeze
SUMMARY_RE = %r{<summary[^>]*>|</summary>\n?}i.freeze

HEADING_SHIFT_RE = /<(\/?)h([1-6])\b/.freeze

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

# Step 2: whitespace span wrapping.
# Step 2: unwrap <details>/<summary> for print layout.
result.gsub!(DETAILS_OPEN_RE, "")
result.gsub!(DETAILS_CLOSE_RE, "")
result.gsub!(SUMMARY_RE, "")

# Step 3: whitespace span wrapping.
WHITESPACE_PATTERNS.each do |search, replacement|
result.gsub!(search, replacement)
end

# Step 3: heading shift cascade by N levels (0..3).
# Step 4: heading shift cascade by N levels (0..3).
n = heading_shift_n.to_i
if n > 0
result.gsub!(HEADING_SHIFT_RE) do
Expand All @@ -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
Expand Down
35 changes: 25 additions & 10 deletions docs/assets/css/print.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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 <span class="part-title-string"> 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 <body>, so it lands on
page 1 without any forced break. Chrome (running header + page number)
Expand Down Expand Up @@ -191,7 +205,8 @@ article.front-matter {
}

@page front-matter {
@top-right { content: ""; }
@top-right { content: ""; }
@bottom-right { content: var(--page-num); }
}


Expand Down
1 change: 1 addition & 0 deletions docs/book.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ <h1 class="book-title">twinBASIC Documentation</h1>

{%- for part in site.data.book.parts -%}
<article class="part-divider{% if part.no_outline_entry %} silent{% endif %}" id="pt-{{ forloop.index }}">
<span class="part-title-string">{{ part.title }}</span>
<p class="part-number">Part {{ roman[forloop.index0] }}</p>
{%- if part.no_outline_entry %}
<p class="part-title-silent">{{ part.title }}</p>
Expand Down
18 changes: 14 additions & 4 deletions docs/lib/paged.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -32071,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)}"`);


}
Expand Down
Loading