Skip to content

Commit 3cc7b12

Browse files
committed
first pass composing library
nearly identical in an abstract sense to blastula, but content is wrapped in mjml tags instead of my own custom html template.
1 parent b9a53e6 commit 3cc7b12

File tree

6 files changed

+864
-2
lines changed

6 files changed

+864
-2
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ preview:
55
cd docs && quarto preview
66

77
test:
8-
pytest nbmail/tests nbmail/mjml/tests --cov-report=xml
8+
pytest nbmail/tests nbmail/mjml/tests nbmail/compose/tests --cov-report=xml
99

1010
test-update:
11-
pytest nbmail/tests nbmail/mjml/tests --snapshot-update
11+
pytest nbmail/tests nbmail/mjml/tests nbmail/compose/tests --snapshot-update
1212

1313
generate-mjml-tags:
1414
python3 nbmail/mjml/scripts/generate_tags.py

nbmail/compose/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from .compose import compose_email, create_blocks
2+
from .blocks import block_text, block_title, block_spacer
3+
from .inline_utils import (
4+
add_image,
5+
add_plot,
6+
md,
7+
add_cta_button,
8+
add_readable_time,
9+
)
10+
11+
__all__ = (
12+
"compose_email",
13+
"create_blocks",
14+
"block_text",
15+
"block_title",
16+
"block_spacer",
17+
"add_image",
18+
"add_plot",
19+
"md",
20+
"add_cta_button",
21+
"add_readable_time",
22+
)

nbmail/compose/blocks.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
from typing import Union
2+
from nbmail.mjml.tags import section, column, text as mjml_text, spacer as mjml_spacer
3+
from nbmail.mjml._core import MJMLTag
4+
from .inline_utils import _process_markdown
5+
from typing import Literal
6+
7+
8+
__all__ = [
9+
"Block",
10+
"BlockList",
11+
"block_text",
12+
"block_title",
13+
"block_spacer",
14+
]
15+
16+
17+
class Block:
18+
"""
19+
Represents a single block component in an email.
20+
21+
Parameters
22+
----------
23+
mjml_tag
24+
The underlying MJML tag
25+
"""
26+
27+
def __init__(self, mjml_tag: MJMLTag):
28+
"""
29+
Internal constructor. Users create blocks via `block_*()` functions.
30+
31+
Parameters
32+
----------
33+
mjml_tag
34+
The underlying MJML tag
35+
"""
36+
self._mjml_tag = mjml_tag
37+
38+
def _to_mjml(self) -> MJMLTag:
39+
"""
40+
Internal method: retrieve the underlying MJML tag for processing.
41+
42+
Returns
43+
-------
44+
MJMLTag
45+
The underlying MJML tag structure.
46+
"""
47+
return self._mjml_tag
48+
49+
50+
class BlockList:
51+
"""
52+
Container for multiple block components.
53+
54+
Parameters
55+
----------
56+
*args
57+
One or more Block objects or strings (which will be converted to blocks).
58+
59+
Examples
60+
--------
61+
Users typically create BlockList via the `create_blocks()` function:
62+
63+
```python
64+
from nbmail.compose import create_blocks, block_text, block_title
65+
66+
content = create_blocks(
67+
block_title("My Email"),
68+
block_text("Hello world!")
69+
)
70+
```
71+
"""
72+
73+
def __init__(self, *args: Union["Block", str]):
74+
"""
75+
Parameters
76+
----------
77+
*args
78+
One or more `Block` objects or strings.
79+
"""
80+
self.items = list(args)
81+
82+
def _to_mjml_list(self) -> list[MJMLTag]:
83+
"""
84+
Internal method: Convert all blocks to MJML tags.
85+
86+
Used by `compose_email()`.
87+
88+
Returns
89+
-------
90+
list[MJMLTag]
91+
A list of MJML tag structures.
92+
"""
93+
result = []
94+
for item in self.items:
95+
if isinstance(item, Block):
96+
result.append(item._to_mjml())
97+
elif isinstance(item, str):
98+
html = _process_markdown(item)
99+
100+
# Create a simple text block from the string
101+
mjml_tree = section(
102+
column(mjml_text(content=html)),
103+
attributes={"padding": "0px"}, # TODO check what happens if we remove this
104+
)
105+
result.append(mjml_tree)
106+
return result
107+
108+
def __repr__(self) -> str:
109+
return f"<BlockList: {len(self.items)} items>"
110+
111+
112+
def block_text(
113+
text: str, align: Literal["left", "center", "right", "justify"] = "left"
114+
) -> Block:
115+
"""
116+
Create a block of text (supports Markdown).
117+
118+
Parameters
119+
----------
120+
text
121+
Plain text or Markdown. Markdown will be converted to HTML.
122+
123+
align
124+
Text alignment. Default is `"left"`.
125+
126+
Returns
127+
-------
128+
Block
129+
A block containing the formatted text.
130+
131+
Examples
132+
--------
133+
```python
134+
from nbmail.compose import block_text
135+
136+
# Simple text
137+
block = block_text("Hello world")
138+
139+
# Markdown text
140+
block = block_text("This is **bold** and this is *italic*")
141+
142+
# Centered text
143+
block = block_text("Centered content", align="center")
144+
```
145+
"""
146+
html = _process_markdown(text)
147+
148+
mjml_tree = section(
149+
column(
150+
mjml_text(content=html, attributes={"align": align}),
151+
),
152+
attributes={"padding": "0px"},
153+
)
154+
155+
return Block(mjml_tree)
156+
157+
158+
def block_title(
159+
title: str, align: Literal["left", "center", "right", "justify"] = "center"
160+
) -> Block:
161+
"""
162+
Create a block of large, emphasized text for headings.
163+
164+
Parameters
165+
----------
166+
title
167+
The title text. Markdown will be converted to HTML.
168+
align
169+
Text alignment. Default is "center".
170+
171+
Returns
172+
-------
173+
Block
174+
A block containing the formatted title.
175+
176+
Examples
177+
--------
178+
```python
179+
from nbmail.compose import block_title
180+
181+
# Simple title
182+
title = block_title("My Newsletter")
183+
184+
# Centered title (default)
185+
title = block_title("Welcome!", align="center")
186+
```
187+
"""
188+
189+
html = _process_markdown(title)
190+
html_wrapped = (
191+
f'<h1 style="margin: 0; font-size: 32px; font-weight: 300;">{html}</h1>'
192+
)
193+
194+
mjml_tree = section(
195+
column(
196+
mjml_text(
197+
content=html_wrapped,
198+
attributes={"align": align},
199+
)
200+
),
201+
attributes={"padding": "0px"},
202+
)
203+
204+
return Block(mjml_tree)
205+
206+
207+
def block_spacer(height: str = "20px") -> Block:
208+
"""
209+
Insert vertical spacing.
210+
211+
Parameters
212+
----------
213+
height
214+
The height of the spacer. Can be any valid CSS height value (e.g., "20px", "2em").
215+
Default is "20px".
216+
217+
Returns
218+
-------
219+
Block
220+
A block containing the spacer.
221+
222+
Examples
223+
--------
224+
```python
225+
from nbmail.compose import block_spacer, create_blocks, block_text
226+
227+
email_body = create_blocks(
228+
block_text("First section"),
229+
block_spacer("30px"),
230+
block_text("Second section"),
231+
)
232+
```
233+
"""
234+
mjml_tree = section(
235+
column(
236+
mjml_spacer(attributes={"height": height}),
237+
),
238+
attributes={"padding": "0px"},
239+
)
240+
241+
return Block(mjml_tree)

0 commit comments

Comments
 (0)