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::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#[derive(Debug, Clone)]
38pub struct HighlightingHtmlRendererOptions {
39 pub theme: &'static str,
43
44 pub mode: HighlightingMode,
46
47 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub enum HighlightingMode {
65 #[default]
66 Attribute,
67 Class,
68}
69
70pub 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(®ions[..], 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 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_str(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_str(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
265pub 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