//! Inline markup flattened to plain text. //! //! Plain text has no markup to carry emphasis, so it is dropped and the words //! kept. A link becomes `label (url)`: self-contained, and readable without //! scrolling to a reference list somewhere else. use itsybitsy_core::ir::{Doc, Inline}; /// Flatten a run of inlines, spelling out where each link points. pub fn flatten(inline: &[Inline]) -> String { let mut out = String::new(); write(inline, &mut out); out } fn write(inline: &[Inline], out: &mut String) { for item in inline { match item { Inline::Text(text) => out.push_str(text), // Backticks are the only markup worth keeping: they mark where a // literal begins and end, which prose otherwise cannot show. Inline::Code(code) => out.push_str(&format!("`{code}`")), Inline::Emph(inner) | Inline::Strong(inner) | Inline::Strike(inner) => { write(inner, out) } Inline::Link { href, label, .. } => { let text = Doc::plain_text(label); write_target(&text, href, out); } Inline::Image { src, alt, .. } => { let text = Doc::plain_text(alt); write_target(&text, src, out); } Inline::SoftBreak => out.push(' '), // A hard break is honoured by the layout, which splits on it. Inline::HardBreak => out.push('\n'), // Raw markup shown literally would be worse than leaving it out. Inline::Html(_) => {} } } } /// `label (url)`, or just the url when the label would repeat it. fn write_target(label: &str, url: &str, out: &mut String) { let label = label.trim(); if label.is_empty() || label == url { out.push_str(url); } else { out.push_str(&format!("{label} ({url})")); } } #[cfg(test)] mod tests { use super::*; fn text(value: &str) -> Vec { vec![Inline::Text(value.into())] } #[test] fn emphasis_is_dropped_and_its_words_kept() { let inline = vec![ Inline::Text("a ".into()), Inline::Strong(text("b")), Inline::Text(" ".into()), Inline::Emph(text("c")), ]; assert_eq!(flatten(&inline), "a b c"); } #[test] fn code_keeps_its_backticks_to_mark_where_a_literal_ends() { assert_eq!(flatten(&[Inline::Code("x = 1".into())]), "`x = 1`"); } #[test] fn a_link_spells_out_where_it_points() { let inline = vec![Inline::Link { href: "/about".into(), title: None, label: text("about page") }]; assert_eq!(flatten(&inline), "about page (/about)"); } #[test] fn a_link_whose_label_is_its_url_is_not_repeated() { let url = "https://example.com/"; let inline = vec![Inline::Link { href: url.into(), title: None, label: text(url) }]; assert_eq!(flatten(&inline), url); } #[test] fn an_image_is_treated_like_a_link() { let inline = vec![Inline::Image { src: "/i.png".into(), title: None, alt: text("a photo") }]; assert_eq!(flatten(&inline), "a photo (/i.png)"); } #[test] fn raw_html_is_left_out() { let inline = vec![Inline::Text("a".into()), Inline::Html("
".into())]; assert_eq!(flatten(&inline), "a"); } }