Unicode i slugs: «Grønt er skjønt» → grønt-er-skjønt
Bokstaver og tall i alle skriftsystemer beholdes, alt annet blir bindestrek. NFC-normalisering gir samme slug uansett om ø/å kommer prekomponert eller som base + kombinerende tegn (macOS). URL-er i feed.xml percent-encodes siden RSS-lesere krever ASCII. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcEy43fNYpwg6K6oTKakWR
This commit is contained in:
parent
9eb375e339
commit
1218c5d650
4 changed files with 26 additions and 20 deletions
|
|
@ -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(
|
||||
"<item>"
|
||||
f"<title>{escape(p.title)}</title>"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "<strong>verden</strong>" 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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue