• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cobalt-org / cobalt.rs / 18109737057

29 Sep 2025 08:25PM UTC coverage: 18.413% (-0.4%) from 18.782%
18109737057

push

github

web-flow
Merge pull request #1261 from vbfox/coverage_fixes

Coverage fixes

441 of 2395 relevant lines covered (18.41%)

0.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.11
/src/syntax_highlight.rs
1
use std::io::Write;
2

3
use crate::error;
4
use itertools::Itertools;
5
use liquid_core::Language;
6
use liquid_core::TagBlock;
7
use liquid_core::TagTokenIter;
8
use liquid_core::ValueView;
9
use liquid_core::error::ResultLiquidReplaceExt;
10
use liquid_core::parser::TryMatchToken;
11
use liquid_core::{Renderable, Runtime};
12
use pulldown_cmark as cmark;
13
use pulldown_cmark::Event::{self, End, Html, Start, Text};
14

15
#[cfg(not(feature = "syntax-highlight"))]
16
pub use engarde::Raw as SyntaxHighlight;
17
#[cfg(feature = "syntax-highlight")]
18
pub use engarde::Syntax as SyntaxHighlight;
19

20
#[derive(Clone, Debug)]
21
struct CodeBlock {
22
    syntax: std::sync::Arc<SyntaxHighlight>,
23
    lang: Option<liquid::model::KString>,
24
    code: String,
25
    theme: Option<liquid::model::KString>,
26
}
27

28
impl Renderable for CodeBlock {
29
    fn render_to(
1✔
30
        &self,
31
        writer: &mut dyn Write,
32
        _context: &dyn Runtime,
33
    ) -> Result<(), liquid_core::Error> {
34
        write!(
3✔
35
            writer,
36
            "{}",
37
            self.syntax
1✔
38
                .format(&self.code, self.lang.as_deref(), self.theme.as_deref())
1✔
39
        )
40
        .replace("Failed to render")?;
41

42
        Ok(())
1✔
43
    }
44
}
45

46
#[derive(Clone, Debug)]
47
pub(crate) struct CodeBlockParser {
48
    syntax: std::sync::Arc<SyntaxHighlight>,
49
    syntax_theme: Option<liquid::model::KString>,
50
}
51

52
impl CodeBlockParser {
53
    pub(crate) fn new(
1✔
54
        syntax: std::sync::Arc<SyntaxHighlight>,
55
        theme: Option<liquid::model::KString>,
56
    ) -> error::Result<Self> {
57
        verify_theme(&syntax, theme.as_deref())?;
2✔
58
        Ok(Self {
1✔
59
            syntax,
1✔
60
            syntax_theme: theme,
1✔
61
        })
62
    }
63
}
64

65
fn verify_theme(syntax: &SyntaxHighlight, theme: Option<&str>) -> error::Result<()> {
1✔
66
    if let Some(theme) = &theme {
1✔
67
        match has_syntax_theme(syntax, theme) {
2✔
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(())
1✔
76
}
77

78
#[cfg(feature = "syntax-highlight")]
79
fn has_syntax_theme(syntax: &SyntaxHighlight, name: &str) -> error::Result<bool> {
1✔
80
    Ok(syntax.has_theme(name))
1✔
81
}
82

83
#[cfg(not(feature = "syntax-highlight"))]
84
fn has_syntax_theme(syntax: &SyntaxHighlight, name: &str) -> error::Result<bool> {
85
    anyhow::bail!("Themes are unsupported in this build.");
86
}
87

88
impl liquid_core::BlockReflection for CodeBlockParser {
89
    fn start_tag(&self) -> &'static str {
1✔
90
        "highlight"
91
    }
92

93
    fn end_tag(&self) -> &'static str {
1✔
94
        "endhighlight"
95
    }
96

97
    fn description(&self) -> &'static str {
×
98
        "Syntax highlight code using HTML"
99
    }
100
}
101

