use std::io::Write;
use crate::error;
use itertools::Itertools;
use liquid_core::error::ResultLiquidReplaceExt;
use liquid_core::parser::TryMatchToken;
use liquid_core::Language;
use liquid_core::TagBlock;
use liquid_core::TagTokenIter;
use liquid_core::ValueView;
use liquid_core::{Renderable, Runtime};
use pulldown_cmark as cmark;
use pulldown_cmark::Event::{self, End, Html, Start, Text};
#[cfg(not(feature = "syntax-highlight"))]
pub use engarde::Raw as SyntaxHighlight;
#[cfg(feature = "syntax-highlight")]
pub use engarde::Syntax as SyntaxHighlight;
#[cfg(feature = "syntax-highlight")]
fn has_syntax_theme(syntax: &SyntaxHighlight, name: &str) -> error::Result<bool> {
Ok(syntax.has_theme(name))
}
#[cfg(not(feature = "syntax-highlight"))]
fn has_syntax_theme(syntax: &SyntaxHighlight, name: &str) -> error::Result<bool> {
anyhow::bail!("Themes are unsupported in this build.");
}
fn verify_theme(syntax: &SyntaxHighlight, theme: Option<&str>) -> error::Result<()> {
if let Some(theme) = &theme {
match has_syntax_theme(syntax, theme) {
Ok(true) => {}
Ok(false) => anyhow::bail!("Syntax theme '{}' is unsupported", theme),
Err(err) => {
log::warn!("Syntax theme named '{}' ignored. Reason: {}", theme, err);
}
};
}
Ok(())
}
#[derive(Clone, Debug)]
struct CodeBlock {
syntax: std::sync::Arc<SyntaxHighlight>,
lang: Option<liquid::model::KString>,
code: String,
theme: Option<liquid::model::KString>,
}
impl Renderable for CodeBlock {
fn render_to(
&self,
writer: &mut dyn Write,
_context: &dyn Runtime,
) -> Result<(), liquid_core::Error> {
write!(
writer,
"{}",
self.syntax
.format(&self.code, self.lang.as_deref(), self.theme.as_deref())
)
.replace("Failed to render")?;
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct CodeBlockParser {
syntax: std::sync::Arc<SyntaxHighlight>,
syntax_theme: Option<liquid::model::KString>,
}
impl CodeBlockParser {
pub fn new(
syntax: std::sync::Arc<SyntaxHighlight>,
theme: Option<liquid::model::KString>,
) -> error::Result<Self> {
verify_theme(&syntax, theme.as_deref())?;
Ok(Self {
syntax,
syntax_theme: theme,
})
}
}
impl liquid_core::BlockReflection for CodeBlockParser {
fn start_tag(&self) -> &'static str {
"highlight"
}
fn end_tag(&self) -> &'static str {
"endhighlight"
}
fn description(&self) -> &'static str {
"Syntax highlight code using HTML"
}
}
impl liquid_core::ParseBlock for CodeBlockParser {
fn reflection(&self) -> &dyn liquid_core::BlockReflection {
self
}
fn parse(
&self,
mut arguments: TagTokenIter<'_>,
mut tokens: TagBlock<'_, '_>,
_options: &Language,
) -> Result<Box<dyn Renderable>, liquid_core::Error> {
let lang = arguments
.expect_next("Identifier or literal expected.")
.ok()
.map(|lang| {
match lang.expect_literal() {
TryMatchToken::Matches(lang) => lang.to_kstr().into_owned(),
TryMatchToken::Fails(lang) => liquid::model::KString::from_ref(lang.as_str()),
}
});
arguments.expect_nothing()?;
let mut content = String::new();
while let Some(element) = tokens.next()? {
content.push_str(element.as_str());
}
tokens.assert_empty();
Ok(Box::new(CodeBlock {
syntax: self.syntax.clone(),
code: content,
lang,
theme: self.syntax_theme.clone(),
}))
}
}
pub struct DecoratedParser<'a> {
parser: cmark::Parser<'a, 'a>,
syntax: std::sync::Arc<SyntaxHighlight>,
theme: Option<&'a str>,
lang: Option<String>,
code: Option<Vec<pulldown_cmark::CowStr<'a>>>,
}
impl<'a> DecoratedParser<'a> {
pub fn new(
parser: cmark::Parser<'a, 'a>,
syntax: std::sync::Arc<SyntaxHighlight>,
theme: Option<&'a str>,
) -> error::Result<Self> {
verify_theme(&syntax, theme)?;
Ok(DecoratedParser {
parser,
syntax,
theme,
lang: None,
code: None,
})
}
}
impl<'a> Iterator for DecoratedParser<'a> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Event<'a>> {
match self.parser.next() {
Some(Text(text)) => {
if let Some(ref mut code) = self.code {
code.push(text);
Some(Text(pulldown_cmark::CowStr::Borrowed("")))
} else {
Some(Text(text))
}
}
Some(Start(cmark::Tag::CodeBlock(info))) => {
let tag = match info {
pulldown_cmark::CodeBlockKind::Indented => "",
pulldown_cmark::CodeBlockKind::Fenced(ref tag) => tag.as_ref(),
};
self.lang = tag.split(' ').map(|s| s.to_owned()).next();
self.code = Some(vec![]);
Some(Text(pulldown_cmark::CowStr::Borrowed("")))
}
Some(End(cmark::Tag::CodeBlock(_))) => {
let html = if let Some(code) = self.code.as_deref() {
let code = code.iter().join("\n");
self.syntax.format(&code, self.lang.as_deref(), self.theme)
} else {
self.syntax.format("", self.lang.as_deref(), self.theme)
};
self.lang = None;
self.code = None;
Some(Html(pulldown_cmark::CowStr::Boxed(html.into_boxed_str())))
}
item => item,
}
}
}
pub fn decorate_markdown<'a>(
parser: cmark::Parser<'a, 'a>,
syntax: std::sync::Arc<SyntaxHighlight>,
theme_name: Option<&'a str>,
) -> error::Result<DecoratedParser<'a>> {
DecoratedParser::new(parser, syntax, theme_name)
}
#[cfg(test)]
#[cfg(feature = "syntax-highlight")]
mod test_syntsx {
use super::*;
const CODE_BLOCK: &str = "mod test {
fn hello(arg: int) -> bool {
\
true
}
}
";
const CODEBLOCK_RENDERED: &str =
"<pre style=\"background-color:#2b303b;\">\n\
<code><span style=\"color:#b48ead;\">mod </span>\
<span style=\"color:#c0c5ce;\">test {\n\
</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;\">{\n\
</span><span style=\"color:#c0c5ce;\"> \
</span><span style=\"color:#d08770;\">true\n\
</span><span style=\"color:#c0c5ce;\"> }\n\
</span><span style=\"color:#c0c5ce;\"> }\n\
</span><span style=\"color:#c0c5ce;\"> </span></code></pre>\n";
#[test]
fn highlight_block_renders_rust() {
let syntax = std::sync::Arc::new(SyntaxHighlight::new());
let highlight: Box<dyn liquid_core::ParseBlock> =
Box::new(CodeBlockParser::new(syntax, Some("base16-ocean.dark".into())).unwrap());
let parser = liquid::ParserBuilder::new()
.block(highlight)
.build()
.unwrap();
let template = parser
.parse(&format!(
"{{% highlight rust %}}{}{{% endhighlight %}}",
CODE_BLOCK
))
.unwrap();
let output = template.render(&liquid::Object::new());
snapbox::assert_eq(CODEBLOCK_RENDERED, &output.unwrap());
}
const MARKDOWN_RENDERED: &str =
"<pre style=\"background-color:#2b303b;\">\n\
<code><span style=\"color:#b48ead;\">mod </span>\
<span style=\"color:#c0c5ce;\">test {\n\
</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;\">{\n\
</span><span style=\"color:#c0c5ce;\"> \
</span><span style=\"color:#d08770;\">true\n\
</span><span style=\"color:#c0c5ce;\"> }\n\
</span><span style=\"color:#c0c5ce;\"> }\n\
</span><span style=\"color:#c0c5ce;\"> \n\
</span></code></pre>\n";
#[test]
fn markdown_renders_rust() {
let html = format!(
"```rust
{}
```",
CODE_BLOCK
);
let mut buf = String::new();
let parser = cmark::Parser::new(&html);
let syntax = std::sync::Arc::new(SyntaxHighlight::new());
cmark::html::push_html(
&mut buf,
decorate_markdown(parser, syntax, Some("base16-ocean.dark")).unwrap(),
);
snapbox::assert_eq(MARKDOWN_RENDERED, &buf);
}
}
#[cfg(test)]
#[cfg(not(feature = "syntax-highlight"))]
mod test_raw {
use super::*;
const CODE_BLOCK: &str = "mod test {
fn hello(arg: int) -> bool {
\
true
}
}
";
const CODEBLOCK_RENDERED: &str = r#"<pre><code class="language-rust">mod test {
fn hello(arg: int) -> bool {
true
}
}
</code></pre>
"#;
#[test]
fn codeblock_renders_rust() {
let syntax = std::sync::Arc::new(SyntaxHighlight::new());
let highlight: Box<dyn liquid_core::ParseBlock> =
Box::new(CodeBlockParser::new(syntax, Some("base16-ocean.dark".into())).unwrap());
let parser = liquid::ParserBuilder::new()
.block(highlight)
.build()
.unwrap();
let template = parser
.parse(&format!(
"{{% highlight rust %}}{}{{% endhighlight %}}",
CODE_BLOCK
))
.unwrap();
let output = template.render(&liquid::Object::new());
assert_eq!(output.unwrap(), CODEBLOCK_RENDERED.to_string());
}
const MARKDOWN_RENDERED: &str = r#"<pre><code class="language-rust">mod test {
fn hello(arg: int) -> bool {
true
}
}
</code></pre>
"#;
#[test]
fn decorate_markdown_renders_rust() {
let html = format!(
"```rust
{}
```",
CODE_BLOCK
);
let mut buf = String::new();
let parser = cmark::Parser::new(&html);
let syntax = std::sync::Arc::new(SyntaxHighlight::new());
cmark::html::push_html(
&mut buf,
decorate_markdown(parser, syntax, Some("base16-ocean.dark")).unwrap(),
);
assert_eq!(buf, MARKDOWN_RENDERED);
}
}