From 376b0f34ebb961e18f6d1f248fe56797ca8456a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delorme?= Date: Mon, 2 Mar 2026 21:35:09 +0100 Subject: [PATCH 1/2] feat(preview): implement SVG caching and rendering for PlantUML diagrams --- src/main/java/ui/PreviewPanel.java | 104 +++++++++++++++++++-------- src/main/resources/html/preview.html | 90 +++++++++++++++++++++++ 2 files changed, 164 insertions(+), 30 deletions(-) create mode 100644 src/main/resources/html/preview.html diff --git a/src/main/java/ui/PreviewPanel.java b/src/main/java/ui/PreviewPanel.java index 9093680..596bef5 100644 --- a/src/main/java/ui/PreviewPanel.java +++ b/src/main/java/ui/PreviewPanel.java @@ -8,6 +8,7 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.security.MessageDigest; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -17,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -95,6 +97,9 @@ public class PreviewPanel extends BasePanel { "
(.*?)
", Pattern.DOTALL); + /** Cache des SVG PlantUML générés (clé = hash SHA-256 du code source). */ + private static final Map pumlSvgCache = new ConcurrentHashMap<>(); + /** * Pattern pour détecter la syntaxe d'image étendue avec dimensions. * Formats supportés : @@ -577,8 +582,9 @@ private String processPlantUmlBlocks(String html) { /** * Démarre un thread de fond par bloc PlantUML en attente. - * Chaque thread génère le SVG via le jar local, puis l'injecte dans la page - * via {@code executeScript}. Quand tous les blocs sont rendus, le callback + * Chaque thread génère le SVG via le jar local (ou utilise le cache), + * puis l'injecte dans la page via {@code executeScript}. + * Quand tous les blocs sont rendus, le callback * {@link #onPlantUmlRenderingChanged} est appelé avec {@code false}. */ private void dispatchLocalPumlRendering() { @@ -595,42 +601,80 @@ private void dispatchLocalPumlRendering() { for (String[] block : blocks) { String id = block[0]; String puml = block[1]; + String cacheKey = computePumlHash(puml); + + // Vérifier le cache d'abord + String cachedSvg = pumlSvgCache.get(cacheKey); + if (cachedSvg != null) { + // Utiliser le SVG mis en cache directement sur le thread FX + injectSvgIntoDom(id, cachedSvg, puml); + continue; + } + + // Pas en cache : lancer le rendu en arrière-plan Thread t = new Thread(() -> { String svg = renderWithLocalJar(puml, jarPath); - Platform.runLater(() -> { - try { - if (svg != null) { - // Encoder en base64 pour éviter tout problème d'échappement JS - String b64 = Base64.getEncoder().encodeToString( - svg.getBytes(StandardCharsets.UTF_8)); - String js = "var el=document.getElementById('" + id + "');" - + "if(el)el.outerHTML='
" - + "\"PlantUML
';"; - webView.getEngine().executeScript(js); - } else { - // Repli : serveur en ligne - String url = PlantUmlEncoder.toSvgUrl(puml); - String js = "var el=document.getElementById('" + id + "');" - + "if(el)el.outerHTML='
" - + "\"PlantUML
';"; - webView.getEngine().executeScript(js); - } - } catch (Exception ignored) { - } finally { - if (pendingPumlCount.decrementAndGet() == 0 - && onPlantUmlRenderingChanged != null) { - onPlantUmlRenderingChanged.accept(false); - } - } - }); + if (svg != null) { + // Stocker dans le cache + pumlSvgCache.put(cacheKey, svg); + } + Platform.runLater(() -> injectSvgIntoDom(id, svg, puml)); }); t.setDaemon(true); t.start(); } } + /** + * Injecte un SVG PlantUML dans le DOM, ou utilise le serveur en ligne en cas d'échec. + */ + private void injectSvgIntoDom(String id, String svg, String puml) { + try { + if (svg != null) { + // Encoder en base64 pour éviter tout problème d'échappement JS + String b64 = Base64.getEncoder().encodeToString( + svg.getBytes(StandardCharsets.UTF_8)); + String js = "var el=document.getElementById('" + id + "');" + + "if(el)el.outerHTML='
" + + "\"PlantUML
';"; + webView.getEngine().executeScript(js); + } else { + // Repli : serveur en ligne + String url = PlantUmlEncoder.toSvgUrl(puml); + String js = "var el=document.getElementById('" + id + "');" + + "if(el)el.outerHTML='
" + + "\"PlantUML
';"; + webView.getEngine().executeScript(js); + } + } catch (Exception ignored) { + } finally { + if (pendingPumlCount.decrementAndGet() == 0 + && onPlantUmlRenderingChanged != null) { + onPlantUmlRenderingChanged.accept(false); + } + } + } + + /** + * Calcule un hash SHA-256 du code PlantUML pour servir de clé de cache. + */ + private String computePumlHash(String puml) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(puml.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + // Fallback : utiliser le code lui-même comme clé + return puml; + } + } + /** * Génère un SVG en exécutant un jar PlantUML local via un sous-processus. * Retourne le SVG inline (chaîne commençant par {@code + + + + {{BASE_TAG}} + + + + + + + + + + +
{{FRONT_MATTER}}
{{CONTENT}}
+ + + + From 43f337177ce24f6a85997b63952e3eb1501a3617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delorme?= Date: Mon, 2 Mar 2026 21:39:42 +0100 Subject: [PATCH 2/2] feat(docs): enhance PlantUML diagram rendering details and add SVG caching information --- README.md | 40 +++++++++++++++++++-------------------- src/docs/user-guide-en.md | 3 ++- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c771ae9..5edfbc4 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ A lightweight and modern Markdown editor built with JavaFX. - **Syntax Highlighting** - Code blocks with automatic language detection and theme-aware syntax coloring (via [highlight.js](https://highlightjs.org/)); each app theme maps to a matching highlight.js style - **Code Block Copy Button** - One-click copy for code blocks in preview, with visual "✓ Copied" feedback - **Markdown Tables** - Full GFM table support with styled rendering -- **PlantUML Diagrams** - Render PlantUML diagrams directly in preview; supports both the **official PlantUML online server** and a **local PlantUML jar** (configurable in Options → Tools); when local rendering is active a spinning ⚙ gear icon and a status indicator are shown in the status bar during generation +- **PlantUML Diagrams** - Render PlantUML diagrams directly in preview; supports both the **official PlantUML online server** and a **local PlantUML jar** (configurable in Options → Tools); when local rendering is active a spinning ⚙ gear icon and a status indicator are shown in the status bar during generation; **in-memory SVG cache** based on source hash avoids regenerating unchanged diagrams - **Mermaid Diagrams** - Render Mermaid diagrams (flowcharts, sequences, etc.) directly in preview; Mermaid theme auto-matches app theme - **Math Equations** - LaTeX/MathML support via KaTeX for inline (`$...$`) and block (`$$...$$`) equations - **Front Matter Panel** - Collapsible panel above the editor for visual editing of YAML front matter (title, tags, authors, summary, UUID, created date, draft); supports custom fields and UUID-based document linking via drag & drop @@ -54,13 +54,13 @@ A lightweight and modern Markdown editor built with JavaFX. MarkNote is available in 5 languages: -| Language | Locale | -|----------|--------| -| Français (French) | `fr` | -| English | `en` | -| Deutsch (German) | `de` | -| Español (Spanish) | `es` | -| Italiano (Italian) | `it` | +| Language | Locale | +| ------------------ | ------ | +| Français (French) | `fr` | +| English | `en` | +| Deutsch (German) | `de` | +| Español (Spanish) | `es` | +| Italiano (Italian) | `it` | The application automatically uses your system's locale. @@ -81,13 +81,13 @@ cd marknote ### Build Commands -| Command | Description | -|---------|-------------| -| `./build` | Compile the project and create the JAR | -| `./build run` | Compile and run the application | -| `./build test` | Run unit tests | -| `./build package` | Create a distributable package for the **current platform** with embedded JRE | -| `./build package-all` | Create distributable packages for **all platforms** (linux, mac, win) | +| Command | Description | +| --------------------- | ----------------------------------------------------------------------------- | +| `./build` | Compile the project and create the JAR | +| `./build run` | Compile and run the application | +| `./build test` | Run unit tests | +| `./build package` | Create a distributable package for the **current platform** with embedded JRE | +| `./build package-all` | Create distributable packages for **all platforms** (linux, mac, win) | ## Running @@ -137,11 +137,11 @@ Create packages for **all three platforms** in one shot: This produces three ZIP archives in `target/`: -| Archive | Platform | -|---------|----------| -| `MarkNote-{version}-linux.zip` | Linux x64 | -| `MarkNote-{version}-mac.zip` | macOS | -| `MarkNote-{version}-win.zip` | Windows x64 | +| Archive | Platform | +| ------------------------------ | ----------- | +| `MarkNote-{version}-linux.zip` | Linux x64 | +| `MarkNote-{version}-mac.zip` | macOS | +| `MarkNote-{version}-win.zip` | Windows x64 | > **Note:** A minimal embedded JRE (via `jlink`) is bundled only for the current host platform. > Cross-platform packages include all required JARs but require a compatible Java runtime already diff --git a/src/docs/user-guide-en.md b/src/docs/user-guide-en.md index 7d48682..e448cda 100644 --- a/src/docs/user-guide-en.md +++ b/src/docs/user-guide-en.md @@ -52,7 +52,7 @@ MarkNote is a cross-platform Markdown editor designed for writers, developers, a - **Markdown Tables** - Full GFM table support with styled rendering - **Task Lists** - GitHub-style checkboxes (`[ ]` / `[x]`) rendered in preview - **GitHub Alerts** - Styled blockquotes for `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]` -- **PlantUML Diagrams** - Render PlantUML diagrams directly in the preview; switch between the **online PlantUML server** (default) or a **local `plantuml.jar`** configured in Options → Tools; local rendering is asynchronous (per-block background threads) and shows a ⚙ spinning gear icon in the status bar during generation +- **PlantUML Diagrams** - Render PlantUML diagrams directly in the preview; switch between the **online PlantUML server** (default) or a **local `plantuml.jar`** configured in Options → Tools; local rendering is asynchronous (per-block background threads) and shows a ⚙ spinning gear icon in the status bar during generation; **in-memory SVG cache** avoids regenerating unchanged diagrams - **Mermaid Diagrams** - Render Mermaid flowcharts, sequences, and more in the preview (theme auto-matches app theme) - **Math Equations** - LaTeX/MathML support via KaTeX (`$...$` inline, `$$...$$` block) - **Front Matter Panel** - Collapsible panel above the editor showing and editing YAML front matter metadata, with UUID-based document linking via drag & drop @@ -889,6 +889,7 @@ When local rendering is active: - The **⚙ spinning gear** icon in the status bar is visible during rendering - On completion the placeholders are replaced inline with the SVG (no page reload) - If the local jar fails, the diagram falls back silently to the online server +- **SVG Cache:** Generated SVG images are cached in memory using a SHA-256 hash of the diagram source; unchanged diagrams are served instantly from cache without invoking the jar, significantly improving preview responsiveness during editing See [Options → Tools Tab](#tools-tab) to configure the local jar.