aboutsummaryrefslogtreecommitdiff
path: root/src/render.rs
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2026-04-30 15:17:30 +0200
committerRuben Beltran del Rio <jj@r.bdr.sh>2026-04-30 15:37:23 +0200
commit8cc11c52a88ec78d175b3ec8de854916f631caab (patch)
tree4ad0bc1664a97418d0475b0b730c54d874953348 /src/render.rs
Initial build
Diffstat (limited to 'src/render.rs')
-rw-r--r--src/render.rs102
1 files changed, 102 insertions, 0 deletions
diff --git a/src/render.rs b/src/render.rs
new file mode 100644
index 0000000..8e366dd
--- /dev/null
+++ b/src/render.rs
@@ -0,0 +1,102 @@
+use std::path::Path;
+
+use syntect::highlighting::ThemeSet;
+use syntect::html::highlighted_html_for_string;
+use syntect::parsing::SyntaxSet;
+
+const THEME: &str = "base16-ocean.dark";
+const STYLE: &str = "html,body{margin:0;background:#2b303b}\
+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<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");
+ out.push_str(&body);
+ out.push('\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"));
+ }
+}