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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 44 additions & 20 deletions _extensions/code-window/_modules/language.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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$', ''))
Expand All @@ -13,12 +13,16 @@ local M = {}

local known_language_cache = {}

--- 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 for the sourceCode class.
--- Renders a test CodeBlock to HTML and checks that Pandoc names a language
--- 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

Expand All @@ -28,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('sourceCode') ~= nil
local is_known = html:find('<code%s+class="sourceCode%s+[^"]') ~= nil
known_language_cache[lang] = is_known
return is_known
end
Expand All @@ -54,32 +58,52 @@ local function set_label(block, label)
end
end

--- Normalise code blocks with no or unknown language class to "default".
--- Both are framed like a highlighted code cell rather than left as a bare
--- block: a no-language block is labelled "default", and an unknown language
--- keeps its original token as the label. 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.
--- Find the class that Pandoc highlights the block with: the first class it
--- knows as a language, which need not be the first class.
--- @param classes pandoc.List
--- @return integer|nil index
local function find_language(classes)
for index, class in ipairs(classes) do
if is_known_language(class) then
return index
end
end
return nil
end

--- Find the label for a block with no language: the first class that is not a
--- line-numbering class.
--- @param classes pandoc.List
--- @return string|nil label
local function find_label(classes)
for _, class in ipairs(classes) do
if not NUMBERING_CLASSES[class] then
return class
end
end
return nil
end

--- 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)
if cell_output.is_marked(block) then
return nil
end

if not block.classes or #block.classes == 0 then
block.classes:insert('default')
set_label(block, 'default')
local index = find_language(block.classes)
if index then
block.classes:insert(1, block.classes:remove(index))
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

Expand Down
2 changes: 2 additions & 0 deletions docs/examples.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ 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, 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.
Expand Down
31 changes: 31 additions & 0 deletions tests/fixtures/language-not-first.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
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. Its window is
named after its first class that does not turn on line numbers.

```{.numberLines}
x = 1
```

```{.numberLines .foo}
x = 2
```
4 changes: 2 additions & 2 deletions tests/fixtures/language-relabelled.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 36 additions & 2 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ============================================================================
Expand Down Expand Up @@ -504,6 +504,40 @@ 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_wrappers "${not_first_html}" | grep -q 'data-filename="foo"' &&
[ "$(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 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
not_first_typ="${work_dir}/language-not-first.typ"
if [ "$(count_typst_windows "${not_first_typ}")" -eq 4 ] &&
[ "$(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" \
"four windows, two of them named bash, in language-not-first.typ"
fi

# ============================================================================

printf '\n%s passed, %s failed\n' "${passed}" "${failed}"
Expand Down
Loading