at main 188 lines 6.6 kB view raw
1#[cfg(feature = "syntax-css")] 2use crate::css::{generate_base_css, generate_syntax_css}; 3use crate::static_site::context::{KaTeXSource, StaticSiteContext}; 4use crate::theme::default_resolved_theme; 5use miette::IntoDiagnostic; 6use weaver_common::jacquard::client::AgentSession; 7 8#[derive(Debug, Clone, Copy)] 9pub enum CssMode { 10 Linked, 11 Inline, 12} 13 14pub async fn write_document_head<A: AgentSession>( 15 context: &StaticSiteContext<A>, 16 writer: &mut (impl tokio::io::AsyncWrite + Unpin), 17 css_mode: CssMode, 18 output_path: &std::path::Path, 19) -> miette::Result<()> { 20 use tokio::io::AsyncWriteExt; 21 22 // Get title from frontmatter or current path 23 let title = if let Some(path) = context 24 .dir_contents 25 .as_ref() 26 .and_then(|contents| contents.get(context.position)) 27 { 28 context 29 .titles 30 .get(path) 31 .map(|t| t.value().to_string()) 32 .unwrap_or_else(|| { 33 path.file_stem() 34 .and_then(|s| s.to_str()) 35 .unwrap_or("Untitled") 36 .to_string() 37 }) 38 } else { 39 "Untitled".to_string() 40 }; 41 42 // Calculate relative path to root based on output file depth 43 let relative_to_root = if let Ok(rel) = output_path.strip_prefix(&context.destination) { 44 let depth = rel.components().count() - 1; // -1 because the file itself doesn't count 45 if depth <= 0 { 46 "./".to_string() 47 } else { 48 "../".repeat(depth) 49 } 50 } else { 51 "./".to_string() 52 }; 53 54 writer 55 .write_all(b"<!DOCTYPE html>\n") 56 .await 57 .into_diagnostic()?; 58 writer 59 .write_all(b"<html lang=\"en\">\n") 60 .await 61 .into_diagnostic()?; 62 writer.write_all(b"<head>\n").await.into_diagnostic()?; 63 writer 64 .write_all(b" <meta charset=\"utf-8\">\n") 65 .await 66 .into_diagnostic()?; 67 writer 68 .write_all(b" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n") 69 .await 70 .into_diagnostic()?; 71 72 // Title 73 writer.write_all(b" <title>").await.into_diagnostic()?; 74 writer.write_all(title.as_bytes()).await.into_diagnostic()?; 75 writer.write_all(b"</title>\n").await.into_diagnostic()?; 76 77 // CSS 78 match css_mode { 79 CssMode::Linked => { 80 writer 81 .write_all( 82 format!( 83 " <link rel=\"stylesheet\" href=\"{}css/base.css\">\n", 84 relative_to_root 85 ) 86 .as_bytes(), 87 ) 88 .await 89 .into_diagnostic()?; 90 writer 91 .write_all( 92 format!( 93 " <link rel=\"stylesheet\" href=\"{}css/syntax.css\">\n", 94 relative_to_root 95 ) 96 .as_bytes(), 97 ) 98 .await 99 .into_diagnostic()?; 100 } 101 #[cfg(feature = "syntax-css")] 102 CssMode::Inline => { 103 let default_theme = default_resolved_theme(); 104 let theme = context.theme.as_deref().unwrap_or(&default_theme); 105 106 writer.write_all(b" <style>\n").await.into_diagnostic()?; 107 writer 108 .write_all(generate_base_css(theme).as_bytes()) 109 .await 110 .into_diagnostic()?; 111 writer.write_all(b" </style>\n").await.into_diagnostic()?; 112 113 writer.write_all(b" <style>\n").await.into_diagnostic()?; 114 let syntax_css = generate_syntax_css(theme).await?; 115 writer 116 .write_all(syntax_css.as_bytes()) 117 .await 118 .into_diagnostic()?; 119 writer.write_all(b" </style>\n").await.into_diagnostic()?; 120 } 121 #[cfg(not(feature = "syntax-css"))] 122 CssMode::Inline => { 123 // CSS generation not available without syntax-css feature 124 return Err(miette::miette!( 125 "Inline CSS mode requires the 'syntax-css' feature" 126 )); 127 } 128 } 129 130 // KaTeX if enabled 131 if let Some(ref katex) = context.katex_source { 132 match katex { 133 KaTeXSource::Cdn => { 134 writer.write_all(b" <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css\">\n").await.into_diagnostic()?; 135 writer.write_all(b" <script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js\"></script>\n").await.into_diagnostic()?; 136 writer.write_all(b" <script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js\" onload=\"renderMathInElement(document.body);\"></script>\n").await.into_diagnostic()?; 137 } 138 KaTeXSource::Local(path) => { 139 let path_str = path.to_string_lossy(); 140 writer 141 .write_all( 142 format!( 143 " <link rel=\"stylesheet\" href=\"{}/katex.min.css\">\n", 144 path_str 145 ) 146 .as_bytes(), 147 ) 148 .await 149 .into_diagnostic()?; 150 writer 151 .write_all( 152 format!( 153 " <script defer src=\"{}/katex.min.js\"></script>\n", 154 path_str 155 ) 156 .as_bytes(), 157 ) 158 .await 159 .into_diagnostic()?; 160 writer.write_all(format!(" <script defer src=\"{}/contrib/auto-render.min.js\" onload=\"renderMathInElement(document.body);\"></script>\n", path_str).as_bytes()).await.into_diagnostic()?; 161 } 162 } 163 } 164 165 writer.write_all(b"</head>\n").await.into_diagnostic()?; 166 writer 167 .write_all(b"<body style=\"background: var(--color-base); min-height: 100vh;\">\n") 168 .await 169 .into_diagnostic()?; 170 writer 171 .write_all(b"<div class=\"notebook-content\">\n") 172 .await 173 .into_diagnostic()?; 174 175 Ok(()) 176} 177 178pub async fn write_document_footer( 179 writer: &mut (impl tokio::io::AsyncWrite + Unpin), 180) -> miette::Result<()> { 181 use tokio::io::AsyncWriteExt; 182 183 writer.write_all(b"</div>\n").await.into_diagnostic()?; 184 writer.write_all(b"</body>\n").await.into_diagnostic()?; 185 writer.write_all(b"</html>\n").await.into_diagnostic()?; 186 187 Ok(()) 188}