diff --git a/CHANGELOG.md b/CHANGELOG.md index 4db0b6d..ecd6d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Bug Fixes +- fix: Keep a block's own language where the filter derives no name for it, which means the filter is off, or `auto-filename` is off, or the output format is neither HTML nor Typst. The block was relabelled as `default` before, and the language the author wrote was lost. (#66) - fix: Keep inline code in a Typst document as a code element, so Pandoc writes the syntax highlighting definitions for it. A document with inline code and no code block failed to compile before. (#65) - fix: Remove the code-window attributes from a block that sets code-window-enabled="false", and from a block that sets code-window-lines when lines-label is off. They reached the HTML output as data-code-window-* attributes before. (#63) - fix: Remove the extension's own label attribute when the output format gets no window chrome, so a writer that keeps attributes no longer prints it. (#63) diff --git a/_extensions/code-window/code-window.lua b/_extensions/code-window/code-window.lua index 851f6a2..ad65751 100644 --- a/_extensions/code-window/code-window.lua +++ b/_extensions/code-window/code-window.lua @@ -72,6 +72,23 @@ local ANNOTATION_BLOCK_COUNTER = 0 -- CELL OUTPUT -- ============================================================================ +--- Check whether the extension acts on the format being rendered. It draws +--- chrome for html, which covers Reveal.js, and for typst, and leaves every +--- other format as Quarto writes it. +--- @return boolean +local function acts_on_format() + return CURRENT_FORMAT == 'html' or CURRENT_FORMAT == 'typst' +end + +--- Check whether this render draws chrome at all: the extension is on, and the +--- format is one it acts on. Every pass that exists only to serve the chrome +--- asks this before it does any work, so none of them has to carry its own +--- copy of the two conditions. +--- @return boolean +local function draws_chrome() + return CONFIG ~= nil and CONFIG.enabled and acts_on_format() +end + --- Check whether a block holds the output of an executed cell that the engine --- did not name. Such a block keeps the shape Quarto gave it. --- @param block pandoc.CodeBlock Code block element @@ -749,9 +766,9 @@ function Meta(meta) -- This is the pass that reads the configuration, so the check runs here, -- before the first option is read. An option the check rejects is still -- read below, because the report says what the extension cannot use and the - -- document renders either way. The extension only acts on html and typst, - -- so the check is gated on the same union those formats already use below. - if CURRENT_FORMAT == 'html' or CURRENT_FORMAT == 'typst' then + -- document renders either way. Only a format the extension acts on reports, + -- because nothing it could say applies anywhere else. + if acts_on_format() then checker:options(meta) end @@ -902,9 +919,12 @@ function CodeBlock(block) -- A filter that draws nothing changes nothing an author wrote. The -- attributes it would read stay on the block and reach the output, which is -- also what a document with this extension not installed produces. Only - -- code-window-auto-label goes, because the language module wrote it and no - -- author did. This holds for a filter switched off, below, and for a format - -- the extension does not act on, at the end of this function. + -- code-window-auto-label goes. The language module no longer writes it in + -- either of the two branches that clear it, since it asks the same question + -- before it runs, so what is left to clear is a document that wrote the + -- extension's own attribute name on a fence by hand. This holds for a filter + -- switched off, below, and for a format the extension does not act on, at + -- the end of this function. if not CURRENT_FORMAT or not CONFIG or not CONFIG.enabled then checker:attributes(block.attributes, 'CodeBlock') block.attributes['code-window-auto-label'] = nil @@ -921,8 +941,9 @@ function CodeBlock(block) -- Typst is finished by the Pandoc filter ahead of this one, which takes the -- attributes off there. Every other format draws no chrome, so the block - -- keeps what its author wrote and loses only the language module's label, - -- which a writer that preserves attributes would otherwise print. + -- keeps what its author wrote and loses only the label, which a writer that + -- preserves attributes would otherwise print. Nothing writes that label here + -- any more, for the reason given above, so this guards a hand-written one. block.attributes['code-window-auto-label'] = nil return block end @@ -1308,4 +1329,5 @@ return { Pandoc = Pandoc, CodeBlock = CodeBlock, CONFIG = function() return CONFIG end, + draws_chrome = draws_chrome, } diff --git a/_extensions/code-window/main.lua b/_extensions/code-window/main.lua index 1f9570f..3f691a4 100644 --- a/_extensions/code-window/main.lua +++ b/_extensions/code-window/main.lua @@ -42,21 +42,52 @@ code_window.set_checker(checker) -- ============================================================================ --- Mark the code blocks that hold the output of an executed cell, so the later ---- passes leave them as Quarto wrote them. Reads the configuration once and ---- walks the document only when the output has to stay unframed. The language ---- pass runs whether the extension is on or off, so the mark is set in both ---- cases; the window passes remove it either way. +--- passes leave them as Quarto wrote them. Walks the document only when there +--- is a pass to hold back, which means this render draws chrome and the output +--- has to stay unframed. Every reader that acts on the mark asks draws_chrome +--- first: the language pass below, and the two window paths. CodeBlock reads it +--- too, but only to remove it, and a mark that was never set costs nothing +--- there. So a render that draws no chrome would walk the whole document to set +--- an attribute nothing goes on to act on. draws_chrome answers false when +--- there is no configuration yet, so the second test below always has one in +--- hand. --- @param doc pandoc.Pandoc --- @return pandoc.Pandoc|nil Marked document, or nil when the pass is skipped local function mark_cell_output(doc) - local config = code_window.CONFIG() - if not config or (config.enabled and config.cell_output) then + if not code_window.draws_chrome() or code_window.CONFIG().cell_output then return nil end doc.blocks = doc.blocks:walk({ Div = cell_output.Div }) return doc end +-- ============================================================================ +-- LANGUAGE +-- ============================================================================ + +--- Normalise a block's language where the render draws chrome. +--- The pass labels a block whose language Pandoc cannot highlight, and the +--- derived filename is the only reader of that label. Nothing derives a +--- filename in a render that draws no chrome, so the pass would rewrite a +--- class for nobody and hand the author back a language they did not write. +--- "auto-filename" belongs in the same question, because it is the reader +--- itself: with no derived name to build, both window paths return before they +--- read the label, so the pass would rewrite a class for nobody again. +--- Every question this asks is about the render, not about one block. A block +--- can still draw no chrome inside a render that does, through +--- "code-window-no-auto-filename" or "code-window-enabled", and its class is +--- 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. +--- @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 + return nil + end + return language.CodeBlock(block) +end + -- ============================================================================ -- SKYLIGHTING HOT-FIX -- ============================================================================ @@ -88,7 +119,7 @@ end local filters = { { Meta = code_window.Meta }, { Pandoc = mark_cell_output }, - { CodeBlock = language.CodeBlock }, + { CodeBlock = normalise_language }, { Pandoc = code_window.Pandoc }, { CodeBlock = code_window.CodeBlock }, } diff --git a/docs/examples.qmd b/docs/examples.qmd index 4e14cbe..ebb5c37 100644 --- a/docs/examples.qmd +++ b/docs/examples.qmd @@ -165,8 +165,9 @@ It then adds no chrome to any block, and loads neither the stylesheet nor the sc So `code-window-enabled="true"` on a block cannot switch the chrome back on. `code-window-enabled` can only turn a block off, never on. -Switching the filter off does not put the document back exactly as Quarto would write it. -The pass that normalises a block's language runs either way, so a block with no language still gains the `default` class. +With the filter off, every code block keeps the classes Quarto gives it. +A block whose language Pandoc cannot highlight keeps that language, and a block with no language gains none. +The attributes the filter reads stay on the block too, and reach the HTML as `data-code-window-*`, which is what a document without the extension also produces. A block that names a file still gets the plain title bar Quarto builds for it. That bar carries no traffic lights, no fold, and no line chip. diff --git a/tests/fixtures/auto-filename-off.qmd b/tests/fixtures/auto-filename-off.qmd new file mode 100644 index 0000000..56bbd28 --- /dev/null +++ b/tests/fixtures/auto-filename-off.qmd @@ -0,0 +1,16 @@ +--- +title: "No derived names anywhere" +filters: + - code-window +extensions: + code-window: + auto-filename: false +--- + +With no derived name to build, nothing reads the label, so the block below keeps +the language its author wrote even though the filter is on and the format gets +chrome. + +```foo +x = 1 +``` diff --git a/tests/fixtures/filter-disabled-no-language.qmd b/tests/fixtures/filter-disabled-no-language.qmd new file mode 100644 index 0000000..4f9e3e6 --- /dev/null +++ b/tests/fixtures/filter-disabled-no-language.qmd @@ -0,0 +1,13 @@ +--- +title: "The filter turned off, and a block with no language" +filters: + - code-window +extensions: + code-window: + enabled: false +--- + +A block with no language gains none while the filter is off. The block below is +indented rather than fenced, which gives the same code block with no language. + + x = 1 diff --git a/tests/fixtures/filter-disabled.qmd b/tests/fixtures/filter-disabled.qmd new file mode 100644 index 0000000..cc8693e --- /dev/null +++ b/tests/fixtures/filter-disabled.qmd @@ -0,0 +1,16 @@ +--- +title: "The filter turned off" +filters: + - code-window +extensions: + code-window: + enabled: false +--- + +A filter that is off leaves every block as Quarto writes it. The block below +keeps the language its author gave it, even though Pandoc cannot highlight it +and the filter would relabel it when it is on. + +```foo +x = 1 +``` diff --git a/tests/fixtures/language-relabelled.qmd b/tests/fixtures/language-relabelled.qmd new file mode 100644 index 0000000..84d77a6 --- /dev/null +++ b/tests/fixtures/language-relabelled.qmd @@ -0,0 +1,13 @@ +--- +title: "A language Pandoc cannot highlight" +filters: + - code-window +--- + +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. + +```foo +x = 1 +``` diff --git a/tests/fixtures/unsupported-format.qmd b/tests/fixtures/unsupported-format.qmd index e5e1b72..5c7b9a7 100644 --- a/tests/fixtures/unsupported-format.qmd +++ b/tests/fixtures/unsupported-format.qmd @@ -4,9 +4,9 @@ filters: - code-window --- -The filter draws no chrome outside HTML and Typst. The language module still -labels the block below, and that label is the filter's own, so it does not -reach the output. +The filter draws no chrome outside HTML and Typst. Nothing reads a label here, +so the block below keeps the language its author wrote, and the filter's own +label for it is never written at all. ```foo x = 1 diff --git a/tests/run.sh b/tests/run.sh index 0b5c929..31e0e19 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -168,8 +168,9 @@ for fixture in disabled-block lines-label-off; do fi done -# The label the language module writes is the filter's own, so a format that -# draws no chrome drops it rather than printing it. +# The label is the filter's own name for a block, and no reader of it exists on +# a format that draws no chrome, so neither the label nor the pass that writes +# it reaches the output. render unsupported-format markdown if grep -q 'code-window-auto-label' "${work_dir}/unsupported-format.md"; then report fail "unsupported-format: the internal label stays out of the output" \ @@ -178,6 +179,62 @@ else report pass "unsupported-format: the internal label stays out of the output" fi +# ============================================================================ +# A filter that draws no chrome leaves a block's language alone +# ============================================================================ + +# The pass that relabels a language serves the derived filename, and nothing +# derives a filename with the filter off. +render filter-disabled html +if block_classes "${work_dir}/filter-disabled.html" | grep -q 'foo'; then + report pass "filter-disabled: the block keeps its own language" +else + report fail "filter-disabled: the block keeps its own language" \ + "a foo class on the block" +fi + +# The other branch of the same pass inserts a class where the block had none, +# which turns a bare block into a highlighted one. +render filter-disabled-no-language html +if block_classes "${work_dir}/filter-disabled-no-language.html" | grep -q 'default'; then + report fail "filter-disabled-no-language: the block gains no class" \ + "no default class on the block" +else + report pass "filter-disabled-no-language: the block gains no class" +fi + +# A render with no derived name to build reads no label either, whatever the +# format, so the pass has no reader there. +render auto-filename-off html +if block_classes "${work_dir}/auto-filename-off.html" | grep -q 'foo'; then + report pass "auto-filename-off: the block keeps its own language" +else + report fail "auto-filename-off: the block keeps its own language" \ + "a foo class on the block" +fi + +# The same pass serves no reader on a format that gets no chrome either. +if grep -q '^``` foo' "${work_dir}/unsupported-format.md"; then + report pass "unsupported-format: the block keeps its own language" +else + report fail "unsupported-format: the block keeps its own language" \ + "a fence reading \`\`\` foo in unsupported-format.md" +fi + +# Where the chrome is drawn, the pass has work to do: the class becomes the one +# Pandoc has a theme for, the block is framed, and the title bar keeps the +# 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' && + 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" +fi + # ============================================================================ printf '\n%s passed, %s failed\n' "${passed}" "${failed}"