dioxus_mdx/parser/
syntax.rs1use std::sync::LazyLock;
8use syntect::html::{ClassStyle, ClassedHTMLGenerator};
9use syntect::parsing::SyntaxSet;
10use syntect::util::LinesWithEndings;
11
12static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
14
15pub fn highlight_code(code: &str, language: Option<&str>) -> String {
22 let lang = language.unwrap_or("txt");
23
24 let syntax_name = map_language(lang);
26
27 let syntax = SYNTAX_SET
29 .find_syntax_by_extension(syntax_name)
30 .or_else(|| SYNTAX_SET.find_syntax_by_name(syntax_name))
31 .or_else(|| SYNTAX_SET.find_syntax_by_extension(lang))
32 .or_else(|| SYNTAX_SET.find_syntax_by_name(lang))
33 .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
34
35 let mut generator = ClassedHTMLGenerator::new_with_class_style(
39 syntax,
40 &SYNTAX_SET,
41 ClassStyle::SpacedPrefixed { prefix: "sy-" },
42 );
43
44 for line in LinesWithEndings::from(code) {
45 if generator
46 .parse_html_for_line_which_includes_newline(line)
47 .is_err()
48 {
49 return escape_html(code);
50 }
51 }
52
53 generator.finalize()
54}
55
56pub fn syntax_highlight_css() -> &'static str {
64 include_str!("syntax_highlight.css")
65}
66
67fn map_language(lang: &str) -> &str {
70 if lang.eq_ignore_ascii_case("js") || lang.eq_ignore_ascii_case("javascript") {
73 return "JavaScript";
74 }
75 if lang.eq_ignore_ascii_case("jsx") {
76 return "JavaScript (JSX)";
77 }
78 if lang.eq_ignore_ascii_case("ts") || lang.eq_ignore_ascii_case("typescript") {
79 return "TypeScript";
80 }
81 if lang.eq_ignore_ascii_case("tsx") {
82 return "TypeScript (TSX)";
83 }
84
85 if lang.eq_ignore_ascii_case("sh")
87 || lang.eq_ignore_ascii_case("bash")
88 || lang.eq_ignore_ascii_case("shell")
89 || lang.eq_ignore_ascii_case("zsh")
90 {
91 return "Bash";
92 }
93
94 if lang.eq_ignore_ascii_case("rs") || lang.eq_ignore_ascii_case("rust") {
96 return "Rust";
97 }
98
99 if lang.eq_ignore_ascii_case("py") || lang.eq_ignore_ascii_case("python") {
101 return "Python";
102 }
103
104 if lang.eq_ignore_ascii_case("rb") || lang.eq_ignore_ascii_case("ruby") {
106 return "Ruby";
107 }
108
109 if lang.eq_ignore_ascii_case("go") || lang.eq_ignore_ascii_case("golang") {
111 return "Go";
112 }
113
114 if lang.eq_ignore_ascii_case("json") || lang.eq_ignore_ascii_case("jsonc") {
116 return "JSON";
117 }
118
119 if lang.eq_ignore_ascii_case("yml") || lang.eq_ignore_ascii_case("yaml") {
121 return "YAML";
122 }
123
124 if lang.eq_ignore_ascii_case("html") || lang.eq_ignore_ascii_case("htm") {
126 return "HTML";
127 }
128 if lang.eq_ignore_ascii_case("css") {
129 return "CSS";
130 }
131 if lang.eq_ignore_ascii_case("scss") {
132 return "SCSS";
133 }
134 if lang.eq_ignore_ascii_case("sass") {
135 return "Sass";
136 }
137
138 if lang.eq_ignore_ascii_case("toml") {
140 return "TOML";
141 }
142 if lang.eq_ignore_ascii_case("ini") {
143 return "INI";
144 }
145 if lang.eq_ignore_ascii_case("env") {
146 return "Bourne Again Shell (bash)";
147 }
148
149 if lang.eq_ignore_ascii_case("md") || lang.eq_ignore_ascii_case("markdown") {
151 return "Markdown";
152 }
153
154 if lang.eq_ignore_ascii_case("sql") {
156 return "SQL";
157 }
158
159 if lang.eq_ignore_ascii_case("c") || lang.eq_ignore_ascii_case("h") {
161 return "C";
162 }
163 if lang.eq_ignore_ascii_case("cpp")
164 || lang.eq_ignore_ascii_case("cc")
165 || lang.eq_ignore_ascii_case("cxx")
166 || lang.eq_ignore_ascii_case("hpp")
167 {
168 return "C++";
169 }
170
171 if lang.eq_ignore_ascii_case("java") {
173 return "Java";
174 }
175
176 if lang.eq_ignore_ascii_case("cs") || lang.eq_ignore_ascii_case("csharp") {
178 return "C#";
179 }
180
181 if lang.eq_ignore_ascii_case("php") {
183 return "PHP";
184 }
185
186 if lang.eq_ignore_ascii_case("swift") {
188 return "Swift";
189 }
190
191 if lang.eq_ignore_ascii_case("kt") || lang.eq_ignore_ascii_case("kotlin") {
193 return "Kotlin";
194 }
195
196 if lang.eq_ignore_ascii_case("dockerfile") || lang.eq_ignore_ascii_case("docker") {
198 return "Dockerfile";
199 }
200
201 if lang.eq_ignore_ascii_case("txt") || lang.eq_ignore_ascii_case("text") {
203 return "Plain Text";
204 }
205
206 lang
208}
209
210fn escape_html(text: &str) -> String {
212 text.replace('&', "&")
213 .replace('<', "<")
214 .replace('>', ">")
215 .replace('"', """)
216 .replace('\'', "'")
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_highlight_rust() {
225 let code = r#"fn main() {
226 println!("Hello, world!");
227}"#;
228 let html = highlight_code(code, Some("rust"));
229 assert!(html.contains("<span"));
231 assert!(html.contains("sy-"));
232 assert!(html.contains("fn"));
233 }
234
235 #[test]
236 fn test_highlight_javascript() {
237 let code = "const x = 42;";
238 let html = highlight_code(code, Some("js"));
239 assert!(html.contains("<span"));
240 }
241
242 #[test]
243 fn test_highlight_no_inline_styles() {
244 let code = "let x = 42;";
245 let html = highlight_code(code, Some("rust"));
246 assert!(!html.contains("style="));
248 }
249
250 #[test]
251 fn test_highlight_unknown_language() {
252 let code = "some text";
253 let html = highlight_code(code, Some("unknown_lang_xyz"));
254 assert!(!html.is_empty());
256 }
257
258 #[test]
259 fn test_highlight_no_language() {
260 let code = "plain text";
261 let html = highlight_code(code, None);
262 assert!(!html.is_empty());
263 }
264
265 #[test]
266 fn test_escape_html() {
267 assert_eq!(escape_html("<div>"), "<div>");
268 assert_eq!(escape_html("a & b"), "a & b");
269 }
270}