Author:
Hash:
Timestamp:
+99 -40 +/-11 browse
Kevin Schoon [me@kevinschoon.com]
f47fcac33b554b429871ccc220016aa917df8965
Sun, 26 May 2024 20:44:24 +0000 (1.6 years ago)
| 1 | diff --git a/ayllu/src/config.rs b/ayllu/src/config.rs |
| 2 | index 792bf10..e602201 100644 |
| 3 | --- a/ayllu/src/config.rs |
| 4 | +++ b/ayllu/src/config.rs |
| 5 | @@ -69,7 +69,6 @@ pub struct Web { |
| 6 | } |
| 7 | |
| 8 | impl Web { |
| 9 | - |
| 10 | fn default_default_theme() -> String { |
| 11 | String::from("default") |
| 12 | } |
| 13 | @@ -341,24 +340,6 @@ Disallow: /*/*/chart/* |
| 14 | pub fn to_json(&self) -> String { |
| 15 | serde_json::to_string(self).unwrap() |
| 16 | } |
| 17 | - |
| 18 | - pub fn markdown_render_options(&self) -> ComrakOptions { |
| 19 | - let mut opts = ComrakOptions::default(); |
| 20 | - opts.extension.description_lists = true; |
| 21 | - opts.extension.footnotes = true; |
| 22 | - opts.extension.strikethrough = true; |
| 23 | - opts.extension.superscript = true; |
| 24 | - opts.extension.table = true; |
| 25 | - opts.extension.tasklist = true; |
| 26 | - |
| 27 | - opts.parse.smart = true; |
| 28 | - opts.parse.relaxed_tasklist_matching = true; |
| 29 | - // allow raw html in the markdown documents |
| 30 | - opts.render.unsafe_ = self.web.unsafe_markdown; |
| 31 | - // not relevent since we paint ourselves I think |
| 32 | - // opts.render.github_pre_lang = false; |
| 33 | - opts |
| 34 | - } |
| 35 | } |
| 36 | |
| 37 | #[cfg(test)] |
| 38 | diff --git a/ayllu/src/readme.rs b/ayllu/src/readme.rs |
| 39 | index 64f7248..644797b 100644 |
| 40 | --- a/ayllu/src/readme.rs |
| 41 | +++ b/ayllu/src/readme.rs |
| 42 | @@ -4,12 +4,10 @@ use comrak::{ |
| 43 | ComrakPlugins, |
| 44 | }; |
| 45 | |
| 46 | - // TODO: Make configurable as part of the loaded theme |
| 47 | - const ARROW_SYMBOL_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon" width="24" height="24" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .75.75 0 0 1 .018-1.042.75.75 0 0 1 1.042-.018 2 2 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.75.75 0 0 1-1.042-.018.75.75 0 0 1-.018-1.042m-4.69 9.64a2 2 0 0 0 2.83 0l1.25-1.25a.75.75 0 0 1 1.042.018.75.75 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .75.75 0 0 1-.018 1.042.75.75 0 0 1-1.042.018 2 2 0 0 0-2.83 0l-2.5 2.5a2 2 0 0 0 0 2.83"></path></svg>"#; |
| 48 | + /// wraps readme headings with an anchor link and image |
| 49 | + struct HeadingWriter<'a>(&'a str); |
| 50 | |
| 51 | - struct HeadingWriter; |
| 52 | - |
| 53 | - impl HeadingAdapter for HeadingWriter { |
| 54 | + impl HeadingAdapter for HeadingWriter<'_> { |
| 55 | fn enter( |
| 56 | &self, |
| 57 | output: &mut dyn std::io::prelude::Write, |
| 58 | @@ -19,8 +17,9 @@ impl HeadingAdapter for HeadingWriter { |
| 59 | let mut anchorizer = Anchorizer::new(); |
| 60 | let id = anchorizer.anchorize(heading.content.clone()); |
| 61 | let level = heading.level; |
| 62 | + let anchor_symbol = self.0; |
| 63 | output.write_all(format!( |
| 64 | - "<header><a class=\"icon-link\" href=\"#{id}\">{ARROW_SYMBOL_SVG}</a><h{level} id={id} dir=auto>" |
| 65 | + "<header><a class=\"icon-link\" href=\"#{id}\">{anchor_symbol}</a><h{level} id={id} dir=auto>" |
| 66 | ).as_bytes()) |
| 67 | } |
| 68 | |
| 69 | @@ -42,6 +41,8 @@ pub struct Renderer<'a> { |
| 70 | pub collection: &'a str, |
| 71 | /// project name |
| 72 | pub name: &'a str, |
| 73 | + /// raw svg content of the anchor symbol |
| 74 | + pub anchor_symbol: &'a str, |
| 75 | /// enable comrak unsafe markdown option |
| 76 | pub allow_unsafe_markdown: bool, |
| 77 | } |
| 78 | @@ -62,10 +63,14 @@ impl Renderer<'_> { |
| 79 | opts.render.unsafe_ = self.allow_unsafe_markdown; |
| 80 | // not relevent since we paint ourselves I think |
| 81 | // opts.render.github_pre_lang = false; |
| 82 | + // |
| 83 | + |
| 84 | + let anchor_symbol = self.anchor_symbol; |
| 85 | + let heading_writer = &HeadingWriter(anchor_symbol); |
| 86 | |
| 87 | let mut plugins = ComrakPlugins::default(); |
| 88 | plugins.render.codefence_syntax_highlighter = Some(&adapter); |
| 89 | - plugins.render.heading_adapter = Some(&HeadingWriter {}); |
| 90 | + plugins.render.heading_adapter = Some(heading_writer); |
| 91 | markdown_to_html_with_plugins(readme, &opts, &plugins) |
| 92 | } |
| 93 | } |
| 94 | @@ -84,6 +89,7 @@ mod tests { |
| 95 | origin: "https://ayllu-forge.org", |
| 96 | collection: "projects", |
| 97 | name: "ayllu", |
| 98 | + anchor_symbol: "", |
| 99 | allow_unsafe_markdown: true, |
| 100 | }; |
| 101 | |
| 102 | diff --git a/ayllu/src/web2/middleware/mod.rs b/ayllu/src/web2/middleware/mod.rs |
| 103 | index 59371b3..b16da1f 100644 |
| 104 | --- a/ayllu/src/web2/middleware/mod.rs |
| 105 | +++ b/ayllu/src/web2/middleware/mod.rs |
| 106 | @@ -3,3 +3,5 @@ pub mod repository; |
| 107 | pub mod rpc_initiator; |
| 108 | pub mod sites; |
| 109 | pub mod template; |
| 110 | + pub mod theme; |
| 111 | + |
| 112 | diff --git a/ayllu/src/web2/middleware/template.rs b/ayllu/src/web2/middleware/template.rs |
| 113 | index 4efe163..1907ed2 100644 |
| 114 | --- a/ayllu/src/web2/middleware/template.rs |
| 115 | +++ b/ayllu/src/web2/middleware/template.rs |
| 116 | @@ -17,6 +17,7 @@ pub struct CommonParams { |
| 117 | pub name: Option<String>, |
| 118 | } |
| 119 | |
| 120 | + /// initialize the currently configured theme as a (tera, context) |
| 121 | pub async fn middleware( |
| 122 | extract::State(state): State, |
| 123 | ConfigReader(config): ConfigReader, |
| 124 | diff --git a/ayllu/src/web2/middleware/theme.rs b/ayllu/src/web2/middleware/theme.rs |
| 125 | new file mode 100644 |
| 126 | index 0000000..1228750 |
| 127 | --- /dev/null |
| 128 | +++ b/ayllu/src/web2/middleware/theme.rs |
| 129 | @@ -0,0 +1,25 @@ |
| 130 | + use std::sync::Arc; |
| 131 | + |
| 132 | + use axum::{extract, middleware::Next, response::Response}; |
| 133 | + |
| 134 | + use crate::config::Config; |
| 135 | + use crate::web2::extractors::config::ConfigReader; |
| 136 | + use crate::web2::terautil::{Loader, Themes}; |
| 137 | + |
| 138 | + pub type State = extract::State<Arc<(Config, Themes)>>; |
| 139 | + |
| 140 | + /// adds the current theme as an extension for direct access |
| 141 | + pub async fn middleware( |
| 142 | + extract::State(state): State, |
| 143 | + ConfigReader(config): ConfigReader, |
| 144 | + mut req: extract::Request, |
| 145 | + next: Next, |
| 146 | + ) -> Response { |
| 147 | + let loader = Loader { |
| 148 | + themes: state.1.clone(), |
| 149 | + default_theme: state.0.web.default_theme.clone(), |
| 150 | + }; |
| 151 | + let theme = loader.theme(config.theme.as_deref()); |
| 152 | + req.extensions_mut().insert(theme); |
| 153 | + next.run(req).await |
| 154 | + } |
| 155 | diff --git a/ayllu/src/web2/routes/blob.rs b/ayllu/src/web2/routes/blob.rs |
| 156 | index 39f6e3b..d1d3bde 100644 |
| 157 | --- a/ayllu/src/web2/routes/blob.rs |
| 158 | +++ b/ayllu/src/web2/routes/blob.rs |
| 159 | @@ -1,7 +1,5 @@ |
| 160 | use std::time::SystemTime; |
| 161 | |
| 162 | - use comrak::{markdown_to_html_with_plugins, ComrakPlugins}; |
| 163 | - |
| 164 | use axum::{ |
| 165 | body::Bytes, |
| 166 | extract::Extension, |
| 167 | @@ -10,12 +8,14 @@ use axum::{ |
| 168 | }; |
| 169 | |
| 170 | use crate::config::Config; |
| 171 | + use crate::highlight::{Highlighter, TreeSitterAdapter}; |
| 172 | use crate::languages::LANGUAGE_TABLE; |
| 173 | + use crate::readme::Renderer; |
| 174 | use crate::web2::error::Error; |
| 175 | - use crate::highlight::{Highlighter, TreeSitterAdapter}; |
| 176 | use crate::web2::middleware::repository::Preamble; |
| 177 | use crate::web2::middleware::template::Template; |
| 178 | use crate::web2::navigation; |
| 179 | + use crate::web2::terautil::Theme; |
| 180 | use crate::web2::util; |
| 181 | use ayllu_git::Wrapper; |
| 182 | |
| 183 | @@ -25,6 +25,7 @@ pub async fn serve( |
| 184 | Extension(preamble): Extension<Preamble>, |
| 185 | Extension(highlighter): Extension<Highlighter>, |
| 186 | Extension(adapter): Extension<TreeSitterAdapter>, |
| 187 | + Extension(current_theme): Extension<Theme>, |
| 188 | Extension((templates, mut ctx)): Extension<Template>, |
| 189 | ) -> Result<Html<String>, Error> { |
| 190 | let repository = Wrapper::new(preamble.repo_path.as_path())?; |
| 191 | @@ -59,14 +60,23 @@ pub async fn serve( |
| 192 | ctx.insert("is_binary", &true); |
| 193 | ctx.insert("content", &None::<()>); |
| 194 | } else if mime_type.to_string().as_str() == "text/markdown" { |
| 195 | - let mut plugins = ComrakPlugins::default(); |
| 196 | - plugins.render.codefence_syntax_highlighter = Some(&adapter); |
| 197 | - let content = String::from_utf8(blob.content).unwrap(); |
| 198 | - let content = markdown_to_html_with_plugins( |
| 199 | - content.as_str(), |
| 200 | - &cfg.markdown_render_options(), |
| 201 | - &plugins, |
| 202 | - ); |
| 203 | + // find the anchor symbol in the current theme |
| 204 | + let anchor_symbol = current_theme |
| 205 | + .1 |
| 206 | + .0 |
| 207 | + .get("anchor.svg") |
| 208 | + .expect("missing default asset"); |
| 209 | + |
| 210 | + let anchor_symbol = anchor_symbol.read_string().await?; |
| 211 | + let renderer = Renderer { |
| 212 | + origin: &cfg.origin, |
| 213 | + collection: &preamble.collection_name, |
| 214 | + name: &preamble.repo_name, |
| 215 | + anchor_symbol: &anchor_symbol, |
| 216 | + allow_unsafe_markdown: cfg.web.unsafe_markdown, |
| 217 | + }; |
| 218 | + let readme = String::from_utf8(blob.content).unwrap(); |
| 219 | + let content = renderer.render(adapter, &readme); |
| 220 | ctx.insert("content", &content); |
| 221 | is_markdown = true; |
| 222 | } else if blob.is_binary { |
| 223 | diff --git a/ayllu/src/web2/routes/repo.rs b/ayllu/src/web2/routes/repo.rs |
| 224 | index b55f3cc..1c84028 100644 |
| 225 | --- a/ayllu/src/web2/routes/repo.rs |
| 226 | +++ b/ayllu/src/web2/routes/repo.rs |
| 227 | @@ -25,6 +25,7 @@ use crate::web2::middleware::repository::Preamble; |
| 228 | use crate::web2::middleware::rpc_initiator::Initiator; |
| 229 | use crate::web2::middleware::template::Template; |
| 230 | use crate::web2::navigation; |
| 231 | + use crate::web2::terautil::Theme; |
| 232 | use crate::web2::util; |
| 233 | |
| 234 | use ayllu_database::Wrapper as Database; |
| 235 | @@ -141,6 +142,7 @@ pub struct EmailLink { |
| 236 | } |
| 237 | |
| 238 | #[debug_handler] |
| 239 | + #[allow(clippy::too_many_arguments)] |
| 240 | pub async fn serve( |
| 241 | uri: OriginalUri, |
| 242 | Extension(cfg): Extension<Config>, |
| 243 | @@ -148,6 +150,7 @@ pub async fn serve( |
| 244 | Extension(db): Extension<Arc<Database>>, |
| 245 | Extension(preamble): Extension<Preamble>, |
| 246 | Extension(adapter): Extension<TreeSitterAdapter>, |
| 247 | + Extension(current_theme): Extension<Theme>, |
| 248 | Extension((mut templates, mut ctx)): Extension<Template>, |
| 249 | ) -> Result<Html<String>, Error> { |
| 250 | let repository = Wrapper::new(preamble.repo_path.as_path())?; |
| 251 | @@ -218,10 +221,19 @@ pub async fn serve( |
| 252 | |
| 253 | let readme = match readme { |
| 254 | Some(readme) => { |
| 255 | + // find the anchor symbol in the current theme |
| 256 | + let anchor_symbol = current_theme |
| 257 | + .1 |
| 258 | + .0 |
| 259 | + .get("anchor.svg") |
| 260 | + .expect("missing default asset"); |
| 261 | + |
| 262 | + let anchor_symbol = anchor_symbol.read_string().await?; |
| 263 | let renderer = Renderer { |
| 264 | origin: &cfg.origin, |
| 265 | collection: &preamble.collection_name, |
| 266 | name: &preamble.repo_name, |
| 267 | + anchor_symbol: &anchor_symbol, |
| 268 | allow_unsafe_markdown: cfg.web.unsafe_markdown, |
| 269 | }; |
| 270 | renderer.render(adapter, &readme) |
| 271 | diff --git a/ayllu/src/web2/server.rs b/ayllu/src/web2/server.rs |
| 272 | index 6bd546d..431ebba 100644 |
| 273 | --- a/ayllu/src/web2/server.rs |
| 274 | +++ b/ayllu/src/web2/server.rs |
| 275 | @@ -16,13 +16,14 @@ use tower_http::{ |
| 276 | use tracing::{Level, Span}; |
| 277 | |
| 278 | use crate::config::Config; |
| 279 | - use crate::languages::{Hint, LANGUAGE_TABLE}; |
| 280 | use crate::highlight::{Highlighter, Loader, TreeSitterAdapter}; |
| 281 | + use crate::languages::{Hint, LANGUAGE_TABLE}; |
| 282 | use crate::web2::middleware::error; |
| 283 | use crate::web2::middleware::repository; |
| 284 | use crate::web2::middleware::rpc_initiator; |
| 285 | use crate::web2::middleware::sites; |
| 286 | use crate::web2::middleware::template; |
| 287 | + use crate::web2::middleware::theme; |
| 288 | use crate::web2::routes::about; |
| 289 | use crate::web2::routes::assets; |
| 290 | use crate::web2::routes::authors; |
| 291 | @@ -235,6 +236,10 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
| 292 | .layer(from_fn_with_state( |
| 293 | Arc::new((cfg.clone(), themes.clone())), |
| 294 | template::middleware, |
| 295 | + )) |
| 296 | + .layer(from_fn_with_state( |
| 297 | + Arc::new((cfg.clone(), themes.clone())), |
| 298 | + theme::middleware, |
| 299 | )), |
| 300 | ) |
| 301 | .route( |
| 302 | diff --git a/ayllu/src/web2/terautil/loader.rs b/ayllu/src/web2/terautil/loader.rs |
| 303 | index 9c9b37c..d4c6b60 100644 |
| 304 | --- a/ayllu/src/web2/terautil/loader.rs |
| 305 | +++ b/ayllu/src/web2/terautil/loader.rs |
| 306 | @@ -24,7 +24,7 @@ pub struct Loader { |
| 307 | } |
| 308 | |
| 309 | impl Loader { |
| 310 | - fn theme(&self, name: Option<&str>) -> super::themes::Theme { |
| 311 | + pub fn theme(&self, name: Option<&str>) -> super::themes::Theme { |
| 312 | let default_tmpl = || { |
| 313 | self.themes |
| 314 | .0 |
| 315 | @@ -42,7 +42,7 @@ impl Loader { |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | - // load the tera context of the theme or default theme if unspecified |
| 320 | + /// load the tera context of the theme or default theme if unspecified |
| 321 | pub fn load(&self, options: Options, theme_name: Option<String>) -> (Tera, Context) { |
| 322 | let theme = self.theme(theme_name.as_deref()); |
| 323 | let mut ctx = Context::new(); |
| 324 | diff --git a/ayllu/src/web2/terautil/themes.rs b/ayllu/src/web2/terautil/themes.rs |
| 325 | index 705ab70..8b2bb5f 100644 |
| 326 | --- a/ayllu/src/web2/terautil/themes.rs |
| 327 | +++ b/ayllu/src/web2/terautil/themes.rs |
| 328 | @@ -39,6 +39,22 @@ pub enum Asset { |
| 329 | FilePath(PathBuf), |
| 330 | } |
| 331 | |
| 332 | + impl Asset { |
| 333 | + /// read the asset to byte vec |
| 334 | + pub async fn read_bytes(&self) -> Result<Vec<u8>, IoError> { |
| 335 | + match self { |
| 336 | + Asset::Raw(content) => Ok(content.to_vec()), |
| 337 | + Asset::FilePath(fp) => tokio::fs::read(fp).await, |
| 338 | + } |
| 339 | + } |
| 340 | + |
| 341 | + /// read the asset as a string assuming utf8 |
| 342 | + pub async fn read_string(&self) -> Result<String, IoError> { |
| 343 | + let raw_bytes = self.read_bytes().await?; |
| 344 | + Ok(String::from_utf8_lossy(&raw_bytes).to_string()) |
| 345 | + } |
| 346 | + } |
| 347 | + |
| 348 | /// Collection of all the assets associated with the given theme that are |
| 349 | /// served either from memory or on the file system. |
| 350 | #[derive(Clone, Debug)] |
| 351 | diff --git a/ayllu/themes/default/assets/anchor.svg b/ayllu/themes/default/assets/anchor.svg |
| 352 | new file mode 100644 |
| 353 | index 0000000..62454e3 |
| 354 | --- /dev/null |
| 355 | +++ b/ayllu/themes/default/assets/anchor.svg |
| 356 | @@ -0,0 +1 @@ |
| 357 | + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon" width="24" height="24" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .75.75 0 0 1 .018-1.042.75.75 0 0 1 1.042-.018 2 2 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.75.75 0 0 1-1.042-.018.75.75 0 0 1-.018-1.042m-4.69 9.64a2 2 0 0 0 2.83 0l1.25-1.25a.75.75 0 0 1 1.042.018.75.75 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .75.75 0 0 1-.018 1.042.75.75 0 0 1-1.042.018 2 2 0 0 0-2.83 0l-2.5 2.5a2 2 0 0 0 0 2.83"></path></svg> |