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