diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a926d5..165a998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- feat: Add the `code-window-auto-label` attribute, so another filter can ask for a window with its own label on a block that has no `filename`. The label works even when `auto-filename` is off. (#82) + ## 1.7.0 (2026-11-04) ### Breaking Changes diff --git a/_extensions/code-window/_modules/language.lua b/_extensions/code-window/_modules/language.lua index 59bdeef..d4763a7 100644 --- a/_extensions/code-window/_modules/language.lua +++ b/_extensions/code-window/_modules/language.lua @@ -6,6 +6,8 @@ local cell_output = require( quarto.utils.resolve_path('_modules/cell-output.lua'):gsub('%.lua$', '')) +local str = require( + quarto.utils.resolve_path('_vendor/quarto-lua-modules/string.lua'):gsub('%.lua$', '')) local M = {} @@ -31,6 +33,27 @@ local function is_known_language(lang) return is_known end +--- Read the label that names a block's window, set by this module, by the +--- author, or by another filter. An empty value counts as no label. +--- @param block pandoc.CodeBlock +--- @return string|nil label +function M.auto_label(block) + local label = block.attributes['code-window-auto-label'] + if str.is_empty(label) then + return nil + end + return label +end + +--- Set the label unless the block already has a filename or a label. +--- @param block pandoc.CodeBlock +--- @param label string +local function set_label(block, label) + if str.is_empty(block.attributes['filename']) and not M.auto_label(block) then + block.attributes['code-window-auto-label'] = 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 @@ -47,18 +70,14 @@ function M.CodeBlock(block) if not block.classes or #block.classes == 0 then block.classes:insert('default') - if not block.attributes['filename'] or block.attributes['filename'] == '' then - block.attributes['code-window-auto-label'] = 'default' - end + set_label(block, 'default') return block end local lang = block.classes[1] if not is_known_language(lang) then block.classes[1] = 'default' - if not block.attributes['filename'] or block.attributes['filename'] == '' then - block.attributes['code-window-auto-label'] = lang - end + set_label(block, lang) end return block diff --git a/_extensions/code-window/_schema.yml b/_extensions/code-window/_schema.yml index 2484de6..34003e3 100644 --- a/_extensions/code-window/_schema.yml +++ b/_extensions/code-window/_schema.yml @@ -71,6 +71,9 @@ attributes: type: boolean default: false description: "Suppress the auto-generated filename for this specific code block without changing the global setting." + code-window-auto-label: + type: string + description: "Frame this code block with this label, styled as an auto-generated filename, even when auto-filename is false." code-window-style: type: string enum: ["default", "macos", "windows"] diff --git a/_extensions/code-window/code-window.lua b/_extensions/code-window/code-window.lua index 287fb33..b699279 100644 --- a/_extensions/code-window/code-window.lua +++ b/_extensions/code-window/code-window.lua @@ -18,6 +18,7 @@ local meta_mod = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/m local pdoc = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/pandoc-helpers.lua'):gsub('%.lua$', '')) local html_mod = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/html.lua'):gsub('%.lua$', '')) local cell_output = require(quarto.utils.resolve_path('_modules/cell-output.lua'):gsub('%.lua$', '')) +local language = require(quarto.utils.resolve_path('_modules/language.lua'):gsub('%.lua$', '')) local code_annotations = nil local checker = nil @@ -146,7 +147,7 @@ end --- @param block pandoc.CodeBlock Code block element --- @return string|nil auto_label Label written by the language module, if any local function take_block_attributes(block) - local auto_label = block.attributes['code-window-auto-label'] + local auto_label = language.auto_label(block) local owned = {} for key, _ in pairs(block.attributes) do if key:sub(1, #ATTRIBUTE_PREFIX) == ATTRIBUTE_PREFIX and not cell_output.is_marker(key) then @@ -706,7 +707,7 @@ local function process_html(block, resolved, auto_label) return block end - if not CONFIG.auto_filename or overrides.no_auto_filename then + if overrides.no_auto_filename or not (auto_label or CONFIG.auto_filename) then return block end @@ -1036,10 +1037,12 @@ local function resolve_window_params(block, resolved, auto_label) local is_auto = false if (not filename or filename == '') and not overrides.no_auto_filename then - if CONFIG.auto_filename and block.classes and #block.classes > 0 then - -- Default/unknown/no-language blocks carry their label on - -- code-window-auto-label; everything else uses its language class. - filename = auto_label or block.classes[1] + -- A label asks for a window whatever auto-filename says. + if auto_label then + filename = auto_label + is_auto = true + elseif CONFIG.auto_filename and block.classes and #block.classes > 0 then + filename = block.classes[1] is_auto = true end end diff --git a/_extensions/code-window/main.lua b/_extensions/code-window/main.lua index 59727d4..77cbe79 100644 --- a/_extensions/code-window/main.lua +++ b/_extensions/code-window/main.lua @@ -79,12 +79,27 @@ end --- rewritten with no reader either. Answering that per block means relabelling --- where the name is built, which is a change to the two window paths rather --- than to this gate. +--- A block that carries "code-window-auto-label" asks for a window by itself, +--- so it takes the pass whatever "auto-filename" says. Without it, a block in +--- a language Pandoc cannot highlight would get no wrapper to frame. Such a +--- block that also refuses the window has no reader for the label, so it is +--- left alone here. --- @param block pandoc.CodeBlock --- @return pandoc.CodeBlock|nil Relabelled block, or nil when the pass is skipped local function normalise_language(block) - if not code_window.draws_chrome() or not code_window.CONFIG().auto_filename then + if not code_window.draws_chrome() then return nil end + if not code_window.CONFIG().auto_filename then + -- Lowercased to match the schema's boolean coercion. Resolving through the + -- schema check here would report each of its warnings twice per block. + local enabled = (block.attributes['code-window-enabled'] or ''):lower() + local no_auto_filename = (block.attributes['code-window-no-auto-filename'] or ''):lower() + local refuses_window = enabled == 'false' or no_auto_filename == 'true' + if not language.auto_label(block) or refuses_window then + return nil + end + end return language.CodeBlock(block) end diff --git a/docs/reference.qmd b/docs/reference.qmd index c560b9e..4e42542 100644 --- a/docs/reference.qmd +++ b/docs/reference.qmd @@ -47,6 +47,7 @@ extensions: | --- | --- | --- | | `code-window-enabled` | boolean | Whether this block gets chrome. Annotations still render when it does not. | | `code-window-no-auto-filename` | boolean | Suppress the derived filename for this block alone. | +| `code-window-auto-label` | string | Frame this block with this label, styled as a derived filename. Works when `auto-filename` is `false`. | | `code-window-style` | string | Override the style for this block: `macos`, `windows`, `default`. | | `code-window-collapse` | boolean or string | Fold this block: `open`, `closed`, `true`, or `false`. HTML only. | | `code-window-lines` | string | The line spec shown in the chip, such as `1,3-5`. Defaults to Quarto's `code-line-numbers`. | @@ -62,6 +63,23 @@ The result is the same window; it needs JavaScript, as the rest of the HTML chro Typst frames derived and author-set filenames alike, without a script. ::: +### Labels from another filter + +A filter that runs before Code Window can ask for a window on a block that has no `filename`. +Set `code-window-auto-label` on the block to the label that the title bar must show. + +```lua +block.attributes['code-window-auto-label'] = 'Terminal' +``` + +The label shows in the same style as a derived filename. +The block gets the window also when the document sets `auto-filename` to `false`. +These attributes win over the label: + +- A `filename` attribute names the window instead of the label. +- `code-window-enabled="false"` removes the window. +- `code-window-no-auto-filename="true"` removes the window. + ## Executed cells Quarto writes the output of an executed cell as a code block with no language. diff --git a/tests/fixtures/auto-label-forced.qmd b/tests/fixtures/auto-label-forced.qmd new file mode 100644 index 0000000..48370eb --- /dev/null +++ b/tests/fixtures/auto-label-forced.qmd @@ -0,0 +1,50 @@ +--- +title: "A label that asks for its own window" +filters: + - code-window +format: + html: default + typst: + keep-typ: true +extensions: + code-window: + auto-filename: false +--- + +The document derives no names, yet the first block names its own label, so it +gets a window with that label. Each block after it writes something that wins +over the label, so none of them gets the label in its title bar. + +```{.bash code-window-auto-label="Terminal"} +echo forced +``` + +```{.bash filename="run.sh" code-window-auto-label="Terminal"} +echo explicit +``` + +```{.bash code-window-enabled="false" code-window-auto-label="Terminal"} +echo disabled +``` + +```{.bash code-window-no-auto-filename="true" code-window-auto-label="Terminal"} +echo refused +``` + +A labelled block in a language Pandoc cannot highlight still goes through the +language pass, so it is framed like any other labelled block. + +```{.foo code-window-auto-label="Shell"} +x = 1 +``` + +A labelled block that turns the window off keeps the language its author wrote, +because no window reads the label. + +```{.foo code-window-no-auto-filename="true" code-window-auto-label="Terminal"} +x = 2 +``` + +```{.foo code-window-enabled="False" code-window-auto-label="Terminal"} +x = 3 +``` diff --git a/tests/run.sh b/tests/run.sh index 05e1ebe..15b07c4 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -455,6 +455,55 @@ else "pdftotext on PATH (brew install poppler)" fi +# ============================================================================ +# A label set on a block asks for a window of its own +# ============================================================================ + +# Another filter sets code-window-auto-label to get a window with a derived +# style on a block that names no file. The label has to work when the document +# derives no names, and an explicit filename or a per-block opt-out still wins. +render auto-label-forced html +forced_html="${work_dir}/auto-label-forced.html" +forced_labels="$(block_wrappers "${forced_html}" | grep -c 'data-filename="Terminal"' || true)" +forced_auto="$(block_classes "${forced_html}" | grep -c 'cw-auto' || true)" +if [ "${forced_labels}" -eq 1 ] && [ "${forced_auto}" -eq 2 ] && + grep -q 'data-filename="run.sh"' "${forced_html}"; then + report pass "auto-label-forced: only the labelled block takes the label in HTML" +else + report fail "auto-label-forced: only the labelled block takes the label in HTML" \ + "one data-filename=\"Terminal\", two cw-auto blocks, and data-filename=\"run.sh\", got ${forced_labels} and ${forced_auto}" +fi + +render auto-label-forced typst +forced_typst="${work_dir}/auto-label-forced.typ" +if [ "$(count_typst_windows "${forced_typst}")" -eq 3 ] && + grep -q '^#code-window(filename: "Terminal", is-auto: true' "${forced_typst}" && + grep -q '^#code-window(filename: "run.sh", is-auto: false' "${forced_typst}"; then + report pass "auto-label-forced: only the labelled block takes the label in Typst" +else + report fail "auto-label-forced: only the labelled block takes the label in Typst" \ + "three code-window calls, with one for Terminal with is-auto: true and one for run.sh" +fi + +# The language pass runs for a labelled block and keeps its label, or the +# label would read "foo". +if block_wrappers "${forced_html}" | grep -q 'data-filename="Shell"'; then + report pass "auto-label-forced: a labelled block in an unknown language is framed" +else + report fail "auto-label-forced: a labelled block in an unknown language is framed" \ + "data-filename=\"Shell\" on a sourceCode wrapper in auto-label-forced.html" +fi + +# A labelled block that refuses the window has no reader for the label, so the +# language pass leaves its language alone. +opted_out="$(block_classes "${forced_html}" | grep -c '"foo' || true)" +if [ "${opted_out}" -eq 2 ]; then + report pass "auto-label-forced: a labelled block that refuses the window keeps its language" +else + report fail "auto-label-forced: a labelled block that refuses the window keeps its language" \ + "two blocks with a foo class in auto-label-forced.html, got ${opted_out}" +fi + # ============================================================================ printf '\n%s passed, %s failed\n' "${passed}" "${failed}"