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
103
104
105
|
use syntect::highlighting::ThemeSet;
use syntect::html::{css_for_theme_with_class_style, ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "hl-" };
const MAX_HIGHLIGHT_BYTES: usize = 512 * 1024;
pub struct Highlighter {
syntaxes: SyntaxSet,
pub css: String,
}
impl Highlighter {
pub fn new() -> Highlighter {
let syntaxes = SyntaxSet::load_defaults_newlines();
let themes = ThemeSet::load_defaults();
let light =
css_for_theme_with_class_style(&themes.themes["InspiredGitHub"], CLASS_STYLE).unwrap_or_default();
let dark = css_for_theme_with_class_style(&themes.themes["base16-ocean.dark"], CLASS_STYLE)
.unwrap_or_default();
let css = format!("{light}\n@media (prefers-color-scheme: dark) {{\n{dark}\n}}\n");
Highlighter { syntaxes, css }
}
pub fn highlight(&self, file_name: &str, code: &str) -> String {
let ext = file_name.rsplit('.').next().unwrap_or("");
let syntax = self
.syntaxes
.find_syntax_by_extension(ext)
.or_else(|| self.syntaxes.find_syntax_by_extension(file_name))
.or_else(|| self.syntaxes.find_syntax_by_first_line(code.lines().next().unwrap_or("")));
let syntax = match syntax {
Some(s) if code.len() <= MAX_HIGHLIGHT_BYTES => s,
_ => return escape(code),
};
let mut gen = ClassedHTMLGenerator::new_with_class_style(syntax, &self.syntaxes, CLASS_STYLE);
for line in LinesWithEndings::from(code) {
if gen.parse_html_for_line_which_includes_newline(line).is_err() {
return escape(code);
}
}
gen.finalize()
}
}
impl Default for Highlighter {
fn default() -> Self {
Highlighter::new()
}
}
pub fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escapes() {
assert_eq!(escape("<a href=\"x\">&'"), "<a href="x">&'");
}
#[test]
fn highlights_rust_and_escapes() {
let h = Highlighter::new();
let out = h.highlight("main.rs", "fn main() { let s = \"<b>\"; }\n");
assert!(out.contains("hl-"), "expected class spans: {out}");
assert!(!out.contains("<b>"));
assert!(out.contains("<b>"));
assert!(h.css.contains("prefers-color-scheme: dark"));
}
#[test]
fn unknown_type_is_plain_escaped() {
let h = Highlighter::new();
assert_eq!(h.highlight("data.zzzunknown", "<x>"), "<x>");
}
}
|