Skip to content

Commit fa119c9

Browse files
committed
feat: add multiple window decoration styles for code blocks
Introduce three styles: macOS traffic lights, Windows title bar buttons, and a plain filename bar. Update documentation and configuration options to support style overrides at both global and block levels.
1 parent b99f49e commit fa119c9

5 files changed

Lines changed: 285 additions & 59 deletions

File tree

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Code Window Extension for Quarto
22

3-
A Quarto extension that styles code blocks as macOS-style windows with traffic light buttons and a filename bar.
3+
A Quarto extension that adds window-style decorations to code blocks.
4+
Three styles are available: macOS traffic lights, Windows title bar buttons, or a plain filename bar.
45
Supports HTML, Reveal.js, and Typst formats.
56

67
## Installation
@@ -24,7 +25,8 @@ filters:
2425
2526
### Explicit Filename
2627
27-
Code blocks with a `filename` attribute display a window header with traffic light buttons and the filename right-aligned.
28+
Code blocks with a `filename` attribute display a window header with the filename.
29+
The decoration style depends on the `style` option.
2830

2931
````markdown
3032
```{.python filename="fibonacci.py"}
@@ -54,6 +56,7 @@ extensions:
5456
code-window:
5557
enabled: true
5658
auto-filename: true
59+
style: "macos"
5760
wrapper: "code-window"
5861
```
5962

@@ -63,8 +66,25 @@ extensions:
6366
| --------------- | ------- | --------------- | -------------------------------------------------------------------- |
6467
| `enabled` | boolean | `true` | Enable or disable the code-window filter. |
6568
| `auto-filename` | boolean | `true` | Automatically generate filename labels from the code block language. |
69+
| `style` | string | `"macos"` | Window decoration style: `"macos"`, `"windows"`, or `"default"`. |
6670
| `wrapper` | string | `"code-window"` | Typst wrapper function name for code-window rendering. |
6771

72+
### Styles
73+
74+
- **`"macos"`** (default): Traffic light buttons (red, yellow, green) on the left, filename on the right.
75+
- **`"windows"`**: Minimise, maximise, and close buttons on the right, filename on the left.
76+
- **`"default"`**: Plain filename on the left, no window decorations.
77+
78+
### Block-Level Style Override
79+
80+
Override the style for a single code block using the `code-window-style` attribute:
81+
82+
````markdown
83+
```{.python filename="example.py" code-window-style="windows"}
84+
print("Windows style for this block only")
85+
```
86+
````
87+
6888
### Typst Skylighting Hot-fix (Integrated)
6989

7090
`code-window` loads its Typst skylighting hot-fix internally from `_extensions/code-window/skylighting-typst-fix.lua`, so no second filter entry is required.

_extensions/code-window/_schema.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ options:
99
type: boolean
1010
default: true
1111
description: "Automatically generate filename labels from the code block language."
12+
style:
13+
type: string
14+
enum: ["default", "macos", "windows"]
15+
default: "macos"
16+
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."
1217
wrapper:
1318
type: string
1419
default: "code-window"

_extensions/code-window/code-window.lua

Lines changed: 153 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
--- @copyright 2026 Mickaël Canouil
44
--- @author Mickaël Canouil
55
--- @version 0.1.0
6-
--- @brief macOS-style code block window decorations
7-
--- @description Adds macOS-style window chrome (traffic lights and filename bar)
8-
--- to code blocks in HTML, Reveal.js, and Typst formats.
9-
--- Registered at pre-quarto to process all formats in a single pass.
6+
--- @brief Code block window decorations with multiple styles
7+
--- @description Adds window chrome (macOS traffic lights, Windows title bar
8+
--- buttons, or plain filename) to code blocks in HTML, Reveal.js, and Typst
9+
--- formats. Registered at pre-quarto to process all formats in a single pass.
1010

1111
-- ============================================================================
1212
-- EXTENSION NAME
@@ -21,11 +21,15 @@ local EXTENSION_NAME = 'code-window'
2121
--- @class CodeWindowConfig
2222
--- @field enabled boolean Whether code-window styling is enabled
2323
--- @field auto_filename boolean Whether to auto-generate filename from language
24+
--- @field style string Window decoration style ('macos', 'windows', 'default')
2425
--- @field typst_wrapper string Typst wrapper function name
2526

