synthkit 0.1.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.
- synthkit/__init__.py +3 -0
- synthkit/base.py +89 -0
- synthkit/cli.py +106 -0
- synthkit/doc.py +24 -0
- synthkit/email.py +77 -0
- synthkit/html.py +24 -0
- synthkit/pdf.py +134 -0
- synthkit-0.1.0.dist-info/METADATA +165 -0
- synthkit-0.1.0.dist-info/RECORD +11 -0
- synthkit-0.1.0.dist-info/WHEEL +4 -0
- synthkit-0.1.0.dist-info/entry_points.txt +6 -0
synthkit/__init__.py
ADDED
synthkit/base.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Base converter with shared logic for all Synthkit converters."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pypandoc
|
|
9
|
+
|
|
10
|
+
BASE_FORMAT = "markdown+lists_without_preceding_blankline"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_format(hard_breaks: bool = False) -> str:
|
|
14
|
+
fmt = BASE_FORMAT
|
|
15
|
+
if hard_breaks:
|
|
16
|
+
fmt += "+hard_line_breaks"
|
|
17
|
+
return fmt
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def config_path(tool_name: str, filename: str) -> Path | None:
|
|
21
|
+
p = Path.home() / ".config" / tool_name / filename
|
|
22
|
+
return p if p.is_file() else None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def bundled_style_css() -> Path:
|
|
26
|
+
return Path(__file__).parent.parent.parent / "style.css"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cleanup_mermaid_err() -> None:
|
|
30
|
+
err = Path("mermaid-filter.err")
|
|
31
|
+
if err.is_file() and err.stat().st_size == 0:
|
|
32
|
+
err.unlink()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_pandoc_bin() -> str:
|
|
36
|
+
return str(pypandoc.get_pandoc_path())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_pandoc(
|
|
40
|
+
args: list[str], env: dict[str, str] | None = None
|
|
41
|
+
) -> subprocess.CompletedProcess[bytes]:
|
|
42
|
+
return subprocess.run([get_pandoc_bin(), *args], capture_output=False, env=env)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def mermaid_args(mermaid: bool) -> list[str]:
|
|
46
|
+
if mermaid:
|
|
47
|
+
return ["--filter", "mermaid-filter"]
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def batch_convert(
|
|
52
|
+
files: tuple[str, ...],
|
|
53
|
+
hard_breaks: bool,
|
|
54
|
+
mermaid: bool,
|
|
55
|
+
convert_one: Callable[[Path, bool, bool], None],
|
|
56
|
+
) -> None:
|
|
57
|
+
if not files:
|
|
58
|
+
print("No input files provided.", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
success = 0
|
|
62
|
+
fail = 0
|
|
63
|
+
|
|
64
|
+
for filepath in files:
|
|
65
|
+
path = Path(filepath)
|
|
66
|
+
if path.suffix != ".md":
|
|
67
|
+
print(f"Skipping non-markdown file: {filepath}")
|
|
68
|
+
continue
|
|
69
|
+
if not path.is_file():
|
|
70
|
+
print(f"Warning: File '{filepath}' not found, skipping.")
|
|
71
|
+
fail += 1
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
convert_one(path, hard_breaks, mermaid)
|
|
76
|
+
success += 1
|
|
77
|
+
except ConversionError as e:
|
|
78
|
+
print(f"Error: {e}")
|
|
79
|
+
fail += 1
|
|
80
|
+
|
|
81
|
+
if mermaid:
|
|
82
|
+
cleanup_mermaid_err()
|
|
83
|
+
print(f"\nDone: {success} succeeded, {fail} failed.")
|
|
84
|
+
if fail > 0:
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ConversionError(Exception):
|
|
89
|
+
pass
|
synthkit/cli.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Click CLI for Synthkit."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from . import doc, email, html, pdf
|
|
8
|
+
from .base import batch_convert
|
|
9
|
+
|
|
10
|
+
_MERMAID_HELP = "Enable Mermaid diagram rendering (requires mermaid-filter)."
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def main() -> None:
|
|
15
|
+
"""Synthkit: Convert AI-generated Markdown into production-ready documents."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@main.command("doc")
|
|
19
|
+
@click.option("--hard-breaks", is_flag=True, help="Preserve line breaks in source.")
|
|
20
|
+
@click.option("--mermaid", is_flag=True, help=_MERMAID_HELP)
|
|
21
|
+
@click.argument("files", nargs=-1, type=click.Path())
|
|
22
|
+
def doc_subcmd(hard_breaks: bool, mermaid: bool, files: tuple[str, ...]) -> None:
|
|
23
|
+
"""Convert Markdown to Word (.docx)."""
|
|
24
|
+
batch_convert(files, hard_breaks, mermaid, doc.convert)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@main.command("email")
|
|
28
|
+
@click.option("--hard-breaks", is_flag=True, help="Preserve line breaks in source.")
|
|
29
|
+
@click.option("--mermaid", is_flag=True, help=_MERMAID_HELP)
|
|
30
|
+
@click.argument("file", type=click.Path())
|
|
31
|
+
def email_subcmd(hard_breaks: bool, mermaid: bool, file: str) -> None:
|
|
32
|
+
"""Convert Markdown to clipboard-ready email."""
|
|
33
|
+
email.convert(Path(file), hard_breaks, mermaid)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@main.command("html")
|
|
37
|
+
@click.option("--hard-breaks", is_flag=True, help="Preserve line breaks in source.")
|
|
38
|
+
@click.option("--mermaid", is_flag=True, help=_MERMAID_HELP)
|
|
39
|
+
@click.argument("files", nargs=-1, type=click.Path())
|
|
40
|
+
def html_subcmd(hard_breaks: bool, mermaid: bool, files: tuple[str, ...]) -> None:
|
|
41
|
+
"""Convert Markdown to HTML."""
|
|
42
|
+
batch_convert(files, hard_breaks, mermaid, html.convert)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@main.command("pdf")
|
|
46
|
+
@click.option("--hard-breaks", is_flag=True, help="Preserve line breaks in source.")
|
|
47
|
+
@click.option("--mermaid", is_flag=True, help=_MERMAID_HELP)
|
|
48
|
+
@click.argument("files", nargs=-1, type=click.Path())
|
|
49
|
+
def pdf_subcmd(hard_breaks: bool, mermaid: bool, files: tuple[str, ...]) -> None:
|
|
50
|
+
"""Convert Markdown to PDF."""
|
|
51
|
+
batch_convert(files, hard_breaks, mermaid, pdf.convert)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Backward-compatible standalone entry points
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def md2doc_cmd() -> None:
|
|
58
|
+
"""Standalone md2doc entry point."""
|
|
59
|
+
|
|
60
|
+
@click.command()
|
|
61
|
+
@click.option("--hard-breaks", is_flag=True, help="Preserve line breaks in source.")
|
|
62
|
+
@click.option("--mermaid", is_flag=True, help=_MERMAID_HELP)
|
|
63
|
+
@click.argument("files", nargs=-1, type=click.Path())
|
|
64
|
+
def _cmd(hard_breaks: bool, mermaid: bool, files: tuple[str, ...]) -> None:
|
|
65
|
+
batch_convert(files, hard_breaks, mermaid, doc.convert)
|
|
66
|
+
|
|
67
|
+
_cmd()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def md2email_cmd() -> None:
|
|
71
|
+
"""Standalone md2email entry point."""
|
|
72
|
+
|
|
73
|
+
@click.command()
|
|
74
|
+
@click.option("--hard-breaks", is_flag=True, help="Preserve line breaks in source.")
|
|
75
|
+
@click.option("--mermaid", is_flag=True, help=_MERMAID_HELP)
|
|
76
|
+
@click.argument("file", type=click.Path())
|
|
77
|
+
def _cmd(hard_breaks: bool, mermaid: bool, file: str) -> None:
|
|
78
|
+
email.convert(Path(file), hard_breaks, mermaid)
|
|
79
|
+
|
|
80
|
+
_cmd()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def md2html_cmd() -> None:
|
|
84
|
+
"""Standalone md2html entry point."""
|
|
85
|
+
|
|
86
|
+
@click.command()
|
|
87
|
+
@click.option("--hard-breaks", is_flag=True, help="Preserve line breaks in source.")
|
|
88
|
+
@click.option("--mermaid", is_flag=True, help=_MERMAID_HELP)
|
|
89
|
+
@click.argument("files", nargs=-1, type=click.Path())
|
|
90
|
+
def _cmd(hard_breaks: bool, mermaid: bool, files: tuple[str, ...]) -> None:
|
|
91
|
+
batch_convert(files, hard_breaks, mermaid, html.convert)
|
|
92
|
+
|
|
93
|
+
_cmd()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def md2pdf_cmd() -> None:
|
|
97
|
+
"""Standalone md2pdf entry point."""
|
|
98
|
+
|
|
99
|
+
@click.command()
|
|
100
|
+
@click.option("--hard-breaks", is_flag=True, help="Preserve line breaks in source.")
|
|
101
|
+
@click.option("--mermaid", is_flag=True, help=_MERMAID_HELP)
|
|
102
|
+
@click.argument("files", nargs=-1, type=click.Path())
|
|
103
|
+
def _cmd(hard_breaks: bool, mermaid: bool, files: tuple[str, ...]) -> None:
|
|
104
|
+
batch_convert(files, hard_breaks, mermaid, pdf.convert)
|
|
105
|
+
|
|
106
|
+
_cmd()
|
synthkit/doc.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Markdown to Word (.docx) conversion."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .base import ConversionError, build_format, config_path, mermaid_args, run_pandoc
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convert(path: Path, hard_breaks: bool = False, mermaid: bool = False) -> None:
|
|
9
|
+
output = path.with_suffix(".docx").name
|
|
10
|
+
fmt = build_format(hard_breaks)
|
|
11
|
+
|
|
12
|
+
args = [str(path), "-f", fmt, "-t", "docx", *mermaid_args(mermaid)]
|
|
13
|
+
|
|
14
|
+
ref = config_path("md2doc", "reference.docx")
|
|
15
|
+
if ref:
|
|
16
|
+
args += [f"--reference-doc={ref}"]
|
|
17
|
+
|
|
18
|
+
args += ["-o", output]
|
|
19
|
+
|
|
20
|
+
print(f"Converting {path} to {output}...")
|
|
21
|
+
result = run_pandoc(args)
|
|
22
|
+
if result.returncode != 0:
|
|
23
|
+
raise ConversionError(f"Pandoc failed to generate {output}")
|
|
24
|
+
print(f"Successfully created: {output}")
|
synthkit/email.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Markdown to clipboard email conversion."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pyperclip
|
|
9
|
+
|
|
10
|
+
from .base import (
|
|
11
|
+
build_format,
|
|
12
|
+
cleanup_mermaid_err,
|
|
13
|
+
config_path,
|
|
14
|
+
get_pandoc_bin,
|
|
15
|
+
mermaid_args,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def convert(path: Path, hard_breaks: bool = False, mermaid: bool = False) -> None:
|
|
20
|
+
# Smart file finding: accept with or without .md extension
|
|
21
|
+
if not path.is_file() and path.with_suffix(".md").is_file():
|
|
22
|
+
path = path.with_suffix(".md")
|
|
23
|
+
|
|
24
|
+
if not path.is_file():
|
|
25
|
+
print(f"Error: Could not find file '{path}' or '{path.with_suffix('.md')}'")
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
fmt = build_format(hard_breaks)
|
|
29
|
+
|
|
30
|
+
args = [
|
|
31
|
+
get_pandoc_bin(),
|
|
32
|
+
str(path),
|
|
33
|
+
"-f",
|
|
34
|
+
fmt,
|
|
35
|
+
"-t",
|
|
36
|
+
"html",
|
|
37
|
+
"-s",
|
|
38
|
+
"--self-contained",
|
|
39
|
+
*mermaid_args(mermaid),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
style = config_path("md2email", "style.css")
|
|
43
|
+
if style:
|
|
44
|
+
args += [f"--css={style}"]
|
|
45
|
+
|
|
46
|
+
print(f"Converting {path} to clipboard...")
|
|
47
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
48
|
+
if result.returncode != 0:
|
|
49
|
+
print(f"Error: Pandoc failed to convert {path}")
|
|
50
|
+
if result.stderr:
|
|
51
|
+
print(result.stderr, file=sys.stderr)
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
html = result.stdout
|
|
55
|
+
|
|
56
|
+
# On macOS, convert to RTF via textutil for richer paste support
|
|
57
|
+
if platform.system() == "Darwin":
|
|
58
|
+
try:
|
|
59
|
+
rtf_result = subprocess.run(
|
|
60
|
+
["textutil", "-stdin", "-format", "html", "-convert", "rtf", "-stdout"],
|
|
61
|
+
input=html,
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
)
|
|
65
|
+
if rtf_result.returncode == 0:
|
|
66
|
+
subprocess.run(["pbcopy"], input=rtf_result.stdout, text=True, check=True)
|
|
67
|
+
print("Success! Formatted text copied to clipboard.")
|
|
68
|
+
if mermaid:
|
|
69
|
+
cleanup_mermaid_err()
|
|
70
|
+
return
|
|
71
|
+
except FileNotFoundError:
|
|
72
|
+
pass # Fall through to pyperclip
|
|
73
|
+
|
|
74
|
+
pyperclip.copy(html)
|
|
75
|
+
print("Success! HTML copied to clipboard.")
|
|
76
|
+
if mermaid:
|
|
77
|
+
cleanup_mermaid_err()
|
synthkit/html.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Markdown to HTML conversion."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .base import ConversionError, build_format, config_path, mermaid_args, run_pandoc
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convert(path: Path, hard_breaks: bool = False, mermaid: bool = False) -> None:
|
|
9
|
+
output = path.with_suffix(".html").name
|
|
10
|
+
fmt = build_format(hard_breaks)
|
|
11
|
+
|
|
12
|
+
args = [str(path), "-f", fmt, "-t", "html", "-s", *mermaid_args(mermaid)]
|
|
13
|
+
|
|
14
|
+
style = config_path("md2html", "style.css")
|
|
15
|
+
if style:
|
|
16
|
+
args += [f"--css={style}", "--self-contained"]
|
|
17
|
+
|
|
18
|
+
args += ["-o", output]
|
|
19
|
+
|
|
20
|
+
print(f"Converting {path} to {output}...")
|
|
21
|
+
result = run_pandoc(args)
|
|
22
|
+
if result.returncode != 0:
|
|
23
|
+
raise ConversionError(f"Pandoc failed to generate {output}")
|
|
24
|
+
print(f"Successfully created: {output}")
|
synthkit/pdf.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Markdown to PDF conversion via weasyprint."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .base import ConversionError, build_format, config_path, mermaid_args, run_pandoc
|
|
10
|
+
|
|
11
|
+
_INSTALL_HELP = {
|
|
12
|
+
"Darwin": ("On macOS, install them with:\n brew install pango"),
|
|
13
|
+
"Linux": (
|
|
14
|
+
"On Ubuntu/Debian, install them with:\n"
|
|
15
|
+
" sudo apt install libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev\n"
|
|
16
|
+
"On Fedora/RHEL:\n"
|
|
17
|
+
" sudo dnf install pango-devel cairo-devel gdk-pixbuf2-devel"
|
|
18
|
+
),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_WEASYPRINT_DOCS = "https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _brew_lib_path() -> str | None:
|
|
25
|
+
"""Return Homebrew lib directory if available."""
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(["brew", "--prefix"], capture_output=True, timeout=5)
|
|
28
|
+
if result.returncode == 0:
|
|
29
|
+
prefix = result.stdout.decode().strip()
|
|
30
|
+
lib_dir = os.path.join(prefix, "lib")
|
|
31
|
+
if os.path.isdir(lib_dir):
|
|
32
|
+
return lib_dir
|
|
33
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
34
|
+
pass
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _weasyprint_env() -> dict[str, str] | None:
|
|
39
|
+
"""Build environment with library paths for weasyprint on macOS."""
|
|
40
|
+
if platform.system() != "Darwin":
|
|
41
|
+
return None
|
|
42
|
+
brew_lib = _brew_lib_path()
|
|
43
|
+
if not brew_lib:
|
|
44
|
+
return None
|
|
45
|
+
env = os.environ.copy()
|
|
46
|
+
existing = env.get("DYLD_FALLBACK_LIBRARY_PATH", "")
|
|
47
|
+
if brew_lib not in existing:
|
|
48
|
+
env["DYLD_FALLBACK_LIBRARY_PATH"] = f"{brew_lib}:{existing}" if existing else brew_lib
|
|
49
|
+
return env
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _check_weasyprint_deps() -> dict[str, str] | None:
|
|
53
|
+
"""Verify weasyprint's system deps are available. Returns env dict if needed."""
|
|
54
|
+
env = None
|
|
55
|
+
try:
|
|
56
|
+
result = subprocess.run(
|
|
57
|
+
["weasyprint", "--info"],
|
|
58
|
+
capture_output=True,
|
|
59
|
+
timeout=10,
|
|
60
|
+
)
|
|
61
|
+
if result.returncode != 0:
|
|
62
|
+
stderr = result.stderr.decode(errors="replace")
|
|
63
|
+
if "gobject" in stderr or "pango" in stderr or "cairo" in stderr:
|
|
64
|
+
# Try again with Homebrew library path on macOS
|
|
65
|
+
env = _weasyprint_env()
|
|
66
|
+
if env:
|
|
67
|
+
retry = subprocess.run(
|
|
68
|
+
["weasyprint", "--info"],
|
|
69
|
+
capture_output=True,
|
|
70
|
+
timeout=10,
|
|
71
|
+
env=env,
|
|
72
|
+
)
|
|
73
|
+
if retry.returncode == 0:
|
|
74
|
+
return env # libs found with env fix
|
|
75
|
+
|
|
76
|
+
# Still broken — report helpful error
|
|
77
|
+
system = platform.system()
|
|
78
|
+
hint = _INSTALL_HELP.get(system, "")
|
|
79
|
+
msg = (
|
|
80
|
+
"weasyprint is installed but cannot find its system libraries "
|
|
81
|
+
"(pango, cairo, gobject).\n"
|
|
82
|
+
)
|
|
83
|
+
if hint:
|
|
84
|
+
msg += f"\n{hint}\n"
|
|
85
|
+
msg += f"\nFor full details see: {_WEASYPRINT_DOCS}"
|
|
86
|
+
raise ConversionError(msg)
|
|
87
|
+
except FileNotFoundError:
|
|
88
|
+
pass # handled by the shutil.which check below
|
|
89
|
+
except subprocess.TimeoutExpired:
|
|
90
|
+
pass # let pandoc attempt it
|
|
91
|
+
return env
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def convert(path: Path, hard_breaks: bool = False, mermaid: bool = False) -> None:
|
|
95
|
+
output = path.with_suffix(".pdf").name
|
|
96
|
+
fmt = build_format(hard_breaks)
|
|
97
|
+
|
|
98
|
+
if not shutil.which("weasyprint"):
|
|
99
|
+
raise ConversionError(
|
|
100
|
+
"weasyprint not found on PATH. Install it with: pip install weasyprint"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
env = _check_weasyprint_deps()
|
|
104
|
+
|
|
105
|
+
args = [
|
|
106
|
+
str(path),
|
|
107
|
+
"-f",
|
|
108
|
+
fmt,
|
|
109
|
+
"-t",
|
|
110
|
+
"html",
|
|
111
|
+
"--pdf-engine=weasyprint",
|
|
112
|
+
*mermaid_args(mermaid),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
style = config_path("md2pdf", "style.css")
|
|
116
|
+
if style:
|
|
117
|
+
args += [f"--css={style}"]
|
|
118
|
+
|
|
119
|
+
args += ["-o", output]
|
|
120
|
+
|
|
121
|
+
print(f"Converting {path} to {output}...")
|
|
122
|
+
result = run_pandoc(args, env=env)
|
|
123
|
+
if result.returncode != 0:
|
|
124
|
+
system = platform.system()
|
|
125
|
+
hint = _INSTALL_HELP.get(system, "")
|
|
126
|
+
msg = f"Pandoc failed to generate {output}"
|
|
127
|
+
if hint:
|
|
128
|
+
msg += (
|
|
129
|
+
"\n\nThis may be caused by missing system dependencies for weasyprint.\n"
|
|
130
|
+
f"{hint}\n"
|
|
131
|
+
f"\nFor full details see: {_WEASYPRINT_DOCS}"
|
|
132
|
+
)
|
|
133
|
+
raise ConversionError(msg)
|
|
134
|
+
print(f"Successfully created: {output}")
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: synthkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert AI-generated Markdown into production-ready documents
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: click
|
|
8
|
+
Requires-Dist: pypandoc-binary
|
|
9
|
+
Requires-Dist: pyperclip
|
|
10
|
+
Requires-Dist: weasyprint
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Synthkit
|
|
19
|
+
|
|
20
|
+
[](https://pypi.org/project/synthkit/)
|
|
21
|
+
[](https://www.python.org/downloads/)
|
|
22
|
+
[](https://opensource.org/licenses/MIT)
|
|
23
|
+
[](https://github.com/rappdw/synthkit/actions/workflows/tests.yml)
|
|
24
|
+
[](https://github.com/rappdw/synthkit/actions/workflows/tests.yml)
|
|
25
|
+
[](https://github.com/astral-sh/ruff)
|
|
26
|
+
[](https://mypy-lang.org/)
|
|
27
|
+
|
|
28
|
+
A "last-mile" toolkit for working with generative AI. Synthkit bridges the gap between raw LLM output and production-ready deliverables through:
|
|
29
|
+
|
|
30
|
+
- **Document conversion** — Transform AI-generated Markdown into Word, HTML, PDF, or clipboard-ready email
|
|
31
|
+
- **Prompt templates** — Curated templates for structured AI interactions (reports, emails, analysis)
|
|
32
|
+
- **Guidelines** — Reference standards and style guides to steer AI output quality
|
|
33
|
+
|
|
34
|
+
## Document Conversion
|
|
35
|
+
|
|
36
|
+
### Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Run directly with uvx (no install needed)
|
|
40
|
+
uvx synthkit html document.md
|
|
41
|
+
|
|
42
|
+
# Or install globally
|
|
43
|
+
uv tool install synthkit
|
|
44
|
+
|
|
45
|
+
# Or install with pip
|
|
46
|
+
pip install synthkit
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Pandoc is bundled automatically via [`pypandoc_binary`](https://pypi.org/project/pypandoc-binary/) — no separate install needed.
|
|
50
|
+
|
|
51
|
+
#### System dependencies for PDF
|
|
52
|
+
|
|
53
|
+
PDF conversion uses [WeasyPrint](https://weasyprint.org/), which requires system libraries:
|
|
54
|
+
|
|
55
|
+
| Platform | Install command |
|
|
56
|
+
|----------|----------------|
|
|
57
|
+
| **macOS** | `brew install pango` |
|
|
58
|
+
| **Ubuntu/Debian** | `apt install libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev` |
|
|
59
|
+
| **Windows** | See [WeasyPrint docs](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#windows) |
|
|
60
|
+
|
|
61
|
+
`doc`, `html`, and `email` commands work without these.
|
|
62
|
+
|
|
63
|
+
### Usage
|
|
64
|
+
|
|
65
|
+
#### Unified CLI
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
synthkit doc report.md # → report.docx
|
|
69
|
+
synthkit html report.md # → report.html
|
|
70
|
+
synthkit pdf report.md # → report.pdf
|
|
71
|
+
synthkit email report.md # → clipboard
|
|
72
|
+
|
|
73
|
+
# Batch processing
|
|
74
|
+
synthkit doc *.md
|
|
75
|
+
synthkit html *.md --hard-breaks
|
|
76
|
+
|
|
77
|
+
# Mermaid diagrams (requires mermaid-filter)
|
|
78
|
+
synthkit html report.md --mermaid
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Backward-compatible commands
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
md2doc report.md
|
|
85
|
+
md2html report.md
|
|
86
|
+
md2pdf report.md
|
|
87
|
+
md2email report.md
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Options
|
|
91
|
+
|
|
92
|
+
| Flag | Description |
|
|
93
|
+
|------|-------------|
|
|
94
|
+
| `--hard-breaks` | Preserve line breaks from source markdown |
|
|
95
|
+
| `--mermaid` | Enable Mermaid diagram rendering (requires [`mermaid-filter`](https://github.com/raghur/mermaid-filter)) |
|
|
96
|
+
|
|
97
|
+
### Configuration
|
|
98
|
+
|
|
99
|
+
Each converter looks for optional config files under `~/.config/<toolname>/`:
|
|
100
|
+
|
|
101
|
+
| Converter | Config Files |
|
|
102
|
+
|-----------|-------------|
|
|
103
|
+
| `doc` | `~/.config/md2doc/reference.docx` |
|
|
104
|
+
| `email` | `~/.config/md2email/style.css` |
|
|
105
|
+
| `html` | `~/.config/md2html/style.css` |
|
|
106
|
+
| `pdf` | `~/.config/md2pdf/style.css` |
|
|
107
|
+
|
|
108
|
+
## Prompt Templates
|
|
109
|
+
|
|
110
|
+
The `prompt-templates/` directory contains curated prompt templates for structured AI interactions. These are optimized for Markdown-first responses to ensure compatibility with the document conversion tools.
|
|
111
|
+
|
|
112
|
+
Copy the contents of any template into your LLM of choice (Claude, Gemini, ChatGPT, etc.) to get consistently structured output ready for conversion.
|
|
113
|
+
|
|
114
|
+
## Guidelines
|
|
115
|
+
|
|
116
|
+
The `guidelines/` directory contains reference standards and style guides that can be provided as context to AI models to steer output quality and consistency.
|
|
117
|
+
|
|
118
|
+
## Testing
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Run tests
|
|
122
|
+
uv run --extra dev pytest
|
|
123
|
+
|
|
124
|
+
# With coverage
|
|
125
|
+
uv run --extra dev pytest --cov=synthkit --cov-report=term-missing
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Tests run automatically on push/PR to `main` across Python 3.10-3.13 on Linux, macOS, and Windows.
|
|
129
|
+
|
|
130
|
+
## Repository Structure
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
├── .github/workflows/
|
|
134
|
+
│ ├── tests.yml # CI: test on push/PR to main
|
|
135
|
+
│ └── publish.yml # CD: publish to PyPI on release
|
|
136
|
+
├── pyproject.toml
|
|
137
|
+
├── src/synthkit/ # Python package
|
|
138
|
+
│ ├── cli.py # Click CLI with subcommands
|
|
139
|
+
│ ├── base.py # Shared conversion logic
|
|
140
|
+
│ ├── doc.py # Word conversion
|
|
141
|
+
│ ├── email.py # Email clipboard conversion
|
|
142
|
+
│ ├── html.py # HTML conversion
|
|
143
|
+
│ └── pdf.py # PDF conversion (via WeasyPrint)
|
|
144
|
+
├── tests/ # Test suite (pytest)
|
|
145
|
+
│ ├── conftest.py # Shared fixtures
|
|
146
|
+
│ ├── test_base.py # Base module tests
|
|
147
|
+
│ ├── test_cli.py # CLI + integration tests
|
|
148
|
+
│ ├── test_doc.py # Word converter tests
|
|
149
|
+
│ ├── test_email.py # Email converter tests
|
|
150
|
+
│ ├── test_html.py # HTML converter tests
|
|
151
|
+
│ └── test_pdf.py # PDF converter tests
|
|
152
|
+
├── style.css # Default stylesheet
|
|
153
|
+
├── prompt-templates/ # AI interaction prompt templates
|
|
154
|
+
└── guidelines/ # Reference guidelines
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Dependencies
|
|
158
|
+
|
|
159
|
+
| Package | Purpose | Bundled? |
|
|
160
|
+
|---------|---------|----------|
|
|
161
|
+
| [`click`](https://click.palletsprojects.com/) | CLI framework | pip |
|
|
162
|
+
| [`pypandoc_binary`](https://pypi.org/project/pypandoc-binary/) | Pandoc document converter | pip (includes binary) |
|
|
163
|
+
| [`pyperclip`](https://pypi.org/project/pyperclip/) | Cross-platform clipboard | pip |
|
|
164
|
+
| [`weasyprint`](https://weasyprint.org/) | PDF engine | pip (needs system libs) |
|
|
165
|
+
| [`mermaid-filter`](https://github.com/raghur/mermaid-filter) | Mermaid diagrams | Optional, external |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
synthkit/__init__.py,sha256=MdHu9RiZYInmSDUdVZAKcZDCixIcdsM49x5cJgwHqIk,102
|
|
2
|
+
synthkit/base.py,sha256=rjcpiCTepgizEs2ygNxTIR3ZHBBA8Z8a7fKwbwx0_dI,2147
|
|
3
|
+
synthkit/cli.py,sha256=YYAEKZ1PxvZVdhIHv-4xjjAYb_bf34HDHoB48WCDGoo,3800
|
|
4
|
+
synthkit/doc.py,sha256=eSL6lHbC_bPblMY2MDU3LIN5xC4GpedKm9wOuRivzg0,752
|
|
5
|
+
synthkit/email.py,sha256=WfjrYAewg_qYFz6LW6CtQwLdfGnAAYyTLRq6xB_KQlo,2152
|
|
6
|
+
synthkit/html.py,sha256=BABUCG_gX-WWrfrOo4a_AmAtdfevtmxXRLJaxVwTpWk,762
|
|
7
|
+
synthkit/pdf.py,sha256=UpGQSRlFxxYTDJCN6HqfoOpN5pbcNtdSWTHguhnWbtI,4468
|
|
8
|
+
synthkit-0.1.0.dist-info/METADATA,sha256=sqU82aulOLONJ9Zei-xrBqZA9M24VuJK7-k6SByQ8aI,6422
|
|
9
|
+
synthkit-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
synthkit-0.1.0.dist-info/entry_points.txt,sha256=zB7h-Oabqqkac6L2XPCF-JWDp3IAPy-_7S-1Qe9BN6A,185
|
|
11
|
+
synthkit-0.1.0.dist-info/RECORD,,
|