at main 227 lines 7.1 kB view raw
1//! Rendering traits for the editor. 2//! 3//! These traits abstract over external concerns during rendering: 4//! - Resolving embed URLs to HTML content 5//! - Resolving image URLs to CDN paths 6//! - Validating wikilinks 7//! 8//! Implementations are provided by the consuming application (e.g., weaver-app). 9 10use markdown_weaver::Tag; 11 12/// Provides HTML content for embedded resources. 13/// 14/// When rendering markdown with embeds (e.g., `![[at://...]]`), this trait 15/// is consulted to get the pre-rendered HTML for the embed. 16/// 17/// The full `Tag::Embed` is provided so implementations can access all context: 18/// embed_type, dest_url, title, id, and attrs. 19pub trait EmbedContentProvider { 20 /// Get HTML content for an embed tag. 21 /// 22 /// Returns `Some(html)` if the embed content is available, 23 /// `None` to render a placeholder. 24 fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String>; 25} 26 27/// Unit type implementation - no embeds available. 28impl EmbedContentProvider for () { 29 fn get_embed_content(&self, _tag: &Tag<'_>) -> Option<String> { 30 None 31 } 32} 33 34/// Resolves image URLs from markdown to actual paths. 35/// 36/// Markdown may reference images by name (e.g., `/image/photo.jpg`). 37/// This trait maps those to actual CDN URLs or data URLs. 38pub trait ImageResolver { 39 /// Resolve an image URL from markdown to an actual URL. 40 /// 41 /// Returns `Some(resolved_url)` if the image is found, 42 /// `None` to use the original URL unchanged. 43 fn resolve_image_url(&self, url: &str) -> Option<String>; 44} 45 46/// Unit type implementation - no image resolution. 47impl ImageResolver for () { 48 fn resolve_image_url(&self, _url: &str) -> Option<String> { 49 None 50 } 51} 52 53/// Validates wikilinks during rendering. 54/// 55/// Used to add CSS classes indicating whether a wikilink target exists. 56pub trait WikilinkValidator { 57 /// Check if a wikilink target is valid (exists). 58 fn is_valid_link(&self, target: &str) -> bool; 59} 60 61/// Unit type implementation - all links are valid. 62impl WikilinkValidator for () { 63 fn is_valid_link(&self, _target: &str) -> bool { 64 true 65 } 66} 67 68/// Reference implementations for common patterns. 69 70impl<T: EmbedContentProvider> EmbedContentProvider for &T { 71 fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String> { 72 (*self).get_embed_content(tag) 73 } 74} 75 76impl<T: ImageResolver> ImageResolver for &T { 77 fn resolve_image_url(&self, url: &str) -> Option<String> { 78 (*self).resolve_image_url(url) 79 } 80} 81 82impl<T: WikilinkValidator> WikilinkValidator for &T { 83 fn is_valid_link(&self, target: &str) -> bool { 84 (*self).is_valid_link(target) 85 } 86} 87 88impl<T: EmbedContentProvider> EmbedContentProvider for Option<T> { 89 fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String> { 90 self.as_ref().and_then(|p| p.get_embed_content(tag)) 91 } 92} 93 94impl<T: ImageResolver> ImageResolver for Option<T> { 95 fn resolve_image_url(&self, url: &str) -> Option<String> { 96 self.as_ref().and_then(|r| r.resolve_image_url(url)) 97 } 98} 99 100impl<T: WikilinkValidator> WikilinkValidator for Option<T> { 101 fn is_valid_link(&self, target: &str) -> bool { 102 self.as_ref().map(|v| v.is_valid_link(target)).unwrap_or(true) 103 } 104} 105 106/// Implementation for ResolvedContent from weaver-common. 107/// 108/// Resolves AT Protocol embeds by looking up the content in the ResolvedContent map. 109impl EmbedContentProvider for weaver_common::ResolvedContent { 110 fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String> { 111 if let Tag::Embed { dest_url, .. } = tag { 112 let url = dest_url.as_ref(); 113 if url.starts_with("at://") { 114 if let Ok(at_uri) = jacquard::types::string::AtUri::new(url) { 115 return weaver_common::ResolvedContent::get_embed_content(self, &at_uri) 116 .map(|s| s.to_string()); 117 } 118 } 119 } 120 None 121 } 122} 123 124#[cfg(test)] 125mod tests { 126 use super::*; 127 use markdown_weaver::EmbedType; 128 129 struct TestEmbedProvider; 130 131 impl EmbedContentProvider for TestEmbedProvider { 132 fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String> { 133 if let Tag::Embed { dest_url, .. } = tag { 134 if dest_url.as_ref() == "at://test/embed" { 135 return Some("<div>Test Embed</div>".to_string()); 136 } 137 } 138 None 139 } 140 } 141 142 struct TestImageResolver; 143 144 impl ImageResolver for TestImageResolver { 145 fn resolve_image_url(&self, url: &str) -> Option<String> { 146 if url.starts_with("/image/") { 147 Some(format!("https://cdn.example.com{}", url)) 148 } else { 149 None 150 } 151 } 152 } 153 154 struct TestWikilinkValidator { 155 valid: Vec<String>, 156 } 157 158 impl WikilinkValidator for TestWikilinkValidator { 159 fn is_valid_link(&self, target: &str) -> bool { 160 self.valid.iter().any(|v| v == target) 161 } 162 } 163 164 fn make_embed_tag(url: &str) -> Tag<'_> { 165 Tag::Embed { 166 embed_type: EmbedType::Other, 167 dest_url: url.into(), 168 title: "".into(), 169 id: "".into(), 170 attrs: None, 171 } 172 } 173 174 #[test] 175 fn test_embed_provider() { 176 let provider = TestEmbedProvider; 177 assert_eq!( 178 provider.get_embed_content(&make_embed_tag("at://test/embed")), 179 Some("<div>Test Embed</div>".to_string()) 180 ); 181 assert_eq!(provider.get_embed_content(&make_embed_tag("at://other")), None); 182 } 183 184 #[test] 185 fn test_image_resolver() { 186 let resolver = TestImageResolver; 187 assert_eq!( 188 resolver.resolve_image_url("/image/photo.jpg"), 189 Some("https://cdn.example.com/image/photo.jpg".to_string()) 190 ); 191 assert_eq!(resolver.resolve_image_url("https://other.com/img.png"), None); 192 } 193 194 #[test] 195 fn test_wikilink_validator() { 196 let validator = TestWikilinkValidator { 197 valid: vec!["Home".to_string(), "About".to_string()], 198 }; 199 assert!(validator.is_valid_link("Home")); 200 assert!(validator.is_valid_link("About")); 201 assert!(!validator.is_valid_link("Missing")); 202 } 203 204 #[test] 205 fn test_unit_impls() { 206 let embed: () = (); 207 assert_eq!(embed.get_embed_content(&make_embed_tag("anything")), None); 208 209 let image: () = (); 210 assert_eq!(image.resolve_image_url("anything"), None); 211 212 let wiki: () = (); 213 assert!(wiki.is_valid_link("anything")); // default true 214 } 215 216 #[test] 217 fn test_option_impls() { 218 let some_provider: Option<TestEmbedProvider> = Some(TestEmbedProvider); 219 assert_eq!( 220 some_provider.get_embed_content(&make_embed_tag("at://test/embed")), 221 Some("<div>Test Embed</div>".to_string()) 222 ); 223 224 let none_provider: Option<TestEmbedProvider> = None; 225 assert_eq!(none_provider.get_embed_content(&make_embed_tag("at://test/embed")), None); 226 } 227}