diff options
| author | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-04-30 15:17:30 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-04-30 15:37:23 +0200 |
| commit | 8cc11c52a88ec78d175b3ec8de854916f631caab (patch) | |
| tree | 4ad0bc1664a97418d0475b0b730c54d874953348 /src/render.rs | |
Initial build
Diffstat (limited to 'src/render.rs')
| -rw-r--r-- | src/render.rs | 102 |
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("&"), + '<' => out.push_str("<"), + '>' => out.push_str(">"), + '"' => out.push_str("""), + '\'' => out.push_str("'"), + 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("<script>.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")); + } +} |