1use syntect::easy::HighlightLines;
6use syntect::highlighting::{Style, ThemeSet};
7use syntect::parsing::SyntaxSet;
8
9#[derive(Debug, Clone)]
11pub struct HighlightedSegment {
12 pub text: String,
13 pub color: String, pub bold: bool,
15 pub italic: bool,
16}
17
18pub fn highlight_code(code: &str, language: &str) -> Vec<Vec<HighlightedSegment>> {
20 let ps = SyntaxSet::load_defaults_newlines();
21 let ts = ThemeSet::load_defaults();
22
23 let syntax_name = match language.to_lowercase().as_str() {
25 "rust" | "rs" => "Rust",
26 "python" | "py" => "Python",
27 "javascript" | "js" => "JavaScript",
28 "typescript" | "ts" => "TypeScript",
29 "java" => "Java",
30 "c" => "C",
31 "cpp" | "c++" => "C++",
32 "csharp" | "c#" | "cs" => "C#",
33 "go" => "Go",
34 "ruby" | "rb" => "Ruby",
35 "php" => "PHP",
36 "swift" => "Swift",
37 "kotlin" | "kt" => "Kotlin",
38 "scala" => "Scala",
39 "html" => "HTML",
40 "css" => "CSS",
41 "json" => "JSON",
42 "yaml" | "yml" => "YAML",
43 "xml" => "XML",
44 "sql" => "SQL",
45 "bash" | "sh" | "shell" => "Bourne Again Shell (bash)",
46 "powershell" | "ps1" => "PowerShell",
47 "markdown" | "md" => "Markdown",
48 "toml" => "TOML",
49 _ => "Plain Text",
50 };
51
52 let syntax = ps
53 .find_syntax_by_name(syntax_name)
54 .or_else(|| ps.find_syntax_by_extension(language))
55 .unwrap_or_else(|| ps.find_syntax_plain_text());
56
57 let theme = &ts.themes["Solarized (dark)"];
59 let mut highlighter = HighlightLines::new(syntax, theme);
60
61 let mut lines = Vec::new();
62
63 for line in code.lines() {
64 let ranges = highlighter.highlight_line(line, &ps).unwrap_or_default();
65 let segments: Vec<HighlightedSegment> = ranges
66 .iter()
67 .map(|(style, text)| HighlightedSegment {
68 text: text.to_string(),
69 color: style_to_hex(style),
70 bold: style
71 .font_style
72 .contains(syntect::highlighting::FontStyle::BOLD),
73 italic: style
74 .font_style
75 .contains(syntect::highlighting::FontStyle::ITALIC),
76 })
77 .collect();
78 lines.push(segments);
79 }
80
81 lines
82}
83
84fn style_to_hex(style: &Style) -> String {
86 format!(
87 "{:02X}{:02X}{:02X}",
88 style.foreground.r, style.foreground.g, style.foreground.b
89 )
90}
91
92pub fn generate_highlighted_code_xml(code: &str, language: &str) -> String {
94 let highlighted = highlight_code(code, language);
95 let mut xml = String::new();
96
97 for line_segments in highlighted {
98 xml.push_str("<a:p><a:pPr algn=\"l\"/>");
99
100 if line_segments.is_empty() {
101 xml.push_str(r#"<a:r><a:rPr lang="en-US" sz="1400" dirty="0"><a:solidFill><a:srgbClr val="839496"/></a:solidFill><a:latin typeface="Consolas"/></a:rPr><a:t> </a:t></a:r>"#);
103 } else {
104 for segment in line_segments {
105 let bold = if segment.bold { r#" b="1""# } else { "" };
106 let italic = if segment.italic { r#" i="1""# } else { "" };
107 let text = escape_xml(&segment.text);
108
109 xml.push_str(&format!(
111 r#"<a:r><a:rPr lang="en-US" sz="1400" dirty="0"{}{}><a:solidFill><a:srgbClr val="{}"/></a:solidFill><a:latin typeface="Consolas"/></a:rPr><a:t>{}</a:t></a:r>"#,
112 bold, italic, segment.color, text
113 ));
114 }
115 }
116
117 xml.push_str("</a:p>");
118 }
119
120 xml
121}
122
123use crate::core::escape_xml;
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_highlight_rust() {
131 let code = "fn main() {\n println!(\"Hello\");\n}";
132 let highlighted = highlight_code(code, "rust");
133 assert_eq!(highlighted.len(), 3);
134 assert!(!highlighted[0].is_empty());
135 }
136
137 #[test]
138 fn test_highlight_python() {
139 let code = "def hello():\n print('Hello')";
140 let highlighted = highlight_code(code, "python");
141 assert_eq!(highlighted.len(), 2);
142 }
143
144 #[test]
145 fn test_highlight_unknown() {
146 let code = "some text";
147 let highlighted = highlight_code(code, "unknown");
148 assert_eq!(highlighted.len(), 1);
149 }
150
151 #[test]
152 fn test_generate_xml() {
153 let xml = generate_highlighted_code_xml("let x = 1;", "rust");
154 assert!(xml.contains("<a:p>"));
155 assert!(xml.contains("Consolas"));
156 }
157
158 #[test]
159 fn test_highlight_colors_not_black() {
160 let code = "fn main() {\n println!(\"Hello\");\n}";
161 let highlighted = highlight_code(code, "rust");
162
163 let mut has_non_black = false;
165 for line in &highlighted {
166 for segment in line {
167 if segment.color != "000000" && !segment.text.trim().is_empty() {
169 has_non_black = true;
170 }
171 }
172 }
173 assert!(
174 has_non_black,
175 "Syntax highlighting should produce non-black colors"
176 );
177 }
178
179 #[test]
180 fn test_xml_has_solarized_colors() {
181 let xml = generate_highlighted_code_xml("fn main() {}", "rust");
182 let has_color = xml.contains("859900")
185 || xml.contains("268BD2")
186 || xml.contains("2AA198")
187 || xml.contains("B58900")
188 || xml.contains("CB4B16")
189 || xml.contains("DC322F")
190 || xml.contains("D33682")
191 || xml.contains("6C71C4")
192 || xml.contains("839496")
193 || xml.contains("93A1A1");
194 assert!(
195 has_color,
196 "XML should contain Solarized theme colors, got: {}",
197 xml
198 );
199 }
200}