Commit

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)

rewrite relative urls so they resolve the correct paths
rewrite relative urls so they resolve the correct paths

This makes it so that relative links and media assets will resolve to the
correct URL on the server.

For example:

[fuu](/fuu)
https://ayllu-forge.org/projects/ayllu/blob/main/fuu
![bar](/baz.png)
https://ayllu-forge.org/projects/ayllu/raw/master/baz.png
1diff --git a/ayllu/src/readme.rs b/ayllu/src/readme.rs
2index 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+ ![baz](./aaaa/baz/ "hmmm")
193+ ![qux](./bbbb/cccc/qux/)
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
224index 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
236index 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 };