fix(wikilinks): supporte les ancres internes [[#Titre|Texte]] et corrige les IDs de titres
CI / lint (push) Failing after 19s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 23s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

- Ajoute la reconnaissance de la syntaxe Obsidian [[#Titre|Texte]] pour
  créer des liens d'ancre vers les titres du document courant.
- Corrige _heading_slugify pour ignorer les balises HTML et décoder les
  entités HTML (ex. &) avant de générer l'ID, évitant ainsi la
  pollution par les wikilinks placés dans les titres.
- Ajoute le style .wikilink-anchor et des tests couvrant ces cas.

Fixe le rendu du document TestDir/Agents IA - Panorama Complet 2026.md.
This commit is contained in:
2026-08-13 15:24:52 -04:00
parent c8d35b2ee6
commit 6da7fa6786
3 changed files with 52 additions and 8 deletions
+25 -6
View File
@@ -824,17 +824,23 @@ def _heading_slugify(text: str) -> str:
Matches the JavaScript slugify algorithm exactly using Matches the JavaScript slugify algorithm exactly using
Unicode-aware character classification: Unicode-aware character classification:
1. Lowercase 1. Strip HTML tags (e.g. wikilink spans rendered inside headings)
2. NFD normalize + strip combining marks 2. Decode HTML entities (e.g. ``&`` → ``&``)
3. Keep only Unicode letters, numbers, spaces, hyphens 3. Lowercase
4. Replace spaces with hyphens, collapse multiple hyphens 4. NFD normalize + strip combining marks
5. Keep only Unicode letters, numbers, spaces, hyphens
6. Replace spaces with hyphens, collapse multiple hyphens
Args: Args:
text: The heading text content. text: The heading text content (may contain inline HTML).
Returns: Returns:
A URL-safe slug string. A URL-safe slug string.
""" """
# Strip any inline HTML so it does not pollute the slug
text = re.sub(r"<[^>]+>", "", text)
# Decode HTML entities so &amp; becomes & before slugification
text = html_mod.unescape(text)
text = text.lower() text = text.lower()
text = unicodedata.normalize("NFD", text) text = unicodedata.normalize("NFD", text)
text = "".join(ch for ch in text if not unicodedata.combining(ch)) text = "".join(ch for ch in text if not unicodedata.combining(ch))
@@ -894,7 +900,12 @@ _markdown_renderer = mistune.create_markdown(
def _convert_wikilinks(content: str, current_vault: str) -> str: def _convert_wikilinks(content: str, current_vault: str) -> str:
"""Convert ``[[wikilinks]]`` and ``[[target|display]]`` to clickable HTML. """Convert ``[[wikilinks]]`` and ``[[target|display]]`` to clickable HTML.
Resolved links get a ``data-vault`` / ``data-path`` attribute pair. Supports:
- Internal file links: ``[[My Note]]`` / ``[[My Note|display]]``
- Same-document anchors: ``[[#Heading]]`` / ``[[#Heading|display]]``
Resolved file links get a ``data-vault`` / ``data-path`` attribute pair.
Anchor links target the slugified heading ID in the current document.
Unresolved links are rendered as ``<span class="wikilink-missing">``. Unresolved links are rendered as ``<span class="wikilink-missing">``.
Args: Args:
@@ -907,6 +918,14 @@ def _convert_wikilinks(content: str, current_vault: str) -> str:
def _replace(match): def _replace(match):
target = match.group(1).strip() target = match.group(1).strip()
display = match.group(2).strip() if match.group(2) else target display = match.group(2).strip() if match.group(2) else target
# Same-document anchor link: [[#Heading|display]]
if target.startswith("#"):
anchor_text = target[1:].strip()
anchor_slug = _heading_slugify(anchor_text)
link_display = display if display != target else anchor_text
return f'<a class="wikilink-anchor" href="#{anchor_slug}">{link_display}</a>'
found = find_file_in_index(target, current_vault) found = find_file_in_index(target, current_vault)
if found: if found:
return ( return (
+7 -2
View File
@@ -2076,14 +2076,19 @@ select {
} }
/* Wikilinks */ /* Wikilinks */
.wikilink { .wikilink,
.wikilink-anchor {
color: var(--accent); color: var(--accent);
border-bottom: 1px dashed var(--accent); border-bottom: 1px dashed var(--accent);
cursor: pointer; cursor: pointer;
} }
.wikilink:hover { .wikilink:hover,
.wikilink-anchor:hover {
opacity: 0.8; opacity: 0.8;
} }
.wikilink-anchor {
text-decoration: none;
}
.wikilink-missing { .wikilink-missing {
color: var(--text-muted); color: var(--text-muted);
border-bottom: 1px dashed var(--text-muted); border-bottom: 1px dashed var(--text-muted);
+20
View File
@@ -655,6 +655,26 @@ class TestConvertWikilinks:
result = _convert_wikilinks("See ![[image.png]] here", "TestVault") result = _convert_wikilinks("See ![[image.png]] here", "TestVault")
assert "image" in result or "img" in result assert "image" in result or "img" in result
def test_convert_wikilink_anchor(self):
from backend.main import _convert_wikilinks
result = _convert_wikilinks("[[#Section importante|voir section]]", "TestVault")
assert 'href="#section-importante"' in result
assert "wikilink-anchor" in result
assert "voir section" in result
def test_convert_wikilink_anchor_without_alias(self):
from backend.main import _convert_wikilinks
result = _convert_wikilinks("[[#Claude Code]]", "TestVault")
assert 'href="#claude-code"' in result
assert "Claude Code" in result
class TestHeadingSlugifyHtmlStripping:
def test_slugify_strips_html_tags(self):
from backend.main import _heading_slugify
result = _heading_slugify('## 1. Agents installés localement <a href="#table-des-matieres">↩</a>')
assert result == "1-agents-installes-localement"
# ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════
# Secret Redactor # Secret Redactor