at main 205 lines 6.8 kB view raw
1//! Sync notebook to site.standard.publication. 2 3use jacquard::CowStr; 4use jacquard::IntoStatic; 5use jacquard::client::AgentSessionExt; 6use jacquard::types::aturi::AtUri; 7use jacquard::types::string::{RecordKey, Uri}; 8use weaver_api::site_standard::publication::Publication; 9use weaver_api::site_standard::theme::basic::Basic as BasicTheme; 10use weaver_api::site_standard::theme::color::Rgb; 11use weaver_common::WeaverError; 12 13use crate::components::inline_theme_editor::InlineThemeValues; 14use crate::fetch::Fetcher; 15 16/// Create or update a site.standard.publication for a notebook. 17pub async fn sync_publication( 18 fetcher: &Fetcher, 19 notebook_uri: &AtUri<'_>, 20 title: &str, 21 path: &str, 22 theme: &InlineThemeValues, 23) -> Result<AtUri<'static>, WeaverError> { 24 // Build basicTheme from inline values. 25 let basic_theme = build_basic_theme(theme); 26 27 // Build publication record. 28 let url_str = format!("https://weaver.sh/{}/{}", notebook_uri.authority(), path); 29 let url = Uri::new(&url_str) 30 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 31 .into_static(); 32 let publication = Publication::new() 33 .name(CowStr::from(title)) 34 .url(url) 35 .maybe_basic_theme(Some(basic_theme)) 36 .build(); 37 38 // Use same rkey as notebook for 1:1 mapping. 39 let notebook_rkey = notebook_uri 40 .rkey() 41 .ok_or_else(|| WeaverError::InvalidNotebook("Notebook URI missing rkey".into()))?; 42 43 // Use put_record for upsert behavior. 44 let response = fetcher 45 .put_record( 46 RecordKey::any(notebook_rkey.as_ref()) 47 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 48 .into_static(), 49 publication, 50 ) 51 .await 52 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; 53 54 Ok(response.uri.into_static()) 55} 56 57/// Delete a site.standard.publication. 58pub async fn delete_publication( 59 fetcher: &Fetcher, 60 notebook_uri: &AtUri<'_>, 61) -> Result<(), WeaverError> { 62 let notebook_rkey = notebook_uri 63 .rkey() 64 .ok_or_else(|| WeaverError::InvalidNotebook("Notebook URI missing rkey".into()))?; 65 66 // Use delete_record - ignore errors if doesn't exist. 67 let _ = fetcher 68 .delete_record::<Publication>( 69 RecordKey::any(notebook_rkey.as_ref()) 70 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 71 .into_static(), 72 ) 73 .await; 74 75 Ok(()) 76} 77 78/// Build a BasicTheme from InlineThemeValues. 79fn build_basic_theme(values: &InlineThemeValues) -> BasicTheme<'static> { 80 // Parse hex colours to RGB values. 81 let (bg_r, bg_g, bg_b) = parse_hex_to_rgb(&values.background); 82 let (fg_r, fg_g, fg_b) = parse_hex_to_rgb(&values.text); 83 let (ac_r, ac_g, ac_b) = parse_hex_to_rgb(&values.primary); 84 85 // Calculate accentForeground for contrast. 86 let (ac_fg_r, ac_fg_g, ac_fg_b) = calculate_contrast_rgb(ac_r, ac_g, ac_b); 87 88 BasicTheme::new() 89 .background( 90 Rgb::new() 91 .r(bg_r as i64) 92 .g(bg_g as i64) 93 .b(bg_b as i64) 94 .build(), 95 ) 96 .foreground( 97 Rgb::new() 98 .r(fg_r as i64) 99 .g(fg_g as i64) 100 .b(fg_b as i64) 101 .build(), 102 ) 103 .accent( 104 Rgb::new() 105 .r(ac_r as i64) 106 .g(ac_g as i64) 107 .b(ac_b as i64) 108 .build(), 109 ) 110 .accent_foreground( 111 Rgb::new() 112 .r(ac_fg_r as i64) 113 .g(ac_fg_g as i64) 114 .b(ac_fg_b as i64) 115 .build(), 116 ) 117 .build() 118} 119 120/// Parse a hex string (without #) to RGB tuple. 121fn parse_hex_to_rgb(hex: &str) -> (u8, u8, u8) { 122 // Strip leading # if present. 123 let hex = hex.trim_start_matches('#'); 124 125 let r = u8::from_str_radix(&hex.get(0..2).unwrap_or("80"), 16).unwrap_or(128); 126 let g = u8::from_str_radix(&hex.get(2..4).unwrap_or("80"), 16).unwrap_or(128); 127 let b = u8::from_str_radix(&hex.get(4..6).unwrap_or("80"), 16).unwrap_or(128); 128 129 (r, g, b) 130} 131 132/// Calculate a contrasting colour (black or white) based on luminance. 133fn calculate_contrast_rgb(r: u8, g: u8, b: u8) -> (u8, u8, u8) { 134 // Calculate relative luminance. 135 let luminance = (0.299 * r as f64 + 0.587 * g as f64 + 0.114 * b as f64) / 255.0; 136 137 // Return black or white for contrast. 138 if luminance > 0.5 { 139 (0, 0, 0) // Black. 140 } else { 141 (255, 255, 255) // White. 142 } 143} 144 145#[cfg(test)] 146mod tests { 147 use super::*; 148 149 #[test] 150 fn test_parse_hex_to_rgb() { 151 assert_eq!(parse_hex_to_rgb("FF0000"), (255, 0, 0)); 152 assert_eq!(parse_hex_to_rgb("00FF00"), (0, 255, 0)); 153 assert_eq!(parse_hex_to_rgb("0000FF"), (0, 0, 255)); 154 assert_eq!(parse_hex_to_rgb("#FFFFFF"), (255, 255, 255)); 155 assert_eq!(parse_hex_to_rgb("000000"), (0, 0, 0)); 156 } 157 158 #[test] 159 fn test_calculate_contrast_rgb() { 160 // White background -> black text. 161 assert_eq!(calculate_contrast_rgb(255, 255, 255), (0, 0, 0)); 162 // Black background -> white text. 163 assert_eq!(calculate_contrast_rgb(0, 0, 0), (255, 255, 255)); 164 // Bright yellow -> black text. 165 assert_eq!(calculate_contrast_rgb(255, 255, 0), (0, 0, 0)); 166 // Dark blue -> white text. 167 assert_eq!(calculate_contrast_rgb(0, 0, 128), (255, 255, 255)); 168 } 169 170 #[test] 171 fn test_build_basic_theme() { 172 let values = InlineThemeValues { 173 background: "FFFFFF".to_string(), 174 text: "000000".to_string(), 175 primary: "0066CC".to_string(), 176 link: "0066CC".to_string(), 177 light_code_theme: "github".to_string(), 178 dark_code_theme: "github-dark".to_string(), 179 default_mode: "light".to_string(), 180 ..Default::default() 181 }; 182 183 let theme = build_basic_theme(&values); 184 185 // Background should be white. 186 assert_eq!(theme.background.r, 255); 187 assert_eq!(theme.background.g, 255); 188 assert_eq!(theme.background.b, 255); 189 190 // Foreground should be black. 191 assert_eq!(theme.foreground.r, 0); 192 assert_eq!(theme.foreground.g, 0); 193 assert_eq!(theme.foreground.b, 0); 194 195 // Accent should be blue. 196 assert_eq!(theme.accent.r, 0); 197 assert_eq!(theme.accent.g, 102); 198 assert_eq!(theme.accent.b, 204); 199 200 // Accent foreground should be white (contrast against dark blue). 201 assert_eq!(theme.accent_foreground.r, 255); 202 assert_eq!(theme.accent_foreground.g, 255); 203 assert_eq!(theme.accent_foreground.b, 255); 204 } 205}