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
80 changes: 45 additions & 35 deletions docs/_extensions/mcanouil/atelier/_extension.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,59 @@
title: Atelier
author: Mickaël Canouil
version: 0.10.2
quarto-required: '>=1.9.36'
source: mcanouil/quarto-atelier@0.10.2
source-type: registry
version: 0.10.3
quarto-required: ">=1.9.36"
contributes:
project:
project:
type: website
output-dir: _site
website:
repo-actions: [edit, issue]
repo-link-target: _blank
repo-link-rel: noopener noreferrer
# `locale` matches `lang: en-GB` below; override the two together.
# Naming `twitter-card` at all is what switches Quarto's Twitter
# provider on, after which it inherits title, description, image, and
# image alt from each page.
open-graph:
locale: en_GB
twitter-card:
card-style: summary_large_image
page-navigation: true
back-to-top-navigation: true
llms-txt: true
search:
location: navbar
type: overlay
# `background` and `foreground` are left to the theme on both bars: the
# chrome palette in `html/chrome.scss` sets `$navbar-bg`, `$navbar-fg`,
# `$footer-bg`, and `$footer-fg` from a layer Quarto evaluates first, so
# naming them here would be a configuration with no effect.
navbar:
search: true
page-footer:
border: true
left: |
Powered by [Quarto](https://quarto.org){target="_blank" rel="noopener noreferrer"}.
center: |
© []{#current-year} [Mickaël CANOUIL](https://mickael.canouil.fr){target="_blank" rel="noopener noreferrer"}.
format: atelier-html
formats:
common:
lang: en-GB
date-format: dddd[, the] Do [of] MMMM, YYYY
date-format: "dddd[, the] Do [of] MMMM, YYYY"
code-copy: true
code-overflow: wrap
code-link: false
html:
respect-user-color-scheme: true
# Quarto builds the canonical link from `website.site-url`, giving a
# directory index the URL of its directory. Set `canonical-url: false`
# on a page served from more than one URL, such as `404.qmd`.
canonical-url: true
# `html/chrome.scss` comes before `brand` on purpose: Quarto evaluates
# user layer defaults in reverse list order, so a file placed first is
# evaluated last, after the brand palette it derives the chrome from.
theme:
light:
- html/chrome.scss
Expand Down Expand Up @@ -45,33 +84,4 @@ contributes:
- file: html/scripts/ordinal-dates.html
- file: html/scripts/a11y-fixes.html
- file: html/scripts/navbar-tooltips.html
project:
project:
type: website
output-dir: _site
website:
repo-actions:
- edit
- issue
repo-link-target: _blank
repo-link-rel: noopener noreferrer
open-graph:
locale: en_GB
twitter-card:
card-style: summary_large_image
page-navigation: true
back-to-top-navigation: true
llms-txt: true
search:
location: navbar
type: overlay
navbar:
search: true
page-footer:
border: true
left: |
Powered by [Quarto](https://quarto.org){target="_blank" rel="noopener noreferrer"}.
center: >
© []{#current-year} [Mickaël CANOUIL](https://mickael.canouil.fr){target="_blank" rel="noopener
noreferrer"}.
format: atelier-html
source: mcanouil/quarto-atelier@0.10.3
183 changes: 181 additions & 2 deletions docs/_extensions/mcanouil/atelier/_modules/string.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
--- @license MIT
--- @copyright 2026 Mickaël Canouil
--- @author Mickaël Canouil
--- @version 1.0.0
--- @version 1.1.0

local M = {}

Expand Down Expand Up @@ -76,6 +76,158 @@ function M.to_string(val)
return str ~= '' and str or nil
end

--- Strip one layer of surrounding bracket or punctuation characters.
--- Handles balanced pairs: () [] {} "" '' `` «»
--- Handles trailing-only punctuation: , . ; : ! ?
--- @param text string The input text
--- @return string prefix Characters stripped from the start (may be empty)
--- @return string inner The inner text after stripping
--- @return string suffix Characters stripped from the end (may be empty)
function M.strip_surrounding(text)
if not text or #text < 2 then
return '', text or '', ''
end

local balanced = {
['('] = ')', ['['] = ']', ['{'] = '}',
['"'] = '"', ["'"] = "'", ['`'] = '`',
}
-- UTF-8 guillemets
local first_two = text:sub(1, 2)
local last_two = text:sub(-2)
if first_two == '\xC2\xAB' and last_two == '\xC2\xBB' then
return first_two, text:sub(3, -3), last_two
end

local first = text:sub(1, 1)
local last = text:sub(-1)

if balanced[first] and last == balanced[first] then
return first, text:sub(2, -2), last
end

local trailing = {
[','] = true, ['.'] = true, [';'] = true,
[':'] = true, ['!'] = true, ['?'] = true,
}
if trailing[last] then
return '', text:sub(1, -2), last
end

return '', text, ''
end

--- Peel unbalanced surrounding brackets and trailing punctuation from a token.
--- Unlike `strip_surrounding`, this does not require a balanced pair: it removes
--- any run of leading opening-bracket characters and any run of trailing
--- closing-bracket or punctuation characters. This handles bracket groups that
--- Pandoc split across whitespace, e.g. "(#2," and "#3)" from "(#2, #3)".
--- Leading set: ( [ { " ' ` and the 2-byte UTF-8 « (\xC2\xAB).
--- Trailing set: ) ] } " ' ` , . ; : ! ? and the 2-byte UTF-8 » (\xC2\xBB).
--- @param text string The input text
--- @return string prefix Characters peeled from the start (may be empty)
--- @return string inner The inner text after peeling
--- @return string suffix Characters peeled from the end (may be empty)
function M.strip_edges(text)
if not text or text == '' then
return '', text or '', ''
end

local leading = {
['('] = true, ['['] = true, ['{'] = true,
['"'] = true, ["'"] = true, ['`'] = true,
}
local trailing = {
[')'] = true, [']'] = true, ['}'] = true,
['"'] = true, ["'"] = true, ['`'] = true,
[','] = true, ['.'] = true, [';'] = true,
[':'] = true, ['!'] = true, ['?'] = true,
}

local first = 1
local last = #text
local prefix = ''
local suffix = ''

while first <= last do
if text:sub(first, first + 1) == '\xC2\xAB' then
prefix = prefix .. '\xC2\xAB'
first = first + 2
elseif leading[text:sub(first, first)] then
prefix = prefix .. text:sub(first, first)
first = first + 1
else
break
end
end

while last >= first do
-- The two-byte window can only match a real «/» pair: the leading loop
-- never leaves \xC2 at first - 1 (openers are ASCII or the \xAB of a peeled
-- «), so the guillemet check cannot straddle the already-peeled prefix.
if last >= 2 and text:sub(last - 1, last) == '\xC2\xBB' then
suffix = '\xC2\xBB' .. suffix
last = last - 2
elseif trailing[text:sub(last, last)] then
suffix = text:sub(last, last) .. suffix
last = last - 1
else
break
end
end

return prefix, text:sub(first, last), suffix
end

--- Find a balanced bracket pair anywhere in the text and split around it.
--- Walks the text from `start_pos` looking for an opening bracket whose matching
--- closing bracket appears later in the string. Returns the text split into
--- a prefix (up to and including the opening bracket), the inner content, and
--- a suffix (closing bracket and everything after).
--- Supports the same bracket pairs as `strip_surrounding`:
--- () [] {} "" '' `` and the 2-byte UTF-8 guillemets «».
--- @param text string The input text
--- @param start_pos integer|nil Byte position to start searching from (default 1)
--- @return string|nil prefix Text up to and including the opening bracket
--- @return string|nil content Non-empty text between the brackets
--- @return string|nil suffix Closing bracket and trailing text
--- @return integer|nil open_pos Byte position of the opening bracket
function M.find_bracketed_content(text, start_pos)
if not text or #text < 2 then
return nil, nil, nil, nil
end
start_pos = start_pos or 1

local balanced = {
['('] = ')', ['['] = ']', ['{'] = '}',
['"'] = '"', ["'"] = "'", ['`'] = '`',
}

local i = start_pos
while i <= #text do
-- UTF-8 guillemet «…»
if text:sub(i, i + 1) == '\xC2\xAB' then
local close_pos = text:find('\xC2\xBB', i + 2, true)
if close_pos and close_pos > i + 2 then
return text:sub(1, i + 1), text:sub(i + 2, close_pos - 1), text:sub(close_pos), i
end
i = i + 2
else
local c = text:sub(i, i)
local close_char = balanced[c]
if close_char then
local close_pos = text:find(close_char, i + 1, true)
if close_pos and close_pos > i + 1 then
return text:sub(1, i), text:sub(i + 1, close_pos - 1), text:sub(close_pos), i
end
end
i = i + 1
end
end

return nil, nil, nil, nil
end

-- ============================================================================
-- ESCAPE UTILITIES
-- ============================================================================
Expand Down Expand Up @@ -106,10 +258,37 @@ function M.escape_typst(text)
end

--- Escape characters for Typst string literals (inside `"..."`).
--- Handles backslash, double quote, newline, carriage return, and tab.
--- @param text string The text to escape
--- @return string The escaped text safe for Typst string literals
function M.escape_typst_string(text)
return text:gsub('\\', '\\\\'):gsub('"', '\\"')
return (text
:gsub('\\', '\\\\')
:gsub('"', '\\"')
:gsub('\n', '\\n')
:gsub('\r', '\\r')
:gsub('\t', '\\t'))
end

--- Escape characters for JavaScript string literals (inside `"..."` or `'...'`).
--- Handles backslash, both quote styles, newlines, carriage returns, tabs,
--- form feeds, and the `</` sequence so payloads cannot break out of a
--- surrounding inline `<script>` block.
--- @param text string|nil The text to escape
--- @return string The escaped text safe for JavaScript string literals
--- @usage local safe = M.escape_js_string([[a "b" </script>]])
function M.escape_js_string(text)
if text == nil then return '' end
if type(text) ~= 'string' then text = tostring(text) end
return (text
:gsub('\\', '\\\\')
:gsub('"', '\\"')
:gsub("'", "\\'")
:gsub('\n', '\\n')
:gsub('\r', '\\r')
:gsub('\t', '\\t')
:gsub('\f', '\\f')
:gsub('</', '<\\/'))
end

--- Escape special Lua pattern characters for use in string.gsub.
Expand Down
31 changes: 31 additions & 0 deletions docs/_extensions/mcanouil/atelier/html/theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ $border-radius-sm: 0.375rem !default;
$border-radius-lg: 0.75rem !default;
$enable-smooth-scroll: true !default;

// `.btn-close-white` asks Bootstrap to invert the dismiss icon to force a light
// one. The icon follows the text around it instead, see DISMISS BUTTON below,
// so there is nothing left for the class to invert.
$btn-close-white-filter: none !default;

/*-- scss:functions --*/

// Mix the page background and foreground. brand injects different
Expand Down Expand Up @@ -756,6 +761,32 @@ body .navbar .gitlink-widget-dropdown {
}
}

// ===========================================================================
// DISMISS BUTTON
// ===========================================================================
// Bootstrap bakes black into the .btn-close icon and lightens it under
// [data-bs-theme="dark"], which Quarto never sets: it marks the scheme on the
// body and swaps the whole stylesheet bundle. The icon stayed black on the
// dark surface of a modal, an offcanvas, or a toast.
//
// Painting it from the page foreground would only move the problem. A
// contextual dismissible alert keeps its light tint in the dark bundle,
// because Bootstrap gates the dark tints on that same attribute, and a light
// icon is no more readable there than a black one is on a modal. The icon is
// masked from the text colour around it instead, so it follows the surface it
// sits on, whichever bundle is active.
.btn-close,
.btn-close:hover {
color: inherit;
}

.btn-close {
background-image: none;
background-color: currentcolor;
mask: var(--bs-btn-close-bg) center / $btn-close-width auto no-repeat;
-webkit-mask: var(--bs-btn-close-bg) center / $btn-close-width auto no-repeat;
}

// ===========================================================================
// 404 PAGE
// ===========================================================================
Expand Down
7 changes: 3 additions & 4 deletions docs/_extensions/mcanouil/gitlink/_extension.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
title: gitlink
author: Mickaël Canouil
version: 1.10.0
quarto-required: '>=1.9.38'
source: mcanouil/quarto-gitlink@1.10.0
source-type: registry
version: 1.10.1
quarto-required: ">=1.9.38"
contributes:
filters:
- path: gitlink.lua
at: post-quarto
source: mcanouil/quarto-gitlink@1.10.1
13 changes: 11 additions & 2 deletions docs/_extensions/mcanouil/gitlink/_modules/bitbucket.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
--- MC Bitbucket - Bitbucket-specific functionality for gitlink extension
--- @module bitbucket
--- @module "bitbucket"
--- @license MIT
--- @copyright 2026 Mickaël Canouil
--- @author Mickaël Canouil

local str = require("_modules/string")
--- Load a sibling module from the same directory as this file.
--- @param filename string The sibling module filename (e.g., 'string.lua')
--- @return table The loaded module
local function load_sibling(filename)
local source = debug.getinfo(1, 'S').source:sub(2)
local dir = source:match('(.*[/\\])') or ''
return require((dir .. filename):gsub('%.lua$', ''))
end

local str = load_sibling('string.lua')

local bitbucket_module = {}

Expand Down
2 changes: 1 addition & 1 deletion docs/_extensions/mcanouil/gitlink/_modules/git.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
--- MC Git - Git repository utilities for Quarto Lua filters and shortcodes
--- @module git
--- @module "git"
--- @license MIT
--- @copyright 2026 Mickaël Canouil
--- @author Mickaël Canouil
Expand Down
Loading