Andrew Ridgway bec1eaac87
Some checks failed
Test / test (push) Has been cancelled
first pass at the newspaper builder
2026-09-14 11:57:22 +10:00

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")