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