decktile 0.2.0__py3-none-any.whl
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.
- decktile/__init__.py +5 -0
- decktile/__main__.py +3 -0
- decktile/assets/lucide/VENDORED.md +21 -0
- decktile/assets/lucide/lucide.css +2193 -0
- decktile/assets/lucide/lucide.ttf +3 -0
- decktile/cli.py +269 -0
- decktile/parser/__init__.py +0 -0
- decktile/parser/ast.py +191 -0
- decktile/parser/markdown.py +289 -0
- decktile/plugins/cards.py +25 -0
- decktile/plugins/directives.py +56 -0
- decktile/plugins/fenced.py +228 -0
- decktile/plugins/layouts.py +69 -0
- decktile/plugins/lucide.py +65 -0
- decktile/plugins/lucide_font.py +26 -0
- decktile/render/__init__.py +0 -0
- decktile/render/html.py +287 -0
- decktile/render/ir.py +536 -0
- decktile/render/odf/__init__.py +4 -0
- decktile/render/odf/geometry.py +510 -0
- decktile/render/odf/shapes.py +719 -0
- decktile/render/odf/styles.py +828 -0
- decktile/render/odp.py +380 -0
- decktile/render/pdf.py +17 -0
- decktile/render/pdg.py +72 -0
- decktile/render/seaborn.py +155 -0
- decktile/render/templates/base.css +741 -0
- decktile/render/templates/decktile.js +371 -0
- decktile/render/templates/shell.html +106 -0
- decktile/seaborn_styles/__init__.py +5 -0
- decktile/seaborn_styles/ossummit_europe.py +30 -0
- decktile/theme/__init__.py +0 -0
- decktile/theme/colors.py +53 -0
- decktile/theme/loader.py +22 -0
- decktile/theme/parser.py +264 -0
- decktile/themes/default.css +146 -0
- decktile/themes/kcd_vietnam.css +159 -0
- decktile/themes/kubecon_china.css +213 -0
- decktile/themes/kubecon_japan.css +156 -0
- decktile/themes/ossummit_europe.css +196 -0
- decktile/themes/ossummit_korea.css +196 -0
- decktile-0.2.0.dist-info/METADATA +625 -0
- decktile-0.2.0.dist-info/RECORD +46 -0
- decktile-0.2.0.dist-info/WHEEL +4 -0
- decktile-0.2.0.dist-info/entry_points.txt +6 -0
- decktile-0.2.0.dist-info/licenses/LICENSE +674 -0
decktile/cli.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""decktile CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from decktile.parser.markdown import parse
|
|
11
|
+
from decktile.render.html import render as render_html, default_theme, load_theme, base_css
|
|
12
|
+
from decktile.render.ir import assemble_css
|
|
13
|
+
from decktile.render.odp import render as render_odp
|
|
14
|
+
from decktile.render.pdf import render as render_pdf
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Markdown to styled HTML + ODP + PDF", no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.callback(invoke_without_command=True)
|
|
20
|
+
def main(
|
|
21
|
+
files: Optional[list[Path]] = typer.Argument(None, help="Markdown files to build; globs like '*.md' or 'talks/**/*.md' are expanded"),
|
|
22
|
+
output_dir: Optional[Path] = typer.Option(None, "-o", "--output-dir", help="Output directory (default: <input>/dist/)"),
|
|
23
|
+
pdf: bool = typer.Option(False, "--pdf", help="Generate PDF"),
|
|
24
|
+
odp: bool = typer.Option(False, "--odp", help="Generate ODP (programmatic)"),
|
|
25
|
+
image_odp: bool = typer.Option(False, "--image-odp", help="Generate ODP (PDF screenshots)"),
|
|
26
|
+
dist: bool = typer.Option(False, "--dist", help="Package HTML + referenced assets into a zip"),
|
|
27
|
+
optimize_images: bool = typer.Option(False, "--optimize-images/--no-optimize-images", help="Optimize images in PDF output (jpeg_quality=85, dpi=150)"),
|
|
28
|
+
watch: bool = typer.Option(False, "-w", "--watch", help="Watch file and rebuild on changes"),
|
|
29
|
+
debug: bool = typer.Option(False, "--debug", help="Dump AST + write debug CSS"),
|
|
30
|
+
css: Optional[Path] = typer.Option(None, "--css", help="Custom CSS theme file (overrides default)"),
|
|
31
|
+
):
|
|
32
|
+
if not files:
|
|
33
|
+
raise typer.Exit()
|
|
34
|
+
if css and not css.is_file():
|
|
35
|
+
_error(css, "CSS file not found")
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
|
|
38
|
+
paths, missing = _expand(files)
|
|
39
|
+
for f in missing:
|
|
40
|
+
_error(f, "no files match" if any(c in str(f) for c in "*?[")
|
|
41
|
+
else "not a file" if f.exists() else "no such file")
|
|
42
|
+
|
|
43
|
+
if watch:
|
|
44
|
+
_watch(paths, output_dir, pdf, odp, image_odp, debug, css)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
failed = len(missing)
|
|
48
|
+
for f in paths:
|
|
49
|
+
try:
|
|
50
|
+
_build(f, output_dir, pdf, odp, image_odp, dist, optimize_images, debug, css)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
if debug:
|
|
53
|
+
raise
|
|
54
|
+
_error(f, e)
|
|
55
|
+
failed += 1
|
|
56
|
+
if failed:
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _expand(args: list[Path]) -> tuple[list[Path], list[Path]]:
|
|
61
|
+
"""Expand glob patterns the shell left alone (quoted, or no match);
|
|
62
|
+
keep order, drop duplicates. Returns (existing files, unmatched args).
|
|
63
|
+
Duplicates are by resolved path: output dirs hold symlinks back to the
|
|
64
|
+
source dirs, so '**/*.md' reaches each deck again through every dist/.
|
|
65
|
+
The real (non-symlinked) path wins, so output lands next to it."""
|
|
66
|
+
paths: list[Path] = []
|
|
67
|
+
seen: set[Path] = set()
|
|
68
|
+
missing: list[Path] = []
|
|
69
|
+
for a in args:
|
|
70
|
+
matches = ([Path(m) for m in glob.glob(str(a), recursive=True)]
|
|
71
|
+
if any(c in str(a) for c in "*?[") else [a])
|
|
72
|
+
found = sorted((m for m in matches if m.is_file()),
|
|
73
|
+
key=lambda m: (m.resolve() != m.absolute(), str(m)))
|
|
74
|
+
if not found:
|
|
75
|
+
missing.append(a)
|
|
76
|
+
for m in found:
|
|
77
|
+
if m.resolve() not in seen:
|
|
78
|
+
seen.add(m.resolve())
|
|
79
|
+
paths.append(m)
|
|
80
|
+
return paths, missing
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _error(path: Path, msg: object) -> None:
|
|
84
|
+
typer.echo(f"error: {path}: {msg}", err=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _build(file: Path, output_dir: Optional[Path], pdf: bool,
|
|
88
|
+
odp: bool, image_odp: bool, dist: bool, optimize_images: bool,
|
|
89
|
+
debug: bool, css_path: Optional[Path]) -> None:
|
|
90
|
+
content = file.read_text(encoding="utf-8")
|
|
91
|
+
doc = parse(content)
|
|
92
|
+
dims = doc.meta.dimensions()
|
|
93
|
+
|
|
94
|
+
out_dir = output_dir or file.parent / "dist"
|
|
95
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
_symlink_assets(file.parent, out_dir)
|
|
97
|
+
stem = file.stem
|
|
98
|
+
|
|
99
|
+
if doc.meta.theme and doc.meta.theme != "default":
|
|
100
|
+
theme_css = load_theme(doc.meta.theme)
|
|
101
|
+
else:
|
|
102
|
+
theme_css = default_theme()
|
|
103
|
+
if css_path and css_path.is_file():
|
|
104
|
+
theme_css += "\n" + css_path.read_text(encoding="utf-8")
|
|
105
|
+
theme_css += "\n" + (doc.meta.style or "")
|
|
106
|
+
|
|
107
|
+
# Assemble CSS: base.css (with dims) + theme + logo + pygments
|
|
108
|
+
css = assemble_css(base_css(), theme_css, dims, doc.meta.logo,
|
|
109
|
+
doc.meta.watermark, doc.meta.pygments_style or "default",
|
|
110
|
+
doc.meta.logo_dark, doc.meta.background)
|
|
111
|
+
|
|
112
|
+
html = render_html(doc, theme_css, css, dims, doc.meta.logo)
|
|
113
|
+
html_path = out_dir / f"{stem}.html"
|
|
114
|
+
html_path.write_text(html, encoding="utf-8")
|
|
115
|
+
typer.echo(f"Wrote {html_path} ({len(html)} bytes)")
|
|
116
|
+
|
|
117
|
+
if dist:
|
|
118
|
+
_make_dist(file.parent, out_dir, html_path, html, theme_css, stem)
|
|
119
|
+
|
|
120
|
+
if debug:
|
|
121
|
+
css = base_css().replace("SLIDE_W", str(dims[0])).replace("SLIDE_H", str(dims[1]))
|
|
122
|
+
css = css.replace("THEME_CSS", theme_css)
|
|
123
|
+
css = css.replace("LOGO_CSS", "").replace("{theme_css}", "").replace("{logo_css}", "")
|
|
124
|
+
css_path = out_dir / f"{stem}.css"
|
|
125
|
+
css_path.write_text(css, encoding="utf-8")
|
|
126
|
+
typer.echo(f"Wrote {css_path} (debug CSS, {len(css)} bytes)")
|
|
127
|
+
|
|
128
|
+
if odp:
|
|
129
|
+
odp_path = out_dir / f"{stem}.odp"
|
|
130
|
+
render_odp(doc, odp_path, base_css(), theme_css,
|
|
131
|
+
source_dir=file.parent)
|
|
132
|
+
typer.echo(f"Wrote {odp_path} ({odp_path.stat().st_size} bytes)")
|
|
133
|
+
|
|
134
|
+
if image_odp:
|
|
135
|
+
img_path = out_dir / f"{stem}.odp"
|
|
136
|
+
pdf_path = out_dir / f"{stem}.pdf"
|
|
137
|
+
render_pdf(html_path, pdf_path)
|
|
138
|
+
_pdf_to_odp(pdf_path, img_path, dims=dims)
|
|
139
|
+
typer.echo(f"Wrote {img_path} ({img_path.stat().st_size} bytes)")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
if pdf:
|
|
143
|
+
pdf_path = out_dir / f"{stem}.pdf"
|
|
144
|
+
render_pdf(html_path, pdf_path, optimize_images=optimize_images)
|
|
145
|
+
typer.echo(f"Wrote {pdf_path} ({pdf_path.stat().st_size} bytes)")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
typer.echo(f"Parsed {len(doc.slides)} slides")
|
|
149
|
+
if debug:
|
|
150
|
+
for i, slide in enumerate(doc.slides):
|
|
151
|
+
typer.echo(f" Slide {i+1}: {slide.layout} ({len(slide.children)} children)")
|
|
152
|
+
for n in slide.children:
|
|
153
|
+
typer.echo(f" {type(n).__name__}: {_node_summary(n)}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _watch(files: list[Path], output_dir: Optional[Path], pdf: bool,
|
|
157
|
+
odp: bool, image_odp: bool, debug: bool, css_path: Optional[Path]) -> None:
|
|
158
|
+
if not files:
|
|
159
|
+
raise typer.Exit(1)
|
|
160
|
+
mtimes: dict[Path, float] = {}
|
|
161
|
+
typer.echo(f"Watching {len(files)} file(s) for changes (Ctrl+C to stop)")
|
|
162
|
+
|
|
163
|
+
while True:
|
|
164
|
+
try:
|
|
165
|
+
for f in files:
|
|
166
|
+
try:
|
|
167
|
+
mtime = f.stat().st_mtime
|
|
168
|
+
except FileNotFoundError:
|
|
169
|
+
continue # mid-save rename or deleted: pick it up when it's back
|
|
170
|
+
if mtimes.get(f) != mtime:
|
|
171
|
+
mtimes[f] = mtime
|
|
172
|
+
typer.echo("---")
|
|
173
|
+
try:
|
|
174
|
+
_build(f, output_dir, pdf, odp, image_odp, False, False, debug, css_path)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
_error(f, e)
|
|
177
|
+
time.sleep(1)
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
typer.echo("\nStopped watching.")
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _pdf_to_odp(pdf_path: Path, output_path: Path, dims: tuple[int, int]) -> None:
|
|
184
|
+
import os, subprocess, tempfile
|
|
185
|
+
from odfdo import Document, DrawPage, Frame
|
|
186
|
+
|
|
187
|
+
sw, sh = dims
|
|
188
|
+
odp = Document("presentation")
|
|
189
|
+
odp.body.clear()
|
|
190
|
+
|
|
191
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
192
|
+
subprocess.run(
|
|
193
|
+
["pdftoppm", "-png", "-r", "150", str(pdf_path), f"{tmp}/page"],
|
|
194
|
+
capture_output=True, text=True, timeout=60,
|
|
195
|
+
)
|
|
196
|
+
for png_file in sorted(Path(tmp).glob("page-*.png")):
|
|
197
|
+
i = int(png_file.stem.split("-")[1]) - 1
|
|
198
|
+
page = DrawPage(f"slide{i + 1}", name=f"Slide {i + 1}")
|
|
199
|
+
uri = odp.add_file(str(png_file))
|
|
200
|
+
frame = Frame.image_frame(
|
|
201
|
+
image=uri, text="",
|
|
202
|
+
size=(f"{sw * 2.54 / 96:.2f}cm", f"{sh * 2.54 / 96:.2f}cm"),
|
|
203
|
+
position=("0cm", "0cm"),
|
|
204
|
+
anchor_type="page",
|
|
205
|
+
)
|
|
206
|
+
page.append(frame)
|
|
207
|
+
odp.body.append(page)
|
|
208
|
+
|
|
209
|
+
odp.save(str(output_path), pretty=True)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _node_summary(node) -> str:
|
|
213
|
+
from decktile.parser.ast import Heading, Table, Grid, AttrNode
|
|
214
|
+
|
|
215
|
+
if isinstance(node, Heading):
|
|
216
|
+
return f"L{node.level}: {node.content[0].content if node.content else ''}"
|
|
217
|
+
elif isinstance(node, Table):
|
|
218
|
+
return f"({len(node.headers)} cols x {len(node.rows)} rows)"
|
|
219
|
+
elif isinstance(node, Grid):
|
|
220
|
+
return f"cols={node.cols} children={len(node.children)}"
|
|
221
|
+
elif isinstance(node, AttrNode):
|
|
222
|
+
return f"{node.type}: {node.value}"
|
|
223
|
+
return ""
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _symlink_assets(source_dir: Path, dist_dir: Path) -> None:
|
|
227
|
+
"""Symlink all directories from source to dist. Skips files and dist itself."""
|
|
228
|
+
for item in source_dir.iterdir():
|
|
229
|
+
if not item.is_dir():
|
|
230
|
+
continue
|
|
231
|
+
if item.name == dist_dir.name:
|
|
232
|
+
continue
|
|
233
|
+
dest = dist_dir / item.name
|
|
234
|
+
if not dest.exists():
|
|
235
|
+
dest.symlink_to(item.resolve())
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _make_dist(source_dir: Path, out_dir: Path, html_path: Path,
|
|
239
|
+
html: str, theme_css: str, stem: str) -> None:
|
|
240
|
+
import re, zipfile
|
|
241
|
+
|
|
242
|
+
zip_path = out_dir / f"{stem}.zip"
|
|
243
|
+
seen = set()
|
|
244
|
+
|
|
245
|
+
with zipfile.ZipFile(str(zip_path), "w", zipfile.ZIP_DEFLATED) as zf:
|
|
246
|
+
zf.write(str(html_path), html_path.name)
|
|
247
|
+
|
|
248
|
+
urls = set()
|
|
249
|
+
for pattern in [r'src="([^"]+)"', r'href="([^"]+)"', r'url\(["\']?([^)"\']+)["\']?\)']:
|
|
250
|
+
for m in re.finditer(pattern, html + theme_css):
|
|
251
|
+
urls.add(m.group(1))
|
|
252
|
+
|
|
253
|
+
for url in sorted(urls):
|
|
254
|
+
if url.startswith(("http:", "https:", "data:", "#")):
|
|
255
|
+
continue
|
|
256
|
+
if url.startswith("/"):
|
|
257
|
+
continue
|
|
258
|
+
asset = source_dir / url
|
|
259
|
+
if not asset.is_file():
|
|
260
|
+
continue
|
|
261
|
+
# Use real path for dedup, resolved symlink for actual file access
|
|
262
|
+
real = asset.resolve()
|
|
263
|
+
if str(real) in seen:
|
|
264
|
+
continue
|
|
265
|
+
seen.add(str(real))
|
|
266
|
+
# Preserve original relative path structure in zip
|
|
267
|
+
zf.write(str(real), url)
|
|
268
|
+
|
|
269
|
+
typer.echo(f"Wrote {zip_path} ({zip_path.stat().st_size} bytes, {len(seen)} assets)")
|
|
File without changes
|
decktile/parser/ast.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""AST node types for decktile."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LayoutType(Enum):
|
|
11
|
+
TITLE = "title"
|
|
12
|
+
GRID2 = "grid-2"
|
|
13
|
+
GRID3 = "grid-3"
|
|
14
|
+
GRID4 = "grid-4"
|
|
15
|
+
CONTENT = "content"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Meta:
|
|
20
|
+
theme: str = ""
|
|
21
|
+
title: str = ""
|
|
22
|
+
footer: str = ""
|
|
23
|
+
paginate: bool = False
|
|
24
|
+
size: str = "16:9"
|
|
25
|
+
style: str = ""
|
|
26
|
+
logo: str = ""
|
|
27
|
+
logo_dark: str = ""
|
|
28
|
+
watermark: str = ""
|
|
29
|
+
background: str = ""
|
|
30
|
+
pygments_style: str = "default"
|
|
31
|
+
seaborn_theme: str = ""
|
|
32
|
+
theme_variant: str = "light"
|
|
33
|
+
transition: str = ""
|
|
34
|
+
autoplay: int = 7
|
|
35
|
+
|
|
36
|
+
def dimensions(self) -> tuple[int, int]:
|
|
37
|
+
raw = str(self.size)
|
|
38
|
+
if "x" in raw:
|
|
39
|
+
w, h = raw.split("x")
|
|
40
|
+
return (min(max(int(w), 320), 7680), min(max(int(h), 320), 7680))
|
|
41
|
+
return {"16:9": (1280, 720), "4:3": (1024, 768), "16:10": (1280, 800)}.get(raw, (1280, 720))
|
|
42
|
+
|
|
43
|
+
def physical_dims(self) -> tuple[float, float]:
|
|
44
|
+
"""Slide dimensions in mm for ODP/print."""
|
|
45
|
+
raw = str(self.size)
|
|
46
|
+
return {"16:9": (280.0, 157.5), "4:3": (280.0, 210.0),
|
|
47
|
+
"16:10": (280.0, 175.0)}.get(raw, (280.0, 157.5))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Document:
|
|
52
|
+
meta: Meta
|
|
53
|
+
slides: list["Slide"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Slide:
|
|
58
|
+
layout: str = "content"
|
|
59
|
+
children: list["Node"] = field(default_factory=list)
|
|
60
|
+
notes: str = ""
|
|
61
|
+
variant: str = ""
|
|
62
|
+
transition: str = ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Node:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Heading(Node):
|
|
71
|
+
level: int = 1
|
|
72
|
+
content: list["Inline"] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class Paragraph(Node):
|
|
77
|
+
content: list["Inline"] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Grid(Node):
|
|
82
|
+
cols: int = 2
|
|
83
|
+
class_: str = ""
|
|
84
|
+
children: list[Node] = field(default_factory=list)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class Card(Node):
|
|
89
|
+
header: str = ""
|
|
90
|
+
body: list[str] = field(default_factory=list)
|
|
91
|
+
children: list[Node] = field(default_factory=list)
|
|
92
|
+
tag: Optional[str] = None
|
|
93
|
+
class_: str = ""
|
|
94
|
+
label: str = "" # ::: card {label="Q1"}: short name rendered on a
|
|
95
|
+
# timeline layout's spine, next to the milestone dot
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class Arrow(Node):
|
|
100
|
+
"""Connector between two cards (arrow icon, label, etc.)."""
|
|
101
|
+
content: str = ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class Notes(Node):
|
|
106
|
+
"""Full-width conclusion card below a comparison layout."""
|
|
107
|
+
content: str = ""
|
|
108
|
+
tag: Optional[str] = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class Row(Node):
|
|
113
|
+
"""Horizontal row of elements placed side by side."""
|
|
114
|
+
children: list[Node] = field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class Table(Node):
|
|
119
|
+
headers: list[str] = field(default_factory=list)
|
|
120
|
+
rows: list[list[str]] = field(default_factory=list)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class Quote(Node):
|
|
125
|
+
content: list["Inline"] = field(default_factory=list)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class ListNode(Node):
|
|
130
|
+
items: list[list[Inline]] = field(default_factory=list)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class AttrNode(Node):
|
|
135
|
+
type: str = ""
|
|
136
|
+
value: str = ""
|
|
137
|
+
attrs: dict[str, str] = field(default_factory=dict)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class CodeBlock(Node):
|
|
142
|
+
content: str = ""
|
|
143
|
+
language: str = ""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class Inline:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class Text(Inline):
|
|
153
|
+
content: str = ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class Strong(Inline):
|
|
158
|
+
children: list[Inline] = field(default_factory=list)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class Emphasis(Inline):
|
|
163
|
+
children: list[Inline] = field(default_factory=list)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class Strikethrough(Inline):
|
|
168
|
+
children: list[Inline] = field(default_factory=list)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class CodeSpan(Inline):
|
|
173
|
+
content: str = ""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass
|
|
177
|
+
class Image(Inline):
|
|
178
|
+
src: str = ""
|
|
179
|
+
alt: str = ""
|
|
180
|
+
title: str = ""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class SoftBreak(Inline):
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class LucideIcon(Inline):
|
|
190
|
+
name: str = ""
|
|
191
|
+
attrs: dict[str, str] = field(default_factory=dict)
|