diff --git a/handlers/chat.go b/handlers/chat.go
index 954efc5..e9210a9 100644
--- a/handlers/chat.go
+++ b/handlers/chat.go
@@ -82,6 +82,12 @@ func init() {
go hub.Run()
}
+type IncomingChatMessage struct {
+ Type string `json:"type"`
+ Content string `json:"content"`
+ ReplyTo int `json:"replyTo"`
+}
+
func ChatHandler(app *App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*sessions.Session)
@@ -158,32 +164,26 @@ func ChatHandler(app *App) http.HandlerFunc {
log.Printf("Error reading WebSocket message: %v", err)
break
}
- var chatMsg struct {
- Type string `json:"type"`
- Content string `json:"content"`
- ReplyTo int `json:"replyTo"`
- }
+ var chatMsg IncomingChatMessage
if err := json.Unmarshal(msg, &chatMsg); err != nil {
log.Printf("Error unmarshaling message: %v", err)
continue
}
if chatMsg.Type == "message" {
- msgObj := models.ChatMessage{
- BoardID: boardID,
- UserID: userID,
- Content: chatMsg.Content,
- ReplyTo: chatMsg.ReplyTo,
- }
- if err := models.CreateChatMessage(app.DB, msgObj); err != nil {
+ if err := models.CreateChatMessage(app.DB, boardID, userID, chatMsg.Content, chatMsg.ReplyTo); err != nil {
log.Printf("Error saving chat message: %v", err)
continue
}
var msgID int
- app.DB.QueryRow("SELECT LAST_INSERT_ID()").Scan(&msgID)
+ err = app.DB.QueryRow("SELECT LAST_INSERT_ID()").Scan(&msgID)
+ if err != nil {
+ log.Printf("Error getting last insert id: %v", err)
+ continue
+ }
savedMsg, err := models.GetChatMessageByID(app.DB, msgID)
if err != nil {
- log.Printf("Error fetching saved message: %v", err)
+ log.Printf("Error fetching saved message for broadcast: %v", err)
continue
}
hub.broadcast <- *savedMsg
diff --git a/models/chat.go b/models/chat.go
index 8bcdb8f..97301dc 100644
--- a/models/chat.go
+++ b/models/chat.go
@@ -2,7 +2,11 @@ package models
import (
"database/sql"
+ "fmt"
+ "html"
+ "html/template"
"regexp"
+ "strings"
"time"
)
@@ -10,7 +14,7 @@ type ChatMessage struct {
ID int `json:"id"`
BoardID int `json:"boardId"`
UserID int `json:"userId"`
- Content string `json:"content"`
+ Content template.HTML `json:"content"`
ReplyTo int `json:"replyTo"`
Timestamp time.Time `json:"timestamp"`
Username string `json:"username"`
@@ -18,9 +22,9 @@ type ChatMessage struct {
Mentions []string `json:"mentions"`
}
-func CreateChatMessage(db *sql.DB, msg ChatMessage) error {
+func CreateChatMessage(db *sql.DB, boardID, userID int, rawContent string, replyTo int) error {
query := "INSERT INTO chat_messages (board_id, user_id, content, reply_to, timestamp) VALUES (?, ?, ?, ?, NOW())"
- _, err := db.Exec(query, msg.BoardID, msg.UserID, msg.Content, msg.ReplyTo)
+ _, err := db.Exec(query, boardID, userID, rawContent, replyTo)
return err
}
@@ -42,7 +46,8 @@ func GetRecentChatMessages(db *sql.DB, boardID int, limit int) ([]ChatMessage, e
for rows.Next() {
var msg ChatMessage
var timestampStr string
- err := rows.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID)
+ var rawContent string
+ err := rows.Scan(&msg.ID, &msg.UserID, &rawContent, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID)
if err != nil {
return nil, err
}
@@ -51,7 +56,8 @@ func GetRecentChatMessages(db *sql.DB, boardID int, limit int) ([]ChatMessage, e
if err != nil {
msg.Timestamp = time.Time{}
}
- msg.Mentions = extractMentions(msg.Content)
+ msg.Content = renderMarkdown(rawContent)
+ msg.Mentions = extractMentions(rawContent)
messages = append(messages, msg)
}
return messages, nil
@@ -66,7 +72,8 @@ func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) {
row := db.QueryRow(query, id)
var msg ChatMessage
var timestampStr string
- err := row.Scan(&msg.ID, &msg.BoardID, &msg.UserID, &msg.Content, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID)
+ var rawContent string
+ err := row.Scan(&msg.ID, &msg.BoardID, &msg.UserID, &rawContent, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID)
if err == sql.ErrNoRows {
return nil, nil
}
@@ -77,11 +84,11 @@ func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) {
if err != nil {
msg.Timestamp = time.Time{}
}
- msg.Mentions = extractMentions(msg.Content)
+ msg.Content = renderMarkdown(rawContent)
+ msg.Mentions = extractMentions(rawContent)
return &msg, nil
}
-// Simple utility to extract mentions from content
func extractMentions(content string) []string {
re := regexp.MustCompile(`@(\w+)`)
matches := re.FindAllStringSubmatch(content, -1)
@@ -90,4 +97,109 @@ func extractMentions(content string) []string {
mentions[i] = match[1]
}
return mentions
+}
+
+func processInlineMarkdown(line string) string {
+ line = regexp.MustCompile("`([^`]+)`").ReplaceAllString(line, "$1
")
+ line = regexp.MustCompile(`\*\*([^\*]+)\*\*`).ReplaceAllString(line, "$1")
+ line = regexp.MustCompile(`__([^_]+)__`).ReplaceAllString(line, "$1")
+ line = regexp.MustCompile(`\*([^\*]+)\*`).ReplaceAllString(line, "$1")
+ line = regexp.MustCompile(`_([^_]+)_`).ReplaceAllString(line, "$1")
+ line = regexp.MustCompile(`@(\w+)`).ReplaceAllString(line, ` `)
+ return line
+}
+
+func renderMarkdown(content string) template.HTML {
+ var sb strings.Builder
+
+ codeBlockRegex := regexp.MustCompile("(?s)```(\\w*)\n(.*?)\n```")
+ codeBlocks := make(map[string]string)
+ codeBlockCounter := 0
+
+ contentWithoutCodeBlocks := codeBlockRegex.ReplaceAllStringFunc(content, func(match string) string {
+ submatches := codeBlockRegex.FindStringSubmatch(match)
+
+ lang := submatches[1]
+ codeContent := submatches[2]
+
+ escapedCode := html.EscapeString(codeContent)
+ var renderedCodeBlock string
+ if lang != "" {
+ renderedCodeBlock = fmt.Sprintf("
%s
", lang, escapedCode)
+ } else {
+ renderedCodeBlock = fmt.Sprintf("%s
", escapedCode)
+ }
+
+ placeholder := fmt.Sprintf("", codeBlockCounter)
+ codeBlocks[placeholder] = renderedCodeBlock
+ codeBlockCounter++
+ return placeholder
+ })
+
+ lines := strings.Split(contentWithoutCodeBlocks, "\n")
+
+ inList := false
+
+ for _, line := range lines {
+ trimmedLine := strings.TrimSpace(line)
+
+ if strings.HasPrefix(trimmedLine, "### ") {
+ if inList { sb.WriteString("\n"); inList = false }
+ sb.WriteString("") + sb.WriteString(processInlineMarkdown(trimmedLine)) + sb.WriteString("
\n") + } else { + sb.WriteString("\n") + } + } + + if inList { + sb.WriteString("\n") + } + + finalContent := sb.String() + + for placeholder, blockHTML := range codeBlocks { + finalContent = strings.ReplaceAll(finalContent, placeholder, blockHTML) + } + + finalContent = regexp.MustCompile(`\n{3,}`).ReplaceAllString(finalContent, "\n\n") + + return template.HTML(finalContent) } \ No newline at end of file diff --git a/templates/pages/chat.html b/templates/pages/chat.html index 938e2c9..e1a44f7 100644 --- a/templates/pages/chat.html +++ b/templates/pages/chat.html @@ -275,9 +275,9 @@ {{if gt .ReplyTo 0}} {{end}} - + {{end}} @@ -299,7 +299,7 @@ let autocompleteActive = false; let replyToId = -1; const allUsernames = {{.AllUsernames}}; - const currentUsername = "{{.CurrentUsername}}"; // Get current user's username + const currentUsername = "{{.CurrentUsername}}"; function connectWebSocket() { const boardID = {{.Board.ID}}; @@ -349,7 +349,6 @@ pfpHTML = ` `; } let replyHTML = msg.replyTo > 0 ? ` ` : ''; - let content = msg.content.replace(/@(\w+)/g, ' '); msgDiv.innerHTML = ` ${replyHTML} - + @@ -433,12 +432,9 @@ function getCaretCoordinates(element, position) { const mirrorDivId = 'input-mirror-div'; - let div = document.getElementById(mirrorDivId); - if (!div) { - div = document.createElement('div'); - div.id = mirrorDivId; - document.body.appendChild(div); - } + let div = document.createElement('div'); + div.id = mirrorDivId; + document.body.appendChild(div); const style = window.getComputedStyle(element); const properties = ['border', 'boxSizing', 'fontFamily', 'fontSize', 'fontWeight', 'letterSpacing', 'lineHeight', 'padding', 'textDecoration', 'textIndent', 'textTransform', 'whiteSpace', 'wordSpacing', 'wordWrap', 'width']; properties.forEach(prop => { div.style[prop] = style[prop]; }); @@ -451,7 +447,9 @@ span.textContent = element.value.substring(position) || '.'; div.appendChild(span); - return { top: span.offsetTop, left: span.offsetLeft }; + const coords = { top: span.offsetTop, left: span.offsetLeft }; + document.body.removeChild(div); + return coords; } document.getElementById('chat-input-text').addEventListener('input', (e) => { @@ -499,14 +497,6 @@ window.onload = function() { connectWebSocket(); - - // This part is now handled by the server-side rendering with the `chat-message-highlighted` class - // document.querySelectorAll('.chat-message-content').forEach(function(el) { - // const text = el.innerHTML; - // const newHTML = text.replace(/@(\w+)/g, ' '); - // el.innerHTML = newHTML; - // }); - document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight; };