use crate::types::elixir_types::{ExFormatterOption, ThemeOrString};
use comrak::adapters::SyntaxHighlighterAdapter;
use lumis::highlight::highlight_iter;
use lumis::html;
use lumis::languages::Language;
use lumis::themes::{self, Appearance};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::fmt::Write;
use std::io;
use std::sync::Arc;
struct FmtToIoAdapter<'a, W: Write + ?Sized>(&'a mut W);
impl<'a, W: Write + ?Sized> io::Write for FmtToIoAdapter<'a, W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let s =
std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
self.0
.write_str(s)
.map_err(|_| io::Error::other("write error"))?;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
struct MultiThemesConfig {
themes: HashMap<String, themes::Theme>,
default_theme: Option<String>,
css_variable_prefix: String,
italic: bool,
include_highlights: bool,
}
pub struct LumisAdapter {
formatter_config: ExFormatterOption,
stored_attrs: Mutex<Option<Arc<HashMap<String, String>>>>,
stored_lang: Mutex<Option<Language>>,
}
impl Default for LumisAdapter {
fn default() -> Self {
Self {
formatter_config: ExFormatterOption::default(),
stored_attrs: Mutex::new(None),
stored_lang: Mutex::new(None),
}
}
}
impl LumisAdapter {
pub fn new(formatter_config: ExFormatterOption) -> Self {
Self {
formatter_config,
stored_attrs: Mutex::new(None),
stored_lang: Mutex::new(None),
}
}
fn parse_custom_attributes(info_string: &str) -> Option<HashMap<String, String>> {
let tokens = shlex::split(info_string)?;
if tokens.is_empty() {
return None;
}
let mut attributes = HashMap::with_capacity(tokens.len());
for token in tokens {
if let Some((key, value)) = token.split_once('=') {
attributes.insert(key.trim().to_string(), value.to_string());
} else {
// Handle standalone flags (e.g., "include_highlights" without "=true")
attributes.insert(token.trim().to_string(), "true".to_string());
}
}
if attributes.is_empty() {
None
} else {
Some(attributes)
}
}
fn theme_from_formatter(&self) -> Option<themes::Theme> {
match &self.formatter_config {
ExFormatterOption::HtmlInline { theme, .. } => theme.as_ref().and_then(|t| match t {
ThemeOrString::Theme(ex_theme) => Some(ex_theme.clone().into()),
ThemeOrString::String(name) => themes::get(name).ok(),
}),
ExFormatterOption::Terminal { theme } => theme.as_ref().and_then(|t| match t {
ThemeOrString::Theme(ex_theme) => Some(ex_theme.clone().into()),
ThemeOrString::String(name) => themes::get(name).ok(),
}),
_ => None,
}
}
fn multi_themes_config(&self) -> Option<MultiThemesConfig> {
match &self.formatter_config {
ExFormatterOption::HtmlMultiThemes {
themes,
default_theme,
css_variable_prefix,
italic,
include_highlights,
..
} => {
let themes_map: HashMap<String, themes::Theme> = themes
.iter()
.map(|(k, v)| (k.clone(), v.clone().into()))
.collect();
Some(MultiThemesConfig {
themes: themes_map,
default_theme: default_theme.clone(),
css_variable_prefix: css_variable_prefix
.clone()
.unwrap_or_else(|| "--athl".to_string()),
italic: *italic,
include_highlights: *include_highlights,
})
}
_ => None,
}
}
fn pre_class(&self) -> Option<&str> {
match &self.formatter_config {
ExFormatterOption::HtmlInline { pre_class, .. } => pre_class.as_deref(),
ExFormatterOption::HtmlLinked { pre_class, .. } => pre_class.as_deref(),
ExFormatterOption::HtmlMultiThemes { pre_class, .. } => pre_class.as_deref(),
_ => None,
}
}
fn italic_enabled(&self) -> bool {
match &self.formatter_config {
ExFormatterOption::HtmlInline { italic, .. } => *italic,
ExFormatterOption::HtmlMultiThemes { italic, .. } => *italic,
_ => false,
}
}
fn should_include_highlights(&self) -> bool {
let stored_attrs = self.stored_attrs.lock();
if let Some(attrs) = stored_attrs.as_ref() {
if attrs.contains_key("include_highlights") {
return true;
}
}
match &self.formatter_config {
ExFormatterOption::HtmlInline {
include_highlights, ..
} => *include_highlights,
ExFormatterOption::HtmlMultiThemes {
include_highlights, ..
} => *include_highlights,
_ => false,
}
}
fn decorator_theme(&self) -> Option<themes::Theme> {
let stored_attrs = self.stored_attrs.lock();
if let Some(attrs) = stored_attrs.as_ref() {
if let Some(theme_name) = attrs.get("theme") {
return themes::get(theme_name).ok();
}
}
None
}
fn decorator_pre_class(&self) -> Option<String> {
let stored_attrs = self.stored_attrs.lock();
if let Some(attrs) = stored_attrs.as_ref() {
if let Some(pre_class) = attrs.get("pre_class") {
return Some(pre_class.clone());
}
}
None
}
fn parse_highlight_lines(line_spec: &str) -> Vec<usize> {
let mut lines = Vec::new();
for part in line_spec.split(',') {
let part = part.trim();
if let Some((start, end)) = part.split_once('-') {
if let (Ok(s), Ok(e)) = (start.trim().parse::<usize>(), end.trim().parse::<usize>())
{
for line in s..=e {
lines.push(line);
}
}
} else if let Ok(line) = part.parse::<usize>() {
lines.push(line);
}
}
lines
}
fn highlight_lines_config(
&self,
theme: &Option<themes::Theme>,
) -> Option<(Vec<usize>, Option<String>, Option<String>)> {
let stored_attrs = self.stored_attrs.lock();
if let Some(attrs) = stored_attrs.as_ref() {
if let Some(lines_spec) = attrs.get("highlight_lines") {
let lines = Self::parse_highlight_lines(lines_spec);
let is_linked =
matches!(self.formatter_config, ExFormatterOption::HtmlLinked { .. });
let class = attrs.get("highlight_lines_class").cloned().or_else(|| {
if is_linked {
Some("highlighted".to_string())
} else {
None
}
});
let style = attrs
.get("highlight_lines_style")
.and_then(|s| {
if s == "theme" {
theme.as_ref().and_then(|t| {
t.get_style("highlighted").map(|style| style.css(true, " "))
})
} else {
Some(s.clone())
}
})
.or_else(|| {
if is_linked {
None
} else {
theme.as_ref().map(|t| {
let is_light = matches!(t.appearance, Appearance::Light)
|| t.name.to_lowercase().contains("light");
let highlight_bg = if is_light { "#e7eaf0" } else { "#3b4252" };
format!("background-color: {};", highlight_bg)
})
}
});
return Some((lines, style, class));
}
}
drop(stored_attrs);
self.formatter_highlight_lines_config(theme)
}
fn formatter_highlight_lines_config(
&self,
theme: &Option<themes::Theme>,
) -> Option<(Vec<usize>, Option<String>, Option<String>)> {
match &self.formatter_config {
ExFormatterOption::HtmlInline {
highlight_lines: Some(hl),
..
}
| ExFormatterOption::HtmlMultiThemes {
highlight_lines: Some(hl),
..
} => {
let lines = Self::convert_line_specs(&hl.lines);
let style = hl.style.as_ref().map(|s| match s {
crate::types::elixir_types::ExHtmlInlineHighlightLinesStyle::Theme => theme
.as_ref()
.and_then(|t| t.get_style("highlighted").map(|style| style.css(true, " ")))
.unwrap_or_else(|| {
let is_light = theme
.as_ref()
.map(|t| {
matches!(t.appearance, Appearance::Light)
|| t.name.to_lowercase().contains("light")
})
.unwrap_or(false);
let highlight_bg = if is_light { "#e7eaf0" } else { "#3b4252" };
format!("background-color: {};", highlight_bg)
}),
crate::types::elixir_types::ExHtmlInlineHighlightLinesStyle::Style {
style,
} => style.clone(),
});
let class = hl.class.clone();
Some((lines, style, class))
}
ExFormatterOption::HtmlLinked {
highlight_lines: Some(hl),
..
} => {
let lines = Self::convert_line_specs(&hl.lines);
let class = Some(hl.class.clone());
Some((lines, None, class))
}
_ => None,
}
}
fn convert_line_specs(lines: &[crate::types::elixir_types::ExLineSpec]) -> Vec<usize> {
let mut result = Vec::new();
for spec in lines {
match spec {
crate::types::elixir_types::ExLineSpec::Single(n) => result.push(*n),
crate::types::elixir_types::ExLineSpec::Range { start, end } => {
for n in *start..=*end {
result.push(n);
}
}
}
}
result
}
#[allow(dead_code)]
fn header_config(&self) -> Option<(&str, &str)> {
match &self.formatter_config {
ExFormatterOption::HtmlInline {
header: Some(h), ..
}
| ExFormatterOption::HtmlLinked {
header: Some(h), ..
}
| ExFormatterOption::HtmlMultiThemes {
header: Some(h), ..
} => Some((&h.open_tag, &h.close_tag)),
_ => None,
}
}
fn custom_attrs(
attributes: &HashMap<&'static str, std::borrow::Cow<'_, str>>,
) -> Option<HashMap<String, String>> {
attributes
.get("data-meta")
.and_then(|info| Self::parse_custom_attributes(info.as_ref()))
}
fn language_from_attrs(
attributes: &HashMap<&'static str, std::borrow::Cow<'_, str>>,
) -> Language {
if let Some(lang) = attributes.get("lang") {
Language::guess(Some(lang.as_ref()), "")
} else if let Some(class) = attributes.get("class") {
let language = class.strip_prefix("language-").unwrap_or("plaintext");
Language::guess(Some(language), "")
} else {
Language::guess(Some("plaintext"), "")
}
}
}
impl SyntaxHighlighterAdapter for LumisAdapter {
fn write_pre_tag<'s>(
&self,
output: &mut dyn Write,
attributes: HashMap<&'static str, std::borrow::Cow<'s, str>>,
) -> std::fmt::Result {
let custom_attrs = Self::custom_attrs(&attributes);
let lang = attributes.get("lang").map(|l| Language::guess(Some(l), ""));
if let Some(attrs) = custom_attrs {
*self.stored_attrs.lock() = Some(Arc::new(attrs));
}
if let Some(language) = lang {
*self.stored_lang.lock() = Some(language);
}
let theme = self
.decorator_theme()
.or_else(|| self.theme_from_formatter());
let pre_class = self
.decorator_pre_class()
.or_else(|| self.pre_class().map(|s| s.to_string()));
let mut adapter = FmtToIoAdapter(output);
html::open_pre_tag(&mut adapter, pre_class.as_deref(), theme.as_ref())
.map_err(|_| std::fmt::Error)
}
fn write_code_tag<'s>(
&self,
output: &mut dyn Write,
attributes: HashMap<&'static str, std::borrow::Cow<'s, str>>,
) -> std::fmt::Result {
let custom_attrs = Self::custom_attrs(&attributes);
let lang = Self::language_from_attrs(&attributes);
if let Some(attrs) = custom_attrs {
*self.stored_attrs.lock() = Some(Arc::new(attrs));
}
if !attributes.is_empty() {
*self.stored_lang.lock() = Some(lang);
}
let stored_lang = *self.stored_lang.lock();
let effective_lang = stored_lang.unwrap_or(lang);
let mut adapter = FmtToIoAdapter(output);
html::open_code_tag(&mut adapter, &effective_lang).map_err(|_| std::fmt::Error)
}
fn write_highlighted(
&self,
output: &mut dyn Write,
lang: Option<&str>,
source: &str,
) -> std::fmt::Result {
let stored_lang = *self.stored_lang.lock();
let language = if let Some(stored_lang) = stored_lang {
stored_lang
} else if let Some(lang_str) = lang {
Language::guess(Some(lang_str), source)
} else {
Language::guess(Some("plaintext"), source)
};
let theme = self
.decorator_theme()
.or_else(|| self.theme_from_formatter());
let is_linked = matches!(self.formatter_config, ExFormatterOption::HtmlLinked { .. });
let is_multi_themes = matches!(
self.formatter_config,
ExFormatterOption::HtmlMultiThemes { .. }
);
let include_highlights = self.should_include_highlights();
let italic = self.italic_enabled();
let multi_themes_config = self.multi_themes_config();
let mut html_output = String::new();
let mut last_end = 0usize;
highlight_iter(
source,
language,
theme.clone(),
|text, range, scope, _style| {
if range.start > last_end {
let gap = &source[last_end..range.start];
html_output.push_str(gap);
}
if text.trim().is_empty() {
html_output.push_str(text);
} else {
let span = if is_linked {
html::span_linked(text, scope)
} else if is_multi_themes {
if let Some(ref config) = multi_themes_config {
html::span_multi_themes(
text,
scope,
Some(language),
&config.themes,
config.default_theme.as_deref(),
&config.css_variable_prefix,
config.italic,
config.include_highlights,
)
} else {
html::span_inline(
text,
scope,
Some(language),
theme.as_ref(),
italic,
include_highlights,
)
}
} else {
html::span_inline(
text,
scope,
Some(language),
theme.as_ref(),
italic,
include_highlights,
)
};
html_output.push_str(&span);
}
last_end = range.end;
Ok::<(), std::fmt::Error>(())
},
)
.map_err(|_| std::fmt::Error)?;
if last_end < source.len() {
let remaining = &source[last_end..];
html_output.push_str(remaining);
}
let highlight_config = self.highlight_lines_config(&theme);
for (i, line) in html_output.lines().enumerate() {
let line_number = i + 1;
let mut class_name = String::from("line");
let mut style_attr = String::new();
if let Some((ref lines, ref style, ref class)) = highlight_config {
if lines.contains(&line_number) {
if let Some(ref c) = class {
class_name.push(' ');
class_name.push_str(c);
}
if let Some(ref s) = style {
style_attr = format!(" style=\"{}\"", s);
}
}
}
write!(
output,
"<div class=\"{}\"{} data-line=\"{}\">{}\n</div>",
class_name, style_attr, line_number, line
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#[cfg(test)]
use pretty_assertions::assert_str_eq;
use super::*;
use comrak::options::Plugins;
use comrak::{format_html_with_plugins, parse_document, Arena, Options};
fn run_test(markdown: &str, formatter: ExFormatterOption, options: Options) -> String {
let arena = Arena::new();
let root = parse_document(&arena, markdown, &options);
let adapter = LumisAdapter::new(formatter);
let plugins = Plugins {
render: comrak::options::RenderPlugins {
codefence_syntax_highlighter: Some(&adapter),
..Default::default()
},
};
let mut html = String::new();
format_html_with_plugins(root, &options, &mut html, &plugins)
.expect("Failed to format HTML with plugins");
html
}
#[test]
fn test_default_formatter_option() {
let markdown = r#"
```rust
fn main() {
let message = "Hello, world!";
}
```
"#;
let output = run_test(markdown, ExFormatterOption::default(), Options::default());
let expected = r#"<pre class="athl"><code class="language-rust" translate="no" tabindex="0"><div class="line" data-line="1">fn main() {
</div><div class="line" data-line="2"> let message = "Hello, world!";
</div><div class="line" data-line="3">}
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
#[test]
fn test_html_inline_no_attrs() {
let markdown = r#"
```rust
fn main() {
let message = "Hello, world!";
}
```
"#;
let formatter = ExFormatterOption::HtmlInline {
theme: None,
pre_class: None,
italic: false,
include_highlights: false,
highlight_lines: None,
header: None,
};
let output = run_test(markdown, formatter, Options::default());
let expected = r#"<pre class="athl"><code class="language-rust" translate="no" tabindex="0"><div class="line" data-line="1">fn main() {
</div><div class="line" data-line="2"> let message = "Hello, world!";
</div><div class="line" data-line="3">}
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
#[test]
fn test_html_inline_all_attrs() {
let markdown = r#"
```rust
fn main() {
let a = 1;
let b = 2;
let sum = a + b;
}
```
"#;
let formatter = ExFormatterOption::HtmlInline {
theme: Some(ThemeOrString::String("nord".to_string())),
pre_class: Some("custom-pre-class".to_string()),
italic: true,
include_highlights: true,
highlight_lines: None,
header: None,
};
let output = run_test(markdown, formatter, Options::default());
let expected = r#"<pre class="athl custom-pre-class" style="color: #d8dee9; background-color: #2e3440;"><code class="language-rust" translate="no" tabindex="0"><div class="line" data-line="1"><span data-highlight="keyword.function" style="color: #88c0d0; font-style: italic;">fn</span> <span data-highlight="function" style="color: #88c0d0; font-style: italic;">main</span><span data-highlight="punctuation.bracket" style="color: #88c0d0;">(</span><span data-highlight="punctuation.bracket" style="color: #88c0d0;">)</span> <span data-highlight="punctuation.bracket" style="color: #88c0d0;">{</span>
</div><div class="line" data-line="2"> <span data-highlight="keyword" style="color: #81a1c1; font-style: italic;">let</span> <span data-highlight="variable" style="color: #d8dee9; font-weight: bold;">a</span> <span data-highlight="operator" style="color: #81a1c1;">=</span> <span data-highlight="number" style="color: #b48ead;">1</span><span data-highlight="punctuation.delimiter" style="color: #88c0d0;">;</span>
</div><div class="line" data-line="3"> <span data-highlight="keyword" style="color: #81a1c1; font-style: italic;">let</span> <span data-highlight="variable" style="color: #d8dee9; font-weight: bold;">b</span> <span data-highlight="operator" style="color: #81a1c1;">=</span> <span data-highlight="number" style="color: #b48ead;">2</span><span data-highlight="punctuation.delimiter" style="color: #88c0d0;">;</span>
</div><div class="line" data-line="4"> <span data-highlight="keyword" style="color: #81a1c1; font-style: italic;">let</span> <span data-highlight="variable" style="color: #d8dee9; font-weight: bold;">sum</span> <span data-highlight="operator" style="color: #81a1c1;">=</span> <span data-highlight="variable" style="color: #d8dee9; font-weight: bold;">a</span> <span data-highlight="operator" style="color: #81a1c1;">+</span> <span data-highlight="variable" style="color: #d8dee9; font-weight: bold;">b</span><span data-highlight="punctuation.delimiter" style="color: #88c0d0;">;</span>
</div><div class="line" data-line="5"><span data-highlight="punctuation.bracket" style="color: #88c0d0;">}</span>
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
#[test]
fn test_html_inline_decorators() {
let markdown = r#"
```rust pre_class="my-custom-pre extra-class" theme=github_light include_highlights highlight_lines="1,3-5" highlight_lines_style="background-color: #ffffcc; border-left: 3px solid #ff0000" highlight_lines_class="custom-highlight-class"
fn main() {
let x = 1;
let y = 2;
let z = 3;
let message = "Hello, world!";
}
```
"#;
let formatter = ExFormatterOption::HtmlInline {
theme: Some(ThemeOrString::String("nord".to_string())),
pre_class: Some("default-pre-class".to_string()),
italic: true,
include_highlights: false,
highlight_lines: None,
header: None,
};
let mut options = Options::default();
options.render.github_pre_lang = true;
options.render.full_info_string = true;
let output = run_test(markdown, formatter, options);
let expected = r#"<pre class="athl my-custom-pre extra-class" style="color: #1f2328; background-color: #ffffff;"><code class="language-rust" translate="no" tabindex="0"><div class="line custom-highlight-class" style="background-color: #ffffcc; border-left: 3px solid #ff0000" data-line="1"><span data-highlight="keyword.function" style="color: #cf222e;">fn</span> <span data-highlight="function" style="color: #6639ba;">main</span><span data-highlight="punctuation.bracket" style="color: #1f2328;">(</span><span data-highlight="punctuation.bracket" style="color: #1f2328;">)</span> <span data-highlight="punctuation.bracket" style="color: #1f2328;">{</span>
</div><div class="line" data-line="2"> <span data-highlight="keyword" style="color: #cf222e;">let</span> <span data-highlight="variable" style="color: #1f2328;">x</span> <span data-highlight="operator" style="color: #0550ae;">=</span> <span data-highlight="number" style="color: #0550ae;">1</span><span data-highlight="punctuation.delimiter" style="color: #1f2328;">;</span>
</div><div class="line custom-highlight-class" style="background-color: #ffffcc; border-left: 3px solid #ff0000" data-line="3"> <span data-highlight="keyword" style="color: #cf222e;">let</span> <span data-highlight="variable" style="color: #1f2328;">y</span> <span data-highlight="operator" style="color: #0550ae;">=</span> <span data-highlight="number" style="color: #0550ae;">2</span><span data-highlight="punctuation.delimiter" style="color: #1f2328;">;</span>
</div><div class="line custom-highlight-class" style="background-color: #ffffcc; border-left: 3px solid #ff0000" data-line="4"> <span data-highlight="keyword" style="color: #cf222e;">let</span> <span data-highlight="variable" style="color: #1f2328;">z</span> <span data-highlight="operator" style="color: #0550ae;">=</span> <span data-highlight="number" style="color: #0550ae;">3</span><span data-highlight="punctuation.delimiter" style="color: #1f2328;">;</span>
</div><div class="line custom-highlight-class" style="background-color: #ffffcc; border-left: 3px solid #ff0000" data-line="5"> <span data-highlight="keyword" style="color: #cf222e;">let</span> <span data-highlight="variable" style="color: #1f2328;">message</span> <span data-highlight="operator" style="color: #0550ae;">=</span> <span data-highlight="string" style="color: #0a3069;">"Hello, world!"</span><span data-highlight="punctuation.delimiter" style="color: #1f2328;">;</span>
</div><div class="line" data-line="6"><span data-highlight="punctuation.bracket" style="color: #1f2328;">}</span>
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
#[test]
fn test_html_inline_decorators_without_github_pre_lang() {
let markdown = r#"
```rust pre_class="custom-class" highlight_lines="1,3" theme=github_light
fn main() {
let x = 1;
let message = "Hello, world!";
}
```
"#;
let formatter = ExFormatterOption::HtmlInline {
theme: Some(ThemeOrString::String("nord".to_string())),
pre_class: Some("default-class".to_string()),
italic: false,
include_highlights: true,
highlight_lines: None,
header: None,
};
let mut options = Options::default();
options.render.github_pre_lang = false;
options.render.full_info_string = true;
let output = run_test(markdown, formatter, options);
let expected = r#"<pre class="athl default-class" style="color: #d8dee9; background-color: #2e3440;"><code class="language-rust" translate="no" tabindex="0"><div class="line" style="background-color: #e7eaf0;" data-line="1"><span data-highlight="keyword.function" style="color: #cf222e;">fn</span> <span data-highlight="function" style="color: #6639ba;">main</span><span data-highlight="punctuation.bracket" style="color: #1f2328;">(</span><span data-highlight="punctuation.bracket" style="color: #1f2328;">)</span> <span data-highlight="punctuation.bracket" style="color: #1f2328;">{</span>
</div><div class="line" data-line="2"> <span data-highlight="keyword" style="color: #cf222e;">let</span> <span data-highlight="variable" style="color: #1f2328;">x</span> <span data-highlight="operator" style="color: #0550ae;">=</span> <span data-highlight="number" style="color: #0550ae;">1</span><span data-highlight="punctuation.delimiter" style="color: #1f2328;">;</span>
</div><div class="line" style="background-color: #e7eaf0;" data-line="3"> <span data-highlight="keyword" style="color: #cf222e;">let</span> <span data-highlight="variable" style="color: #1f2328;">message</span> <span data-highlight="operator" style="color: #0550ae;">=</span> <span data-highlight="string" style="color: #0a3069;">"Hello, world!"</span><span data-highlight="punctuation.delimiter" style="color: #1f2328;">;</span>
</div><div class="line" data-line="4"><span data-highlight="punctuation.bracket" style="color: #1f2328;">}</span>
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
#[test]
fn test_html_linked_no_attrs() {
let markdown = r#"
```rust
fn main() {
let message = "Hello, world!";
}
```
"#;
let formatter = ExFormatterOption::HtmlLinked {
pre_class: None,
highlight_lines: None,
header: None,
};
let output = run_test(markdown, formatter, Options::default());
let expected = r#"<pre class="athl"><code class="language-rust" translate="no" tabindex="0"><div class="line" data-line="1"><span class="keyword-function">fn</span> <span class="function">main</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="punctuation-bracket">{</span>
</div><div class="line" data-line="2"> <span class="keyword">let</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">"Hello, world!"</span><span class="punctuation-delimiter">;</span>
</div><div class="line" data-line="3"><span class="punctuation-bracket">}</span>
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
#[test]
fn test_html_linked_all_attrs() {
let markdown = r#"
```rust
fn main() {
let a = 1;
let b = 2;
let sum = a + b;
}
```
"#;
let formatter = ExFormatterOption::HtmlLinked {
pre_class: Some("custom-linked-class".to_string()),
highlight_lines: None,
header: None,
};
let output = run_test(markdown, formatter, Options::default());
let expected = r#"<pre class="athl custom-linked-class"><code class="language-rust" translate="no" tabindex="0"><div class="line" data-line="1"><span class="keyword-function">fn</span> <span class="function">main</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="punctuation-bracket">{</span>
</div><div class="line" data-line="2"> <span class="keyword">let</span> <span class="variable">a</span> <span class="operator">=</span> <span class="number">1</span><span class="punctuation-delimiter">;</span>
</div><div class="line" data-line="3"> <span class="keyword">let</span> <span class="variable">b</span> <span class="operator">=</span> <span class="number">2</span><span class="punctuation-delimiter">;</span>
</div><div class="line" data-line="4"> <span class="keyword">let</span> <span class="variable">sum</span> <span class="operator">=</span> <span class="variable">a</span> <span class="operator">+</span> <span class="variable">b</span><span class="punctuation-delimiter">;</span>
</div><div class="line" data-line="5"><span class="punctuation-bracket">}</span>
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
#[test]
fn test_html_linked_decorators() {
let markdown = r#"
```rust pre_class="custom-linked-pre extra-linked" highlight_lines="1-2,4-5" highlight_lines_class="my-custom-highlight-line"
fn main() {
let x = 1;
let y = 2;
let z = 3;
let message = "Hello, world!";
}
```
"#;
let formatter = ExFormatterOption::HtmlLinked {
pre_class: Some("default-linked-pre".to_string()),
highlight_lines: None,
header: None,
};
let mut options = Options::default();
options.render.github_pre_lang = true;
options.render.full_info_string = true;
let output = run_test(markdown, formatter, options);
let expected = r#"<pre class="athl custom-linked-pre extra-linked"><code class="language-rust" translate="no" tabindex="0"><div class="line my-custom-highlight-line" data-line="1"><span class="keyword-function">fn</span> <span class="function">main</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="punctuation-bracket">{</span>
</div><div class="line my-custom-highlight-line" data-line="2"> <span class="keyword">let</span> <span class="variable">x</span> <span class="operator">=</span> <span class="number">1</span><span class="punctuation-delimiter">;</span>
</div><div class="line" data-line="3"> <span class="keyword">let</span> <span class="variable">y</span> <span class="operator">=</span> <span class="number">2</span><span class="punctuation-delimiter">;</span>
</div><div class="line my-custom-highlight-line" data-line="4"> <span class="keyword">let</span> <span class="variable">z</span> <span class="operator">=</span> <span class="number">3</span><span class="punctuation-delimiter">;</span>
</div><div class="line my-custom-highlight-line" data-line="5"> <span class="keyword">let</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">"Hello, world!"</span><span class="punctuation-delimiter">;</span>
</div><div class="line" data-line="6"><span class="punctuation-bracket">}</span>
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
#[test]
fn test_decorator_include_highlights_with_value() {
let markdown = r#"
```rust include_highlights=true
fn main() {
let message = "Hello, world!";
}
```
"#;
let formatter = ExFormatterOption::HtmlInline {
theme: Some(ThemeOrString::String("nord".to_string())),
pre_class: None,
italic: false,
include_highlights: false,
highlight_lines: None,
header: None,
};
let mut options = Options::default();
options.render.github_pre_lang = true;
options.render.full_info_string = true;
let output = run_test(markdown, formatter, options);
let expected = r#"<pre class="athl" style="color: #d8dee9; background-color: #2e3440;"><code class="language-rust" translate="no" tabindex="0"><div class="line" data-line="1"><span data-highlight="keyword.function" style="color: #88c0d0;">fn</span> <span data-highlight="function" style="color: #88c0d0;">main</span><span data-highlight="punctuation.bracket" style="color: #88c0d0;">(</span><span data-highlight="punctuation.bracket" style="color: #88c0d0;">)</span> <span data-highlight="punctuation.bracket" style="color: #88c0d0;">{</span>
</div><div class="line" data-line="2"> <span data-highlight="keyword" style="color: #81a1c1;">let</span> <span data-highlight="variable" style="color: #d8dee9; font-weight: bold;">message</span> <span data-highlight="operator" style="color: #81a1c1;">=</span> <span data-highlight="string" style="color: #a3be8c;">"Hello, world!"</span><span data-highlight="punctuation.delimiter" style="color: #88c0d0;">;</span>
</div><div class="line" data-line="3"><span data-highlight="punctuation.bracket" style="color: #88c0d0;">}</span>
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
#[test]
fn test_decorator_include_highlights_standalone() {
let markdown = r#"
```rust include_highlights
fn main() {
let message = "Hello, world!";
}
```
"#;
let formatter = ExFormatterOption::HtmlInline {
theme: Some(ThemeOrString::String("nord".to_string())),
pre_class: None,
italic: false,
include_highlights: false,
highlight_lines: None,
header: None,
};
let mut options = Options::default();
options.render.github_pre_lang = true;
options.render.full_info_string = true;
let output = run_test(markdown, formatter, options);
let expected = r#"<pre class="athl" style="color: #d8dee9; background-color: #2e3440;"><code class="language-rust" translate="no" tabindex="0"><div class="line" data-line="1"><span data-highlight="keyword.function" style="color: #88c0d0;">fn</span> <span data-highlight="function" style="color: #88c0d0;">main</span><span data-highlight="punctuation.bracket" style="color: #88c0d0;">(</span><span data-highlight="punctuation.bracket" style="color: #88c0d0;">)</span> <span data-highlight="punctuation.bracket" style="color: #88c0d0;">{</span>
</div><div class="line" data-line="2"> <span data-highlight="keyword" style="color: #81a1c1;">let</span> <span data-highlight="variable" style="color: #d8dee9; font-weight: bold;">message</span> <span data-highlight="operator" style="color: #81a1c1;">=</span> <span data-highlight="string" style="color: #a3be8c;">"Hello, world!"</span><span data-highlight="punctuation.delimiter" style="color: #88c0d0;">;</span>
</div><div class="line" data-line="3"><span data-highlight="punctuation.bracket" style="color: #88c0d0;">}</span>
</div></code></pre>"#;
assert_str_eq!(output.trim(), expected.trim());
}
}