aboutsummaryrefslogtreecommitdiff
path: root/src/render.rs
blob: 24922a44f99d3e8a36b820a287ddaaa733d0a3c5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
use std::path::Path;

use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;

const THEME: &str = "InspiredGitHub";
const STYLE: &str = "html,body{margin:0;background:#fff}\
pre{margin:0;padding:1rem;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:auto}";

pub fn page(path: &Path, text: &str) -> String {
    let title = path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("paste");

    let body = highlight(path, text);

    let mut out = String::with_capacity(body.len() + 256);
    out.push_str("<!doctype html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>");
    push_escaped(&mut out, title);
    out.push_str("</title>\n<style>");
    out.push_str(STYLE);
    out.push_str("</style>\n</head>\n<body>\n");
    out.push_str(&body);
    out.push_str("\n</body>\n</html>\n");
    out
}

fn highlight(path: &Path, text: &str) -> String {
    let syntaxes = SyntaxSet::load_defaults_newlines();
    let themes = ThemeSet::load_defaults();

    let syntax = path
        .extension()
        .and_then(|e| e.to_str())
        .and_then(|e| syntaxes.find_syntax_by_extension(e))
        .or_else(|| syntaxes.find_syntax_by_first_line(text))
        .unwrap_or_else(|| syntaxes.find_syntax_plain_text());

    let Some(theme) = themes.themes.get(THEME) else {
        return fallback_pre(text);
    };

    match highlighted_html_for_string(text, &syntaxes, syntax, theme) {
        Ok(html) => html,
        Err(_) => fallback_pre(text),
    }
}

fn fallback_pre(text: &str) -> String {
    let mut out = String::with_capacity(text.len() + 16);
    out.push_str("<pre>");
    push_escaped(&mut out, text);
    out.push_str("</pre>");
    out
}

fn push_escaped(out: &mut String, s: &str) {
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&#39;"),
            other => out.push(other),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn page_contains_filename_in_title() {
        let html = page(Path::new("/srv/x.rs"), "fn main() {}\n");
        assert!(html.contains("<title>x.rs</title>"));
    }

    #[test]
    fn page_escapes_filename() {
        let html = page(Path::new("/srv/<script>.txt"), "hi");
        assert!(html.contains("&lt;script&gt;.txt"));
        assert!(!html.contains("<title><script>"));
    }

    #[test]
    fn page_emits_pre_block() {
        let html = page(Path::new("/srv/x.txt"), "hello");
        assert!(html.contains("<pre"));
        assert!(html.contains("hello"));
    }

    #[test]
    fn page_highlights_known_extension() {
        // Rust syntax should produce styled spans, not just a plain <pre>hello</pre>.
        let html = page(Path::new("/srv/x.rs"), "fn main() {}\n");
        assert!(html.contains("<span"));
    }
}