From bbe7eac13df60bd924b9e2b2e4c0306e2bdd3904 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Meyer Date: Sun, 17 May 2026 12:16:36 +0200 Subject: [PATCH] Fix anchor navigation in preview: add heading IDs and intercept #fragment clicks (issue #73) - Add processHeadingIds() to inject GitHub-style id attributes on

-

tags so that #fragment anchors have a target element to scroll to - Intercept #fragment navigation in the SCHEDULED state listener: cancel the full page reload and use scrollIntoView() via executeScript instead, preventing flicker and correctly scrolling to the heading Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/ui/PreviewPanel.java | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/main/java/ui/PreviewPanel.java b/src/main/java/ui/PreviewPanel.java index c7321f8..54b0349 100644 --- a/src/main/java/ui/PreviewPanel.java +++ b/src/main/java/ui/PreviewPanel.java @@ -184,6 +184,18 @@ public PreviewPanel() { if (newState == Worker.State.SCHEDULED) { String location = webView.getEngine().getLocation(); if (location != null && !location.isEmpty() && !location.equals("about:blank")) { + // Ancre interne (#fragment) : défiler vers l'élément sans recharger la page + if (location.contains("#")) { + String fragment = location.substring(location.indexOf('#') + 1); + if (!fragment.isEmpty()) { + webView.getEngine().getLoadWorker().cancel(); + webView.getEngine().executeScript( + "var el = document.getElementById('" + fragment.replace("'", "\\'") + "');" + + "if (el) el.scrollIntoView({behavior: 'smooth', block: 'start'});" + ); + } + return; + } // Vérifier si c'est un lien vers un fichier .md if (location.toLowerCase().endsWith(".md") || location.toLowerCase().endsWith(".markdown")) { // Annuler la navigation @@ -267,6 +279,9 @@ private void updatePreview(String markdown, boolean addToHistory) { String html = htmlRenderer.render(markdownParser.parse(body)); + // ── IDs sur les titres : nécessaires pour la navigation par ancres (#fragment) + html = processHeadingIds(html); + // ── Checkboxes : convertir [ ] et [x] en éléments checkbox HTML html = processCheckboxes(html); @@ -527,6 +542,40 @@ private String loadResourceAsString(String resourcePath) { }); } + /** + * Injecte un attribut {@code id} GitHub-compatible sur chaque titre HTML ({@code

}–{@code

}) + * qui n'en possède pas encore. Requis pour que la navigation par ancres (#fragment) fonctionne. + */ + private String processHeadingIds(String html) { + Pattern p = Pattern.compile( + "<(h[1-6])(\\s[^>]*)?>(.+?)", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + Matcher m = p.matcher(html); + StringBuffer sb = new StringBuffer(); + java.util.Map seen = new java.util.HashMap<>(); + while (m.find()) { + String tag = m.group(1); + String attrs = m.group(2) != null ? m.group(2) : ""; + String content = m.group(3); + if (attrs.contains("id=")) { + m.appendReplacement(sb, Matcher.quoteReplacement(m.group(0))); + continue; + } + String text = content.replaceAll("<[^>]+>", "").trim(); + String id = text.toLowerCase() + .replaceAll("[^\\w\\s-]", "") + .trim() + .replaceAll("\\s+", "-"); + int count = seen.getOrDefault(id, 0); + seen.put(id, count + 1); + String uid = count == 0 ? id : id + "-" + count; + m.appendReplacement(sb, Matcher.quoteReplacement( + "<" + tag + attrs + " id=\"" + uid + "\">" + content + "")); + } + m.appendTail(sb); + return sb.toString(); + } + /** * Convertit les marqueurs de checkbox Markdown ([ ] et [x]) en éléments HTML checkbox. *