Author:
Hash:
Timestamp:
+115 -27 +/-3 browse
Kevin Schoon [me@kevinschoon.com]
be9face6bfb12c5b4cf8b517e070511a5d2244a1
Sun, 26 May 2024 23:00:02 +0000 (1.1 years ago)
1 | diff --git a/ayllu/src/readme.rs b/ayllu/src/readme.rs |
2 | index 644797b..04607bc 100644 |
3 | --- a/ayllu/src/readme.rs |
4 | +++ b/ayllu/src/readme.rs |
5 | @@ -1,8 +1,14 @@ |
6 | - use crate::highlight::TreeSitterAdapter; |
7 | + use std::path::Path; |
8 | + |
9 | use comrak::{ |
10 | - adapters::HeadingAdapter, markdown_to_html_with_plugins, Anchorizer, ComrakOptions, |
11 | - ComrakPlugins, |
12 | + adapters::HeadingAdapter, |
13 | + format_html_with_plugins, |
14 | + nodes::{AstNode, NodeLink, NodeValue}, |
15 | + parse_document, Anchorizer, Arena, ComrakOptions, ComrakPlugins, |
16 | }; |
17 | + use url::Url; |
18 | + |
19 | + use crate::highlight::TreeSitterAdapter; |
20 | |
21 | /// wraps readme headings with an anchor link and image |
22 | struct HeadingWriter<'a>(&'a str); |
23 | @@ -34,6 +40,10 @@ impl HeadingAdapter for HeadingWriter<'_> { |
24 | } |
25 | |
26 | /// Responsible for transforming markdown based readme files into HTML |
27 | + /// Local links are re-written as: |
28 | + /// /{collection}/{name}/blob/{refname}/{PATH} |
29 | + /// Local image or video links are re-written as: |
30 | + /// /{collection}/{name}/raw/{refname}/{PATH} |
31 | pub struct Renderer<'a> { |
32 | /// base URL used for re-writes |
33 | pub origin: &'a str, |
34 | @@ -41,6 +51,8 @@ pub struct Renderer<'a> { |
35 | pub collection: &'a str, |
36 | /// project name |
37 | pub name: &'a str, |
38 | + /// refname for path constructor |
39 | + pub refname: &'a str, |
40 | /// raw svg content of the anchor symbol |
41 | pub anchor_symbol: &'a str, |
42 | /// enable comrak unsafe markdown option |
43 | @@ -48,7 +60,34 @@ pub struct Renderer<'a> { |
44 | } |
45 | |
46 | impl Renderer<'_> { |
47 | - pub fn render(self, adapter: TreeSitterAdapter, readme: &str) -> String { |
48 | + fn rewrite_url( |
49 | + collection: &str, |
50 | + name: &str, |
51 | + refname: &str, |
52 | + kind: &str, |
53 | + input: &str, |
54 | + ) -> Option<String> { |
55 | + // ignore any anchor links |
56 | + if input.starts_with('#') { |
57 | + return None; |
58 | + }; |
59 | + // ignore any fully formed valid urls |
60 | + if Url::parse(input).is_ok() { |
61 | + return None; |
62 | + }; |
63 | + let base = format!("/{collection}/{name}/{kind}/{refname}/"); |
64 | + let base = Path::new(&base); |
65 | + let target = Path::new(input.trim_start_matches('/')); |
66 | + let out = base.join(target).to_string_lossy().to_string(); |
67 | + // ensure the resulting URL is actually valid |
68 | + if Url::parse(&("file://".to_string() + &out)).is_ok() { |
69 | + Some(out) |
70 | + } else { |
71 | + None |
72 | + } |
73 | + } |
74 | + |
75 | + pub fn render(&self, adapter: TreeSitterAdapter, readme: &str) -> String { |
76 | let mut opts = ComrakOptions::default(); |
77 | opts.extension.description_lists = true; |
78 | opts.extension.footnotes = true; |
79 | @@ -63,7 +102,53 @@ impl Renderer<'_> { |
80 | opts.render.unsafe_ = self.allow_unsafe_markdown; |
81 | // not relevent since we paint ourselves I think |
82 | // opts.render.github_pre_lang = false; |
83 | - // |
84 | + |
85 | + // re-write any local urls or media links to point to the correct paths |
86 | + let arena = Arena::new(); |
87 | + |
88 | + let root = parse_document(&arena, readme, &opts.clone()); |
89 | + |
90 | + fn iter_nodes<'a, F>(node: &'a AstNode<'a>, f: &F) |
91 | + where |
92 | + F: Fn(&'a AstNode<'a>), |
93 | + { |
94 | + f(node); |
95 | + for c in node.children() { |
96 | + iter_nodes(c, f); |
97 | + } |
98 | + } |
99 | + |
100 | + iter_nodes(root, &|node| match &mut node.data.borrow_mut().value { |
101 | + NodeValue::Link(ref mut link) => { |
102 | + if let Some(rewrite) = Renderer::rewrite_url( |
103 | + self.collection, |
104 | + self.name, |
105 | + self.refname, |
106 | + "blob", |
107 | + link.url.as_ref(), |
108 | + ) { |
109 | + *link = NodeLink { |
110 | + title: link.title.clone(), |
111 | + url: rewrite, |
112 | + }; |
113 | + } |
114 | + } |
115 | + NodeValue::Image(ref mut link) => { |
116 | + if let Some(rewrite) = Renderer::rewrite_url( |
117 | + self.collection, |
118 | + self.name, |
119 | + self.refname, |
120 | + "raw", |
121 | + link.url.as_ref(), |
122 | + ) { |
123 | + *link = NodeLink { |
124 | + title: link.title.clone(), |
125 | + url: rewrite, |
126 | + }; |
127 | + } |
128 | + } |
129 | + _ => (), |
130 | + }); |
131 | |
132 | let anchor_symbol = self.anchor_symbol; |
133 | let heading_writer = &HeadingWriter(anchor_symbol); |
134 | @@ -71,7 +156,9 @@ impl Renderer<'_> { |
135 | let mut plugins = ComrakPlugins::default(); |
136 | plugins.render.codefence_syntax_highlighter = Some(&adapter); |
137 | plugins.render.heading_adapter = Some(heading_writer); |
138 | - markdown_to_html_with_plugins(readme, &opts, &plugins) |
139 | + let mut html = vec![]; |
140 | + format_html_with_plugins(root, &opts, &mut html, &plugins).unwrap(); |
141 | + String::from_utf8(html).unwrap() |
142 | } |
143 | } |
144 | |
145 | @@ -89,6 +176,7 @@ mod tests { |
146 | origin: "https://ayllu-forge.org", |
147 | collection: "projects", |
148 | name: "ayllu", |
149 | + refname: "", |
150 | anchor_symbol: "", |
151 | allow_unsafe_markdown: true, |
152 | }; |
153 | @@ -98,19 +186,17 @@ mod tests { |
154 | |
155 | let result = renderer.render(adapter, "# Hello World"); |
156 | assert!(result |
157 | - .contains("<h1 id=hello-world dir=auto><a class=\"anchor\" href=\"#hello-world\">")); |
158 | + .contains("<header><a class=\"icon-link\" href=\"#hello-world\"></a><h1 id=hello-world dir=auto>Hello World</h1></header>")); |
159 | } |
160 | |
161 | #[test] |
162 | fn rewrite_links() { |
163 | - |
164 | - // TODO |
165 | - |
166 | - /* |
167 | let renderer = Renderer { |
168 | origin: "https://ayllu-forge.org", |
169 | collection: "projects", |
170 | name: "ayllu", |
171 | + refname: "main", |
172 | + anchor_symbol: "", |
173 | allow_unsafe_markdown: true, |
174 | }; |
175 | |
176 | @@ -120,30 +206,30 @@ mod tests { |
177 | let result = renderer.render( |
178 | adapter, |
179 | r#" |
180 | - # Ayllu |
181 | + # Ayllu |
182 | |
183 | - ## Hyper Performant Hackable Code Forge Built on Open Standards |
184 | + ## Hyper Performant Hackable Code Forge Built on Open Standards |
185 | |
186 | - [fuu]("/fuu") |
187 | - [bar](./bar) |
188 | - [baz]("./aaaa/baz/") |
189 | - [qux]("./bbbb/cccc/qux/") |
190 | + [fuu](/fuu) |
191 | + [bar](./bar) |
192 | +  |
193 | +  |
194 | |
195 | - ### Fascinating Content! |
196 | + ### Fascinating Content! |
197 | |
198 | - ddddeeeeffffggg |
199 | + ddddeeeeffffggg |
200 | |
201 | - 111111111111111111111111111111111111111111111 |
202 | + 111111111111111111111111111111111111111111111 |
203 | |
204 | "#, |
205 | ); |
206 | |
207 | - |
208 | - assert!(result.contains("https://ayllu-forge.org/projects/ayllu/render/fuu")); |
209 | - assert!(result.contains("https://ayllu-forge.org/projects/ayllu/render/bar")); |
210 | - assert!(result.contains("https://ayllu-forge.org/projects/ayllu/render/aaaa/baz")); |
211 | - assert!(result.contains("https://ayllu-forge.org/projects/ayllu/render/bbbb/cccc/qux")); |
212 | - |
213 | - */ |
214 | + assert!(result.contains("<a href=\"/projects/ayllu/blob/main/fuu\">fuu</a>")); |
215 | + assert!(result.contains("<a href=\"/projects/ayllu/blob/main/./bar\">bar</a>")); |
216 | + assert!(result.contains( |
217 | + "<img src=\"/projects/ayllu/raw/main/./aaaa/baz/\" alt=\"baz\" title=\"hmmm\" />" |
218 | + )); |
219 | + assert!(result |
220 | + .contains("<img src=\"/projects/ayllu/raw/main/./bbbb/cccc/qux/\" alt=\"qux\" />")); |
221 | } |
222 | } |
223 | diff --git a/ayllu/src/web2/routes/blob.rs b/ayllu/src/web2/routes/blob.rs |
224 | index d1d3bde..6fdb5f5 100644 |
225 | --- a/ayllu/src/web2/routes/blob.rs |
226 | +++ b/ayllu/src/web2/routes/blob.rs |
227 | @@ -72,6 +72,7 @@ pub async fn serve( |
228 | origin: &cfg.origin, |
229 | collection: &preamble.collection_name, |
230 | name: &preamble.repo_name, |
231 | + refname: &preamble.refname, |
232 | anchor_symbol: &anchor_symbol, |
233 | allow_unsafe_markdown: cfg.web.unsafe_markdown, |
234 | }; |
235 | diff --git a/ayllu/src/web2/routes/repo.rs b/ayllu/src/web2/routes/repo.rs |
236 | index 1c84028..2bfc464 100644 |
237 | --- a/ayllu/src/web2/routes/repo.rs |
238 | +++ b/ayllu/src/web2/routes/repo.rs |
239 | @@ -233,6 +233,7 @@ pub async fn serve( |
240 | origin: &cfg.origin, |
241 | collection: &preamble.collection_name, |
242 | name: &preamble.repo_name, |
243 | + refname: &preamble.refname, |
244 | anchor_symbol: &anchor_symbol, |
245 | allow_unsafe_markdown: cfg.web.unsafe_markdown, |
246 | }; |