Skip to main content

rushdown_highlighting/
lib.rs

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