fix: strip XML prolog from the text/html fallback body

This commit is contained in:
randogoth 2026-09-26 15:02:54 +03:00
parent 3d9239b7a5
commit 12956a5034
3 changed files with 40 additions and 2 deletions

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlsplit
from ..negotiate import FORMAT_WML, content_type_for, negotiate
from ..negotiate import FORMAT_HTML, FORMAT_WML, content_type_for, negotiate
from ..site import NotFound, RawFile, Redirect, Site, WmlCard
@ -55,7 +55,12 @@ class SmolwebHTTPHandler(BaseHTTPRequestHandler):
)
return
body = resource.wml_index if fmt == FORMAT_WML else resource.xhtml
if fmt == FORMAT_WML:
body = resource.wml_index
elif fmt == FORMAT_HTML:
body = resource.html
else:
body = resource.xhtml
self._send(
200,
content_type_for(fmt),

View file

@ -11,6 +11,7 @@ from __future__ import annotations
import mimetypes
import posixpath
import re
import threading
from dataclasses import dataclass, field
from pathlib import Path
@ -25,6 +26,15 @@ from wapdown.conversion.core import strip_frontmatter as wapdown_strip_frontmatt
from wapdown.parsers.markdown import CARD_BREAK_PATTERN
# XHTML-MP's leading `<?xml ...?>` processing instruction is valid XML but
# not valid HTML5: a browser parsing it as text/html (the fallback for a
# client that didn't ask for application/vnd.wap.xhtml+xml) tolerates it as
# a bogus comment and still renders the page, but logs a console warning.
# Stripped only for that fallback -- real XHTML-MP and WAP 2.0 clients get
# the untouched, fully valid XML document.
_XML_PROLOG_RE = re.compile(rb"^<\?xml[^>]*\?>\r?\n?")
class NotFound(Exception):
"""No resource exists at the requested path."""
@ -48,6 +58,7 @@ class Document:
gemtext: bytes
xhtml: bytes
html: bytes # xhtml with the XML prolog stripped, for the text/html fallback
wml_index: bytes # the deck served at the document's own URL
wml_cards: Dict[str, bytes] = field(default_factory=dict) # deck_per_card only
cache_control: Optional[int] = None
@ -235,6 +246,7 @@ class Site:
xhtml = wapdown_convert(
wap_body, config=config, renderer_name="xhtmlmp", base_path=base_path
).encode("utf-8")
html = _XML_PROLOG_RE.sub(b"", xhtml, count=1)
wml_output = wapdown_convert(
wap_body, config=config, renderer_name="wml", base_path=base_path
@ -244,6 +256,7 @@ class Site:
return Document(
gemtext=gemtext,
xhtml=xhtml,
html=html,
wml_index=wml_index,
wml_cards=wml_cards,
cache_control=config.cache_control,

View file

@ -115,6 +115,26 @@ class TestWmlCardUrls:
assert b"{.card" not in doc.xhtml
class TestHtmlFallbackHasNoXmlProlog:
def test_html_field_has_no_prolog(self, site: Site):
# A browser parsing this as text/html (rather than XML) tolerates
# a leading <?xml ...?> as a bogus comment and still renders the
# page, but logs a console warning -- so the text/html fallback
# must not carry it.
doc = site.resolve("/")
assert not doc.html.startswith(b"<?xml")
assert doc.html.startswith(b"<!DOCTYPE html")
def test_xhtml_field_keeps_the_prolog(self, site: Site):
# Real XHTML-MP/WAP 2.0 clients still need well-formed XML.
doc = site.resolve("/")
assert doc.xhtml.startswith(b'<?xml version="1.0"')
def test_html_and_xhtml_are_otherwise_identical(self, site: Site):
doc = site.resolve("/")
assert doc.html == doc.xhtml[doc.xhtml.index(b"<!DOCTYPE"):]
class TestLiveReload:
def test_edited_file_is_rerendered(self, tmp_path: Path):
target = tmp_path / "index.md"