from __future__ import annotations import mimetypes import os import subprocess import tempfile from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse, Response from pydantic import BaseModel BASE_DIR = Path(__file__).resolve().parent DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" class DocxRequest(BaseModel): markdown: str filename: str | None = None app = FastAPI() @app.get("/") def index(): return FileResponse(BASE_DIR / "index.html") @app.get("/{path:path}") def static_files(path: str): if path.startswith("api/"): raise HTTPException(status_code=404) candidate = (BASE_DIR / path).resolve() if candidate == BASE_DIR or BASE_DIR not in candidate.parents: raise HTTPException(status_code=404) if not candidate.exists() or not candidate.is_file(): raise HTTPException(status_code=404) media_type, _ = mimetypes.guess_type(str(candidate)) return FileResponse(candidate, media_type=media_type) @app.post("/api/docx") def export_docx(req: DocxRequest): markdown = (req.markdown or "").strip() if not markdown: raise HTTPException(status_code=400, detail="empty markdown") filename = (req.filename or "output.docx").strip() or "output.docx" if not filename.lower().endswith(".docx"): filename += ".docx" with tempfile.TemporaryDirectory() as td: td_path = Path(td) input_md = td_path / "input.md" output_docx = td_path / "output.docx" input_md.write_text(markdown, encoding="utf-8") cmd = [ "pandoc", str(input_md), "--from", "markdown+tex_math_dollars+tex_math_single_backslash", "--to", "docx", "--output", str(output_docx), ] try: subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except FileNotFoundError as e: raise HTTPException(status_code=500, detail="pandoc not installed") from e except subprocess.CalledProcessError as e: detail = (e.stderr or b"").decode("utf-8", errors="replace")[-4000:] raise HTTPException(status_code=400, detail=f"pandoc failed: {detail}") from e data = output_docx.read_bytes() return Response( content=data, media_type=DOCX_MIME, headers={"Content-Disposition": f'attachment; filename="{filename}"'}, )