diff --git a/CHANGELOG.md b/CHANGELOG.md index 52aa5a9..5496fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/_extensions/code-window/_modules/cell-output.lua b/_extensions/code-window/_modules/cell-output.lua new file mode 100644 index 0000000..ebc5ee3 --- /dev/null +++ b/_extensions/code-window/_modules/cell-output.lua @@ -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 diff --git a/_extensions/code-window/_modules/language.lua b/_extensions/code-window/_modules/language.lua index a769346..59bdeef 100644 --- a/_extensions/code-window/_modules/language.lua +++ b/_extensions/code-window/_modules/language.lua @@ -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 = {} @@ -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 diff --git a/_extensions/code-window/_schema.yml b/_extensions/code-window/_schema.yml index 66c0d10..41bbfb4 100644 --- a/_extensions/code-window/_schema.yml +++ b/_extensions/code-window/_schema.yml @@ -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" diff --git a/_extensions/code-window/code-window.lua b/_extensions/code-window/code-window.lua index 87ae7a4..1523271 100644 --- a/_extensions/code-window/code-window.lua +++ b/_extensions/code-window/code-window.lua @@ -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 -- ============================================================================ @@ -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 @@ -37,6 +39,7 @@ local DEFAULTS = { ['enabled'] = 'true', ['auto-filename'] = 'true', ['style'] = 'macos', + ['cell-output'] = 'false', ['wrapper'] = 'code-window', ['collapse'] = 'false', ['lines-label'] = 'true', @@ -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 -- ============================================================================ @@ -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 @@ -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 @@ -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, }) @@ -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', @@ -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 @@ -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. @@ -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) @@ -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. @@ -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 diff --git a/_extensions/code-window/main.lua b/_extensions/code-window/main.lua index 44e1498..25b61da 100644 --- a/_extensions/code-window/main.lua +++ b/_extensions/code-window/main.lua @@ -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$', '')) @@ -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 -- ============================================================================ @@ -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 }, } diff --git a/docs/reference.qmd b/docs/reference.qmd index 6b66f21..27720df 100644 --- a/docs/reference.qmd +++ b/docs/reference.qmd @@ -18,6 +18,7 @@ extensions: code-window: style: macos auto-filename: true + cell-output: false collapse: false lines-label: true ``` @@ -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 `
`. `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. | @@ -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.