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
Unicode-aware character classification:
1. Lowercase
2. NFD normalize + strip combining marks
3. Keep only Unicode letters, numbers, spaces, hyphens
4. Replace spaces with hyphens, collapse multiple hyphens
1. Strip HTML tags (e.g. wikilink spans rendered inside headings)
2. Decode HTML entities (e.g. ``&`` → ``&``)
3. Lowercase
4. NFD normalize + strip combining marks
5. Keep only Unicode letters, numbers, spaces, hyphens
6. Replace spaces with hyphens, collapse multiple hyphens
Args:
text: The heading text content.
text: The heading text content (may contain inline HTML).
Returns:
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 = unicodedata.normalize("NFD", text)
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:
"""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">``.
Args:
@@ -907,6 +918,14 @@ def _convert_wikilinks(content: str, current_vault: str) -> str:
def _replace(match):
target = match.group(1).strip()
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)
if found:
return (
+7 -2
View File
@@ -2076,14 +2076,19 @@ select {
}
/* Wikilinks */
.wikilink {
.wikilink,
.wikilink-anchor {
color: var(--accent);
border-bottom: 1px dashed var(--accent);
cursor: pointer;
}
.wikilink:hover {
.wikilink:hover,
.wikilink-anchor:hover {
opacity: 0.8;
}
.wikilink-anchor {
text-decoration: none;
}
.wikilink-missing {
color: 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")
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