From e3146068bc409e449375539a13861a182905e9d5 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Sun, 24 Aug 2025 21:28:31 -0500 Subject: [PATCH] largely finish forum implementation --- assets/dk.css | 48 ++++- data/dk.db | Bin 73728 -> 73728 bytes internal/helpers/pagination.go | 27 +++ internal/routes/forum.go | 360 ++++++++++++++++++++------------- internal/template/template.go | 289 +++++++++++++++++--------- templates/forum/index.html | 16 +- templates/forum/reply.html | 18 -- templates/forum/thread.html | 64 +++--- 8 files changed, 521 insertions(+), 301 deletions(-) create mode 100644 internal/helpers/pagination.go delete mode 100644 templates/forum/reply.html diff --git a/assets/dk.css b/assets/dk.css index 21d7ed5..91fc988 100644 --- a/assets/dk.css +++ b/assets/dk.css @@ -285,7 +285,8 @@ form.standard { padding: 0.25rem 0.25rem; background-color: rgba(0, 0, 0, 0.4); color: white; - min-height: 200px; + min-height: 100px; + resize: vertical; &:hover { background-color: rgba(0, 0, 0, 0.5); @@ -530,4 +531,49 @@ div.thread-title { display: flex; align-items: center; justify-content: space-between; +} + +div.forum-post { + display: grid; + grid-template-columns: 3fr 1fr; + background-color: rgba(0, 0, 0, 0.1); + + &:not(:first-of-type) { + margin-top: 0.5rem; + } + + &.original-post { + background-color: rgba(0, 0, 0, 0.15); + border: 1px solid rgba(0, 0, 0, 0.5); + } + + & > div.left { + padding: 0.5rem; + } + + & > div.right { + padding: 0.5rem; + border-left: 1px solid rgba(0, 0, 0, 0.2); + } + + div.timestamp { + color: rgba(0, 0, 0, 0.6); + font-size: 0.8rem; + margin-bottom: 0.25rem; + } + + div.user-class { + color: rgba(0, 0, 0, 0.6); + font-size: 0.8rem; + } +} + +div.bottom-reply { + margin-top: 1rem; + padding-top: 1rem; + border-top: 2px solid black; +} + +.mt-1 { + margin-top: 1rem; } \ No newline at end of file diff --git a/data/dk.db b/data/dk.db index 79b4a309c55fb301a55c7fac5b83107041cf131e..727455e604ceee3bfc2cf36c1146e7221b854b28 100644 GIT binary patch delta 1619 zcmaKs%TE(g6vq3+d31Ueh4K(PrBcHfKwcq17hv(RKuy4)7!#eo7(!)ArzJ>2hDk^a zi7QC_128crjYeeUMu-a*?$o7{xIx^RK-hcEG&A0oN^`!OCinM!_uexzcX7+JxaE0Q zCDvsfA@bqAa&HJg7Tt}b*u8xpx?2YE==i9vpl z2LHbQvwzJ$@4xMD7WRct!io?Rx&)W+t8dMhTV_FzzJ?^0cE$$*tkDUQJ7{mLh;K9)w`cQQ2!TsnM zRo%6g=Q8j;D9U4|4_zM|EmgSDG{IxUzU`psqZaqne#U}&m4Y#kD(S!3i-abpioHq6 z?Wbu2G}`#rXox(@SJV6~@U!$Tj`F8;+K>lB{&>Sln%98Wj^_!@&j3GD%Ik27k`w!B zLk0|)QbRc}(EK#;)5r5Znx9hif-!Z(fUVW!q9oojv_beJsv?gbiKE#GU?={|Mp%>8 z;f;Y8*hrx&!V$&_MJ|D=%$=aQIB@aGToujfF#ux~!2z!YjstTEWpm-MIIiYJIhz!< zOfo(xrv*;nq|xGkQIe7gNs_`&Gl6jiT9ntj@Nkw68qJFhng#vE(&a?E+gb;|iYAo3 zPVoj5rIe~lkQ$S-lGN(5_=`$m_z}wHA(*aakA-4v2xX&T@g`iaE;aOi;MGF4G3;z} z)EIYK83Ar%hGm1RS2tf=D7+cDHkV1O*~#e%&iuIxF^Ni8m^Oe{_*PK2UO4$EpM bPB_id*w|>bxpZGLBlAjG`^~rat0n*d@t-a} diff --git a/internal/helpers/pagination.go b/internal/helpers/pagination.go new file mode 100644 index 0000000..ec189eb --- /dev/null +++ b/internal/helpers/pagination.go @@ -0,0 +1,27 @@ +package helpers + +type Pagination struct { + Page int + PerPage int + Total int +} + +func (p Pagination) Offset() int { + return (p.Page - 1) * p.PerPage +} + +func (p Pagination) TotalPages() int { + pages := (p.Total + p.PerPage - 1) / p.PerPage + if pages < 1 { + return 1 + } + return pages +} + +func (p Pagination) HasNext() bool { + return p.Page < p.TotalPages() +} + +func (p Pagination) HasPrev() bool { + return p.Page > 1 +} diff --git a/internal/routes/forum.go b/internal/routes/forum.go index 7406c4e..0ab1d86 100644 --- a/internal/routes/forum.go +++ b/internal/routes/forum.go @@ -7,23 +7,84 @@ import ( "dk/internal/models/forum" "dk/internal/models/users" "fmt" + "log" "strings" + "time" sushi "git.sharkk.net/Sharkk/Sushi" "git.sharkk.net/Sharkk/Sushi/auth" ) -// ThreadInfo combines forum thread with author and last reply info -type ThreadInfo struct { - Thread *forum.Forum - Author *users.User - LastReplyBy *users.User +// ThreadData flattened struct for template use +type ThreadData struct { + ID int + Posted int64 + LastPost int64 + Author int + Parent int + Replies int + Title string + Content string + AuthorUsername string + AuthorLevel int + AuthorClass string + LastReplyUsername string + LastReplyLevel int + LastReplyClassID int + HasReplies bool } -// PostInfo combines a forum post/reply with its author -type PostInfo struct { - Post *forum.Forum - Author *users.User +// ReplyData flattened struct for template use +type ReplyData struct { + ID int + Posted int64 + Author int + Title string + Content string + AuthorUsername string + AuthorLevel int + AuthorClass string +} + +// Helper methods for ThreadData +func (t *ThreadData) PostedTime() time.Time { + return time.Unix(t.Posted, 0) +} + +func (t *ThreadData) LastPostTime() time.Time { + return time.Unix(t.LastPost, 0) +} + +// Helper methods for ReplyData +func (r *ReplyData) PostedTime() time.Time { + return time.Unix(r.Posted, 0) +} + +// PaginationParams handles common pagination logic +type PaginationParams struct { + Page int + PerPage int + Total int +} + +func (p PaginationParams) Offset() int { + return (p.Page - 1) * p.PerPage +} + +func (p PaginationParams) TotalPages() int { + pages := (p.Total + p.PerPage - 1) / p.PerPage + if pages < 1 { + return 1 + } + return pages +} + +func (p PaginationParams) HasNext() bool { + return p.Page < p.TotalPages() +} + +func (p PaginationParams) HasPrev() bool { + return p.Page > 1 } func RegisterForumRoutes(app *sushi.App) { @@ -33,63 +94,98 @@ func RegisterForumRoutes(app *sushi.App) { authed.Get("/new", showNew) authed.Post("/new", new) authed.Get("/:id", showThread) - authed.Get("/:id/reply", showReply) authed.Post("/:id/reply", reply) } -func index(ctx sushi.Ctx) { - page := int(ctx.QueryArgs().GetUintOrZero("page")) - if page < 1 { - page = 1 +// Helper function to validate thread exists and is actually a thread (not a reply) +func validateThread(ctx sushi.Ctx, id int) (*forum.Forum, bool) { + sess := ctx.GetCurrentSession() + thread, err := forum.Find(id) + if err != nil { + sess.SetFlash("error", fmt.Sprintf("Forum thread %d not found", id)) + ctx.Redirect("/forum") + return nil, false } - perPage := 30 - offset := (page - 1) * perPage + if thread.Parent != 0 { + sess.SetFlash("error", fmt.Sprintf("Forum post %d is not a thread", id)) + ctx.Redirect("/forum") + return nil, false + } - // Get threads with pagination - var threads []*forum.Forum - err := database.Select(&threads, "SELECT * FROM forum WHERE parent = 0 ORDER BY last_post DESC, id DESC LIMIT %d OFFSET %d", perPage, offset) + return thread, true +} + +// Helper function to get pagination params from request +func getPaginationParams(ctx sushi.Ctx, perPage int) PaginationParams { + page := max(int(ctx.QueryArgs().GetUintOrZero("page")), 1) + return PaginationParams{ + Page: page, + PerPage: perPage, + } +} + +func index(ctx sushi.Ctx) { + pagination := getPaginationParams(ctx, 30) + + // Get threads with user data + var threads []*ThreadData + query := ` + SELECT f.id, f.posted, f.last_post, f.author, f.parent, f.replies, f.title, f.content, + COALESCE(u.username, '[Deleted]') as author_username, + COALESCE(u.level, 1) as author_level, + COALESCE(c.name, 'Unknown') as author_class + FROM forum f + LEFT JOIN users u ON f.author = u.id + LEFT JOIN classes c ON u.class_id = c.id + WHERE f.parent = 0 + ORDER BY f.last_post DESC, f.id DESC + LIMIT %d OFFSET %d` + + err := database.Select(&threads, query, pagination.PerPage, pagination.Offset()) if err != nil { - threads = make([]*forum.Forum, 0) + log.Printf("Error fetching forum threads: %v", err) + threads = make([]*ThreadData, 0) + } + + // Get last reply info for each thread + for i := range threads { + type ReplyCount struct{ Count int } + var replyCount ReplyCount + database.Get(&replyCount, "SELECT COUNT(*) as count FROM forum WHERE parent = %d", threads[i].ID) + + if replyCount.Count > 0 { + type LastReply struct{ Username string } + var lastReply LastReply + err := database.Get(&lastReply, ` + SELECT COALESCE(u.username, '[Deleted]') as username + FROM forum f + LEFT JOIN users u ON f.author = u.id + WHERE f.parent = %d + ORDER BY f.posted DESC + LIMIT 1`, threads[i].ID) + + if err == nil { + threads[i].LastReplyUsername = lastReply.Username + threads[i].HasReplies = true + } else { + log.Printf("Error getting last reply author for thread %d: %v", threads[i].ID, err) + } + } } // Get total count for pagination - var totalCount int - database.Get(&totalCount, "SELECT COUNT(*) FROM forum WHERE parent = 0") - - // Build thread info with authors and last reply info - var threadInfos []ThreadInfo - for _, thread := range threads { - author, _ := users.Find(thread.Author) - if author == nil { - author = &users.User{Username: "[Deleted]", ClassID: 1} - } - - // Get last reply author if there are replies - var lastReplyBy *users.User - if thread.Replies > 0 { - var lastReply forum.Forum - err := database.Get(&lastReply, "SELECT * FROM forum WHERE parent = %d ORDER BY posted DESC LIMIT 1", thread.ID) - if err == nil { - lastReplyBy, _ = users.Find(lastReply.Author) - } - } - - threadInfos = append(threadInfos, ThreadInfo{ - Thread: thread, - Author: author, - LastReplyBy: lastReplyBy, - }) - } - - totalPages := (totalCount + perPage - 1) / perPage + type CountResult struct{ Count int } + var paginationResult CountResult + database.Get(&paginationResult, "SELECT COUNT(*) as count FROM forum WHERE parent = 0") + pagination.Total = paginationResult.Count components.RenderPage(ctx, "Forum", "forum/index.html", map[string]any{ - "threadInfos": threadInfos, - "currentPage": page, - "totalPages": totalPages, - "hasNext": page < totalPages, - "hasPrev": page > 1, + "threads": threads, + "currentPage": pagination.Page, + "totalPages": pagination.TotalPages(), + "hasNext": pagination.HasNext(), + "hasPrev": pagination.HasPrev(), }) } @@ -122,100 +218,73 @@ func new(ctx sushi.Ctx) { thread.Title = title thread.Content = content - database.Transaction(func() error { - return thread.Insert() - }) + if err := thread.Insert(); err != nil { + log.Printf("Error creating thread: %v", err) + sess.SetFlash("error", "Failed to create thread") + ctx.Redirect("/forum/new") + return + } ctx.Redirect(fmt.Sprintf("/forum/%d", thread.ID)) } func showThread(ctx sushi.Ctx) { - sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() - page := int(ctx.QueryArgs().GetUintOrZero("page")) - if page < 1 { - page = 1 + thread, ok := validateThread(ctx, id) + if !ok { + return } - thread, err := forum.Find(id) + pagination := getPaginationParams(ctx, 10) + pagination.Total = thread.Replies + + // Get thread with author data + var threadData ThreadData + err := database.Get(&threadData, ` + SELECT f.id, f.posted, f.last_post, f.author, f.parent, f.replies, f.title, f.content, + COALESCE(u.username, '[Deleted]') as author_username, + COALESCE(u.level, 1) as author_level, + COALESCE(c.name, 'Unknown') as author_class + FROM forum f + LEFT JOIN users u ON f.author = u.id + LEFT JOIN classes c ON u.class_id = c.id + WHERE f.id = %d`, id) + if err != nil { - sess.SetFlash("error", fmt.Sprintf("Forum thread %d not found", id)) + log.Printf("Error loading thread %d: %v", id, err) + sess := ctx.GetCurrentSession() + sess.SetFlash("error", "Failed to load thread") ctx.Redirect("/forum") return } - if thread.Parent != 0 { - sess.SetFlash("error", fmt.Sprintf("Forum post %d is not a thread", id)) - ctx.Redirect("/forum") - return - } + // Get replies with user data + var replies []*ReplyData + err = database.Select(&replies, ` + SELECT f.id, f.posted, f.author, f.title, f.content, + COALESCE(u.username, '[Deleted]') as author_username, + COALESCE(u.level, 1) as author_level, + COALESCE(c.name, 'Unknown') as author_class + FROM forum f + LEFT JOIN users u ON f.author = u.id + LEFT JOIN classes c ON u.class_id = c.id + WHERE f.parent = %d + ORDER BY f.posted ASC + LIMIT %d OFFSET %d`, id, pagination.PerPage, pagination.Offset()) - // Get thread author - threadAuthor, _ := users.Find(thread.Author) - if threadAuthor == nil { - threadAuthor = &users.User{Username: "[Deleted]", Level: 1, ClassID: 1} - } - - // Get replies with pagination - perPage := 30 - offset := (page - 1) * perPage - - var replies []*forum.Forum - err = database.Select(&replies, "SELECT * FROM forum WHERE parent = %d ORDER BY posted ASC LIMIT %d OFFSET %d", id, perPage, offset) if err != nil { - replies = make([]*forum.Forum, 0) + log.Printf("Error loading replies for thread %d: %v", id, err) + replies = make([]*ReplyData, 0) } - // Build reply info with authors - var replyInfos []PostInfo - for _, reply := range replies { - author, _ := users.Find(reply.Author) - if author == nil { - author = &users.User{Username: "[Deleted]", Level: 1, ClassID: 1} - } - - replyInfos = append(replyInfos, PostInfo{ - Post: reply, - Author: author, - }) - } - - totalPages := (thread.Replies + perPage - 1) / perPage - if totalPages < 1 { - totalPages = 1 - } - - components.RenderPage(ctx, thread.Title, "forum/thread.html", map[string]any{ - "thread": thread, - "threadAuthor": threadAuthor, - "replyInfos": replyInfos, - "currentPage": page, - "totalPages": totalPages, - "hasNext": page < totalPages, - "hasPrev": page > 1, - "markdown": markdown.MarkdownToHTML, - }) -} - -func showReply(ctx sushi.Ctx) { - sess := ctx.GetCurrentSession() - id := ctx.Param("id").Int() - - thread, err := forum.Find(id) - if err != nil { - sess.SetFlash("error", fmt.Sprintf("Forum thread %d not found", id)) - ctx.Redirect("/forum") - return - } - - if thread.Parent != 0 { - sess.SetFlash("error", fmt.Sprintf("Forum post %d is not a thread", id)) - ctx.Redirect("/forum") - return - } - - components.RenderPage(ctx, "Reply to "+thread.Title, "forum/reply.html", map[string]any{ - "thread": thread, + components.RenderPage(ctx, threadData.Title, "forum/thread.html", map[string]any{ + "thread": threadData, + "replies": replies, + "currentPage": pagination.Page, + "totalPages": pagination.TotalPages(), + "hasNext": pagination.HasNext(), + "hasPrev": pagination.HasPrev(), + "markdown": markdown.MarkdownToHTML, }) } @@ -223,23 +292,15 @@ func reply(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() - thread, err := forum.Find(id) - if err != nil { - sess.SetFlash("error", fmt.Sprintf("Forum thread %d not found", id)) - ctx.Redirect("/forum") - return - } - - if thread.Parent != 0 { - sess.SetFlash("error", fmt.Sprintf("Forum post %d is not a thread", id)) - ctx.Redirect("/forum") + thread, ok := validateThread(ctx, id) + if !ok { return } content := strings.TrimSpace(ctx.Form("content").String()) if content == "" { sess.SetFlash("error", "Reply content cannot be empty") - ctx.Redirect(fmt.Sprintf("/forum/%d/reply", id)) + ctx.Redirect(fmt.Sprintf("/forum/%d#reply", id)) return } @@ -251,7 +312,7 @@ func reply(ctx sushi.Ctx) { reply.Title = "Re: " + thread.Title reply.Content = content - database.Transaction(func() error { + err := database.Transaction(func() error { if err := reply.Insert(); err != nil { return err } @@ -264,5 +325,16 @@ func reply(ctx sushi.Ctx) { thread.Replies, thread.LastPost, thread.ID) }) - ctx.Redirect(fmt.Sprintf("/forum/%d", thread.ID)) + if err != nil { + log.Printf("Error posting reply to thread %d: %v", id, err) + sess.SetFlash("error", "Failed to post reply") + ctx.Redirect(fmt.Sprintf("/forum/%d#reply", id)) + return + } + + // Calculate the last page where the new reply will appear + const repliesPerPage = 10 + lastPage := max((thread.Replies+repliesPerPage-1)/repliesPerPage, 1) + + ctx.Redirect(fmt.Sprintf("/forum/%d?page=%d", thread.ID, lastPage)) } diff --git a/internal/template/template.go b/internal/template/template.go index 3f42e63..d1b7839 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -386,7 +386,6 @@ func (t *Template) processConditionals(content string, data map[string]any) stri func (t *Template) processVariables(content string, data map[string]any) string { result := content - // Process function calls and complex expressions first start := 0 for { startIdx := strings.Index(result[start:], "{") @@ -403,28 +402,9 @@ func (t *Template) processVariables(content string, data map[string]any) string placeholder := result[startIdx+1 : endIdx] - // Check for function calls - if parenIdx := strings.Index(placeholder, "("); parenIdx != -1 && strings.HasSuffix(placeholder, ")") { - value := t.callFunction(placeholder, data) - if value != nil { - result = result[:startIdx] + fmt.Sprintf("%v", value) + result[endIdx+1:] - start = startIdx + len(fmt.Sprintf("%v", value)) - continue - } - } - - // Check for method calls or dot notation - if strings.Contains(placeholder, ".") { - value := t.getNestedValue(data, placeholder) - if value != nil { - result = result[:startIdx] + fmt.Sprintf("%v", value) + result[endIdx+1:] - start = startIdx + len(fmt.Sprintf("%v", value)) - continue - } - } - - // Simple variable - if value, ok := data[placeholder]; ok { + // Try to evaluate as expression + value := t.evaluateExpression(placeholder, data) + if value != nil { result = result[:startIdx] + fmt.Sprintf("%v", value) + result[endIdx+1:] start = startIdx + len(fmt.Sprintf("%v", value)) continue @@ -436,6 +416,188 @@ func (t *Template) processVariables(content string, data map[string]any) string return result } +// evaluateExpression handles expressions - tries arithmetic first, falls back to original logic +func (t *Template) evaluateExpression(expr string, data map[string]any) any { + expr = strings.TrimSpace(expr) + + // Special handling for length operator + if strings.HasPrefix(expr, "#") { + return t.getLength(t.getNestedValue(data, expr[1:])) + } + + // Check if it contains arithmetic operators (but not inside method calls) + var foundOp string + var opPos int = -1 + parenLevel := 0 + inQuotes := false + + // Scan for operators not inside parentheses or quotes + for i, r := range expr { + switch r { + case '"': + inQuotes = !inQuotes + case '(': + if !inQuotes { + parenLevel++ + } + case ')': + if !inQuotes { + parenLevel-- + } + case '+', '-': + if !inQuotes && parenLevel == 0 && i > 0 { + foundOp = string(r) + opPos = i + } + case '*', '/', '%': + if !inQuotes && parenLevel == 0 && i > 0 && (opPos == -1 || (foundOp != "+" && foundOp != "-")) { + foundOp = string(r) + opPos = i + } + } + } + + // If we found an arithmetic operator, evaluate it + if opPos > 0 { + left := strings.TrimSpace(expr[:opPos]) + right := strings.TrimSpace(expr[opPos+1:]) + + leftVal := t.evaluateExpression(left, data) + rightVal := t.evaluateExpression(right, data) + + return t.performArithmetic(leftVal, rightVal, foundOp) + } + + // Handle parentheses (but only if they wrap the entire expression) + if strings.HasPrefix(expr, "(") && strings.HasSuffix(expr, ")") && t.matchingParen(expr) == len(expr)-1 { + return t.evaluateExpression(expr[1:len(expr)-1], data) + } + + // Use original parsing logic for everything else + return t.parseValue(expr, data) +} + +// matchingParen finds the matching closing paren for the opening paren at position 0 +func (t *Template) matchingParen(expr string) int { + level := 0 + inQuotes := false + for i, r := range expr { + switch r { + case '"': + inQuotes = !inQuotes + case '(': + if !inQuotes { + level++ + } + case ')': + if !inQuotes { + level-- + if level == 0 { + return i + } + } + } + } + return -1 +} + +// parseValue uses the original parsing logic +func (t *Template) parseValue(expr string, data map[string]any) any { + // String literal + if strings.HasPrefix(expr, "\"") && strings.HasSuffix(expr, "\"") { + return expr[1 : len(expr)-1] + } + + // Number + if num, err := strconv.ParseFloat(expr, 64); err == nil { + if strings.Contains(expr, ".") { + return num + } + return int(num) + } + + // Boolean + if expr == "true" { + return true + } + if expr == "false" { + return false + } + + // Function call (simple function name only, no dots) + if parenIdx := strings.Index(expr, "("); parenIdx != -1 && strings.HasSuffix(expr, ")") { + funcName := expr[:parenIdx] + if !strings.Contains(funcName, ".") { + return t.callFunction(expr, data) + } + } + + // Method calls or dot notation + if strings.Contains(expr, ".") { + return t.getNestedValue(data, expr) + } + + // Simple variable + if value, ok := data[expr]; ok { + return value + } + + return expr +} + +func (t *Template) performArithmetic(left, right any, op string) any { + leftNum, leftOk := t.toFloat(left) + rightNum, rightOk := t.toFloat(right) + + if !leftOk || !rightOk { + return nil + } + + switch op { + case "+": + result := leftNum + rightNum + if t.isInteger(left) && t.isInteger(right) { + return int(result) + } + return result + case "-": + result := leftNum - rightNum + if t.isInteger(left) && t.isInteger(right) { + return int(result) + } + return result + case "*": + result := leftNum * rightNum + if t.isInteger(left) && t.isInteger(right) { + return int(result) + } + return result + case "/": + if rightNum == 0 { + return nil + } + return leftNum / rightNum + case "%": + if rightNum == 0 { + return nil + } + if t.isInteger(left) && t.isInteger(right) { + return int(leftNum) % int(rightNum) + } + return nil + } + + return nil +} + +func (t *Template) isInteger(value any) bool { + switch value.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return true + } + return false +} + func (t *Template) callFunction(expr string, data map[string]any) any { parenIdx := strings.Index(expr, "(") funcName := strings.TrimSpace(expr[:parenIdx]) @@ -520,7 +682,7 @@ func (t *Template) parseArgs(argsStr string, data map[string]any) []any { current += string(r) case ',': if !inQuotes && parenLevel == 0 { - args = append(args, t.parseArgValue(strings.TrimSpace(current), data)) + args = append(args, t.evaluateExpression(strings.TrimSpace(current), data)) current = "" continue } @@ -531,54 +693,12 @@ func (t *Template) parseArgs(argsStr string, data map[string]any) []any { } if current != "" { - args = append(args, t.parseArgValue(strings.TrimSpace(current), data)) + args = append(args, t.evaluateExpression(strings.TrimSpace(current), data)) } return args } -func (t *Template) parseArgValue(arg string, data map[string]any) any { - arg = strings.TrimSpace(arg) - - // String literal - if strings.HasPrefix(arg, "\"") && strings.HasSuffix(arg, "\"") { - return arg[1 : len(arg)-1] - } - - // Number - if num, err := strconv.ParseFloat(arg, 64); err == nil { - if strings.Contains(arg, ".") { - return num - } - return int(num) - } - - // Boolean - if arg == "true" { - return true - } - if arg == "false" { - return false - } - - // Function call - if parenIdx := strings.Index(arg, "("); parenIdx != -1 && strings.HasSuffix(arg, ")") { - return t.callFunction(arg, data) - } - - // Variable or dot notation - if strings.Contains(arg, ".") { - return t.getNestedValue(data, arg) - } - - // Simple variable - if value, ok := data[arg]; ok { - return value - } - - return arg -} - func (t *Template) convertArg(arg any, targetType reflect.Type) reflect.Value { argValue := reflect.ValueOf(arg) @@ -785,43 +905,12 @@ func (t *Template) evaluateCondition(condition string, data map[string]any) bool if len(parts) == 2 { left := strings.TrimSpace(parts[0]) right := strings.TrimSpace(parts[1]) - return t.compareValues(t.getConditionValue(left, data), t.getConditionValue(right, data), op) + return t.compareValues(t.evaluateExpression(left, data), t.evaluateExpression(right, data), op) } } } - return t.isTruthy(t.getConditionValue(condition, data)) -} - -func (t *Template) getConditionValue(expr string, data map[string]any) any { - expr = strings.TrimSpace(expr) - - if strings.HasPrefix(expr, "#") { - return t.getLength(t.getNestedValue(data, expr[1:])) - } - - if num, err := strconv.ParseFloat(expr, 64); err == nil { - return num - } - - if strings.HasPrefix(expr, "\"") && strings.HasSuffix(expr, "\"") { - return expr[1 : len(expr)-1] - } - - // Function call - if parenIdx := strings.Index(expr, "("); parenIdx != -1 && strings.HasSuffix(expr, ")") { - return t.callFunction(expr, data) - } - - if strings.Contains(expr, ".") { - return t.getNestedValue(data, expr) - } - - if value, ok := data[expr]; ok { - return value - } - - return expr + return t.isTruthy(t.evaluateExpression(condition, data)) } func (t *Template) compareValues(left, right any, op string) bool { diff --git a/templates/forum/index.html b/templates/forum/index.html index 42f36fb..1ed6c14 100644 --- a/templates/forum/index.html +++ b/templates/forum/index.html @@ -8,7 +8,7 @@ -{if #threadInfos > 0} +{if #threads > 0} @@ -18,23 +18,23 @@ - {for threadInfo in threadInfos} + {for thread in threads}
- by {threadInfo.Author.Username} • {threadInfo.Thread.PostedTime().Format("Jan 2, 2006 3:04 PM")} + by {thread.AuthorUsername} • {thread.PostedTime().Format("Jan 2, 2006 3:04 PM")}
- {threadInfo.Thread.Replies} + {thread.Replies} - {if threadInfo.LastReplyBy} - by {threadInfo.LastReplyBy.Username}
- {threadInfo.Thread.LastPostTime().Format("Jan 2, 2006 3:04 PM")} + {if thread.HasReplies} + by {thread.LastReplyUsername}
+ {thread.LastPostTime().Format("Jan 2, 2006 3:04 PM")} {else} No replies {/if} diff --git a/templates/forum/reply.html b/templates/forum/reply.html deleted file mode 100644 index da170c3..0000000 --- a/templates/forum/reply.html +++ /dev/null @@ -1,18 +0,0 @@ -{include "layout.html"} - -{block "content"} -

Reply to: {thread.Title}

- -
- {csrf} - -
- -
- -
- - -
-
-{/block} diff --git a/templates/forum/thread.html b/templates/forum/thread.html index 25ef39b..db60a2b 100644 --- a/templates/forum/thread.html +++ b/templates/forum/thread.html @@ -5,47 +5,41 @@

{thread.Title}

-
-
-
{threadAuthor.Username}
-
- Level {threadAuthor.Level}
- {threadAuthor.Class().Name} +
+
+
+ OP - {thread.PostedTime().Format("Jan 2, 2006 3:04 PM")}
-
-
{md(thread.Content)}
-
- Posted: {thread.PostedTime().Format("Jan 2, 2006 3:04 PM")} -
+
+
+
{thread.AuthorUsername}
+
Level {thread.AuthorLevel} {thread.AuthorClass}
-{if #replyInfos > 0} -{for replyInfo in replyInfos} -
-
-
{replyInfo.Author.Username}
+{if #replies > 0} +{for reply in replies} +
+
+
+ {reply.PostedTime().Format("Jan 2, 2006 3:04 PM")} +
- Level {replyInfo.Author.Level}
- {replyInfo.Author.Class().Name} + {md(reply.Content)}
-
-
- {markdown(replyInfo.Post.Content)} -
-
- Posted: {replyInfo.Post.PostedTime().Format("Jan 2, 2006 3:04 PM")} -
+
+
{reply.AuthorUsername}
+
Level {reply.AuthorLevel} {reply.AuthorClass}
{/for} @@ -53,7 +47,7 @@ {if totalPages > 1} -
+
{if hasPrev} {/if} @@ -66,8 +60,18 @@
{/if} - -
- + +
+
+ {csrf} + +
+ +
+ +
+ +
+
{/block} \ No newline at end of file