Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/docs/user-guide-en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
104 changes: 74 additions & 30 deletions src/main/java/ui/PreviewPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -95,6 +97,9 @@ public class PreviewPanel extends BasePanel {
"<pre><code\\s+class=\"language-plantuml\">(.*?)</code></pre>",
Pattern.DOTALL);

/** Cache des SVG PlantUML générés (clé = hash SHA-256 du code source). */
private static final Map<String, String> pumlSvgCache = new ConcurrentHashMap<>();

/**
* Pattern pour détecter la syntaxe d'image étendue avec dimensions.
* Formats supportés :
Expand Down Expand Up @@ -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() {
Expand All @@ -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='<div class=\"plantuml-diagram\">"
+ "<img src=\"data:image/svg+xml;base64," + b64 + "\""
+ " alt=\"PlantUML diagram\"></div>';";
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='<div class=\"plantuml-diagram\">"
+ "<img src=\"" + url + "\""
+ " alt=\"PlantUML diagram\"></div>';";
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='<div class=\"plantuml-diagram\">"
+ "<img src=\"data:image/svg+xml;base64," + b64 + "\""
+ " alt=\"PlantUML diagram\"></div>';";
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='<div class=\"plantuml-diagram\">"
+ "<img src=\"" + url + "\""
+ " alt=\"PlantUML diagram\"></div>';";
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 <svg}),
Expand Down
90 changes: 90 additions & 0 deletions src/main/resources/html/preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
{{BASE_TAG}}
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="{{HLJS_CSS_PATH}}">
<script src="{{HLJS_JS_PATH}}"></script>
<!-- Mermaid for diagrams -->
<script src="{{MERMAID_JS_PATH}}"></script>
<!-- KaTeX for math equations -->
<link rel="stylesheet" href="{{KATEX_CSS_PATH}}">
<script src="{{KATEX_JS_PATH}}"></script>
<style>
body { font-family: sans-serif; margin: 1em; }
pre { background: {{PRE_BG}}; padding: 0.8em; border-radius: 6px; overflow-x: auto; }
pre code { font-family: 'Source Code Pro', 'Fira Code', 'Consolas', monospace;
font-size: 0.9em; color: {{CODE_FG}}; }
code { font-family: monospace; }
img { max-width: 100%; height: auto; }
/* PlantUML diagrams */
.plantuml-diagram { text-align: center; margin: 1em 0; }
.plantuml-diagram img { max-width: 100%; height: auto; }
/* Mermaid diagrams */
.mermaid { text-align: center; margin: 1em 0; }
/* Tables */
table { border-collapse: collapse; width: auto; margin: 1em 0; }
th, td { border: 1px solid #888; padding: 6px 12px; text-align: left; }
th { background: rgba(128,128,128,0.15); font-weight: bold; }
tr:nth-child(even) { background: rgba(128,128,128,0.06); }
/* Checkboxes (task list) */
input[type="checkbox"] { width: 1.1em; height: 1.1em; margin-right: 0.4em;
vertical-align: middle; cursor: default;
accent-color: #0078d7; }
li:has(input[type="checkbox"]) { list-style: none; margin-left: -1.2em; }
/* Front Matter metadata */
.front-matter { background: rgba(128,128,128,0.08); border: 1px solid rgba(128,128,128,0.25);
border-radius: 6px; padding: 0.6em 1em; margin-bottom: 1.2em;
font-size: 0.9em; color: #555; }
.front-matter h1 { font-size: 1.4em; margin: 0 0 0.3em 0; color: #333; }
.front-matter .fm-field { margin: 0.15em 0; }
.front-matter .fm-label { font-weight: bold; }
.front-matter .fm-tag { display: inline-block; background: rgba(0,120,215,0.12);
border-radius: 3px; padding: 1px 6px; margin: 1px 2px;
font-size: 0.85em; }
.front-matter .fm-draft { color: #d9534f; font-weight: bold; }
.front-matter .fm-link { display: inline-block; background: rgba(0,120,215,0.12);
border-radius: 3px; padding: 1px 6px; margin: 1px 2px;
font-size: 0.85em; text-decoration: none; color: #0078d7; }
.front-matter .fm-link:hover { text-decoration: underline; background: rgba(0,120,215,0.2); }
.front-matter .fm-summary { cursor: pointer; font-weight: bold; font-size: 0.9em;
color: #666; padding: 0.2em 0; }
.front-matter .fm-summary:hover { color: #333; }
/* Copy button on code blocks */
pre { position: relative; }
pre .copy-btn { position: absolute; top: 4px; right: 4px; padding: 2px 8px;
font-size: 0.75em; cursor: pointer; background: rgba(128,128,128,0.2);
border: 1px solid rgba(128,128,128,0.3); border-radius: 4px;
color: inherit; opacity: 0; transition: opacity 0.2s; }
pre:hover .copy-btn { opacity: 1; }
pre .copy-btn:hover { background: rgba(128,128,128,0.35); }
pre .copy-btn.copied { background: rgba(76,175,80,0.3); border-color: rgba(76,175,80,0.5); }
/* GitHub-style Alerts */
.markdown-alert { padding: 0.8em 1em; margin: 1em 0; border-left: 4px solid; border-radius: 6px; }
.markdown-alert-title { display: flex; align-items: center; font-weight: 600; margin-bottom: 0.4em; }
.markdown-alert-title svg { margin-right: 0.5em; }
.markdown-alert p { margin: 0.3em 0; }
.markdown-alert-note { background: rgba(9, 105, 218, 0.1); border-color: #0969da; }
.markdown-alert-note .markdown-alert-title { color: #0969da; }
.markdown-alert-tip { background: rgba(26, 127, 55, 0.1); border-color: #1a7f37; }
.markdown-alert-tip .markdown-alert-title { color: #1a7f37; }
.markdown-alert-important { background: rgba(130, 80, 223, 0.1); border-color: #8250df; }
.markdown-alert-important .markdown-alert-title { color: #8250df; }
.markdown-alert-warning { background: rgba(154, 103, 0, 0.1); border-color: #9a6700; }
.markdown-alert-warning .markdown-alert-title { color: #9a6700; }
.markdown-alert-caution { background: rgba(207, 34, 46, 0.1); border-color: #cf222e; }
.markdown-alert-caution .markdown-alert-title { color: #cf222e; }
/* Content container for incremental updates */
#mn-content { min-height: 1em; }
.mn-block { transition: background-color 0.15s ease-out; }
</style>
</head>
<body><div id="mn-frontmatter">{{FRONT_MATTER}}</div><div id="mn-content">{{CONTENT}}</div>
<script src="{{PREVIEW_JS_PATH}}"></script>
<script>
// Initialize preview features
initializePreview('{{MERMAID_THEME}}');
</script>
</body>
</html>