all repos — bibel @ 995003ab506a7dc2fc1a212da5b5d9bd2cdd58ad

Unnamed repository; edit this file 'description' to name the repository.

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			sb.WriteString(fmt.Sprintf("%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}
241// max returns the larger of two integers
242func max(a, b int) int {
243	if a > b {
244		return a
245	}
246	return b
247}