rs_web/markdown/transforms/
lazy_images.rs

1use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
2
3use super::AstTransform;
4use crate::markdown::TransformContext;
5
6/// Transform that adds lazy loading attributes to images
7pub struct LazyImagesTransform;
8
9impl AstTransform for LazyImagesTransform {
10    fn name(&self) -> &'static str {
11        "lazy_images"
12    }
13
14    fn priority(&self) -> i32 {
15        50
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() + 10);
20        let mut in_image = false;
21        let mut image_info: Option<(CowStr<'a>, CowStr<'a>, CowStr<'a>)> = None;
22
23        for event in events {
24            match &event {
25                Event::Start(Tag::Image {
26                    link_type,
27                    dest_url,
28                    title,
29                    id,
30                }) => {
31                    in_image = true;
32                    image_info = Some((dest_url.clone(), title.clone(), id.clone()));
33                    // We'll emit a custom HTML instead
34                    let _ = link_type; // Suppress unused warning
35                }
36                Event::End(TagEnd::Image) if in_image => {
37                    in_image = false;
38                    if let Some((dest_url, title, _id)) = image_info.take() {
39                        // Convert to WebP path if it's a local image
40                        let src = if !dest_url.starts_with("http") && !dest_url.ends_with(".webp") {
41                            // Replace extension with .webp for local images
42                            if let Some(pos) = dest_url.rfind('.') {
43                                format!("{}.webp", &dest_url[..pos])
44                            } else {
45                                dest_url.to_string()
46                            }
47                        } else {
48                            dest_url.to_string()
49                        };
50
51                        let html = format!(
52                            r#"<img src="{}" alt="{}" loading="lazy" decoding="async">"#,
53                            src,
54                            html_escape(&title)
55                        );
56                        result.push(Event::Html(CowStr::from(html)));
57                    }
58                }
59                Event::Text(text) if in_image => {
60                    // This is the alt text, update image_info
61                    if let Some((dest, _title, id)) = image_info.take() {
62                        image_info = Some((dest, text.clone(), id));
63                    }
64                }
65                _ => {
66                    result.push(event);
67                }
68            }
69        }
70
71        result
72    }
73}
74
75fn html_escape(s: &str) -> String {
76    s.replace('&', "&amp;")
77        .replace('<', "&lt;")
78        .replace('>', "&gt;")
79        .replace('"', "&quot;")
80}