dd-format 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.
- dd_format-0.1.0/LICENSE +21 -0
- dd_format-0.1.0/PKG-INFO +22 -0
- dd_format-0.1.0/README.md +2 -0
- dd_format-0.1.0/dd_format/__init__.py +13 -0
- dd_format-0.1.0/dd_format/cli.py +68 -0
- dd_format-0.1.0/dd_format/docx_writer.py +75 -0
- dd_format-0.1.0/dd_format/html_writer.py +163 -0
- dd_format-0.1.0/dd_format/pdf_writer.py +85 -0
- dd_format-0.1.0/dd_format.egg-info/PKG-INFO +22 -0
- dd_format-0.1.0/dd_format.egg-info/SOURCES.txt +15 -0
- dd_format-0.1.0/dd_format.egg-info/dependency_links.txt +1 -0
- dd_format-0.1.0/dd_format.egg-info/entry_points.txt +2 -0
- dd_format-0.1.0/dd_format.egg-info/requires.txt +8 -0
- dd_format-0.1.0/dd_format.egg-info/top_level.txt +1 -0
- dd_format-0.1.0/pyproject.toml +39 -0
- dd_format-0.1.0/setup.cfg +4 -0
- dd_format-0.1.0/tests/test_format.py +67 -0
dd_format-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 digital-duck
|
|
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.
|
dd_format-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dd-format
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Abstraction layer for formatting documents from markdown to HTML, PDF, and DOCX.
|
|
5
|
+
Author-email: Digital Duck & Dog Team <info@digitalduck.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/digital-duck/dd-format
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: fpdf2>=2.7
|
|
14
|
+
Requires-Dist: click>=8.0
|
|
15
|
+
Provides-Extra: docx
|
|
16
|
+
Requires-Dist: python-docx>=1.0; extra == "docx"
|
|
17
|
+
Provides-Extra: all
|
|
18
|
+
Requires-Dist: python-docx>=1.0; extra == "all"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# dd-format
|
|
22
|
+
Abstraction layer for formatting document from markdown to others such as html, pdf
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""dd-format: convert markdown to PDF, DOCX, and HTML."""
|
|
2
|
+
|
|
3
|
+
from dd_format.pdf_writer import markdown_to_pdf
|
|
4
|
+
from dd_format.html_writer import markdown_to_html
|
|
5
|
+
|
|
6
|
+
__all__ = ["markdown_to_pdf", "markdown_to_html", "markdown_to_docx"]
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def markdown_to_docx(md_text: str, output_path: str, title: str = ""):
|
|
11
|
+
"""Lazy import — requires ``pip install dd-format[docx]``."""
|
|
12
|
+
from dd_format.docx_writer import markdown_to_docx as _impl
|
|
13
|
+
return _impl(md_text, output_path, title=title)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""dd-format CLI: convert markdown files to PDF, DOCX, or HTML."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("source", type=click.Path(exists=True))
|
|
13
|
+
@click.option(
|
|
14
|
+
"--format",
|
|
15
|
+
"out_fmt",
|
|
16
|
+
type=click.Choice(["pdf", "html", "docx"]),
|
|
17
|
+
default="pdf",
|
|
18
|
+
show_default=True,
|
|
19
|
+
help="Output format.",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"--out",
|
|
23
|
+
default="",
|
|
24
|
+
help="Output file path (default: <source>.<format>).",
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--title",
|
|
28
|
+
default="",
|
|
29
|
+
help="Document title.",
|
|
30
|
+
)
|
|
31
|
+
def main(source: str, out_fmt: str, out: str, title: str) -> None:
|
|
32
|
+
"""Convert a markdown file to PDF, DOCX, or HTML.
|
|
33
|
+
|
|
34
|
+
\b
|
|
35
|
+
Examples:
|
|
36
|
+
dd-format notes.md
|
|
37
|
+
dd-format notes.md --format html --out notes.html
|
|
38
|
+
dd-format notes.md --format docx --title "My Notes"
|
|
39
|
+
"""
|
|
40
|
+
src = Path(source)
|
|
41
|
+
md_text = src.read_text(encoding="utf-8")
|
|
42
|
+
|
|
43
|
+
if not out:
|
|
44
|
+
out = str(src.with_suffix(f".{out_fmt}"))
|
|
45
|
+
|
|
46
|
+
if out_fmt == "pdf":
|
|
47
|
+
from dd_format.pdf_writer import markdown_to_pdf
|
|
48
|
+
markdown_to_pdf(md_text, out, title=title)
|
|
49
|
+
elif out_fmt == "html":
|
|
50
|
+
from dd_format.html_writer import markdown_to_html
|
|
51
|
+
markdown_to_html(md_text, out, title=title)
|
|
52
|
+
elif out_fmt == "docx":
|
|
53
|
+
try:
|
|
54
|
+
from dd_format.docx_writer import markdown_to_docx
|
|
55
|
+
except ImportError:
|
|
56
|
+
click.echo(
|
|
57
|
+
"Error: python-docx is required for DOCX output.\n"
|
|
58
|
+
"Install it with: pip install dd-format[docx]",
|
|
59
|
+
err=True,
|
|
60
|
+
)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
markdown_to_docx(md_text, out, title=title)
|
|
63
|
+
|
|
64
|
+
click.echo(f"Written: {out}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Convert markdown text to a .docx file using python-docx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def markdown_to_docx(md_text: str, output_path: str, title: str = "") -> Path:
|
|
10
|
+
"""Write *md_text* (simple markdown) to a Word document at *output_path*.
|
|
11
|
+
|
|
12
|
+
Supports: headings (# / ## / ###), bullet lists (- / *),
|
|
13
|
+
**bold**, and ``code`` spans.
|
|
14
|
+
|
|
15
|
+
Requires ``pip install dd-format[docx]``.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
md_text : str
|
|
20
|
+
Markdown-formatted text.
|
|
21
|
+
output_path : str
|
|
22
|
+
Destination file path.
|
|
23
|
+
title : str, optional
|
|
24
|
+
Document title rendered as Heading 0.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
Path
|
|
29
|
+
The output file path.
|
|
30
|
+
"""
|
|
31
|
+
from docx import Document # type: ignore
|
|
32
|
+
|
|
33
|
+
doc = Document()
|
|
34
|
+
if title:
|
|
35
|
+
doc.add_heading(title, level=0)
|
|
36
|
+
|
|
37
|
+
for line in md_text.split("\n"):
|
|
38
|
+
stripped = line.strip()
|
|
39
|
+
if not stripped:
|
|
40
|
+
doc.add_paragraph("")
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
# Headings
|
|
44
|
+
if stripped.startswith("### "):
|
|
45
|
+
doc.add_heading(stripped[4:], level=3)
|
|
46
|
+
elif stripped.startswith("## "):
|
|
47
|
+
doc.add_heading(stripped[3:], level=2)
|
|
48
|
+
elif stripped.startswith("# "):
|
|
49
|
+
doc.add_heading(stripped[2:], level=1)
|
|
50
|
+
elif stripped.startswith("- ") or stripped.startswith("* "):
|
|
51
|
+
p = doc.add_paragraph(style="List Bullet")
|
|
52
|
+
_add_runs(p, stripped[2:])
|
|
53
|
+
else:
|
|
54
|
+
p = doc.add_paragraph()
|
|
55
|
+
_add_runs(p, stripped)
|
|
56
|
+
|
|
57
|
+
out = Path(output_path)
|
|
58
|
+
doc.save(str(out))
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _add_runs(paragraph, text: str) -> None:
|
|
63
|
+
"""Add runs to a paragraph, handling **bold** and `code` spans."""
|
|
64
|
+
pos = 0
|
|
65
|
+
for m in re.finditer(r"\*\*(.+?)\*\*|`([^`]+)`", text):
|
|
66
|
+
if m.start() > pos:
|
|
67
|
+
paragraph.add_run(text[pos : m.start()])
|
|
68
|
+
if m.group(1) is not None:
|
|
69
|
+
paragraph.add_run(m.group(1)).bold = True
|
|
70
|
+
else:
|
|
71
|
+
run = paragraph.add_run(m.group(2))
|
|
72
|
+
run.font.name = "Courier New"
|
|
73
|
+
pos = m.end()
|
|
74
|
+
if pos < len(text):
|
|
75
|
+
paragraph.add_run(text[pos:])
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Convert markdown text to a self-contained HTML file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_TEMPLATE = """\
|
|
9
|
+
<!DOCTYPE html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="UTF-8">
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
14
|
+
<title>{title}</title>
|
|
15
|
+
<style>
|
|
16
|
+
:root {{
|
|
17
|
+
--bg: #0f1117; --card: #1a1d27; --accent: #4f8ef7;
|
|
18
|
+
--text: #e0e0e0; --sub: #9ca3af; --border: #2d3148;
|
|
19
|
+
}}
|
|
20
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
21
|
+
body {{
|
|
22
|
+
background: var(--bg); color: var(--text);
|
|
23
|
+
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
24
|
+
padding: 2rem; max-width: 800px; margin: 0 auto;
|
|
25
|
+
line-height: 1.7;
|
|
26
|
+
}}
|
|
27
|
+
h1 {{ font-size: 1.8rem; color: var(--accent); margin: 1.5rem 0 0.5rem; }}
|
|
28
|
+
h2 {{ font-size: 1.4rem; color: var(--accent); margin: 1.2rem 0 0.4rem; }}
|
|
29
|
+
h3 {{ font-size: 1.1rem; color: var(--accent); margin: 1rem 0 0.3rem; }}
|
|
30
|
+
p {{ margin: 0.5rem 0; }}
|
|
31
|
+
ul {{ padding-left: 1.5rem; margin: 0.5rem 0; }}
|
|
32
|
+
li {{ margin: 0.2rem 0; }}
|
|
33
|
+
pre {{
|
|
34
|
+
background: var(--card); border: 1px solid var(--border);
|
|
35
|
+
border-radius: 8px; padding: 1rem; overflow-x: auto;
|
|
36
|
+
font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 0.88rem;
|
|
37
|
+
margin: 0.8rem 0;
|
|
38
|
+
}}
|
|
39
|
+
code {{
|
|
40
|
+
background: var(--card); padding: 0.15rem 0.4rem; border-radius: 4px;
|
|
41
|
+
font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 0.88rem;
|
|
42
|
+
}}
|
|
43
|
+
strong {{ color: #fff; }}
|
|
44
|
+
hr {{ border: none; border-top: 1px solid var(--border); margin: 1.5rem 0; }}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
{body}
|
|
49
|
+
</body>
|
|
50
|
+
</html>"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def markdown_to_html(md_text: str, output_path: str, title: str = "") -> Path:
|
|
54
|
+
"""Convert simple markdown to a dark-mode, self-contained HTML file.
|
|
55
|
+
|
|
56
|
+
Supports: headings, bold, code spans, fenced code blocks,
|
|
57
|
+
bullet lists, horizontal rules, and paragraphs.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
md_text : str
|
|
62
|
+
Markdown-formatted text.
|
|
63
|
+
output_path : str
|
|
64
|
+
Destination file path.
|
|
65
|
+
title : str, optional
|
|
66
|
+
HTML ``<title>`` value (defaults to "Document").
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
Path
|
|
71
|
+
The output file path.
|
|
72
|
+
"""
|
|
73
|
+
lines = md_text.split("\n")
|
|
74
|
+
out: list[str] = []
|
|
75
|
+
in_code = False
|
|
76
|
+
in_list = False
|
|
77
|
+
|
|
78
|
+
for line in lines:
|
|
79
|
+
stripped = line.strip()
|
|
80
|
+
|
|
81
|
+
# Fenced code blocks
|
|
82
|
+
if stripped.startswith("```"):
|
|
83
|
+
if in_code:
|
|
84
|
+
out.append("</pre>")
|
|
85
|
+
else:
|
|
86
|
+
if in_list:
|
|
87
|
+
out.append("</ul>")
|
|
88
|
+
in_list = False
|
|
89
|
+
out.append("<pre>")
|
|
90
|
+
in_code = not in_code
|
|
91
|
+
continue
|
|
92
|
+
if in_code:
|
|
93
|
+
out.append(html.escape(line))
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
if not stripped:
|
|
97
|
+
if in_list:
|
|
98
|
+
out.append("</ul>")
|
|
99
|
+
in_list = False
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Horizontal rule
|
|
103
|
+
if stripped in ("---", "***", "___"):
|
|
104
|
+
if in_list:
|
|
105
|
+
out.append("</ul>")
|
|
106
|
+
in_list = False
|
|
107
|
+
out.append("<hr>")
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Headings
|
|
111
|
+
if stripped.startswith("### "):
|
|
112
|
+
if in_list:
|
|
113
|
+
out.append("</ul>")
|
|
114
|
+
in_list = False
|
|
115
|
+
out.append(f"<h3>{_inline(stripped[4:])}</h3>")
|
|
116
|
+
elif stripped.startswith("## "):
|
|
117
|
+
if in_list:
|
|
118
|
+
out.append("</ul>")
|
|
119
|
+
in_list = False
|
|
120
|
+
out.append(f"<h2>{_inline(stripped[3:])}</h2>")
|
|
121
|
+
elif stripped.startswith("# "):
|
|
122
|
+
if in_list:
|
|
123
|
+
out.append("</ul>")
|
|
124
|
+
in_list = False
|
|
125
|
+
out.append(f"<h1>{_inline(stripped[2:])}</h1>")
|
|
126
|
+
# Bullet lists
|
|
127
|
+
elif stripped.startswith("- ") or stripped.startswith("* "):
|
|
128
|
+
if not in_list:
|
|
129
|
+
out.append("<ul>")
|
|
130
|
+
in_list = True
|
|
131
|
+
out.append(f"<li>{_inline(stripped[2:])}</li>")
|
|
132
|
+
# Paragraphs
|
|
133
|
+
else:
|
|
134
|
+
if in_list:
|
|
135
|
+
out.append("</ul>")
|
|
136
|
+
in_list = False
|
|
137
|
+
out.append(f"<p>{_inline(stripped)}</p>")
|
|
138
|
+
|
|
139
|
+
if in_list:
|
|
140
|
+
out.append("</ul>")
|
|
141
|
+
if in_code:
|
|
142
|
+
out.append("</pre>")
|
|
143
|
+
|
|
144
|
+
body = "\n".join(out)
|
|
145
|
+
doc_title = html.escape(title or "Document")
|
|
146
|
+
result = _TEMPLATE.format(title=doc_title, body=body)
|
|
147
|
+
|
|
148
|
+
p = Path(output_path)
|
|
149
|
+
p.write_text(result, encoding="utf-8")
|
|
150
|
+
return p
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _inline(text: str) -> str:
|
|
154
|
+
"""Process inline markdown: **bold**, `code`, and escape HTML."""
|
|
155
|
+
import re
|
|
156
|
+
|
|
157
|
+
# Escape HTML first, then apply markdown formatting
|
|
158
|
+
text = html.escape(text)
|
|
159
|
+
# Bold
|
|
160
|
+
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
|
|
161
|
+
# Code spans
|
|
162
|
+
text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
|
|
163
|
+
return text
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Convert markdown text to a PDF file using fpdf2."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def markdown_to_pdf(md_text: str, output_path: str, title: str = "") -> Path:
|
|
9
|
+
"""Write *md_text* (simple markdown) to a PDF at *output_path*.
|
|
10
|
+
|
|
11
|
+
Supports: headings (# / ## / ###), bullet lists (- / *),
|
|
12
|
+
fenced code blocks, and plain paragraphs.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
md_text : str
|
|
17
|
+
Markdown-formatted text.
|
|
18
|
+
output_path : str
|
|
19
|
+
Destination file path.
|
|
20
|
+
title : str, optional
|
|
21
|
+
Document title rendered at the top.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
Path
|
|
26
|
+
The output file path.
|
|
27
|
+
"""
|
|
28
|
+
from fpdf import FPDF # type: ignore
|
|
29
|
+
|
|
30
|
+
pdf = FPDF()
|
|
31
|
+
pdf.set_auto_page_break(auto=True, margin=15)
|
|
32
|
+
pdf.add_page()
|
|
33
|
+
pdf.set_font("Helvetica", size=11)
|
|
34
|
+
|
|
35
|
+
if title:
|
|
36
|
+
pdf.set_font("Helvetica", "B", 18)
|
|
37
|
+
pdf.cell(0, 12, title, new_x="LMARGIN", new_y="NEXT")
|
|
38
|
+
pdf.ln(4)
|
|
39
|
+
|
|
40
|
+
in_code_block = False
|
|
41
|
+
|
|
42
|
+
def _write_block(text: str, h: float = 6) -> None:
|
|
43
|
+
"""Write a multi_cell block, resetting x to left margin afterward."""
|
|
44
|
+
pdf.set_x(pdf.l_margin)
|
|
45
|
+
w = pdf.w - pdf.l_margin - pdf.r_margin
|
|
46
|
+
pdf.multi_cell(w, h, text)
|
|
47
|
+
|
|
48
|
+
for line in md_text.split("\n"):
|
|
49
|
+
stripped = line.strip()
|
|
50
|
+
|
|
51
|
+
# Fenced code blocks
|
|
52
|
+
if stripped.startswith("```"):
|
|
53
|
+
in_code_block = not in_code_block
|
|
54
|
+
continue
|
|
55
|
+
if in_code_block:
|
|
56
|
+
pdf.set_font("Courier", size=9)
|
|
57
|
+
_write_block(line, 5)
|
|
58
|
+
pdf.set_font("Helvetica", size=11)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if not stripped:
|
|
62
|
+
pdf.ln(4)
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# Headings
|
|
66
|
+
if stripped.startswith("### "):
|
|
67
|
+
pdf.set_font("Helvetica", "B", 13)
|
|
68
|
+
pdf.cell(0, 8, stripped[4:], new_x="LMARGIN", new_y="NEXT")
|
|
69
|
+
pdf.set_font("Helvetica", size=11)
|
|
70
|
+
elif stripped.startswith("## "):
|
|
71
|
+
pdf.set_font("Helvetica", "B", 15)
|
|
72
|
+
pdf.cell(0, 10, stripped[3:], new_x="LMARGIN", new_y="NEXT")
|
|
73
|
+
pdf.set_font("Helvetica", size=11)
|
|
74
|
+
elif stripped.startswith("# "):
|
|
75
|
+
pdf.set_font("Helvetica", "B", 17)
|
|
76
|
+
pdf.cell(0, 12, stripped[2:], new_x="LMARGIN", new_y="NEXT")
|
|
77
|
+
pdf.set_font("Helvetica", size=11)
|
|
78
|
+
elif stripped.startswith("- ") or stripped.startswith("* "):
|
|
79
|
+
_write_block(f" - {stripped[2:]}")
|
|
80
|
+
else:
|
|
81
|
+
_write_block(stripped)
|
|
82
|
+
|
|
83
|
+
out = Path(output_path)
|
|
84
|
+
pdf.output(str(out))
|
|
85
|
+
return out
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dd-format
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Abstraction layer for formatting documents from markdown to HTML, PDF, and DOCX.
|
|
5
|
+
Author-email: Digital Duck & Dog Team <info@digitalduck.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/digital-duck/dd-format
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: fpdf2>=2.7
|
|
14
|
+
Requires-Dist: click>=8.0
|
|
15
|
+
Provides-Extra: docx
|
|
16
|
+
Requires-Dist: python-docx>=1.0; extra == "docx"
|
|
17
|
+
Provides-Extra: all
|
|
18
|
+
Requires-Dist: python-docx>=1.0; extra == "all"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# dd-format
|
|
22
|
+
Abstraction layer for formatting document from markdown to others such as html, pdf
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
dd_format/__init__.py
|
|
5
|
+
dd_format/cli.py
|
|
6
|
+
dd_format/docx_writer.py
|
|
7
|
+
dd_format/html_writer.py
|
|
8
|
+
dd_format/pdf_writer.py
|
|
9
|
+
dd_format.egg-info/PKG-INFO
|
|
10
|
+
dd_format.egg-info/SOURCES.txt
|
|
11
|
+
dd_format.egg-info/dependency_links.txt
|
|
12
|
+
dd_format.egg-info/entry_points.txt
|
|
13
|
+
dd_format.egg-info/requires.txt
|
|
14
|
+
dd_format.egg-info/top_level.txt
|
|
15
|
+
tests/test_format.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dd_format
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dd-format"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Digital Duck & Dog Team", email="info@digitalduck.org" },
|
|
10
|
+
]
|
|
11
|
+
description = "Abstraction layer for formatting documents from markdown to HTML, PDF, and DOCX."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"fpdf2>=2.7",
|
|
21
|
+
"click>=8.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
docx = ["python-docx>=1.0"]
|
|
26
|
+
all = ["python-docx>=1.0"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
"Homepage" = "https://github.com/digital-duck/dd-format"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
dd-format = "dd_format.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["."]
|
|
36
|
+
include = ["dd_format*"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Basic tests for dd-format writers."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from dd_format.pdf_writer import markdown_to_pdf
|
|
8
|
+
from dd_format.html_writer import markdown_to_html
|
|
9
|
+
|
|
10
|
+
SAMPLE_MD = """\
|
|
11
|
+
# Test Document
|
|
12
|
+
|
|
13
|
+
## Section One
|
|
14
|
+
|
|
15
|
+
This is a **bold** paragraph with `inline code`.
|
|
16
|
+
|
|
17
|
+
- Item one
|
|
18
|
+
- Item two
|
|
19
|
+
|
|
20
|
+
### Subsection
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
code block here
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
Regular paragraph.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_markdown_to_pdf():
|
|
33
|
+
fd, path = tempfile.mkstemp(suffix=".pdf")
|
|
34
|
+
os.close(fd)
|
|
35
|
+
try:
|
|
36
|
+
out = markdown_to_pdf(SAMPLE_MD, path, title="Test")
|
|
37
|
+
assert out.exists()
|
|
38
|
+
assert out.stat().st_size > 0
|
|
39
|
+
finally:
|
|
40
|
+
Path(path).unlink(missing_ok=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_markdown_to_html():
|
|
44
|
+
fd, path = tempfile.mkstemp(suffix=".html")
|
|
45
|
+
os.close(fd)
|
|
46
|
+
try:
|
|
47
|
+
out = markdown_to_html(SAMPLE_MD, path, title="Test")
|
|
48
|
+
assert out.exists()
|
|
49
|
+
content = out.read_text()
|
|
50
|
+
assert "<h1>" in content
|
|
51
|
+
assert "<strong>bold</strong>" in content
|
|
52
|
+
assert "<code>inline code</code>" in content
|
|
53
|
+
assert "<li>" in content
|
|
54
|
+
assert "<pre>" in content
|
|
55
|
+
finally:
|
|
56
|
+
Path(path).unlink(missing_ok=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_html_no_title():
|
|
60
|
+
fd, path = tempfile.mkstemp(suffix=".html")
|
|
61
|
+
os.close(fd)
|
|
62
|
+
try:
|
|
63
|
+
out = markdown_to_html("# Hello", path)
|
|
64
|
+
content = out.read_text()
|
|
65
|
+
assert "<title>Document</title>" in content
|
|
66
|
+
finally:
|
|
67
|
+
Path(path).unlink(missing_ok=True)
|