1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package main
6
7import (
8 "regexp"
9 "strings"
10
11 "github.com/microcosm-cc/bluemonday"
12)
13
14var numberedListRegex = regexp.MustCompile(`^\d+\.\s`)
15
16func formatBody(body string) (string, error) {
17 p := bluemonday.UGCPolicy()
18 sanitized := p.Sanitize(body)
19
20 lines := strings.Split(sanitized, "\n")
21 var result []string
22
23 for _, line := range lines {
24 trimmed := strings.TrimSpace(line)
25 if trimmed == "" {
26 result = append(result, "")
27 continue
28 }
29
30 if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") {
31 marker := trimmed[:2]
32 content := trimmed[2:]
33 wrapped := wrapWithHangingIndent(marker, " ", content, 72)
34 result = append(result, wrapped)
35 continue
36 }
37
38 if numberedListRegex.MatchString(trimmed) {
39 parts := strings.SplitN(trimmed, " ", 2)
40 marker := parts[0] + " "
41 content := ""
42 if len(parts) > 1 {
43 content = parts[1]
44 }
45 indent := strings.Repeat(" ", len(marker))
46 wrapped := wrapWithHangingIndent(marker, indent, content, 72)
47 result = append(result, wrapped)
48 continue
49 }
50
51 result = append(result, wordWrap(trimmed, 72))
52 }
53
54 return strings.Join(result, "\n"), nil
55}
56
57func wrapWithHangingIndent(firstPrefix, contPrefix, text string, width int) string {
58 firstWidth := width - len(firstPrefix)
59 contWidth := width - len(contPrefix)
60
61 words := strings.Fields(text)
62 if len(words) == 0 {
63 return firstPrefix
64 }
65
66 var lines []string
67 var currentLine strings.Builder
68 var currentWidth int
69 isFirstLine := true
70
71 for _, word := range words {
72 wordLen := len(word)
73 maxWidth := firstWidth
74 if !isFirstLine {
75 maxWidth = contWidth
76 }
77
78 if currentLine.Len() == 0 {
79 currentLine.WriteString(word)
80 currentWidth = wordLen
81 } else if currentWidth+1+wordLen <= maxWidth {
82 currentLine.WriteString(" ")
83 currentLine.WriteString(word)
84 currentWidth += 1 + wordLen
85 } else {
86 if isFirstLine {
87 lines = append(lines, firstPrefix+currentLine.String())
88 isFirstLine = false
89 } else {
90 lines = append(lines, contPrefix+currentLine.String())
91 }
92 currentLine.Reset()
93 currentLine.WriteString(word)
94 currentWidth = wordLen
95 }
96 }
97
98 if currentLine.Len() > 0 {
99 if isFirstLine {
100 lines = append(lines, firstPrefix+currentLine.String())
101 } else {
102 lines = append(lines, contPrefix+currentLine.String())
103 }
104 }
105
106 return strings.Join(lines, "\n")
107}
108
109func wordWrap(text string, width int) string {
110 words := strings.Fields(text)
111 if len(words) == 0 {
112 return ""
113 }
114
115 var result strings.Builder
116 var currentLine strings.Builder
117 var currentWidth int
118
119 for _, word := range words {
120 wordLen := len(word)
121
122 if currentLine.Len() == 0 {
123 currentLine.WriteString(word)
124 currentWidth = wordLen
125 } else if currentWidth+1+wordLen <= width {
126 currentLine.WriteString(" ")
127 currentLine.WriteString(word)
128 currentWidth += 1 + wordLen
129 } else {
130 if result.Len() > 0 {
131 result.WriteString("\n")
132 }
133 result.WriteString(currentLine.String())
134 currentLine.Reset()
135 currentLine.WriteString(word)
136 currentWidth = wordLen
137 }
138 }
139
140 if currentLine.Len() > 0 {
141 if result.Len() > 0 {
142 result.WriteString("\n")
143 }
144 result.WriteString(currentLine.String())
145 }
146
147 return result.String()
148}