diff --git a/doc/SPEC.md b/doc/SPEC.md
index e015e22..696e508 100644
--- a/doc/SPEC.md
+++ b/doc/SPEC.md
@@ -41,7 +41,7 @@ A Mews page MUST be a well-formed, valid XHTML-MP 1.2 document that also follows
### 3.1 Prologue and doctype
-A Mews page MUST begin with an XML declaration specifying UTF-8, followed by the XHTML-MP 1.2 doctype. Either quote character may be used in the declaration, as XML allows:
+A Mews page MUST begin with an XML declaration specifying UTF-8, followed by the XHTML-MP 1.2 doctype:
```xml
diff --git a/mews/lint.py b/mews/lint.py
index 5ed1b36..da2280f 100644
--- a/mews/lint.py
+++ b/mews/lint.py
@@ -46,12 +46,7 @@ SIZE_SHOULD = 64 * 1024
SIZE_TOTAL = 320 * 1024
IMAGE_SHOULD = 50 * 1024
-# Either quote character, because XML permits both and serialisers differ:
-# lxml writes single quotes. Section 3.1 asks for a declaration naming UTF-8,
-# and its example is one way of writing that rather than the only way.
-XML_DECLARATION = re.compile(
- rb"^<\?xml\s+version\s*=\s*([\"'])1\.0\1\s+encoding\s*=\s*([\"'])(?i:UTF-8)\2\s*\?>"
-)
+XML_DECLARATION = re.compile(rb'^<\?xml version="1\.0" encoding="(?i:UTF-8)"\?>')
DOCTYPE = re.compile(
rb''
diff --git a/pelican-mews/README.md b/pelican-mews/README.md
index 5c11a88..4a92070 100644
--- a/pelican-mews/README.md
+++ b/pelican-mews/README.md
@@ -10,10 +10,9 @@ well-formed UTF-8 XML with the XHTML namespace on ``. The plugin also
copies the Mews stylesheet into the output tree and gives themes a
`MEWS_CSS` global plus the `mews/head.html` partial.
-The subset comes from `pelican/plugins/mews/dtd/mews.dtd`, which is a copy of
-the Mews Profile 0.1 DTD. Only element and attribute names are read from it, so
-the plugin lists none of them in code: change the DTD and the allowlist changes
-with it.
+The bundled `pelican/plugins/mews/dtd/mews.dtd` is a minimal placeholder.
+Replace it with the real Mews Profile DTD when it is available; the plugin
+picks up the change without any code edit.
## Install
diff --git a/pelican-mews/pelican/plugins/mews/dtd/mews.dtd b/pelican-mews/pelican/plugins/mews/dtd/mews.dtd
index e09c5da..1eb98d3 100644
--- a/pelican-mews/pelican/plugins/mews/dtd/mews.dtd
+++ b/pelican-mews/pelican/plugins/mews/dtd/mews.dtd
@@ -1,396 +1,87 @@
-
- XHTML Mobile Profile 1.2 restricted as in the Mews Profile
- Specification 0.1 section 4, extended with lang and dir (section 3).
- Maintained by hand and checked against the XHTML Mobile 1.2 driver,
- xhtml-mobile12.dtd (OMA-SUP-DTD_xhtml_mobile12-V1_2-20080331-A), by
- check_dtd.py. It cannot be generated by stripping the driver: the
- driver declares no elements itself, attribute lists only ever add,
- and the rules below do not exist in XHTML-MP to be kept.
+
- This file is standalone: it declares every element and attribute
- itself, so validation never depends on the Open Mobile Alliance's or
- W3C's servers. Pages keep the XHTML-MP doctype (section 3.1); only
- validators use this file (section 9.1).
-
- Encoded here: the permitted elements and attributes (4.1), the head
- rules (3.3), the body variant classes (5.3) and the ban on nested
- tables (4.2). 51 elements are kept from XHTML Mobile 1.2. Dropped:
- acronym, base, big, button, div, legend, noscript, object, param,
- script, small, span, style, sub, sup, tt. Event attributes and the
- style attribute are omitted entirely.
--->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/pelican-mews/tests/conftest.py b/pelican-mews/tests/conftest.py
index 1658f1e..fe7d5ba 100644
--- a/pelican-mews/tests/conftest.py
+++ b/pelican-mews/tests/conftest.py
@@ -21,15 +21,7 @@ THEME = TESTS / "theme"
BASE_SETTINGS = {
"ARTICLE_PATHS": [],
- # Tables and definition lists are both in the Mews subset, and both need
- # an extension turned on before python-markdown will emit them.
- "MARKDOWN": {
- "output_format": "xhtml",
- "extension_configs": {
- "markdown.extensions.tables": {},
- "markdown.extensions.def_list": {},
- },
- },
+ "MARKDOWN": {"output_format": "xhtml"},
"PAGE_PATHS": ["pages"],
"PAGE_SAVE_AS": "{slug}.xhtml",
"PAGE_URL": "{slug}.xhtml",
diff --git a/pelican-mews/tests/fixtures/structure/expected.xhtml b/pelican-mews/tests/fixtures/structure/expected.xhtml
deleted file mode 100644
index eb09b65..0000000
--- a/pelican-mews/tests/fixtures/structure/expected.xhtml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-Structure
-
-
-
-
-
A table, an ordered list and a definition list: everything the placeholder
-DTD used to throw away.
-
-
-
-
Network
-
Band
-
-
-
-
-
GSM 900
-
900 MHz
-
-
-
-
-
First
-
Second
-
-
-
Term
-
Its meaning
-
-
-
\ No newline at end of file
diff --git a/pelican-mews/tests/fixtures/structure/input.md b/pelican-mews/tests/fixtures/structure/input.md
deleted file mode 100644
index ffb05b6..0000000
--- a/pelican-mews/tests/fixtures/structure/input.md
+++ /dev/null
@@ -1,16 +0,0 @@
-Title: Structure
-Slug: page
-Date: 2026-10-11
-
-A table, an ordered list and a definition list: everything the placeholder
-DTD used to throw away.
-
-| Network | Band |
-| --- | --- |
-| GSM 900 | 900 MHz |
-
-1. First
-2. Second
-
-Term
-: Its meaning
diff --git a/pelican-mews/tests/test_mews.py b/pelican-mews/tests/test_mews.py
index 7267aa0..f14b12d 100644
--- a/pelican-mews/tests/test_mews.py
+++ b/pelican-mews/tests/test_mews.py
@@ -18,7 +18,7 @@ PACKAGE = "pelican.plugins.mews"
STRICT_OVERRIDES = {"MEWS_STRICT": True}
-@pytest.mark.parametrize("case", ["basic", "strip", "cssdir", "structure"])
+@pytest.mark.parametrize("case", ["basic", "strip", "cssdir"])
def test_written_page_matches_expected(tmp_path, case):
"""The written page matches the fixture, whatever was stripped."""
overrides = {"MEWS_CSS_DIR": "style"} if case == "cssdir" else {}
@@ -28,7 +28,7 @@ def test_written_page_matches_expected(tmp_path, case):
assert normalize(page) == normalize(expected)
-@pytest.mark.parametrize("case", ["basic", "strip", "cssdir", "structure"])
+@pytest.mark.parametrize("case", ["basic", "strip", "cssdir"])
def test_css_is_copied(tmp_path, case):
"""The bundled stylesheet lands under the configured directory."""
overrides = {"MEWS_CSS_DIR": "style"} if case == "cssdir" else {}
@@ -81,28 +81,13 @@ def test_parallel_output_folder(tmp_path):
assert not (output / "css").exists()
-# Section 4.1 of the specification keeps this many elements. The plugin lists
-# none of them itself; the DTD is the only place they are written down.
-SECTION_4_1_ELEMENTS = 51
-
-
def test_allowlist_is_read_from_the_dtd():
"""The subset comes from the DTD, not from code."""
dtd = files(PACKAGE).joinpath("dtd", "mews.dtd").read_text(encoding="utf-8")
allowlist = parse_allowlist(dtd)
- assert len(allowlist) == SECTION_4_1_ELEMENTS
- # The common attributes, including the two the profile adds to XHTML-MP.
- assert allowlist["p"] == {"dir", "id", "lang", "title", "xml:lang"}
- # html takes no id or title; section 4.1 gives it these four and no more.
- assert allowlist["html"] == {"dir", "lang", "xml:lang", "xmlns"}
+ assert allowlist["p"] == {"id", "title", "xml:lang"}
+ assert allowlist["html"] == {"dir", "id", "lang", "title", "xml:lang", "xmlns"}
assert "href" in allowlist["a"]
- # class is permitted on body and nowhere else (section 5.3).
- assert "class" in allowlist["body"]
- assert not [
- name for name, attrs in allowlist.items() if name != "body" and "class" in attrs
- ]
- # The parts the placeholder DTD lacked, which the plugin was stripping.
- assert {"table", "td", "ol", "dl", "form", "input", "address"} <= set(allowlist)
@pytest.mark.skipif(shutil.which("uv") is None, reason="uv is not installed")
diff --git a/tests/test_data.py b/tests/test_data.py
index 2ea1c32..956dad8 100644
--- a/tests/test_data.py
+++ b/tests/test_data.py
@@ -14,12 +14,6 @@ COPIES = {
"mews-0.1.dtd": ROOT / "dtd" / "mews-0.1.dtd",
}
-# The Pelican plugin is a separate distributable package, so it carries its own
-# copy of the DTD rather than importing one. It shipped a hand-written
-# placeholder for a while, which silently stripped tables, ordered lists, forms
-# and the body variant class out of every page it produced.
-PLUGIN_DTD = ROOT / "pelican-mews" / "pelican" / "plugins" / "mews" / "dtd" / "mews.dtd"
-
def test_packaged_copies_match_the_originals():
for name, original in COPIES.items():
@@ -34,10 +28,3 @@ def test_the_spec_quotes_the_same_stylesheet():
spec = (ROOT / "doc" / "SPEC.md").read_text()
quoted = spec.split("```css\n", 1)[1].split("```", 1)[0]
assert quoted.strip() == (ROOT / "mews-0.1.css").read_text().strip()
-
-
-def test_the_pelican_plugin_bundles_the_real_dtd():
- canonical = (ROOT / "dtd" / "mews-0.1.dtd").read_bytes()
- assert PLUGIN_DTD.read_bytes() == canonical, (
- f"{PLUGIN_DTD} differs from dtd/mews-0.1.dtd. Copy the original over it."
- )
diff --git a/tests/test_lint_rules.py b/tests/test_lint_rules.py
index 9b5b1e2..bb9e29f 100644
--- a/tests/test_lint_rules.py
+++ b/tests/test_lint_rules.py
@@ -34,17 +34,6 @@ def test_rule_failure_maps_to_its_section(fail_page, name, expected):
assert (match[0].section, match[0].level) == (section, level)
-def test_either_quote_character_opens_the_declaration(conforming):
- """Serialisers differ: lxml writes single quotes, and XML allows them."""
- page = conforming().replace(
- b'',
- b"",
- )
- report = validate_bytes(page)
- assert "xml-declaration" not in [f.code for f in report.findings]
- assert report.conforms
-
-
def test_entity_declarations_are_refused_before_parsing(fail_page):
"""A page declaring its own entities is rejected without expanding them."""
report = validate_bytes(fail_page("billion-laughs"))
diff --git a/tests/test_site.py b/tests/test_site.py
index 560b4a7..39445a7 100644
--- a/tests/test_site.py
+++ b/tests/test_site.py
@@ -24,18 +24,6 @@ def test_the_projects_own_pages_have_no_warnings_either(page):
assert not report.warnings, "\n".join(str(f) for f in report.warnings)
-PLUGIN_PAGES = sorted(
- (ROOT / "pelican-mews" / "tests" / "fixtures").glob("*/expected.xhtml")
-)
-
-
-@pytest.mark.parametrize("page", PLUGIN_PAGES, ids=lambda p: p.parent.name)
-def test_pages_the_pelican_plugin_produces_conform(page):
- """What the plugin writes has to pass the validator, or it is no use."""
- report = validate_file(str(page))
- assert report.conforms, "\n".join(str(f) for f in report.failures)
-
-
def test_the_dtd_still_matches_the_spec():
"""dtd/check_dtd.py is hand-maintained; keep it running in CI."""
result = subprocess.run(