diff --git a/Cargo.lock b/Cargo.lock index 8f8a209a..7cbf3ddd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,10 +170,11 @@ dependencies = [ name = "anstyle-svg" version = "0.1.7" dependencies = [ - "anstream 0.6.18", "anstyle 1.0.10", "anstyle-lossy", + "anstyle-parse 0.2.6", "html-escape", + "proptest", "snapbox", "unicode-width", ] diff --git a/crates/anstream/src/adapter/wincon.rs b/crates/anstream/src/adapter/wincon.rs index f9003fa6..c2114919 100644 --- a/crates/anstream/src/adapter/wincon.rs +++ b/crates/anstream/src/adapter/wincon.rs @@ -106,102 +106,102 @@ impl anstyle_parse::Perform for WinconCapture { let mut style = self.style; // param/value differences are dependent on the escape code - let mut state = State::Normal; + let mut state = CsiState::Normal; let mut r = None; let mut g = None; let mut color_target = ColorTarget::Fg; for param in params { for value in param { match (state, *value) { - (State::Normal, 0) => { + (CsiState::Normal, 0) => { style = anstyle::Style::default(); break; } - (State::Normal, 1) => { + (CsiState::Normal, 1) => { style = style.bold(); break; } - (State::Normal, 2) => { + (CsiState::Normal, 2) => { style = style.dimmed(); break; } - (State::Normal, 3) => { + (CsiState::Normal, 3) => { style = style.italic(); break; } - (State::Normal, 4) => { + (CsiState::Normal, 4) => { style = style.underline(); - state = State::Underline; + state = CsiState::Underline; } - (State::Normal, 21) => { + (CsiState::Normal, 21) => { style |= anstyle::Effects::DOUBLE_UNDERLINE; break; } - (State::Normal, 7) => { + (CsiState::Normal, 7) => { style = style.invert(); break; } - (State::Normal, 8) => { + (CsiState::Normal, 8) => { style = style.hidden(); break; } - (State::Normal, 9) => { + (CsiState::Normal, 9) => { style = style.strikethrough(); break; } - (State::Normal, 30..=37) => { + (CsiState::Normal, 30..=37) => { let color = to_ansi_color(value - 30).expect("within 4-bit range"); style = style.fg_color(Some(color.into())); break; } - (State::Normal, 38) => { + (CsiState::Normal, 38) => { color_target = ColorTarget::Fg; - state = State::PrepareCustomColor; + state = CsiState::PrepareCustomColor; } - (State::Normal, 39) => { + (CsiState::Normal, 39) => { style = style.fg_color(None); break; } - (State::Normal, 40..=47) => { + (CsiState::Normal, 40..=47) => { let color = to_ansi_color(value - 40).expect("within 4-bit range"); style = style.bg_color(Some(color.into())); break; } - (State::Normal, 48) => { + (CsiState::Normal, 48) => { color_target = ColorTarget::Bg; - state = State::PrepareCustomColor; + state = CsiState::PrepareCustomColor; } - (State::Normal, 49) => { + (CsiState::Normal, 49) => { style = style.bg_color(None); break; } - (State::Normal, 58) => { + (CsiState::Normal, 58) => { color_target = ColorTarget::Underline; - state = State::PrepareCustomColor; + state = CsiState::PrepareCustomColor; } - (State::Normal, 90..=97) => { + (CsiState::Normal, 90..=97) => { let color = to_ansi_color(value - 90) .expect("within 4-bit range") .bright(true); style = style.fg_color(Some(color.into())); break; } - (State::Normal, 100..=107) => { + (CsiState::Normal, 100..=107) => { let color = to_ansi_color(value - 100) .expect("within 4-bit range") .bright(true); style = style.bg_color(Some(color.into())); break; } - (State::PrepareCustomColor, 5) => { - state = State::Ansi256; + (CsiState::PrepareCustomColor, 5) => { + state = CsiState::Ansi256; } - (State::PrepareCustomColor, 2) => { - state = State::Rgb; + (CsiState::PrepareCustomColor, 2) => { + state = CsiState::Rgb; r = None; g = None; } - (State::Ansi256, n) => { + (CsiState::Ansi256, n) => { let color = anstyle::Ansi256Color(n as u8); style = match color_target { ColorTarget::Fg => style.fg_color(Some(color.into())), @@ -210,7 +210,7 @@ impl anstyle_parse::Perform for WinconCapture { }; break; } - (State::Rgb, b) => match (r, g) { + (CsiState::Rgb, b) => match (r, g) { (None, _) => { r = Some(b); } @@ -227,29 +227,29 @@ impl anstyle_parse::Perform for WinconCapture { break; } }, - (State::Underline, 0) => { + (CsiState::Underline, 0) => { style = style.effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)); } - (State::Underline, 1) => { + (CsiState::Underline, 1) => { // underline already set } - (State::Underline, 2) => { + (CsiState::Underline, 2) => { style = style .effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)) | anstyle::Effects::DOUBLE_UNDERLINE; } - (State::Underline, 3) => { + (CsiState::Underline, 3) => { style = style .effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)) | anstyle::Effects::CURLY_UNDERLINE; } - (State::Underline, 4) => { + (CsiState::Underline, 4) => { style = style .effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)) | anstyle::Effects::DOTTED_UNDERLINE; } - (State::Underline, 5) => { + (CsiState::Underline, 5) => { style = style .effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)) | anstyle::Effects::DASHED_UNDERLINE; @@ -269,7 +269,7 @@ impl anstyle_parse::Perform for WinconCapture { } #[derive(Copy, Clone, PartialEq, Eq, Debug)] -enum State { +enum CsiState { Normal, PrepareCustomColor, Ansi256, @@ -301,7 +301,6 @@ fn to_ansi_color(digit: u16) -> Option { #[cfg(test)] mod test { use super::*; - use owo_colors::OwoColorize as _; use proptest::prelude::*; #[track_caller] @@ -317,12 +316,10 @@ mod test { #[test] fn start() { - let input = format!("{} world!", "Hello".green().on_red()); + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!("{green_on_red}Hello{green_on_red:#} world!"); let expected = vec![ - ( - anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red), - "Hello", - ), + (green_on_red, "Hello"), (anstyle::Style::default(), " world!"), ]; verify(&input, expected); @@ -330,13 +327,11 @@ mod test { #[test] fn middle() { - let input = format!("Hello {}!", "world".green().on_red()); + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!("Hello {green_on_red}world{green_on_red:#}!"); let expected = vec![ (anstyle::Style::default(), "Hello "), - ( - anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red), - "world", - ), + (green_on_red, "world"), (anstyle::Style::default(), "!"), ]; verify(&input, expected); @@ -344,27 +339,23 @@ mod test { #[test] fn end() { - let input = format!("Hello {}", "world!".green().on_red()); + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!("Hello {green_on_red}world!{green_on_red:#}"); let expected = vec![ (anstyle::Style::default(), "Hello "), - ( - anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red), - "world!", - ), + (green_on_red, "world!"), ]; verify(&input, expected); } #[test] fn ansi256_colors() { + let ansi_11 = anstyle::Ansi256Color(11).on_default(); // termcolor only supports "brights" via these - let input = format!( - "Hello {}!", - "world".color(owo_colors::XtermColors::UserBrightYellow) - ); + let input = format!("Hello {ansi_11}world{ansi_11:#}!"); let expected = vec![ (anstyle::Style::default(), "Hello "), - (anstyle::Ansi256Color(11).on_default(), "world"), + (ansi_11, "world"), (anstyle::Style::default(), "!"), ]; verify(&input, expected); diff --git a/crates/anstyle-svg/Cargo.toml b/crates/anstyle-svg/Cargo.toml index 0a206317..7c8c3d89 100644 --- a/crates/anstyle-svg/Cargo.toml +++ b/crates/anstyle-svg/Cargo.toml @@ -25,12 +25,13 @@ pre-release-replacements = [ [dependencies] anstyle = { version = "1.0.0", path = "../anstyle" } -anstream = { version = "0.6", path = "../anstream", default-features = false } +anstyle-parse = { version = "0.2.6", path = "../anstyle-parse" } anstyle-lossy = { version = "1.0.0", path = "../anstyle-lossy" } html-escape = "0.2.13" unicode-width = "0.2.0" [dev-dependencies] +proptest = "1.5.0" snapbox = "0.6.5" [lints] diff --git a/crates/anstyle-svg/src/adapter.rs b/crates/anstyle-svg/src/adapter.rs new file mode 100644 index 00000000..4cb6467b --- /dev/null +++ b/crates/anstyle-svg/src/adapter.rs @@ -0,0 +1,622 @@ +/// Incrementally convert to styled string fragments for non-contiguous data +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub(crate) struct AnsiBytes { + parser: anstyle_parse::Parser, + capture: AnsiCapture, +} + +impl AnsiBytes { + /// Initial state + pub(crate) fn new() -> Self { + Default::default() + } + + /// Strip the next segment of data + pub(crate) fn extract_next<'s>(&'s mut self, bytes: &'s [u8]) -> AnsiBytesIter<'s> { + self.capture.reset(); + self.capture.printable.reserve(bytes.len()); + AnsiBytesIter { + bytes, + parser: &mut self.parser, + capture: &mut self.capture, + } + } +} + +/// See [`AnsiBytes`] +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct AnsiBytesIter<'s> { + bytes: &'s [u8], + parser: &'s mut anstyle_parse::Parser, + capture: &'s mut AnsiCapture, +} + +impl Iterator for AnsiBytesIter<'_> { + type Item = Element; + + #[inline] + fn next(&mut self) -> Option { + next_bytes(&mut self.bytes, self.parser, self.capture) + } +} + +#[inline] +fn next_bytes( + bytes: &mut &[u8], + parser: &mut anstyle_parse::Parser, + capture: &mut AnsiCapture, +) -> Option { + capture.reset(); + while capture.ready.is_none() { + let byte = if let Some((byte, remainder)) = (*bytes).split_first() { + *bytes = remainder; + *byte + } else { + break; + }; + parser.advance(capture, byte); + } + if capture.printable.is_empty() { + return None; + } + + let (style, url) = capture.ready.clone().unwrap_or((capture.style, None)); + Some(Element { + text: std::mem::take(&mut capture.printable), + style, + url, + }) +} + +#[derive(Default, Clone, Debug, PartialEq, Eq)] +struct AnsiCapture { + style: anstyle::Style, + printable: String, + hyperlink: Option, + ready: Option<(anstyle::Style, Option)>, +} + +impl AnsiCapture { + fn reset(&mut self) { + self.ready = None; + } +} + +impl anstyle_parse::Perform for AnsiCapture { + /// Draw a character to the screen and update states. + fn print(&mut self, c: char) { + self.printable.push(c); + } + + /// Execute a C0 or C1 control function. + fn execute(&mut self, byte: u8) { + if byte.is_ascii_whitespace() { + self.printable.push(byte as char); + } + } + + fn csi_dispatch( + &mut self, + params: &anstyle_parse::Params, + _intermediates: &[u8], + ignore: bool, + action: u8, + ) { + if ignore { + return; + } + if action != b'm' { + return; + } + + let mut style = self.style; + // param/value differences are dependent on the escape code + let mut state = CsiState::Normal; + let mut r = None; + let mut g = None; + let mut color_target = ColorTarget::Fg; + for param in params { + for value in param { + match (state, *value) { + (CsiState::Normal, 0) => { + style = anstyle::Style::default(); + break; + } + (CsiState::Normal, 1) => { + style = style.bold(); + break; + } + (CsiState::Normal, 2) => { + style = style.dimmed(); + break; + } + (CsiState::Normal, 3) => { + style = style.italic(); + break; + } + (CsiState::Normal, 4) => { + style = style.underline(); + state = CsiState::Underline; + } + (CsiState::Normal, 21) => { + style |= anstyle::Effects::DOUBLE_UNDERLINE; + break; + } + (CsiState::Normal, 7) => { + style = style.invert(); + break; + } + (CsiState::Normal, 8) => { + style = style.hidden(); + break; + } + (CsiState::Normal, 9) => { + style = style.strikethrough(); + break; + } + (CsiState::Normal, 30..=37) => { + let color = to_ansi_color(value - 30).expect("within 4-bit range"); + style = style.fg_color(Some(color.into())); + break; + } + (CsiState::Normal, 38) => { + color_target = ColorTarget::Fg; + state = CsiState::PrepareCustomColor; + } + (CsiState::Normal, 39) => { + style = style.fg_color(None); + break; + } + (CsiState::Normal, 40..=47) => { + let color = to_ansi_color(value - 40).expect("within 4-bit range"); + style = style.bg_color(Some(color.into())); + break; + } + (CsiState::Normal, 48) => { + color_target = ColorTarget::Bg; + state = CsiState::PrepareCustomColor; + } + (CsiState::Normal, 49) => { + style = style.bg_color(None); + break; + } + (CsiState::Normal, 58) => { + color_target = ColorTarget::Underline; + state = CsiState::PrepareCustomColor; + } + (CsiState::Normal, 90..=97) => { + let color = to_ansi_color(value - 90) + .expect("within 4-bit range") + .bright(true); + style = style.fg_color(Some(color.into())); + break; + } + (CsiState::Normal, 100..=107) => { + let color = to_ansi_color(value - 100) + .expect("within 4-bit range") + .bright(true); + style = style.bg_color(Some(color.into())); + break; + } + (CsiState::PrepareCustomColor, 5) => { + state = CsiState::Ansi256; + } + (CsiState::PrepareCustomColor, 2) => { + state = CsiState::Rgb; + r = None; + g = None; + } + (CsiState::Ansi256, n) => { + let color = anstyle::Ansi256Color(n as u8); + style = match color_target { + ColorTarget::Fg => style.fg_color(Some(color.into())), + ColorTarget::Bg => style.bg_color(Some(color.into())), + ColorTarget::Underline => style.underline_color(Some(color.into())), + }; + break; + } + (CsiState::Rgb, b) => match (r, g) { + (None, _) => { + r = Some(b); + } + (Some(_), None) => { + g = Some(b); + } + (Some(r), Some(g)) => { + let color = anstyle::RgbColor(r as u8, g as u8, b as u8); + style = match color_target { + ColorTarget::Fg => style.fg_color(Some(color.into())), + ColorTarget::Bg => style.bg_color(Some(color.into())), + ColorTarget::Underline => style.underline_color(Some(color.into())), + }; + break; + } + }, + (CsiState::Underline, 0) => { + style = + style.effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)); + } + (CsiState::Underline, 1) => { + // underline already set + } + (CsiState::Underline, 2) => { + style = style + .effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)) + | anstyle::Effects::DOUBLE_UNDERLINE; + } + (CsiState::Underline, 3) => { + style = style + .effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)) + | anstyle::Effects::CURLY_UNDERLINE; + } + (CsiState::Underline, 4) => { + style = style + .effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)) + | anstyle::Effects::DOTTED_UNDERLINE; + } + (CsiState::Underline, 5) => { + style = style + .effects(style.get_effects().remove(anstyle::Effects::UNDERLINE)) + | anstyle::Effects::DASHED_UNDERLINE; + } + _ => { + break; + } + } + } + } + + if style != self.style && !self.printable.is_empty() { + self.ready = Some((self.style, self.hyperlink.clone())); + } + self.style = style; + } + + fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) { + let mut state = OscState::Normal; + for value in params { + match (state, value) { + (OscState::Normal, &[b'8']) => { + state = OscState::HyperlinkParams; + } + (OscState::HyperlinkParams, _) => { + state = OscState::HyperlinkUri; + } + (OscState::HyperlinkUri, &[]) => { + if self.hyperlink.is_some() { + self.ready = Some((self.style, std::mem::take(&mut self.hyperlink))); + } + break; + } + (OscState::HyperlinkUri, uri) => { + let hyperlink = uri.iter().map(|b| *b as char).collect::(); + self.hyperlink = Some(hyperlink); + + // Any current text in `self.printable` needs to be + // rendered, so it doesn't get confused with Hyperlink text + if !self.printable.is_empty() { + self.ready = Some((self.style, None)); + } + break; + } + + _ => { + break; + } + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Element { + pub(crate) text: String, + pub(crate) style: anstyle::Style, + pub(crate) url: Option, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +enum CsiState { + Normal, + PrepareCustomColor, + Ansi256, + Rgb, + Underline, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +enum OscState { + Normal, + HyperlinkParams, + HyperlinkUri, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +enum ColorTarget { + Fg, + Bg, + Underline, +} + +fn to_ansi_color(digit: u16) -> Option { + match digit { + 0 => Some(anstyle::AnsiColor::Black), + 1 => Some(anstyle::AnsiColor::Red), + 2 => Some(anstyle::AnsiColor::Green), + 3 => Some(anstyle::AnsiColor::Yellow), + 4 => Some(anstyle::AnsiColor::Blue), + 5 => Some(anstyle::AnsiColor::Magenta), + 6 => Some(anstyle::AnsiColor::Cyan), + 7 => Some(anstyle::AnsiColor::White), + _ => None, + } +} + +#[cfg(test)] +mod test { + use super::*; + use proptest::prelude::*; + + const URL: &str = "https://example.com"; + + #[track_caller] + fn verify(input: &str, expected: Vec) { + let expected = expected.into_iter().collect::>(); + let mut state = AnsiBytes::new(); + let actual = state.extract_next(input.as_bytes()).collect::>(); + assert_eq!(expected, actual, "{input:?}"); + } + + fn hyperlink(input: &str, url: &str) -> String { + format!("\x1B]8;;{url}\x1B\\{input}\x1B]8;;\x1B\\") + } + + #[test] + fn start() { + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!("{green_on_red}Hello{green_on_red:#} world!"); + let expected = vec![ + Element { + text: "Hello".to_owned(), + style: green_on_red, + url: None, + }, + Element { + text: " world!".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + ]; + verify(&input, expected); + } + + #[test] + fn middle() { + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!("Hello {green_on_red}world{green_on_red:#}!"); + let expected = vec![ + Element { + text: "Hello ".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + Element { + text: "world".to_owned(), + style: green_on_red, + url: None, + }, + Element { + text: "!".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + ]; + verify(&input, expected); + } + + #[test] + fn end() { + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!("Hello {green_on_red}world!{green_on_red:#}"); + let expected = vec![ + Element { + text: "Hello ".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + Element { + text: "world!".to_owned(), + style: green_on_red, + url: None, + }, + ]; + verify(&input, expected); + } + + #[test] + fn ansi256_colors() { + let ansi_11 = anstyle::Ansi256Color(11).on_default(); + // termcolor only supports "brights" via these + let input = format!("Hello {ansi_11}world{ansi_11:#}!"); + let expected = vec![ + Element { + text: "Hello ".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + Element { + text: "world".to_owned(), + style: ansi_11, + url: None, + }, + Element { + text: "!".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + ]; + verify(&input, expected); + } + + #[test] + fn hyperlink_start() { + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!( + "{green_on_red}{}{green_on_red:#} world!", + hyperlink("Hello", URL) + ); + let expected = vec![ + Element { + text: "Hello".to_owned(), + style: green_on_red, + url: Some(URL.to_owned()), + }, + Element { + text: " world!".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + ]; + verify(&input, expected); + } + + #[test] + fn hyperlink_middle() { + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!( + "Hello {green_on_red}{}{green_on_red:#}!", + hyperlink("world", URL) + ); + let expected = vec![ + Element { + text: "Hello ".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + Element { + text: "world".to_owned(), + style: green_on_red, + url: Some(URL.to_owned()), + }, + Element { + text: "!".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + ]; + verify(&input, expected); + } + + #[test] + fn hyperlink_end() { + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!( + "Hello {green_on_red}{}{green_on_red:#}", + hyperlink("world!", URL) + ); + let expected = vec![ + Element { + text: "Hello ".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + Element { + text: "world!".to_owned(), + style: green_on_red, + url: Some(URL.to_owned()), + }, + ]; + verify(&input, expected); + } + + #[test] + fn hyperlink_ansi256_colors() { + let ansi_11 = anstyle::Ansi256Color(11).on_default(); + // termcolor only supports "brights" via these + let input = format!("Hello {ansi_11}{}{ansi_11:#}!", hyperlink("world", URL)); + let expected = vec![ + Element { + text: "Hello ".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + Element { + text: "world".to_owned(), + style: ansi_11, + url: Some(URL.to_owned()), + }, + Element { + text: "!".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + ]; + verify(&input, expected); + } + + #[test] + fn style_mid_hyperlink_text() { + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let styled_str = format!("Hello {green_on_red}world{green_on_red:#}!"); + let input = hyperlink(&styled_str, URL); + let expected = vec![ + Element { + text: "Hello ".to_owned(), + style: anstyle::Style::default(), + url: Some(URL.to_owned()), + }, + Element { + text: "world".to_owned(), + style: green_on_red, + url: Some(URL.to_owned()), + }, + Element { + text: "!".to_owned(), + style: anstyle::Style::default(), + url: Some(URL.to_owned()), + }, + ]; + verify(&input, expected); + } + + #[test] + fn hyperlink_empty() { + let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red); + let input = format!( + "{green_on_red}{}{green_on_red:#} world!", + hyperlink("Hello", "") + ); + let expected = vec![ + Element { + text: "Hello".to_owned(), + style: green_on_red, + url: None, + }, + Element { + text: " world!".to_owned(), + style: anstyle::Style::default(), + url: None, + }, + ]; + verify(&input, expected); + } + + proptest! { + #[test] + #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 + fn wincon_no_escapes(s in "\\PC*") { + let expected = if s.is_empty() { + vec![] + } else { + vec![Element { + text: s.clone(), + style: anstyle::Style::default(), + url: None, + }] + }; + let mut state = AnsiBytes::new(); + let actual = state.extract_next(s.as_bytes()).collect::>(); + assert_eq!(expected, actual); + } + } +} diff --git a/crates/anstyle-svg/src/lib.rs b/crates/anstyle-svg/src/lib.rs index 43dc9563..131608cb 100644 --- a/crates/anstyle-svg/src/lib.rs +++ b/crates/anstyle-svg/src/lib.rs @@ -17,6 +17,8 @@ #![warn(clippy::print_stderr)] #![warn(clippy::print_stdout)] +mod adapter; + pub use anstyle_lossy::palette::Palette; pub use anstyle_lossy::palette::VGA; pub use anstyle_lossy::palette::WIN10_CONSOLE; @@ -88,10 +90,11 @@ impl Term { const FG: &str = "fg"; const BG: &str = "bg"; - let mut styled = anstream::adapter::WinconBytes::new(); - let mut styled = styled.extract_next(ansi.as_bytes()).collect::>(); + let mut styled = adapter::AnsiBytes::new(); + let mut elements = styled.extract_next(ansi.as_bytes()).collect::>(); let mut effects_in_use = anstyle::Effects::new(); - for (style, _) in &mut styled { + for element in &mut elements { + let style = &mut element.style; // Pre-process INVERT to make fg/bg calculations easier if style.get_effects().contains(anstyle::Effects::INVERT) { *style = style @@ -101,7 +104,7 @@ impl Term { } effects_in_use |= style.get_effects(); } - let styled_lines = split_lines(&styled); + let styled_lines = split_lines(&elements); let fg_color = rgb_value(self.fg_color, self.palette); let bg_color = rgb_value(self.bg_color, self.palette); @@ -111,7 +114,7 @@ impl Term { let height = styled_lines.len() * line_height + self.padding_px * 2; let max_width = styled_lines .iter() - .map(|l| l.iter().map(|(_, t)| t.width()).sum()) + .map(|l| l.iter().map(|e| e.text.width()).sum()) .max() .unwrap_or(0); let width_px = (max_width as f64 * 8.4).ceil() as usize; @@ -126,7 +129,7 @@ impl Term { writeln!(&mut buffer, r#" + + + + + Link to original file + + + + Tests for gnome-terminal #779734 and iTerm2 #5158 + + ═════════════════════════════════════════════════ + + + + commit a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731 + + Author: Egmont Koblinger <egmont@gmail.com> + + Date: Sat Oct 24 00:12:22 2015 +0200 + + + + widget: Implement smooth scrolling + + + + Bug #746690 + + + + commit 6a74baeaabb0a1ce54444611b324338f94721a5c + + Merge: 3fac446 56ea581 + + Author: Christian Persch <chpe@gnome.org> + + Date: Mon Apr 27 13:48:52 2015 +0200 + + + + Merge branch 'work-html' into merge-html + + + + A file with a % sign in its name (escaped as %25) + + Icons: Theme Graphics Star Exit Terminal + + Backgrounds: Bokeh Chmiri Ivy Flower Iceland Icescape Mirror Road Sandstone Stones Waterfalls Waves + + + + Wiki page of Á (unescaped raw UTF-8) + + Wiki page of Á (escaped as %C3%81) + + Wiki page of % (escaped as %25) + + http://المغرب.icom.museum (with URI-escaped domain name) + + http://xn--4wa8awb4637h.org (Παν語.org) + + + + Two adjacent links pointing to the same URL: foofoo + + Two adjacent links pointing to different URLs: foobar + + + + The same two without closing the first link: foofoo foobar + + + + A URL wrapping to the next line, and a trailing whitespace: foo + + bar + + + + + + Multi-colour link also tests that "\e[m" or "\e[0m" does not terminate the link + + + + Soft reset "\e[!p" resets attributes and terminates link: foobar + + + + Some CJK and combining accents: 䀀䀁䀂ćĝm̃n̄o̅ + + + + (Introducing the "under_score" character for even more fun) + + + + Explicit and implicit link: http://example.com/under_score + + Explicit and implicit link with different targets: http://example.com/implicit_under_score + + Explicit and implicit link, broken into two lines: http://examp + + le.com/under_score + + + + Explicitly underlined links ("\e[4m"): + + Explicit link only: I'm an explicit link with under_score + + Implicit link only: http://example.com/under_score + + Both: http://example.com/under_score + + + + Conflicting explicit and implicit links: http://example.com/foobar-explicit-rest + + + + Invisible explicit link: «» + + Invisible implicit link: «» + + + + Explicit link with stupid target + + + + URL of 100 bytes + + URL of 200 bytes + + URL of 500 bytes + + URL of 1000 bytes + + URL of 1500 bytes + + URL of 2000 bytes + + URL of 2083 bytes + + URL of 2084 bytes + + + + ID of 250 bytes once, twice + + ID of 251 bytes once, twice + + + + ID of 250 bytes + URL of 2083 bytes + + ID of 251 bytes + URL of 2083 bytes + + ID of 250 bytes + URL of 2084 bytes + + ID of 251 bytes + URL of 2084 bytes + + + + BEL instead of ST (not standard) + + 8;;http://example.com/C1œC1 (U+009D [UTF-8: 0xC2 0x9D] as OSC and U+009C [UTF-8: 0xC2 0x9C] as ST)8;;œ (note: not all terminal emulators support C1 in UTF-8) + + + + Cursor movement within the same OSC 8 run: moveright + + + + Alternating URIs, all with the same ID. Either all foos or all bars should be underlined on hover: + + foobarfoobarfoobarfoo + + + + Screenshot from an imaginary text editor: + + ╔═ file1 ════╗ + + ║ ╔═ file2 ═══╗ + + http://exa║Lorem ipsum║ + + le.com ║ dolor sit ║ + + ║ ║amet, conse║ + + ╚══════════║ctetur adip║ + + ╚═══════════╝ + + + + + + diff --git a/crates/anstyle-svg/tests/hyperlink-demo.vte b/crates/anstyle-svg/tests/hyperlink-demo.vte new file mode 100644 index 00000000..4b626916 --- /dev/null +++ b/crates/anstyle-svg/tests/hyperlink-demo.vte @@ -0,0 +1,96 @@ +]8;;https://gitlab.gnome.org/GNOME/vte/-/blob/64ca16f975a63033dc0c4555ff4a5505102fceff/perf/hyperlink-demo.txt\Link to original file]8;;\ + +Tests for ]8;;https://bugzilla.gnome.org/show_bug.cgi?id=779734\gnome-terminal #779734]8;;\ and ]8;;https://gitlab.com/gnachman/iterm2/issues/5158\iTerm2 #5158]8;;\ +═════════════════════════════════════════════════ + +commit ]8;;https://git.gnome.org/browse/vte/commit/?id=a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731\a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731]8;;\ +Author: Egmont Koblinger <]8;;mailto:egmont@gmail.com\egmont@gmail.com]8;;\> +Date: Sat Oct 24 00:12:22 2015 +0200 + + widget: Implement smooth scrolling + + ]8;;https://bugzilla.gnome.org/show_bug.cgi?id=746690\Bug #746690]8;;\ + +commit ]8;;https://git.gnome.org/browse/vte/commit/?id=6a74baeaabb0a1ce54444611b324338f94721a5c\6a74baeaabb0a1ce54444611b324338f94721a5c]8;;\ +Merge: ]8;;https://git.gnome.org/browse/vte/commit/?id=3fac4469de267f662c761ea4f247c8017ced483d\3fac446]8;;\ ]8;;https://git.gnome.org/browse/vte/commit/?id=56ea5810759b9943a4203f9382919f058a66f224\56ea581]8;;\ +Author: Christian Persch <]8;;mailto:chpe@gnome.org\chpe@gnome.org]8;;\> +Date: Mon Apr 27 13:48:52 2015 +0200 + + Merge branch 'work-html' into merge-html + +]8;;file:///var/lib/gconf/defaults/%25gconf-tree.xml\A file with a % sign in its name (escaped as %25)]8;;\ +Icons: ]8;;file:///usr/share/icons/Adwaita/256x256/apps/preferences-desktop-theme.png\Theme]8;;\ ]8;;file:///usr/share/icons/Adwaita/256x256/categories/applications-graphics.png\Graphics]8;;\ ]8;;file:///usr/share/icons/Adwaita/256x256/status/starred.png\Star]8;;\ ]8;;file:///usr/share/icons/Adwaita/256x256/actions/system-log-out.png\Exit]8;;\ ]8;;file:///usr/share/icons/Adwaita/512x512/apps/utilities-terminal.png\Terminal]8;;\ +Backgrounds: ]8;;file:///usr/share/backgrounds/gnome/Bokeh_Tails.jpg\Bokeh]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Chmiri.jpg\Chmiri]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Dark_Ivy.jpg\Ivy]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Flowerbed.jpg\Flower]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Godafoss_Iceland.jpg\Iceland]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Icescape.jpg\Icescape]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Mirror.jpg\Mirror]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Road.jpg\Road]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Sandstone.jpg\Sandstone]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Stones.jpg\Stones]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Waterfalls.jpg\Waterfalls]8;;\ ]8;;file:///usr/share/backgrounds/gnome/Waves.jpg\Waves]8;;\ + +]8;;https://en.wikipedia.org/wiki/Á\Wiki page of Á (unescaped raw UTF-8)]8;;\ +]8;;https://en.wikipedia.org/wiki/%C3%81\Wiki page of Á (escaped as %C3%81)]8;;\ +]8;;https://en.wikipedia.org/wiki/%25\Wiki page of % (escaped as %25)]8;;\ +]8;;http://%d8%a7%d9%84%d9%85%d8%ba%d8%b1%d8%a8.icom.museum\http://المغرب.icom.museum (with URI-escaped domain name)]8;;\ +]8;;http://xn--4wa8awb4637h.org\http://xn--4wa8awb4637h.org (Παν語.org)]8;;\ + +Two adjacent links pointing to the same URL: ]8;;http://example.com/foo\foo]8;;\]8;;http://example.com/foo\foo]8;;\ +Two adjacent links pointing to different URLs: ]8;;http://example.com/foo\foo]8;;\]8;;http://example.com/bar\bar]8;;\ + +The same two without closing the first link: ]8;;http://example.com/foo\foo]8;;http://example.com/foo\foo]8;;\ ]8;;http://example.com/foo\foo]8;;http://example.com/bar\bar]8;;\ + +A URL wrapping to the next line, and a trailing whitespace: ]8;;http://example.com/foobar\foo +bar ]8;;\ + +]8;;http://example.com/colors\Multi-colour link also tests that "\e[m" or "\e[0m" does not terminate the link]8;;\ + +Soft reset "\e[!p" resets attributes and terminates link: ]8;;http://example.com/softreset\foo[!pbar + +]8;;http://example.com/width\Some CJK and combining accents: 䀀䀁䀂ćĝm̃n̄o̅]8;;\ + +(Introducing the "under_score" character for even more fun) + +Explicit and implicit link: ]8;;http://example.com/under_score\http://example.com/under_score]8;;\ +Explicit and implicit link with different targets: ]8;;http://example.com/explicit_under_score\http://example.com/implicit_under_score]8;;\ +Explicit and implicit link, broken into two lines: ]8;;http://example.com/under_score\http://examp +le.com/under_score]8;;\ + +Explicitly underlined links ("\e[4m"): +Explicit link only: ]8;;http://example.com/under_score\I'm an explicit link with under_score]8;;\ +Implicit link only: http://example.com/under_score +Both: ]8;;http://example.com/under_score\http://example.com/under_score]8;;\ + +Conflicting explicit and implicit links: http://example.com/foobar-]8;;http://example.com/explicit\explicit]8;;\-rest + +Invisible explicit link: «]8;;http://example.com/invisible\Can you see me?]8;;\» +Invisible implicit link: «http://example.com/how_about_me» + +]8;;asdfghjkl\Explicit link with stupid target]8;;\ + +]8;;http://example.com/.........30........40........50........60........70........80........90.......100\URL of 100 bytes]8;;\ +]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200\URL of 200 bytes]8;;\ +]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500\URL of 500 bytes]8;;\ +]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000\URL of 1000 bytes]8;;\ +]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500\URL of 1500 bytes]8;;\ +]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000\URL of 2000 bytes]8;;\ +]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080...\URL of 2083 bytes]8;;\ +]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080....\URL of 2084 bytes]8;;\ + +]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250;http://example.com/id\ID of 250 bytes once,]8;;\ ]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250;http://example.com/id\twice]8;;\ +]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.;http://example.com/id\ID of 251 bytes once,]8;;\ ]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.;http://example.com/id\twice]8;;\ + +]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080...\ID of 250 bytes + URL of 2083 bytes]8;;\ +]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080...\ID of 251 bytes + URL of 2083 bytes]8;;\ +]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080....\ID of 250 bytes + URL of 2084 bytes]8;;\ +]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080....\ID of 251 bytes + URL of 2084 bytes]8;;\ + +]8;;http://example.com/BELBEL instead of ST]8;; (not standard) +8;;http://example.com/C1œC1 (U+009D [UTF-8: 0xC2 0x9D] as OSC and U+009C [UTF-8: 0xC2 0x9C] as ST)8;;œ (note: not all terminal emulators support C1 in UTF-8) + +Cursor movement within the same OSC 8 run: ]8;;http://example.com/cursor\moveright]8;;\ + +Alternating URIs, all with the same ID. Either all foos or all bars should be underlined on hover: +]8;id=1;http://example.com/foo\foo]8;;\]8;id=1;http://example.com/bar\bar]8;;\]8;foo=bar:id=1;http://example.com/foo\foo]8;;\]8;id=1;http://example.com/bar\bar]8;;\]8;id=1:baz=quux;http://example.com/foo\foo]8;;\]8;id=1;http://example.com/bar\bar]8;;\]8;foo=bar:id=1:baz=quux;http://example.com/foo\foo]8;;\ + +Screenshot from an imaginary text editor: +╔═ file1 ════╗ +║ ╔═ file2 ═══╗ +║]8;id=imaginary-text-editor-file1;http://example.com\http://exa]8;;\║Lorem ipsum║ +║]8;id=imaginary-text-editor-file1;http://example.com\le.com]8;;\ ║ dolor sit ║ +║ ║amet, conse║ +╚══════════║ctetur adip║ + ╚═══════════╝ diff --git a/crates/anstyle-svg/tests/term.rs b/crates/anstyle-svg/tests/term.rs index bf68505a..787f6d6a 100644 --- a/crates/anstyle-svg/tests/term.rs +++ b/crates/anstyle-svg/tests/term.rs @@ -11,3 +11,12 @@ fn rg_linus() { let actual = anstyle_svg::Term::new().render_svg(&input); snapbox::assert_data_eq!(actual, snapbox::file!["rg_linus.svg": Text].raw()); } + +#[test] +fn hyperlink_demo() { + let bytes = std::fs::read("tests/hyperlink-demo.vte").unwrap(); + String::from_utf8(bytes).unwrap(); + let input = std::fs::read_to_string("tests/hyperlink-demo.vte").unwrap(); + let actual = anstyle_svg::Term::new().render_svg(&input); + snapbox::assert_data_eq!(actual, snapbox::file!["hyperlink-demo.svg": Text].raw()); +}