Commit

Author:

Hash:

Timestamp:

+99 -40 +/-11 browse

Kevin Schoon [me@kevinschoon.com]

f47fcac33b554b429871ccc220016aa917df8965

Sun, 26 May 2024 20:44:24 +0000 (11 months ago)

make url anchors configurable via system themes
make url anchors configurable via system themes

This makes it so the anchor SVG image is configurable with the rest of the
theme. Additionally a theme Extension has been added so that routes can access
the full theme object with it's underlying assets.
1diff --git a/ayllu/src/config.rs b/ayllu/src/config.rs
2index 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
39index 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
103index 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
113index 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
125new file mode 100644
126index 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
156index 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
224index 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
272index 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
303index 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
325index 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
352new file mode 100644
353index 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>