repos
/ repos-rust master

repos-rust

mirror archived upstream

A minimal self-hosted git browser on Rust axum: bare repos rendered as a website with commits, diffs, syntax-highlighted blobs, atom feeds, and clone over HTTPS.

axumdockergitgit-browsergitoxidegixrustself-hosted

2.6 KB · 72 lines · Rust Raw History
 1//! Markdown rendering for READMEs. pulldown-cmark with tables, footnotes,
 2//! strikethrough, and task-list extensions enabled (the subset Github lets
 3//! you use in a README), then run through ammonia so any inline <script>
 4//! or other dangerous HTML in the markdown source is stripped.
 5
 6use pulldown_cmark::{CowStr, Event, Options, Parser, Tag};
 7
 8/// Render a README's markdown to sanitized HTML. Relative link targets are
 9/// prefixed with `link_base` (typically `/<name>/blob/<branch>`) and relative
10/// image targets with `image_base` (typically `/<name>/raw/<branch>`) so they
11/// resolve against the repo instead of the page's URL.
12pub fn render(input: &str, link_base: &str, image_base: &str) -> String {
13    let mut opts = Options::empty();
14    opts.insert(Options::ENABLE_TABLES);
15    opts.insert(Options::ENABLE_FOOTNOTES);
16    opts.insert(Options::ENABLE_STRIKETHROUGH);
17    opts.insert(Options::ENABLE_TASKLISTS);
18    opts.insert(Options::ENABLE_SMART_PUNCTUATION);
19    let parser = Parser::new_ext(input, opts).map(|event| match event {
20        Event::Start(Tag::Link { link_type, dest_url, title, id }) => Event::Start(Tag::Link {
21            link_type,
22            dest_url: rewrite(&dest_url, link_base),
23            title,
24            id,
25        }),
26        Event::Start(Tag::Image { link_type, dest_url, title, id }) => Event::Start(Tag::Image {
27            link_type,
28            dest_url: rewrite(&dest_url, image_base),
29            title,
30            id,
31        }),
32        other => other,
33    });
34    let mut html = String::with_capacity(input.len());
35    pulldown_cmark::html::push_html(&mut html, parser);
36    ammonia::clean(&html)
37}
38
39/// Prefix a URL with `base` if it's a relative path (i.e. not absolute,
40/// protocol-relative, scheme-bearing, or a bare fragment).
41fn rewrite<'a>(url: &CowStr<'a>, base: &str) -> CowStr<'a> {
42    if url.is_empty()
43        || url.starts_with('/')
44        || url.starts_with('#')
45        || url.starts_with("//")
46        || has_scheme(url)
47    {
48        return url.clone();
49    }
50    let mut path: &str = url;
51    while let Some(rest) = path.strip_prefix("./") {
52        path = rest;
53    }
54    CowStr::from(format!("{}/{}", base.trim_end_matches('/'), path))
55}
56
57/// True if `url` begins with an RFC 3986 scheme (`http:`, `mailto:`, etc.).
58fn has_scheme(url: &str) -> bool {
59    let bytes = url.as_bytes();
60    if bytes.first().is_none_or(|b| !b.is_ascii_alphabetic()) {
61        return false;
62    }
63    for (i, &b) in bytes.iter().enumerate().skip(1) {
64        match b {
65            b':' => return i > 0,
66            b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'+' | b'.' | b'-' => continue,
67            _ => return false,
68        }
69    }
70    false
71}