Skip to main content

rushdown_highlighting/
lib.rs

1#![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// Renderer {{{
34
35/// Options for the `HighlightingHtmlRenderer`.
36#[derive(Debug, Clone)]
37pub struct HighlightingHtmlRendererOptions {
38    /// The name of the syntax highlighting theme to use.
39    /// This value is only used if the `mode` is set to `HighlightingMode::Attribute`. If the theme
40    /// is not found, it falls back to "InspiredGitHub".
41    pub theme: &'static str,
42
43    /// The mode to use for syntax highlighting. This determines how the HTML output is structured.
44    pub mode: HighlightingMode,
45
46    /// An optional `ThemeSet` to use for syntax highlighting. If not provided, the default themes
47    /// will be used.
48    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/// The mode to use for syntax highlighting. This determines how the HTML output is structured.
62#[derive(Debug, Clone, Default, PartialEq, Eq)]
63pub enum HighlightingMode {
64    #[default]
65    Attribute,
66    Class,
67}
68
69/// Generates CSS for the specified theme. This is only necessary if the `mode` in
70/// `HighlightingHtmlRendererOptions` is set to `HighlightingMode::Class`.
71pub 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(&regions[..], 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    /// Renders a paragraph node.
192    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
252// }}}
253
254// Extension {{{
255
256/// Returns a renderer extension that adds support for rendering code blocks with syntax
257/// highlighting.
258pub 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// }}}