1use crate::html::{config, HtmlError};
2use crate::html::{HtmlConfig, HtmlState, HtmlWriter, HtmlWriterBase};
3use crate::{html_writer, HtmlRenderer};
4use lazy_static::lazy_static;
5use pulldown_cmark_escape::{escape_html_body_text, StrWrite};
6use serde::{Deserialize, Deserializer};
7use syntect::highlighting::{Theme, ThemeSet};
8use syntect::html::{ClassStyle, ClassedHTMLGenerator};
9use syntect::parsing::SyntaxSet;
10use syntect::util::LinesWithEndings;
11
12lazy_static! {
13 static ref SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_newlines();
14 static ref THEME_SET: ThemeSet = ThemeSet::load_defaults();
15}
16
17fn deserialize_class_style<'de, D>(deserializer: D) -> Result<ClassStyle, D::Error>
18where
19 D: Deserializer<'de>,
20{
21 #[derive(Deserialize)]
22 #[serde(rename_all = "snake_case")]
23 enum ClassStyleHelper {
24 Spaced,
25 SpacedPrefix,
26 }
27
28 let style = ClassStyleHelper::deserialize(deserializer)?;
29 Ok(match style {
30 ClassStyleHelper::Spaced => ClassStyle::Spaced,
31 ClassStyleHelper::SpacedPrefix => ClassStyle::SpacedPrefixed { prefix: "" },
32 })
33}
34
35#[derive(Clone, Debug, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub struct SyntectConfigStyle {
39 pub theme: String,
41 #[serde(
43 deserialize_with = "deserialize_class_style",
44 default = "default_class_style"
45 )]
46 pub class_style: ClassStyle,
47 #[serde(default = "default_inject_css")]
49 pub inject_css: bool,
50}
51
52fn default_class_style() -> ClassStyle {
53 ClassStyle::Spaced
54}
55
56fn default_inject_css() -> bool {
57 true
58}
59
60#[derive(Debug, Default)]
62pub struct SyntectConfig {
63 pub style: SyntectConfigStyle,
65 #[doc(hidden)]
67 pub syntax_set: Option<SyntaxSet>,
68 #[doc(hidden)]
70 pub theme_set: Option<ThemeSet>,
71}
72
73impl Default for SyntectConfigStyle {
74 fn default() -> Self {
75 Self {
76 theme: "base16-ocean.dark".to_string(),
77 class_style: ClassStyle::Spaced,
78 inject_css: true,
79 }
80 }
81}
82
83impl HtmlConfig {
84 pub fn with_syntect(syntect_config: SyntectConfig) -> Self {
86 HtmlConfig {
87 syntect: Some(syntect_config.style),
88 ..Default::default()
89 }
90 }
91}
92
93#[html_writer]
95pub struct SyntectWriter<'a, W: StrWrite>
96where
97 W: 'a,
98{
99 base: HtmlWriterBase<W>,
100 style: SyntectConfigStyle,
101 syntax_set: Option<&'a SyntaxSet>,
102 theme_set: Option<&'a ThemeSet>,
103 current_lang: Option<String>,
104}
105
106impl<'a, W: StrWrite> SyntectWriter<'a, W> {
107 pub fn new(writer: W, config: &'a config::HtmlConfig) -> Self {
108 let style = config.syntect.clone().unwrap_or_default();
109 Self {
110 base: HtmlWriterBase::new(writer, config.clone()),
111 style,
112 syntax_set: None,
113 theme_set: None,
114 current_lang: None,
115 }
116 }
117
118 pub fn with_custom_sets(
119 writer: W,
120 config: &'a config::HtmlConfig,
121 syntax_set: Option<&'a SyntaxSet>,
122 theme_set: Option<&'a ThemeSet>,
123 ) -> Self {
124 let style = config.syntect.clone().unwrap_or_default();
125 Self {
126 base: HtmlWriterBase::new(writer, config.clone()),
127 style,
128 syntax_set,
129 theme_set,
130 current_lang: None,
131 }
132 }
133
134 fn highlight_code(&self, code: &str, lang: Option<&str>) -> String {
135 let syntax_set = self.syntax_set.unwrap_or(&SYNTAX_SET);
136
137 let syntax = match lang {
138 Some(lang) => syntax_set
139 .find_syntax_by_token(lang)
140 .or_else(|| syntax_set.find_syntax_by_extension(lang)),
141 None => None,
142 }
143 .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
144
145 let mut html_generator =
146 ClassedHTMLGenerator::new_with_class_style(syntax, syntax_set, self.style.class_style);
147
148 for line in LinesWithEndings::from(code) {
149 let _ = html_generator.parse_html_for_line_which_includes_newline(line);
150 }
151
152 html_generator.finalize()
153 }
154
155 fn get_theme(&self) -> Result<&Theme, String> {
156 let theme_set = self.theme_set.unwrap_or(&THEME_SET);
157 theme_set
158 .themes
159 .get(&self.style.theme)
160 .ok_or_else(|| format!("Theme '{}' not found", self.style.theme))
161 }
162
163 pub fn get_theme_css(&self) -> Result<String, String> {
164 let theme = self.get_theme()?;
165 syntect::html::css_for_theme_with_class_style(theme, self.style.class_style)
166 .map_err(|e| e.to_string())
167 }
168}
169
170impl<'a, W: StrWrite> SyntectWriter<'a, W> {
171 fn start_code_block(&mut self, kind: pulldown_cmark::CodeBlockKind) -> Result<(), HtmlError> {
172 self.current_lang = match kind {
173 pulldown_cmark::CodeBlockKind::Fenced(ref info) => {
174 if info.is_empty() {
175 None
176 } else {
177 Some(info.to_string())
178 }
179 }
180 _ => None,
181 };
182
183 self.write_str("<pre")?;
184 self.write_attributes("pre")?;
185 self.write_str("><code")?;
186
187 if let Some(ref lang) = self.current_lang {
188 self.write_str(&format!(" class=\"language-{}\"", lang))?;
189 }
190
191 self.write_attributes("code")?;
192 self.write_str(">")?;
193
194 self.get_state().currently_in_code_block = true;
195 Ok(())
196 }
197
198 fn text(&mut self, text: &str) -> Result<(), HtmlError> {
199 if self.get_state().currently_in_code_block {
200 let highlighted = self.highlight_code(text, self.current_lang.as_deref());
201 self.write_str(&highlighted)
202 } else {
203 if self.get_config().html.escape_html {
204 escape_html_body_text(self.get_writer(), text)
205 .map_err(|_| HtmlError::Write(std::fmt::Error))?;
206 } else {
207 self.write_str(text)?;
208 }
209 Ok(())
210 }
211 }
212
213 fn end_code_block(&mut self) -> Result<(), HtmlError> {
214 self.write_str("</code></pre>")?;
215 self.current_lang = None;
216 self.get_state().currently_in_code_block = false;
217 Ok(())
218 }
219}
220
221pub fn push_html_with_highlighting(
223 markdown: &str,
224 config: &HtmlConfig,
225) -> Result<String, HtmlError> {
226 use pulldown_cmark::Parser;
227 use pulldown_cmark_escape::FmtWriter;
228
229 let mut output = String::new();
230 let writer = SyntectWriter::new(FmtWriter(&mut output), config);
231 let mut renderer: HtmlRenderer<FmtWriter<&mut String>, SyntectWriter<FmtWriter<&mut String>>> =
232 HtmlRenderer::new(writer);
233
234 let parser = Parser::new(markdown);
235 renderer.run(parser)?;
236
237 if let Some(ref style) = config.syntect {
239 if style.inject_css {
240 match renderer.writer.get_theme_css() {
241 Ok(css) => return Ok(format!("<style>{}</style>\n{}", css, output)),
242 Err(e) => eprintln!("Failed to generate syntax highlighting CSS: {}", e),
243 }
244 }
245 }
246
247 Ok(output)
248}