Skip to content

Commit 70dd95e

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

3 files changed

Lines changed: 134 additions & 54 deletions

File tree

crates/anstyle-svg/src/adapter.rs

Lines changed: 74 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,52 @@ impl anstyle_parse::Perform for AnsiCapture {
265267
}
266268

267269
if style != self.style && !self.printable.is_empty() {
268-
self.ready = Some(self.style);
270+
self.ready = Some((self.style, self.hyperlink.clone()));
269271
}
270272
self.style = style;
271273
}
274+
275+
fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
276+
let mut state = OscState::Normal;
277+
for value in params {
278+
match (state, value) {
279+
(OscState::Normal, &[b'8']) => {
280+
state = OscState::HyperlinkParams;
281+
}
282+
(OscState::HyperlinkParams, _) => {
283+
state = OscState::HyperlinkUri;
284+
}
285+
(OscState::HyperlinkUri, &[]) => {
286+
if self.hyperlink.is_some() {
287+
self.ready = Some((self.style, std::mem::take(&mut self.hyperlink)));
288+
}
289+
break;
290+
}
291+
(OscState::HyperlinkUri, uri) => {
292+
let hyperlink = uri.iter().map(|b| *b as char).collect::<String>();
293+
self.hyperlink = Some(hyperlink);
294+
295+
// Any current text in `self.printable` needs to be
296+
// rendered, so it doesn't get confused with Hyperlink text
297+
if !self.printable.is_empty() {
298+
self.ready = Some((self.style, None));
299+
}
300+
break;
301+
}
302+
303+
_ => {
304+
break;
305+
}
306+
}
307+
}
308+
}
272309
}
273310

274311
#[derive(Clone, Debug, PartialEq, Eq)]
275312
pub(crate) struct Element {
276313
pub(crate) text: String,
277314
pub(crate) style: anstyle::Style,
315+
pub(crate) url: Option<String>,
278316
}
279317

280318
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
@@ -286,6 +324,13 @@ enum CsiState {
286324
Underline,
287325
}
288326

327+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
328+
enum OscState {
329+
Normal,
330+
HyperlinkParams,
331+
HyperlinkUri,
332+
}
333+
289334
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
290335
enum ColorTarget {
291336
Fg,
@@ -334,10 +379,12 @@ mod test {
334379
Element {
335380
text: "Hello".to_owned(),
336381
style: green_on_red,
382+
url: None,
337383
},
338384
Element {
339385
text: " world!".to_owned(),
340386
style: anstyle::Style::default(),
387+
url: None,
341388
},
342389
];
343390
verify(&input, expected);
@@ -351,14 +398,17 @@ mod test {
351398
Element {
352399
text: "Hello ".to_owned(),
353400
style: anstyle::Style::default(),
401+
url: None,
354402
},
355403
Element {
356404
text: "world".to_owned(),
357405
style: green_on_red,
406+
url: None,
358407
},
359408
Element {
360409
text: "!".to_owned(),
361410
style: anstyle::Style::default(),
411+
url: None,
362412
},
363413
];
364414
verify(&input, expected);
@@ -372,10 +422,12 @@ mod test {
372422
Element {
373423
text: "Hello ".to_owned(),
374424
style: anstyle::Style::default(),
425+
url: None,
375426
},
376427
Element {
377428
text: "world!".to_owned(),
378429
style: green_on_red,
430+
url: None,
379431
},
380432
];
381433
verify(&input, expected);
@@ -390,14 +442,17 @@ mod test {
390442
Element {
391443
text: "Hello ".to_owned(),
392444
style: anstyle::Style::default(),
445+
url: None,
393446
},
394447
Element {
395448
text: "world".to_owned(),
396449
style: ansi_11,
450+
url: None,
397451
},
398452
Element {
399453
text: "!".to_owned(),
400454
style: anstyle::Style::default(),
455+
url: None,
401456
},
402457
];
403458
verify(&input, expected);
@@ -414,10 +469,12 @@ mod test {
414469
Element {
415470
text: "Hello".to_owned(),
416471
style: green_on_red,
472+
url: Some(URL.to_owned()),
417473
},
418474
Element {
419475
text: " world!".to_owned(),
420476
style: anstyle::Style::default(),
477+
url: None,
421478
},
422479
];
423480
verify(&input, expected);
@@ -434,14 +491,17 @@ mod test {
434491
Element {
435492
text: "Hello ".to_owned(),
436493
style: anstyle::Style::default(),
494+
url: None,
437495
},
438496
Element {
439497
text: "world".to_owned(),
440498
style: green_on_red,
499+
url: Some(URL.to_owned()),
441500
},
442501
Element {
443502
text: "!".to_owned(),
444503
style: anstyle::Style::default(),
504+
url: None,
445505
},
446506
];
447507
verify(&input, expected);
@@ -458,10 +518,12 @@ mod test {
458518
Element {
459519
text: "Hello ".to_owned(),
460520
style: anstyle::Style::default(),
521+
url: None,
461522
},
462523
Element {
463524
text: "world!".to_owned(),
464525
style: green_on_red,
526+
url: Some(URL.to_owned()),
465527
},
466528
];
467529
verify(&input, expected);
@@ -476,14 +538,17 @@ mod test {
476538
Element {
477539
text: "Hello ".to_owned(),
478540
style: anstyle::Style::default(),
541+
url: None,
479542
},
480543
Element {
481544
text: "world".to_owned(),
482545
style: ansi_11,
546+
url: Some(URL.to_owned()),
483547
},
484548
Element {
485549
text: "!".to_owned(),
486550
style: anstyle::Style::default(),
551+
url: None,
487552
},
488553
];
489554
verify(&input, expected);
@@ -498,14 +563,17 @@ mod test {
498563
Element {
499564
text: "Hello ".to_owned(),
500565
style: anstyle::Style::default(),
566+
url: Some(URL.to_owned()),
501567
},
502568
Element {
503569
text: "world".to_owned(),
504570
style: green_on_red,
571+
url: Some(URL.to_owned()),
505572
},
506573
Element {
507574
text: "!".to_owned(),
508575
style: anstyle::Style::default(),
576+
url: Some(URL.to_owned()),
509577
},
510578
];
511579
verify(&input, expected);
@@ -522,10 +590,12 @@ mod test {
522590
Element {
523591
text: "Hello".to_owned(),
524592
style: green_on_red,
593+
url: None,
525594
},
526595
Element {
527596
text: " world!".to_owned(),
528597
style: anstyle::Style::default(),
598+
url: None,
529599
},
530600
];
531601
verify(&input, expected);
@@ -541,6 +611,7 @@ mod test {
541611
vec![Element {
542612
text: s.clone(),
543613
style: anstyle::Style::default(),
614+
url: None,
544615
}]
545616
};
546617
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)