Files
flowdeck/app/services/wiki_links.py
T
bruno 937ecfc2e0
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
fix: A30 + A37 + A39 + A40 + A41 — fin du P2/P3 XS/S (v7.4.0)
- A30 — `require_scope()` câblé : 69 sites stricts de api_v2.py passent par la
  factory (Bearer + scope en 1 appel, contrôle manuel supprimé) ; sémantique
  alignée sur celle des handlers (pas de default "read" → 0 changement de
  comportement) ; 12 top-level morts supprimés (0 ref app ET tests) :
  unsync_block, find_referring, _b64url, strip_markdown, format_number,
  get_auto_property_value, get_next_unique_id, local_date_in_tz,
  verify_device_token, _get_dynamic_groups, _require_user_gitea,
  validate_upload_request
- A37 — CORS sans `*` : origines = app_base_url + allow_origin_regex
  (localhost/dev, origines d'extension pour le Web Clipper), méthodes et
  entêtes minutées, allow_credentials explicite + test test_cors_no_star
- A39 — htmx : décision « rien » documentée (32 attributs hx-* réels sur 6
  templates, conversion = refonte du view-switching sans test E2E)
- A40 — version d'assets à source unique : ENV.globals["asset_version"] lu au
  boot depuis le fichier VERSION ; littéraux `?v=` de base.html éliminés ;
  test test_asset_version_single_source
- A41 — app.css : 91 règles mortes purgées (-10 274 octets, 121 618 → 111 344),
  scan templates/JS/CSS/Python à 0 référence

suite **1031/1031** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.4.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
2026-10-01 09:30:37 -04:00

86 lines
3.2 KiB
Python

"""FlowDeck — Wiki-links & page mentions (v5.11.0).
Inline references live inside plain-text block content as tokens::
[[fdpage:123]] link to page id 123, label resolved at render time
[[fddate:2026-10-01]] date chip (static label)
The page token stores only the id, so renaming a page propagates everywhere
(the label is resolved through the DB / the titles endpoint at render time).
Backlinks keep working because the server renderer emits ``/pages/<id>``
anchors, and the backlink scanner also matches the raw token.
Pure-stdlib helpers shared by the public renderer and the block export.
"""
from __future__ import annotations
import html as _html
import re
WIKI_PAGE_RE = re.compile(r"\[\[fdpage:(\d+)\]\]")
WIKI_DATE_RE = re.compile(r"\[\[fddate:(\d{4}-\d{2}-\d{2})(?:T[0-9:]{5,8})?\]\]")
_DATE_LABELS = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri", 5: "Sat", 6: "Sun"}
_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def extract_page_ids(text: str) -> list[int]:
"""All page ids referenced by wiki tokens in a content string."""
return sorted({int(m) for m in WIKI_PAGE_RE.findall(text or "")})
def _date_label(iso: str) -> str:
try:
parts = iso.split("-")
y, m, d = int(parts[0]), int(parts[1]), int(parts[2])
import datetime as _dt
wd = _dt.date(y, m, d).weekday()
return f"{_DATE_LABELS[wd]} {d} {_MONTHS[m-1]} {y}"
except (ValueError, IndexError):
return iso
def token_labels(conn, content: str) -> dict[str, str]:
"""Map every token in ``content`` to its display label (page title)."""
out: dict[str, str] = {}
ids = extract_page_ids(content)
if ids:
placeholders = ",".join("?" * len(ids))
rows = conn.execute(
f"SELECT id, title, page_icon FROM pages WHERE id IN ({placeholders})", ids,
).fetchall()
by_id = {str(r["id"]): (r["title"] or "Untitled", r["page_icon"] or "") for r in rows}
for token_id in ids:
title, icon = by_id.get(str(token_id), ("Deleted page", ""))
out[f"[[fdpage:{token_id}]]"] = f"{icon + ' ' if icon else ''}{title}"
for m in WIKI_DATE_RE.finditer(content or ""):
iso = m.group(1)
out[f"[[fddate:{iso}]]"] = _date_label(iso)
return out
def resolve_tokens_html(content: str, titles: dict[str, str]) -> str:
"""Escape plain-text content then turn wiki tokens into HTML chips/links.
``titles`` maps token → label (from :func:`token_labels`). Used by the
public page renderer and any server-side HTML output of block content.
"""
s = _html.escape(content or "")
def _page(m: re.Match) -> str:
pid = m.group(1)
label = titles.get(m.group(0)) or "Deleted page"
return (f'<a class="fd-wiki-link" href="/pages/{pid}" '
f'data-pid="{pid}" title="{_html.escape(label)}">{_html.escape(label)}</a>')
def _date(m: re.Match) -> str:
iso = m.group(1)
label = titles.get(f"[[fddate:{iso}]]") or iso
return f'<span class="fd-wiki-date" title="{_html.escape(iso)}">{_html.escape(label)}</span>'
s = WIKI_PAGE_RE.sub(_page, s)
s = WIKI_DATE_RE.sub(_date, s)
return s