Files
flowdeck/app/routers/export.py
T
bruno 6e30589133
FlowDeck CI / test (push) Failing after 20s
FlowDeck CI / docker (push) Skipped
fix(export): v4.7.3 tableaux + emojis en PDF/HTML
- Tableaux GFM rendus comme texte brut dans les exports HTML/PDF. Ajout d'un
  parseur de tableaux pipe (bloc 'table') + rendu <table> (thead/tbody,
  alignement gauche/centre/droite, bordures .ftable) dans blocks_to_html;
  blocks_to_markdown reconstruit un tableau pipe valide.
- Emojis 'carrés noirs' en PDF: xhtml2pdf n'embarque que des polices de base
  sans glyphes emoji. Moteur PDF -> WeasyPrint (tables CSS + emojis couleur via
  pango + fonts-noto-color-emoji installes dans l'image). Repli automatique sur
  xhtml2pdf quand weasyprint n'a pas ses libs natives (dev Windows).
- Dockerfile: libs weasyprint (pango/harfbuzz/gdk-pixbuf/shared-mime-info) +
  fonts-dejavu-core + fonts-noto-color-emoji. requirements: + weasyprint==69.0.
- Verifie en reel sur README.md: HTML = <table class=ftable> (thead/th, center);
  PDF 10 pages, texte de table present, Noto-Color-Emoji embarque + pixels
  colores confirmes. 199/199 tests (4 nouveaux).
2026-09-03 01:59:39 -04:00

94 lines
3.2 KiB
Python

"""FlowDeck — Export endpoints (v4.7.0).
Routes /api/export/* — generate Markdown, HTML, PDF and static-site (zip)
exports server-side for a given page.
"""
from __future__ import annotations
import logging
import re
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import Response
from app.db import get_conn
from app.services.export import (
page_to_markdown,
page_to_pdf_bytes,
page_to_standalone_html,
build_static_site_bytes,
)
logger = logging.getLogger(__name__)
router = APIRouter(tags=["export"], prefix="/api/export")
def _load_page_or_404(page_id: int) -> dict:
with get_conn() as conn:
row = conn.execute(
"SELECT * FROM pages WHERE id=? AND deleted_at IS NULL",
(page_id,),
).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Page not found")
return dict(row)
def _download_header(filename: str, media_type: str) -> dict:
ascii_name = re.sub(r"[^\x00-\x7F]", "_", filename)
quoted = filename.replace('"', '')
return {
"Content-Disposition": f'attachment; filename="{ascii_name}"; filename*=UTF-8\'\'{quoted}',
"Cache-Control": "no-store",
"Content-Type": media_type,
}
def _safe_filename(page: dict, ext: str) -> str:
title = (page.get("title") or "Untitled").strip() or "Untitled"
title = re.sub(r'[\\/:*?"<>|]+', "_", title)
return f"{title}.{ext}"
@router.get("/markdown/{page_id}")
async def export_markdown(page_id: int, request: Request):
page = _load_page_or_404(page_id)
md = page_to_markdown(page)
filename = _safe_filename(page, "md")
headers = _download_header(filename, "text/markdown")
return Response(content=md.encode("utf-8"), status_code=200, headers=headers)
@router.get("/html/{page_id}")
async def export_html(page_id: int, request: Request):
page = _load_page_or_404(page_id)
html = page_to_standalone_html(page)
filename = _safe_filename(page, "html")
headers = _download_header(filename, "text/html")
return Response(content=html.encode("utf-8"), status_code=200, headers=headers)
@router.get("/pdf/{page_id}")
async def export_pdf(page_id: int, request: Request):
page = _load_page_or_404(page_id)
try:
pdf_bytes = page_to_pdf_bytes(page)
except ImportError:
raise HTTPException(status_code=501, detail="PDF export requires 'weasyprint' or 'xhtml2pdf'")
except Exception as exc: # noqa: BLE001
logger.error("PDF export failed for page %s: %s", page_id, exc)
raise HTTPException(status_code=500, detail="PDF generation failed")
filename = _safe_filename(page, "pdf")
headers = _download_header(filename, "application/pdf")
return Response(content=pdf_bytes, status_code=200, headers=headers)
@router.get("/site/{page_id}")
async def export_site(page_id: int, request: Request):
page = _load_page_or_404(page_id)
site_bytes = build_static_site_bytes(page)
title = _safe_filename(page, "site").replace(".site", "") or "flowdeck-site"
filename = f"{title}_site.zip"
headers = _download_header(filename, "application/zip")
return Response(content=site_bytes, status_code=200, headers=headers)