27+
local VALID_STYLES = { ['default'] = true, ['macos'] = true, ['windows'] = true }
28+
2629
local DEFAULT_CONFIG = {
2730
enabled = true,
2831
auto_filename = true,
32+
style = 'macos',
2933
typst_wrapper = 'code-window',
3034
}
3135

@@ -65,6 +69,7 @@ local function get_config(meta)
6569
local config = {
6670
enabled = DEFAULT_CONFIG.enabled,
6771
auto_filename = DEFAULT_CONFIG.auto_filename,
72+
style = DEFAULT_CONFIG.style,
6873
typst_wrapper = DEFAULT_CONFIG.typst_wrapper,
6974
}
7075

@@ -79,6 +84,17 @@ local function get_config(meta)
7984
if ext_config['auto-filename'] ~= nil then
8085
config.auto_filename = pandoc.utils.stringify(ext_config['auto-filename']) == 'true'
8186
end
87+
if ext_config.style ~= nil then
88+
local style_val = pandoc.utils.stringify(ext_config.style)
89+
if VALID_STYLES[style_val] then
90+
config.style = style_val
91+
else
92+
io.stderr:write(string.format(
93+
'[code-window] warning: unknown style "%s", falling back to "macos".\n',
94+
style_val
95+
))
96+
end
97+
end
8298
if ext_config.wrapper ~= nil then
8399
config.typst_wrapper = pandoc.utils.stringify(ext_config.wrapper)
84100
end
@@ -114,18 +130,106 @@ local function is_known_language(lang)
114130
return is_known
115131
end
116132

133+
-- ============================================================================
134+
-- BLOCK-LEVEL STYLE OVERRIDE
135+
-- ============================================================================
136+
137+
--- Read the block-level style override from code-window-style attribute.
138+
--- Returns the validated style value or nil.
139+
--- Strips the attribute from the block.
140+
--- @param block pandoc.CodeBlock Code block element
141+
--- @return string|nil Style override value
142+
local function read_block_style(block)
143+
local block_style = block.attributes['code-window-style']
144+
if not block_style or block_style == '' then
145+
return nil
146+
end
147+
block.attributes['code-window-style'] = nil
148+
if VALID_STYLES[block_style] then
149+
return block_style
150+
end
151+
io.stderr:write(string.format(
152+
'[code-window] warning: unknown block style "%s", using configured default.\n',
153+
block_style
154+
))
155+
return nil
156+
end
157+
117158
-- ============================================================================
118159
-- TYPST FUNCTION DEFINITION
119160
-- ============================================================================
120161

