diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..e792c99 --- /dev/null +++ b/claude.md @@ -0,0 +1,357 @@ +# Lorelai Project Overview + +## What is Lorelai? + +Lorelai is a Go package and CLI tool for generating Lorem Ipsum placeholder text. It provides both random word generation and classic Lorem Ipsum text generation with a clean, simple API. + +## Project Structure + +``` +lorelai/ +├── cmd/ # CLI application +│ └── root.go # Command-line interface implementation +├── pkg/ # Core library package +│ ├── root.go # Random Lorem generation functions +│ ├── classic.go # Classic Lorem Ipsum functions +│ ├── convenience.go # Utility functions (URL, Email, Domain) +│ ├── data.go # Word corpus +│ ├── tlds.go # Top-level domain data +│ ├── utils.go # Internal helper functions +│ └── *_test.go # Test files +├── example/ # Example usage code +└── readme.md # Documentation +``` + +## Core Architecture + +### Two Modes of Operation + +**1. Random Mode** - Generates random Lorem Ipsum-like text: + +- Uses a corpus of Latin words (`DATA` in `data.go`) +- Randomly selects words for generation +- Functions: `Word()`, `Sentence()`, `Paragraph()`, `Generate()` + +**2. Classic Mode** - Uses traditional Lorem Ipsum text: + +- Based on Cicero's "De finibus bonorum et malorum" +- Always starts with "Lorem ipsum dolor sit amet..." +- Functions: `ClassicWords()`, `ClassicSentence()`, `ClassicParagraph()`, `ClassicGenerate()` + +### Key Design Patterns + +#### 1. Consistent Naming Convention + +All functions follow a clear pattern: + +- Base functions for random mode: `Word()`, `Sentence()`, `Paragraph()` +- Classic variants prefixed: `ClassicWord()`, `ClassicSentence()`, `ClassicParagraph()` + +#### 2. Composition Over Complexity + +- Simple, focused functions that do one thing well +- No stateful services or complex object hierarchies +- Functions compose easily: `Sentence()` uses `LoremWords(8)` + +#### 3. Performance Optimizations + +- Uses `strings.Builder` with capacity hints (`b.Grow()`) +- Avoids unnecessary string concatenations +- Thread-safe random generation with `math/rand/v2` + +## API Design Philosophy + +### Simplicity First + +```go +// Simple, direct API +text := lorelai.Paragraph() + +// Structured generation with metadata +result := lorelai.Generate(3, 5) +fmt.Printf("Generated %d words\n", result.WordCount) +``` + +### Sensible Defaults + +- Sentences are always 8 words +- Paragraphs are always 45 words +- All text is properly capitalized and punctuated + +### No Configuration Required + +- Zero-dependency operation (except stdlib) +- No initialization or setup needed +- Import and use immediately + +## How Text Generation Works + +### Random Mode Process + +1. Select random words from `DATA` corpus +2. Join words with spaces +3. Capitalize first word +4. Add period at end +5. Return formatted string + +### Classic Mode Process + +1. Use sequential words from `classicText` array +2. Start from beginning: "lorem", "ipsum", "dolor"... +3. Wrap around if more words needed than available +4. Format with capitalization and punctuation + +### Structured Generation + +Both `Generate()` and `ClassicGenerate()`: + +1. Calculate total capacity needed +2. Pre-allocate `strings.Builder` buffer +3. Loop through paragraphs and sentences +4. Insert separators (`\n\n` for paragraphs, space for sentences) +5. Return `Lorem` struct with text and metadata + +## Testing Strategy + +### Test Coverage: 97.1% + +**Unit Tests** cover: + +- Individual function outputs (word counts, formatting) +- Edge cases (0, negative numbers) +- Formatting correctness (capitalization, punctuation) +- Metadata accuracy + +**Concurrent Tests** verify: + +- Thread-safe random generation +- No race conditions in word selection + +**Integration Tests** ensure: + +- CLI flags work correctly +- File output functions properly +- Colors render as expected + +## CLI vs Package Semantics + +### Important Distinction + +**CLI** (additive behavior): + +```bash +lorelai -p 3 -s 5 # Generates 3 paragraphs + 5 sentences +``` + +**Package** (multiplicative behavior): + +```go +lorelai.Generate(3, 5) // Generates 3 paragraphs OF 5 sentences each +``` + +This difference is intentional: + +- CLI optimized for quick, ad-hoc text generation +- Package optimized for structured, programmatic use + +## Convenience Features + +Beyond Lorem Ipsum text, lorelai provides: + +### Domain Names + +```go +lorelai.Domain() // "neque.net" +``` + +### URLs + +```go +lorelai.URL() // "https://pellentesque.org" +``` + +### Email Addresses + +```go +lorelai.Email() // "bibendum@id.pe" +``` + +These use the same word corpus plus TLD (Top-Level Domain) data. + +## Code Quality Practices + +### 1. Clear Function Documentation + +Every exported function has: + +- Clear description of what it does +- Input/output specifications +- Usage examples where helpful + +### 2. Efficient String Building + +Always use `strings.Builder` with capacity hints: + +```go +var b strings.Builder +b.Grow(expectedSize) // Pre-allocate +``` + +### 3. DRY Principle + +Common operations extracted to helpers: + +- `formatWords()` - Capitalize and add punctuation +- `capitalizeFirstWord()` - Title case first character +- `trimSpaceAddDot()` - Clean up and punctuate + +### 4. Defensive Programming + +- Handle zero/negative inputs gracefully +- Validate array bounds before access +- Return empty strings for invalid input (not errors) + +## Performance Characteristics + +### Memory Efficiency + +- Pre-allocated buffers reduce reallocations +- `strings.Builder` avoids intermediate string copies +- Fixed-size word corpus (`DATA` array) + +### Time Complexity + +- Random word selection: O(1) +- Text generation: O(n) where n is output length +- No complex algorithms or data structures + +### Concurrency + +- Thread-safe due to Go 1.22+ `math/rand/v2` +- No shared mutable state +- Functions are pure (aside from randomness) + +## Common Use Cases + +### 1. Web Development + +Generate placeholder text for UI mockups: + +```go +description := lorelai.Paragraph() +title := lorelai.Sentence() +``` + +### 2. Testing + +Create realistic test data: + +```go +for i := 0; i < 100; i++ { + users[i].Bio = lorelai.Generate(2, 3).Text +} +``` + +### 3. Documentation + +Fill example content in docs: + +```go +example := lorelai.ClassicGenerate(1, 2) +``` + +### 4. CLI Quick Tasks + +```bash +lorelai -p 5 | pbcopy # Copy to clipboard +lorelai -w 100 > file.txt # Save to file +``` + +## Extending Lorelai + +### Adding New Functions + +Follow the established patterns: + +1. **Name consistently**: `Thing()` for random, `ClassicThing()` for classic +2. **Use helpers**: Reuse `formatWords()`, `capitalizeFirstWord()` +3. **Add tests**: Include edge cases and format verification +4. **Document**: Clear godoc comments with examples + +### Adding New Features + +Consider: + +- Is it stateless? (Should be) +- Does it fit the "simple text generation" domain? +- Is the API obvious without reading docs? +- Does it maintain backward compatibility? + +## Dependencies + +### Production + +- **Zero external dependencies** +- Uses only Go standard library +- Minimum Go version: 1.22 (for `math/rand/v2`) + +### Development/CLI + +- `github.com/spf13/cobra` - CLI framework +- `github.com/UltiRequiem/chigo/pkg` - Color output + +## Project Philosophy + +### Simplicity + +> "Do one thing well" - Generate placeholder text, nothing more + +### Pragmatism + +> Sensible defaults over configuration. 8 words? Perfect. No need to change it. + +### Accessibility + +> Zero learning curve. Import, call a function, get text. Done. + +### Performance + +> Fast enough to not think about it. Pre-allocate, avoid copies, use `strings.Builder`. + +## Maintenance Guidelines + +### When Adding Code + +- ✅ Maintain 95%+ test coverage +- ✅ Keep functions small and focused +- ✅ Follow existing naming patterns +- ✅ Update documentation and examples + +### When Reviewing PRs + +- ❓ Does it add real value? +- ❓ Is the API intuitive? +- ❓ Are there tests? +- ❓ Is it backward compatible? + +### When Fixing Bugs + +- 🐛 Add a test that reproduces the bug +- 🐛 Fix the issue +- 🐛 Verify the test passes +- 🐛 Consider similar issues elsewhere + +## Historical Context + +Lorelai was created to provide a simple, dependency-free Lorem Ipsum generator for Go projects. Unlike other libraries that require configuration or complex APIs, Lorelai focuses on: + +1. **Immediate usability** - No setup, just import and use +2. **Predictable output** - Consistent word counts and formatting +3. **Both modes** - Random for variety, classic for tradition +4. **CLI included** - Quick shell usage without writing code + +The project has grown to include convenience utilities (domains, URLs, emails) while maintaining its core simplicity. + +--- + +_For implementation details and changes, see the Git history and PR discussions._ diff --git a/example/main.go b/example/main.go index cc5b9b0..61d8b81 100644 --- a/example/main.go +++ b/example/main.go @@ -28,6 +28,24 @@ func printTonsOfText() { } } +func generateStructuredContent() { + fmt.Println("\n=== Structured Random Lorem ===") + // Generate 3 paragraphs with 5 sentences each + result := lorelai.Generate(3, 5) + fmt.Println(result.Text) + fmt.Printf("\nGenerated %d words across %d paragraphs (%d sentences per paragraph)\n", + result.WordCount, result.Paragraphs, result.Sentences) +} + +func generateClassicContent() { + fmt.Println("\n=== Classic Lorem Ipsum ===") + // Generate classic Lorem Ipsum text: 2 paragraphs with 4 sentences each + result := lorelai.ClassicGenerate(2, 4) + fmt.Println(result.Text) + fmt.Printf("\nGenerated %d words of classic Lorem Ipsum\n", result.WordCount) +} + func main() { - printTonsOfText() + generateStructuredContent() + generateClassicContent() } diff --git a/pkg/classic.go b/pkg/classic.go index 42bbca9..c579051 100644 --- a/pkg/classic.go +++ b/pkg/classic.go @@ -4,7 +4,7 @@ import "strings" // Classic Lorem Ipsum text from Cicero's "De finibus bonorum et malorum" // This is the standard Lorem Ipsum that starts with "Lorem ipsum dolor sit amet" -var classicText = []string{ +var classicText = [...]string{ "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua", "ut", "enim", "ad", "minim", "veniam", "quis", "nostrud", @@ -16,6 +16,8 @@ var classicText = []string{ "est", "laborum", } +var wordsPerSentence = 8 + // ClassicParagraph returns the classic Lorem Ipsum paragraph that starts with // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..." func ClassicParagraph() string { @@ -24,12 +26,12 @@ func ClassicParagraph() string { // ClassicSentence returns a sentence from the classic Lorem Ipsum text func ClassicSentence() string { - return buildClassicText(8) + return buildClassicText(wordsPerSentence) } // ClassicWordsPerSentence returns the word count per sentence func ClassicWordsPerSentence() int { - return 8 + return wordsPerSentence } // ClassicWordsPerSentence returns the word count per praragraph @@ -48,13 +50,57 @@ func ClassicWords(quantity int) string { return buildClassicText(quantity) } +// ClassicGenerate creates classic Lorem Ipsum text with specified paragraphs and sentences. +// Each paragraph contains exactly 'sentences' sentences. +// Each sentence contains 8 words from the classic Lorem Ipsum text. +// +// Example: ClassicGenerate(3, 5) creates 3 paragraphs, each with 5 sentences. +// +// This differs from the CLI behavior where -p and -s are additive. +func ClassicGenerate(paragraphs int, sentences int) Lorem { + if paragraphs <= 0 || sentences <= 0 { + return Lorem{ + Text: "", + Paragraphs: paragraphs, + Sentences: sentences, + WordCount: 0, + } + } + + var b strings.Builder + + // Heuristic to reduce reallocations; exact sizing not required + b.Grow(paragraphs * sentences * 64) + + for p := range paragraphs { + if p > 0 { + b.WriteString("\n\n") + } + for s := range sentences { + if s > 0 { + b.WriteByte(' ') + } + b.WriteString(ClassicSentence()) + } + } + + text := b.String() + + return Lorem{ + Text: text, + Paragraphs: paragraphs, + Sentences: sentences, + WordCount: paragraphs * sentences * ClassicWordsPerSentence(), + } +} + func buildClassicText(wordCount int) string { if wordCount <= 0 { return "" } var b strings.Builder - b.Grow(wordCount * 8) + b.Grow(wordCount * wordsPerSentence) for i := 0; i < wordCount && i < len(classicText); i++ { if i > 0 { @@ -64,5 +110,5 @@ func buildClassicText(wordCount int) string { } result := b.String() - return capitalizeFirstWord(result) + "." + return formatWords(result) } diff --git a/pkg/classic_test.go b/pkg/classic_test.go index 3535c96..7b9a449 100644 --- a/pkg/classic_test.go +++ b/pkg/classic_test.go @@ -68,3 +68,86 @@ func TestClassicWords(t *testing.T) { }) } } + +func TestClassicGenerate(t *testing.T) { + tests := []struct { + name string + paragraphs int + sentences int + wantWords int + }{ + {"1 paragraph 1 sentence", 1, 1, 8}, + {"2 paragraphs 3 sentences", 2, 3, 48}, + {"3 paragraphs 5 sentences", 3, 5, 120}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ClassicGenerate(tt.paragraphs, tt.sentences) + + // Verify metadata + if result.Paragraphs != tt.paragraphs { + t.Errorf("expected %d paragraphs, got %d", tt.paragraphs, result.Paragraphs) + } + if result.Sentences != tt.sentences { + t.Errorf("expected %d sentences, got %d", tt.sentences, result.Sentences) + } + if result.WordCount != tt.wantWords { + t.Errorf("expected %d words, got %d", tt.wantWords, result.WordCount) + } + + // Verify text is not empty + if result.Text == "" { + t.Error("expected non-empty text") + } + + // Verify it's classic text (starts with Lorem) + if !strings.HasPrefix(result.Text, "Lorem") { + t.Errorf("expected classic text to start with 'Lorem', got %q", result.Text[:20]) + } + + // Verify paragraph count + paragraphs := strings.Split(result.Text, "\n\n") + if len(paragraphs) != tt.paragraphs { + t.Errorf("expected %d paragraph blocks, got %d", tt.paragraphs, len(paragraphs)) + } + }) + } +} + +func TestClassicGenerateEdgeCases(t *testing.T) { + t.Run("zero paragraphs", func(t *testing.T) { + result := ClassicGenerate(0, 5) + if result.Text != "" { + t.Errorf("expected empty text for 0 paragraphs, got %q", result.Text) + } + if result.WordCount != 0 { + t.Errorf("expected 0 word count, got %d", result.WordCount) + } + }) + + t.Run("zero sentences", func(t *testing.T) { + result := ClassicGenerate(5, 0) + if result.Text != "" { + t.Errorf("expected empty text for 0 sentences, got %q", result.Text) + } + if result.WordCount != 0 { + t.Errorf("expected 0 word count, got %d", result.WordCount) + } + }) + + t.Run("1x1", func(t *testing.T) { + result := ClassicGenerate(1, 1) + if result.Text == "" { + t.Error("expected non-empty text for 1x1") + } + // Should start with classic Lorem text + if !strings.HasPrefix(result.Text, "Lorem ipsum") { + t.Errorf("expected to start with 'Lorem ipsum', got %q", result.Text[:20]) + } + // Should not contain paragraph separator for single paragraph + if strings.Contains(result.Text, "\n\n") { + t.Error("unexpected paragraph separator in single paragraph") + } + }) +} diff --git a/pkg/root.go b/pkg/root.go index ef71069..379b296 100644 --- a/pkg/root.go +++ b/pkg/root.go @@ -6,6 +6,14 @@ import ( "strings" ) +// Lorem represents generated Lorem Ipsum text with metadata +type Lorem struct { + Text string + Paragraphs int + Sentences int + WordCount int +} + // Get [quantity] words func LoremWords(quantity int) string { if quantity <= 0 { @@ -50,3 +58,47 @@ func Sentence() string { func Paragraph() string { return formatWords(LoremWords(45)) } + +// Generate creates Lorem Ipsum text with specified paragraphs and sentences. +// Each paragraph contains exactly 'sentences' sentences. +// Each sentence contains 8 random words. +// +// Example: Generate(3, 5) creates 3 paragraphs, each with 5 sentences. +// +// This differs from the CLI behavior where -p and -s are additive. +func Generate(paragraphs int, sentences int) Lorem { + if paragraphs <= 0 || sentences <= 0 { + return Lorem{ + Text: "", + Paragraphs: paragraphs, + Sentences: sentences, + WordCount: 0, + } + } + + var b strings.Builder + + // Heuristic to reduce reallocations; exact sizing not required + b.Grow(paragraphs * sentences * 64) + + for p := range paragraphs { + if p > 0 { + b.WriteString("\n\n") + } + for s := range sentences { + if s > 0 { + b.WriteByte(' ') + } + b.WriteString(Sentence()) + } + } + + text := b.String() + + return Lorem{ + Text: text, + Paragraphs: paragraphs, + Sentences: sentences, + WordCount: paragraphs * sentences * 8, + } +} diff --git a/pkg/root_test.go b/pkg/root_test.go index 07c245f..f14f1f5 100644 --- a/pkg/root_test.go +++ b/pkg/root_test.go @@ -96,3 +96,77 @@ func TestParagraph(t *testing.T) { t.Errorf("Expected paragraph to start with an uppercase letter, got %c", paragraph[0]) } } + +func TestGenerate(t *testing.T) { + tests := []struct { + name string + paragraphs int + sentences int + wantWords int + }{ + {"1 paragraph 1 sentence", 1, 1, 8}, + {"2 paragraphs 3 sentences", 2, 3, 48}, + {"3 paragraphs 5 sentences", 3, 5, 120}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Generate(tt.paragraphs, tt.sentences) + + // Verify metadata + if result.Paragraphs != tt.paragraphs { + t.Errorf("expected %d paragraphs, got %d", tt.paragraphs, result.Paragraphs) + } + if result.Sentences != tt.sentences { + t.Errorf("expected %d sentences, got %d", tt.sentences, result.Sentences) + } + if result.WordCount != tt.wantWords { + t.Errorf("expected %d words, got %d", tt.wantWords, result.WordCount) + } + + // Verify text is not empty + if result.Text == "" { + t.Error("expected non-empty text") + } + + // Verify paragraph count + paragraphs := strings.Split(result.Text, "\n\n") + if len(paragraphs) != tt.paragraphs { + t.Errorf("expected %d paragraph blocks, got %d", tt.paragraphs, len(paragraphs)) + } + }) + } +} + +func TestGenerateEdgeCases(t *testing.T) { + t.Run("zero paragraphs", func(t *testing.T) { + result := Generate(0, 5) + if result.Text != "" { + t.Errorf("expected empty text for 0 paragraphs, got %q", result.Text) + } + if result.WordCount != 0 { + t.Errorf("expected 0 word count, got %d", result.WordCount) + } + }) + + t.Run("zero sentences", func(t *testing.T) { + result := Generate(5, 0) + if result.Text != "" { + t.Errorf("expected empty text for 0 sentences, got %q", result.Text) + } + if result.WordCount != 0 { + t.Errorf("expected 0 word count, got %d", result.WordCount) + } + }) + + t.Run("1x1", func(t *testing.T) { + result := Generate(1, 1) + if result.Text == "" { + t.Error("expected non-empty text for 1x1") + } + // Should not contain paragraph separator for single paragraph + if strings.Contains(result.Text, "\n\n") { + t.Error("unexpected paragraph separator in single paragraph") + } + }) +} diff --git a/pkg/utils.go b/pkg/utils.go index c8d0431..5ff0fdc 100644 --- a/pkg/utils.go +++ b/pkg/utils.go @@ -6,6 +6,7 @@ func capitalizeFirstWord(text string) string { if len(text) == 0 { return text } + return strings.ToUpper(text[:1]) + text[1:] } diff --git a/readme.md b/readme.md index 2ac42e0..35adc87 100644 --- a/readme.md +++ b/readme.md @@ -31,6 +31,24 @@ func printTonsOfText() { } ``` +### Structured Text Generation + +```go +func generateStructuredContent() { + // Generate 3 paragraphs with 5 sentences each + result := lorelai.Generate(3, 5) + fmt.Println(result.Text) + fmt.Printf("Generated %d words across %d paragraphs\n", + result.WordCount, result.Paragraphs) +} + +func generateClassicLorem() { + // Generate classic Lorem Ipsum text + result := lorelai.ClassicGenerate(2, 4) + fmt.Println(result.Text) +} +``` + ### Convenience Utilities ```go @@ -45,52 +63,55 @@ For more examples check the [examples directory](./example/main.go). ## Documentation -This package exports 8 functions: +### Core Text Generation Functions -- [Word](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go#L29): - Returns 1 Word +**Random Lorem Ipsum:** -E.g: "sodales", "phasellus" , "diam", etc. +- **[Word](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go)**: Returns 1 random word + E.g: "sodales", "phasellus", "diam" -- [Sentence](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go#L34): - Returns 8 Words +- **[Sentence](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go)**: Returns 8 random words (1 sentence) + E.g: "Varius sed imperdiet amet laoreet ex sapien placerat." -E.g: "Varius sed imperdiet amet laoreet ex sapien placerat.", etc. +- **[Paragraph](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go)**: Returns 45 random words (1 paragraph) + E.g: "Nisi lacinia ante non nunc eros nibh mattis enim orci ante in ornare accumsan..." -- [Paragraph](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go#L39): - Returns 45 Words +- **[Generate](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go)**: Generate structured text with X paragraphs of Y sentences each + Returns: `Lorem{Text, Paragraphs, Sentences, WordCount}` + E.g: `Generate(3, 5)` creates 3 paragraphs with 5 sentences each (120 words) -E.g: "Nisi lacinia ante non nunc eros nibh mattis enim orci ante in ornare -accumsan iaculis vel..." +**Classic Lorem Ipsum:** -- [FormattedLoremWords](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go#L24): - It receives a number and returns a string with the number of words you have - indicated. The first letter will be capital and the sentence will end with a - dot. +- **[ClassicSentence](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/classic.go)**: Returns 8 words from classic text + Always starts with "Lorem ipsum dolor sit amet..." -E.g: "Libero malesuada duis massa luctus.", "Curabitur hendrerit sed.", -"Ligula.", etc. +- **[ClassicParagraph](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/classic.go)**: Returns full classic Lorem paragraph + The traditional Lorem Ipsum text -- [LoremWords](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go#L10): - It receives a number and returns a string with the number of words you have - indicated. +- **[ClassicGenerate](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/classic.go)**: Generate classic text with X paragraphs of Y sentences each + Returns: `Lorem{Text, Paragraphs, Sentences, WordCount}` + E.g: `ClassicGenerate(2, 4)` creates 2 paragraphs with 4 classic sentences each -E.g: "arcu", "blandit porttitor a scelerisque", "donec justo lacinia", etc. +- **[ClassicWords](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/classic.go)**: Returns N words from classic text -- [Domain](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/convenience.go#L10): - Returns a domain +### Word Generation Functions -E.g: "neque.net", "arcu.org" , "lorem.io", etc. +- **[LoremWords](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go)**: Returns N random words + E.g: "arcu", "blandit porttitor a scelerisque", "donec justo lacinia" -- [URL](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/convenience.go#L17): - Returns an URL +- **[FormattedLoremWords](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/root.go)**: Returns N words with capitalization and period + E.g: "Libero malesuada duis massa luctus.", "Curabitur hendrerit sed." + +### Convenience Utilities -E.g: "https://pellentesque.org", "https://id.io" , "https://efficitur.com", etc. +- **[Domain](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/convenience.go)**: Returns a random domain + E.g: "neque.net", "arcu.org", "lorem.io" -- [Email](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/convenience.go#L22): - Returns an email address +- **[URL](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/convenience.go)**: Returns a random URL + E.g: "https://pellentesque.org", "https://id.io", "https://efficitur.com" -E.g: "bibendum@id.pe", "ornare@duis.pe" , "quisque@faucibus.org", etc. +- **[Email](https://github.com/bobadilla-tech/lorelai/blob/main/pkg/convenience.go)**: Returns a random email address + E.g: "bibendum@id.pe", "ornare@duis.pe", "quisque@faucibus.org" ## CLI Tool