mews.page/mews/pages.py

228 lines
8.2 KiB
Python

"""Emitting the pages mews.page serves.
Every page the service produces is itself a Mews page, so this module writes the
same prologue, head and body shape as md2mews.py. The emitter is repeated here
rather than imported because md2mews.py is a standalone script that pulls in a
Markdown parser; tests/test_pages.py fails if the two drift apart.
"""
from datetime import UTC, datetime
import fcntl
from html import escape
import os
from pathlib import Path
import sqlite3
from mews import VERSION
from mews.lint import validate_bytes
DOCTYPE = (
'<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN"\n'
' "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
)
VARIANTS = ("mews-warm", "mews-cool", "mews-green", "mews-mono")
STYLESHEET = "mews-0.1.css"
SITE_VARIANT = "mews-cool"
# SPEC.md 4.3 caps a page at 256 KB and asks for 64 KB. The directory refuses to
# publish a page over the first and warns past the second.
SIZE_MUST = 256 * 1024
SIZE_SHOULD = 64 * 1024
def _text(value: str) -> str:
"""Escape text content, leaving apostrophes alone so prose reads as prose."""
return escape(value, quote=False)
def _attr(value: str) -> str:
"""Escape a value going into an attribute."""
return escape(value, quote=True)
def page(
title: str,
body: list[str],
language: str = "en",
direction: str | None = None,
variant: str | None = None,
description: str | None = None,
) -> str:
"""Wrap body lines in a complete Mews page. Mirrors md2mews.page()."""
attrs = f'xml:lang="{language}" lang="{language}"'
if direction:
attrs += f' dir="{direction}"'
head = [
f" <title>{_text(title)}</title>",
f' <meta name="mews-profile" content="{VERSION}" />',
' <meta name="viewport" content="width=device-width" />',
]
if description:
head.append(f' <meta name="description" content="{_attr(description)}" />')
head.append(f' <link rel="stylesheet" type="text/css" href="{STYLESHEET}" />')
body_attrs = f' class="{variant}"' if variant else ""
return "\n".join(
[
'<?xml version="1.0" encoding="UTF-8"?>',
DOCTYPE,
f'<html xmlns="http://www.w3.org/1999/xhtml" {attrs}>',
" <head>",
*head,
" </head>",
f" <body{body_attrs}>",
*body,
" </body>",
"</html>",
"",
]
)
def site_page(title: str, body: list[str], description: str | None = None) -> str:
"""Wrap body lines in a page with the mews.page look."""
return page(title, body, variant=SITE_VARIANT, description=description)
def message(title: str, paragraphs: list[str], links: list[tuple[str, str]]) -> str:
"""Build a short page that says one thing, with a way onwards."""
body = [f" <h1>{_text(title)}</h1>"]
body += [f" <p>{_text(line)}</p>" for line in paragraphs]
body.append(" <hr />")
body += [
f' <p><a href="{_attr(href)}">{_text(label)}</a></p>'
for href, label in links
]
return site_page(title, body, description=paragraphs[0] if paragraphs else None)
def report(findings_report, *, listed: bool, command: str) -> str:
"""Build the page an author gets back after a check."""
conforms = findings_report.conforms
if listed:
title = "Your site is listed"
opening = (
"That page follows Mews Profile 0.1, so your site is in the "
"directory now. Thank you for adding it."
)
elif conforms:
title = "This page conforms"
opening = (
"That page follows Mews Profile 0.1. Nothing was stored, and the "
"site is not in the directory: use Check and list for that."
)
else:
title = "This page doesn't conform yet"
opening = (
"Here is what to fix. Each line names the section of the spec that "
"explains the rule, so you can go and read why."
)
body = [f" <h1>{_text(title)}</h1>", f" <p>{_text(opening)}</p>"]
if findings_report.url:
body.append(
f' <p>Checked <a href="{_attr(findings_report.url)}">'
f"{_text(findings_report.url)}</a>.</p>"
)
if findings_report.failures:
body.append(" <h2>To fix</h2>")
body.append(" <ul>")
body += [f" <li>{_text(str(f))}</li>" for f in findings_report.failures]
body.append(" </ul>")
if findings_report.warnings:
body.append(" <h2>Worth fixing</h2>")
body.append(" <ul>")
body += [f" <li>{_text(str(f))}</li>" for f in findings_report.warnings]
body.append(" </ul>")
for note in findings_report.notes:
body.append(f" <p>{_text(note)}</p>")
body.append(" <hr />")
body.append(
' <p>The rules are all in the <a href="/spec/0.1">spec</a>. Checking '
"on your own machine is quicker while you are still fixing things:</p>"
)
body.append(f" <pre>{_text(command)}</pre>")
body.append(
' <p><a href="/check" accesskey="1">Check another page</a> '
'· <a href="/directory" accesskey="2">The directory</a></p>'
)
return site_page(title, body)
def directory(rows: list[sqlite3.Row]) -> str:
"""Build the directory page: every site whose page passes the checks."""
body = [" <h1>Mews directory</h1>"]
if rows:
count = f"{len(rows)} site" + ("" if len(rows) == 1 else "s")
body.append(
f" <p>{count} listed. Every one of these pages passed the checks "
'against <a href="/spec/0.1">Mews Profile 0.1</a>, and gets looked '
"at again every week.</p>"
)
body.append(" <ul>")
for row in rows:
label = _text(row["title"] or row["domain"])
line = f' <li><a href="{_attr(row["url"])}">{label}</a>'
line += f" — {_text(row['domain'])}"
if row["description"]:
line += f". {_text(row['description'])}"
body.append(line + "</li>")
body.append(" </ul>")
else:
body.append(" <p>Nothing here yet. Yours could be the first one in!</p>")
body.append(" <hr />")
body.append(
' <p><a href="/check" accesskey="1">Add your site</a> '
'· <a href="/" accesskey="0">Mews</a></p>'
)
body.append(
f" <address>Updated {datetime.now(UTC).strftime('%Y-%m-%d %H:%M')} "
"UTC</address>"
)
return site_page(
"Mews directory",
body,
description="Sites whose pages follow Mews Profile 0.1.",
)
def write_directory(target: str, rows: list[sqlite3.Row]) -> list[str]:
"""Write the directory page, replacing it only if it conforms.
The page is validated before it is published, because the one page that has
to be a good example is this one. Returns any warnings about it.
"""
destination = Path(target)
destination.parent.mkdir(parents=True, exist_ok=True)
text = directory(rows).encode()
if len(text) > SIZE_MUST:
raise ValueError(
"The directory page would be over 256 KB, which section 4.3 "
"forbids. Split it before listing more sites."
)
lock = destination.with_name(destination.name + ".lock")
with open(lock, "w") as handle:
fcntl.flock(handle, fcntl.LOCK_EX)
result = validate_bytes(text)
if not result.conforms:
raise ValueError(
"The directory page doesn't conform: "
+ "; ".join(str(f) for f in result.failures)
)
temporary = destination.with_name("." + destination.name + ".tmp")
temporary.write_bytes(text)
# The web server reads this file straight off disk as another user, so
# it is made readable on purpose. The service runs with a private umask
# to keep the database to itself, and that would otherwise apply here.
temporary.chmod(0o644)
os.replace(temporary, destination)
warnings = [str(f) for f in result.warnings]
if len(text) > SIZE_SHOULD:
warnings.append(
f"The directory page is {len(text) // 1024} KB, over the 64 KB "
"section 4.3 asks for."
)
return warnings