Author:
Hash:
Timestamp:
+99 -40 +/-11 browse
Kevin Schoon [me@kevinschoon.com]
f47fcac33b554b429871ccc220016aa917df8965
Sun, 26 May 2024 20:44:24 +0000 (11 months 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> |