itsybitsy/text/src/inline.rs

104 lines
3.3 KiB
Rust
Raw Normal View History

2026-10-04 20:27:29 +03:00
//! 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.
2026-10-04 20:27:29 +03:00
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<Inline> {
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("<br>".into())];
assert_eq!(flatten(&inline), "a");
}
}