pub mod output;
use crate::error::GivError;
pub use output::LoremOutput;
use rand::Rng;
pub const DEFAULT_COUNT: usize = 50;
const WORDS_PER_SENTENCE_RANGE: std::ops::Range<usize> = 8..16;
const SENTENCES_PER_PARAGRAPH_RANGE: std::ops::Range<usize> = 3..7;
const SOURCE_WORD_COUNT: usize = 500;
const CLASSIC_OPENING_WORDS: usize = 5;
fn get_classic_words() -> Vec<&'static str> {
lipsum::LOREM_IPSUM
.split_whitespace()
.map(|word| word.trim_end_matches(|c: char| !c.is_alphabetic()))
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoremUnit {
Words,
Sentences,
Paragraphs,
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
}
fn clean_spacing(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut prev_was_space = false;
for ch in text.chars() {
if ch == ' ' {
if !prev_was_space {
result.push(ch);
prev_was_space = true;
}
} else {
result.push(ch);
prev_was_space = false;
}
}
result.replace(" .", ".")
}
fn generate_words(count: usize, start_with_classic: bool) -> Vec<String> {
use rand::prelude::IndexedRandom;
let mut words = Vec::with_capacity(count);
if start_with_classic && count > 0 {
let classic_words = get_classic_words();
let classic_count = CLASSIC_OPENING_WORDS.min(count);
for (i, word) in classic_words.iter().take(classic_count).enumerate() {
if i == 0 {
words.push(word.to_string());
} else {
words.push(word.to_lowercase());
}
}
if count > classic_count {
let remaining = count - classic_count;
let source_text = lipsum::lipsum(SOURCE_WORD_COUNT);
let source_words: Vec<&str> = source_text.split_whitespace().collect();
let mut rng = rand::rng();
for _ in 0..remaining {
let word = source_words
.choose(&mut rng)
.unwrap_or(&"lorem")
.trim_end_matches(|c: char| !c.is_alphabetic())
.to_lowercase();
words.push(word.to_string());
}
}
} else {
let source_text = lipsum::lipsum(SOURCE_WORD_COUNT);
let source_words: Vec<&str> = source_text.split_whitespace().collect();
let mut rng = rand::rng();
for _ in 0..count {
let word = source_words
.choose(&mut rng)
.unwrap_or(&"lorem")
.trim_end_matches(|c: char| !c.is_alphabetic())
.to_lowercase();
words.push(word.to_string());
}
}
words
}
fn get_some_words(word_range: std::ops::Range<usize>, start_with_classic: bool) -> String {
let mut rng = rand::rng();
let word_count = rng.random_range(word_range);
let words = generate_words(word_count, start_with_classic);
if words.is_empty() {
return String::from(".");
}
let mut sentence = if start_with_classic {
words[0].clone()
} else {
capitalize_first(&words[0])
};
for word in &words[1..] {
sentence.push(' ');
sentence.push_str(word);
}
sentence.push('.');
sentence
}
fn get_some_sentences(sentence_range: std::ops::Range<usize>, start_with_classic: bool) -> String {
let mut rng = rand::rng();
let sentence_count = rng.random_range(sentence_range);
(0..sentence_count)
.enumerate()
.map(|(i, _)| {
let use_classic = start_with_classic && i == 0;
get_some_words(WORDS_PER_SENTENCE_RANGE, use_classic)
})
.collect::<Vec<_>>()
.join(" ")
}
fn generate_lorem_text(count: usize, unit: LoremUnit) -> Result<String, GivError> {
if count == 0 {
return Ok(String::new());
}
let text = match unit {
LoremUnit::Words => {
let words = generate_words(count, true);
let mut result = words.join(" ");
result.push('.');
result
}
LoremUnit::Sentences => {
(0..count)
.enumerate()
.map(|(i, _)| {
get_some_words(WORDS_PER_SENTENCE_RANGE, i == 0)
})
.collect::<Vec<_>>()
.join(" ")
}
LoremUnit::Paragraphs => {
(0..count)
.enumerate()
.map(|(i, _)| {
if i == 0 {
lipsum::LOREM_IPSUM
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
} else {
get_some_sentences(SENTENCES_PER_PARAGRAPH_RANGE, false)
}
})
.collect::<Vec<_>>()
.join("\n\n")
}
};
Ok(clean_spacing(&text))
}
pub fn generate_lorem(count: Option<usize>, unit: LoremUnit) -> Result<LoremOutput, GivError> {
let count = count.unwrap_or(DEFAULT_COUNT);
let lorem = generate_lorem_text(count, unit)?;
Ok(LoremOutput { lorem })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_lorem() {
let text = generate_lorem_text(10, LoremUnit::Words).unwrap();
assert!(!text.is_empty());
let text = generate_lorem_text(3, LoremUnit::Sentences).unwrap();
assert!(!text.is_empty());
assert!(text.contains('.'));
let text = generate_lorem_text(2, LoremUnit::Paragraphs).unwrap();
assert!(!text.is_empty());
assert!(text.contains("\n\n") || text.split("\n\n").count() >= 1);
}
#[test]
fn test_generate_lorem_zero() {
let text = generate_lorem_text(0, LoremUnit::Words).unwrap();
assert!(text.is_empty() || text.len() < 10);
}
#[test]
fn test_words_classic_opening() {
let text = generate_lorem_text(5, LoremUnit::Words).unwrap();
assert_eq!(text, "Lorem ipsum dolor sit amet.");
let text = generate_lorem_text(3, LoremUnit::Words).unwrap();
assert_eq!(text, "Lorem ipsum dolor.");
let text = generate_lorem_text(10, LoremUnit::Words).unwrap();
assert!(text.starts_with("Lorem ipsum dolor sit amet"));
}
#[test]
fn test_sentences_classic_opening() {
let text = generate_lorem_text(1, LoremUnit::Sentences).unwrap();
assert!(text.starts_with("Lorem ipsum dolor sit amet"));
let text = generate_lorem_text(3, LoremUnit::Sentences).unwrap();
assert!(text.starts_with("Lorem ipsum dolor sit amet"));
}
#[test]
fn test_paragraphs_classic_opening() {
let text = generate_lorem_text(1, LoremUnit::Paragraphs).unwrap();
let classic = lipsum::LOREM_IPSUM
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
assert_eq!(text, classic);
let text = generate_lorem_text(2, LoremUnit::Paragraphs).unwrap();
assert!(text.starts_with(&classic));
assert!(text.contains("\n\n"));
}
#[test]
fn test_get_classic_words() {
let words = get_classic_words();
assert!(!words.is_empty());
assert_eq!(words[0], "Lorem");
assert_eq!(words[1], "ipsum");
assert_eq!(words[2], "dolor");
assert_eq!(words[3], "sit");
assert_eq!(words[4], "amet");
}
#[test]
fn test_generate_words() {
let words = generate_words(5, true);
assert_eq!(words.len(), 5);
assert_eq!(words[0], "Lorem");
assert_eq!(words[1], "ipsum");
assert_eq!(words[2], "dolor");
assert_eq!(words[3], "sit");
assert_eq!(words[4], "amet");
let words = generate_words(5, false);
assert_eq!(words.len(), 5);
assert_ne!(words[0], "Lorem");
}
}