rushdown_highlighting/
lib.rs1#![doc = include_str!("../README.md")]
2
3use rushdown::as_kind_data;
4use rushdown::as_type_data;
5use rushdown::ast::Arena;
6use rushdown::ast::CodeBlock;
7use rushdown::ast::NodeRef;
8use rushdown::ast::WalkStatus;
9use rushdown::renderer;
10use rushdown::renderer::html;
11use rushdown::renderer::html::Renderer;
12use rushdown::renderer::html::RendererExtension;
13use rushdown::renderer::html::RendererExtensionFn;
14use rushdown::renderer::BoxRenderNode;
15use rushdown::renderer::NodeRenderer;
16use rushdown::renderer::NodeRendererRegistry;
17use rushdown::renderer::RenderNode;
18use rushdown::renderer::RendererOptions;
19use rushdown::renderer::TextWrite;
20use rushdown::Result;
21use std::any::TypeId;
22use std::rc::Rc;
23use syntect::easy::HighlightLines;
24use syntect::highlighting::ThemeSet;
25use syntect::html::css_for_theme_with_class_style;
26use syntect::html::styled_line_to_highlighted_html;
27use syntect::html::ClassStyle;
28use syntect::html::ClassedHTMLGenerator;
29use syntect::html::IncludeBackground;
30use syntect::parsing::SyntaxSet;
31use syntect::util::LinesWithEndings;
32
33#[derive(Debug, Clone)]
37pub struct HighlightingHtmlRendererOptions {
38 pub theme: &'static str,
42
43 pub mode: HighlightingMode,
45
46 pub theme_set: Option<Rc<ThemeSet>>,
49}
50
51impl Default for HighlightingHtmlRendererOptions {
52 fn default() -> Self {
53 Self {
54 theme: "InspiredGitHub",
55 mode: HighlightingMode::default(),
56 theme_set: None,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Eq)]
63pub enum HighlightingMode {
64 #[default]
65 Attribute,
66 Class,
67}
68
69pub fn generate_css(theme: &str, theme_set: Option<&ThemeSet>) -> Option<String> {
72 if let Some(ts) = theme_set {
73 let theme = ts.themes.get(theme)?;
74 css_for_theme_with_class_style(theme, ClassStyle::Spaced).ok()
75 } else {
76 let ts = ThemeSet::load_defaults();
77 let theme = ts.themes.get(theme)?;
78 css_for_theme_with_class_style(theme, ClassStyle::Spaced).ok()
79 }
80}
81
82impl RendererOptions for HighlightingHtmlRendererOptions {}
83
84#[allow(dead_code)]
85struct HighlightingHtmlRenderer<W: TextWrite> {
86 _phantom: core::marker::PhantomData<W>,
87 writer: html::Writer,
88 options: HighlightingHtmlRendererOptions,
89 syntax_set: SyntaxSet,
90 default_theme_set: ThemeSet,
91}
92
93impl<W: TextWrite> HighlightingHtmlRenderer<W> {
94 fn with_options(options: HighlightingHtmlRendererOptions, html_opts: html::Options) -> Self {
95 Self {
96 _phantom: core::marker::PhantomData,
97 writer: html::Writer::with_options(html_opts),
98 syntax_set: SyntaxSet::load_defaults_newlines(),
99 default_theme_set: ThemeSet::load_defaults(),
100 options,
101 }
102 }
103
104 fn render_code_to_html_attr(
105 &self,
106 language: &str,
107 code: &str,
108 theme_name: &str,
109 ) -> Option<String> {
110 let ps = &self.syntax_set;
111 let ts = if let Some(ref theme_set) = self.options.theme_set {
112 theme_set
113 } else {
114 &self.default_theme_set
115 };
116
117 let theme = ts
118 .themes
119 .get(theme_name)
120 .unwrap_or_else(|| &ts.themes["InspiredGitHub"]);
121
122 let lang = if language.is_empty() {
123 "plaintext"
124 } else {
125 language
126 };
127 let syntax = ps
128 .find_syntax_by_token(lang)
129 .or_else(|| ps.find_syntax_by_extension(lang))
130 .unwrap_or_else(|| ps.find_syntax_plain_text());
131
132 let bg = theme
133 .settings
134 .background
135 .map(|c| format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b))
136 .unwrap_or_else(|| "#ffffff".to_string());
137
138 let mut out = String::new();
139 out.push_str(&format!(
140 r#"<pre style="background-color: {}; padding: 12px; overflow: auto;"><code class="language-{}">"#,
141 bg, language
142 ));
143
144 let mut h = HighlightLines::new(syntax, theme);
145
146 for line in LinesWithEndings::from(code) {
147 let regions = h.highlight_line(line, ps).ok()?;
148 let html_line =
149 styled_line_to_highlighted_html(®ions[..], IncludeBackground::No).ok()?;
150 out.push_str(&html_line);
151 }
152
153 out.push_str("</code></pre>\n");
154 Some(out)
155 }
156
157 fn render_with_classes(&self, language: &str, code: &str) -> Option<String> {
158 let ps = &self.syntax_set;
159
160 let lang = if language.is_empty() {
161 "plaintext"
162 } else {
163 language
164 };
165
166 let syntax = ps
167 .find_syntax_by_token(lang)
168 .or_else(|| ps.find_syntax_by_extension(lang))
169 .unwrap_or_else(|| ps.find_syntax_plain_text());
170
171 let mut html_gen =
172 ClassedHTMLGenerator::new_with_class_style(syntax, ps, ClassStyle::Spaced);
173
174 let mut html = String::new();
175 html.push_str(&format!(
176 r#"<pre class="code"><code class="language-{}">"#,
177 language
178 ));
179 for line in LinesWithEndings::from(code) {
180 html_gen
181 .parse_html_for_line_which_includes_newline(line)
182 .ok()?;
183 }
184 html.push_str(&html_gen.finalize());
185 html.push_str("</code></pre>\n");
186 Some(html)
187 }
188}
189
190impl<W: TextWrite> RenderNode<W> for HighlightingHtmlRenderer<W> {
191 fn render_node<'a>(
193 &self,
194 w: &mut W,
195 source: &'a str,
196 arena: &'a Arena,
197 node_ref: NodeRef,
198 entering: bool,
199 _ctx: &mut renderer::Context,
200 ) -> Result<WalkStatus> {
201 if entering {
202 let kd = as_kind_data!(arena, node_ref, CodeBlock);
203 let block = as_type_data!(arena, node_ref, Block);
204 let mut code = String::new();
205 for line in block.lines().iter() {
206 code.push_str(&line.str(source));
207 }
208 let lang = kd.language(source).unwrap_or("plaintext");
209 match self.options.mode {
210 HighlightingMode::Attribute => {
211 if let Some(html) =
212 self.render_code_to_html_attr(lang, &code, self.options.theme)
213 {
214 w.write_str(&html)?;
215 return Ok(WalkStatus::Continue);
216 }
217 }
218 HighlightingMode::Class => {
219 if let Some(html) = self.render_with_classes(lang, &code) {
220 w.write_str(&html)?;
221 return Ok(WalkStatus::Continue);
222 }
223 }
224 }
225
226 self.writer.write_safe_str(w, "<pre><code")?;
227 if let Some(lang) = kd.language(source) {
228 self.writer.write_safe_str(w, " class=\"language-")?;
229 self.writer.write(w, lang)?;
230 self.writer.write_safe_str(w, "\"")?;
231 }
232 self.writer.write_safe_str(w, ">")?;
233 let block = as_type_data!(arena, node_ref, Block);
234 for line in block.lines().iter() {
235 self.writer.raw_write(w, &line.str(source))?;
236 }
237 self.writer.write_safe_str(w, "</code></pre>\n")?;
238 }
239 Ok(WalkStatus::Continue)
240 }
241}
242
243impl<'r, W> NodeRenderer<'r, W> for HighlightingHtmlRenderer<W>
244where
245 W: TextWrite + 'r,
246{
247 fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'r, W>) {
248 nrr.register_node_renderer_fn(TypeId::of::<CodeBlock>(), BoxRenderNode::new(self));
249 }
250}
251
252pub fn highlighting_html_renderer_extension<'cb, W>(
259 options: impl Into<HighlightingHtmlRendererOptions>,
260) -> impl RendererExtension<'cb, W>
261where
262 W: TextWrite + 'cb,
263{
264 RendererExtensionFn::new(move |r: &mut Renderer<'cb, W>| {
265 let options = options.into();
266 r.add_node_renderer(HighlightingHtmlRenderer::with_options, options);
267 })
268}
269
270