Skip to content

Navigation Menu

Sign in
Sign up

feat: clean two-level PDF outline (sections/headlines) by default, configurable - #131

Open
Smengerl wants to merge 2 commits into
j6k4m8:master from
Smengerl:feature/pdf-bookmark-levels
Open

feat: clean two-level PDF outline (sections/headlines) by default, configurable #131
Smengerl wants to merge 2 commits into
j6k4m8:master from
Smengerl:feature/pdf-bookmark-levels

Conversation

@Smengerl

@Smengerl Smengerl commented Aug 3, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Summary

  • WeasyPrint's default UA stylesheet turns every <h1>-<h6> into a PDF bookmark matching its tag, which - combined with section headings rendering as <h2> and story headlines as <h1> - produced a flat, cluttered outline where a story's own incidental subheadings (e.g. an article's preserved "Pros"/"Cons" subheadings from its source markup) sat alongside section and story titles.
  • to_pdf() now generates the bookmark-level CSS itself from three parameters (section_bookmark_level=1, headline_bookmark_level=2, body_heading_bookmarks=False by default) instead of requiring callers to ship their own stylesheet for it. Pass None for either level to leave WeasyPrint's default behavior untouched, or body_heading_bookmarks=True to keep in-body headings in the outline.

Why this matters

A clean, two-level outline is what lets you actually navigate a document on a reMarkable: tap the page-overview/outline button and jump straight to a section or story, no scrolling. That matters most exactly when a paper combines many different sources into one long document - a handful of RSS feeds, Wikipedia, weather, puzzles, whatever - where without this, the outline is either flooded with noise from every incidental heading inside fetched article bodies, or (on plain WeasyPrint defaults) just unusably flat. The outline stays available from anywhere in the document, regardless of how deep you've scrolled - not just at the top.

Follow-up fixes (second commit)

A closer review of _bookmark_css() surfaced three sharp edges, all fixed in the second commit:

  • Selectors were coupled to a heading tag that's actually variable. The rule matched h1.story-headline, but Story.to_html() takes headline_tag as a parameter (default "h1", currently unused elsewhere but live). If a future caller ever renders a headline as e.g. <h2> (say, for ear stories), the bookmark rule would silently stop matching and that headline would fall back to WeasyPrint's raw per-tag default. Selectors are now class-only (.story-headline, .story-section-title), independent of the tag.
  • The masthead title was never covered by _bookmark_css() at all. It's a bare <h1> with no .story-headline class, so it only ever landed at the "right" outline level by coincidence - WeasyPrint's UA default for <h1> happens to equal the default section_bookmark_level of 1. It's now explicitly anchored to section_bookmark_level, so that relationship survives someone changing that parameter later instead of silently drifting.
  • Setting section_bookmark_level and headline_bookmark_level to the same value collapses the whole point of the feature (a two-level outline) with no signal that anything went wrong. _bookmark_css() now emits a UserWarning when this happens, and the docstring calls out that headline_bookmark_level=None can numerically coincide with section_bookmark_level for the same reason (both resolving to WeasyPrint's raw level for their tag).

Test plan

  • pytest goosepaper/test_goosepaper.py - 21 passed, including 8 covering _bookmark_css(): default levels, class-only selectors, the masthead rule, a level set to None, body_heading_bookmarks=True, everything disabled, and the new same-level warning (both that it fires and that it doesn't fire for independent/None levels)
  • Full suite (pytest) - 86 passed
  • End-to-end verification with a real WeasyPrint render (not just the string-generation unit tests): built a fixture paper with two sections, four stories, and bodies containing their own subheadings (Pros/Cons, Ingredients/Steps, etc. - the exact scenario this PR targets), rendered it to PDF across 5 bookmark configurations, and inspected the actual PDF outline tree with pypdf. Confirms the default config produces a clean two-level outline with body subheadings fully excluded, and that the previously-undetected headline_bookmark_level=None / section_bookmark_level collision produced an identical flat outline to explicitly setting both to 1 - the exact issue the second commit's warning now surfaces.
    • (WeasyPrint needs its native Pango/GObject libs on the loader path to run at all - on macOS with Homebrew that's DYLD_LIBRARY_PATH=/opt/homebrew/lib; CI images with those system packages installed can run this directly.)

Smengerl added a commit to Smengerl/goosepaper-logicpuzzles that referenced this pull request Aug 5, 2026
...vel, warn on collisions
Backport of j6k4m8#131's second commit - mainline only had the first commit
(the original _bookmark_css), missing all three follow-up fixes found
during review: class-only selectors instead of h1./h2.-coupled ones,
explicit masthead <h1> anchoring, and a UserWarning when
section_bookmark_level == headline_bookmark_level.
(cherry picked from commit b4131fc)
Smengerl added a commit to Smengerl/goosepaper-logicpuzzles that referenced this pull request Aug 5, 2026
...able
Six open PRs (j6k4m8#126-j6k4m8#131, all opened the same day) were never added
to the "About this fork" tracking table. j6k4m8#121's row still described
its original scope (content_filters + skip_title_patterns) despite
the PR having grown substantially since - renamed to
skip_content_filters, plus accept_content_filters/accept_title_patterns
and min/max_body_text_length.
Smengerl and others added 2 commits August 18, 2026 21:50
...nfigurable
WeasyPrint's default UA stylesheet turns every <h1>-<h6> into a PDF bookmark
matching its tag, which - combined with section headings rendering as <h2>
and story headlines as <h1> - produced a flat, cluttered outline where a
story's own incidental subheadings (e.g. a product review's own "Pros"/
"Cons" headings, preserved verbatim from the source markup) sat alongside
section and story titles.
to_pdf() now generates the bookmark-level CSS itself from three parameters
(section_bookmark_level=1, headline_bookmark_level=2,
body_heading_bookmarks=False by default) instead of requiring callers to
ship their own stylesheet for it. Pass None for either level to leave
WeasyPrint's default behavior untouched, or body_heading_bookmarks=True to
keep in-body headings in the outline.
...vel, warn on collisions
_bookmark_css() matched story headlines via `h1.story-headline`, coupling the PDF-outline
rule to a heading tag that Story.to_html() actually takes as a variable `headline_tag`
parameter (default "h1", currently unused elsewhere but live). Switch both the section and
headline selectors to class-only so a future caller passing a different headline_tag doesn't
silently fall out of the bookmark rule and back to WeasyPrint's raw per-tag default.
The masthead title (a bare <h1> with no story-headline class) was never targeted by
_bookmark_css() at all - it only ever looked right by coincidence, because WeasyPrint's UA
default level for <h1> happens to equal the default section_bookmark_level of 1. Anchor it
explicitly to section_bookmark_level instead of relying on that coincidence.
Also warn when section_bookmark_level and headline_bookmark_level are set to the same value,
since that silently collapses the two-level hierarchy the whole mechanism exists to produce.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Smengerl force-pushed the feature/pdf-bookmark-levels branch from b4131fc to b413958 Compare August 18, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /