aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/scadere/__init__.py13
-rw-r--r--src/scadere/atom2xhtml.xslt49
-rw-r--r--src/scadere/listen.py40
3 files changed, 86 insertions, 16 deletions
diff --git a/src/scadere/__init__.py b/src/scadere/__init__.py
index 823e1a9..157166e 100644
--- a/src/scadere/__init__.py
+++ b/src/scadere/__init__.py
@@ -4,11 +4,12 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from argparse import HelpFormatter, ONE_OR_MORE
+from functools import cache
+from importlib.resources import files
__all__ = ['__version__', 'GNUHelpFormatter', 'NetLoc',
- 'format_epilog', 'format_version']
-__version__ = '0.1.3'
-
+ 'atom2xhtml', 'format_epilog', 'format_version']
+__version__ = '0.2.0'
EXAMPLE_PREFIX = ' ' * 2
# help2man's implementation detail
@@ -80,6 +81,12 @@ class NetLoc:
return hostname, int(port) # ValueError to be handled by argparse
+@cache
+def atom2xhtml():
+ """Load stylesheet from package resources exactly once."""
+ return files(__name__).joinpath('atom2xhtml.xslt').read_bytes()
+
+
def format_epilog(examples):
"""Format example commands and their description ."""
lines = ['Examples:']
diff --git a/src/scadere/atom2xhtml.xslt b/src/scadere/atom2xhtml.xslt
new file mode 100644
index 0000000..5bcc864
--- /dev/null
+++ b/src/scadere/atom2xhtml.xslt
@@ -0,0 +1,49 @@
+<?xml version='1.0' encoding='utf-8'?>
+<!--
+ -- Atom-to-XHTML transformation
+ --
+ -- SPDX-FileCopyrightText: 2025 Nguyễn Gia Phong
+ -- SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+<xsl:stylesheet version='1.0'
+ xmlns:atom='http://www.w3.org/2005/Atom'
+ xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
+ <xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/>
+
+ <xsl:template match="atom:generator">
+ <p>
+ <xsl:text>Generated by </xsl:text>
+ <a href='{@uri}'>
+ <xsl:value-of select='.'/>
+ </a>
+ <xsl:text> </xsl:text>
+ <xsl:value-of select='@version'/>
+ </p>
+ </xsl:template>
+
+ <xsl:template match="/atom:feed">
+ <xsl:variable name='home' select='link'/>
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset='utf-8'/>
+ <meta name='color-scheme' content='dark light'/>
+ <meta name='viewport' content='width=device-width, initial-scale=1'/>
+ <link rel='icon' href='data:,'/>
+ <title><xsl:value-of select='atom:title'/></title>
+ </head>
+ <body>
+ <h1><xsl:value-of select='atom:title'/></h1>
+ <ul>
+ <xsl:for-each select='atom:entry'>
+ <li>
+ <a href='{atom:link[@type="application/xhtml+xml"]/@href}'>
+ <xsl:value-of select='atom:title'/>
+ </a>
+ </li>
+ </xsl:for-each>
+ </ul>
+ <xsl:apply-templates select='atom:generator'/>
+ </body>
+ </html>
+ </xsl:template>
+</xsl:stylesheet>
diff --git a/src/scadere/listen.py b/src/scadere/listen.py
index 151a108..dcf4754 100644
--- a/src/scadere/listen.py
+++ b/src/scadere/listen.py
@@ -22,7 +22,7 @@ from xml.etree.ElementTree import (Element as xml_element,
indent, tostring as string_from_xml)
from . import (__version__, GNUHelpFormatter, NetLoc,
- format_epilog, format_version)
+ atom2xhtml, format_epilog, format_version)
from .check import base64_from_str
__all__ = ['main']
@@ -145,7 +145,7 @@ def is_subdomain(subject, objects):
for obj_parts in map(split_domain, objects))
-def feed(mtime, base_url, name, certificates, domains):
+def feed(base_url, name, mtime, certificates, domains):
"""Construct an Atom feed based on the given information."""
return ('feed', {'xmlns': 'http://www.w3.org/2005/Atom'},
('id', base_url),
@@ -202,17 +202,24 @@ def xml(tree, parent=None):
@lru_cache
-def unparsed_feed(*args):
+def atom2xhtml_url(base_url):
+ """Return the URL to the immutable stylesheet."""
+ return urljoin(base_url, f'{urlsplit(base_url).path}{__version__}.xslt')
+
+
+@lru_cache
+def unparsed_feed(base_url, *args):
"""Cache Atom feed."""
- return string_from_xml(xml(feed(*args)), 'unicode',
- xml_declaration=True, default_namespace=None)
+ return (b'<?xml version="1.0" encoding="utf-8"?>\n'
+ b'<?xml-stylesheet type="text/xsl"'
+ + f' href="{atom2xhtml_url(base_url)}"?>\n'.encode()
+ + string_from_xml(xml(feed(base_url, *args)), 'utf-8'))
@lru_cache
def unparsed_page(*args):
"""Cache XHTML page."""
- return string_from_xml(xml(page(*args)), 'unicode',
- xml_declaration=True, default_namespace=None)
+ return string_from_xml(xml(page(*args)), 'utf-8', xml_declaration=True)
@lru_cache
@@ -221,10 +228,10 @@ def set_http_time_locale():
setlocale(LC_TIME, 'C')
-def write_xml(writer, http_version, application, func, mtime, *args):
+def write_xml(writer, http_version, application, func, *args, mtime=None):
"""Write given document as XML."""
try:
- content = func(mtime, *args).encode()
+ content = func(*args)
except Exception: # pragma: no cover
describe_status(writer, HTTPStatus.INTERNAL_SERVER_ERROR, http_version)
raise
@@ -232,9 +239,13 @@ def write_xml(writer, http_version, application, func, mtime, *args):
write_status(writer, http_version, HTTPStatus.OK)
write_content_type(writer, f'application/{application}+xml')
writer.write(f'Content-Length: {len(content)}\r\n'.encode())
- set_http_time_locale()
- http_time = mtime.strftime('%a, %d %b %Y %H:%M:%S GMT')
- writer.write(f'Last-Modified: {http_time}\r\n\r\n'.encode())
+ if mtime is None:
+ writer.write(b'Cache-Control: public,'
+ b' max-age: 31536000, immutable\r\n\r\n')
+ else:
+ set_http_time_locale()
+ http_time = mtime.strftime('%a, %d %b %Y %H:%M:%S GMT')
+ writer.write(f'Last-Modified: {http_time}\r\n\r\n'.encode())
writer.write(content)
@@ -278,7 +289,10 @@ async def handle(certs, base_url, reader, writer, title=''):
if url_parts.path == urlsplit(base_url).path: # Atom feed
write_xml(writer, http_version, 'atom', unparsed_feed,
- mtime, base_url, title or certs.name, summaries, domains)
+ base_url, title or certs.name, mtime, summaries, domains,
+ mtime=mtime)
+ elif url_parts.path == urlsplit(atom2xhtml_url(base_url)).path:
+ write_xml(writer, http_version, 'xslt', atom2xhtml)
elif url_parts.path in lookup: # accessible Atom entry's link/ID
write_xml(writer, http_version, 'xhtml', unparsed_page,
*lookup.get(url_parts.path))