diff --git a/CHANGELOG.md b/CHANGELOG.md index d61aee8..cd1fa7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Bug Fixes - fix: Style inline code in a Typst theorem title like inline code anywhere else. (#78) +- fix: Keep the title bar on the same page as the first lines of its code in Typst, so it never sits alone at the bottom of a page. The Typst bundled with Quarto 1.10.19 still breaks a window taller than a page from the current page, which 1.2.1 could not rely on. (#80) ## 1.6.0 (2026-10-05) diff --git a/_extensions/code-window/code-window.lua b/_extensions/code-window/code-window.lua index b1cf221..287fb33 100644 --- a/_extensions/code-window/code-window.lua +++ b/_extensions/code-window/code-window.lua @@ -538,6 +538,7 @@ local function build_typst_function_def(has_hotfixes) below: 0pt, radius: 0pt, stroke: (bottom: 1pt + border-colour), + sticky: true, title-bar, ) // Strip code block chrome so content fills flush against the window body. diff --git a/tests/fixtures/page-break-title.qmd b/tests/fixtures/page-break-title.qmd new file mode 100644 index 0000000..085b758 --- /dev/null +++ b/tests/fixtures/page-break-title.qmd @@ -0,0 +1,104 @@ +--- +title: "A title bar kept with its code across a page break" +filters: + - code-window +format: + typst: + keep-typ: true +--- + +The first two windows start 3em above the bottom of a page. +That leaves room for the title bar but not for the first line of code. + +```{=typst} +#set page(height: 10cm, margin: 1cm) +#v(8cm - 3em) +``` + +```{.python filename="plain.py"} +plain_first = 1 +plain_second = 2 +``` + +```{=typst} +#pagebreak() +#v(8cm - 3em) +``` + +```{.python filename="annotated.py"} +annotated_first = 1 # <1> +annotated_second = 2 +``` + +1. The first line. + +```{=typst} +#pagebreak() +``` + +Text above the tall window. + +## Tall heading + +```{.python filename="tall.py"} +tall_0 = 0 +tall_1 = 1 +tall_2 = 2 +tall_3 = 3 +tall_4 = 4 +tall_5 = 5 +tall_6 = 6 +tall_7 = 7 +tall_8 = 8 +tall_9 = 9 +tall_10 = 10 +tall_11 = 11 +tall_12 = 12 +tall_13 = 13 +tall_14 = 14 +tall_15 = 15 +tall_16 = 16 +tall_17 = 17 +tall_18 = 18 +tall_19 = 19 +tall_20 = 20 +tall_21 = 21 +tall_22 = 22 +tall_23 = 23 +tall_24 = 24 +tall_25 = 25 +tall_26 = 26 +tall_27 = 27 +tall_28 = 28 +tall_29 = 29 +tall_30 = 30 +tall_31 = 31 +tall_32 = 32 +tall_33 = 33 +tall_34 = 34 +tall_35 = 35 +tall_36 = 36 +tall_37 = 37 +tall_38 = 38 +tall_39 = 39 +tall_40 = 40 +tall_41 = 41 +tall_42 = 42 +tall_43 = 43 +tall_44 = 44 +tall_45 = 45 +tall_46 = 46 +tall_47 = 47 +tall_48 = 48 +tall_49 = 49 +tall_50 = 50 +tall_51 = 51 +tall_52 = 52 +tall_53 = 53 +tall_54 = 54 +tall_55 = 55 +tall_56 = 56 +tall_57 = 57 +tall_58 = 58 +tall_59 = 59 +``` diff --git a/tests/run.sh b/tests/run.sh index 0bc9c75..05e1ebe 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -9,6 +9,8 @@ set -euo pipefail # fixtures rather than linked. docs/_scripts/sync-extension.sh solves the same # problem for the documentation site. # +# Requires: quarto, and pdftotext from poppler for the page-break test. +# # Usage: tests/run.sh tests_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -54,6 +56,15 @@ count_typst_windows() { grep -c '^#code-window(filename' "$1" || true } +# Succeed when one page of page-break-title.txt holds both strings. pdftotext +# ends each page with a form feed, so awk reads one page per record. +# $1 first string. $2 second string. +same_page() { + awk -v RS='\f' -v first="$1" -v second="$2" \ + 'index($0, first) && index($0, second) { found = 1 } END { exit !found }' \ + "${work_dir}/page-break-title.txt" +} + # Answer the classes of every highlighted block in a rendered HTML file. The # injected script names its marker classes in a selector, never in a tag, so # reading the tags alone keeps the script out of the answer. @@ -390,6 +401,60 @@ else "a box(fill: call inside the #theorem(title: [...]) line of theorem-title.typ" fi +# ============================================================================ +# A title bar never sits alone at the bottom of a page +# ============================================================================ + +# Each window in the fixture starts where the title bar fits on the page but +# the first line of code does not. The title bar has to move to the next page +# with its code. The plain window and the annotated window take different +# content paths in the wrapper, so both are checked. The test reads the page +# text of the PDF, so it needs pdftotext from poppler. +if command -v pdftotext >/dev/null 2>&1; then + render page-break-title typst + pdftotext "${work_dir}/page-break-title.pdf" "${work_dir}/page-break-title.txt" + + for window in plain annotated; do + if same_page "${window}.py" "${window}_first = 1"; then + report pass "page-break-title: the ${window} title bar stays with its code" + else + report fail "page-break-title: the ${window} title bar stays with its code" \ + "${window}.py and ${window}_first = 1 on the same page of page-break-title.pdf" + fi + done + + # A window taller than a page has to start on the page where it is placed + # and break across pages. The sticky title bar must not push it whole to + # the next page and leave a near-empty page behind. + if same_page "Text above the tall window." "tall_0 = 0"; then + report pass "page-break-title: a window taller than a page starts on the current page" + else + report fail "page-break-title: a window taller than a page starts on the current page" \ + "the text above tall.py and tall_0 = 0 on the same page of page-break-title.pdf" + fi + + # Without the sticky title bar, the same fixture has to leave the title bar + # alone on its page. This proves that the fixture still forces the break, + # so the checks above test the fix and not a layout that fits anyway. + lua_file="${work_dir}/_extensions/code-window/code-window.lua" + sed -i.aside '/sticky: true,/d' "${lua_file}" + render page-break-title typst + mv "${lua_file}.aside" "${lua_file}" + pdftotext "${work_dir}/page-break-title.pdf" "${work_dir}/page-break-title.txt" + + for window in plain annotated; do + if same_page "${window}.py" "${window}_first = 1"; then + report fail "page-break-title: the fixture orphans the ${window} title bar without the fix" \ + "${window}.py and ${window}_first = 1 on different pages without sticky: true" + else + report pass "page-break-title: the fixture orphans the ${window} title bar without the fix" + fi + done +else + report fail "page-break-title: the page text can be read" \ + "pdftotext on PATH (brew install poppler)" +fi + # ============================================================================ printf '\n%s passed, %s failed\n' "${passed}" "${failed}"