102
impl liquid_core::ParseBlock for CodeBlockParser {
103
    fn reflection(&self) -> &dyn liquid_core::BlockReflection {
1✔
104
        self
105
    }
106

107
    fn parse(
1✔
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
1✔
114
            .expect_next("Identifier or literal expected.")
115
            .ok()
116
            .map(|lang| {
2✔
117
                // This may accept strange inputs such as `{% include 0 %}` or `{% include filterchain | filter:0 %}`.
118
                // Those inputs would fail anyway by there being not a path with those langs so they are not a big concern.
119
                match lang.expect_literal() {
1✔
120
                    // Using `to_str()` on literals ensures `Strings` will have their quotes trimmed.
121
                    TryMatchToken::Matches(lang) => lang.to_kstr().into_owned(),
×
122
                    TryMatchToken::Fails(lang) => liquid::model::KString::from_ref(lang.as_str()),
1✔
123
                }
124
            });
125
        // no more arguments should be supplied, trying to supply them is an error
126
        arguments.expect_nothing()?;
2✔
127

128
        let mut content = String::new();
1✔
129
        while let Some(element) = tokens.next()? {
2✔
130
            content.push_str(element.as_str());
2✔
131
        }
132
        tokens.assert_empty();
1✔
133

134
        Ok(Box::new(CodeBlock {
1✔
135
            syntax: self.syntax.clone(),
1✔
136
            code: content,
1✔
137
            lang,
1✔
138
            theme: self.syntax_theme.clone(),
1✔
139
        }))
140
    }
141
}
142

143
pub(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

151
impl<'a> DecoratedParser<'a> {
152
    pub(crate) fn new(
1✔
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)?;
2✔
158
        Ok(DecoratedParser {
1✔
159
            parser,
1✔
160
            syntax,
1✔
161
            theme,
×
162
            lang: None,
1✔
163
            code: None,
1✔
164
        })
165
    }
166
}
167

168
impl<'a> Iterator for DecoratedParser<'a> {
169
    type Item = Event<'a>;
170

171
    fn next(&mut self) -> Option<Event<'a>> {
1✔
172
        match self.parser.next() {
2✔
173
            Some(Text(text)) => {
1✔
174
                if let Some(ref mut code) = self.code {
3✔
175
                    code.push(text);
1✔
176
                    Some(Text(pulldown_cmark::CowStr::Borrowed("")))
1✔
177
                } else {
178
                    Some(Text(text))
×
179
                }
180
            }
181
            Some(Start(cmark::Tag::CodeBlock(info))) => {
1✔
182
                let tag = match info {
1✔
183
                    pulldown_cmark::CodeBlockKind::Indented => "",
×
184
                    pulldown_cmark::CodeBlockKind::Fenced(ref tag) => tag.as_ref(),
2✔
185
                };
186
                self.lang = tag.split(' ').map(|s| s.to_owned()).next();
4✔
187
                self.code = Some(vec![]);
1✔
188
                Some(Text(pulldown_cmark::CowStr::Borrowed("")))
1✔
189
            }
190
            Some(End(cmark::TagEnd::CodeBlock)) => {
×
191
                let html = if let Some(code) = self.code.as_deref() {
1✔
192
                    let code = code.iter().join("\n");
2✔
193
                    self.syntax.format(&code, self.lang.as_deref(), self.theme)
2✔
194
                } else {
195
                    self.syntax.format("", self.lang.as_deref(), self.theme)
×
196
                };
197
                // reset highlighter
198
                self.lang = None;
1✔
199
                self.code = None;
1✔
200
                // close the code block
201
                Some(Html(pulldown_cmark::CowStr::Boxed(html.into_boxed_str())))
2✔
202
            }
203
            item => item,
1✔
204
        }
205
    }
206
}
207

208
pub(crate) fn decorate_markdown<'a>(
1✔
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)
1✔
214
}
215

216
#[cfg(test)]
217
#[cfg(feature = "syntax-highlight")]
218
mod 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) -&gt; </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) -&gt; </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"))]
295
mod 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) -&gt; 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) -&gt; bool {
358
            true
359
        }
360
    }
361

362
</code></pre>
363

364
"#]];
365

366
        assert_data_eq!(&buf, expected.raw());
367
    }
368
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc