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