feat: pelican plugin
This commit is contained in:
parent
970e0da9bc
commit
370fdfb067
33 changed files with 2274 additions and 0 deletions
Binary file not shown.
BIN
pelican-mews/tests/__pycache__/conftest.cpython-314.pyc
Normal file
BIN
pelican-mews/tests/__pycache__/conftest.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
55
pelican-mews/tests/conftest.py
Normal file
55
pelican-mews/tests/conftest.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Shared helpers: build a tiny Pelican site for one fixture case.
|
||||
|
||||
Each case is a directory under tests/fixtures holding the Markdown input
|
||||
and the expected page. The test theme renders a Mews page skeleton that
|
||||
includes the plugin's mews/head.html partial.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from pelican import Pelican
|
||||
from pelican.settings import read_settings
|
||||
|
||||
TESTS = Path(__file__).parent
|
||||
FIXTURES = TESTS / "fixtures"
|
||||
THEME = TESTS / "theme"
|
||||
|
||||
BASE_SETTINGS = {
|
||||
"ARTICLE_PATHS": [],
|
||||
"MARKDOWN": {"output_format": "xhtml"},
|
||||
"PAGE_PATHS": ["pages"],
|
||||
"PAGE_SAVE_AS": "{slug}.xhtml",
|
||||
"PAGE_URL": "{slug}.xhtml",
|
||||
"PLUGINS": ["pelican.plugins.mews"],
|
||||
"SITEURL": "",
|
||||
"STATIC_PATHS": [],
|
||||
"TIMEZONE": "UTC",
|
||||
}
|
||||
|
||||
|
||||
def build_case(case: str, root: Path, **override) -> Path:
|
||||
"""Build the fixture site for one case; return its output directory."""
|
||||
pages = root / "content" / "pages"
|
||||
pages.mkdir(parents=True)
|
||||
shutil.copy(FIXTURES / case / "input.md", pages / "input.md")
|
||||
settings = read_settings(
|
||||
override={
|
||||
**BASE_SETTINGS,
|
||||
"PATH": str(root / "content"),
|
||||
"OUTPUT_PATH": str(root / "output"),
|
||||
"THEME": str(THEME),
|
||||
**override,
|
||||
}
|
||||
)
|
||||
Pelican(settings).run()
|
||||
return root / "output"
|
||||
|
||||
|
||||
def normalize(data: bytes) -> str:
|
||||
"""Canonical form of a Mews page, indifferent to whitespace."""
|
||||
return etree.canonicalize(xml_data=data.decode("utf-8"), strip_text=True)
|
||||
16
pelican-mews/tests/fixtures/basic/expected.xhtml
vendored
Normal file
16
pelican-mews/tests/fixtures/basic/expected.xhtml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title>Basic</title>
|
||||
<meta name="mews-profile" content="0.1"/>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<link rel="stylesheet" type="text/css" href="css/mews-0.1.css"/></head>
|
||||
<body>
|
||||
<p>A <em>simple</em> page with a <a href="a.html">link</a> and <code>code</code>.</p>
|
||||
<ul>
|
||||
<li>One</li>
|
||||
<li>Two</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
8
pelican-mews/tests/fixtures/basic/input.md
vendored
Normal file
8
pelican-mews/tests/fixtures/basic/input.md
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Title: Basic
|
||||
Slug: page
|
||||
Date: 2026-10-11
|
||||
|
||||
A *simple* page with a [link](a.html) and `code`.
|
||||
|
||||
- One
|
||||
- Two
|
||||
16
pelican-mews/tests/fixtures/cssdir/expected.xhtml
vendored
Normal file
16
pelican-mews/tests/fixtures/cssdir/expected.xhtml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title>Basic</title>
|
||||
<meta name="mews-profile" content="0.1"/>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<link rel="stylesheet" type="text/css" href="style/mews-0.1.css"/></head>
|
||||
<body>
|
||||
<p>A <em>simple</em> page with a <a href="a.html">link</a> and <code>code</code>.</p>
|
||||
<ul>
|
||||
<li>One</li>
|
||||
<li>Two</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
8
pelican-mews/tests/fixtures/cssdir/input.md
vendored
Normal file
8
pelican-mews/tests/fixtures/cssdir/input.md
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Title: Basic
|
||||
Slug: page
|
||||
Date: 2026-10-11
|
||||
|
||||
A *simple* page with a [link](a.html) and `code`.
|
||||
|
||||
- One
|
||||
- Two
|
||||
9
pelican-mews/tests/fixtures/strict/input.md
vendored
Normal file
9
pelican-mews/tests/fixtures/strict/input.md
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
Title: Strip
|
||||
Slug: page
|
||||
Date: 2026-10-11
|
||||
|
||||
A paragraph with <span class="x">an unknown element</span> and <em class="big">kept emphasis</em>.
|
||||
|
||||
<!-- a stray comment -->
|
||||
|
||||
Second paragraph.
|
||||
15
pelican-mews/tests/fixtures/strip/expected.xhtml
vendored
Normal file
15
pelican-mews/tests/fixtures/strip/expected.xhtml
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title>Strip</title>
|
||||
<meta name="mews-profile" content="0.1"/>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<link rel="stylesheet" type="text/css" href="css/mews-0.1.css"/></head>
|
||||
<body>
|
||||
<p>A paragraph with an unknown element and <em>kept emphasis</em>.</p>
|
||||
|
||||
|
||||
<p>Second paragraph.</p>
|
||||
</body>
|
||||
</html>
|
||||
9
pelican-mews/tests/fixtures/strip/input.md
vendored
Normal file
9
pelican-mews/tests/fixtures/strip/input.md
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
Title: Strip
|
||||
Slug: page
|
||||
Date: 2026-10-11
|
||||
|
||||
A paragraph with <span class="x">an unknown element</span> and <em class="big">kept emphasis</em>.
|
||||
|
||||
<!-- a stray comment -->
|
||||
|
||||
Second paragraph.
|
||||
115
pelican-mews/tests/test_mews.py
Normal file
115
pelican-mews/tests/test_mews.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"""End-to-end tests: Pelican builds small fixture sites with the plugin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
from conftest import FIXTURES, build_case, normalize
|
||||
import pytest
|
||||
|
||||
from pelican.plugins.mews.mews import CSS_NAME
|
||||
from pelican.plugins.mews.subset import MewsSubsetError, parse_allowlist
|
||||
|
||||
PACKAGE = "pelican.plugins.mews"
|
||||
STRICT_OVERRIDES = {"MEWS_STRICT": True}
|
||||
|
||||
|
||||
@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 {}
|
||||
output = build_case(case, tmp_path, **overrides)
|
||||
page = (output / "page.xhtml").read_bytes()
|
||||
expected = (FIXTURES / case / "expected.xhtml").read_bytes()
|
||||
assert normalize(page) == normalize(expected)
|
||||
|
||||
|
||||
@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 {}
|
||||
output = build_case(case, tmp_path, **overrides)
|
||||
css_dir = "style" if case == "cssdir" else "css"
|
||||
bundled = files(PACKAGE).joinpath("static", CSS_NAME).read_bytes()
|
||||
assert (output / css_dir / CSS_NAME).read_bytes() == bundled
|
||||
|
||||
|
||||
def test_template_global_points_at_the_css(tmp_path):
|
||||
"""Themes see MEWS_CSS as the relative stylesheet path."""
|
||||
output = build_case("basic", tmp_path)
|
||||
page = (output / "page.xhtml").read_text(encoding="utf-8")
|
||||
assert 'href="css/mews-0.1.css"' in page
|
||||
|
||||
|
||||
def test_lenient_mode_logs_removals(tmp_path, caplog):
|
||||
"""Default mode strips and logs what it removed."""
|
||||
caplog.set_level("WARNING", logger=f"{PACKAGE}.mews")
|
||||
build_case("strip", tmp_path)
|
||||
assert "<span> is not in the Mews subset" in caplog.text
|
||||
assert "em class is not in the Mews subset" in caplog.text
|
||||
|
||||
|
||||
def test_strict_mode_fails_the_build(tmp_path):
|
||||
"""MEWS_STRICT turns disallowed content into a build failure."""
|
||||
with pytest.raises(MewsSubsetError, match="span"):
|
||||
build_case("strict", tmp_path, **STRICT_OVERRIDES)
|
||||
|
||||
|
||||
def test_parallel_output_folder(tmp_path):
|
||||
"""MEWS_OUTPUT_PATH keeps the normal output free of Mews files.
|
||||
|
||||
The processed page and the stylesheet move to the parallel folder,
|
||||
keeping the subpath; the directory the move emptied is pruned.
|
||||
"""
|
||||
parallel = tmp_path / "mews"
|
||||
output = build_case(
|
||||
"basic",
|
||||
tmp_path,
|
||||
MEWS_OUTPUT_PATH=str(parallel),
|
||||
PAGE_SAVE_AS="posts/{slug}.xhtml",
|
||||
PAGE_URL="posts/{slug}.xhtml",
|
||||
)
|
||||
expected = (FIXTURES / "basic" / "expected.xhtml").read_bytes()
|
||||
page = (parallel / "posts" / "page.xhtml").read_bytes()
|
||||
assert normalize(page) == normalize(expected)
|
||||
assert (parallel / "css" / CSS_NAME).exists()
|
||||
assert not (output / "posts").exists()
|
||||
assert not (output / "css").exists()
|
||||
|
||||
|
||||
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 allowlist["p"] == {"id", "title", "xml:lang"}
|
||||
assert allowlist["html"] == {"dir", "id", "lang", "title", "xml:lang", "xmlns"}
|
||||
assert "href" in allowlist["a"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("uv") is None, reason="uv is not installed")
|
||||
def test_wheel_bundles_plugin_data(tmp_path):
|
||||
"""The wheel carries the CSS, templates and DTD.
|
||||
|
||||
The namespace directories ship without __init__.py.
|
||||
"""
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
subprocess.run(
|
||||
["uv", "build", "--wheel", "-o", str(tmp_path), str(root)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
(wheel,) = tmp_path.glob("pelican_mews-*.whl")
|
||||
names = set(zipfile.ZipFile(wheel).namelist())
|
||||
expected = {
|
||||
"pelican/plugins/mews/static/mews-0.1.css",
|
||||
"pelican/plugins/mews/templates/mews/head.html",
|
||||
"pelican/plugins/mews/dtd/mews.dtd",
|
||||
"pelican/plugins/mews/py.typed",
|
||||
}
|
||||
assert expected <= names
|
||||
assert "pelican/__init__.py" not in names
|
||||
assert "pelican/plugins/__init__.py" not in names
|
||||
12
pelican-mews/tests/theme/templates/page.html
Normal file
12
pelican-mews/tests/theme/templates/page.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN"
|
||||
"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="{{ DEFAULT_LANG }}" lang="{{ DEFAULT_LANG }}">
|
||||
<head>
|
||||
<title>{{ page.title }}</title>
|
||||
{% include "mews/head.html" %}
|
||||
</head>
|
||||
<body>
|
||||
{{ page.content }}
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue