iced_markdown_editor/
syntect.rs1use crate::highlight::{CodeHighlighter, Color, HighlightRequest, HighlightSpan};
14use std::collections::BTreeSet;
15
16use ::syntect::easy::HighlightLines;
17use ::syntect::util::LinesWithEndings;
18
19pub use ::syntect::highlighting::{Theme, ThemeSet};
22pub use ::syntect::parsing::{SyntaxReference, SyntaxSet};
23
24pub const DEFAULT_LIGHT_THEME: &str = "InspiredGitHub";
26pub const DEFAULT_DARK_THEME: &str = "base16-ocean.dark";
28
29const ALIASES: &[(&str, &str)] = &[
32 ("ts", "js"),
33 ("tsx", "js"),
34 ("typescript", "js"),
35 ("jsx", "js"),
36 ("jsonc", "json"),
37 ("json5", "json"),
38 ("shell", "sh"),
39 ("console", "sh"),
40 ("zsh", "sh"),
41 ("powershell", "sh"),
42 ("ps1", "sh"),
43 ("csharp", "cs"),
44 ("c#", "cs"),
45 ("c++", "cpp"),
46 ("golang", "go"),
47 ("py3", "py"),
48 ("python3", "py"),
49 ("yml", "yaml"),
50 ("objc", "m"),
51 ("html5", "html"),
52 ("svg", "xml"),
53];
54
55pub struct SyntectHighlighter {
57 syntaxes: SyntaxSet,
58 light: Theme,
59 dark: Theme,
60}
61
62impl Default for SyntectHighlighter {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl SyntectHighlighter {
69 pub fn new() -> Self {
71 Self::with_themes(DEFAULT_LIGHT_THEME, DEFAULT_DARK_THEME)
72 }
73
74 pub fn with_themes(light: &str, dark: &str) -> Self {
77 let themes = ThemeSet::load_defaults();
78 let pick = |name: &str, fallback: &str| {
79 themes
80 .themes
81 .get(name)
82 .or_else(|| themes.themes.get(fallback))
83 .cloned()
84 .unwrap_or_default()
85 };
86 Self {
87 syntaxes: SyntaxSet::load_defaults_newlines(),
88 light: pick(light, DEFAULT_LIGHT_THEME),
89 dark: pick(dark, DEFAULT_DARK_THEME),
90 }
91 }
92
93 pub fn from_parts(syntaxes: SyntaxSet, light: Theme, dark: Theme) -> Self {
96 Self {
97 syntaxes,
98 light,
99 dark,
100 }
101 }
102
103 pub fn theme_names() -> Vec<String> {
105 ThemeSet::load_defaults().themes.keys().cloned().collect()
106 }
107
108 fn find(&self, language: &str) -> Option<&SyntaxReference> {
109 let language = language.trim().to_lowercase();
110 let token = ALIASES
111 .iter()
112 .find(|(alias, _)| *alias == language)
113 .map(|(_, token)| *token)
114 .unwrap_or(language.as_str());
115
116 self.syntaxes
117 .find_syntax_by_token(token)
118 .or_else(|| self.syntaxes.find_syntax_by_extension(token))
119 .filter(|syntax| syntax.name != "Plain Text")
120 }
121}
122
123fn to_color(color: ::syntect::highlighting::Color) -> Color {
124 Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0)
125}
126
127impl CodeHighlighter for SyntectHighlighter {
128 fn highlight(&self, request: &HighlightRequest<'_>) -> Vec<HighlightSpan> {
129 let Some(syntax) = self.find(request.language) else {
130 return Vec::new();
131 };
132 let theme = if request.dark {
133 &self.dark
134 } else {
135 &self.light
136 };
137 let mut lines = HighlightLines::new(syntax, theme);
138 let mut spans = Vec::new();
139 let mut offset = 0usize;
140
141 for line in LinesWithEndings::from(request.code) {
142 let Ok(ranges) = lines.highlight_line(line, &self.syntaxes) else {
143 break;
144 };
145 for (style, piece) in ranges {
146 let len = piece.chars().count();
147 spans.push(HighlightSpan {
148 range: offset..offset + len,
149 color: to_color(style.foreground),
150 });
151 offset += len;
152 }
153 }
154
155 spans
156 }
157
158 fn languages(&self) -> Vec<String> {
159 let mut names = BTreeSet::new();
160 for syntax in self.syntaxes.syntaxes() {
161 if syntax.hidden || syntax.name == "Plain Text" {
162 continue;
163 }
164 let name = syntax.name.to_lowercase();
165 let short = if name.contains(' ') || name.contains('(') {
166 match syntax.file_extensions.first() {
167 Some(extension) => extension.to_lowercase(),
168 None => continue,
169 }
170 } else {
171 name
172 };
173 names.insert(short);
174 }
175 names.into_iter().collect()
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn highlights_rust_and_ignores_unknown_languages() {
185 let highlighter = SyntectHighlighter::new();
186 let request = |language| HighlightRequest {
187 language,
188 code: "fn main() {}\n",
189 background: Color::WHITE,
190 dark: false,
191 };
192 assert!(!highlighter.highlight(&request("rust")).is_empty());
193 assert!(!highlighter.highlight(&request("rs")).is_empty());
194 assert!(
195 highlighter
196 .highlight(&request("no-such-language"))
197 .is_empty()
198 );
199 assert!(highlighter.languages().contains(&"rust".to_string()));
200 }
201}