121162
--- Typst function definition for code-window rendering.
122163
--- Injected once at the start of the document body.
123164
local TYPST_FUNCTION_DEF = [==[
124-
#let code-window(content, filename: none, is-auto: false) = {
165+
#let code-window(content, filename: none, is-auto: false, style: "macos") = {
125166
let border-colour = luma(200)
126167
let surface-fill = luma(237)
127168
let muted-colour = luma(120)
128169
170+
let filename-label = if filename != none {
171+
text(
172+
size: if is-auto { 0.7em } else { 0.85em },
173+
weight: 500,
174+
fill: muted-colour,
175+
if is-auto { upper(filename) } else { filename },
176+
)
177+
}
178+
179+
let traffic-lights = box(
180+
inset: (right: 0.5em),
181+
stack(
182+
dir: ltr,
183+
spacing: 0.425em,
184+
circle(radius: 0.425em, fill: rgb("#ff5f56"), stroke: none),
185+
circle(radius: 0.425em, fill: rgb("#ffbd2e"), stroke: none),
186+
circle(radius: 0.425em, fill: rgb("#27c93f"), stroke: none),
187+
),
188+
)
189+
190+
let window-buttons = box(
191+
inset: (left: 0.5em),
192+
{
193+
set line(stroke: 1pt + muted-colour)
194+
stack(
195+
dir: ltr,
196+
spacing: 0.8em,
197+
// Minimise (horizontal line)
198+
box(width: 0.6em, height: 0.6em, align(horizon, line(length: 100%))),
199+
// Maximise (square)
200+
box(width: 0.6em, height: 0.6em, stroke: 1pt + muted-colour),
201+
// Close (x)
202+
box(width: 0.6em, height: 0.6em, {
203+
place(line(start: (0%, 0%), end: (100%, 100%)))
204+
place(line(start: (100%, 0%), end: (0%, 100%)))
205+
}),
206+
)
207+
},
208+
)
209+
210+
let title-bar = if style == "macos" {
211+
grid(
212+
columns: (auto, 1fr),
213+
align: (left + horizon, right + horizon),
214+
gutter: 0.5em,
215+
stroke: 0pt,
216+
traffic-lights,
217+
filename-label,
218+
)
219+
} else if style == "windows" {
220+
grid(
221+
columns: (1fr, auto),
222+
align: (left + horizon, right + horizon),
223+
gutter: 0.5em,
224+
stroke: 0pt,
225+
filename-label,
226+
window-buttons,
227+
)
228+
} else {
229+
// default: plain filename, left-aligned
230+
filename-label
231+
}
232+
129233
block(
130234
width: 100%,
131235
stroke: 1pt + border-colour,
@@ -140,32 +244,7 @@ local TYPST_FUNCTION_DEF = [==[
140244
radius: 0pt,
141245
stroke: (bottom: 1pt + border-colour),
142246
sticky: true,
143-
{
144-
grid(
145-
columns: (auto, 1fr),
146-
align: (left + horizon, right + horizon),
147-
gutter: 0.5em,
148-
stroke: 0pt,
149-
box(
150-
inset: (right: 0.5em),
151-
stack(
152-
dir: ltr,
153-
spacing: 0.425em,
154-
circle(radius: 0.425em, fill: rgb("#ff5f56"), stroke: none),
155-
circle(radius: 0.425em, fill: rgb("#ffbd2e"), stroke: none),
156-
circle(radius: 0.425em, fill: rgb("#27c93f"), stroke: none),
157-
),
158-
),
159-
if filename != none {
160-
text(
161-
size: if is-auto { 0.7em } else { 0.85em },
162-
weight: 500,
163-
fill: muted-colour,
164-
if is-auto { upper(filename) } else { filename },
165-
)
166-
},
167-
)
168-
},
247+
title-bar,
169248
)
170249
// Strip code block chrome so content fills flush against the window body.
171250
// set block() provides defaults for Skylighting blocks (explicit fill preserved).
@@ -204,6 +283,7 @@ local TYPST_FUNCTION_DEF = [==[
204283
--- @param block pandoc.CodeBlock Code block element
205284
--- @return pandoc.RawBlock|pandoc.CodeBlock Transformed or original block
206285
local function process_typst(block)
286+
local block_style = read_block_style(block)
207287
local explicit_filename = block.attributes['filename']
208288
local filename = explicit_filename
209289
local is_auto = false
@@ -219,6 +299,8 @@ local function process_typst(block)
219299
return block
220300
end
221301

302+
local effective_style = block_style or CONFIG.style
303+
222304
-- Render through Pandoc's Typst writer to preserve syntax highlighting.
223305
-- Pass highlight_method from the document's writer options so the
224306
-- user's chosen theme (e.g. github-dark) is respected.
@@ -232,10 +314,11 @@ local function process_typst(block)
232314
rendered = rendered:gsub('%s+$', '')
233315

234316
local typst_code = string.format(
235-
'#%s(filename: "%s", is-auto: %s)[\n%s\n]',
317+
'#%s(filename: "%s", is-auto: %s, style: "%s")[\n%s\n]',
236318
CONFIG.typst_wrapper,
237319
filename:gsub('"', '\\"'),
238320
is_auto and 'true' or 'false',
321+
effective_style,
239322
rendered
240323
)
241324

@@ -246,15 +329,23 @@ end
246329
-- HTML PROCESSING
247330
-- ============================================================================
248331

249-
--- Process CodeBlock for HTML/Reveal.js formats (auto-filename only).
250-
--- Blocks with explicit filenames are handled by Quarto; our CSS styles them.
332+
--- Process CodeBlock for HTML/Reveal.js formats.
333+
--- Explicit-filename blocks are returned for Quarto to wrap; a marker class
334+
--- is added when a block-level style override is present.
335+
--- Auto-filename blocks are wrapped directly with the style class.
251336
--- @param block pandoc.CodeBlock Code block element
252337
--- @return pandoc.Div|pandoc.CodeBlock Wrapped block or original
253338
local function process_html(block)
254-
-- Blocks with explicit filename are returned unchanged so Quarto
255-
-- creates the .code-with-filename Div between passes; our CSS styles it.
339+
local block_style = read_block_style(block)
256340
local explicit_filename = block.attributes['filename']
341+
257342
if explicit_filename and explicit_filename ~= '' then
343+
-- Let Quarto create the .code-with-filename wrapper.
344+
-- Add a marker class for block-level style override; the injected JS
345+
-- reads it and promotes it to the wrapper div.
346+
if block_style then
347+
table.insert(block.classes, 'cw-style-' .. block_style)
348+
end
258349
return block
259350
end
260351

@@ -267,6 +358,7 @@ local function process_html(block)
267358
end
268359

269360
local filename = block.classes[1]
361+
local effective_style = block_style or CONFIG.style
270362

271363
-- Normalise unknown languages to 'default' so Pandoc renders them
272364
-- with sourceCode wrapper, copy button, and consistent styling.
@@ -284,27 +376,49 @@ local function process_html(block)
284376

285377
return pandoc.Div(
286378
{ filename_header, block },
287-
pandoc.Attr('', { 'code-with-filename', 'code-window-auto' })
379+
pandoc.Attr('', { 'code-with-filename', 'code-window-' .. effective_style, 'code-window-auto' })
288380
)
289381
end
290382

291383
-- ============================================================================
292384
-- FILTER FUNCTIONS
293385
-- ============================================================================
294386

295-
--- Load configuration and inject CSS dependency.
387+
--- Generate a JS snippet that adds the configured default style class
388+
--- to Quarto-created .code-with-filename wrappers (explicit filenames)
389+
--- and promotes block-level cw-style-* marker classes.
390+
--- @param default_style string The configured default style
391+
--- @return string JavaScript code
392+
local function make_style_js(default_style)
393+
return string.format([=[
394+
document.addEventListener("DOMContentLoaded",function(){
395+
document.querySelectorAll(".code-with-filename").forEach(function(el){
396+
if(/\bcode-window-(macos|windows|default)\b/.test(el.className))return;
397+
var c=el.querySelector('[class*="cw-style-"]');
398+
if(c){var m=c.className.match(/cw-style-(\w+)/);if(m){el.classList.add("code-window-"+m[1]);return;}}
399+
el.classList.add("code-window-%s");
400+
});
401+
});]=], default_style)
402+
end
403+
404+
--- Load configuration and inject CSS/JS dependencies.
296405
function Meta(meta)
297406
CURRENT_FORMAT = get_format()
298407
CONFIG = get_config(meta)
299408

300-
-- Inject CSS for HTML/Reveal.js (idempotent by name across passes)
409+
-- Inject CSS and JS for HTML/Reveal.js (idempotent by name across passes)
301410
if (CURRENT_FORMAT == 'html' or CURRENT_FORMAT == 'revealjs')
302411
and CONFIG and CONFIG.enabled then
303412
quarto.doc.add_html_dependency({
304413
name = EXTENSION_NAME,
305414
version = '0.1.0',
306415
stylesheets = { 'style.css' },
307416
})
417+
quarto.doc.add_html_dependency({
418+
name = EXTENSION_NAME .. '-style-init',
419+
version = '0.1.0',
420+
head = '<script>' .. make_style_js(CONFIG.style) .. '</script>',
421+
})
308422
end
309423

310424
return meta

0 commit comments

Comments
 (0)