87 lines
3.6 KiB
Python
87 lines
3.6 KiB
Python
"""generator HTML emitter.
|
|
|
|
Per BR5.1/BR5.3 (US5/US6): writes a self-contained zero-network newspaper.html,
|
|
file:// printable to A4 with margins, clean print. BR5.2: friendly guided
|
|
empty-state when no content.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
from pathlib import Path
|
|
|
|
from .model import Article, Issue, MastheadConfig, Page
|
|
|
|
|
|
def _render_masthead(m: MastheadConfig) -> str:
|
|
return (
|
|
"<header class=\"masthead\">"
|
|
f"<h1>{html.escape(m.title)}</h1>"
|
|
f"<p class=\"meta\">{html.escape(m.date_line)} · Issue {html.escape(m.issue_number)} · Volume {html.escape(m.volume)}</p>"
|
|
"</header>"
|
|
)
|
|
|
|
|
|
def _render_article(a: Article) -> str:
|
|
by = f"<span class=\"byline\">{html.escape(a.byline)}</span>" if a.byline else ""
|
|
return (
|
|
f"<article class=\"article\"><h2>{html.escape(a.headline)}</h2>{by}"
|
|
f"<p>{html.escape(a.body)}</p></article>"
|
|
)
|
|
|
|
|
|
def _render_page(page: Page, articles_by_id: dict[str, Article]) -> str:
|
|
cols = []
|
|
for col in page.columns:
|
|
cells = []
|
|
for item in col.items:
|
|
art = articles_by_id.get(item.source_ref)
|
|
if art:
|
|
cells.append(_render_article(art))
|
|
else:
|
|
cells.append(f"<p class=\"empty-box\">({html.escape(item.kind)})</p>")
|
|
cols.append(f"<div class=\"col\">{''.join(cells)}</div>")
|
|
return f"<section class=\"page\"><header class=\"running\">{html.escape(page.running_header)}</header>{''.join(cols)}</section>"
|
|
|
|
|
|
def emit_newspaper(issue: Issue, out_path: Path) -> None:
|
|
"""Write the self-contained, zero-network newspaper.html (BR5.1)."""
|
|
articles_by_id = {a.article_id: a for a in issue.articles}
|
|
pages = "".join(_render_page(p, articles_by_id) for p in issue.pages)
|
|
css = (
|
|
"@page { size: A4; margin: 15mm; }"
|
|
"body { font-family: serif; color: #111827; background: #fbf8f1; margin:0; }"
|
|
".masthead { border-bottom: 2px solid #000; margin-bottom: 12px; }"
|
|
".masthead h1 { margin: 0; font-size: 2.2em; }"
|
|
".meta { margin: 4px 0 8px; }"
|
|
".page { page-break-after: always; }"
|
|
".col { width: 32%; display: inline-block; vertical-align: top; padding-right: 8px; }"
|
|
".running { font-size: .8em; border-bottom: 1px solid #999; margin-bottom: 8px; }"
|
|
"@media print { .page { break-after: page; } }"
|
|
)
|
|
doc = (
|
|
"<!doctype html><html><head><meta charset=\"utf-8\">"
|
|
"<title>Wedding Newspaper</title><style>" + css + "</style></head><body>"
|
|
+ _render_masthead(issue.masthead) + pages + "</body></html>"
|
|
)
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
out_path.write_text(doc, encoding="utf-8")
|
|
|
|
|
|
def emit_empty_state(out_path: Path, masthead: MastheadConfig | None = None) -> None:
|
|
"""BR5.2/US10: friendly guided empty-state page with a sample-issue pointer."""
|
|
title = masthead.title if masthead else "The Wedding Times"
|
|
doc = (
|
|
"<!doctype html><html><head><meta charset=\"utf-8\">"
|
|
"<title>Welcome — Wedding Newspaper</title><style>"
|
|
"body{font-family:serif;color:#111827;background:#fbf8f1;max-width:600px;margin:60px auto;padding:20px}"
|
|
"</style></head><body>"
|
|
"<h1>Welcome to " + html.escape(title) + "</h1>"
|
|
"<p>Add a <code>content/</code> folder of markdown or txt files and a "
|
|
"<code>config.json</code>, then run <code>newspaper generate</code> to "
|
|
"produce your paper. Try the sample issue first to see the layout.</p>"
|
|
"</body></html>"
|
|
)
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
out_path.write_text(doc, encoding="utf-8")
|