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}