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