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