Skip to content

Commit 51ad9ef

Browse files
committed
feat(anstyle-svg): Add support for rendering Hyperlinks
1 parent 96e3f75 commit 51ad9ef

File tree

3 files changed

+138
-54
lines changed

3 files changed

+138
-54
lines changed

crates/anstyle-svg/src/adapter.rs

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,20 @@ fn next_bytes(
6060
return None;
6161
}
6262

63-
let style = capture.ready.unwrap_or(capture.style);
63+
let (style, url) = capture.ready.clone().unwrap_or((capture.style, None));
6464
Some(Element {
6565
text: std::mem::take(&mut capture.printable),
6666
style,
67+
url,
6768
})
6869
}
6970

7071
#[derive(Default, Clone, Debug, PartialEq, Eq)]
7172
struct AnsiCapture {
7273
style: anstyle::Style,
7374
printable: String,
74-
ready: Option<anstyle::Style>,
75+
hyperlink: Option<String>,
76+
ready: Option<(anstyle::Style, Option<String>)>,
7577
}
7678

7779
impl AnsiCapture {
@@ -265,16 +267,56 @@ impl anstyle_parse::Perform for AnsiCapture {
265267
}
266268

267269
if style != self.style && !self.printable.is_empty() {
268-
self.ready = Some(self.style);
270+
if self.hyperlink.is_some() {
271+
self.ready = Some((self.style, self.hyperlink.clone()));
272+
} else {
273+
self.ready = Some((self.style, None));
274+
}
269275
}
270276
self.style = style;
271277
}
278+
279+
fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
280+
let mut state = OscState::Normal;
281+
for value in params {
282+
match (state, value) {
283+
(OscState::Normal, &[b'8']) => {
284+
state = OscState::HyperlinkParams;
285+
}
286+
(OscState::HyperlinkParams, _) => {
287+
state = OscState::HyperlinkUri;
288+
}
289+
(OscState::HyperlinkUri, &[]) => {
290+
if self.hyperlink.is_some() {
291+
self.ready = Some((self.style, std::mem::take(&mut self.hyperlink)));
292+
}
293+
break;
294+
}
295+
(OscState::HyperlinkUri, uri) => {
296+
let hyperlink = uri.iter().map(|b| *b as char).collect::<String>();
297+
self.hyperlink = Some(hyperlink);
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+
}
272313
}
273314

274315
#[derive(Clone, Debug, PartialEq, Eq)]
275316
pub(crate) struct Element {
276317
pub(crate) text: String,
277318
pub(crate) style: anstyle::Style,
319+
pub(crate) url: Option<String>,
278320
}
279321

280322
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
@@ -286,6 +328,13 @@ enum CsiState {
286328
Underline,
287329
}
288330

331+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
332+
enum OscState {
333+
Normal,
334+
HyperlinkParams,
335+
HyperlinkUri,
336+
}
337+
289338
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
290339
enum ColorTarget {
291340
Fg,
@@ -334,10 +383,12 @@ mod test {
334383
Element {
335384
text: "Hello".to_owned(),
336385
style: green_on_red,
386+
url: None,
337387
},
338388
Element {
339389
text: " world!".to_owned(),
340390
style: anstyle::Style::default(),
391+
url: None,
341392
},
342393
];
343394
verify(&input, expected);
@@ -351,14 +402,17 @@ mod test {
351402
Element {
352403
text: "Hello ".to_owned(),
353404
style: anstyle::Style::default(),
405+
url: None,
354406
},
355407
Element {
356408
text: "world".to_owned(),
357409
style: green_on_red,
410+
url: None,
358411
},
359412
Element {
360413
text: "!".to_owned(),
361414
style: anstyle::Style::default(),
415+
url: None,
362416
},
363417
];
364418
verify(&input, expected);
@@ -372,10 +426,12 @@ mod test {
372426
Element {
373427
text: "Hello ".to_owned(),
374428
style: anstyle::Style::default(),
429+
url: None,
375430
},
376431
Element {
377432
text: "world!".to_owned(),
378433
style: green_on_red,
434+
url: None,
379435
},
380436
];
381437
verify(&input, expected);
@@ -390,14 +446,17 @@ mod test {
390446
Element {
391447
text: "Hello ".to_owned(),
392448
style: anstyle::Style::default(),
449+
url: None,
393450
},
394451
Element {
395452
text: "world".to_owned(),
396453
style: ansi_11,
454+
url: None,
397455
},
398456
Element {
399457
text: "!".to_owned(),
400458
style: anstyle::Style::default(),
459+
url: None,
401460
},
402461
];
403462
verify(&input, expected);
@@ -414,10 +473,12 @@ mod test {
414473
Element {
415474
text: "Hello".to_owned(),
416475
style: green_on_red,
476+
url: Some(URL.to_owned()),
417477
},
418478
Element {
419479
text: " world!".to_owned(),
420480
style: anstyle::Style::default(),
481+
url: None,
421482
},
422483
];
423484
verify(&input, expected);
@@ -434,14 +495,17 @@ mod test {
434495
Element {
435496
text: "Hello ".to_owned(),
436497
style: anstyle::Style::default(),
498+
url: None,
437499
},
438500
Element {
439501
text: "world".to_owned(),
440502
style: green_on_red,
503+
url: Some(URL.to_owned()),
441504
},
442505
Element {
443506
text: "!".to_owned(),
444507
style: anstyle::Style::default(),
508+
url: None,
445509
},
446510
];
447511
verify(&input, expected);
@@ -458,10 +522,12 @@ mod test {
458522
Element {
459523
text: "Hello ".to_owned(),
460524
style: anstyle::Style::default(),
525+
url: None,
461526
},
462527
Element {
463528
text: "world!".to_owned(),
464529
style: green_on_red,
530+
url: Some(URL.to_owned()),
465531
},
466532
];
467533
verify(&input, expected);
@@ -476,14 +542,17 @@ mod test {
476542
Element {
477543
text: "Hello ".to_owned(),
478544
style: anstyle::Style::default(),
545+
url: None,
479546
},
480547
Element {
481548
text: "world".to_owned(),
482549
style: ansi_11,
550+
url: Some(URL.to_owned()),
483551
},
484552
Element {
485553
text: "!".to_owned(),
486554
style: anstyle::Style::default(),
555+
url: None,
487556
},
488557
];
489558
verify(&input, expected);
@@ -498,14 +567,17 @@ mod test {
498567
Element {
499568
text: "Hello ".to_owned(),
500569
style: anstyle::Style::default(),
570+
url: Some(URL.to_owned()),
501571
},
502572
Element {
503573
text: "world".to_owned(),
504574
style: green_on_red,
575+
url: Some(URL.to_owned()),
505576
},
506577
Element {
507578
text: "!".to_owned(),
508579
style: anstyle::Style::default(),
580+
url: Some(URL.to_owned()),
509581
},
510582
];
511583
verify(&input, expected);
@@ -522,10 +594,12 @@ mod test {
522594
Element {
523595
text: "Hello".to_owned(),
524596
style: green_on_red,
597+
url: None,
525598
},
526599
Element {
527600
text: " world!".to_owned(),
528601
style: anstyle::Style::default(),
602+
url: None,
529603
},
530604
];
531605
verify(&input, expected);
@@ -541,6 +615,7 @@ mod test {
541615
vec![Element {
542616
text: s.clone(),
543617
style: anstyle::Style::default(),
618+
url: None,
544619
}]
545620
};
546621
let mut state = AnsiBytes::new();

crates/anstyle-svg/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,22 @@ fn write_fg_span(buffer: &mut String, element: &adapter::Element, fragment: &str
327327
classes.push("hidden");
328328
}
329329

330+
let mut need_closing_a = false;
331+
330332
write!(buffer, r#"<tspan"#).unwrap();
331333
if !classes.is_empty() {
332334
let classes = classes.join(" ");
333335
write!(buffer, r#" class="{classes}""#).unwrap();
334336
}
335337
write!(buffer, r#">"#).unwrap();
338+
if let Some(hyperlink) = &element.url {
339+
write!(buffer, r#"<a href="{hyperlink}">"#).unwrap();
340+
need_closing_a = true;
341+
}
336342
write!(buffer, "{fragment}").unwrap();
343+
if need_closing_a {
344+
write!(buffer, r#"</a>"#).unwrap();
345+
}
337346
write!(buffer, r#"</tspan>"#).unwrap();
338347
}
339348

0 commit comments

Comments
 (0)