Wissensdatenbank: Markdown, FTS5-Suche, Sanitizing, UX

- Markdown-Rendering fuer Notizen (fett, kursiv, Ueberschriften, Listen, Code, Links)
- HTML-Sanitizing im Frontend und Backend (XSS-Schutz)
- FTS5 Volltextindex fuer schnelle Suche mit Ranking
- Kategorie-Loeschung zeigt Anzahl betroffener Eintraege

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Server Deploy
2026-03-19 19:38:43 +01:00
Ursprung 5c87254e97
Commit 48c917eb28
6 geänderte Dateien mit 277 neuen und 23 gelöschten Zeilen

Datei anzeigen

@@ -441,6 +441,84 @@
white-space: pre-wrap;
}
/* Markdown Styles in Notizen */
.knowledge-entry-notes h1 {
font-size: 1.2rem;
font-weight: 700;
color: var(--text-primary);
margin: 0.5rem 0 0.25rem 0;
}
.knowledge-entry-notes h2 {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
margin: 0.5rem 0 0.25rem 0;
}
.knowledge-entry-notes h3 {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
margin: 0.4rem 0 0.2rem 0;
}
.knowledge-entry-notes code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: var(--radius-sm);
font-family: 'Courier New', monospace;
font-size: 0.85em;
}
.knowledge-entry-notes pre {
background: var(--bg-tertiary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
overflow-x: auto;
margin: 0.5rem 0;
}
.knowledge-entry-notes pre code {
background: transparent;
padding: 0;
border-radius: 0;
font-size: 0.85rem;
line-height: 1.5;
}
.knowledge-entry-notes ul,
.knowledge-entry-notes ol {
padding-left: 1.5rem;
margin: 0.25rem 0;
}
.knowledge-entry-notes li {
margin: 0.15rem 0;
}
.knowledge-entry-notes a {
color: var(--primary);
text-decoration: underline;
}
.knowledge-entry-notes a:hover {
opacity: 0.8;
}
.knowledge-entry-notes strong {
font-weight: 700;
color: var(--text-primary);
}
.knowledge-entry-notes em {
font-style: italic;
}
.knowledge-entry-notes p {
margin: 0.25rem 0;
}
.knowledge-entry-attachments-info {
display: flex;
align-items: center;

Datei anzeigen

@@ -348,7 +348,7 @@ class KnowledgeManager {
</a>
` : ''}
${hasNotes ? `
<div class="knowledge-entry-notes">${this.escapeHtml(entry.notes)}</div>
<div class="knowledge-entry-notes">${this.renderMarkdown(this.sanitizeHtml(entry.notes))}</div>
` : ''}
${hasAttachments ? `
<div class="knowledge-entry-attachments-info">
@@ -937,7 +937,11 @@ class KnowledgeManager {
const category = this.categories.find(c => c.id === categoryId);
if (!category) return;
const confirmDelete = confirm(`Kategorie "${category.name}" und alle Einträge wirklich löschen?`);
const count = category.entryCount || 0;
const msg = count > 0
? `Kategorie "${category.name}" mit ${count} Einträgen löschen? Dies kann nicht rückgängig gemacht werden.`
: `Kategorie "${category.name}" löschen? Dies kann nicht rückgängig gemacht werden.`;
const confirmDelete = confirm(msg);
if (!confirmDelete) return;
try {
@@ -1239,6 +1243,94 @@ class KnowledgeManager {
}
}
// ==========================================
// MARKDOWN RENDERING
// ==========================================
/**
* Sanitize HTML - entfernt gefaehrliche Tags und Attribute
* Muss VOR renderMarkdown aufgerufen werden
*/
sanitizeHtml(html) {
if (!html) return '';
// Entferne script, iframe, object, embed, form, style Tags
let clean = html.replace(/<\s*(script|iframe|object|embed|form|style|link|meta|base)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, '');
// Entferne selbstschliessende gefaehrliche Tags
clean = clean.replace(/<\s*(script|iframe|object|embed|form|style|link|meta|base)\b[^>]*\/?>/gi, '');
// Entferne on*-Event-Attribute (onclick, onerror, onload etc.)
clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '');
// Entferne javascript: URLs
clean = clean.replace(/href\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'href="#"');
clean = clean.replace(/src\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'src=""');
return clean;
}
/**
* Einfacher Markdown-Renderer
* Unterstuetzt: fett, kursiv, Ueberschriften, Listen, Links, Code, Zeilenumbrueche
*/
renderMarkdown(text) {
if (!text) return '';
// Zuerst HTML-Entities escapen fuer Sicherheit
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Code-Bloecke (dreifache Backticks) - vor anderen Regeln
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, lang, code) => {
return `<pre><code>${code.trim()}</code></pre>`;
});
// Inline-Code (einfache Backticks)
html = html.replace(/`([^`\n]+)`/g, '<code>$1</code>');
// Ueberschriften (# bis ###)
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// Fett (**text** oder __text__)
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
// Kursiv (*text* oder _text_)
html = html.replace(/(?<!\w)\*([^*\n]+)\*(?!\w)/g, '<em>$1</em>');
html = html.replace(/(?<!\w)_([^_\n]+)_(?!\w)/g, '<em>$1</em>');
// Links [text](url)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
// Ungeordnete Listen (- oder * am Zeilenanfang)
html = html.replace(/^(?:- |\* )(.+)$/gm, '<li>$1</li>');
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
// Geordnete Listen (1. am Zeilenanfang)
html = html.replace(/^\d+\. (.+)$/gm, '<oli>$1</oli>');
html = html.replace(/((?:<oli>.*<\/oli>\n?)+)/g, (match) => {
return '<ol>' + match.replace(/<\/?oli>/g, (tag) => tag.replace('oli', 'li')) + '</ol>';
});
// Leere Zeilen als Absatz-Trennung
html = html.replace(/\n\n+/g, '</p><p>');
// Einfache Zeilenumbrueche (ausser in pre/code und nach Block-Elementen)
html = html.replace(/(?<!<\/h[123]>|<\/li>|<\/ul>|<\/ol>|<\/pre>|<\/p>|<p>)\n(?!<h[123]|<li>|<ul>|<ol>|<pre>|<\/p>|<p>)/g, '<br>');
// In Absatz einwickeln
html = '<p>' + html + '</p>';
// Leere Absaetze entfernen
html = html.replace(/<p>\s*<\/p>/g, '');
// Block-Elemente nicht in <p> einwickeln
html = html.replace(/<p>\s*(<(?:h[123]|ul|ol|pre|blockquote)[\s>])/g, '$1');
html = html.replace(/(<\/(?:h[123]|ul|ol|pre|blockquote)>)\s*<\/p>/g, '$1');
return html;
}
// ==========================================
// UTILITIES
// ==========================================

Datei anzeigen

@@ -4,7 +4,7 @@
* Offline support and caching
*/
const CACHE_VERSION = '391';
const CACHE_VERSION = '392';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;