This commit is contained in:
retoor 2026-07-19 21:26:18 +02:00
parent c53e2a3319
commit 43c5a948e8
2 changed files with 23 additions and 4 deletions

View File

@ -193,6 +193,8 @@ The CLIENT pipeline (`ContentRenderer.js`, `dp-content`/`dp-title`) is retained
**Emoji shortcodes are the full GitHub/Discord `:name:` set** (~4869 names), generated once from the `emoji` library by `rendering.py` `build_emoji_shortcodes()`; the frontend gets the identical map via the generated `static/js/emoji-shortcodes.js` (regenerate with `devplace emoji-sync` after bumping the dependency, never hand-edit).
**Email anonymization:** both `_render_content` and `_render_title` mask email addresses to prevent doxing. `_mask_emails` in `rendering.py` runs on rendered text nodes only (inside `_transform_text`, `_MediaProcessor`, and `_InlineFilter` - never before mistune, or the `*` mask characters would be parsed as emphasis) and stars ~80% of the local part (keeps a leading 20%, minimum one visible char). Addresses on `molodetz.nl` (and its subdomains) are exempt and render verbatim.
**Em-dash normalization:** `_normalize_dashes` in `rendering.py` replaces all forms (em dash `\u2014`, en dash `\u2013`, and their HTML entities) with a hyphen BEFORE mistune processes the text. Runs inside `_render_content`/`_render_title` which are `@lru_cache`d, so each unique text is normalized once. No per-template overhead.
### Auth

View File

@ -69,6 +69,9 @@ _YOUTUBE_ALLOW = (
"gyroscope; picture-in-picture"
)
_EMAIL_RE = re.compile(r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b")
_EMAIL_KEEP_DOMAIN = "molodetz.nl"
_MEDIA_SKIP_TAGS = {"a", "code", "pre"}
_TITLE_INLINE_TAGS = {
"b", "strong", "i", "em", "code", "del", "s", "mark", "sub", "sup", "span", "br",
@ -153,12 +156,26 @@ def _embed_url(url: str) -> str:
)
def _mask_email(match: re.Match) -> str:
email = match.group(0)
local, _, domain = email.partition("@")
lowered = domain.lower()
if lowered == _EMAIL_KEEP_DOMAIN or lowered.endswith("." + _EMAIL_KEEP_DOMAIN):
return email
reveal = max(1, len(local) - round(len(local) * 0.8))
return f"{local[:reveal]}{'*' * (len(local) - reveal)}@{domain}"
def _mask_emails(text: str) -> str:
return _EMAIL_RE.sub(_mask_email, text)
def _transform_text(text: str) -> str:
out: list[str] = []
pos = 0
for match in _TOKEN_RE.finditer(text):
if match.start() > pos:
out.append(html.escape(text[pos:match.start()]))
out.append(html.escape(_mask_emails(text[pos:match.start()])))
if match.group("url"):
out.append(_embed_url(match.group("url")))
else:
@ -169,7 +186,7 @@ def _transform_text(text: str) -> str:
)
pos = match.end()
if pos < len(text):
out.append(html.escape(text[pos:]))
out.append(html.escape(_mask_emails(text[pos:])))
return "".join(out)
@ -205,7 +222,7 @@ class _MediaProcessor(HTMLParser):
def handle_data(self, data: str) -> None:
if self._skip_depth > 0:
self._out.append(html.escape(data))
self._out.append(html.escape(_mask_emails(data)))
else:
self._out.append(_transform_text(data))
@ -231,7 +248,7 @@ class _InlineFilter(HTMLParser):
self._out.append(f"</{tag}>")
def handle_data(self, data: str) -> None:
self._out.append(html.escape(data))
self._out.append(html.escape(_mask_emails(data)))
def result(self) -> str:
return "".join(self._out).strip()