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.

- - - - - - - - - - - - - -
NetworkBand
GSM 900900 MHz
-
    -
  1. First
  2. -
  3. Second
  4. -
-
-
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(