Skip to content

Commit 4cce0f6

Browse files
committed
feat(anstyle-svg): Add support for rendering Hyperlinks
1 parent 263c2c9 commit 4cce0f6

File tree

4 files changed

+354
-108
lines changed

4 files changed

+354
-108
lines changed

crates/anstyle-svg/src/adapter.rs

Lines changed: 187 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub(crate) struct AnsiBytesIter<'s> {
3232
}
3333

3434
impl Iterator for AnsiBytesIter<'_> {
35-
type Item = (anstyle::Style, String);
35+
type Item = Element;
3636

3737
#[inline]
3838
fn next(&mut self) -> Option<Self::Item> {
@@ -45,7 +45,7 @@ fn next_bytes(
4545
bytes: &mut &[u8],
4646
parser: &mut anstyle_parse::Parser,
4747
capture: &mut AnsiCapture,
48-
) -> Option<(anstyle::Style, String)> {
48+
) -> Option<Element> {
4949
capture.reset();
5050
while capture.ready.is_none() {
5151
let byte = if let Some((byte, remainder)) = (*bytes).split_first() {
@@ -59,16 +59,20 @@ fn next_bytes(
5959
if capture.printable.is_empty() {
6060
return None;
6161
}
62-
63-
let style = capture.ready.unwrap_or(capture.style);
64-
Some((style, std::mem::take(&mut capture.printable)))
62+
let (style, url) = capture.ready.clone().unwrap_or((capture.style, None));
63+
Some(Element {
64+
text: std::mem::take(&mut capture.printable),
65+
style,
66+
url,
67+
})
6568
}
6669

6770
#[derive(Default, Clone, Debug, PartialEq, Eq)]
6871
struct AnsiCapture {
6972
style: anstyle::Style,
7073
printable: String,
71-
ready: Option<anstyle::Style>,
74+
hyperlink: String,
75+
ready: Option<(anstyle::Style, Option<String>)>,
7276
}
7377

7478
impl AnsiCapture {
@@ -262,10 +266,57 @@ impl anstyle_parse::Perform for AnsiCapture {
262266
}
263267

264268
if style != self.style && !self.printable.is_empty() {
265-
self.ready = Some(self.style);
269+
if self.hyperlink.is_empty() {
270+
self.ready = Some((self.style, None));
271+
} else {
272+
self.ready = Some((self.style, Some(self.hyperlink.clone())));
273+
}
266274
}
267275
self.style = style;
268276
}
277+
278+
fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
279+
let mut state = OscState::Normal;
280+
for value in params {
281+
match (state, value) {
282+
(OscState::Normal, &[b'8']) => {
283+
state = OscState::HyperlinkParams;
284+
}
285+
(OscState::HyperlinkParams, _) => {
286+
state = OscState::HyperlinkUri;
287+
}
288+
(OscState::HyperlinkUri, &[]) => {
289+
if !self.hyperlink.is_empty() {
290+
self.ready = Some((self.style, Some(std::mem::take(&mut self.hyperlink))));
291+
}
292+
break;
293+
}
294+
(OscState::HyperlinkUri, uri) => {
295+
for b in uri.iter() {
296+
self.hyperlink.push(*b as char);
297+
}
298+
299+
// Any current text in `self.printable` needs to be
300+
// rendered, so it doesn't get confused with Hyperlink text
301+
if !self.printable.is_empty() {
302+
self.ready = Some((self.style, None));
303+
}
304+
break;
305+
}
306+
307+
_ => {
308+
break;
309+
}
310+
}
311+
}
312+
}
313+
}
314+
315+
#[derive(Clone, Debug, PartialEq, Eq)]
316+
pub(crate) struct Element {
317+
pub(crate) text: String,
318+
pub(crate) style: anstyle::Style,
319+
pub(crate) url: Option<String>,
269320
}
270321

271322
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
@@ -277,6 +328,13 @@ enum CsiState {
277328
Underline,
278329
}
279330

331+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
332+
enum OscState {
333+
Normal,
334+
HyperlinkParams,
335+
HyperlinkUri,
336+
}
337+
280338
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
281339
enum ColorTarget {
282340
Fg,
@@ -306,11 +364,8 @@ mod test {
306364
const URL: &str = "https://example.com";
307365

308366
#[track_caller]
309-
fn verify(input: &str, expected: Vec<(anstyle::Style, &str)>) {
310-
let expected = expected
311-
.into_iter()
312-
.map(|(style, value)| (style, value.to_owned()))
313-
.collect::<Vec<_>>();
367+
fn verify(input: &str, expected: Vec<Element>) {
368+
let expected = expected.into_iter().collect::<Vec<_>>();
314369
let mut state = AnsiBytes::new();
315370
let actual = state.extract_next(input.as_bytes()).collect::<Vec<_>>();
316371
assert_eq!(expected, actual, "{input:?}");
@@ -325,8 +380,16 @@ mod test {
325380
let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red);
326381
let input = format!("{green_on_red}Hello{green_on_red:#} world!");
327382
let expected = vec![
328-
(green_on_red, "Hello"),
329-
(anstyle::Style::default(), " world!"),
383+
Element {
384+
text: "Hello".to_string(),
385+
style: green_on_red,
386+
url: None,
387+
},
388+
Element {
389+
text: " world!".to_string(),
390+
style: anstyle::Style::default(),
391+
url: None,
392+
},
330393
];
331394
verify(&input, expected);
332395
}
@@ -336,9 +399,21 @@ mod test {
336399
let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red);
337400
let input = format!("Hello {green_on_red}world{green_on_red:#}!");
338401
let expected = vec![
339-
(anstyle::Style::default(), "Hello "),
340-
(green_on_red, "world"),
341-
(anstyle::Style::default(), "!"),
402+
Element {
403+
text: "Hello ".to_string(),
404+
style: anstyle::Style::default(),
405+
url: None,
406+
},
407+
Element {
408+
text: "world".to_string(),
409+
style: green_on_red,
410+
url: None,
411+
},
412+
Element {
413+
text: "!".to_string(),
414+
style: anstyle::Style::default(),
415+
url: None,
416+
},
342417
];
343418
verify(&input, expected);
344419
}
@@ -348,8 +423,16 @@ mod test {
348423
let green_on_red = anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Red);
349424
let input = format!("Hello {green_on_red}world!{green_on_red:#}");
350425
let expected = vec![
351-
(anstyle::Style::default(), "Hello "),
352-
(green_on_red, "world!"),
426+
Element {
427+
text: "Hello ".to_string(),
428+
style: anstyle::Style::default(),
429+
url: None,
430+
},
431+
Element {
432+
text: "world!".to_string(),
433+
style: green_on_red,
434+
url: None,
435+
},
353436
];
354437
verify(&input, expected);
355438
}
@@ -360,9 +443,21 @@ mod test {
360443
// termcolor only supports "brights" via these
361444
let input = format!("Hello {ansi_11}world{ansi_11:#}!");
362445
let expected = vec![
363-
(anstyle::Style::default(), "Hello "),
364-
(ansi_11, "world"),
365-
(anstyle::Style::default(), "!"),
446+
Element {
447+
text: "Hello ".to_string(),
448+
style: anstyle::Style::default(),
449+
url: None,
450+
},
451+
Element {
452+
text: "world".to_string(),
453+
style: ansi_11,
454+
url: None,
455+
},
456+
Element {
457+
text: "!".to_string(),
458+
style: anstyle::Style::default(),
459+
url: None,
460+
},
366461
];
367462
verify(&input, expected);
368463
}
@@ -375,8 +470,16 @@ mod test {
375470
hyperlink("Hello")
376471
);
377472
let expected = vec![
378-
(green_on_red, "Hello"),
379-
(anstyle::Style::default(), " world!"),
473+
Element {
474+
text: "Hello".to_string(),
475+
style: green_on_red,
476+
url: Some(URL.to_owned()),
477+
},
478+
Element {
479+
text: " world!".to_string(),
480+
style: anstyle::Style::default(),
481+
url: None,
482+
},
380483
];
381484
verify(&input, expected);
382485
}
@@ -389,9 +492,21 @@ mod test {
389492
hyperlink("world")
390493
);
391494
let expected = vec![
392-
(anstyle::Style::default(), "Hello "),
393-
(green_on_red, "world"),
394-
(anstyle::Style::default(), "!"),
495+
Element {
496+
text: "Hello ".to_string(),
497+
style: anstyle::Style::default(),
498+
url: None,
499+
},
500+
Element {
501+
text: "world".to_string(),
502+
style: green_on_red,
503+
url: Some(URL.to_owned()),
504+
},
505+
Element {
506+
text: "!".to_string(),
507+
style: anstyle::Style::default(),
508+
url: None,
509+
},
395510
];
396511
verify(&input, expected);
397512
}
@@ -404,8 +519,16 @@ mod test {
404519
hyperlink("world!")
405520
);
406521
let expected = vec![
407-
(anstyle::Style::default(), "Hello "),
408-
(green_on_red, "world!"),
522+
Element {
523+
text: "Hello ".to_string(),
524+
style: anstyle::Style::default(),
525+
url: None,
526+
},
527+
Element {
528+
text: "world!".to_string(),
529+
style: green_on_red,
530+
url: Some(URL.to_owned()),
531+
},
409532
];
410533
verify(&input, expected);
411534
}
@@ -416,9 +539,21 @@ mod test {
416539
// termcolor only supports "brights" via these
417540
let input = format!("Hello {ansi_11}{}{ansi_11:#}!", hyperlink("world"));
418541
let expected = vec![
419-
(anstyle::Style::default(), "Hello "),
420-
(ansi_11, "world"),
421-
(anstyle::Style::default(), "!"),
542+
Element {
543+
text: "Hello ".to_string(),
544+
style: anstyle::Style::default(),
545+
url: None,
546+
},
547+
Element {
548+
text: "world".to_string(),
549+
style: ansi_11,
550+
url: Some(URL.to_owned()),
551+
},
552+
Element {
553+
text: "!".to_string(),
554+
style: anstyle::Style::default(),
555+
url: None,
556+
},
422557
];
423558
verify(&input, expected);
424559
}
@@ -429,9 +564,21 @@ mod test {
429564
let styled_str = format!("Hello {green_on_red}world{green_on_red:#}!");
430565
let input = hyperlink(&styled_str);
431566
let expected = vec![
432-
(anstyle::Style::default(), "Hello "),
433-
(green_on_red, "world"),
434-
(anstyle::Style::default(), "!"),
567+
Element {
568+
text: "Hello ".to_string(),
569+
style: anstyle::Style::default(),
570+
url: Some(URL.to_owned()),
571+
},
572+
Element {
573+
text: "world".to_string(),
574+
style: green_on_red,
575+
url: Some(URL.to_owned()),
576+
},
577+
Element {
578+
text: "!".to_string(),
579+
style: anstyle::Style::default(),
580+
url: Some(URL.to_owned()),
581+
},
435582
];
436583
verify(&input, expected);
437584
}
@@ -443,7 +590,11 @@ mod test {
443590
let expected = if s.is_empty() {
444591
vec![]
445592
} else {
446-
vec![(anstyle::Style::default(), s.clone())]
593+
vec![Element {
594+
text: s.clone(),
595+
style: anstyle::Style::default(),
596+
url: None,
597+
}]
447598
};
448599
let mut state = AnsiBytes::new();
449600
let actual = state.extract_next(s.as_bytes()).collect::<Vec<_>>();

0 commit comments

Comments
 (0)