diff --git a/crates/ipx/src/signals.rs b/crates/ipx/src/signals.rs index 1159a98..345dd45 100644 --- a/crates/ipx/src/signals.rs +++ b/crates/ipx/src/signals.rs @@ -248,21 +248,49 @@ fn chosen(answer: &serde_json::Value, candidates: &[(String, String)]) -> Option candidates.iter().find(|(id, _)| id == c).map(|(id, _)| id.clone()) } -/// Show notes are HTML: Jev is given their words. +/// Show notes are HTML: Jev is given their words, and an image's alt and title text, which is all +/// the words some posts have. xkcd's is a comic and its hover text; dropping the tag whole sent Jev +/// "Ground Effect" alone, an F1 aerodynamics term, and the comic went into Formula 1 (#267). fn text_of(html: &str) -> String { let mut out = String::with_capacity(html.len()); - let mut in_tag = false; + let mut tag: Option = None; for c in html.chars() { - match c { - '<' => in_tag = true, - '>' if in_tag => { in_tag = false; out.push(' '); } - _ if !in_tag => out.push(c), - _ => {} + match (&mut tag, c) { + (None, '<') => tag = Some(String::new()), + (Some(t), '>') => { + let mut said: Vec<&str> = Vec::new(); + for name in ["alt", "title"] { + if let Some(v) = attr(t, name).filter(|v| !v.trim().is_empty() && !said.contains(v)) { + said.push(v); + } + } + for v in said { + out.push(' '); + out.push_str(v); + } + out.push(' '); + tag = None; + } + (Some(t), c) => t.push(c), + (None, c) => out.push(c), } } out.split_whitespace().collect::>().join(" ") } +/// A quoted attribute's value in a tag's text, `alt="..."` or `alt='...'`. +fn attr<'a>(tag: &'a str, name: &str) -> Option<&'a str> { + let lower = tag.to_ascii_lowercase(); + for q in ['"', '\''] { + let key = format!(" {name}={q}"); + if let Some(at) = lower.find(&key) { + let rest = &tag[at + key.len()..]; + return rest.find(q).map(|end| &rest[..end]); + } + } + None +} + #[cfg(test)] mod tests { use super::*; @@ -291,6 +319,14 @@ mod tests { assert_eq!(chosen(&stray, &offered), None, "never one it was not offered"); } + #[test] + fn an_images_words_are_read_with_the_text() { + let xkcd = r#"Runners wear sneakers."#; + assert_eq!(text_of(xkcd), "Runners wear sneakers.", "alt and title once, when they say the same"); + assert_eq!(text_of("

Qualifying report

A grid"), "Qualifying report A grid"); + assert_eq!(text_of(r#""#), ""); + } + #[test] fn a_batch_is_full_by_its_rows_not_its_items() { let item = |n: usize| Unjudged {