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 datetime import UTC, datetime
|
||||||
from email.utils import format_datetime
|
from email.utils import format_datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import quote
|
||||||
from xml.sax.saxutils import escape
|
from xml.sax.saxutils import escape
|
||||||
|
|
||||||
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, select_autoescape
|
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 + "/"
|
base = cfg.site_url + "/"
|
||||||
items = []
|
items = []
|
||||||
for p in posts:
|
for p in posts:
|
||||||
link = base + p.url_path
|
link = base + quote(p.url_path) # RSS-lesere krever ASCII-URL-er
|
||||||
items.append(
|
items.append(
|
||||||
"<item>"
|
"<item>"
|
||||||
f"<title>{escape(p.title)}</title>"
|
f"<title>{escape(p.title)}</title>"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
"""Lager URL-vennlige slugs fra titler.
|
"""Lager URL-vennlige slugs fra titler.
|
||||||
|
|
||||||
Norske tegn translittereres eksplisitt (æ→ae, ø→oe, å→aa) fordi generisk
|
Unicode-bokstaver beholdes ("Grønt er skjønt" → "grønt-er-skjønt"); moderne
|
||||||
unicode-normalisering ville mistet dem helt ("blåbær" → "blbr").
|
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
|
from __future__ import annotations
|
||||||
|
|
@ -9,15 +12,13 @@ from __future__ import annotations
|
||||||
import re
|
import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
_NORDIC = str.maketrans(
|
# Alt som ikke er bokstav eller tall (i alle skriftsystemer) blir skilletegn.
|
||||||
{"æ": "ae", "ø": "oe", "å": "aa", "ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss"}
|
_SEP = re.compile(r"[^\w]+|_+", re.UNICODE)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def slugify(text: str) -> str:
|
def slugify(text: str) -> str:
|
||||||
text = text.lower().translate(_NORDIC)
|
text = unicodedata.normalize("NFC", text).casefold()
|
||||||
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode()
|
text = _SEP.sub("-", text).strip("-")
|
||||||
text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
|
|
||||||
return text or "post"
|
return text or "post"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import re
|
import re
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from soapbox.app import create_app
|
from soapbox.app import create_app
|
||||||
from soapbox.config import load_config
|
from soapbox.config import load_config
|
||||||
|
|
@ -50,31 +51,31 @@ def test_full_flow_owner(client, cfg):
|
||||||
f"/admin/edit/{post_id}",
|
f"/admin/edit/{post_id}",
|
||||||
data={"csrf": token, "title": "Grønt er skjønt", "body": body, "action": "publish"},
|
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 unquote(r.headers["Location"]).endswith("/admin/edit/_owner/grønt-er-skjønt")
|
||||||
assert (cfg.content_dir / "posts/groent-er-skjoent/index.md").exists()
|
assert (cfg.content_dir / "posts/grønt-er-skjønt/index.md").exists()
|
||||||
assert (cfg.content_dir / "posts/groent-er-skjoent/mitt-bilde.png").exists()
|
assert (cfg.content_dir / "posts/grønt-er-skjønt/mitt-bilde.png").exists()
|
||||||
|
|
||||||
# Statisk output
|
# Statisk output
|
||||||
r = client.get("/groent-er-skjoent")
|
r = client.get("/grønt-er-skjønt")
|
||||||
assert r.status_code == 301 and r.headers["Location"].endswith("/groent-er-skjoent/")
|
assert r.status_code == 301 and unquote(r.headers["Location"]).endswith("/grønt-er-skjønt/")
|
||||||
html = client.get("/groent-er-skjoent/").text
|
html = client.get("/grønt-er-skjønt/").text
|
||||||
assert "<strong>verden</strong>" in html
|
assert "<strong>verden</strong>" in html
|
||||||
assert 'href="../theme/style.css"' in html
|
assert 'href="../theme/style.css"' in html
|
||||||
assert 'href="../theme/favicon.svg"' in html
|
assert 'href="../theme/favicon.svg"' in html
|
||||||
assert 'href="../"' in html # intern lenke omskrevet til relativ
|
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
|
front = client.get("/").text
|
||||||
assert "Grønt er skjønt" in front
|
assert "Grønt er skjønt" in front
|
||||||
assert 'href="https://kode.naiv.no/olemd/soapbox"' in front # AGPL: lenke til kilden
|
assert 'href="https://kode.naiv.no/olemd/soapbox"' in front # AGPL: lenke til kilden
|
||||||
feed = client.get("/feed.xml").text
|
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
|
# Etter publisering fryses slug selv om tittelen endres
|
||||||
r = client.post(
|
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"},
|
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
|
# Git-historikk
|
||||||
from soapbox.gitstore import log
|
from soapbox.gitstore import log
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ from tests.conftest import png_bytes
|
||||||
|
|
||||||
|
|
||||||
def test_slugify_norwegian():
|
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 slugify(" ") == "post"
|
||||||
assert unique_slug("a", {"a", "a-2"}) == "a-3"
|
assert unique_slug("a", {"a", "a-2"}) == "a-3"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue