internal/formatter.go (view raw)
1package bible
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "github.com/charmbracelet/lipgloss"
9)
10
11// Formatter handles formatting Bible text for display
12type Formatter struct {
13 bible *Bible
14 useColours bool
15 headerFormat string
16 numbered bool
17 paragraphs bool
18}
19
20// NewFormatter creates a new formatter with default settings
21func NewFormatter(bible *Bible) *Formatter {
22 return &Formatter{
23 bible: bible,
24 useColours: true,
25 headerFormat: "{book} {chapter}\nv. {first_verse}-{second_verse}",
26 numbered: false,
27 paragraphs: false,
28 }
29}
30
31// NewFormatterWithFullConfig creates a new formatter with full configuration
32func NewFormatterWithFullConfig(bible *Bible, useColours bool, headerFormat string, numbered bool, paragraphs bool) *Formatter {
33 return &Formatter{
34 bible: bible,
35 useColours: useColours,
36 headerFormat: headerFormat,
37 numbered: numbered,
38 paragraphs: paragraphs,
39 }
40}
41
42// NewFormatterWithConfig creates a new formatter with configuration
43func NewFormatterWithConfig(bible *Bible, useColours bool, headerFormat string) *Formatter {
44 return NewFormatterWithFullConfig(bible, useColours, headerFormat, false, false)
45}
46
47// FormatHeader formats the header for a bookmark using the configured template
48func (f *Formatter) FormatHeader(bookmark *Bookmark) string {
49 template := f.headerFormat
50 if template == "" {
51 // Fall back to default template
52 template = "{book} {chapter}\nv. {first_verse}-{second_verse}"
53 }
54
55 // Get book name from Bible data
56 bookName := f.getBookName(bookmark.Book)
57
58 // Replace template variables
59 result := template
60 result = strings.ReplaceAll(result, "{book}", bookName)
61 result = strings.ReplaceAll(result, "{chapter}", strconv.Itoa(bookmark.Chapter))
62 result = strings.ReplaceAll(result, "{first_verse}", strconv.Itoa(bookmark.FirstVerse))
63 result = strings.ReplaceAll(result, "{second_verse}", strconv.Itoa(bookmark.SecondVerse))
64
65 if !f.useColours {
66 return result
67 }
68
69 // Add ANSI colours - simple implementation
70 // For more advanced templating, we'd need a proper template engine
71 const (
72 reset = "\033[0m"
73 green = "\033[32m"
74 yellow = "\033[33m"
75 )
76
77 // Simple colouring - book and chapter in green, verse range in yellow
78 lines := strings.Split(result, "\n")
79 if len(lines) >= 1 {
80 lines[0] = green + lines[0] + reset
81 }
82 if len(lines) >= 2 {
83 lines[1] = yellow + lines[1] + reset
84 }
85 result = strings.Join(lines, "\n")
86
87 return result
88}
89
90// getBookName retrieves the book name from Bible data
91func (f *Formatter) getBookName(book BookIndex) string {
92 // Try to find any verse from this book to get the book name
93 // Start from chapter 1, verse 1 and work forward
94 for chapter := 1; chapter <= 150; chapter++ {
95 for verse := 1; verse <= 200; verse++ {
96 if v, ok := f.bible.GetVerse(int(book), chapter, verse); ok {
97 return v.BookName
98 }
99 }
100 }
101 // If we can't find the book in the data, return a generic name
102 return fmt.Sprintf("Book %d", book)
103} // FormatHeaderWithTemplate is deprecated - use FormatHeader instead
104func (f *Formatter) FormatHeaderWithTemplate(bookmark *Bookmark) string {
105 return f.FormatHeader(bookmark)
106}
107
108// FormatSnippet formats the verse range text
109func (f *Formatter) FormatSnippet(verses []*Verse) string {
110 // Use the new options-aware method
111 return f.FormatSnippetWithOptions(verses)
112}
113
114// FormatSnippetWithOptions formats the verse range text with configuration options
115func (f *Formatter) FormatSnippetWithOptions(verses []*Verse) string {
116 if len(verses) == 0 {
117 return "Error: No text matched"
118 }
119
120 var sb strings.Builder
121 for i, verse := range verses {
122 // Check for pilcrow to handle paragraph breaks
123 text := strings.TrimSpace(verse.Text)
124 hasPilcrow := strings.HasPrefix(text, "¶ ") || strings.HasPrefix(text, "¶")
125
126 // Remove pilcrow markers if present
127 if strings.HasPrefix(text, "¶ ") {
128 text = text[2:]
129 } else if strings.HasPrefix(text, "¶") {
130 text = text[1:]
131 }
132
133 // Handle paragraph breaks (blank line before verse with pilcrow)
134 if f.paragraphs && hasPilcrow && i > 0 {
135 // Add blank line before this verse (paragraph break)
136 // If numbered mode, we'll add a newline anyway, so just add one more
137 if f.numbered {
138 sb.WriteString("\n\n")
139 } else {
140 // In non-numbered mode, need space then blank line
141 if i > 0 {
142 sb.WriteString(" \n\n")
143 } else {
144 sb.WriteString("\n\n")
145 }
146 }
147 }
148
149 // Handle numbered output
150 if f.numbered {
151 if i > 0 && (!f.paragraphs || !hasPilcrow) {
152 // Don't add newline if we already added one for paragraph
153 sb.WriteString("\n")
154 }
155 fmt.Fprintf(&sb, "%d. %s", verse.VerseNum, text)
156 } else {
157 // Regular mode - join with spaces
158 // Don't add space if we just added paragraph break
159 if i > 0 && (!f.paragraphs || !hasPilcrow) {
160 sb.WriteString(" ")
161 }
162 sb.WriteString(text)
163 }
164 }
165
166 return sb.String()
167}
168
169// FormatSnippetForTUI formats verse text for TUI with lipgloss styling
170func (f *Formatter) FormatSnippetForTUI(verses []*Verse, style lipgloss.Style, numberStyle lipgloss.Style, contentWidth int) string {
171 if len(verses) == 0 {
172 return "Error: No text matched"
173 }
174
175 var resultLines []string
176
177 for i, verse := range verses {
178 // Process verse text
179 text := strings.TrimSpace(verse.Text)
180 hasPilcrow := strings.HasPrefix(text, "¶ ") || strings.HasPrefix(text, "¶")
181
182 // Remove pilcrow markers if present
183 if strings.HasPrefix(text, "¶ ") {
184 text = text[2:]
185 } else if strings.HasPrefix(text, "¶") {
186 text = text[1:]
187 }
188
189 // Handle paragraph breaks (blank line before verse with pilcrow)
190 if f.paragraphs && hasPilcrow && i > 0 {
191 // Add blank line before this verse (paragraph break)
192 resultLines = append(resultLines, "")
193 }
194
195 if f.numbered {
196 // For numbered mode: "1. text"
197 // Estimate number takes about 4 characters (for verse numbers up to 3 digits + ". ")
198 const numberWidthEstimate = 4
199 remainingWidth := max(1, contentWidth-numberWidthEstimate)
200
201 numberPart := numberStyle.Render(fmt.Sprintf("%d. ", verse.VerseNum))
202 textPart := style.Width(remainingWidth).Render(text)
203 resultLines = append(resultLines, numberPart+textPart)
204 } else {
205 // For non-numbered mode, we'll handle text accumulation separately
206 // Store plain text for now, will join and render later
207 if len(resultLines) == 0 || resultLines[len(resultLines)-1] == "" {
208 // Start new line
209 resultLines = append(resultLines, text)
210 } else {
211 // Continue current line with space
212 resultLines[len(resultLines)-1] = resultLines[len(resultLines)-1] + " " + text
213 }
214 }
215 }
216
217 // Apply styling to non-numbered lines
218 if !f.numbered {
219 for i, line := range resultLines {
220 if line != "" { // Keep empty lines (paragraph breaks) as is
221 resultLines[i] = style.Width(contentWidth).Render(line)
222 }
223 }
224 }
225
226 return strings.Join(resultLines, "\n")
227}
228
229// ExtractAndFormat extracts verses for a bookmark and formats them
230func (f *Formatter) ExtractAndFormat(bible *Bible, bookmark *Bookmark) string {
231 verses := bible.GetVerseRange(int(bookmark.Book), bookmark.Chapter,
232 bookmark.FirstVerse, bookmark.SecondVerse)
233 return f.FormatSnippet(verses)
234}
235
236// ExtractAndFormatWithHeader extracts verses and formats with header
237func (f *Formatter) ExtractAndFormatWithHeader(bible *Bible, bookmark *Bookmark) string {
238 header := f.FormatHeader(bookmark)
239 content := f.ExtractAndFormat(bible, bookmark)
240 return header + "\n" + content
241}