Info-Menü im Tray ergaenzen, Sprunganker im Benutzerhandbuch beheben

Tray-Menü bekommt einen "Info"-Untermenüpunkt (Benutzerhandbuch/
Disclaimer/Lizenz), analog zum Menü in der Titelleiste. Markdown-
Überschriften erhalten jetzt stabile id-Attribute (marked vergibt seit
Version 5 keine eigenen mehr), damit das neue Inhaltsverzeichnis im
Benutzerhandbuch als Sprunganker funktioniert. Reine Sprunganker im
Info-Fenster navigieren nicht mehr wie externe Links. Nach jedem
Abschnitt verlinkt "[nach oben]" zurück zum Inhaltsverzeichnis.

Claude-Session: https://claude.ai/code/session_01VeqoaAZDVSCU4QBtiYDC5C
This commit is contained in:
Winkler, Stefan committed 2026-09-01 12:17:02 +02:00
1 parent f8a286f219
commit fff2f288cb
2 files changed
+85 -3

No files matched your search

+47 -1
View File
@@ -272,6 +272,14 @@ function buildTrayMenu() {
},
{ type: 'separator' },
{ label: 'Einstellungen …', click: openSettings },
{
label: 'Info',
submenu: [
{ label: 'Benutzerhandbuch', click: () => openInfoDoc('manual') },
{ label: 'Disclaimer', click: () => openInfoDoc('disclaimer') },
{ label: 'Lizenz', click: () => openInfoDoc('license') },
],
},
{ label: 'Datenordner öffnen', click: () => shell.openPath(USER_DATA_DIR) },
{ type: 'separator' },
{ label: 'Beenden', click: () => app.quit() },
@@ -396,6 +404,39 @@ function escapeHtml(text) {
.replace(/>/g, '>');
}
/** GitHub-ähnlicher Anker-Slug — reicht für die deutschen Überschriften hier
* (Umlaute bleiben erhalten, nur Satzzeichen fallen weg). */
function slugify(text) {
return text
.toLowerCase()
.trim()
.replace(/[^\p{L}\p{N}\s-]/gu, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
}
/**
* Markdown→HTML mit `id`-Attributen auf jeder Überschrift, damit
* Sprunganker (z. B. aus einem Inhaltsverzeichnis) funktionieren — marked
* vergibt seit Version 5 selbst keine Heading-IDs mehr (früher `headerIds`,
* jetzt eine separate Erweiterung, die wir nicht zusätzlich einbinden).
* Ein frischer Renderer pro Aufruf hält die Dopplungs-Zählung dokumentweise
* sauber getrennt.
*/
function renderMarkdown(raw) {
const renderer = new marked.Renderer();
const seen = new Map();
renderer.heading = function ({ tokens, depth, text }) {
const inline = this.parser.parseInline(tokens);
let slug = slugify(text) || 'abschnitt';
const count = seen.get(slug) || 0;
seen.set(slug, count + 1);
if (count > 0) slug = `${slug}-${count}`;
return `<h${depth} id="${slug}">${inline}</h${depth}>\n`;
};
return marked.parse(raw, { renderer });
}
/**
* Behandelt einen Link-Klick im Info-Dialog. Ohne dieses Abfangen navigiert
* Electron das Fenster selbst zum verlinkten Ziel — bei einem relativen Link
@@ -434,7 +475,7 @@ function openInfoDoc(kind) {
let html;
try {
const raw = fs.readFileSync(doc.file, 'utf8');
html = marked.parse(raw);
html = renderMarkdown(raw);
} catch (err) {
html = `<p>Datei konnte nicht gelesen werden: ${escapeHtml(err.message)}</p>`;
}
@@ -480,6 +521,11 @@ function openInfoDoc(kind) {
// lassen (siehe handleInfoLink()) — sonst bleibt ein leeres, unschließbares
// Fenster zurück.
infoWin.webContents.on('will-navigate', (event, url) => {
// Reiner Sprunganker innerhalb desselben Dokuments (z. B. aus dem
// Inhaltsverzeichnis) — Standardverhalten (Scrollen zum Anker) zulassen,
// statt es wie eine echte Navigation zu behandeln.
const currentBase = infoWin.webContents.getURL().split('#')[0];
if (url.split('#')[0] === currentBase) return;
event.preventDefault();
handleInfoLink(url);
});