rs_web/markdown/transforms/
external_links.rs

1use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
2
3use super::AstTransform;
4use crate::markdown::TransformContext;
5
6/// Transform that adds rel="noopener" and target="_blank" to external links
7pub struct ExternalLinksTransform;
8
9impl AstTransform for ExternalLinksTransform {
10    fn name(&self) -> &'static str {
11        "external_links"
12    }
13
14    fn priority(&self) -> i32 {
15        70
16    }
17
18    fn transform<'a>(&self, events: Vec<Event<'a>>, _ctx: &TransformContext<'_>) -> Vec<Event<'a>> {
19        let mut result = Vec::with_capacity(events.len());
20        let mut in_external_link = false;
21
22        for event in events {
23            match &event {
24                Event::Start(Tag::Link {
25                    dest_url, title, ..
26                }) => {
27                    if is_external_url(dest_url) {
28                        in_external_link = true;
29                        // Emit custom HTML for external link start
30                        let html = format!(
31                            r#"<a href="{}" target="_blank" rel="noopener noreferrer"{}>"#,
32                            html_escape(dest_url),
33                            if title.is_empty() {
34                                String::new()
35                            } else {
36                                format!(r#" title="{}""#, html_escape(title))
37                            }
38                        );
39                        result.push(Event::Html(CowStr::from(html)));
40                    } else {
41                        result.push(event);
42                    }
43                }
44                Event::End(TagEnd::Link) if in_external_link => {
45                    in_external_link = false;
46                    result.push(Event::Html(CowStr::from("</a>")));
47                }
48                _ => {
49                    result.push(event);
50                }
51            }
52        }
53
54        result
55    }
56}
57
58fn is_external_url(url: &str) -> bool {
59    url.starts_with("http://") || url.starts_with("https://")
60}
61
62fn html_escape(s: &str) -> String {
63    s.replace('&', "&amp;")
64        .replace('<', "&lt;")
65        .replace('>', "&gt;")
66        .replace('"', "&quot;")
67}