diff options
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | README.md | 19 | ||||
| -rw-r--r-- | app.py | 121 | ||||
| -rw-r--r-- | latex/template.tex | 55 | ||||
| -rw-r--r-- | requirements.txt | 1 | ||||
| -rw-r--r-- | static/main.js | 15 | ||||
| -rw-r--r-- | templates/index.html | 77 | ||||
| -rw-r--r-- | templates/partials/error.html | 4 | ||||
| -rw-r--r-- | templates/partials/result.html | 5 |
9 files changed, 301 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2788fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.venv/ +generated/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..853cfda --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# no name yet + +simple web app that converts markdown to pdf using pandoc and lualatex. + +## requirements + +- python 3.10+ +- pandoc +- lualatex + + +## run + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +python app.py +```
\ No newline at end of file @@ -0,0 +1,121 @@ +from __future__ import annotations + +import subprocess +import tempfile +import time +import uuid +from pathlib import Path + +from flask import Flask, render_template, request, send_from_directory, url_for + +app = Flask(__name__) + +BASE_DIR = Path(__file__).resolve().parent +GENERATED_DIR = BASE_DIR / "generated" +LATEX_TEMPLATE = BASE_DIR / "latex" / "template.tex" + +GENERATED_DIR.mkdir(parents=True, exist_ok=True) + +PAPER_SIZES = { + "a4paper": "A4", + "letterpaper": "US Letter", + "legalpaper": "US Legal", +} + +MARGINS = { + "0.75in": "Narrow (0.75in)", + "1in": "Normal (1in)", + "1.25in": "Comfort (1.25in)", + "1.5in": "Wide (1.5in)", +} + +MAIN_FONTS = { + "serif": "TeX Gyre Pagella", + "sans": "TeX Gyre Heros", +} + + +def _pick(options: dict[str, str], key: str, default_key: str) -> str: + if key in options: + return key + return default_key + + +@app.get("/") +def index(): + return render_template( + "index.html", + paper_sizes=PAPER_SIZES, + margins=MARGINS, + ) + + +@app.post("/convert") +def convert_markdown(): + markdown = request.form.get("markdown", "").strip() + if not markdown: + return render_template("partials/error.html", message="Markdown content is required."), 400 + + paper_size_key = _pick(PAPER_SIZES, request.form.get("paper_size", ""), "a4paper") + margin_key = _pick(MARGINS, request.form.get("margin", ""), "1in") + main_family_key = request.form.get("main_font", "serif") + + if main_family_key not in MAIN_FONTS: + main_family_key = "serif" + + epoch = int(time.time()) + unique_id = uuid.uuid4().hex + output_name = f"document_{epoch}_{unique_id}.pdf" + output_path = GENERATED_DIR / output_name + + with tempfile.TemporaryDirectory() as tmp_dir: + temp_markdown = Path(tmp_dir) / "source.md" + temp_markdown.write_text(markdown, encoding="utf-8") + + command = [ + "pandoc", + str(temp_markdown), + "--from", + "markdown+emoji", + "--pdf-engine=lualatex", + "--template", + str(LATEX_TEMPLATE), + "-V", + f"papersize={paper_size_key}", + "-V", + f"margin={margin_key}", + "-V", + f"mainfont={MAIN_FONTS[main_family_key]}", + "-o", + str(output_path), + ] + + try: + subprocess.run(command, check=True, capture_output=True, text=True) + except FileNotFoundError: + return ( + render_template( + "partials/error.html", + message="Pandoc is not installed or not in PATH.", + ), + 500, + ) + except subprocess.CalledProcessError as exc: + stderr = (exc.stderr or "").strip() + error_message = stderr[-1200:] if stderr else "PDF conversion failed." + return render_template("partials/error.html", message=error_message), 400 + + return render_template( + "partials/result.html", + download_url=url_for("download_pdf", filename=output_name), + filename=output_name, + ) + + +@app.get("/download/<path:filename>") +def download_pdf(filename: str): + return send_from_directory(GENERATED_DIR, filename, as_attachment=True) + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/latex/template.tex b/latex/template.tex new file mode 100644 index 0000000..9cb470b --- /dev/null +++ b/latex/template.tex @@ -0,0 +1,55 @@ +\documentclass[11pt]{article} + +\usepackage{fontspec} +\newfontfamily{\emojifont}{Noto Color Emoji}[Renderer=HarfBuzz] +\directlua{ + luaotfload.add_fallback("emojifallback", { + "Noto Color Emoji:mode=harf;" + }) +} +\setmainfont{$mainfont$}[RawFeature={fallback=emojifallback}] +\setmonofont{Latin Modern Mono} + +\usepackage[paper=$papersize$,margin=$margin$]{geometry} +\usepackage{microtype} +\usepackage{parskip} +\usepackage{xcolor} +\usepackage{booktabs} +\usepackage{longtable} +\usepackage{array} +\usepackage{calc} +\usepackage{etoolbox} +\usepackage{fancyvrb} +\usepackage{fvextra} +\DefineVerbatimEnvironment{Highlighting}{Verbatim}{breaklines,commandchars=\\\{\}} +\usepackage{hyperref} +\hypersetup{ + colorlinks=true, + linkcolor=black, + urlcolor=blue, + citecolor=black, + pdfauthor={}, + pdftitle={Markdown Export} +} +\urlstyle{same} + +\setlength{\emergencystretch}{3em} +\setcounter{secnumdepth}{0} +\providecommand{\tightlist}{ + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt} +} + +$if(highlighting-macros)$ +$highlighting-macros$ +$endif$ + +\begin{document} + +$if(title)$ +{\huge\bfseries $title$\par} +\vspace{1em} +$endif$ + +$body$ + +\end{document}
\ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4a4d074 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask>=3.0.0,<4.0.0 diff --git a/static/main.js b/static/main.js new file mode 100644 index 0000000..f280d99 --- /dev/null +++ b/static/main.js @@ -0,0 +1,15 @@ +const convertButton = document.getElementById("convert-button"); + +document.body.addEventListener("htmx:beforeRequest", () => { + if (convertButton) { + convertButton.disabled = true; + convertButton.textContent = "generating..."; + } +}); + +document.body.addEventListener("htmx:afterRequest", () => { + if (convertButton) { + convertButton.disabled = false; + convertButton.textContent = "generate pdf"; + } +}); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..cd9c316 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,77 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>Markdown to PDF</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/kj-sh604/noir.css@latest/out/noir.min.css" /> + <script src="https://unpkg.com/htmx.org@1.9.12"></script> + <style> + .htmx-indicator { + display: none; + } + + .htmx-request .htmx-indicator, + .htmx-request.htmx-indicator { + display: inline; + } + </style> + </head> + <body> + <main> + <h1>markdown → pdf</h1> + <p>simple markdown export with pandoc + lualatex.</p> + + <form + hx-post="/convert" + hx-target="#result" + hx-swap="innerHTML" + hx-indicator="#loading" + > + <label for="markdown">Markdown</label> + <textarea + id="markdown" + name="markdown" + rows="16" + placeholder="markdown goes here..." + required + ></textarea> + + <section> + <h3>pdf options</h3> + + <label for="paper_size">Paper size</label> + <select id="paper_size" name="paper_size"> + {% for key, label in paper_sizes.items() %} + <option value="{{ key }}">{{ label }}</option> + {% endfor %} + </select> + + <label for="margin">Margins</label> + <select id="margin" name="margin"> + {% for key, label in margins.items() %} + <option value="{{ key }}" {% if key == "1in" %}selected{% endif %}>{{ label }}</option> + {% endfor %} + </select> + + <fieldset> + <legend>main font</legend> + <label> + <input type="radio" name="main_font" value="serif" checked /> serif + </label> + <label> + <input type="radio" name="main_font" value="sans" /> sans-serif + </label> + </fieldset> + </section> + + <button id="convert-button" type="submit">generate pdf</button> + <small id="loading" class="htmx-indicator">Converting…</small> + </form> + + <section id="result" aria-live="polite"></section> + </main> + + <script src="{{ url_for('static', filename='main.js') }}"></script> + </body> +</html> diff --git a/templates/partials/error.html b/templates/partials/error.html new file mode 100644 index 0000000..83825f1 --- /dev/null +++ b/templates/partials/error.html @@ -0,0 +1,4 @@ +<article> + <h3>Conversion failed</h3> + <pre>{{ message }}</pre> +</article> diff --git a/templates/partials/result.html b/templates/partials/result.html new file mode 100644 index 0000000..427382d --- /dev/null +++ b/templates/partials/result.html @@ -0,0 +1,5 @@ +<article> + <h3>pdf ready</h3> + <p><strong>{{ filename }}</strong></p> + <a href="{{ download_url }}">download pdf</a> +</article> |
