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		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}