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