aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--README.md19
-rw-r--r--app.py121
-rw-r--r--latex/template.tex55
-rw-r--r--requirements.txt1
-rw-r--r--static/main.js15
-rw-r--r--templates/index.html77
-rw-r--r--templates/partials/error.html4
-rw-r--r--templates/partials/result.html5
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
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..991bc57
--- /dev/null
+++ b/app.py
@@ -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>