Skip to content

Thread $heading-level through HTML block templates#2851

Closed
rbeezer wants to merge 1 commit into
PreTeXtBook:masterfrom
rbeezer:heading-level-threading
Closed

Thread $heading-level through HTML block templates#2851
rbeezer wants to merge 1 commit into
PreTeXtBook:masterfrom
rbeezer:heading-level-threading

Conversation

@rbeezer
Copy link
Copy Markdown
Collaborator

@rbeezer rbeezer commented May 13, 2026

Thread $heading-level through HTML block templates

Summary

mode="hN" in pretext-html.xsl had two branches: a when branch using a
passed-in $heading-level, and an otherwise branch that computed the level
from a count() heuristic over ancestors. This PR threads $heading-level
through every path that produces an HTML <hN> heading. The otherwise
branch is now unreachable in normal operation; entering it is treated as a
bug, reported via PTX:BUG (with the offending element's ancestor path), and
defaults to h2.

After this change, mode="hN" is a simple lookup: emit h{$heading-level},
clamped to h6. The count() arithmetic and the chunk-level-zero-adjustment
are gone.

Motivation: why count() was unsatisfactory

The previous formula approximated nesting depth by counting structural and
block-like ancestors and then patching the result with a sequence of
context-specific adjustments:

count(STRUCTURAL ancestors) - $chunk-level - $chunk-level-zero-adjustment
- count(backmatter/frontmatter ancestors)
+ count(DEFINITION/THEOREM/AXIOM/REMARK/COMPUTATION/OPENPROBLEM/EXAMPLE/PROJECT/GOAL/subexercises/exercise/task/exercisegroup ancestors)
- count(self::answer|hint|solution)
- count(self::INNER-PROOF)
+ count(ASIDE/introduction/conclusion/paragraphs/li with title ancestors)
+ 2

The shortcomings:

  1. Source structure, not rendered structure. The formula counts source
    ancestors, but several block types render as siblings rather than nested
    children (notably PROOF-LIKE is emitted as a sibling of THEOREM-LIKE).
    The formula compensated with a hard-coded INNER-PROOF subtraction. Any
    future rendering refactor of this kind would require another patch.

  2. Hand-maintained per-type adjustments. Each new block type needs to be
    added to one or more of the count(ancestor::*[...]) lists; otherwise
    nesting silently miscounts. This is invisible failure — output looks
    plausible but the headings drift.

  3. Wrong inside <page> wrappers. worksheet/page and handout/page
    are non-structural wrappers that introduce a rendered nesting level the
    formula does not see. Blocks inside <page> were emitted one level too
    deep (e.g. h5 where h4 was correct).

  4. Wrong inside the solutions backmatter division. The mode="solutions"
    recursion through exercises/projects/tasks produced proofs at one level
    too deep.

  5. Wrong for appendage headings. The - count(self::answer|hint|solution)
    subtraction forced hint/answer/solution headings to the same hN as the
    parent example/exercise/task, even though they sit inside the parent's
    <article> in the DOM. Screen readers and outline navigation see this
    as the appendage being a sibling rather than a child of the parent.

  6. Wrong for standalone and xref-knowl pages. These are freshly framed
    HTML pages with their own masthead h1. The formula returned a level
    derived from the source location of the target, which is irrelevant to
    the standalone context.

In every case above, the rendering code knows the correct level because the
chain of apply-templates calls produces the nesting. Threading the
parameter through that chain makes the level both correct and obvious from
reading the templates.

Correctness changes in output

Most of the output is byte-identical except for build timestamps. The
deliberate behavioral changes are:

Where Old hN New hN Why new is correct
Blocks inside worksheet/page or handout/page one level too deep correct page wrapper now properly accounted for
Proofs inside solutions backmatter one level too deep correct mode="solutions" chain no longer double-increments
Appendages (hint, answer, solution) inside example/task same level as parent block one level deeper matches DOM nesting; better outline structure
Cases inside a proof same level as proof one level deeper same reasoning as appendages
xref-knowl content varied per target fixed h2 knowl is a standalone page with its own masthead h1
Standalone interactive page varied per source location fixed h2 standalone page has its own masthead h1

All other heading levels are unchanged. Verified by directory diff against
the prior master on sample-article and sample-book at chunk levels 0-4;
no PTX:BUG messages fire.

Implementation outline

  • Added <xsl:param name="heading-level"/> to: the block default-match
    template, mode="born-visible", mode="born-hidden", mode="body" (all
    variants including the p, p[ol|ul|dl|...], ol/li|ul/li|var/li,
    dl/li, gi specializations), every mode="heading-birth" and
    mode="heading-xref-knowl" specialization, the helper heading-*
    templates (heading-list-number, heading-divisional-exercise-{serial,typed},
    heading-full-implicit-number, heading-type, heading-no-number,
    heading-non-singleton-number, heading-title-paragraphs, heading-case),
    every mode="wrapped-content" template, mode="exercise-components",
    mode="solutions-div", the mode="solutions" paths for
    exercise|PROJECT-LIKE and task, the interactive and
    interactive-core templates and the runestone mode="tabbed-tasks"
    template, the sidebyside/sbsgroup/panel-panel/sidebyside/stack
    templates in pretext-common.xsl, the ol|ul/dl templates, and the
    structural worksheet/page|handout/page, introduction, and conclusion
    templates.

  • mode="body" increments $heading-level + 1 before invoking
    wrapped-content, codifying the "children are deeper than the parent
    block" rule. Sibling-rendered PROOF-LIKE after a THEOREM-LIKE body
    receives the parent theorem's $heading-level unchanged.

  • Entry points that begin a fresh page set $heading-level explicitly to
    2: manufacture-knowl for xref-knowl content; the interactive /
    standalone-page invocation; the mode="intermediate" summary page (for
    pre-content introduction|titlepage|abstract|objectives and post-content
    conclusion|outcomes).

  • The mode="hN" otherwise branch was simplified to:

    <xsl:message>PTX:BUG: "hN" template reached without a $heading-level
      parameter on element <name> at ancestor/path/...; defaulting to h2</xsl:message>
    <xsl:text>2</xsl:text>

    The ancestor path makes it easy to locate the source element when a
    regression slips a missing parameter past review.

Validation

For both examples/sample-article and examples/sample-book at
-x chunk.level values 0, 1, 2, 3, 4: zero PTX:BUG messages from
mode="hN".

Files

  • xsl/pretext-html.xsl — bulk of the threading
  • xsl/pretext-common.xslsidebyside, sbsgroup, sidebyside/stack
  • xsl/pretext-runestone.xslmode="tabbed-tasks"

Claude Opus 4.7, acting as a coding assistant for Rob Beezer

@rbeezer rbeezer marked this pull request as draft May 14, 2026 16:24
@rbeezer rbeezer marked this pull request as ready for review May 16, 2026 17:08
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@rbeezer rbeezer force-pushed the heading-level-threading branch from 7a87a3f to 7b70213 Compare May 16, 2026 17:12
@rbeezer
Copy link
Copy Markdown
Collaborator Author

rbeezer commented May 16, 2026

Force-push was just a rebase onto current master

@rbeezer
Copy link
Copy Markdown
Collaborator Author

rbeezer commented May 16, 2026

Merged as-is, just edited commit message to add the PR number.

Using the new profiling tools at #2829, I see a roughly 0.1s/3% speedup (3.83s down to 3.74s) for the sample article.

@rbeezer rbeezer closed this May 16, 2026
@rbeezer rbeezer deleted the heading-level-threading branch May 16, 2026 22:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant