103 lines
3.3 KiB
Rust
103 lines
3.3 KiB
Rust
//! 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<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");
|
|
}
|
|
}
|