1use std::sync::LazyLock;
7
8static WHITELISTED_DOMAINS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
9 vec![
10 "www.youtube.com",
11 "youtube.com",
12 "youtu.be",
13 "www.figma.com",
14 "figma.com",
15 "gist.github.com",
16 "codepen.io",
17 "cdpn.io",
18 "platform.twitter.com",
19 "syndication.twitter.com",
20 "x.com",
21 "www.x.com",
22 ]
23});
24
25#[allow(dead_code)]
26const MAX_EMBEDS: usize = 10;
27
28pub fn is_domain_whitelisted(url: &str) -> bool {
30 WHITELISTED_DOMAINS.iter().any(|d| url.contains(d))
31}
32
33pub fn count_embeds(content: &str) -> usize {
35 let mut count = 0;
36 let mut in_code_block = false;
37 for line in content.lines() {
38 if line.trim_start().starts_with("```") {
39 in_code_block = !in_code_block;
40 continue;
41 }
42 if in_code_block {
43 continue;
44 }
45 for alt in ["youtube", "figma", "gist", "codepen", "tweet"] {
46 let prefix = format!(";
47 let mut rest = line;
48 while let Some(pos) = rest.find(&prefix) {
49 let after = &rest[pos + prefix.len()..];
50 if let Some(close) = after.find(')') {
51 let url = &after[..close];
52 if !url.trim().is_empty() {
53 count += 1;
54 }
55 rest = &after[close + 1..];
56 } else {
57 break;
58 }
59 }
60 }
61 }
62 count
63}
64
65pub fn render_youtube(video_id: &str) -> String {
67 let video_id = video_id.trim();
68 if video_id.is_empty() {
69 return "<div class=\"embed-error\">YouTube embed: empty video ID</div>".to_string();
70 }
71 if !is_domain_whitelisted(video_id) && video_id.contains('/') {
72 return "<div class=\"embed-error\">YouTube embed blocked: untrusted URL</div>".to_string();
73 }
74 let id = video_id.split('/').next_back().unwrap_or(video_id);
75 format!(
76 r#"<div class="embed-youtube" data-video-id="{id}"><iframe src="https://www.youtube.com/embed/{id}" width="100%" height="360" frameborder="0" allowfullscreen loading="lazy" sandbox="allow-scripts allow-same-origin"></iframe></div>"#,
77 id = html_escape(id)
78 )
79}
80
81pub fn render_figma(url: &str) -> String {
83 let url = url.trim();
84 if !is_domain_whitelisted(url) {
85 return "<div class=\"embed-error\">Figma embed blocked: untrusted URL</div>".to_string();
86 }
87 format!(
88 r#"<div class="embed-figma"><iframe src="https://www.figma.com/embed?embed_host=share&url={url}" width="100%" height="450" frameborder="0" allowfullscreen loading="lazy" sandbox="allow-scripts allow-same-origin"></iframe></div>"#,
89 url = url_encode(url)
90 )
91}
92
93pub fn render_gist(url: &str) -> String {
95 let url = url.trim();
96 if !is_domain_whitelisted(url) {
97 return "<div class=\"embed-error\">Gist embed blocked: untrusted URL</div>".to_string();
98 }
99 let raw = if url.ends_with(".js") {
100 url.to_string()
101 } else {
102 format!("{}.js", url)
103 };
104 format!(
105 r#"<div class="embed-gist"><iframe src="{raw}" width="100%" height="300" frameborder="0" loading="lazy" sandbox="allow-scripts allow-same-origin"></iframe></div>"#,
106 raw = html_escape(&raw)
107 )
108}
109
110pub fn render_codepen(url: &str) -> String {
112 let url = url.trim();
113 if !is_domain_whitelisted(url) {
114 return "<div class=\"embed-error\">CodePen embed blocked: untrusted URL</div>".to_string();
115 }
116 let embed_url = url
117 .replace("codepen.io/", "codepen.io/embed/")
118 .replace("cdpn.io/", "cdpn.io/embed/");
119 let embed_url = if !embed_url.contains("/embed/") {
120 format!("{}/embed/", url.trim_end_matches('/'))
121 } else {
122 embed_url
123 };
124 format!(
125 r#"<div class="embed-codepen"><iframe src="{embed_url}" width="100%" height="400" frameborder="0" loading="lazy" sandbox="allow-scripts allow-same-origin"></iframe></div>"#,
126 embed_url = html_escape(&embed_url)
127 )
128}
129
130pub fn render_tweet(url: &str) -> String {
132 let url = url.trim();
133 if !is_domain_whitelisted(url) {
134 return "<div class=\"embed-error\">Tweet embed blocked: untrusted URL</div>".to_string();
135 }
136 format!(
137 r#"<div class="embed-tweet"><blockquote class="twitter-tweet" data-tweet-url="{url}"><a href="{url}">View Tweet</a></blockquote></div>"#,
138 url = html_escape(url)
139 )
140}
141
142fn html_escape(s: &str) -> String {
143 s.replace('&', "&")
144 .replace('"', """)
145 .replace('<', "<")
146 .replace('>', ">")
147}
148
149fn url_encode(s: &str) -> String {
150 s.replace(' ', "%20")
151 .replace('"', "%22")
152 .replace('#', "%23")
153}
154
155pub fn render_embed(alt: &str, url: &str) -> Option<String> {
159 Some(match alt {
160 "youtube" => render_youtube(url),
161 "figma" => render_figma(url),
162 "gist" => render_gist(url),
163 "codepen" => render_codepen(url),
164 "tweet" => render_tweet(url),
165 _ => return None,
166 })
167}
168
169pub fn embed_csp_policy() -> &'static str {
171 concat!(
172 "frame-src https://www.youtube.com https://youtube.com https://figma.com https://www.figma.com ",
173 "https://gist.github.com https://codepen.io https://cdpn.io ",
174 "https://platform.twitter.com https://syndication.twitter.com https://x.com https://www.x.com; ",
175 "script-src 'self' 'unsafe-inline' https://platform.twitter.com;"
176 )
177}
178
179#[cfg(test)]
180mod tests {
181 #![allow(clippy::unwrap_used)]
182 use super::*;
183
184 #[test]
185 fn test_youtube_render() {
186 let html = render_youtube("dQw4w9WgXcQ");
187 assert!(html.contains("embed-youtube"));
188 assert!(html.contains("youtube.com/embed/dQw4w9WgXcQ"));
189 assert!(html.contains("loading=\"lazy\""));
190 assert!(html.contains("sandbox=\"allow-scripts allow-same-origin\""));
191 }
192
193 #[test]
194 fn test_youtube_render_with_url() {
195 let html = render_youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
196 assert!(html.contains("dQw4w9WgXcQ"));
197 }
198
199 #[test]
200 fn test_figma_render() {
201 let html = render_figma("https://www.figma.com/file/abc123");
202 assert!(html.contains("embed-figma"));
203 assert!(html.contains("figma.com/embed"));
204 assert!(html.contains("loading=\"lazy\""));
205 }
206
207 #[test]
208 fn test_gist_render() {
209 let html = render_gist("https://gist.github.com/user/abc123");
210 assert!(html.contains("embed-gist"));
211 assert!(html.contains("gist.github.com/user/abc123.js"));
212 assert!(html.contains("loading=\"lazy\""));
213 }
214
215 #[test]
216 fn test_codepen_render() {
217 let html = render_codepen("https://codepen.io/user/pen/abc123");
218 assert!(html.contains("embed-codepen"));
219 assert!(html.contains("codepen.io"));
220 assert!(html.contains("loading=\"lazy\""));
221 }
222
223 #[test]
224 fn test_tweet_render() {
225 let html = render_tweet("https://x.com/user/status/123456");
226 assert!(html.contains("embed-tweet"));
227 assert!(html.contains("twitter-tweet"));
228 }
229
230 #[test]
231 fn test_untrusted_domain_blocked() {
232 let html = render_youtube("https://evil.com/steal");
233 assert!(html.contains("blocked"));
234 assert!(!html.contains("<iframe"));
235
236 let html = render_figma("https://evil.com/fake");
237 assert!(html.contains("blocked"));
238
239 let html = render_gist("https://evil.com/fake");
240 assert!(html.contains("blocked"));
241
242 let html = render_codepen("https://evil.com/fake");
243 assert!(html.contains("blocked"));
244
245 let html = render_tweet("https://evil.com/fake");
246 assert!(html.contains("blocked"));
247 }
248
249 #[test]
250 fn test_domain_whitelist() {
251 assert!(is_domain_whitelisted("https://www.youtube.com/watch?v=123"));
252 assert!(is_domain_whitelisted("https://youtube.com/watch?v=123"));
253 assert!(is_domain_whitelisted("https://www.figma.com/file/abc"));
254 assert!(is_domain_whitelisted("https://gist.github.com/user/123"));
255 assert!(is_domain_whitelisted("https://codepen.io/user/pen/abc"));
256 assert!(is_domain_whitelisted("https://x.com/user/status/123"));
257 assert!(!is_domain_whitelisted("https://evil.com/malicious"));
258 }
259
260 #[test]
261 fn test_max_embed_count() {
262 let content = (0..15)
263 .map(|i| format!("", i))
264 .collect::<Vec<_>>()
265 .join("\n");
266 assert_eq!(count_embeds(&content), 15);
267 }
268
269 #[test]
270 fn test_count_embeds_skips_code_blocks() {
271 let content = "\n```\n\n```\n";
272 assert_eq!(count_embeds(content), 2);
273 }
274
275 #[test]
276 fn test_max_embeds_enforced() {
277 let mut embeds = Vec::new();
278 for i in 0..15 {
279 let url = format!("https://www.youtube.com/watch?v={}", i);
280 let count = embeds.len();
281 if count < MAX_EMBEDS {
282 embeds.push(render_youtube(&url));
283 }
284 }
285 assert_eq!(embeds.len(), MAX_EMBEDS);
286 }
287
288 #[test]
289 fn test_render_embed_dispatch() {
290 let html = render_embed("youtube", "abc123").unwrap();
291 assert!(html.contains("embed-youtube"));
292 assert!(render_embed("unknown", "abc123").is_none());
293 }
294
295 #[test]
296 fn test_csp_policy() {
297 let csp = embed_csp_policy();
298 assert!(csp.contains("frame-src"));
299 assert!(csp.contains("youtube.com"));
300 assert!(csp.contains("figma.com"));
301 assert!(csp.contains("twitter.com"));
302 }
303
304 #[test]
305 fn test_html_escape() {
306 assert_eq!(html_escape("a&b\"c<d>e"), "a&b"c<d>e");
307 }
308
309 #[test]
310 fn test_empty_url_returns_error() {
311 let html = render_youtube("");
312 assert!(
313 html.contains("empty") || html.contains("error"),
314 "Expected error for empty URL, got: {}",
315 html
316 );
317 }
318}