1use std::io::Write;
2
3use crate::error;
4use itertools::Itertools;
5use liquid_core::Language;
6use liquid_core::TagBlock;
7use liquid_core::TagTokenIter;
8use liquid_core::ValueView;
9use liquid_core::error::ResultLiquidReplaceExt;
10use liquid_core::parser::TryMatchToken;
11use liquid_core::{Renderable, Runtime};
12use pulldown_cmark as cmark;
13use pulldown_cmark::Event::{self, End, Html, Start, Text};
14
15#[cfg(not(feature = "syntax-highlight"))]
16pub use engarde::Raw as SyntaxHighlight;
17#[cfg(feature = "syntax-highlight")]
18pub use engarde::Syntax as SyntaxHighlight;
19
20#[derive(Clone, Debug)]
21struct CodeBlock {
22 syntax: std::sync::Arc<SyntaxHighlight>,
23 lang: Option<liquid::model::KString>,
24 code: String,
25 theme: Option<liquid::model::KString>,
26}
27
28impl Renderable for CodeBlock {
29 fn render_to(
30 &self,
31 writer: &mut dyn Write,
32 _context: &dyn Runtime,
33 ) -> Result<(), liquid_core::Error> {
34 write!(
35 writer,
36 "{}",
37 self.syntax
38 .format(&self.code, self.lang.as_deref(), self.theme.as_deref())
39 )
40 .replace("Failed to render")?;
41
42 Ok(())
43 }
44}
45
46#[derive(Clone, Debug)]
47pub(crate) struct CodeBlockParser {
48 syntax: std::sync::Arc<SyntaxHighlight>,
49 syntax_theme: Option<liquid::model::KString>,
50}
51
52impl CodeBlockParser {
53 pub(crate) fn new(
54 syntax: std::sync::Arc<SyntaxHighlight>,
55 theme: Option<liquid::model::KString>,
56 ) -> error::Result<Self> {
57 verify_theme(&syntax, theme.as_deref())?;
58 Ok(Self {
59 syntax,
60 syntax_theme: theme,
61 })
62 }
63}
64
65fn verify_theme(syntax: &SyntaxHighlight, theme: Option<&str>) -> error::Result<()> {
66 if let Some(theme) = &theme {
67 match has_syntax_theme(syntax, theme) {
68 Ok(true) => {}
69 Ok(false) => anyhow::bail!("Syntax theme '{}' is unsupported", theme),
70 Err(err) => {
71 log::warn!("Syntax theme named '{theme}' ignored. Reason: {err}");
72 }
73 };
74 }
75 Ok(())
76}
77
78#[cfg(feature = "syntax-highlight")]
79fn has_syntax_theme(syntax: &SyntaxHighlight, name: &str) -> error::Result<bool> {
80 Ok(syntax.has_theme(name))
81}
82
83#[cfg(not(feature = "syntax-highlight"))]
84fn has_syntax_theme(syntax: &SyntaxHighlight, name: &str) -> error::Result<bool> {
85 anyhow::bail!("Themes are unsupported in this build.");
86}
87
88impl liquid_core::BlockReflection for CodeBlockParser {
89 fn start_tag(&self) -> &'static str {
90 "highlight"
91 }
92
93 fn end_tag(&self) -> &'static str {
94 "endhighlight"
95 }
96
97 fn description(&self) -> &'static str {
98 "Syntax highlight code using HTML"
99 }
100}
101
102impl liquid_core::ParseBlock for CodeBlockParser {
103 fn reflection(&self) -> &dyn liquid_core::BlockReflection {
104 self
105 }
106
107 fn parse(
108 &self,
109 mut arguments: TagTokenIter<'_>,
110 mut tokens: TagBlock<'_, '_>,
111 _options: &Language,
112 ) -> Result<Box<dyn Renderable>, liquid_core::Error> {
113 let lang = arguments
114 .expect_next("Identifier or literal expected.")
115 .ok()
116 .map(|lang| {
117 match lang.expect_literal() {
120 TryMatchToken::Matches(lang) => lang.to_kstr().into_owned(),
122 TryMatchToken::Fails(lang) => liquid::model::KString::from_ref(lang.as_str()),
123 }
124 });
125 arguments.expect_nothing()?;
127
128 let mut content = String::new();
129 while let Some(element) = tokens.next()? {
130 content.push_str(element.as_str());
131 }
132 tokens.assert_empty();
133
134 Ok(Box::new(CodeBlock {
135 syntax: self.syntax.clone(),
136 code: content,
137 lang,
138 theme: self.syntax_theme.clone(),
139 }))
140 }
141}
142
143pub(crate) struct DecoratedParser<'a> {
144 parser: cmark::Parser<'a>,
145 syntax: std::sync::Arc<SyntaxHighlight>,
146 theme: Option<&'a str>,
147 lang: Option<String>,
148 code: Option<Vec<pulldown_cmark::CowStr<'a>>>,
149}
150
151impl<'a> DecoratedParser<'a> {
152 pub(crate) fn new(
153 parser: cmark::Parser<'a>,
154 syntax: std::sync::Arc<SyntaxHighlight>,
155 theme: Option<&'a str>,
156 ) -> error::Result<Self> {
157 verify_theme(&syntax, theme)?;
158 Ok(DecoratedParser {
159 parser,
160 syntax,
161 theme,
162 lang: None,
163 code: None,
164 })
165 }
166}
167
168impl<'a> Iterator for DecoratedParser<'a> {
169 type Item = Event<'a>;
170
171 fn next(&mut self) -> Option<Event<'a>> {
172 match self.parser.next() {
173 Some(Text(text)) => {
174 if let Some(ref mut code) = self.code {
175 code.push(text);
176 Some(Text(pulldown_cmark::CowStr::Borrowed("")))
177 } else {
178 Some(Text(text))
179 }
180 }
181 Some(Start(cmark::Tag::CodeBlock(info))) => {
182 let tag = match info {
183 pulldown_cmark::CodeBlockKind::Indented => "",
184 pulldown_cmark::CodeBlockKind::Fenced(ref tag) => tag.as_ref(),
185 };
186 self.lang = tag.split(' ').map(|s| s.to_owned()).next();
187 self.code = Some(vec![]);
188 Some(Text(pulldown_cmark::CowStr::Borrowed("")))
189 }
190 Some(End(cmark::TagEnd::CodeBlock)) => {
191 let html = if let Some(code) = self.code.as_deref() {
192 let code = code.iter().join("\n");
193 self.syntax.format(&code, self.lang.as_deref(), self.theme)
194 } else {
195 self.syntax.format("", self.lang.as_deref(), self.theme)
196 };
197 self.lang = None;
199 self.code = None;
200 Some(Html(pulldown_cmark::CowStr::Boxed(html.into_boxed_str())))
202 }
203 item => item,
204 }
205 }
206}
207
208pub(crate) fn decorate_markdown<'a>(
209 parser: cmark::Parser<'a>,
210 syntax: std::sync::Arc<SyntaxHighlight>,
211 theme_name: Option<&'a str>,
212) -> error::Result<DecoratedParser<'a>> {
213 DecoratedParser::new(parser, syntax, theme_name)
214}
215
216#[cfg(test)]
217#[cfg(feature = "syntax-highlight")]
218mod test_syntsx {
219 use super::*;
220
221 use snapbox::assert_data_eq;
222 use snapbox::prelude::*;
223 use snapbox::str;
224
225 const CODE_BLOCK: &str = "mod test {
226 fn hello(arg: int) -> bool {
227 \
228 true
229 }
230 }
231 ";
232
233 #[test]
234 fn highlight_block_renders_rust() {
235 let syntax = std::sync::Arc::new(SyntaxHighlight::new());
236 let highlight: Box<dyn liquid_core::ParseBlock> =
237 Box::new(CodeBlockParser::new(syntax, Some("base16-ocean.dark".into())).unwrap());
238 let parser = liquid::ParserBuilder::new()
239 .block(highlight)
240 .build()
241 .unwrap();
242 let template = parser
243 .parse(&format!(
244 "{{% highlight rust %}}{CODE_BLOCK}{{% endhighlight %}}"
245 ))
246 .unwrap();
247 let output = template.render(&liquid::Object::new());
248 let expected = str![[r#"
249<pre style="background-color:#2b303b;">
250<code><span style="color:#b48ead;">mod </span><span style="color:#c0c5ce;">test {
251</span><span style="color:#c0c5ce;"> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">hello</span><span style="color:#c0c5ce;">(</span><span style="color:#bf616a;">arg</span><span style="color:#c0c5ce;">: int) -> </span><span style="color:#b48ead;">bool </span><span style="color:#c0c5ce;">{
252</span><span style="color:#c0c5ce;"> </span><span style="color:#d08770;">true
253</span><span style="color:#c0c5ce;"> }
254</span><span style="color:#c0c5ce;"> }
255</span><span style="color:#c0c5ce;"> </span></code></pre>
256
257"#]];
258
259 assert_data_eq!(output.unwrap(), expected.raw());
260 }
261
262 #[test]
263 fn markdown_renders_rust() {
264 let html = format!(
265 "```rust
266{CODE_BLOCK}
267```"
268 );
269
270 let mut buf = String::new();
271 let parser = cmark::Parser::new(&html);
272 let syntax = std::sync::Arc::new(SyntaxHighlight::new());
273 cmark::html::push_html(
274 &mut buf,
275 decorate_markdown(parser, syntax, Some("base16-ocean.dark")).unwrap(),
276 );
277 let expected = str![[r#"
278<pre style="background-color:#2b303b;">
279<code><span style="color:#b48ead;">mod </span><span style="color:#c0c5ce;">test {
280</span><span style="color:#c0c5ce;"> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">hello</span><span style="color:#c0c5ce;">(</span><span style="color:#bf616a;">arg</span><span style="color:#c0c5ce;">: int) -> </span><span style="color:#b48ead;">bool </span><span style="color:#c0c5ce;">{
281</span><span style="color:#c0c5ce;"> </span><span style="color:#d08770;">true
282</span><span style="color:#c0c5ce;"> }
283</span><span style="color:#c0c5ce;"> }
284</span><span style="color:#c0c5ce;">
285</span></code></pre>
286
287"#]];
288
289 assert_data_eq!(&buf, expected.raw());
290 }
291}
292
293#[cfg(test)]
294#[cfg(not(feature = "syntax-highlight"))]
295mod test_raw {
296 use super::*;
297
298 use snapbox::assert_data_eq;
299 use snapbox::prelude::*;
300 use snapbox::str;
301
302 const CODE_BLOCK: &str = "mod test {
303 fn hello(arg: int) -> bool {
304 \
305 true
306 }
307 }
308";
309
310 #[test]
311 fn codeblock_renders_rust() {
312 let syntax = std::sync::Arc::new(SyntaxHighlight::new());
313 let highlight: Box<dyn liquid_core::ParseBlock> =
314 Box::new(CodeBlockParser::new(syntax, Some("base16-ocean.dark".into())).unwrap());
315 let parser = liquid::ParserBuilder::new()
316 .block(highlight)
317 .build()
318 .unwrap();
319 let template = parser
320 .parse(&format!(
321 "{{% highlight rust %}}{}{{% endhighlight %}}",
322 CODE_BLOCK
323 ))
324 .unwrap();
325 let output = template.render(&liquid::Object::new());
326 let expected = str![[r#"
327<pre><code class="language-rust">mod test {
328 fn hello(arg: int) -> bool {
329 true
330 }
331 }
332</code></pre>
333
334"#]];
335
336 assert_data_eq!(output.unwrap(), expected.raw());
337 }
338
339 #[test]
340 fn decorate_markdown_renders_rust() {
341 let html = format!(
342 "```rust
343{}
344```",
345 CODE_BLOCK
346 );
347
348 let mut buf = String::new();
349 let parser = cmark::Parser::new(&html);
350 let syntax = std::sync::Arc::new(SyntaxHighlight::new());
351 cmark::html::push_html(
352 &mut buf,
353 decorate_markdown(parser, syntax, Some("base16-ocean.dark")).unwrap(),
354 );
355 let expected = str![[r#"
356<pre><code class="language-rust">mod test {
357 fn hello(arg: int) -> bool {
358 true
359 }
360 }
361
362</code></pre>
363
364"#]];
365
366 assert_data_eq!(&buf, expected.raw());
367 }
368}