Skip to content

Commit 2d0a61c

Browse files
committed
parse and test new recipients field
1 parent 88ad981 commit 2d0a61c

5 files changed

Lines changed: 382 additions & 0 deletions

File tree

src/resources/filters/quarto-post/email.lua

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,127 @@ function str_truthy_falsy(str)
6565
return false
6666
end
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+
68189
local 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
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
title: Email Recipients Formats Test Document
3+
author: Jules Walzer-Goldfeld
4+
format: email
5+
---
6+
7+
Test document for various recipient input formats.
8+
9+
::: {.email}
10+
11+
::: {.subject}
12+
Line-Separated Recipients
13+
:::
14+
15+
::: {.recipients}
16+
alice@example.com
17+
bob@example.com
18+
charlie@example.com
19+
:::
20+
21+
::: {.email-text}
22+
Email with line-separated recipients (plain text, one per line).
23+
:::
24+
25+
First email with line-separated recipients.
26+
27+
:::
28+
29+
::: {.email}
30+
31+
::: {.subject}
32+
Comma-Separated Recipients
33+
:::
34+
35+
::: {.recipients}
36+
alice@example.com, bob@example.com, charlie@example.com
37+
:::
38+
39+
::: {.email-text}
40+
Email with comma-separated recipients (plain text).
41+
:::
42+
43+
Second email with comma-separated recipients.
44+
45+
:::
46+
47+
Done with test emails.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
title: Email Recipients Python Test Document
3+
author: Jules Walzer-Goldfeld
4+
format: email
5+
---
6+
7+
The report content. Anything that is here is not part of the email message.
8+
9+
::: {.email}
10+
11+
::: {.subject}
12+
Python Recipients Email
13+
:::
14+
15+
```{python}
16+
# Static recipients list
17+
recipients = ["alice@example.com", "bob@example.com", "charlie@example.com"]
18+
```
19+
20+
::: {.recipients}
21+
`{python} recipients`
22+
:::
23+
24+
::: {.email-text}
25+
Text version of email with Python recipients.
26+
:::
27+
28+
First email HTML content with Python recipients.
29+
30+
:::
31+
32+
::: {.email}
33+
34+
::: {.subject}
35+
Conditional Recipients Email (Weekday)
36+
:::
37+
38+
```{python}
39+
# Conditional recipients based on a fixed boolean (for deterministic testing)
40+
is_weekday = True # Fixed value for testing
41+
42+
if is_weekday:
43+
recipients = ["weekday@example.com", "team@example.com"]
44+
else:
45+
recipients = ["weekend@example.com"]
46+
```
47+
48+
::: {.recipients}
49+
`{python} recipients`
50+
:::
51+
52+
::: {.email-text}
53+
Text version of conditional recipients email.
54+
:::
55+
56+
Second email HTML content with conditional recipients.
57+
58+
:::
59+
60+
Any additional report content not part of the email message.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
title: Email Recipients R Test Document
3+
author: Jules Walzer-Goldfeld
4+
format: email
5+
---
6+
7+
The report content. Anything that is here is not part of the email message.
8+
9+
::: {.email}
10+
11+
::: {.subject}
12+
R Recipients Email
13+
:::
14+
15+
```{r}
16+
# Static recipients vector
17+
recipients <- c("alice@example.com", "bob@example.com", "charlie@example.com")
18+
```
19+
20+
::: {.recipients}
21+
`{r} recipients`
22+
:::
23+
24+
::: {.email-text}
25+
Text version of email with R recipients.
26+
:::
27+
28+
First email HTML content with R recipients.
29+
30+
:::
31+
32+
::: {.email}
33+
34+
::: {.subject}
35+
Conditional Recipients Email (R)
36+
:::
37+
38+
```{r}
39+
# Conditional recipients based on a fixed boolean (for deterministic testing)
40+
is_weekday <- TRUE # Fixed value for testing
41+
42+
if (is_weekday) {
43+
recipients <- c("weekday@example.com", "team@example.com")
44+
} else {
45+
recipients <- c("weekend@example.com")
46+
}
47+
```
48+
49+
::: {.recipients}
50+
`{r} recipients`
51+
:::
52+
53+
::: {.email-text}
54+
Text version of conditional recipients email (R).
55+
:::
56+
57+
Second email HTML content with conditional recipients (R).
58+
59+
:::
60+
61+
Any additional report content not part of the email message.

0 commit comments

Comments
 (0)