/** * TASKMATE - Knowledge Manager * ============================ * Wiki-Layout: Baum-Navigation + Content-Bereich */ import api from './api.js'; import { $, $$ } from './utils.js'; import store from './store.js'; class KnowledgeManager { constructor() { this.categories = []; this.allEntries = []; this.selectedEntryId = null; this.selectedCategoryId = null; this.expandedCategories = new Set(); this.isEditing = false; this.editingEntryId = null; this.filterQuery = ''; this.initialized = false; this.searchDebounceTimer = null; } async init() { console.log('[Knowledge] init() called, initialized:', this.initialized); if (this.initialized) { await this.loadData(); return; } // DOM Elements - Layout this.knowledgeView = $('#view-knowledge'); this.mobileCategories = $('#knowledge-mobile-categories'); // DOM Elements - Sidebar this.sidebarSearch = $('#knowledge-sidebar-search'); this.newCategoryBtn = $('#btn-new-category'); this.tree = $('#knowledge-tree'); this.treeEmpty = $('#knowledge-tree-empty'); // DOM Elements - Content this.emptyState = $('#knowledge-empty'); this.reader = $('#knowledge-reader'); this.editor = $('#knowledge-editor'); // Reader Elements this.readerTitle = $('#knowledge-reader-title'); this.readerMeta = $('#knowledge-reader-meta'); this.readerUrl = $('#knowledge-reader-url'); this.readerBody = $('#knowledge-reader-body'); this.readerAttachments = $('#knowledge-reader-attachments'); this.editEntryBtn = $('#btn-edit-entry'); this.deleteEntryReaderBtn = $('#btn-delete-entry-reader'); // Editor Elements this.editorForm = $('#knowledge-editor-form'); this.editorId = $('#knowledge-editor-id'); this.editorCategoryId = $('#knowledge-editor-category-id'); this.editorTitle = $('#knowledge-editor-title'); this.editorTitleInput = $('#knowledge-editor-title-input'); this.editorUrlInput = $('#knowledge-editor-url-input'); this.editorNotes = $('#knowledge-editor-notes'); this.editorAttachments = $('#knowledge-editor-attachments'); this.editorAttachmentsList = $('#knowledge-editor-attachments-list'); this.editorFileInput = $('#knowledge-editor-file-input'); this.btnUploadFile = $('#btn-upload-file'); this.btnCancelEditor = $('#btn-cancel-editor'); // Category Modal Elements this.categoryModal = $('#knowledge-category-modal'); this.categoryForm = $('#knowledge-category-form'); this.categoryModalTitle = $('#knowledge-category-modal-title'); this.categoryIdInput = $('#knowledge-category-id'); this.categoryNameInput = $('#knowledge-category-name'); this.categoryDescriptionInput = $('#knowledge-category-description'); this.categoryColorInput = $('#knowledge-category-color'); this.categoryIconInput = $('#knowledge-category-icon'); // Icon Picker Elements this.iconPickerPreview = $('#icon-picker-preview'); this.iconPreviewEmoji = $('#icon-preview-emoji'); this.iconPickerSection = $('#icon-picker-section'); this.bindEvents(); this.initialized = true; console.log('[Knowledge] Initialization complete'); await this.loadData(); } bindEvents() { // Sidebar search this.sidebarSearch?.addEventListener('input', (e) => { this.filterQuery = e.target.value.trim().toLowerCase(); this.renderTree(); }); // New Category Button this.newCategoryBtn?.addEventListener('click', () => { this.openCategoryModal(); }); // Category Form Submit this.categoryForm?.addEventListener('submit', (e) => this.handleCategorySubmit(e)); // Modal close buttons this.categoryModal?.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', () => this.closeCategoryModal()); }); // Icon Picker Events this.bindIconPickerEvents(); // Reader buttons this.editEntryBtn?.addEventListener('click', () => { if (this.selectedEntryId) { this.showEditor(this.selectedEntryId); } }); this.deleteEntryReaderBtn?.addEventListener('click', () => { if (this.selectedEntryId) { this.deleteEntry(this.selectedEntryId); } }); // Editor form this.editorForm?.addEventListener('submit', (e) => { e.preventDefault(); this.saveEntry(); }); this.btnCancelEditor?.addEventListener('click', () => { if (this.editingEntryId) { this.showEntry(this.editingEntryId); } else { this.showEmptyState(); } }); // File upload this.btnUploadFile?.addEventListener('click', () => { this.editorFileInput?.click(); }); this.editorFileInput?.addEventListener('change', (e) => { this.handleFileUpload(e); }); // Store subscription for realtime updates store.subscribe('knowledge', () => { this.loadData(); }); // Socket.io realtime events window.addEventListener('app:refresh', () => { if (this.knowledgeView && !this.knowledgeView.classList.contains('hidden')) { this.loadData(); } }); } // ========================================== // DATA LOADING // ========================================== async loadData() { try { const [categories, entriesResult] = await Promise.all([ api.getKnowledgeCategories(), api.getKnowledgeEntries() ]); this.categories = categories; this.categories.sort((a, b) => (a.position || 0) - (b.position || 0)); this.allEntries = Array.isArray(entriesResult) ? entriesResult : []; this.allEntries.sort((a, b) => (a.position || 0) - (b.position || 0)); this.renderTree(); this.renderMobileCategories(); // Refresh current view if an entry is selected if (this.selectedEntryId && !this.isEditing) { const entry = this.allEntries.find(e => e.id === this.selectedEntryId); if (entry) { this.showEntry(this.selectedEntryId); } else { this.selectedEntryId = null; this.showEmptyState(); } } } catch (error) { console.error('[Knowledge] Error loading data:', error); this.showToast('Fehler beim Laden der Wissensdatenbank', 'error'); } } async loadEntryWithAttachments(entryId) { try { return await api.getKnowledgeEntry(entryId); } catch (error) { console.error('Error loading entry:', error); this.showToast('Fehler beim Laden des Eintrags', 'error'); return null; } } // ========================================== // TREE RENDERING (Sidebar) // ========================================== getFilteredTree() { if (!this.filterQuery) { return this.categories.map(cat => ({ ...cat, entries: this.allEntries.filter(e => e.categoryId === cat.id) })); } const q = this.filterQuery; return this.categories .map(cat => { const matchingEntries = this.allEntries.filter(e => e.categoryId === cat.id && (e.title.toLowerCase().includes(q) || (e.notes && e.notes.toLowerCase().includes(q))) ); const catMatches = cat.name.toLowerCase().includes(q); if (catMatches || matchingEntries.length > 0) { return { ...cat, entries: catMatches ? this.allEntries.filter(e => e.categoryId === cat.id) : matchingEntries }; } return null; }) .filter(Boolean); } renderTree() { if (!this.tree) return; const treeData = this.getFilteredTree(); if (treeData.length === 0) { this.tree.innerHTML = ''; this.treeEmpty?.classList.remove('hidden'); return; } this.treeEmpty?.classList.add('hidden'); // If filtering, auto-expand all categories if (this.filterQuery) { treeData.forEach(cat => this.expandedCategories.add(cat.id)); } this.tree.innerHTML = treeData.map(cat => { const isExpanded = this.expandedCategories.has(cat.id); const entries = cat.entries || []; const icon = cat.icon || '\u{1F4C1}'; return `
${icon} ${this.escapeHtml(cat.name)} ${entries.length}
${entries.map(entry => `
${this.escapeHtml(entry.title)}
`).join('')}
Neuer Eintrag
`; }).join(''); this.bindTreeEvents(); } bindTreeEvents() { // Category headers - toggle expand this.tree.querySelectorAll('.knowledge-tree-category-header').forEach(header => { header.addEventListener('click', (e) => { if (e.target.closest('.knowledge-tree-category-actions')) return; const catId = parseInt(header.dataset.categoryId); this.toggleCategory(catId); }); }); // Category edit buttons this.tree.querySelectorAll('[data-action="edit-category"]').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); this.openCategoryModal(parseInt(btn.dataset.id)); }); }); // Category delete buttons this.tree.querySelectorAll('[data-action="delete-category"]').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); this.handleDeleteCategory(parseInt(btn.dataset.id)); }); }); // Entry items - show entry this.tree.querySelectorAll('.knowledge-tree-entry').forEach(item => { item.addEventListener('click', () => { const entryId = parseInt(item.dataset.entryId); this.showEntry(entryId); }); }); // New entry links this.tree.querySelectorAll('.knowledge-tree-new-entry').forEach(item => { item.addEventListener('click', () => { const categoryId = parseInt(item.dataset.categoryId); this.showEditor(null, categoryId); }); }); } toggleCategory(categoryId) { if (this.expandedCategories.has(categoryId)) { this.expandedCategories.delete(categoryId); } else { this.expandedCategories.add(categoryId); } this.renderTree(); } // ========================================== // MOBILE CATEGORIES // ========================================== renderMobileCategories() { if (!this.mobileCategories) return; const chips = this.categories.map(cat => `
${cat.icon || '\u{1F4C1}'} ${this.escapeHtml(cat.name)} (${cat.entryCount || 0})
`).join(''); const addChip = `
+ Neu
`; this.mobileCategories.innerHTML = chips + addChip; this.bindMobileChipEvents(); } bindMobileChipEvents() { this.mobileCategories?.querySelectorAll('.knowledge-mobile-chip').forEach(chip => { if (chip.dataset.action === 'add-category') { chip.addEventListener('click', () => this.openCategoryModal()); } else { const categoryId = parseInt(chip.dataset.categoryId); chip.addEventListener('click', () => { this.selectedCategoryId = categoryId; this.expandedCategories.add(categoryId); this.renderTree(); this.renderMobileCategories(); }); } }); } // ========================================== // CONTENT STATES // ========================================== showEmptyState() { this.isEditing = false; this.editingEntryId = null; this.selectedEntryId = null; this.emptyState?.classList.remove('hidden'); this.reader?.classList.add('hidden'); this.editor?.classList.add('hidden'); this.renderTree(); } async showEntry(entryId) { this.isEditing = false; this.editingEntryId = null; this.selectedEntryId = entryId; // Load full entry with attachments const entry = await this.loadEntryWithAttachments(entryId); if (!entry) { this.showEmptyState(); return; } // Expand parent category if (entry.categoryId) { this.expandedCategories.add(entry.categoryId); this.selectedCategoryId = entry.categoryId; } // Update content area this.emptyState?.classList.add('hidden'); this.editor?.classList.add('hidden'); this.reader?.classList.remove('hidden'); // Title if (this.readerTitle) { this.readerTitle.textContent = entry.title; } // Meta if (this.readerMeta) { const cat = this.categories.find(c => c.id === entry.categoryId); const catName = cat ? cat.name : 'Unbekannt'; const catIcon = cat ? (cat.icon || '\u{1F4C1}') : '\u{1F4C1}'; const date = entry.updatedAt || entry.createdAt; let dateStr = ''; if (date) { const d = new Date(date); const day = String(d.getDate()).padStart(2, '0'); const month = String(d.getMonth() + 1).padStart(2, '0'); const year = d.getFullYear(); dateStr = `${day}.${month}.${year}`; } const creator = entry.creatorName || entry.creator_name || ''; this.readerMeta.innerHTML = ` ${catIcon} ${this.escapeHtml(catName)} ${creator ? `von ${this.escapeHtml(creator)}` : ''} ${dateStr ? `${dateStr}` : ''} `; } // URL if (this.readerUrl) { if (entry.url && entry.url.trim()) { this.readerUrl.classList.remove('hidden'); this.readerUrl.innerHTML = ` ${this.escapeHtml(entry.url)} `; } else { this.readerUrl.classList.add('hidden'); this.readerUrl.innerHTML = ''; } } // Body (Markdown) if (this.readerBody) { if (entry.notes && entry.notes.trim()) { this.readerBody.innerHTML = this.renderMarkdown(this.sanitizeHtml(entry.notes)); } else { this.readerBody.innerHTML = '

Kein Inhalt vorhanden.

'; } } // Attachments if (this.readerAttachments) { const attachments = entry.attachments || []; if (attachments.length > 0) { this.readerAttachments.classList.remove('hidden'); this.readerAttachments.innerHTML = `

Anhaenge (${attachments.length})

${attachments.map(att => ` ${this.escapeHtml(att.originalName || att.original_name || 'Datei')} ${this.formatFileSize(att.sizeBytes || att.size_bytes || 0)} `).join('')} `; } else { this.readerAttachments.classList.add('hidden'); this.readerAttachments.innerHTML = ''; } } // Update tree to highlight active entry this.renderTree(); this.renderMobileCategories(); } async showEditor(entryId = null, categoryId = null) { this.isEditing = true; this.editingEntryId = entryId; this.emptyState?.classList.add('hidden'); this.reader?.classList.add('hidden'); this.editor?.classList.remove('hidden'); // Reset form this.editorForm?.reset(); if (this.editorId) this.editorId.value = ''; if (this.editorCategoryId) this.editorCategoryId.value = ''; if (entryId) { // Edit existing const entry = await this.loadEntryWithAttachments(entryId); if (!entry) return; if (this.editorTitle) this.editorTitle.textContent = 'Eintrag bearbeiten'; if (this.editorId) this.editorId.value = entry.id; if (this.editorCategoryId) this.editorCategoryId.value = entry.categoryId; if (this.editorTitleInput) this.editorTitleInput.value = entry.title; if (this.editorUrlInput) this.editorUrlInput.value = entry.url || ''; if (this.editorNotes) this.editorNotes.value = entry.notes || ''; // Show attachments this.renderEditorAttachments(entry.attachments || []); } else { // New entry if (this.editorTitle) this.editorTitle.textContent = 'Neuer Eintrag'; if (this.editorCategoryId) this.editorCategoryId.value = categoryId || this.selectedCategoryId || ''; // Hide attachments for new entries if (this.editorAttachments) this.editorAttachments.classList.add('hidden'); } this.editorTitleInput?.focus(); } // ========================================== // ENTRY CRUD // ========================================== async saveEntry() { const entryId = this.editorId?.value ? parseInt(this.editorId.value) : null; const categoryId = parseInt(this.editorCategoryId?.value); const data = { categoryId: categoryId, title: this.editorTitleInput?.value.trim(), url: this.editorUrlInput?.value.trim() || null, notes: this.editorNotes?.value.trim() || null }; if (!data.title) { this.showToast('Bitte einen Titel eingeben', 'error'); return; } if (!data.categoryId) { this.showToast('Keine Kategorie ausgewaehlt', 'error'); return; } try { let savedEntry; if (entryId) { savedEntry = await api.updateKnowledgeEntry(entryId, data); this.showToast('Eintrag aktualisiert', 'success'); } else { savedEntry = await api.createKnowledgeEntry(data); this.showToast('Eintrag erstellt', 'success'); } // Reload data await this.loadData(); // Show the saved entry const id = savedEntry?.id || entryId; if (id) { this.showEntry(id); } else { this.showEmptyState(); } } catch (error) { this.showToast(error.message || 'Fehler beim Speichern', 'error'); } } async deleteEntry(entryId) { const entry = this.allEntries.find(e => e.id === entryId); const title = entry?.title || 'Dieser Eintrag'; const confirmDelete = confirm(`"${title}" wirklich loeschen?`); if (!confirmDelete) return; try { await api.deleteKnowledgeEntry(entryId); this.showToast('Eintrag geloescht', 'success'); this.selectedEntryId = null; await this.loadData(); this.showEmptyState(); } catch (error) { this.showToast(error.message || 'Fehler beim Loeschen', 'error'); } } // ========================================== // EDITOR ATTACHMENTS // ========================================== renderEditorAttachments(attachments) { if (!this.editorAttachments || !this.editorAttachmentsList) return; if (!attachments || attachments.length === 0) { this.editorAttachments.classList.remove('hidden'); this.editorAttachmentsList.innerHTML = '

Keine Anhaenge vorhanden

'; return; } this.editorAttachments.classList.remove('hidden'); this.editorAttachmentsList.innerHTML = attachments.map(att => `
${this.escapeHtml(att.originalName || att.original_name || 'Datei')} ${this.formatFileSize(att.sizeBytes || att.size_bytes || 0)}
`).join(''); // Bind delete events this.editorAttachmentsList.querySelectorAll('[data-action="delete-attachment"]').forEach(btn => { btn.addEventListener('click', () => { this.handleDeleteAttachment(parseInt(btn.dataset.id)); }); }); } async handleFileUpload(e) { const files = e.target.files; if (!files || files.length === 0) return; const entryId = parseInt(this.editorId?.value); if (!entryId) { this.showToast('Speichern Sie zuerst den Eintrag, dann koennen Sie Dateien hochladen', 'info'); e.target.value = ''; return; } for (const file of files) { try { await api.uploadKnowledgeAttachment(entryId, file); this.showToast(`${file.name} hochgeladen`, 'success'); } catch (error) { this.showToast(`Fehler beim Hochladen von ${file.name}`, 'error'); } } e.target.value = ''; // Reload entry attachments const entry = await this.loadEntryWithAttachments(entryId); if (entry) { this.renderEditorAttachments(entry.attachments); } } async handleDeleteAttachment(attachmentId) { const confirmDelete = confirm('Anhang wirklich loeschen?'); if (!confirmDelete) return; try { await api.deleteKnowledgeAttachment(attachmentId); this.showToast('Anhang geloescht', 'success'); // Reload entry attachments const entryId = parseInt(this.editorId?.value); if (entryId) { const entry = await this.loadEntryWithAttachments(entryId); if (entry) { this.renderEditorAttachments(entry.attachments); } } } catch (error) { this.showToast(error.message || 'Fehler beim Loeschen', 'error'); } } // ========================================== // SEARCH (Header search integration) // ========================================== setSearchQuery(query) { clearTimeout(this.searchDebounceTimer); if (!query || !query.trim()) { this.filterQuery = ''; if (this.sidebarSearch) this.sidebarSearch.value = ''; this.renderTree(); return; } this.searchDebounceTimer = setTimeout(() => { this.filterQuery = query.trim().toLowerCase(); if (this.sidebarSearch) this.sidebarSearch.value = query.trim(); this.renderTree(); }, 300); } // ========================================== // CATEGORY CRUD // ========================================== openCategoryModal(categoryId = null) { const isEdit = !!categoryId; this.categoryModalTitle.textContent = isEdit ? 'Kategorie bearbeiten' : 'Neue Kategorie'; this.categoryForm?.reset(); // Hide icon picker section this.iconPickerSection?.classList.add('hidden'); if (isEdit) { const category = this.categories.find(c => c.id === categoryId); if (category) { this.categoryIdInput.value = category.id; this.categoryNameInput.value = category.name; this.categoryDescriptionInput.value = category.description || ''; this.categoryColorInput.value = category.color || '#3B82F6'; const icon = category.icon || '\u{1F4C1}'; this.categoryIconInput.value = icon; if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = icon; } } } else { this.categoryIdInput.value = ''; this.categoryColorInput.value = '#3B82F6'; this.categoryIconInput.value = '\u{1F4C1}'; if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = '\u{1F4C1}'; } } this.openModal(this.categoryModal, 'knowledge-category-modal'); this.categoryNameInput?.focus(); } closeCategoryModal() { this.closeModal(this.categoryModal, 'knowledge-category-modal'); } async handleCategorySubmit(e) { e.preventDefault(); const categoryId = this.categoryIdInput?.value ? parseInt(this.categoryIdInput.value) : null; const data = { name: this.categoryNameInput?.value.trim(), description: this.categoryDescriptionInput?.value.trim() || null, color: this.categoryColorInput?.value || '#3B82F6', icon: this.categoryIconInput?.value.trim() || null }; if (!data.name) { this.showToast('Bitte einen Namen eingeben', 'error'); return; } try { if (categoryId) { await api.updateKnowledgeCategory(categoryId, data); this.showToast('Kategorie aktualisiert', 'success'); } else { await api.createKnowledgeCategory(data); this.showToast('Kategorie erstellt', 'success'); } this.closeCategoryModal(); await this.loadData(); } catch (error) { this.showToast(error.message || 'Fehler beim Speichern', 'error'); } } async handleDeleteCategory(categoryId) { const category = this.categories.find(c => c.id === categoryId); if (!category) return; const count = category.entryCount || 0; const msg = count > 0 ? `Kategorie "${category.name}" mit ${count} Eintraegen loeschen? Dies kann nicht rueckgaengig gemacht werden.` : `Kategorie "${category.name}" loeschen? Dies kann nicht rueckgaengig gemacht werden.`; const confirmDelete = confirm(msg); if (!confirmDelete) return; try { await api.deleteKnowledgeCategory(categoryId); this.showToast('Kategorie geloescht', 'success'); // If any entry from this category was selected, clear if (this.selectedCategoryId === categoryId) { this.selectedCategoryId = null; this.selectedEntryId = null; this.showEmptyState(); } await this.loadData(); } catch (error) { this.showToast(error.message || 'Fehler beim Loeschen', 'error'); } } // ========================================== // ICON PICKER // ========================================== bindIconPickerEvents() { if (!this.iconPickerPreview || !this.iconPickerSection) return; this.iconPickerPreview.addEventListener('click', () => { this.iconPickerSection.classList.toggle('hidden'); }); const tabs = this.iconPickerSection.querySelectorAll('.icon-tab'); tabs.forEach(tab => { tab.addEventListener('click', () => { this.switchIconTab(tab.dataset.tab); }); }); const iconBtns = this.iconPickerSection.querySelectorAll('.icon-btn'); iconBtns.forEach(btn => { btn.addEventListener('click', () => { this.selectIcon(btn.dataset.icon); }); }); } switchIconTab(tabName) { if (!this.iconPickerSection) return; const tabs = this.iconPickerSection.querySelectorAll('.icon-tab'); tabs.forEach(tab => { tab.classList.toggle('active', tab.dataset.tab === tabName); }); const grids = this.iconPickerSection.querySelectorAll('.icon-grid'); grids.forEach(grid => { grid.classList.toggle('hidden', grid.dataset.tabContent !== tabName); }); } selectIcon(icon) { if (!icon) return; if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = icon; } if (this.categoryIconInput) { this.categoryIconInput.value = icon; } if (this.iconPickerSection) { this.iconPickerSection.classList.add('hidden'); } } // ========================================== // MODAL HELPERS // ========================================== openModal(modal, modalId) { if (modal) { modal.classList.remove('hidden'); modal.classList.add('visible'); } const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('hidden'); overlay.classList.add('visible'); } store.openModal(modalId); } closeModal(modal, modalId) { if (modal) { modal.classList.remove('visible'); modal.classList.add('hidden'); } const openModals = store.get('openModals').filter(id => id !== modalId); if (openModals.length === 0) { const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('visible'); overlay.classList.add('hidden'); } } store.closeModal(modalId); } // ========================================== // MARKDOWN RENDERING // ========================================== sanitizeHtml(html) { if (!html) return ''; let clean = html.replace(/<\s*(script|iframe|object|embed|form|style|link|meta|base)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, ''); clean = clean.replace(/<\s*(script|iframe|object|embed|form|style|link|meta|base)\b[^>]*\/?>/gi, ''); clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, ''); clean = clean.replace(/href\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'href="#"'); clean = clean.replace(/src\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'src=""'); return clean; } renderMarkdown(text) { if (!text) return ''; let html = text .replace(/&/g, '&') .replace(//g, '>'); // Code blocks html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, lang, code) => { return `
${code.trim()}
`; }); // Inline code html = html.replace(/`([^`\n]+)`/g, '$1'); // Headings html = html.replace(/^### (.+)$/gm, '

$1

'); html = html.replace(/^## (.+)$/gm, '

$1

'); html = html.replace(/^# (.+)$/gm, '

$1

'); // Bold html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/__(.+?)__/g, '$1'); // Italic html = html.replace(/(?$1'); html = html.replace(/(?$1'); // Links html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Unordered lists html = html.replace(/^(?:- |\* )(.+)$/gm, '
  • $1
  • '); html = html.replace(/((?:
  • .*<\/li>\n?)+)/g, ''); // Ordered lists html = html.replace(/^\d+\. (.+)$/gm, '$1'); html = html.replace(/((?:.*<\/oli>\n?)+)/g, (match) => { return '
      ' + match.replace(/<\/?oli>/g, (tag) => tag.replace('oli', 'li')) + '
    '; }); // Paragraphs html = html.replace(/\n\n+/g, '

    '); // Line breaks html = html.replace(/(?|<\/li>|<\/ul>|<\/ol>|<\/pre>|<\/p>|

    )\n(?!|