diff options
| -rw-r--r-- | src/.gitignore (renamed from .gitignore) | 1 | ||||
| -rw-r--r-- | src/app.py (renamed from app.py) | 47 | ||||
| -rw-r--r-- | src/latex/template.tex (renamed from latex/template.tex) | 25 | ||||
| -rw-r--r-- | src/requirements.txt (renamed from requirements.txt) | 0 | ||||
| -rw-r--r-- | src/static/main.js | 59 | ||||
| -rw-r--r-- | src/templates/index.html (renamed from templates/index.html) | 27 | ||||
| -rw-r--r-- | src/templates/partials/error.html (renamed from templates/partials/error.html) | 0 | ||||
| -rw-r--r-- | src/templates/partials/result.html (renamed from templates/partials/result.html) | 0 | ||||
| -rw-r--r-- | src/templates/partials/upload_error.html | 4 | ||||
| -rw-r--r-- | src/templates/partials/upload_result.html | 12 | ||||
| -rw-r--r-- | static/main.js | 15 |
11 files changed, 169 insertions, 21 deletions
diff --git a/.gitignore b/src/.gitignore index c2788fc..fdbd64b 100644 --- a/.gitignore +++ b/src/.gitignore @@ -2,3 +2,4 @@ __pycache__/ *.pyc .venv/ generated/ +uploads/ @@ -7,14 +7,19 @@ import uuid from pathlib import Path from flask import Flask, render_template, request, send_from_directory, url_for +from werkzeug.utils import secure_filename app = Flask(__name__) BASE_DIR = Path(__file__).resolve().parent GENERATED_DIR = BASE_DIR / "generated" +UPLOADS_DIR = BASE_DIR / "uploads" LATEX_TEMPLATE = BASE_DIR / "latex" / "template.tex" GENERATED_DIR.mkdir(parents=True, exist_ok=True) +UPLOADS_DIR.mkdir(parents=True, exist_ok=True) + +ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp", "svg"} PAPER_SIZES = { "a4paper": "A4", @@ -41,6 +46,13 @@ def _pick(options: dict[str, str], key: str, default_key: str) -> str: return default_key +def _is_allowed_image(filename: str) -> bool: + if "." not in filename: + return False + extension = filename.rsplit(".", 1)[1].lower() + return extension in ALLOWED_IMAGE_EXTENSIONS + + @app.get("/") def index(): return render_template( @@ -65,7 +77,7 @@ def convert_markdown(): epoch = int(time.time()) unique_id = uuid.uuid4().hex - output_name = f"document_{epoch}_{unique_id}.pdf" + output_name = f"likha-pdf_{epoch}_{unique_id}.pdf" output_path = GENERATED_DIR / output_name with tempfile.TemporaryDirectory() as tmp_dir: @@ -86,6 +98,8 @@ def convert_markdown(): f"margin={margin_key}", "-V", f"mainfont={MAIN_FONTS[main_family_key]}", + "--resource-path", + f"{BASE_DIR}:{UPLOADS_DIR}:{tmp_dir}", "-o", str(output_path), ] @@ -112,6 +126,37 @@ def convert_markdown(): ) +@app.post("/upload-image") +def upload_image(): + image = request.files.get("image") + if image is None or image.filename is None or not image.filename.strip(): + return render_template("partials/upload_error.html", message="image file is required."), 400 + + original_name = secure_filename(image.filename) + if not original_name or not _is_allowed_image(original_name): + return render_template("partials/upload_error.html", message="unsupported image type."), 400 + + extension = original_name.rsplit(".", 1)[1].lower() + epoch = int(time.time()) + unique_id = uuid.uuid4().hex + stored_name = f"img_{epoch}_{unique_id}.{extension}" + image_path = UPLOADS_DIR / stored_name + image.save(image_path) + + markdown_snippet = f" / stored_name).as_posix()})" + return render_template( + "partials/upload_result.html", + filename=stored_name, + markdown_snippet=markdown_snippet, + preview_url=url_for("uploaded_image", filename=stored_name), + ) + + +@app.get("/uploads/<path:filename>") +def uploaded_image(filename: str): + return send_from_directory(UPLOADS_DIR, filename) + + @app.get("/download/<path:filename>") def download_pdf(filename: str): return send_from_directory(GENERATED_DIR, filename, as_attachment=True) diff --git a/latex/template.tex b/src/latex/template.tex index 9cb470b..9a0410b 100644 --- a/latex/template.tex +++ b/src/latex/template.tex @@ -14,6 +14,8 @@ \usepackage{microtype} \usepackage{parskip} \usepackage{xcolor} +\usepackage{graphicx} +\usepackage{float} \usepackage{booktabs} \usepackage{longtable} \usepackage{array} @@ -29,12 +31,33 @@ urlcolor=blue, citecolor=black, pdfauthor={}, - pdftitle={Markdown Export} + pdftitle={likha-pdf} } \urlstyle{same} \setlength{\emergencystretch}{3em} \setcounter{secnumdepth}{0} +\setkeys{Gin}{width=\linewidth,keepaspectratio} +\makeatletter +\newsavebox\pandoc@box +\newcommand*\pandocbounded[1]{% + \sbox\pandoc@box{#1}% + \Gscale@div\@tempa{\textheight}{\dimexpr\ht\pandoc@box+\dp\pandoc@box\relax}% + \Gscale@div\@tempb{\linewidth}{\wd\pandoc@box}% + \ifdim\@tempb\p@<\@tempa\p@ + \scalebox{\@tempb}{\usebox\pandoc@box}% + \else + \scalebox{\@tempa}{\usebox\pandoc@box}% + \fi +} +\makeatother +\let\origfigure\figure +\let\endorigfigure\endfigure +\renewenvironment{figure}[1][] { + \expandafter\origfigure\expandafter[H] +} { + \endorigfigure +} \providecommand{\tightlist}{ \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt} } diff --git a/requirements.txt b/src/requirements.txt index 4a4d074..4a4d074 100644 --- a/requirements.txt +++ b/src/requirements.txt diff --git a/src/static/main.js b/src/static/main.js new file mode 100644 index 0000000..f3f4f71 --- /dev/null +++ b/src/static/main.js @@ -0,0 +1,59 @@ +const convertButton = document.getElementById("convert-button"); +const uploadButton = document.getElementById("upload-button"); +const markdownInput = document.getElementById("markdown"); + +function sourceForm(event) { + const sourceElement = event.detail?.elt; + if (!sourceElement) { + return null; + } + return sourceElement.closest("form"); +} + +document.body.addEventListener("htmx:beforeRequest", (event) => { + const form = sourceForm(event); + if (form?.id === "convert-form" && convertButton) { + convertButton.disabled = true; + convertButton.textContent = "generating..."; + } + + if (form?.id === "upload-form" && uploadButton) { + uploadButton.disabled = true; + uploadButton.textContent = "uploading..."; + } +}); + +document.body.addEventListener("htmx:afterRequest", (event) => { + const form = sourceForm(event); + if (form?.id === "convert-form" && convertButton) { + convertButton.disabled = false; + convertButton.textContent = "generate pdf"; + } + + if (form?.id === "upload-form" && uploadButton) { + uploadButton.disabled = false; + uploadButton.textContent = "upload image"; + } +}); + +document.body.addEventListener("click", (event) => { + const target = event.target; + if (!(target instanceof HTMLElement)) { + return; + } + + const button = target.closest("[data-insert-markdown]"); + if (!(button instanceof HTMLElement) || !markdownInput) { + return; + } + + const snippet = button.dataset.insertMarkdown; + if (!snippet) { + return; + } + + const needsLeadingNewline = markdownInput.value && !markdownInput.value.endsWith("\n"); + const prefix = needsLeadingNewline ? "\n" : ""; + markdownInput.value += `${prefix}${snippet}\n`; + markdownInput.focus(); +}); diff --git a/templates/index.html b/src/templates/index.html index cd9c316..42f5f5c 100644 --- a/templates/index.html +++ b/src/templates/index.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <title>Markdown to PDF</title> + <title>likha-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> @@ -19,16 +19,35 @@ </head> <body> <main> - <h1>markdown → pdf</h1> + <h1>likha-pdf</h1> <p>simple markdown export with pandoc + lualatex.</p> + <section> + <h3>image upload</h3> + <form + id="upload-form" + hx-post="/upload-image" + hx-target="#upload-result" + hx-swap="innerHTML" + hx-encoding="multipart/form-data" + hx-indicator="#uploading" + > + <label for="image">image file</label> + <input id="image" name="image" type="file" accept="image/*" required /> + <button id="upload-button" type="submit">upload image</button> + <small id="uploading" class="htmx-indicator">uploading…</small> + </form> + <div id="upload-result" aria-live="polite"></div> + </section> + <form + id="convert-form" hx-post="/convert" hx-target="#result" hx-swap="innerHTML" hx-indicator="#loading" > - <label for="markdown">Markdown</label> + <label for="markdown"><strong>textarea</strong></label> <textarea id="markdown" name="markdown" @@ -66,7 +85,7 @@ </section> <button id="convert-button" type="submit">generate pdf</button> - <small id="loading" class="htmx-indicator">Converting…</small> + <small id="loading" class="htmx-indicator">converting…</small> </form> <section id="result" aria-live="polite"></section> diff --git a/templates/partials/error.html b/src/templates/partials/error.html index 83825f1..83825f1 100644 --- a/templates/partials/error.html +++ b/src/templates/partials/error.html diff --git a/templates/partials/result.html b/src/templates/partials/result.html index 427382d..427382d 100644 --- a/templates/partials/result.html +++ b/src/templates/partials/result.html diff --git a/src/templates/partials/upload_error.html b/src/templates/partials/upload_error.html new file mode 100644 index 0000000..d219783 --- /dev/null +++ b/src/templates/partials/upload_error.html @@ -0,0 +1,4 @@ +<article> + <h4>upload failed</h4> + <pre>{{ message }}</pre> +</article> diff --git a/src/templates/partials/upload_result.html b/src/templates/partials/upload_result.html new file mode 100644 index 0000000..116c74c --- /dev/null +++ b/src/templates/partials/upload_result.html @@ -0,0 +1,12 @@ +<article> + <h4>image uploaded</h4> + <p><a href="{{ preview_url }}" target="_blank" rel="noreferrer">{{ filename }}</a></p> + <p><code id="uploaded-markdown">{{ markdown_snippet }}</code></p> + <button + type="button" + data-insert-markdown="{{ markdown_snippet }}" + class="insert-markdown" + > + insert into markdown + </button> +</article> diff --git a/static/main.js b/static/main.js deleted file mode 100644 index f280d99..0000000 --- a/static/main.js +++ /dev/null @@ -1,15 +0,0 @@ -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"; - } -}); |
