inkletter 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CrocoCode.co
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: inkletter
3
+ Version: 0.1.0
4
+ Summary: Markdown to MJML/HTML converter and previewer
5
+ Author-email: Laurent Pauloin <laurent@crococode.co>
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: click==8.2.1
10
+ Requires-Dist: jinja2==3.1.6
11
+ Requires-Dist: mistune==3.1.3
12
+ Requires-Dist: mjml-python==1.3.5
13
+ Dynamic: license-file
14
+
15
+ # Inkletter
16
+
17
+ **Write your emails like prose, send them like a pro.**
18
+
19
+ Inkletter is a magical little tool that transforms your plain Markdown files into beautiful,
20
+ responsive MJML and HTML email layouts, ready to be previewed, shared or sent to the world.
21
+
22
+ ## Why Inkletter?
23
+
24
+ Because writing HTML emails by hand is like ironing socks: pointless and painful.
25
+
26
+ With Inkletter, you write your content in **Markdown** (like a decent human being),
27
+ and we turn it into **gorgeous, mobile-friendly HTML emails** powered by MJML.
28
+
29
+ Built with care, clarity and just enough elegance,
30
+ the kind that pairs well with good typography and strong espresso.
31
+
32
+ ## Features
33
+
34
+ - ✅ Convert Markdown to MJML
35
+ - ✅ Convert Markdown to full responsive HTML
36
+ - ✅ Live preview in your browser
37
+ - ✅ Device simulator (iPhone, iPad, Desktop, etc.)
38
+ - ✅ One command, zero headache
39
+ - ✅ No vendor lock-in, fully offline
40
+
41
+ ## ✨ Live Samples
42
+
43
+ Explore the included examples to see Inkletter in action:
44
+ - 🔍 [sample.md](sample/sample.md) — the original Markdown source
45
+ - 💎 [sample.html](sample/sample.html) — the final responsive HTML output
46
+ - 🎨 [preview.html](sample/preview.html) — the interactive split preview (Markdown / MJML / Rendered)
47
+
48
+ ## Installation
49
+
50
+ Make sure you have Python 3.8+ installed on your system.
51
+
52
+ Then in your terminal:
53
+
54
+ ```bash
55
+ pip install inkletter
56
+ ```
57
+
58
+ Or for development:
59
+
60
+ ```bash
61
+ git clone https://github.com/your-name/inkletter.git
62
+ cd inkletter
63
+ pip install -e .
64
+ ```
65
+
66
+ ## How it works
67
+
68
+ ### 1. Preview your Markdown as a responsive email
69
+
70
+ ```bash
71
+ inkletter preview yourfile.md
72
+ ```
73
+
74
+ This opens a split view in your browser with:
75
+ - Your original Markdown
76
+ - The generated MJML
77
+ - A live rendered email with responsive device preview
78
+
79
+ Yes, even iPhone 14. You’re welcome.
80
+
81
+
82
+ ### 2. Convert and export to HTML
83
+
84
+ ```bash
85
+ inkletter md2mjml input.md -o output.html –view
86
+ ```
87
+
88
+ This will:
89
+ - Convert the Markdown to MJML
90
+ - Render it into clean HTML
91
+ - Save the HTML to `output.html`
92
+ - Optionally open it in your browser with `–view`
93
+
94
+ No fuss. No noise. Just results.
95
+
96
+
97
+ ## Contributing
98
+
99
+ French or not, you are welcome to contribute.
100
+ Fork it, branch it, test it, PR it — with love.
101
+
102
+ ## License
103
+
104
+ MIT — but don’t forget to say “merci” 😉
105
+
106
+ ### Made with ❤️ and `markdown` in France.
@@ -0,0 +1,92 @@
1
+ # Inkletter
2
+
3
+ **Write your emails like prose, send them like a pro.**
4
+
5
+ Inkletter is a magical little tool that transforms your plain Markdown files into beautiful,
6
+ responsive MJML and HTML email layouts, ready to be previewed, shared or sent to the world.
7
+
8
+ ## Why Inkletter?
9
+
10
+ Because writing HTML emails by hand is like ironing socks: pointless and painful.
11
+
12
+ With Inkletter, you write your content in **Markdown** (like a decent human being),
13
+ and we turn it into **gorgeous, mobile-friendly HTML emails** powered by MJML.
14
+
15
+ Built with care, clarity and just enough elegance,
16
+ the kind that pairs well with good typography and strong espresso.
17
+
18
+ ## Features
19
+
20
+ - ✅ Convert Markdown to MJML
21
+ - ✅ Convert Markdown to full responsive HTML
22
+ - ✅ Live preview in your browser
23
+ - ✅ Device simulator (iPhone, iPad, Desktop, etc.)
24
+ - ✅ One command, zero headache
25
+ - ✅ No vendor lock-in, fully offline
26
+
27
+ ## ✨ Live Samples
28
+
29
+ Explore the included examples to see Inkletter in action:
30
+ - 🔍 [sample.md](sample/sample.md) — the original Markdown source
31
+ - 💎 [sample.html](sample/sample.html) — the final responsive HTML output
32
+ - 🎨 [preview.html](sample/preview.html) — the interactive split preview (Markdown / MJML / Rendered)
33
+
34
+ ## Installation
35
+
36
+ Make sure you have Python 3.8+ installed on your system.
37
+
38
+ Then in your terminal:
39
+
40
+ ```bash
41
+ pip install inkletter
42
+ ```
43
+
44
+ Or for development:
45
+
46
+ ```bash
47
+ git clone https://github.com/your-name/inkletter.git
48
+ cd inkletter
49
+ pip install -e .
50
+ ```
51
+
52
+ ## How it works
53
+
54
+ ### 1. Preview your Markdown as a responsive email
55
+
56
+ ```bash
57
+ inkletter preview yourfile.md
58
+ ```
59
+
60
+ This opens a split view in your browser with:
61
+ - Your original Markdown
62
+ - The generated MJML
63
+ - A live rendered email with responsive device preview
64
+
65
+ Yes, even iPhone 14. You’re welcome.
66
+
67
+
68
+ ### 2. Convert and export to HTML
69
+
70
+ ```bash
71
+ inkletter md2mjml input.md -o output.html –view
72
+ ```
73
+
74
+ This will:
75
+ - Convert the Markdown to MJML
76
+ - Render it into clean HTML
77
+ - Save the HTML to `output.html`
78
+ - Optionally open it in your browser with `–view`
79
+
80
+ No fuss. No noise. Just results.
81
+
82
+
83
+ ## Contributing
84
+
85
+ French or not, you are welcome to contribute.
86
+ Fork it, branch it, test it, PR it — with love.
87
+
88
+ ## License
89
+
90
+ MIT — but don’t forget to say “merci” 😉
91
+
92
+ ### Made with ❤️ and `markdown` in France.
@@ -0,0 +1,10 @@
1
+ def parse_markdown(markdown_text: str) -> list:
2
+ """Parse le Markdown enrichi et retourne l'AST."""
3
+
4
+
5
+ def ast_to_mjml(ast: list) -> str:
6
+ """Transforme l'AST en MJML."""
7
+
8
+
9
+ def markdown_to_mjml(markdown_text: str) -> str:
10
+ """Pipeline complet: Markdown -> AST -> MJML."""
@@ -0,0 +1,277 @@
1
+ class Node:
2
+ def __init__(self):
3
+ self.annotations = {}
4
+
5
+ def get_children(self):
6
+ return []
7
+
8
+ def __repr__(self):
9
+ return f"{self.__class__.__name__}()"
10
+
11
+
12
+ class BlockNode(Node):
13
+ def __init__(self, children):
14
+ super().__init__()
15
+ if children is None:
16
+ self.children = []
17
+ else:
18
+ self.children = children if isinstance(children, list) else [children]
19
+
20
+ def get_children(self):
21
+ return self.children
22
+
23
+
24
+ # --- Text elements ---
25
+
26
+ class Text(Node):
27
+ pass
28
+
29
+
30
+ class TextBlock(Text, BlockNode):
31
+ pass
32
+
33
+
34
+ class LiteralText(Text):
35
+ def __init__(self, value):
36
+ super().__init__()
37
+ self.value = value
38
+
39
+ def __repr__(self):
40
+ return f"LiteralText('{self.value}')"
41
+
42
+
43
+ class CodeSpan(Text):
44
+ def __init__(self, code):
45
+ self.code = code
46
+ super().__init__()
47
+
48
+ def __repr__(self):
49
+ return f"CodeSpan(code='{self.code}')"
50
+
51
+
52
+ class Emphasis(TextBlock):
53
+ def __repr__(self):
54
+ return "Emphasis()"
55
+
56
+
57
+ class Strong(TextBlock):
58
+ def __repr__(self):
59
+ return "Strong()"
60
+
61
+
62
+ class StrikeThrough(TextBlock):
63
+ def __repr__(self):
64
+ return "StrikeThrough()"
65
+
66
+
67
+ class Link(TextBlock):
68
+ def __init__(self, text, href, title=None):
69
+ super().__init__(text)
70
+ self.href = href
71
+ self.title = title
72
+
73
+ def __repr__(self):
74
+ return f"Link(href='{self.href}', title='{self.title}')"
75
+
76
+
77
+ # --- Block elements ---
78
+
79
+
80
+ class Document(BlockNode):
81
+ def __repr__(self):
82
+ return "Document()"
83
+
84
+
85
+ class Paragraph(BlockNode):
86
+ def __repr__(self):
87
+ return "Paragraph()"
88
+
89
+
90
+ class Heading(BlockNode):
91
+ def __init__(self, level, text):
92
+ super().__init__(text)
93
+ self.level = level
94
+
95
+ def __repr__(self):
96
+ return f"Heading(level={self.level})"
97
+
98
+
99
+ class BlockText(BlockNode):
100
+ def __repr__(self):
101
+ return "BlockText()"
102
+
103
+
104
+ class BlockQuote(BlockNode):
105
+ def __repr__(self):
106
+ return "BlockQuote()"
107
+
108
+
109
+ class BlockCode(BlockNode):
110
+ def __init__(self, code=None, language=None, children=None):
111
+ super().__init__(children)
112
+ self.code = code
113
+ self.language = language
114
+
115
+ def __repr__(self):
116
+ return f"BlockCode(language='{self.language}', code='{self.code}')"
117
+
118
+
119
+ # --- Image element ---
120
+
121
+
122
+ class Image(Node):
123
+ def __init__(self, url, alt_text=None, title=None):
124
+ super().__init__()
125
+ self.url = url
126
+ self.alt_text = alt_text
127
+ self.title = title
128
+
129
+ def get_children(self):
130
+ return [self.alt_text]
131
+
132
+ def __repr__(self):
133
+ return (
134
+ f"Image(url='{self.url}', title='{self.title}', alt_text='{self.alt_text}'"
135
+ )
136
+
137
+
138
+ class ImageLink(Node):
139
+ def __init__(self, img, href, title=None):
140
+ super().__init__()
141
+ self.img = img
142
+ self.href = href
143
+ self.title = title
144
+
145
+ def get_children(self):
146
+ return [self.img]
147
+
148
+ def __repr__(self):
149
+ return f"ImageLink(href='{self.href}', img='{self.img}', title='{self.title}')"
150
+
151
+
152
+ # --- Terminal elements ---
153
+
154
+
155
+ class Terminal(Node):
156
+ pass
157
+
158
+
159
+ class TextTerminal(Text, Terminal):
160
+ pass
161
+
162
+
163
+ class ThematicBreak(Terminal):
164
+ def __repr__(self):
165
+ return "ThematicBreak()"
166
+
167
+
168
+ class LineBreak(TextTerminal):
169
+ def __repr__(self):
170
+ return "LineBreak()"
171
+
172
+
173
+ class SoftBreak(TextTerminal):
174
+ def __repr__(self):
175
+ return "SoftBreak()"
176
+
177
+
178
+ class BlankLine(TextTerminal):
179
+ def __repr__(self):
180
+ return "BlankLine()"
181
+
182
+
183
+ # --- List elements ---
184
+
185
+
186
+ class List(Node):
187
+ def __init__(self, elements, ordered=False):
188
+ super().__init__()
189
+ self.elements = elements
190
+ self.ordered = ordered
191
+
192
+ def get_children(self):
193
+ return self.elements
194
+
195
+ def __repr__(self):
196
+ return f"List(ordered={self.ordered})"
197
+
198
+
199
+ class ListItem(BlockNode):
200
+ def __init__(self, children):
201
+ super().__init__(children)
202
+
203
+ def __repr__(self):
204
+ return "ListItem()"
205
+
206
+
207
+ class TaskListItem(BlockNode):
208
+ def __init__(self, children, checked=False):
209
+ super().__init__(children)
210
+ self.checked = checked
211
+
212
+ def __repr__(self):
213
+ return f"TaskListItem(checked={self.checked})"
214
+
215
+
216
+ # --- Table elements ---
217
+
218
+
219
+ class Table(Node):
220
+ def __init__(self, header=None, rows=None):
221
+ super().__init__()
222
+ self.header = header
223
+ self.rows = rows
224
+
225
+ def get_children(self):
226
+ if self.header is None:
227
+ if self.rows is None:
228
+ children = []
229
+ else:
230
+ children = self.rows
231
+ else:
232
+ children = [self.header] + self.rows
233
+ return children
234
+
235
+ def __repr__(self):
236
+ return "Table()"
237
+
238
+
239
+ class TableRow(Node):
240
+ def __init__(self, row, is_header=False):
241
+ super().__init__()
242
+ self.row = row
243
+ self.is_header = is_header
244
+
245
+ def get_children(self):
246
+ return self.row
247
+
248
+ def __repr__(self):
249
+ return f"TableRow(is_header={self.is_header})"
250
+
251
+
252
+ class TableCell(BlockNode):
253
+ def __init__(self, cell, align=None):
254
+ super().__init__(cell)
255
+ self.align = align
256
+
257
+
258
+ class TableHeaderCell(TableCell):
259
+ def __repr__(self):
260
+ return f"TableHeaderCell(align={self.align})"
261
+
262
+
263
+ class TableRegularCell(TableCell):
264
+ def __repr__(self):
265
+ return f"TableRegularCell(align={self.align})"
266
+
267
+
268
+ class TableHeader(Node):
269
+ def __init__(self, headers):
270
+ super().__init__()
271
+ self.headers = headers
272
+
273
+ def get_children(self):
274
+ return self.headers
275
+
276
+ def __repr__(self):
277
+ return "TableHeader()"
@@ -0,0 +1,94 @@
1
+ from pathlib import Path
2
+ import tempfile
3
+ import webbrowser
4
+
5
+ import click
6
+ from jinja2 import Environment, FileSystemLoader
7
+
8
+ from inkletter.md_to_html import parse_markdown_to_html, parse_mjml_to_html
9
+ from inkletter.md_to_mjml import parse_markdown_to_mjml
10
+
11
+ BASE_DIR = Path(__file__).resolve().parent
12
+ TEMPLATES_DIR = BASE_DIR / "templates"
13
+
14
+ @click.group()
15
+ def cli():
16
+ """Inkletter CLI — Convert and preview Markdown as MJML"""
17
+ pass
18
+
19
+
20
+ @cli.command()
21
+ @click.argument("filepath", type=click.Path(exists=True, dir_okay=False, path_type=Path))
22
+ @click.option("-o", "--output", type=click.Path(dir_okay=False, writable=True, path_type=Path), help="Output HTML file path")
23
+ def preview(filepath: Path, output: Path | None):
24
+ """
25
+ Convert a Markdown file into MJML, then into HTML, and preview it in a browser.
26
+ """
27
+ try:
28
+ # Load Markdown
29
+ markdown_text = filepath.read_text(encoding="utf-8")
30
+
31
+ # Markdown → MJML
32
+ mjml_code = parse_markdown_to_mjml(
33
+ markdown_text,
34
+ print_ast=False,
35
+ use_style=True,
36
+ )
37
+
38
+ # MJML → HTML
39
+ html_output = parse_mjml_to_html(mjml_code)
40
+
41
+ # Render with Jinja2
42
+ env = Environment(loader=FileSystemLoader(str(TEMPLATES_DIR)))
43
+ template = env.get_template("preview.html")
44
+ rendered = template.render(
45
+ MD_CONTENT=markdown_text,
46
+ MJML_CONTENT=mjml_code,
47
+ HTML_CONTENT=html_output,
48
+ )
49
+
50
+ if output:
51
+ output.write_text(rendered, encoding="utf-8")
52
+ out_path = output
53
+ else:
54
+ with tempfile.NamedTemporaryFile("w", suffix=".html", encoding="utf-8", delete=False) as f:
55
+ f.write(rendered)
56
+ out_path = Path(f.name)
57
+
58
+ webbrowser.open(f"file://{out_path.resolve().as_uri()}")
59
+
60
+ click.secho(f"✅ Preview opened in browser:\n{out_path}", fg="green")
61
+
62
+ except Exception as e:
63
+ click.secho(f"❌ Error: {e}", fg="red", err=True)
64
+
65
+ @cli.command()
66
+ @click.argument("filepath", type=click.Path(exists=True, dir_okay=False, path_type=Path))
67
+ @click.option("-o", "--output", type=click.Path(dir_okay=False, writable=True, path_type=Path), help="Output HTML file path")
68
+ @click.option("--view/--no-view", default=False, help="Open the result in a browser (default: false)")
69
+ def md2mjml(filepath: Path, output: Path | None, view: bool):
70
+ """Convert Markdown to MJML then HTML, optionally save and open it."""
71
+ try:
72
+ # Lire Markdown
73
+ markdown_text = filepath.read_text(encoding="utf-8")
74
+
75
+ # Markdown → HTML
76
+ html_output = parse_markdown_to_html(markdown_text)
77
+
78
+
79
+ if output:
80
+ output.write_text(html_output, encoding="utf-8")
81
+ out_path = output
82
+ else:
83
+ with tempfile.NamedTemporaryFile("w", suffix=".html", encoding="utf-8", delete=False) as f:
84
+ f.write(html_output)
85
+ out_path = Path(f.name)
86
+
87
+ click.secho(f"✅ HTML written to: {out_path}", fg="green")
88
+
89
+ if view:
90
+ webbrowser.open(f"file://{out_path.resolve().as_uri()}")
91
+
92
+ except Exception as e:
93
+ click.secho(f"❌ Error: {e}", fg="red", err=True)
94
+