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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
38 changes: 30 additions & 8 deletions _extensions/code-window/code-window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -1308,4 +1329,5 @@ return {
Pandoc = Pandoc,
CodeBlock = CodeBlock,
CONFIG = function() return CONFIG end,
draws_chrome = draws_chrome,
}
45 changes: 38 additions & 7 deletions _extensions/code-window/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
-- ============================================================================
Expand Down Expand Up @@ -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 },
}
Expand Down
5 changes: 3 additions & 2 deletions docs/examples.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions tests/fixtures/auto-filename-off.qmd
Original file line number Diff line number Diff line change
@@ -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
```
13 changes: 13 additions & 0 deletions tests/fixtures/filter-disabled-no-language.qmd
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions tests/fixtures/filter-disabled.qmd
Original file line number Diff line number Diff line change
@@ -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
```
13 changes: 13 additions & 0 deletions tests/fixtures/language-relabelled.qmd
Original file line number Diff line number Diff line change
@@ -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
```
6 changes: 3 additions & 3 deletions tests/fixtures/unsupported-format.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 59 additions & 2 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand All @@ -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}"
Expand Down
Loading