use owo_colors::OwoColorize;
use crate::render::theme;
pub fn body(text: &str) -> String {
text.lines().map(line).collect::<Vec<_>>().join("\n")
}
fn line(raw: &str) -> String {
if raw.trim().is_empty() {
return String::new();
}
if let Some(heading) = raw.strip_prefix("### ") {
return format!(" {}", inline(heading).style(theme::strong()));
}
if let Some(item) = raw.strip_prefix("- ") {
return format!(" {} {}", "·".style(theme::accent()), inline(item));
}
format!(" {}", inline(raw))
}
fn inline(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some((before, delimiter, after)) = next_delimiter(rest) {
out.push_str(before);
match after.find(delimiter) {
Some(end) => {
let inner = &after[..end];
out.push_str(&format!("{}", inner.style(style_of(delimiter))));
rest = &after[end + delimiter.len()..];
}
None => {
out.push_str(delimiter);
rest = after;
}
}
}
out.push_str(rest);
out
}
fn style_of(delimiter: &str) -> owo_colors::Style {
match delimiter {
"**" => theme::strong(),
"*" => theme::emphasis(),
_ => theme::accent(),
}
}
fn next_delimiter(text: &str) -> Option<(&str, &'static str, &str)> {
let bytes = text.as_bytes();
for at in 0..bytes.len() {
let delimiter = match bytes[at] {
b'*' if bytes[at..].starts_with(b"**") => "**",
b'*' => "*",
b'`' => "`",
_ => continue,
};
return Some((&text[..at], delimiter, &text[at + delimiter.len()..]));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn plain(text: &str) -> String {
let rendered = body(text);
let mut out = String::with_capacity(rendered.len());
let mut rest = rendered.as_str();
while let Some(at) = rest.find('\u{1b}') {
out.push_str(&rest[..at]);
rest = match rest[at..].find('m') {
Some(end) => &rest[at + end + 1..],
None => "",
};
}
out.push_str(rest);
out
}
#[test]
fn it_should_indent_a_paragraph() {
assert_eq!(plain("Una línea."), " Una línea.");
}
#[test]
fn it_should_leave_a_blank_line_blank() {
assert_eq!(plain("Uno.\n\nDos."), " Uno.\n\n Dos.");
}
#[test]
fn it_should_draw_a_bullet_for_a_list_item() {
assert_eq!(plain("- Algo"), " · Algo");
}
#[test]
fn it_should_drop_the_hashes_of_a_sub_heading() {
assert_eq!(plain("### Un título"), " Un título");
}
#[test]
fn it_should_drop_the_asterisks_around_bold_text() {
assert_eq!(plain("Esto es **importante**."), " Esto es importante.");
}
#[test]
fn it_should_drop_the_backticks_around_a_code_span() {
assert_eq!(
plain("Ejecuta `fundaia status`."),
" Ejecuta fundaia status."
);
}
#[test]
fn it_should_handle_two_code_spans_in_one_line() {
assert_eq!(plain("`uno` y `dos`"), " uno y dos");
}
#[test]
fn it_should_drop_the_asterisks_around_emphasis() {
assert_eq!(plain("qué había *aquí*."), " qué había aquí.");
}
#[test]
fn it_should_read_a_double_asterisk_as_bold_and_not_as_two_emphases() {
assert_eq!(plain("**dos** y *una*"), " dos y una");
}
#[test]
fn it_should_leave_a_delimiter_that_was_never_closed_alone() {
assert_eq!(plain("2 ** 3 es ocho"), " 2 ** 3 es ocho");
}
#[test]
fn it_should_leave_a_lone_asterisk_where_it_was_written() {
assert_eq!(plain("un * suelto"), " un * suelto");
}
#[test]
fn it_should_leave_an_unknown_marker_where_it_was_written() {
assert_eq!(plain("> una cita"), " > una cita");
}
}