aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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.js59
-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.html4
-rw-r--r--src/templates/partials/upload_result.html12
-rw-r--r--static/main.js15
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/
diff --git a/app.py b/src/app.py
index 991bc57..5a3aa7b 100644
--- a/app.py
+++ b/src/app.py
@@ -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"![]({(Path('uploads') / 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";
- }
-});