diff --git a/soapbox/build.py b/soapbox/build.py index de957e7..e898644 100644 --- a/soapbox/build.py +++ b/soapbox/build.py @@ -18,6 +18,7 @@ import shutil from datetime import UTC, datetime from email.utils import format_datetime from pathlib import Path +from urllib.parse import quote from xml.sax.saxutils import escape from jinja2 import ChoiceLoader, Environment, FileSystemLoader, select_autoescape @@ -113,7 +114,7 @@ def _feed(cfg: Config, site: dict, posts: list[Post]) -> str: base = cfg.site_url + "/" items = [] for p in posts: - link = base + p.url_path + link = base + quote(p.url_path) # RSS-lesere krever ASCII-URL-er items.append( "" f"{escape(p.title)}" diff --git a/soapbox/slug.py b/soapbox/slug.py index 8f9c125..5cc2764 100644 --- a/soapbox/slug.py +++ b/soapbox/slug.py @@ -1,7 +1,10 @@ """Lager URL-vennlige slugs fra titler. -Norske tegn translittereres eksplisitt (æ→ae, ø→oe, å→aa) fordi generisk -unicode-normalisering ville mistet dem helt ("blåbær" → "blbr"). +Unicode-bokstaver beholdes ("Grønt er skjønt" → "grønt-er-skjønt"); moderne +nettlesere og servere håndterer UTF-8 i stier, og det gir penere og mer +lesbare adresser enn translitterering. Teksten NFC-normaliseres slik at +f.eks. «ø» skrevet som base + kombinerende tegn (vanlig fra macOS) gir samme +slug som det prekomponerte tegnet. """ from __future__ import annotations @@ -9,15 +12,13 @@ from __future__ import annotations import re import unicodedata -_NORDIC = str.maketrans( - {"æ": "ae", "ø": "oe", "å": "aa", "ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss"} -) +# Alt som ikke er bokstav eller tall (i alle skriftsystemer) blir skilletegn. +_SEP = re.compile(r"[^\w]+|_+", re.UNICODE) def slugify(text: str) -> str: - text = text.lower().translate(_NORDIC) - text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode() - text = re.sub(r"[^a-z0-9]+", "-", text).strip("-") + text = unicodedata.normalize("NFC", text).casefold() + text = _SEP.sub("-", text).strip("-") return text or "post" diff --git a/tests/test_app.py b/tests/test_app.py index a6997c0..3a09710 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,4 +1,5 @@ import re +from urllib.parse import unquote from soapbox.app import create_app from soapbox.config import load_config @@ -50,31 +51,31 @@ def test_full_flow_owner(client, cfg): f"/admin/edit/{post_id}", data={"csrf": token, "title": "Grønt er skjønt", "body": body, "action": "publish"}, ) - assert r.headers["Location"].endswith("/admin/edit/_owner/groent-er-skjoent") - assert (cfg.content_dir / "posts/groent-er-skjoent/index.md").exists() - assert (cfg.content_dir / "posts/groent-er-skjoent/mitt-bilde.png").exists() + assert unquote(r.headers["Location"]).endswith("/admin/edit/_owner/grønt-er-skjønt") + assert (cfg.content_dir / "posts/grønt-er-skjønt/index.md").exists() + assert (cfg.content_dir / "posts/grønt-er-skjønt/mitt-bilde.png").exists() # Statisk output - r = client.get("/groent-er-skjoent") - assert r.status_code == 301 and r.headers["Location"].endswith("/groent-er-skjoent/") - html = client.get("/groent-er-skjoent/").text + r = client.get("/grønt-er-skjønt") + assert r.status_code == 301 and unquote(r.headers["Location"]).endswith("/grønt-er-skjønt/") + html = client.get("/grønt-er-skjønt/").text assert "verden" in html assert 'href="../theme/style.css"' in html assert 'href="../theme/favicon.svg"' in html assert 'href="../"' in html # intern lenke omskrevet til relativ - assert client.get("/groent-er-skjoent/mitt-bilde.png").status_code == 200 + assert client.get("/grønt-er-skjønt/mitt-bilde.png").status_code == 200 front = client.get("/").text assert "Grønt er skjønt" in front assert 'href="https://kode.naiv.no/olemd/soapbox"' in front # AGPL: lenke til kilden feed = client.get("/feed.xml").text - assert "https://blog.example.no/groent-er-skjoent/" in feed + assert "https://blog.example.no/gr%C3%B8nt-er-skj%C3%B8nt/" in feed # Etter publisering fryses slug selv om tittelen endres r = client.post( - "/admin/edit/_owner/groent-er-skjoent", + "/admin/edit/_owner/grønt-er-skjønt", data={"csrf": token, "title": "Ny tittel", "body": body, "action": "save"}, ) - assert r.headers["Location"].endswith("/admin/edit/_owner/groent-er-skjoent") + assert unquote(r.headers["Location"]).endswith("/admin/edit/_owner/grønt-er-skjønt") # Git-historikk from soapbox.gitstore import log diff --git a/tests/test_core.py b/tests/test_core.py index f00586d..3323141 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,7 +9,10 @@ from tests.conftest import png_bytes def test_slugify_norwegian(): - assert slugify("Grønt er skjønt: blåbær & øl!") == "groent-er-skjoent-blaabaer-oel" + assert slugify("Grønt er skjønt: blåbær & øl!") == "grønt-er-skjønt-blåbær-øl" + assert slugify("Hello, World_2!") == "hello-world-2" + assert slugify("Blåbær") == slugify("Bla\u030abær") # NFD → NFC + assert slugify("日本語 タイトル") == "日本語-タイトル" assert slugify(" ") == "post" assert unique_slug("a", {"a", "a-2"}) == "a-3"