@@ -65,6 +65,127 @@ function str_truthy_falsy(str)
6565 return false
6666end
6767
68+ -- Parse recipients from inline code output or plain text
69+ -- Supports multiple formats:
70+ -- 1. Python list: ['a', 'b'] or ["a", "b"]
71+ -- 2. R vector: "a" "b" "c"
72+ -- 3. Comma-separated: a, b, c
73+ -- 4. Line-separated: a\nb\nc
74+ -- Returns an empty array if parsing fails
75+ function parse_recipients (recipient_str )
76+ recipient_str = str_trunc_trim (recipient_str , 10000 )
77+
78+ if recipient_str == " " then
79+ return {}
80+ end
81+
82+ local recipients = {}
83+
84+ -- Try Python list format ['...', '...'] or ["...", "..."]
85+ if string.match (recipient_str , " ^%[" ) and string.match (recipient_str , " %]$" ) then
86+ local content = string.sub (recipient_str , 2 , - 2 )
87+
88+ -- Try to parse as Python/R list by splitting on commas
89+ -- and stripping quotes and brackets from each item
90+ recipients = {}
91+ for item in string.gmatch (content , " [^,]+" ) do
92+ local trimmed = str_trunc_trim (item , 1000 )
93+ -- Strip leading/trailing brackets
94+ trimmed = string.gsub (trimmed , " ^%[" , " " )
95+ trimmed = string.gsub (trimmed , " %]$" , " " )
96+ trimmed = str_trunc_trim (trimmed , 1000 )
97+
98+ -- Strip leading/trailing quotes (ASCII single/double and UTF-8 curly quotes)
99+ -- ASCII single quote '
100+ trimmed = string.gsub (trimmed , " ^'" , " " )
101+ trimmed = string.gsub (trimmed , " '$" , " " )
102+ -- ASCII double quote "
103+ trimmed = string.gsub (trimmed , ' ^"' , " " )
104+ trimmed = string.gsub (trimmed , ' "$' , " " )
105+ -- UTF-8 curly single quotes ' and ' (U+2018, U+2019)
106+ trimmed = string.gsub (trimmed , " ^" .. string.char (226 , 128 , 152 ), " " )
107+ trimmed = string.gsub (trimmed , string.char (226 , 128 , 153 ) .. " $" , " " )
108+ -- UTF-8 curly double quotes " and " (U+201C, U+201D)
109+ trimmed = string.gsub (trimmed , " ^" .. string.char (226 , 128 , 156 ), " " )
110+ trimmed = string.gsub (trimmed , string.char (226 , 128 , 157 ) .. " $" , " " )
111+
112+ trimmed = str_trunc_trim (trimmed , 1000 )
113+ if trimmed ~= " " then
114+ table.insert (recipients , trimmed )
115+ end
116+ end
117+ if # recipients > 0 then
118+ return recipients
119+ end
120+ end
121+
122+ -- Try R-style quoted format (space-separated quoted strings outside of brackets)
123+ recipients = {}
124+ local found_any = false
125+
126+ -- Try single quotes: 'a' 'b' 'c'
127+ for quoted_pair in string.gmatch (recipient_str , " '([^']*)'" ) do
128+ local trimmed = str_trunc_trim (quoted_pair , 1000 )
129+ if trimmed ~= " " then
130+ table.insert (recipients , trimmed )
131+ found_any = true
132+ end
133+ end
134+ if found_any then
135+ return recipients
136+ end
137+
138+ -- Try double quotes: "a" "b" "c"
139+ recipients = {}
140+ for quoted_pair in string.gmatch (recipient_str , ' "([^"]*)"' ) do
141+ local trimmed = str_trunc_trim (quoted_pair , 1000 )
142+ if trimmed ~= " " then
143+ table.insert (recipients , trimmed )
144+ found_any = true
145+ end
146+ end
147+ if found_any then
148+ return recipients
149+ end
150+
151+ -- Try line-separated format (newlines or spaces)
152+ -- Check if there are newlines or multiple space-separated emails
153+ if string.match (recipient_str , " \n " ) or
154+ (string.match (recipient_str , " @.*%s+.*@" ) and not string.match (recipient_str , " ," )) then
155+ recipients = {}
156+ -- Split on newlines or spaces
157+ for item in string.gmatch (recipient_str , " [^\n %s]+" ) do
158+ local trimmed = str_trunc_trim (item , 1000 )
159+ if trimmed ~= " " and string.match (trimmed , " @" ) then
160+ table.insert (recipients , trimmed )
161+ found_any = true
162+ end
163+ end
164+ if found_any then
165+ return recipients
166+ end
167+ end
168+
169+ -- Try comma-separated format without quotes
170+ -- Split by comma and trim each part
171+ recipients = {}
172+ found_any = false
173+ for part in string.gmatch (recipient_str , " [^,]+" ) do
174+ local trimmed = str_trunc_trim (part , 1000 )
175+ if trimmed ~= " " and not string.match (trimmed , " ^[%[%]]" ) then
176+ table.insert (recipients , trimmed )
177+ found_any = true
178+ end
179+ end
180+ if found_any then
181+ return recipients
182+ end
183+
184+ -- Could not parse - log warning and return empty
185+ quarto .log .warning (" Could not parse recipients format: " .. recipient_str )
186+ return {}
187+ end
188+
68189local html_email_template_1 = [[
69190<!DOCTYPE html>
70191<html>
@@ -254,6 +375,7 @@ function process_div(div)
254375 image_tbl = {},
255376 email_images = {},
256377 suppress_scheduled_email = nil , -- nil means not set
378+ recipients = {},
257379 attachments = {}
258380 }
259381
@@ -270,6 +392,8 @@ function process_div(div)
270392 local email_scheduled_str = str_trunc_trim (string.lower (pandoc .utils .stringify (child )), 10 )
271393 local scheduled_email = str_truthy_falsy (email_scheduled_str )
272394 current_email .suppress_scheduled_email = not scheduled_email
395+ elseif child .classes :includes (" recipients" ) then
396+ current_email .recipients = parse_recipients (pandoc .utils .stringify (child ))
273397 else
274398 table.insert (remaining_content , child )
275399 end
@@ -508,6 +632,11 @@ function process_document(doc)
508632 send_report_as_attachment = false
509633 }
510634
635+ -- Only add recipients if present
636+ if not is_empty_table (email_obj .recipients ) then
637+ email_json_obj .recipients = email_obj .recipients
638+ end
639+
511640 -- Only add images if present
512641 if not is_empty_table (email_obj .email_images ) then
513642 email_json_obj .images = email_obj .email_images
0 commit comments