From 4d6e55fd770eb1c81b130ca3b7af5840c17c8c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 5 Nov 2026 10:13:13 +0100 Subject: [PATCH 1/6] fix: take the language from the first known class Pandoc highlights a block with the first class it knows as a language, but the language pass read only the first class. A block such as {.terminal .bash} lost its language to default and was named after terminal. The probe also took numberLines, number-lines and number for languages, because Pandoc writes the sourceCode class on any numbered block. The pass now moves the first class Pandoc can highlight to the front. A block with none gets default in front of its own classes, so line numbers survive. --- _extensions/code-window/_modules/language.lua | 72 ++++++++++++++----- docs/examples.qmd | 3 + tests/fixtures/language-not-first.qmd | 26 +++++++ tests/fixtures/language-relabelled.qmd | 4 +- tests/run.sh | 33 +++++++++ 5 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 tests/fixtures/language-not-first.qmd diff --git a/_extensions/code-window/_modules/language.lua b/_extensions/code-window/_modules/language.lua index d4763a7..3624008 100644 --- a/_extensions/code-window/_modules/language.lua +++ b/_extensions/code-window/_modules/language.lua @@ -2,7 +2,7 @@ --- @license MIT --- @copyright 2026 Mickaël Canouil --- @author Mickaël Canouil ---- @brief Normalise code blocks with no or unknown language class. +--- @brief Put the language class of a code block first, or add one. local cell_output = require( quarto.utils.resolve_path('_modules/cell-output.lua'):gsub('%.lua$', '')) @@ -13,8 +13,14 @@ local M = {} local known_language_cache = {} +--- Classes that turn on line numbers in Pandoc. They are not languages, and a +--- block that has only these classes has no language. +local NUMBERING_CLASSES = { ['numberLines'] = true, ['number-lines'] = true, ['number'] = true } + --- Check if a language is recognised by Pandoc's syntax highlighter. ---- Renders a test CodeBlock to HTML and checks for the sourceCode class. +--- Renders a test CodeBlock to HTML and checks that Pandoc names a language +--- on the code element. The sourceCode class alone is not enough, because +--- Pandoc also writes it for a numbered block that it does not highlight. --- @param lang string Language identifier --- @return boolean local function is_known_language(lang) @@ -28,7 +34,7 @@ local function is_known_language(lang) local test_block = pandoc.CodeBlock('x', pandoc.Attr('', { lang })) local html = pandoc.write(pandoc.Pandoc({ test_block }), 'html') - local is_known = html:find('sourceCode') ~= nil + local is_known = html:find(' 1 then + block.classes:insert(1, block.classes:remove(index)) + end return block end - local lang = block.classes[1] - if not is_known_language(lang) then - block.classes[1] = 'default' - set_label(block, lang) - end - + set_label(block, find_label(block.classes) or 'default') + block.classes:insert(1, 'default') return block end diff --git a/docs/examples.qmd b/docs/examples.qmd index a0a71b2..21dfe6f 100644 --- a/docs/examples.qmd +++ b/docs/examples.qmd @@ -46,6 +46,9 @@ print("hello") The title bar then reads `PYTHON`. A derived name is drawn smaller and in capitals, so a reader can tell it from a file that exists on disk. A block with no language at all is labelled `DEFAULT`, and a block whose language Pandoc cannot highlight keeps its own token as the label. +The language is the first class that Pandoc can highlight, which is also the class Pandoc highlights the block with. +So `{.terminal .bash}` is labelled `BASH`, the same as `{.bash .terminal}`. +Line-numbering classes such as `numberLines` are never the language. This section shows source only. The site sets `auto-filename: false`, so it frames no block that leaves `filename` out. diff --git a/tests/fixtures/language-not-first.qmd b/tests/fixtures/language-not-first.qmd new file mode 100644 index 0000000..3a7ffe0 --- /dev/null +++ b/tests/fixtures/language-not-first.qmd @@ -0,0 +1,26 @@ +--- +title: "A language class that is not the first class" +filters: + - code-window +format: + html: default + typst: + keep-typ: true +--- + +Pandoc highlights a block with the first class it knows as a language, which +need not be the first class. The window is named after that same class. + +```{.terminal .bash} +echo terminal +``` + +```{.numberLines .bash} +echo numbered +``` + +A block with line numbers and no language keeps its line numbers. + +```{.numberLines} +x = 1 +``` diff --git a/tests/fixtures/language-relabelled.qmd b/tests/fixtures/language-relabelled.qmd index 84d77a6..531cc92 100644 --- a/tests/fixtures/language-relabelled.qmd +++ b/tests/fixtures/language-relabelled.qmd @@ -5,8 +5,8 @@ filters: --- The filter labels a block whose language Pandoc cannot highlight, and frames it -like any other. The label keeps the language the author wrote, and the class -becomes `default`, which is the one Pandoc has a theme for. +like any other. The label keeps the language the author wrote, and the block +gains a `default` class in front, which is the one Pandoc has a theme for. ```foo x = 1 diff --git a/tests/run.sh b/tests/run.sh index 15b07c4..40f1c8e 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -504,6 +504,39 @@ else "two blocks with a foo class in auto-label-forced.html, got ${opted_out}" fi +# ============================================================================ +# The language is the first class Pandoc can highlight +# ============================================================================ + +render language-not-first html +not_first_html="${work_dir}/language-not-first.html" +named_bash="$(block_wrappers "${not_first_html}" | grep -c 'data-filename="bash"' || true)" +if [ "${named_bash}" -eq 2 ] && + ! block_classes "${not_first_html}" | grep -q 'default bash'; then + report pass "language-not-first: a later language class names and highlights the block" +else + report fail "language-not-first: a later language class names and highlights the block" \ + "two wrappers with data-filename=\"bash\" and no default class before bash, got ${named_bash}" +fi + +if block_wrappers "${not_first_html}" | grep -q 'data-filename="default"' && + [ "$(block_classes "${not_first_html}" | grep -c 'numberLines')" -eq 2 ]; then + report pass "language-not-first: a numbered block with no language keeps its numbers" +else + report fail "language-not-first: a numbered block with no language keeps its numbers" \ + "a numberLines class on both numbered blocks, and data-filename=\"default\" on one" +fi + +render language-not-first typst +not_first_typ="${work_dir}/language-not-first.typ" +if [ "$(grep -c '^#code-window(filename: "bash"' "${not_first_typ}" || true)" -eq 2 ] && + [ "$(grep -c '^#code-window(filename: "default"' "${not_first_typ}" || true)" -eq 1 ]; then + report pass "language-not-first: the Typst windows are named after the language" +else + report fail "language-not-first: the Typst windows are named after the language" \ + "two windows named bash and one named default in language-not-first.typ" +fi + # ============================================================================ printf '\n%s passed, %s failed\n' "${passed}" "${failed}" From d3d0275d61e71287966ba3af4741c14a21f6f598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 5 Nov 2026 10:15:40 +0100 Subject: [PATCH 2/6] refactor: tighten the language pass and its tests --- _extensions/code-window/_modules/language.lua | 28 ++++++------------- docs/examples.qmd | 3 +- tests/run.sh | 6 ++-- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/_extensions/code-window/_modules/language.lua b/_extensions/code-window/_modules/language.lua index 3624008..2611859 100644 --- a/_extensions/code-window/_modules/language.lua +++ b/_extensions/code-window/_modules/language.lua @@ -13,18 +13,16 @@ local M = {} local known_language_cache = {} ---- Classes that turn on line numbers in Pandoc. They are not languages, and a ---- block that has only these classes has no language. +--- Classes that turn on line numbers in Pandoc. They are not languages. local NUMBERING_CLASSES = { ['numberLines'] = true, ['number-lines'] = true, ['number'] = true } --- Check if a language is recognised by Pandoc's syntax highlighter. --- Renders a test CodeBlock to HTML and checks that Pandoc names a language ---- on the code element. The sourceCode class alone is not enough, because ---- Pandoc also writes it for a numbered block that it does not highlight. +--- on the code element, not only the sourceCode class. --- @param lang string Language identifier --- @return boolean local function is_known_language(lang) - if not lang or lang == '' then + if not lang or lang == '' or NUMBERING_CLASSES[lang] then return false end @@ -86,17 +84,11 @@ local function find_label(classes) return nil end ---- Put the language class first, or add "default" when the block has none. ---- The window is named after the first class, and Pandoc highlights with the ---- first class it knows, so the language class is moved to the front and both ---- read the same class. A block with no language is framed like a highlighted ---- code cell rather than left as a bare block: "default" is added in front of ---- its classes, so that the other classes, line numbers among them, still ---- apply. The block is labelled with its first class that is not a ---- line-numbering class, or with "default" when it has none. The label is ---- carried on the `code-window-auto-label` attribute (not `filename`, which is ---- reserved for author-set filenames) and consumed by the auto-filename ---- windowing path. The output of an executed cell is left as Quarto wrote it. +--- Put the language class first, or add "default" in front when there is none. +--- The window is named after the first class, so it then names the class that +--- Pandoc highlights with. The label goes on `code-window-auto-label`, not on +--- `filename`, which is for author-set names. The output of an executed cell +--- is left as Quarto wrote it. --- @param block pandoc.CodeBlock --- @return pandoc.CodeBlock|nil function M.CodeBlock(block) @@ -106,9 +98,7 @@ function M.CodeBlock(block) local index = find_language(block.classes) if index then - if index > 1 then - block.classes:insert(1, block.classes:remove(index)) - end + block.classes:insert(1, block.classes:remove(index)) return block end diff --git a/docs/examples.qmd b/docs/examples.qmd index 21dfe6f..411de5d 100644 --- a/docs/examples.qmd +++ b/docs/examples.qmd @@ -46,8 +46,7 @@ print("hello") The title bar then reads `PYTHON`. A derived name is drawn smaller and in capitals, so a reader can tell it from a file that exists on disk. A block with no language at all is labelled `DEFAULT`, and a block whose language Pandoc cannot highlight keeps its own token as the label. -The language is the first class that Pandoc can highlight, which is also the class Pandoc highlights the block with. -So `{.terminal .bash}` is labelled `BASH`, the same as `{.bash .terminal}`. +The language is the first class that Pandoc can highlight, so `{.terminal .bash}` is labelled `BASH`, the same as `{.bash .terminal}`. Line-numbering classes such as `numberLines` are never the language. This section shows source only. diff --git a/tests/run.sh b/tests/run.sh index 40f1c8e..fd85f14 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -529,12 +529,12 @@ fi render language-not-first typst not_first_typ="${work_dir}/language-not-first.typ" -if [ "$(grep -c '^#code-window(filename: "bash"' "${not_first_typ}" || true)" -eq 2 ] && - [ "$(grep -c '^#code-window(filename: "default"' "${not_first_typ}" || true)" -eq 1 ]; then +if [ "$(count_typst_windows "${not_first_typ}")" -eq 3 ] && + [ "$(grep -c '^#code-window(filename: "bash"' "${not_first_typ}" || true)" -eq 2 ]; then report pass "language-not-first: the Typst windows are named after the language" else report fail "language-not-first: the Typst windows are named after the language" \ - "two windows named bash and one named default in language-not-first.typ" + "three windows, two of them named bash, in language-not-first.typ" fi # ============================================================================ From 40805afca11b6ee241e97d0a2793c3cd122f8fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 5 Nov 2026 10:23:23 +0100 Subject: [PATCH 3/6] fix: let the language probe match a wrapped tag --- _extensions/code-window/_modules/language.lua | 2 +- tests/fixtures/language-not-first.qmd | 7 ++++++- tests/run.sh | 9 +++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/_extensions/code-window/_modules/language.lua b/_extensions/code-window/_modules/language.lua index 2611859..20f09e4 100644 --- a/_extensions/code-window/_modules/language.lua +++ b/_extensions/code-window/_modules/language.lua @@ -32,7 +32,7 @@ local function is_known_language(lang) local test_block = pandoc.CodeBlock('x', pandoc.Attr('', { lang })) local html = pandoc.write(pandoc.Pandoc({ test_block }), 'html') - local is_known = html:find(' Date: Thu, 5 Nov 2026 10:30:33 +0100 Subject: [PATCH 4/6] test: check line numbers through numberSource --- tests/run.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/run.sh b/tests/run.sh index 42b89e1..e2e5ba8 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -512,7 +512,7 @@ render language-not-first html not_first_html="${work_dir}/language-not-first.html" named_bash="$(block_wrappers "${not_first_html}" | grep -c 'data-filename="bash"' || true)" if [ "${named_bash}" -eq 2 ] && - ! block_classes "${not_first_html}" | grep -q 'default bash'; then + ! block_classes "${not_first_html}" | grep -q 'default.*bash'; then report pass "language-not-first: a later language class names and highlights the block" else report fail "language-not-first: a later language class names and highlights the block" \ @@ -521,11 +521,11 @@ fi if block_wrappers "${not_first_html}" | grep -q 'data-filename="default"' && block_wrappers "${not_first_html}" | grep -q 'data-filename="foo"' && - [ "$(block_classes "${not_first_html}" | grep -c 'numberLines')" -eq 3 ]; then + [ "$(block_classes "${not_first_html}" | grep -c 'numberSource')" -eq 3 ]; then report pass "language-not-first: a numbered block with no language keeps its numbers" else report fail "language-not-first: a numbered block with no language keeps its numbers" \ - "a numberLines class on the three numbered blocks, data-filename=\"default\" on one and data-filename=\"foo\" on another" + "a numberSource class, which Pandoc writes on a numbered block, on the three numbered blocks, data-filename=\"default\" on one and data-filename=\"foo\" on another" fi render language-not-first typst From 15a5be02723ae095b94410bcf9286434daaeb83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 5 Nov 2026 10:39:50 +0100 Subject: [PATCH 5/6] test: pin the class kept for an unknown language --- tests/run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/run.sh b/tests/run.sh index e2e5ba8..d6d17ed 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -237,13 +237,13 @@ fi # language the author wrote. Without this, the two tests above would stay green # if the gate ever closed on a render it should let through. render language-relabelled html -if block_classes "${work_dir}/language-relabelled.html" | grep -q 'default' && +if block_classes "${work_dir}/language-relabelled.html" | grep -q 'default foo' && block_classes "${work_dir}/language-relabelled.html" | grep -q 'cw-auto' && block_wrappers "${work_dir}/language-relabelled.html" | grep -q 'data-filename="foo"'; then report pass "language-relabelled: the block is relabelled and framed" else report fail "language-relabelled: the block is relabelled and framed" \ - "a default class, a cw-auto class, and data-filename=\"foo\" on the block" + "a default class before the foo class, a cw-auto class, and data-filename=\"foo\" on the block" fi # ============================================================================ From e6f6aa4310d61620f56be8cb9815509d0ab14625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 5 Nov 2026 10:40:03 +0100 Subject: [PATCH 6/6] docs: add changelog entry for the language fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efe2b0a..24ee76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- fix: Use the first class that Pandoc can highlight as the language of a block, so `{.terminal .bash}` is labelled and highlighted as `bash`. Line-numbering classes such as `numberLines` are no longer taken for a language. (#84) + ## 1.8.0 (2026-11-04) ### New Features