longcipher_leptos_components/components/editor/
syntax.rs1#[cfg(feature = "syntax-highlighting")]
6use syntect::highlighting::ThemeSet;
7#[cfg(feature = "syntax-highlighting")]
8use syntect::parsing::SyntaxSet;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Language {
13 Rust,
15 JavaScript,
17 TypeScript,
19 Python,
21 Html,
23 Css,
25 Json,
27 Yaml,
29 Toml,
31 Markdown,
33 Sql,
35 Shell,
37 Go,
39 C,
41 Cpp,
43 Java,
45 PlainText,
47}
48
49impl Language {
50 #[must_use]
52 pub fn from_extension(ext: &str) -> Self {
53 match ext.to_lowercase().as_str() {
54 "rs" => Self::Rust,
55 "js" | "mjs" | "cjs" => Self::JavaScript,
56 "ts" | "mts" | "cts" | "tsx" => Self::TypeScript,
57 "py" | "pyi" => Self::Python,
58 "html" | "htm" => Self::Html,
59 "css" | "scss" | "sass" | "less" => Self::Css,
60 "json" => Self::Json,
61 "yaml" | "yml" => Self::Yaml,
62 "toml" => Self::Toml,
63 "md" | "markdown" => Self::Markdown,
64 "sql" => Self::Sql,
65 "sh" | "bash" | "zsh" | "fish" => Self::Shell,
66 "go" => Self::Go,
67 "c" | "h" => Self::C,
68 "cpp" | "cxx" | "cc" | "hpp" | "hxx" => Self::Cpp,
69 "java" => Self::Java,
70 _ => Self::PlainText,
71 }
72 }
73
74 #[must_use]
76 pub fn syntax_name(&self) -> &'static str {
77 match self {
78 Self::Rust => "Rust",
79 Self::JavaScript => "JavaScript",
80 Self::TypeScript => "TypeScript",
81 Self::Python => "Python",
82 Self::Html => "HTML",
83 Self::Css => "CSS",
84 Self::Json => "JSON",
85 Self::Yaml => "YAML",
86 Self::Toml => "TOML",
87 Self::Markdown => "Markdown",
88 Self::Sql => "SQL",
89 Self::Shell => "Bourne Again Shell (bash)",
90 Self::Go => "Go",
91 Self::C => "C",
92 Self::Cpp => "C++",
93 Self::Java => "Java",
94 Self::PlainText => "Plain Text",
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct SyntaxConfig {
102 pub language: Language,
104 pub is_dark: bool,
106 pub enabled: bool,
108}
109
110impl Default for SyntaxConfig {
111 fn default() -> Self {
112 Self {
113 language: Language::PlainText,
114 is_dark: true,
115 enabled: true,
116 }
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct HighlightedLine {
123 pub spans: Vec<HighlightedSpan>,
125}
126
127#[derive(Debug, Clone)]
129pub struct HighlightedSpan {
130 pub text: String,
132 pub color: String,
134 pub font_weight: String,
136 pub font_style: String,
138}
139
140impl HighlightedSpan {
141 #[must_use]
143 pub fn plain(text: impl Into<String>) -> Self {
144 Self {
145 text: text.into(),
146 color: "inherit".to_string(),
147 font_weight: "normal".to_string(),
148 font_style: "normal".to_string(),
149 }
150 }
151
152 #[must_use]
154 pub fn style(&self) -> String {
155 format!(
156 "color: {}; font-weight: {}; font-style: {}",
157 self.color, self.font_weight, self.font_style
158 )
159 }
160}
161
162#[cfg(feature = "syntax-highlighting")]
164pub struct Highlighter {
165 syntax_set: SyntaxSet,
166 theme_set: ThemeSet,
167}
168
169#[cfg(feature = "syntax-highlighting")]
170impl Highlighter {
171 #[must_use]
173 pub fn new() -> Self {
174 Self {
175 syntax_set: SyntaxSet::load_defaults_newlines(),
176 theme_set: ThemeSet::load_defaults(),
177 }
178 }
179
180 pub fn highlight_line(&self, line: &str, language: Language, is_dark: bool) -> HighlightedLine {
182 use syntect::easy::HighlightLines;
183
184 let theme_name = if is_dark {
185 "base16-ocean.dark"
186 } else {
187 "base16-ocean.light"
188 };
189
190 let syntax = self
191 .syntax_set
192 .find_syntax_by_name(language.syntax_name())
193 .or_else(|| Some(self.syntax_set.find_syntax_plain_text()));
194
195 let theme = self.theme_set.themes.get(theme_name).unwrap_or_else(|| {
196 self.theme_set
197 .themes
198 .values()
199 .next()
200 .expect("No themes available")
201 });
202
203 let spans = if let Some(syntax) = syntax {
204 let mut highlighter = HighlightLines::new(syntax, theme);
205
206 match highlighter.highlight_line(line, &self.syntax_set) {
207 Ok(ranges) => ranges
208 .iter()
209 .map(|(style, text)| HighlightedSpan {
210 text: text.to_string(),
211 color: format!(
212 "rgb({}, {}, {})",
213 style.foreground.r, style.foreground.g, style.foreground.b
214 ),
215 font_weight: if style
216 .font_style
217 .contains(syntect::highlighting::FontStyle::BOLD)
218 {
219 "bold".to_string()
220 } else {
221 "normal".to_string()
222 },
223 font_style: if style
224 .font_style
225 .contains(syntect::highlighting::FontStyle::ITALIC)
226 {
227 "italic".to_string()
228 } else {
229 "normal".to_string()
230 },
231 })
232 .collect(),
233 Err(_) => vec![HighlightedSpan::plain(line)],
234 }
235 } else {
236 vec![HighlightedSpan::plain(line)]
237 };
238
239 HighlightedLine { spans }
240 }
241}
242
243#[cfg(feature = "syntax-highlighting")]
244impl Default for Highlighter {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_language_from_extension() {
256 assert_eq!(Language::from_extension("rs"), Language::Rust);
257 assert_eq!(Language::from_extension("js"), Language::JavaScript);
258 assert_eq!(Language::from_extension("py"), Language::Python);
259 assert_eq!(Language::from_extension("unknown"), Language::PlainText);
260 }
261
262 #[test]
263 fn test_highlighted_span_style() {
264 let span = HighlightedSpan {
265 text: "let".to_string(),
266 color: "rgb(255, 0, 0)".to_string(),
267 font_weight: "bold".to_string(),
268 font_style: "normal".to_string(),
269 };
270
271 let style = span.style();
272 assert!(style.contains("color: rgb(255, 0, 0)"));
273 assert!(style.contains("font-weight: bold"));
274 }
275}