use std::fmt::Write as _;
use termimad::MadSkin;
use termimad::crossterm::style::Attribute;
use termimad::minimad::Alignment;
use crate::render::image::Inline;
use crate::render::style::{Painter, Palette};
const MARGIN: &str = "\u{258f} ";
#[must_use]
pub fn render(body: &str, width: usize) -> String {
skin().text(body, Some(width.max(20))).to_string()
}
#[must_use]
pub fn quoted(body: &str, width: usize, paint: Painter, images: &Inline) -> String {
let bar = paint.paint(MARGIN, Palette::untrusted());
let inner = width.saturating_sub(MARGIN.chars().count());
let mut out = String::with_capacity(body.len() * 2);
if images.is_empty() {
push_rendered(&mut out, body, inner, &bar);
return out;
}
let mut run = String::new();
for line in body.lines() {
if let Some(picture) = image_reference(line).and_then(|(_, url)| images.get(url)) {
push_rendered(&mut out, &run, inner, &bar);
run.clear();
let _ = write!(out, "{bar}{}", picture.escape);
let _ = writeln!(
out,
"{bar}{}",
paint.paint(&picture.caption, Palette::label())
);
} else {
run.push_str(line);
run.push('\n');
}
}
push_rendered(&mut out, &run, inner, &bar);
out
}
fn push_rendered(out: &mut String, body: &str, inner: usize, bar: &str) {
if body.trim().is_empty() {
return;
}
for line in render(body, inner).lines() {
let _ = writeln!(out, "{bar}{}", line.trim_end());
}
}
#[must_use]
pub fn image_reference(line: &str) -> Option<(&str, &str)> {
let line = line.trim();
let rest = line.strip_prefix("?;
let url = rest.strip_suffix(')')?;
(!url.is_empty() && !url.contains(char::is_whitespace)).then_some((alt, url))
}
#[must_use]
pub fn image_references(body: &str) -> Vec<(&str, &str)> {
body.lines().filter_map(image_reference).collect()
}
fn skin() -> MadSkin {
let mut skin = MadSkin::default();
for header in &mut skin.headers {
header.align = Alignment::Left;
}
skin.headers[0].add_attr(Attribute::Bold);
skin
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn markup_becomes_style_not_syntax() {
let out = render("**loud**", 40);
assert!(out.contains("loud"));
assert!(!out.contains("**"));
}
#[test]
fn every_line_of_a_quoted_block_carries_the_margin() {
let out = quoted(
"# Title\n\nbody text\n",
40,
Painter::plain(),
&Inline::default(),
);
assert!(out.lines().count() >= 2);
assert!(out.lines().all(|line| line.starts_with(MARGIN)));
}
#[test]
fn a_blank_line_inside_the_block_is_still_marked() {
let out = quoted("one\n\ntwo", 40, Painter::plain(), &Inline::default());
assert!(out.lines().any(|line| line.trim_end() == MARGIN.trim_end()));
}
fn picture() -> Inline {
let mut inline = Inline::default();
inline.insert(
"/ajax/v2/attachments/29?inline=true".to_owned(),
crate::render::image::Picture {
escape: "<PICTURE>\n".to_owned(),
caption: "screenshot.png".to_owned(),
},
);
inline
}
#[test]
fn a_picture_replaces_the_reference_that_names_it() {
let body = "before\n\n\n\nafter";
let out = quoted(body, 60, Painter::plain(), &picture());
let lines: Vec<&str> = out.lines().collect();
let at = lines
.iter()
.position(|line| line.contains("<PICTURE>"))
.expect("the picture was not drawn");
assert!(lines[..at].iter().any(|line| line.contains("before")));
assert!(lines[at..].iter().any(|line| line.contains("after")));
assert!(lines[at + 1].contains("screenshot.png"));
assert!(lines[at + 1].starts_with(MARGIN));
assert!(!out.contains("!["));
}
#[test]
fn an_unmatched_reference_is_left_alone() {
let body = "";
let out = quoted(body, 60, Painter::plain(), &picture());
assert!(out.contains("shot"));
assert!(!out.contains("<PICTURE>"));
}
#[test]
fn only_a_whole_line_reference_is_a_picture() {
assert_eq!(
image_reference(""),
Some(("alt", "/a/b.png"))
);
assert_eq!(image_reference("  "), Some(("", "/a/b.png")));
assert_eq!(image_reference("see  here"), None);
assert_eq!(image_reference("[link](/a/b)"), None);
assert_eq!(image_reference("![alt]()"), None);
}
#[test]
fn lines_do_not_carry_trailing_padding() {
let out = quoted("short", 60, Painter::plain(), &Inline::default());
assert!(out.lines().all(|line| !line.ends_with(' ')));
}
}