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.
- inkletter-0.1.0/LICENSE +21 -0
- inkletter-0.1.0/PKG-INFO +106 -0
- inkletter-0.1.0/README.md +92 -0
- inkletter-0.1.0/inkletter/__init__.py +10 -0
- inkletter-0.1.0/inkletter/ast.py +277 -0
- inkletter-0.1.0/inkletter/cli.py +94 -0
- inkletter-0.1.0/inkletter/codeblock.py +130 -0
- inkletter-0.1.0/inkletter/md_to_ast.py +160 -0
- inkletter-0.1.0/inkletter/md_to_html.py +11 -0
- inkletter-0.1.0/inkletter/md_to_mjml.py +57 -0
- inkletter-0.1.0/inkletter/scope.py +39 -0
- inkletter-0.1.0/inkletter/visitors/__init__.py +0 -0
- inkletter-0.1.0/inkletter/visitors/annotation.py +177 -0
- inkletter-0.1.0/inkletter/visitors/codegen.py +275 -0
- inkletter-0.1.0/inkletter/visitors/generic.py +11 -0
- inkletter-0.1.0/inkletter/visitors/merger.py +61 -0
- inkletter-0.1.0/inkletter/visitors/tree.py +27 -0
- inkletter-0.1.0/inkletter.egg-info/PKG-INFO +106 -0
- inkletter-0.1.0/inkletter.egg-info/SOURCES.txt +26 -0
- inkletter-0.1.0/inkletter.egg-info/dependency_links.txt +1 -0
- inkletter-0.1.0/inkletter.egg-info/entry_points.txt +2 -0
- inkletter-0.1.0/inkletter.egg-info/requires.txt +4 -0
- inkletter-0.1.0/inkletter.egg-info/top_level.txt +1 -0
- inkletter-0.1.0/pyproject.toml +25 -0
- inkletter-0.1.0/setup.cfg +4 -0
- inkletter-0.1.0/tests/test_codeblock.py +122 -0
- inkletter-0.1.0/tests/test_codeblock_from_string.py +87 -0
- inkletter-0.1.0/tests/test_merger.py +69 -0
inkletter-0.1.0/LICENSE
ADDED
|
@@ -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.
|
inkletter-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
|