diff --git a/CHANGELOG.md b/CHANGELOG.md index 13d2c84..b4ac388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ ## Unreleased +### New Features + +- feat: render a highlighted-lines chip alongside the filename in the code-window title bar (HTML, Reveal.js, and Typst). + Uses Quarto's `code-line-numbers` attribute or a new `code-window-lines` attribute. + The chip can be disabled globally via `lines-label: false`. +- feat: collapsible code windows via a `
` wrapper (HTML/Reveal.js). + Per-block opt-in with the `code-window-collapse` attribute (`true`/`false`/`open`/`closed`) or document-wide via the `collapse` option. +- feat: expose CSS custom properties (`--code-window-macos-icon`, `--code-window-windows-icon`, `--code-window-macos-icon-width`, `--code-window-windows-icon-width`, `--code-window-icon-height`) so users can override the window-button icons. + +### Documentation + +- docs: document `code-line-numbers` integration, the new `collapse` option, and the CSS custom properties for icon overrides. + +### Refactoring + +- refactor: synchronise shared modules (`html`, `logging`, `metadata`, `pandoc-helpers`, `string`) with canonical versions. + ## 1.1.5 (2026-04-21) ### Bug Fixes diff --git a/README.md b/README.md index c13d673..b9c4f2f 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ extensions: auto-filename: true style: "macos" wrapper: "code-window" + collapse: false + lines-label: true hotfix: code-annotations: true skylighting: true @@ -66,12 +68,14 @@ extensions: ### Options -| Option | Type | Default | Description | -| --------------- | ------- | --------------- | -------------------------------------------------------------------- | -| `enabled` | boolean | `true` | Enable or disable the code-window filter. | -| `auto-filename` | boolean | `true` | Automatically generate filename labels from the code block language. | -| `style` | string | `"macos"` | Window decoration style: `"macos"`, `"windows"`, or `"default"`. | -| `wrapper` | string | `"code-window"` | Typst wrapper function name for code-window rendering. | +| Option | Type | Default | Description | +| --------------- | -------------- | --------------- | -------------------------------------------------------------------------------------------------------- | +| `enabled` | boolean | `true` | Enable or disable the code-window filter. | +| `auto-filename` | boolean | `true` | Automatically generate filename labels from the code block language. Set to `false` to disable globally. | +| `style` | string | `"macos"` | Window decoration style: `"macos"`, `"windows"`, or `"default"`. | +| `wrapper` | string | `"code-window"` | Typst wrapper function name for code-window rendering. | +| `collapse` | boolean/string | `false` | Wrap every code window in a `
` element (HTML). Accepts `false`, `true`, `"open"`, `"closed"`. | +| `lines-label` | boolean | `true` | Render a small chip next to the filename showing the highlighted-line spec. | ### Hotfix Options @@ -93,13 +97,15 @@ Each hotfix value can be a simple boolean or a map with `enabled` and `quarto-ve ### Block-Level Attributes -Override the style or disable features for a single code block: +Override the style or toggle features for a single code block: -| Attribute | Type | Default | Description | -| ------------------------------- | ------- | ------- | ---------------------------------------------------------------------- | -| `code-window-style` | string | | Override the global style for this block: `"macos"`, `"windows"`, or `"default"`. | -| `code-window-enabled` | boolean | `true` | Set to `false` to disable window chrome while keeping annotations. | -| `code-window-no-auto-filename` | boolean | `false` | Suppress the auto-generated filename for this block. | +| Attribute | Type | Default | Description | +| ------------------------------ | -------------- | ------- | -------------------------------------------------------------------------------------------------------------------- | +| `code-window-style` | string | | Override the global style for this block: `"macos"`, `"windows"`, or `"default"`. | +| `code-window-enabled` | boolean | `true` | Set to `false` to disable window chrome while keeping annotations. | +| `code-window-no-auto-filename` | boolean | `false` | Suppress the auto-generated filename for this block. | +| `code-window-collapse` | boolean/string | | Render this code block inside a `
` element (HTML). Accepts `true` (closed), `false`, `"open"`, `"closed"`. | +| `code-window-lines` | string | | Highlighted-lines spec rendered as a chip alongside the filename (e.g. `"1,3-5"`). Falls back to `code-line-numbers`. | ````markdown ```{.python filename="example.py" code-window-style="windows"} @@ -107,6 +113,50 @@ print("Windows style for this block only") ``` ```` +### Highlighted Lines Chip + +When a code block carries Quarto's `code-line-numbers` attribute with a line specification (e.g. `"1,3-5"`), the spec is rendered as a small chip beside the filename in the title bar. + +````markdown +```{.python filename="loader.py" code-line-numbers="1,4-5"} +import pandas as pd + +def load(path): + df = pd.read_csv(path) + return df.dropna() +``` +```` + +Override the displayed text with `code-window-lines="..."` or disable the chip globally with `lines-label: false`. + +### Collapsible Code Windows (HTML) + +Set `code-window-collapse` on a block, or `collapse` at the document level, to wrap the code window in a `
` element. +The title bar becomes the ``. + +````markdown +```{.python filename="long.py" code-window-collapse="closed"} +# Long block hidden behind a clickable title bar. +print("Click the title bar to expand.") +``` +```` + +This feature applies to HTML and Reveal.js output only. + +### Customising Window-Button Icons (HTML) + +The macOS and Windows window-button icons are exposed as CSS custom properties so you can override them in your own stylesheet: + +```css +:root { + --code-window-macos-icon: url("data:image/svg+xml,..."); + --code-window-windows-icon: url("data:image/svg+xml,..."); + --code-window-macos-icon-width: 3.4em; + --code-window-windows-icon-width: 3.6em; + --code-window-icon-height: 0.85em; +} +``` + ### Temporary Hot-fixes (Typst) The extension includes three temporary hot-fixes for Typst output that compensate for missing Quarto/Pandoc features. diff --git a/_extensions/code-window/_modules/html.lua b/_extensions/code-window/_modules/html.lua index e022cef..8212cc5 100644 --- a/_extensions/code-window/_modules/html.lua +++ b/_extensions/code-window/_modules/html.lua @@ -1,5 +1,5 @@ --- MC HTML - HTML generation and dependency management for Quarto Lua filters and shortcodes ---- @module html +--- @module "html" --- @license MIT --- @copyright 2026 Mickaël Canouil --- @author Mickaël Canouil diff --git a/_extensions/code-window/_modules/logging.lua b/_extensions/code-window/_modules/logging.lua index abf4171..a538809 100644 --- a/_extensions/code-window/_modules/logging.lua +++ b/_extensions/code-window/_modules/logging.lua @@ -1,5 +1,5 @@ --- MC Logging - Formatted log output for Quarto Lua filters and shortcodes ---- @module logging +--- @module "logging" --- @license MIT --- @copyright 2026 Mickaël Canouil --- @author Mickaël Canouil diff --git a/_extensions/code-window/_modules/metadata.lua b/_extensions/code-window/_modules/metadata.lua index 813c985..bab2087 100644 --- a/_extensions/code-window/_modules/metadata.lua +++ b/_extensions/code-window/_modules/metadata.lua @@ -1,5 +1,5 @@ --- MC Metadata - Extension configuration and metadata access for Quarto Lua filters and shortcodes ---- @module metadata +--- @module "metadata" --- @license MIT --- @copyright 2026 Mickaël Canouil --- @author Mickaël Canouil diff --git a/_extensions/code-window/_modules/pandoc-helpers.lua b/_extensions/code-window/_modules/pandoc-helpers.lua index 392bf1d..aad35cf 100644 --- a/_extensions/code-window/_modules/pandoc-helpers.lua +++ b/_extensions/code-window/_modules/pandoc-helpers.lua @@ -1,5 +1,5 @@ --- MC Pandoc Helpers - Pandoc element construction and format detection for Quarto Lua filters and shortcodes ---- @module pandoc_helpers +--- @module "pandoc_helpers" --- @license MIT --- @copyright 2026 Mickaël Canouil --- @author Mickaël Canouil diff --git a/_extensions/code-window/_modules/string.lua b/_extensions/code-window/_modules/string.lua index e711a00..8f89e0b 100644 --- a/_extensions/code-window/_modules/string.lua +++ b/_extensions/code-window/_modules/string.lua @@ -1,5 +1,5 @@ --- MC String - String manipulation and escaping for Quarto Lua filters and shortcodes ---- @module string +--- @module "string" --- @license MIT --- @copyright 2026 Mickaël Canouil --- @author Mickaël Canouil diff --git a/_extensions/code-window/_schema.yml b/_extensions/code-window/_schema.yml index f00c11d..e16b027 100644 --- a/_extensions/code-window/_schema.yml +++ b/_extensions/code-window/_schema.yml @@ -18,6 +18,15 @@ options: type: string default: "code-window" description: "Typst wrapper function name for code-window rendering." + collapse: + type: [boolean, string] + enum: [true, false, "open", "closed"] + default: false + description: "Wrap every code-window block in a collapsible
element (HTML only). Use 'open' to render expanded by default, 'closed' or true to render collapsed by default, false to disable. Per-block overrides use the 'code-window-collapse' attribute." + lines-label: + type: boolean + default: true + description: "Render a small chip next to the filename showing the highlighted-line spec for blocks that set 'code-line-numbers' or 'code-window-lines'." hotfix: type: object description: "Temporary hot-fixes for Typst output. These will be removed when Quarto natively supports the corresponding features (see quarto-dev/quarto-cli#14170)." @@ -70,3 +79,10 @@ attributes: type: string enum: ["default", "macos", "windows"] description: "Override the global window decoration style for this specific code block." + code-window-collapse: + type: [boolean, string] + enum: [true, false, "open", "closed"] + description: "Wrap this code block in a collapsible
element (HTML only). Use 'open' to render expanded, 'closed' or true to render collapsed, false to disable." + code-window-lines: + type: string + description: "Highlighted-lines spec rendered as a chip alongside the filename (e.g. '1,3-5'). When omitted, Quarto's 'code-line-numbers' attribute is used." diff --git a/_extensions/code-window/code-window.lua b/_extensions/code-window/code-window.lua index 159388c..96524de 100644 --- a/_extensions/code-window/code-window.lua +++ b/_extensions/code-window/code-window.lua @@ -38,6 +38,15 @@ local DEFAULTS = { ['auto-filename'] = 'true', ['style'] = 'macos', ['wrapper'] = 'code-window', + ['collapse'] = 'false', + ['lines-label'] = 'true', +} + +local VALID_COLLAPSE = { + ['false'] = false, + ['true'] = 'closed', + ['closed'] = 'closed', + ['open'] = 'open', } local HOTFIX_DEFAULTS = { @@ -74,6 +83,63 @@ local function read_block_style(block) return nil end +--- Resolve a collapse value coming from extension options or a code-block +--- attribute. Returns "open"/"closed" when the window should be collapsible, +--- nil when collapsing is off or the value is unrecognised. +--- Emits a warning when the input is set but not understood. +--- @param raw string|nil Raw collapse value +--- @return string|nil Resolved collapse mode ("open"/"closed") or nil +local function resolve_collapse(raw) + if raw == nil or raw == '' then + return nil + end + local resolved = VALID_COLLAPSE[raw] + if resolved == nil then + log.log_warning(EXTENSION_NAME, + string.format('Unknown collapse value "%s", expected one of true/false/open/closed.', raw)) + return nil + end + if resolved == false then + return nil + end + return resolved +end + +--- Read the per-block collapse override from code-window-collapse attribute. +--- Always strips the attribute from the block before resolving. +--- @param block pandoc.CodeBlock Code block element +--- @return string|nil Resolved collapse mode ("open"/"closed") or nil when off +local function read_block_collapse(block) + local raw = block.attributes['code-window-collapse'] + if raw == nil then + return nil + end + block.attributes['code-window-collapse'] = nil + return resolve_collapse(raw) +end + +--- Read a highlight-lines spec from the block, looking at the +--- code-window-lines attribute first and falling back to Quarto's +--- code-line-numbers attribute when it carries a non-boolean spec. +--- Returns the cleaned spec string or nil. +--- @param block pandoc.CodeBlock Code block element +--- @return string|nil Line spec to display in the title bar +local function read_block_lines_label(block) + local raw = block.attributes['code-window-lines'] + if raw ~= nil then + block.attributes['code-window-lines'] = nil + if raw ~= '' then + return raw + end + end + + local cln = block.attributes['code-line-numbers'] + if not cln or cln == '' or cln == 'true' or cln == 'false' then + return nil + end + return cln +end + -- ============================================================================ -- TYPST FUNCTION DEFINITION -- ============================================================================ @@ -190,6 +256,7 @@ local function build_typst_function_def(has_hotfixes) annotations: (:), bg-colour: none, block-id: 0, + lines-label: none, ) = { context { let page-bg = _cw-page-bg() @@ -207,6 +274,17 @@ local function build_typst_function_def(has_hotfixes) ) } + let lines-chip = if lines-label != none { + box( + inset: (x: 0.4em, y: 0.05em), + outset: (y: 0.15em), + radius: 3pt, + fill: color.mix((fg, 10%%), (page-bg, 90%%)), + stroke: 0.5pt + border-colour, + text(size: 0.7em, weight: 500, fill: muted-colour, lines-label), + ) + } + let traffic-lights = box( inset: (right: 0.5em), stack( @@ -238,6 +316,12 @@ local function build_typst_function_def(has_hotfixes) }, ) + let _label-with-chip = if lines-chip != none { + stack(dir: ltr, spacing: 0.5em, filename-label, lines-chip) + } else { + filename-label + } + let title-bar = if style == "macos" { grid( columns: (auto, 1fr), @@ -245,7 +329,7 @@ local function build_typst_function_def(has_hotfixes) gutter: 0.5em, stroke: 0pt, traffic-lights, - filename-label, + _label-with-chip, ) } else if style == "windows" { grid( @@ -253,12 +337,12 @@ local function build_typst_function_def(has_hotfixes) align: (left + horizon, right + horizon), gutter: 0.5em, stroke: 0pt, - filename-label, + _label-with-chip, window-buttons, ) } else { // default: plain filename, left-aligned - filename-label + _label-with-chip } block( @@ -331,28 +415,42 @@ local function typst_bg_colour_param() return string.format(', bg-colour: rgb("%s")', TYPST_BG_COLOUR) end +--- Escape a string for embedding in a Typst double-quoted string literal. +--- @param s string +--- @return string +local function typst_escape_string(s) + return (s:gsub('\\', '\\\\'):gsub('"', '\\"')) +end + --- Build a code-window opening RawBlock for Typst. --- @param filename string --- @param is_auto boolean --- @param style string --- @param annotations table|nil --- @param block_id integer +--- @param lines_label string|nil Highlighted-lines spec to display in the title bar --- @return pandoc.RawBlock -local function typst_code_window_open(filename, is_auto, style, annotations, block_id) +local function typst_code_window_open(filename, is_auto, style, annotations, block_id, lines_label) local annot_param = '' if annotations and next(annotations) then annot_param = string.format(', annotations: %s, block-id: %d', code_annotations.annotations_to_typst_dict(annotations), block_id) end + local lines_param = '' + if lines_label and lines_label ~= '' then + lines_param = string.format(', lines-label: "%s"', typst_escape_string(lines_label)) + end + return pandoc.RawBlock('typst', string.format( - '#%s(filename: "%s", is-auto: %s, style: "%s"%s%s)[', + '#%s(filename: "%s", is-auto: %s, style: "%s"%s%s%s)[', CONFIG.typst_wrapper, - filename:gsub('"', '\\"'), + typst_escape_string(filename), is_auto and 'true' or 'false', style, annot_param, - typst_bg_colour_param() + typst_bg_colour_param(), + lines_param )) end @@ -393,12 +491,16 @@ local function process_html(block) end local block_style = read_block_style(block) + local block_collapse = read_block_collapse(block) + local effective_collapse = block_collapse or CONFIG.collapse local explicit_filename = block.attributes['filename'] local no_auto = block.attributes['code-window-no-auto-filename'] if no_auto then block.attributes['code-window-no-auto-filename'] = nil end + local lines_label = CONFIG.lines_label and read_block_lines_label(block) or nil + if explicit_filename and explicit_filename ~= '' then -- Let Quarto create the .code-with-filename wrapper. -- Add a marker class for block-level style override; the injected JS @@ -406,6 +508,12 @@ local function process_html(block) if block_style then table.insert(block.classes, 'cw-style-' .. block_style) end + if effective_collapse then + table.insert(block.classes, 'cw-collapse-' .. effective_collapse) + end + if lines_label then + block.attributes['code-window-lines-label'] = lines_label + end return block end @@ -423,6 +531,12 @@ local function process_html(block) if block_style then table.insert(block.classes, 'cw-style-' .. block_style) end + if effective_collapse then + table.insert(block.classes, 'cw-collapse-' .. effective_collapse) + end + if lines_label then + block.attributes['code-window-lines-label'] = lines_label + end return block end @@ -434,12 +548,18 @@ end --- Generate a JS snippet that builds .code-with-filename wrappers for --- auto-filename blocks (marked with cw-auto at pre-quarto), applies the --- configured default style class to all .code-with-filename wrappers, ---- and promotes block-level cw-style-* marker classes. +--- promotes block-level cw-style-* / cw-collapse-* marker classes, inserts +--- a highlighted-lines chip when data-code-window-lines-label is set on the inner +--- pre, and wraps collapsible windows in a
element where the +--- title bar acts as the . --- @param default_style string The configured default style +--- @param default_collapse string|nil Default collapse mode ("open"/"closed"/nil) --- @return string JavaScript code -local function make_style_js(default_style) +local function make_style_js(default_style, default_collapse) + local default_collapse_js = default_collapse and ('"' .. default_collapse .. '"') or 'null' return string.format([=[ document.addEventListener("DOMContentLoaded",function(){ + var DEFAULT_COLLAPSE=%s; document.querySelectorAll("pre.cw-auto").forEach(function(pre){ var fn=pre.closest("[data-filename]"); if(!fn)return; @@ -459,13 +579,52 @@ document.addEventListener("DOMContentLoaded",function(){ w.appendChild(tb);w.appendChild(scaffold); pre.classList.remove("cw-auto"); }); + function findMarkerSource(el){ + return el.querySelector('pre[class*="cw-style-"],pre[class*="cw-collapse-"],pre[data-code-window-lines-label]') + ||el.querySelector('[class*="cw-style-"],[class*="cw-collapse-"],[data-code-window-lines-label]'); + } document.querySelectorAll(".code-with-filename").forEach(function(el){ - if(/\bcode-window-(macos|windows|default)\b/.test(el.className))return; - var c=el.querySelector('[class*="cw-style-"]'); - if(c){var m=c.className.match(/cw-style-(\w+)/);if(m){el.classList.add("code-window-"+m[1]);c.classList.remove(m[0]);return;}} - el.classList.add("code-window-%s"); + var marker=findMarkerSource(el); + var styleApplied=/\bcode-window-(macos|windows|default)\b/.test(el.className); + if(!styleApplied){ + var sm=marker&&marker.className.match(/cw-style-(\w+)/); + if(sm){el.classList.add("code-window-"+sm[1]);marker.classList.remove(sm[0]);} + else{el.classList.add("code-window-%s");} + } + var collapse=null; + if(marker){ + var cm=marker.className.match(/cw-collapse-(open|closed)/); + if(cm){collapse=cm[1];marker.classList.remove(cm[0]);} + } + if(collapse===null&&DEFAULT_COLLAPSE){collapse=DEFAULT_COLLAPSE;} + if(marker&&marker.hasAttribute("data-code-window-lines-label")){ + var spec=marker.getAttribute("data-code-window-lines-label"); + marker.removeAttribute("data-code-window-lines-label"); + var titleBar=el.querySelector(".code-with-filename-file"); + if(titleBar&&!titleBar.querySelector(".code-with-filename-lines")){ + var chip=document.createElement("span"); + chip.className="code-with-filename-lines"; + chip.textContent="L"+spec; + titleBar.appendChild(chip); + } + } + if(collapse&&!el.classList.contains("code-window-collapsible")){ + var titleBar=el.querySelector(".code-with-filename-file"); + if(titleBar){ + var details=document.createElement("details"); + details.className=el.className+" code-window-collapsible"; + if(collapse==="open"){details.open=true;} + var summaryEl=document.createElement("summary"); + summaryEl.className="code-with-filename-file"; + while(titleBar.firstChild){summaryEl.appendChild(titleBar.firstChild);} + titleBar.parentNode.removeChild(titleBar); + details.appendChild(summaryEl); + while(el.firstChild){details.appendChild(el.firstChild);} + el.parentNode.replaceChild(details,el); + } + } }); -});]=], default_style) +});]=], default_collapse_js, default_style) end --- Load configuration and inject CSS/JS dependencies. @@ -473,7 +632,7 @@ function Meta(meta) CURRENT_FORMAT = pdoc.get_quarto_format() local opts = meta_mod.get_options({ extension = EXTENSION_NAME, - keys = { 'enabled', 'auto-filename', 'style', 'wrapper' }, + keys = { 'enabled', 'auto-filename', 'style', 'wrapper', 'collapse', 'lines-label' }, meta = meta, defaults = DEFAULTS, }) @@ -483,6 +642,8 @@ function Meta(meta) string.format('Unknown style "%s", falling back to "macos".', opts['style'])) end + local global_collapse = resolve_collapse(opts['collapse']) + -- Read code-annotations metadata (Quarto standard option). local annot_meta = meta['code-annotations'] local annot_value = annot_meta and pandoc.utils.stringify(annot_meta) or '' @@ -535,6 +696,8 @@ function Meta(meta) auto_filename = opts['auto-filename'] == 'true', style = VALID_STYLES[opts['style']] and opts['style'] or 'macos', typst_wrapper = opts['wrapper'], + collapse = global_collapse, + lines_label = opts['lines-label'] == 'true', hotfix_code_annotations = hotfix['code-annotations'], hotfix_skylighting = hotfix['skylighting'], hotfix_typst_title = hotfix['typst-title'], @@ -570,7 +733,7 @@ function Meta(meta) html_mod.ensure_html_dependency({ name = EXTENSION_NAME .. '-style-init', version = '0.1.0', - head = '', + head = '', }) end @@ -602,6 +765,7 @@ end --- @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. local block_enabled = block.attributes['code-window-enabled'] @@ -609,7 +773,7 @@ local function resolve_window_params(block) block.attributes['code-window-enabled'] = nil end if block_enabled == 'false' then - return nil, false, nil, true + return nil, false, nil, true, nil end local block_style = read_block_style(block) @@ -628,7 +792,12 @@ local function resolve_window_params(block) end end - return filename, is_auto, block_style, false + local lines_label = nil + if CONFIG.lines_label then + lines_label = read_block_lines_label(block) + end + + return filename, is_auto, block_style, false, lines_label end --- Process a single CodeBlock for Typst, returning replacement blocks. @@ -639,7 +808,7 @@ 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 = resolve_window_params(block) + local filename, is_auto, block_style, window_opted_out, lines_label = resolve_window_params(block) local has_window = filename and filename ~= '' local effective_style = block_style or CONFIG.style @@ -668,12 +837,12 @@ local function process_typst_block(block, next_block) if has_window and has_annotations then table.insert(result, typst_code_window_open( - filename, is_auto, effective_style, annotations, block_id)) + filename, is_auto, effective_style, annotations, block_id, lines_label)) table.insert(result, block) table.insert(result, pandoc.RawBlock('typst', ']')) elseif has_window then table.insert(result, typst_code_window_open( - filename, is_auto, effective_style, nil, 0)) + filename, is_auto, effective_style, nil, 0, lines_label)) table.insert(result, block) table.insert(result, pandoc.RawBlock('typst', ']')) elseif has_annotations then diff --git a/_extensions/code-window/style.css b/_extensions/code-window/style.css index 836ae68..3d448fb 100644 --- a/_extensions/code-window/style.css +++ b/_extensions/code-window/style.css @@ -3,6 +3,18 @@ * MIT License - Mickaël Canouil 2026 */ +/* ============================================================================ + * Theming variables (override to customise window-button icons and chip) + * ========================================================================= */ + +:root { + --code-window-macos-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='10'%3E%3Ccircle cx='5' cy='5' r='5' fill='%23ff5f56'/%3E%3Ccircle cx='20' cy='5' r='5' fill='%23ffbd2e'/%3E%3Ccircle cx='35' cy='5' r='5' fill='%2327c93f'/%3E%3C/svg%3E"); + --code-window-windows-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='10' fill='none'%3E%3Cline x1='3' y1='5' x2='11' y2='5' stroke='%23888' stroke-width='1.5' stroke-linecap='round'/%3E%3Crect x='17' y='1' width='8' height='8' rx='1' stroke='%23888' stroke-width='1.5' fill='none'/%3E%3Cline x1='31' y1='1' x2='39' y2='9' stroke='%23888' stroke-width='1.5' stroke-linecap='round'/%3E%3Cline x1='39' y1='1' x2='31' y2='9' stroke='%23888' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E"); + --code-window-macos-icon-width: 3.4em; + --code-window-windows-icon-width: 3.6em; + --code-window-icon-height: 0.85em; +} + /* ============================================================================ * Base container * ========================================================================= */ @@ -51,6 +63,21 @@ color: color-mix(in srgb, currentColor 50%, Canvas); } +/* Highlighted-lines chip rendered next to the filename in the title bar */ +.code-with-filename .code-with-filename-file .code-with-filename-lines { + flex: 0 0 auto; + font-size: 0.7em; + font-weight: 500; + font-family: var(--bs-font-monospace, monospace); + color: color-mix(in srgb, currentColor 60%, Canvas); + background-color: color-mix(in srgb, currentColor 10%, Canvas); + border: 1px solid color-mix(in srgb, currentColor 15%, Canvas); + border-radius: 3px; + padding: 0.05em 0.4em; + line-height: 1.4; + white-space: nowrap; +} + /* Code block content area */ .code-with-filename .sourceCode, .code-with-filename div.sourceCode, @@ -103,9 +130,9 @@ content: ''; display: inline-flex; flex-shrink: 0; - width: 3.4em; - height: 0.85em; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='10'%3E%3Ccircle cx='5' cy='5' r='5' fill='%23ff5f56'/%3E%3Ccircle cx='20' cy='5' r='5' fill='%23ffbd2e'/%3E%3Ccircle cx='35' cy='5' r='5' fill='%2327c93f'/%3E%3C/svg%3E"); + width: var(--code-window-macos-icon-width); + height: var(--code-window-icon-height); + background-image: var(--code-window-macos-icon); background-size: contain; background-repeat: no-repeat; } @@ -126,10 +153,43 @@ content: ''; display: inline-flex; flex-shrink: 0; - width: 3.6em; - height: 0.85em; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='10' fill='none'%3E%3Cline x1='3' y1='5' x2='11' y2='5' stroke='%23888' stroke-width='1.5' stroke-linecap='round'/%3E%3Crect x='17' y='1' width='8' height='8' rx='1' stroke='%23888' stroke-width='1.5' fill='none'/%3E%3Cline x1='31' y1='1' x2='39' y2='9' stroke='%23888' stroke-width='1.5' stroke-linecap='round'/%3E%3Cline x1='39' y1='1' x2='31' y2='9' stroke='%23888' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E"); + width: var(--code-window-windows-icon-width); + height: var(--code-window-icon-height); + background-image: var(--code-window-windows-icon); background-position: right; background-size: contain; background-repeat: no-repeat; } + +/* ============================================================================ + * Collapsible code window (
) + * ========================================================================= */ + +.code-with-filename.code-window-collapsible { + padding: 0; +} + +.code-with-filename.code-window-collapsible > summary.code-with-filename-file { + cursor: pointer; + list-style: none; + user-select: none; +} + +.code-with-filename.code-window-collapsible > summary.code-with-filename-file::-webkit-details-marker { + display: none; +} + +/* Bottom-border on summary only when expanded so the closed state has no + * separator line beneath an already-rounded container. */ +.code-with-filename.code-window-collapsible:not([open]) > summary.code-with-filename-file { + border-bottom: none; +} + +/* Round the title bar corners when the window is closed. */ +.code-with-filename.code-window-collapsible:not([open]) { + border-radius: 8px; +} + +.code-with-filename.code-window-collapsible:not([open]) > summary.code-with-filename-file { + border-radius: 8px; +} diff --git a/example.qmd b/example.qmd index 9e36ae9..492dbf3 100644 --- a/example.qmd +++ b/example.qmd @@ -114,6 +114,53 @@ extensions: {{< pagebreak >}} +## Highlighted Lines + +When a code block uses Quarto's `code-line-numbers` attribute, the line specification is shown as a chip next to the filename. + +```{.python filename="loader.py" code-line-numbers="1,4-5"} +import pandas as pd + +def load(path): + df = pd.read_csv(path) + return df.dropna() +``` + +Use `code-window-lines` to override the displayed text without affecting Quarto's highlighting: + +```{.python filename="custom.py" code-window-lines="2-3"} +def greet(name): + message = f"Hello, {name}!" + print(message) +``` + +Disable the chip globally with `lines-label: false` in the extension config. + +{{< pagebreak >}} + +## Collapsible Code Windows (HTML) + +Set `code-window-collapse` to wrap a code block in a `
` element. +The title bar becomes the ``. +This feature is HTML-only; Typst and PDF output is unaffected. + +### Closed by Default + +```{.python filename="hidden.py" code-window-collapse="closed"} +print("Click the title bar to expand.") +print("Useful for long boilerplate.") +``` + +### Open by Default + +```{.python filename="visible.py" code-window-collapse="open"} +print("Visible by default, can be collapsed.") +``` + +Apply collapsing to every code window by setting `collapse: closed` (or `open`) in the extension config. + +{{< pagebreak >}} + ## Window Styles Three decoration styles are available via the `style` option.