wrapBody.go

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