const ESC: char = '\u{1b}';
const BEL: char = '\u{7}';
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Printed<'a> {
Text(&'a str),
Link(&'a str),
}
pub(super) fn read(said: &str) -> impl Iterator<Item = Printed<'_>> {
let mut rest = said;
std::iter::from_fn(move || {
loop {
let Some(sequence) = rest.strip_prefix(ESC) else {
if rest.is_empty() {
return None;
}
let (text, after) = rest.split_at(rest.find(ESC).unwrap_or(rest.len()));
rest = after;
return Some(Printed::Text(text));
};
let Some((command, after)) = escape(sequence) else {
rest = "";
return None;
};
rest = after;
if let Some(target) = command.and_then(hyperlink_target) {
return Some(Printed::Link(target));
}
}
})
}
fn escape(sequence: &str) -> Option<(Option<&str>, &str)> {
let first = sequence.chars().next()?;
match first {
'[' => {
let body = &sequence[1..];
let end = body.find(|c| !matches!(c, '\u{20}'..='\u{3f}'))?;
Some((None, past_final(&body[end..], '\u{40}'..='\u{7e}')))
}
']' | 'P' | 'X' | '^' | '_' => {
let body = &sequence[1..];
let end = if first == ']' {
body.find([BEL, ESC])
} else {
body.find(ESC)
};
let (string, ended) = body.split_at(end?);
match ended
.strip_prefix(BEL)
.or_else(|| ended.strip_prefix("\u{1b}\\"))
{
Some(after) => Some(((first == ']').then_some(string), after)),
None if ended.len() == ESC.len_utf8() => None,
None => Some((None, ended)),
}
}
'\u{20}'..='\u{2f}' => {
let end = sequence.find(|c| !matches!(c, '\u{20}'..='\u{2f}'))?;
Some((None, past_final(&sequence[end..], '\u{30}'..='\u{7e}')))
}
'\u{30}'..='\u{7e}' => Some((None, &sequence[1..])),
_ => Some((None, sequence)),
}
}
fn past_final(rest: &str, finals: std::ops::RangeInclusive<char>) -> &str {
match rest.chars().next() {
Some(last) if finals.contains(&last) => &rest[last.len_utf8()..],
_ => rest,
}
}
fn hyperlink_target(command: &str) -> Option<&str> {
let (_, target) = command.strip_prefix("8;")?.split_once(';')?;
(!target.is_empty()).then_some(target)
}
#[cfg(test)]
mod tests {
use super::*;
fn pieces(said: &str) -> Vec<Printed<'_>> {
read(said).collect()
}
#[test]
fn a_hyperlink_is_where_it_goes_and_then_its_text() {
for (opens, ends) in [
("\u{1b}]8;;https://a.b/c\u{7}", "\u{1b}]8;;\u{7}"),
("\u{1b}]8;;https://a.b/c\u{1b}\\", "\u{1b}]8;;\u{1b}\\"),
("\u{1b}]8;id=1:x=y;https://a.b/c\u{7}", "\u{1b}]8;;\u{7}"),
] {
assert_eq!(
pieces(&format!("visit {opens}sign in{ends}.")),
[
Printed::Text("visit "),
Printed::Link("https://a.b/c"),
Printed::Text("sign in"),
Printed::Text("."),
],
"{opens:?}"
);
}
assert_eq!(
pieces("\u{1b}]8;;https://a.b/c;d=e\u{7}"),
[Printed::Link("https://a.b/c;d=e")]
);
}
#[test]
fn no_byte_of_another_sequence_is_read() {
let sequences = [
"\u{1b}[94m",
"\u{1b}[38;2;1;2;3m",
"\u{1b}[?25l",
"\u{1b}]0;https://title.example\u{7}",
"\u{1b}]0;https://title.example\u{1b}\\",
"\u{1b}(B",
"\u{1b}P1$r0m\u{1b}\\",
"\u{1b}_Ga=T;AAAA\u{1b}\\",
"\u{1b}7",
];
for sequence in sequences {
assert_eq!(
pieces(&format!("a{sequence}b")),
[Printed::Text("a"), Printed::Text("b")],
"{sequence:?}"
);
}
}
#[test]
fn only_an_operating_system_command_ends_at_bel() {
for opens in ["\u{1b}P", "\u{1b}X", "\u{1b}^", "\u{1b}_"] {
assert_eq!(
pieces(&format!("a{opens}1\u{7}https://x.y\u{1b}\\b")),
[Printed::Text("a"), Printed::Text("b")],
"{opens:?}"
);
assert_eq!(
pieces(&format!("a{opens}1\u{7}https://x.y")),
[Printed::Text("a")],
"{opens:?}"
);
}
assert_eq!(
pieces("a\u{1b}]0;title\u{7}https://x.y"),
[Printed::Text("a"), Printed::Text("https://x.y")]
);
}
#[test]
fn a_string_another_escape_breaks_into_is_cancelled() {
assert_eq!(
pieces("\u{1b}]8;;https://a.b/c\u{1b}[0mtext"),
[Printed::Text("text")]
);
}
#[test]
fn a_sequence_not_finished_yet_ends_what_there_is() {
for said in [
"a\u{1b}",
"a\u{1b}[",
"a\u{1b}[38;5",
"a\u{1b}(",
"a\u{1b}]8;;https://a.b/c",
"a\u{1b}]8;;https://a.b/c\u{1b}",
] {
assert_eq!(pieces(said), [Printed::Text("a")], "{said:?}");
}
}
#[test]
fn a_sequence_broken_off_ends_where_it_was_broken_off() {
assert_eq!(pieces("\u{1b}[1\u{e9}a"), [Printed::Text("\u{e9}a")]);
assert_eq!(pieces("\u{1b}\u{e9}a"), [Printed::Text("\u{e9}a")]);
}
}