Skip to content

Commit 1e371b0

Browse files
committed
initial version of chapters
1 parent 04674b6 commit 1e371b0

21 files changed

+5826
-336
lines changed

.github/workflows/publish.yml

+1-21
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,9 @@ jobs:
3333
uses: quarto-dev/quarto-actions/setup@v2
3434
with:
3535
tinytex: true
36-
37-
# optional step when there is python code to execute
38-
- name: Setup Python
39-
uses: conda-incubator/setup-miniconda@v2
40-
with:
41-
python-version: 3.8
42-
channels: conda-forge
43-
miniforge-variant: Mambaforge
44-
activate-environment: quarto-import
45-
environment-file: _import/environment.yml
46-
47-
# optional steps when Rmarkdown code is used
48-
- name: Set up R
49-
uses: r-lib/actions/setup-r@v2
50-
- name: Install packages
51-
uses: r-lib/actions/setup-r-dependencies@v2
52-
with:
53-
packages: |
54-
any::knitr
55-
any::rmarkdown
5636

5737
- name: Setup Pages
58-
uses: actions/configure-pages@v1
38+
uses: actions/configure-pages@v5
5939

6040
- name: Render Website
6141
run: quarto render book/

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
/book/.quarto/
2+
/book/_site
3+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
title: custom-callout
2+
author: James Joseph Balamuta
3+
version: 0.0.1-dev.2
4+
quarto-required: ">=1.5.0"
5+
contributes:
6+
filters:
7+
- customcallout.lua
8+
- fa.lua
9+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---@meta
2+
3+
---@class CustomCalloutDefinition
4+
---@field type string The type/identifier of the callout
5+
---@field title string|pandoc.Inlines|nil The title of the callout
6+
---@field icon boolean|nil Whether to show an icon
7+
---@field appearance string|nil The appearance style ('default', 'minimal', 'simple')
8+
---@field collapse boolean|string|nil Whether the callout is collapsible
9+
---@field icon_symbol string|nil Custom icon symbol or font awesome icon
10+
---@field color string|nil The color of the callout
11+
---@field background_color string|nil The background color of the callout
12+
13+
---@class CustomCalloutsMap
14+
15+
-- Global variable to store custom callout definitions
16+
---@type table<string, CustomCalloutDefinition>
17+
local customCallouts = {}
18+
19+
local fa = require("fa")
20+
21+
---Converts a valid CSS color string or hexadecimal to RGBA format
22+
---@param color string The color in hex (#RRGGBB) or named format
23+
---@param alpha number The alpha value between 0 and 1
24+
---@return string rgba The color in rgba() or rgb(from color) format
25+
local function colorToRgba(color, alpha)
26+
if color:sub(1,1) == "#" then
27+
local r = tonumber(color:sub(2,3), 16)
28+
local g = tonumber(color:sub(4,5), 16)
29+
local b = tonumber(color:sub(6,7), 16)
30+
return string.format("rgba(%d, %d, %d, %.2f)", r, g, b, alpha)
31+
else
32+
-- For named colors, we use the functional notation of rgba()
33+
return string.format("rgb(from %s r g b / %.0f%%)", color, alpha * 100)
34+
end
35+
end
36+
37+
---Checks if a string represents a Font Awesome icon
38+
---@param icon string|nil The icon string to check
39+
---@return boolean is_fa True if the string starts with "fa-"
40+
local function isFontAwesomeIcon(icon)
41+
return icon ~= nil and icon:sub(1, 3) == "fa-"
42+
end
43+
44+
---Generates CSS for all defined custom callouts
45+
---@return string css The generated CSS rules
46+
local function generateCustomCSS()
47+
local css = ""
48+
49+
-- Translate YAML callout information for custom callouts
50+
for type, callout in pairs(customCallouts) do
51+
if callout.color then
52+
local color = pandoc.utils.stringify(callout.color)
53+
54+
-- Base color
55+
css = css .. string.format("div.callout-%s.callout {\n", type)
56+
css = css .. string.format(" border-left-color: %s;\n", color)
57+
css = css .. "}\n"
58+
59+
-- Header background
60+
css = css .. string.format("div.callout-%s.callout-style-default > .callout-header {\n", type)
61+
css = css .. string.format(" background-color: %s;\n", colorToRgba(color, 0.13))
62+
css = css .. "}\n"
63+
64+
-- Collapse Icon
65+
css = css .. string.format("div.callout-%s .callout-toggle::before {", type)
66+
css = css .. " background-image: url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"rgb(33, 37, 41)\" class=\"bi bi-chevron-down\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z\"/></svg>');"
67+
css = css .. "}\n"
68+
69+
-- Icon Styling
70+
css = css .. string.format("div.callout-%s.callout-style-default .callout-icon::before, div.callout-%s.callout-titled .callout-icon::before {\n", type, type)
71+
72+
if callout.icon_symbol then
73+
local icon_symbol_str = pandoc.utils.stringify(callout.icon_symbol)
74+
if isFontAwesomeIcon(icon_symbol_str) then
75+
-- Font Awesome icon
76+
css = css .. " font-family: 'Font Awesome 6 Free', FontAwesome;\n"
77+
css = css .. " font-style: normal;\n"
78+
css = css .. string.format(" content: '%s' !important;\n", fa.fa_unicode(icon_symbol_str))
79+
else
80+
-- Custom icon symbol
81+
css = css .. string.format(" content: '%s';\n", icon_symbol_str)
82+
end
83+
css = css .. " background-image: none;\n"
84+
else
85+
-- The fallback case
86+
local escapedColor = color:gsub("#", "%%23") -- Escape # in hex colors
87+
css = css .. string.format(" background-image: url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"%s\" class=\"bi bi-exclamation-triangle\" viewBox=\"0 0 16 16\"><path d=\"M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z\"/></svg>');\n", escapedColor)
88+
end
89+
90+
css = css .. "}\n"
91+
92+
end
93+
end
94+
return css
95+
end
96+
97+
98+
---Parses custom callout definitions from document metadata
99+
---@param meta pandoc.Meta The document metadata
100+
local function parseCustomCallouts(meta)
101+
if not meta['custom-callout'] then return end
102+
103+
for k, v in pairs(meta['custom-callout']) do
104+
if type(v) == "table" then
105+
customCallouts[k] = {
106+
type = tostring(k),
107+
title = v.title or k:gsub("^%l", string.upper),
108+
icon = v.icon == 'true' or nil,
109+
appearance = v.appearance or nil,
110+
collapse = v.collapse or nil,
111+
icon_symbol = v['icon-symbol'] or nil,
112+
color = v.color or nil,
113+
background_color = v['background-color'] or nil
114+
}
115+
end
116+
end
117+
118+
119+
-- Generate and add custom CSS to the document
120+
local customCSS = generateCustomCSS()
121+
if customCSS ~= "" then
122+
quarto.doc.include_text('in-header', '<style>\n' .. customCSS .. '</style>')
123+
end
124+
125+
end
126+
127+
128+
---Converts a div to a custom callout if it matches a defined custom callout
129+
---@param div pandoc.Div The div to potentially convert
130+
---@return pandoc.Div|quarto.Callout converted The converted callout or original div
131+
local function convertToCustomCallout(div)
132+
-- Check if the div has classes
133+
for _, class in ipairs(div.classes) do
134+
135+
-- Check if the class matches a custom callout
136+
local callout = customCallouts[class]
137+
138+
if callout then
139+
-- Use the default title if not provided
140+
local title = callout.title
141+
142+
-- Check to see if the title is specified in the div content
143+
if div.content[1] ~= nil and div.content[1].t == "Header" then
144+
title = div.content[1]
145+
div.content:remove(1)
146+
end
147+
148+
-- Create a new Callout with the custom callout parameters
149+
local calloutParams = {
150+
type = callout.type,
151+
content = div.content,
152+
title = div.attributes.title or title,
153+
icon = div.attributes.icon or callout.icon,
154+
appearance = div.attributes.appearance or callout.appearance,
155+
collapse = div.attributes.collapse or callout.collapse
156+
}
157+
158+
return quarto.Callout(calloutParams)
159+
end
160+
end
161+
162+
163+
return div
164+
end
165+
166+
---Walks the Pandoc document and processes divs to
167+
---convert to custom callouts
168+
---@class pandoc.Doc
169+
---@field blocks pandoc.Blocks
170+
---@param doc pandoc.Doc The Pandoc document
171+
---@return pandoc.Doc doc The processed document
172+
local function customCalloutFilter(doc)
173+
174+
-- Walk the AST and process divs
175+
doc.blocks = doc.blocks:walk({
176+
Div = convertToCustomCallout
177+
})
178+
179+
-- Return the modified document
180+
return doc
181+
end
182+
183+
-- Return the Pandoc filter
184+
return {
185+
---@type fun(meta: pandoc.Meta)
186+
Meta = parseCustomCallouts,
187+
---@type fun(doc: pandoc.Doc): pandoc.Doc
188+
Pandoc = customCalloutFilter
189+
}

0 commit comments

Comments
 (0)