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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

### New Features

- feat: Add the `cell-output` option. The output of an executed cell is no longer framed, and `cell-output: true` frames it again. (#39)

### Bug Fixes

- fix: Remove the internal `code-window-auto-label` attribute from the rendered document.

## 1.3.3 (2026-08-02)

### Refactoring
Expand Down
57 changes: 57 additions & 0 deletions _extensions/code-window/_modules/cell-output.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
--- @module "cell-output"
--- @license MIT
--- @copyright 2026 Mickaël Canouil
--- @author Mickaël Canouil
--- @brief Mark the code blocks that hold the output of an executed cell.
--- @description Quarto puts the result of an executed cell in a Div with the
--- `cell-output` classes, and the code block inside carries no language class.
--- A CodeBlock filter sees that block on its own and cannot tell it apart from
--- a fenced block written without a language. This pass marks it while the
--- parent Div is still in reach, so the language and window passes can leave it
--- alone.

local M = {}

local MARKER = 'code-window-cell-output'

--- Check whether a Div holds the output of an executed cell.
--- @param div pandoc.Div
--- @return boolean
local function is_cell_output(div)
for _, class in ipairs(div.classes) do
if class == 'cell-output' or class:match('^cell%-output%-') then
return true
end
end
return false
end

--- Mark every code block inside a cell-output Div.
--- @param div pandoc.Div
--- @return pandoc.Div|nil Marked Div, or nil when the Div is left as it is
function M.Div(div)
if not is_cell_output(div) then
return nil
end
return div:walk({
CodeBlock = function(block)
block.attributes[MARKER] = 'true'
return block
end,
})
end

--- Check whether a code block holds the output of an executed cell.
--- @param block pandoc.CodeBlock
--- @return boolean
function M.is_marked(block)
return block.attributes[MARKER] == 'true'
end

--- Remove the marker, so it never reaches the output document.
--- @param block pandoc.CodeBlock
function M.strip(block)
block.attributes[MARKER] = nil
end

return M
10 changes: 9 additions & 1 deletion _extensions/code-window/_modules/language.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
--- @author Mickaël Canouil
--- @brief Normalise code blocks with no or unknown language class.

local cell_output = require(
quarto.utils.resolve_path('_modules/cell-output.lua'):gsub('%.lua$', ''))

local M = {}

local known_language_cache = {}
Expand Down Expand Up @@ -34,9 +37,14 @@ end
--- 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.
--- @param block pandoc.CodeBlock
--- @return 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')
if not block.attributes['filename'] or block.attributes['filename'] == '' then
Expand Down
4 changes: 4 additions & 0 deletions _extensions/code-window/_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ options:
enum: ["default", "macos", "windows"]
default: "macos"
description: "Window decoration style: 'macos' (traffic lights), 'windows' (title bar buttons), or 'default' (plain filename). Can be overridden per block with the 'code-window-style' attribute."
cell-output:
type: boolean
default: false
description: "Frame the output of an executed cell like a code block. Off by default, so output keeps the shape Quarto gives it."
wrapper:
type: string
default: "code-window"
Expand Down
58 changes: 48 additions & 10 deletions _extensions/code-window/code-window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ local log = require(quarto.utils.resolve_path('_modules/logging.lua'):gsub('%.lu
local meta_mod = require(quarto.utils.resolve_path('_modules/metadata.lua'):gsub('%.lua$', ''))
local pdoc = require(quarto.utils.resolve_path('_modules/pandoc-helpers.lua'):gsub('%.lua$', ''))
local html_mod = require(quarto.utils.resolve_path('_modules/html.lua'):gsub('%.lua$', ''))
local cell_output = require(quarto.utils.resolve_path('_modules/cell-output.lua'):gsub('%.lua$', ''))
local code_annotations = nil

-- ============================================================================
Expand All @@ -27,6 +28,7 @@ local code_annotations = nil
--- @field enabled boolean Whether code-window styling is enabled
--- @field auto_filename boolean Whether to auto-generate filename from language
--- @field style string Window decoration style ('macos', 'windows', 'default')
--- @field cell_output boolean Whether the output of an executed cell is framed
--- @field typst_wrapper string Typst wrapper function name
--- @field hotfix_code_annotations boolean Whether to apply the code-annotations hot-fix for Typst
--- @field hotfix_skylighting boolean Whether to apply the Skylighting hot-fix for Typst
Expand All @@ -37,6 +39,7 @@ local DEFAULTS = {
['enabled'] = 'true',
['auto-filename'] = 'true',
['style'] = 'macos',
['cell-output'] = 'false',
['wrapper'] = 'code-window',
['collapse'] = 'false',
['lines-label'] = 'true',
Expand All @@ -60,6 +63,18 @@ local CONFIG = nil
local TYPST_BG_COLOUR = nil
local ANNOTATION_BLOCK_COUNTER = 0

-- ============================================================================
-- CELL OUTPUT
-- ============================================================================

--- 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
--- @return boolean
local function is_unnamed_cell_output(block)
return cell_output.is_marked(block) and str.is_empty(block.attributes['filename'])
end

-- ============================================================================
-- BLOCK-LEVEL STYLE OVERRIDE
-- ============================================================================
Expand Down Expand Up @@ -480,6 +495,12 @@ end
--- @param block pandoc.CodeBlock Code block element
--- @return pandoc.Div|pandoc.CodeBlock Wrapped block or original
local function process_html(block)
-- Default/unknown/no-language blocks carry their label on
-- code-window-auto-label (set by the language module). Read it here so no
-- return path can leak it into the rendered document.
local auto_label = block.attributes['code-window-auto-label']
block.attributes['code-window-auto-label'] = nil

-- Per-block opt-out: code-window-enabled="false" skips window chrome.
local block_enabled = block.attributes['code-window-enabled']
if block_enabled then
Expand Down Expand Up @@ -520,11 +541,8 @@ local function process_html(block)
return block
end

-- Default/unknown/no-language blocks carry their label on
-- code-window-auto-label (set by the language module); everything else uses
-- its language class.
local filename = block.attributes['code-window-auto-label'] or block.classes[1]
block.attributes['code-window-auto-label'] = nil
-- Blocks with a language of their own are labelled with its class.
local filename = auto_label or block.classes[1]

-- Set the filename attribute so Quarto creates its own .code-with-filename
-- wrapper. This preserves the CodeBlock+OrderedList sibling structure
Expand Down Expand Up @@ -635,7 +653,9 @@ function Meta(meta)
CURRENT_FORMAT = pdoc.get_quarto_format()
local opts = meta_mod.get_options({
extension = EXTENSION_NAME,
keys = { 'enabled', 'auto-filename', 'style', 'wrapper', 'collapse', 'lines-label' },
keys = {
'enabled', 'auto-filename', 'style', 'cell-output', 'wrapper', 'collapse', 'lines-label',
},
meta = meta,
defaults = DEFAULTS,
})
Expand Down Expand Up @@ -698,6 +718,7 @@ function Meta(meta)
enabled = opts['enabled'] == 'true',
auto_filename = opts['auto-filename'] == 'true',
style = VALID_STYLES[opts['style']] and opts['style'] or 'macos',
cell_output = opts['cell-output'] == 'true',
typst_wrapper = opts['wrapper'],
collapse = global_collapse,
lines_label = opts['lines-label'] == 'true',
Expand Down Expand Up @@ -746,8 +767,20 @@ end
--- Process CodeBlock elements for HTML/Reveal.js only.
--- Typst processing is handled by the Blocks filter.
function CodeBlock(block)
-- The Typst path reads the marker in the Pandoc filter, which runs first, so
-- this pass is where it is removed for every format.
local is_plain_output = is_unnamed_cell_output(block)
if cell_output.is_marked(block) then
cell_output.strip(block)
end

if not CURRENT_FORMAT or not CONFIG or not CONFIG.enabled then
block.attributes['code-window-no-auto-filename'] = nil
block.attributes['code-window-auto-label'] = nil
return block
end

if is_plain_output then
return block
end

Expand All @@ -767,7 +800,6 @@ end
--- @return string|nil filename
--- @return boolean is_auto
--- @return string|nil block_style
--- @return boolean window_opted_out True when code-window-enabled="false" was set
--- @return string|nil lines_label Highlighted-lines spec for the title bar
local function resolve_window_params(block)
-- Per-block opt-out: code-window-enabled="false" skips window chrome.
Expand All @@ -776,7 +808,7 @@ local function resolve_window_params(block)
block.attributes['code-window-enabled'] = nil
end
if block_enabled == 'false' then
return nil, false, nil, true, nil
return nil, false, nil, nil
end

local block_style = read_block_style(block)
Expand All @@ -803,7 +835,7 @@ local function resolve_window_params(block)
lines_label = read_block_lines_label(block)
end

return filename, is_auto, block_style, false, lines_label
return filename, is_auto, block_style, lines_label
end

--- Process a single CodeBlock for Typst, returning replacement blocks.
Expand All @@ -814,7 +846,13 @@ end
--- @return boolean consumed_next Whether the next block was consumed
--- @return integer|nil annotation_block_id Block ID if annotations were found (for parent propagation)
local function process_typst_block(block, next_block)
local filename, is_auto, block_style, window_opted_out, lines_label = resolve_window_params(block)
-- The output of an executed cell keeps the shape Quarto gave it, annotations
-- included, unless the engine gave it a filename of its own.
if is_unnamed_cell_output(block) then
return { block }, false, nil
end

local filename, is_auto, block_style, lines_label = resolve_window_params(block)
local has_window = filename and filename ~= ''
local effective_style = block_style or CONFIG.style

Expand Down
28 changes: 27 additions & 1 deletion _extensions/code-window/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ local log = require(quarto.utils.resolve_path('_modules/logging.lua'):gsub('%.lu
-- LOAD SUBMODULES
-- ============================================================================

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$', ''))

Expand All @@ -23,6 +26,26 @@ local code_window = require(

code_window.set_code_annotations(code_annotations)

-- ============================================================================
-- CELL OUTPUT
-- ============================================================================

--- 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.
--- @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
return nil
end
doc.blocks = doc.blocks:walk({ Div = cell_output.Div })
return doc
end

-- ============================================================================
-- SKYLIGHTING HOT-FIX
-- ============================================================================
Expand All @@ -49,9 +72,12 @@ end
-- FILTER ASSEMBLY
-- ============================================================================

-- Meta runs first because the cell-output pass needs the configuration, and
-- that pass runs before the language pass so a marked block is never relabelled.
local filters = {
{ CodeBlock = language.CodeBlock },
{ Meta = code_window.Meta },
{ Pandoc = mark_cell_output },
{ CodeBlock = language.CodeBlock },
{ Pandoc = code_window.Pandoc },
{ CodeBlock = code_window.CodeBlock },
}
Expand Down
19 changes: 19 additions & 0 deletions docs/reference.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ extensions:
code-window:
style: macos
auto-filename: true
cell-output: false
collapse: false
lines-label: true
```
Expand All @@ -27,6 +28,7 @@ extensions:
| `enabled` | boolean | `true` | Whether the filter runs at all. |
| `style` | string | `macos` | The window chrome: `macos` for traffic lights, `windows` for title-bar buttons, `default` for a plain filename. |
| `auto-filename` | boolean | `true` | Use the block's language as the filename when none is given. |
| `cell-output` | boolean | `false` | Frame the output of an executed cell as well as its source. |
| `collapse` | boolean or string | `false` | Wrap every block in a `<details>`. `open` renders it expanded, `closed` or `true` collapsed. HTML only. |
| `lines-label` | boolean | `true` | Show a chip beside the filename with the highlighted-line spec. |
| `wrapper` | string | `code-window` | The Typst function used to draw the chrome. |
Expand Down Expand Up @@ -60,6 +62,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.
:::

## Executed cells

Quarto writes the output of an executed cell as a code block with no language.
That block keeps the shape Quarto gives it: no chrome, and no derived filename.
Only the source of the cell is framed.

Set `cell-output` to `true` to frame the output as well.

```yaml
extensions:
code-window:
cell-output: true
```

An engine can put a filename on the output block, through knitr's `attr-output` for example.
Output named that way is always framed, whatever this option says.

## How the chrome is applied

In HTML the extension marks blocks and injects a stylesheet and a script.
Expand Down