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
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:
+25
-6
@@ -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 & 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
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user