diff --git a/handlers/chat.go b/handlers/chat.go
index 5f2bda3..6abfae4 100644
--- a/handlers/chat.go
+++ b/handlers/chat.go
@@ -223,12 +223,21 @@ func ChatHandler(app *App) http.HandlerFunc {
}
allUsernamesJSON, _ := json.Marshal(allUsernames)
+ // Get user preferences for markdown preview default
+ prefs, err := models.GetUserPreferences(app.DB, userID)
+ if err != nil {
+ log.Printf("Error fetching user preferences: %v", err)
+ // Create default if not found
+ prefs, _ = models.CreateDefaultPreferences(app.DB, userID)
+ }
+
data := struct {
PageData
- Board models.Board
- Messages []models.ChatMessage
- AllUsernames template.JS
- CurrentUsername string
+ Board models.Board
+ Messages []models.ChatMessage
+ AllUsernames template.JS
+ CurrentUsername string
+ MarkdownPreviewDefault string
}{
PageData: PageData{
Title: "ThreadR Chat - " + board.Name,
@@ -239,10 +248,11 @@ func ChatHandler(app *App) http.HandlerFunc {
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.Path,
},
- Board: *board,
- Messages: messages,
- AllUsernames: template.JS(allUsernamesJSON),
- CurrentUsername: currentUsername,
+ Board: *board,
+ Messages: messages,
+ AllUsernames: template.JS(allUsernamesJSON),
+ CurrentUsername: currentUsername,
+ MarkdownPreviewDefault: prefs.MarkdownPreviewDefault,
}
if err := app.Tmpl.ExecuteTemplate(w, "chat", data); err != nil {
log.Printf("Error executing template in ChatHandler: %v", err)
diff --git a/static/app.js b/static/app.js
index 3f374b0..aefdf51 100644
--- a/static/app.js
+++ b/static/app.js
@@ -371,3 +371,118 @@ function showDraftIndicator(content, timestamp, onRestore, onDiscard) {
return indicator;
}
+
+// ============================================
+// Markdown Preview Functions
+// ============================================
+
+// Escape HTML to prevent XSS
+function escapeHTML(str) {
+ const div = document.createElement('div');
+ div.textContent = str;
+ return div.innerHTML;
+}
+
+// Process inline markdown (bold, italic, code, mentions)
+function processInlineMarkdown(line) {
+ // Inline code first (to avoid processing markdown inside code)
+ line = line.replace(/`([^`]+)`/g, '$1');
+
+ // Bold: **text** or __text__
+ line = line.replace(/\*\*([^\*]+)\*\*/g, '$1');
+ line = line.replace(/__([^_]+)__/g, '$1');
+
+ // Italic: *text* or _text_
+ line = line.replace(/\*([^\*]+)\*/g, '$1');
+ line = line.replace(/_([^_]+)_/g, '$1');
+
+ // Mentions: @username
+ line = line.replace(/@(\w+)/g, '');
+
+ return line;
+}
+
+// Render markdown to HTML (matching Go implementation)
+function renderMarkdownPreview(content) {
+ let html = '';
+
+ // Extract code blocks first and replace with placeholders
+ const codeBlocks = [];
+ let codeBlockCounter = 0;
+
+ content = content.replace(/```(\w*)\n([\s\S]*?)\n```/g, (match, lang, code) => {
+ const escapedCode = escapeHTML(code);
+ let renderedBlock;
+ if (lang) {
+ renderedBlock = `
${escapedCode}`;
+ } else {
+ renderedBlock = `${escapedCode}`;
+ }
+ const placeholder = ``;
+ codeBlocks[codeBlockCounter] = renderedBlock;
+ codeBlockCounter++;
+ return placeholder;
+ });
+
+ // Process lines
+ const lines = content.split('\n');
+ let inList = false;
+
+ for (const line of lines) {
+ const trimmedLine = line.trim();
+
+ // Headers
+ if (trimmedLine.startsWith('### ')) {
+ if (inList) { html += '\n'; inList = false; }
+ html += '' + processInlineMarkdown(trimmedLine) + '
\n'; + } else { + html += '\n'; + } + } + + // Close list if still open + if (inList) { + html += '\n'; + } + + // Replace code block placeholders + codeBlocks.forEach((block, index) => { + html = html.replace(``, block); + }); + + // Clean up excessive newlines + html = html.replace(/\n{3,}/g, '\n\n'); + + return html; +} diff --git a/templates/pages/chat.html b/templates/pages/chat.html index 2adba73..28eff8b 100644 --- a/templates/pages/chat.html +++ b/templates/pages/chat.html @@ -247,6 +247,99 @@ padding: 6px 12px; font-size: 0.9em; } + .markdown-tabs { + display: flex; + gap: 0; + margin-bottom: 0; + border-bottom: 1px solid #001858; + } + .markdown-tab { + flex: 1; + padding: 8px 16px; + border: none; + background-color: #f3d2c1; + color: #001858; + font-family: monospace; + font-size: 0.9em; + cursor: pointer; + border-right: 1px solid #001858; + transition: background-color 0.2s ease; + } + .markdown-tab:last-child { + border-right: none; + } + .markdown-tab:hover { + background-color: #fef6e4; + } + .markdown-tab.active { + background-color: #8bd3dd; + color: #001858; + font-weight: bold; + } + .markdown-content-container { + position: relative; + min-height: 50px; + } + .chat-input textarea, + .markdown-preview { + display: none; + resize: none; + height: 50px; + margin-bottom: 8px; + font-size: 0.9em; + width: 100%; + box-sizing: border-box; + } + .chat-input textarea.markdown-visible, + .markdown-preview.markdown-visible { + display: block; + } + .markdown-preview { + border: 1px solid #001858; + border-radius: 5px; + padding: 8px; + background-color: #fef6e4; + color: #001858; + min-height: 50px; + max-height: 200px; + overflow-y: auto; + font-family: monospace; + } + .markdown-preview h1 { + font-size: 1.5em; + margin: 0.5em 0; + } + .markdown-preview h2 { + font-size: 1.3em; + margin: 0.5em 0; + } + .markdown-preview h3 { + font-size: 1.1em; + margin: 0.5em 0; + } + .markdown-preview p { + margin: 0.5em 0; + } + .markdown-preview ul { + margin: 0.5em 0; + padding-left: 2em; + } + .markdown-preview code { + background-color: #f3d2c1; + padding: 2px 4px; + border-radius: 3px; + } + .markdown-preview pre { + background-color: #f3d2c1; + padding: 8px; + border-radius: 5px; + overflow-x: auto; + margin: 0.5em 0; + } + .markdown-preview pre code { + background-color: transparent; + padding: 0; + } .post-actions { position: absolute; top: 5px; @@ -410,6 +503,29 @@ border-color: #f582ae; } } + .markdown-tab { + background-color: #555; + color: #fef6e4; + border-color: #fef6e4; + } + .markdown-tab:hover { + background-color: #666; + } + .markdown-tab.active { + background-color: #8bd3dd; + color: #001858; + } + .markdown-preview { + background-color: #333; + color: #fef6e4; + border-color: #fef6e4; + } + .markdown-preview code { + background-color: #555; + } + .markdown-preview pre { + background-color: #555; + } } @@ -466,7 +582,14 @@ - +Nothing to preview. Type some markdown in the Edit tab.
'; + } else { + preview.innerHTML = renderMarkdownPreview(content); + } + } + + // Update preview on input (debounced) + document.getElementById('chat-input-text').addEventListener('input', () => { + if (currentTab === 'preview') { + clearTimeout(previewUpdateTimeout); + previewUpdateTimeout = setTimeout(updatePreview, 300); + } + }); + window.onload = function() { connectWebSocket(); const messagesContainer = document.getElementById('chat-messages'); @@ -906,6 +1080,9 @@ originalSendMessage(); }; + // Initialize markdown preview tab based on user preference + switchMarkdownTab(currentTab); + // Initial check for scroll position checkScrollPosition(); };