@@ -2,6 +2,7 @@ package formatter
22
33import (
44 "fmt"
5+ "regexp"
56 "strings"
67)
78
@@ -30,7 +31,8 @@ func (f *Formatter) FormatMessage(msg string, isMajor bool) string {
3031 subject := strings .TrimSpace (parts [0 ])
3132 body := ""
3233 if len (parts ) > 1 {
33- body = strings .TrimSpace (parts [1 ])
34+ body = strings .TrimLeft (parts [1 ], "\n \r " )
35+ body = strings .TrimRight (body , "\n \r \t " )
3436 }
3537
3638 // Remove redundant phrases from subject
@@ -49,6 +51,7 @@ func (f *Formatter) FormatMessage(msg string, isMajor bool) string {
4951 subjectParts := strings .SplitN (wrapped , "\n " , 2 )
5052 subject = subjectParts [0 ]
5153 if len (subjectParts ) > 1 {
54+ // Subject overflow becomes the start of the body
5255 if body != "" {
5356 body = subjectParts [1 ] + "\n \n " + body
5457 } else {
@@ -68,7 +71,7 @@ func (f *Formatter) FormatMessage(msg string, isMajor bool) string {
6871 return subject
6972}
7073
71- // wrapString wraps a string at the specified limit, preserving paragraphs
74+ // wrapString wraps a string at the specified limit, preserving paragraphs and structures
7275func (f * Formatter ) wrapString (s string , limit int ) string {
7376 if limit <= 0 {
7477 return s
@@ -82,27 +85,167 @@ func (f *Formatter) wrapString(s string, limit int) string {
8285 result .WriteString ("\n \n " )
8386 }
8487
85- words := strings .Fields (p )
86- if len (words ) == 0 {
87- continue
88- }
88+ lines := strings .Split (p , "\n " )
89+ var currentParagraph strings.Builder
8990
90- currentLineLength := 0
91- for j , word := range words {
92- if j > 0 {
93- if currentLineLength + 1 + len (word ) > limit {
91+ for _ , line := range lines {
92+ if f .isStructural (line ) {
93+ // Flush any pending paragraph text
94+ if currentParagraph .Len () > 0 {
95+ result .WriteString (f .reflow (currentParagraph .String (), limit ))
9496 result .WriteString ("\n " )
95- currentLineLength = 0
96- } else {
97- result .WriteString (" " )
98- currentLineLength ++
97+ currentParagraph .Reset ()
98+ }
99+ // Wrap the structural line itself (preserving its prefix if possible)
100+ result .WriteString (f .wrapLine (line , limit ))
101+ result .WriteString ("\n " )
102+ } else {
103+ trimmed := strings .TrimSpace (line )
104+ if trimmed == "" {
105+ continue
106+ }
107+ if currentParagraph .Len () > 0 {
108+ currentParagraph .WriteString (" " )
99109 }
110+ currentParagraph .WriteString (trimmed )
100111 }
112+ }
113+
114+ if currentParagraph .Len () > 0 {
115+ result .WriteString (f .reflow (currentParagraph .String (), limit ))
116+ }
101117
102- result .WriteString (word )
103- currentLineLength += len (word )
118+ // Cleanup trailing newline from structural lines at the end of a paragraph
119+ resStr := result .String ()
120+ if strings .HasSuffix (resStr , "\n " ) {
121+ result .Reset ()
122+ result .WriteString (strings .TrimSuffix (resStr , "\n " ))
104123 }
105124 }
106125
107126 return result .String ()
108127}
128+
129+ // isStructural identifies lines that should not be reflowed into paragraphs
130+ func (f * Formatter ) isStructural (line string ) bool {
131+ trimmed := strings .TrimSpace (line )
132+ if trimmed == "" {
133+ return false
134+ }
135+
136+ // List markers at the start of the trimmed line
137+ markers := []string {"- " , "* " , "+ " }
138+ for _ , m := range markers {
139+ if strings .HasPrefix (trimmed , m ) {
140+ return true
141+ }
142+ }
143+
144+ // Numeric list markers (e.g., "1. ")
145+ numericRegex := regexp .MustCompile (`^\d+\.\s` )
146+ if numericRegex .MatchString (trimmed ) {
147+ return true
148+ }
149+
150+ // Significant indentation (at least 2 spaces or a tab)
151+ if strings .HasPrefix (line , " " ) || strings .HasPrefix (line , "\t " ) {
152+ return true
153+ }
154+
155+ return false
156+ }
157+
158+ // reflow joins words and wraps them at the limit
159+ func (f * Formatter ) reflow (s string , limit int ) string {
160+ words := strings .Fields (s )
161+ if len (words ) == 0 {
162+ return ""
163+ }
164+
165+ var res strings.Builder
166+ currLen := 0
167+ for i , w := range words {
168+ if i > 0 {
169+ if currLen + 1 + len (w ) > limit {
170+ res .WriteString ("\n " )
171+ currLen = 0
172+ } else {
173+ res .WriteString (" " )
174+ currLen ++
175+ }
176+ }
177+ res .WriteString (w )
178+ currLen += len (w )
179+ }
180+ return res .String ()
181+ }
182+
183+ // wrapLine wraps a single structural line, attempting to preserve indentation
184+ func (f * Formatter ) wrapLine (line string , limit int ) string {
185+ if len (line ) <= limit {
186+ return line
187+ }
188+
189+ // Find indentation and prefix
190+ indent := ""
191+ for _ , char := range line {
192+ if char == ' ' || char == '\t' {
193+ indent += string (char )
194+ } else {
195+ break
196+ }
197+ }
198+
199+ // Also check for list markers
200+ content := line [len (indent ):]
201+ prefix := ""
202+ markers := []string {"- " , "* " , "+ " }
203+ for _ , m := range markers {
204+ if strings .HasPrefix (content , m ) {
205+ prefix = m
206+ content = content [len (m ):]
207+ break
208+ }
209+ }
210+
211+ // Handle numeric markers
212+ numericRegex := regexp .MustCompile (`^\d+\.\s` )
213+ if loc := numericRegex .FindStringIndex (content ); loc != nil && loc [0 ] == 0 {
214+ prefix = content [loc [0 ]:loc [1 ]]
215+ content = content [loc [1 ]:]
216+ }
217+
218+ words := strings .Fields (content )
219+ if len (words ) == 0 {
220+ return line
221+ }
222+
223+ var res strings.Builder
224+ res .WriteString (indent )
225+ res .WriteString (prefix )
226+ currLen := len (indent ) + len (prefix )
227+
228+ for i , w := range words {
229+ if i > 0 {
230+ if currLen + 1 + len (w ) > limit {
231+ res .WriteString ("\n " )
232+ res .WriteString (indent )
233+ // Extra indentation for wrapped list items
234+ if prefix != "" {
235+ res .WriteString (" " )
236+ }
237+ currLen = len (indent )
238+ if prefix != "" {
239+ currLen += 2
240+ }
241+ } else {
242+ res .WriteString (" " )
243+ currLen ++
244+ }
245+ }
246+ res .WriteString (w )
247+ currLen += len (w )
248+ }
249+
250+ return res .String ()
251+ }
0 commit comments