pub mod formatter;
pub mod highlight;
pub mod languages;
pub mod themes;
mod vendor;
pub use lumis_core::events;
pub use lumis_core::highlights;
pub use formatter::ansi;
pub use formatter::html;
use crate::formatter::Formatter;
use std::io::{self, Write};
pub use crate::formatter::{
BBCodeScopedBuilder, HtmlInlineBuilder, HtmlLinkedBuilder, HtmlMultiThemesBuilder,
TerminalBuilder,
};
pub fn highlight<F: Formatter>(source: &str, formatter: F) -> String {
let mut buffer = Vec::new();
formatter
.format(source, &mut buffer)
.expect("formatter failed to format source code");
String::from_utf8(buffer).expect("formatter produced invalid UTF-8")
}
pub fn write_highlight<F: Formatter>(
output: &mut dyn Write,
source: &str,
formatter: F,
) -> io::Result<()> {
formatter.format(source, output)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::languages::Language;
#[test]
fn test_write_highlight() {
let code = r#"const = 1"#;
let expected = r#"<pre class="lumis" style="color: #c6d0f5; background-color: #303446;"><code class="language-javascript" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #ca9ee6;">const</span> <span style="color: #99d1db;">=</span> <span style="color: #ef9f76;">1</span>
</div></code></pre>"#;
let mut buffer = Vec::new();
let formatter = HtmlInlineBuilder::default()
.lang(Language::JavaScript)
.theme(themes::get("catppuccin_frappe").ok())
.build()
.unwrap();
write_highlight(&mut buffer, code, formatter).unwrap();
let result = String::from_utf8(buffer).unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_highlight_html_inline() {
let code = r#"defmodule Foo do
@moduledoc """
Test Module
"""
@projects ["Phoenix", "MDEx"]
def projects, do: @projects
end
"#;
let expected = r#"<pre class="lumis" style="color: #c6d0f5; background-color: #303446;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #ca9ee6;">defmodule</span> <span style="color: #e5c890;">Foo</span> <span style="color: #ca9ee6;">do</span>
</div><div class="line" data-line="2"> <span style="color: #99d1db;"><span style="color: #949cbb;"><span style="color: #949cbb;">@</span><span style="color: #949cbb;">moduledoc</span> <span style="color: #949cbb;">"""</span></span></span>
</div><div class="line" data-line="3"><span style="color: #99d1db;"><span style="color: #949cbb;"><span style="color: #949cbb;"> Test Module</span></span></span>
</div><div class="line" data-line="4"><span style="color: #99d1db;"><span style="color: #949cbb;"><span style="color: #949cbb;"> """</span></span></span>
</div><div class="line" data-line="5">
</div><div class="line" data-line="6"> <span style="color: #99d1db;"><span style="color: #ef9f76;">@<span style="color: #8caaee;"><span style="color: #ef9f76;">projects <span style="color: #949cbb;">[</span><span style="color: #a6d189;">"Phoenix"</span><span style="color: #949cbb;">,</span> <span style="color: #a6d189;">"MDEx"</span><span style="color: #949cbb;">]</span></span></span></span></span>
</div><div class="line" data-line="7">
</div><div class="line" data-line="8"> <span style="color: #ca9ee6;">def</span> <span style="color: #8caaee;">projects</span><span style="color: #949cbb;">,</span> <span style="color: #eebebe;">do: </span><span style="color: #99d1db;"><span style="color: #ef9f76;">@<span style="color: #ef9f76;">projects</span></span></span>
</div><div class="line" data-line="9"><span style="color: #ca9ee6;">end</span>
</div><div class="line" data-line="10">
</div></code></pre>"#;
let formatter = HtmlInlineBuilder::default()
.lang(Language::Elixir)
.theme(themes::get("catppuccin_frappe").ok())
.build()
.unwrap();
let result = highlight(code, formatter);
assert_eq!(result, expected);
}
#[test]
fn test_highlight_html_inline_include_highlights() {
let code = r#"defmodule Foo do
@lang :elixir
end
"#;
let expected = r#"<pre class="lumis" style="color: #c6d0f5; background-color: #303446;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span data-highlight="keyword.function" style="color: #ca9ee6;">defmodule</span> <span data-highlight="module" style="color: #e5c890;">Foo</span> <span data-highlight="keyword" style="color: #ca9ee6;">do</span>
</div><div class="line" data-line="2"> <span data-highlight="operator" style="color: #99d1db;"><span data-highlight="constant" style="color: #ef9f76;">@<span data-highlight="function.call" style="color: #8caaee;"><span data-highlight="constant" style="color: #ef9f76;">lang <span data-highlight="string.special.symbol" style="color: #eebebe;">:elixir</span></span></span></span></span>
</div><div class="line" data-line="3"><span data-highlight="keyword" style="color: #ca9ee6;">end</span>
</div><div class="line" data-line="4">
</div></code></pre>"#;
let formatter = HtmlInlineBuilder::default()
.lang(Language::Elixir)
.include_highlights(true)
.theme(themes::get("catppuccin_frappe").ok())
.build()
.unwrap();
let result = highlight(code, formatter);
assert_eq!(result, expected);
}
#[test]
fn test_highlight_html_inline_escape_curly_braces() {
let code = "{:ok, char: '{'}";
let expected = r#"<pre class="lumis" style="color: #c6d0f5; background-color: #303446;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #949cbb;">{</span><span style="color: #eebebe;">:ok</span><span style="color: #949cbb;">,</span> <span style="color: #eebebe;">char: </span><span style="color: #81c8be;">'{'</span><span style="color: #949cbb;">}</span>
</div></code></pre>"#;
let formatter = HtmlInlineBuilder::default()
.lang(Language::Elixir)
.theme(themes::get("catppuccin_frappe").ok())
.build()
.unwrap();
let result = highlight(code, formatter);
assert_eq!(result, expected);
}
#[test]
fn test_highlight_html_linked() {
let code = r#"defmodule Foo do
@moduledoc """
Test Module
"""
@projects ["Phoenix", "MDEx"]
def projects, do: @projects
end
"#;
let expected = r#"<pre class="lumis"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span class="keyword-function">defmodule</span> <span class="module">Foo</span> <span class="keyword">do</span>
</div><div class="line" data-line="2"> <span class="operator"><span class="comment-documentation"><span class="comment">@</span><span class="comment">moduledoc</span> <span class="comment">"""</span></span></span>
</div><div class="line" data-line="3"><span class="operator"><span class="comment-documentation"><span class="comment"> Test Module</span></span></span>
</div><div class="line" data-line="4"><span class="operator"><span class="comment-documentation"><span class="comment"> """</span></span></span>
</div><div class="line" data-line="5">
</div><div class="line" data-line="6"> <span class="operator"><span class="constant">@<span class="function-call"><span class="constant">projects <span class="punctuation-bracket">[</span><span class="string">"Phoenix"</span><span class="punctuation-delimiter">,</span> <span class="string">"MDEx"</span><span class="punctuation-bracket">]</span></span></span></span></span>
</div><div class="line" data-line="7">
</div><div class="line" data-line="8"> <span class="keyword-function">def</span> <span class="function">projects</span><span class="punctuation-delimiter">,</span> <span class="string-special-symbol">do: </span><span class="operator"><span class="constant">@<span class="constant">projects</span></span></span>
</div><div class="line" data-line="9"><span class="keyword">end</span>
</div><div class="line" data-line="10">
</div></code></pre>"#;
let formatter = HtmlLinkedBuilder::default()
.lang(Language::Elixir)
.build()
.unwrap();
let result = highlight(code, formatter);
assert_eq!(result, expected);
}
#[test]
fn test_highlight_html_linked_escape_curly_braces() {
let code = "{:ok, char: '{'}";
let expected = r#"<pre class="lumis"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span class="punctuation-bracket">{</span><span class="string-special-symbol">:ok</span><span class="punctuation-delimiter">,</span> <span class="string-special-symbol">char: </span><span class="character">'{'</span><span class="punctuation-bracket">}</span>
</div></code></pre>"#;
let formatter = HtmlLinkedBuilder::default()
.lang(Language::Elixir)
.build()
.unwrap();
let result = highlight(code, formatter);
assert_eq!(result, expected);
}
#[test]
fn test_guess_language_by_file_name() {
let code = "foo = 1";
let formatter = HtmlInlineBuilder::default()
.lang(Language::Elixir)
.theme(themes::get("catppuccin_frappe").ok())
.build()
.unwrap();
let result = highlight(code, formatter);
assert!(result.contains("language-elixir"));
}
#[test]
fn test_guess_language_by_file_extension() {
let code1 = "# Title";
let formatter1 = HtmlInlineBuilder::default()
.lang(Language::Markdown)
.theme(themes::get("catppuccin_frappe").ok())
.build()
.unwrap();
let result = highlight(code1, formatter1);
assert!(result.contains("language-markdown"));
let code2 = "foo = 1";
let formatter2 = HtmlInlineBuilder::default()
.lang(Language::Elixir)
.theme(themes::get("catppuccin_frappe").ok())
.build()
.unwrap();
let result = highlight(code2, formatter2);
assert!(result.contains("language-elixir"));
}
#[test]
fn test_guess_language_by_shebang() {
let code = "#!/usr/bin/env elixir";
let formatter = HtmlInlineBuilder::default()
.lang(Language::Elixir)
.theme(themes::get("catppuccin_frappe").ok())
.build()
.unwrap();
let result = highlight(code, formatter);
assert!(result.contains("language-elixir"));
}
#[test]
fn test_fallback_to_plain_text() {
let code = "source code";
let formatter = HtmlInlineBuilder::default()
.lang(Language::PlainText)
.theme(themes::get("catppuccin_frappe").ok())
.build()
.unwrap();
let result = highlight(code, formatter);
assert!(result.contains("language-plaintext"));
}
#[test]
fn test_highlight_terminal() {
let code = "puts 'Hello from Ruby!'";
let formatter = TerminalBuilder::default()
.lang(Language::Ruby)
.theme(themes::get("dracula").ok())
.build()
.unwrap();
let ansi = highlight(code, formatter);
assert!(ansi.contains("[38;2;241;250;140mHello from Ruby!"));
}
#[test]
fn test_formatter_option_with_header() {
let code = "fn main() { println!(\"Hello\"); }";
let inline_formatter = HtmlInlineBuilder::default()
.lang(Language::Rust)
.header(Some(formatter::HtmlElement {
open_tag: "<div class=\"code-container\">".to_string(),
close_tag: "</div>".to_string(),
}))
.build()
.unwrap();
let inline_result = highlight(code, inline_formatter);
assert!(inline_result.starts_with("<div class=\"code-container\">"));
assert!(inline_result.ends_with("</div>"));
assert!(inline_result.contains("<pre class=\"lumis\">"));
let linked_formatter = HtmlLinkedBuilder::default()
.lang(Language::Rust)
.header(Some(formatter::HtmlElement {
open_tag: "<section class=\"code-section\">".to_string(),
close_tag: "</section>".to_string(),
}))
.build()
.unwrap();
let linked_result = highlight(code, linked_formatter);
assert!(linked_result.starts_with("<section class=\"code-section\">"));
assert!(linked_result.ends_with("</section>"));
assert!(linked_result.contains("<pre class=\"lumis\">"));
}
}