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