#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HighlightResult {
pub html: String,
pub language_matched: bool,
}
#[cfg(feature = "syntax-highlighting")]
mod inner {
use super::HighlightResult;
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use syntect::highlighting::ThemeSet;
use syntect::html::ClassedHTMLGenerator;
use syntect::parsing::SyntaxSet;
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
static PREFIX_CACHE: LazyLock<Mutex<HashMap<String, &'static str>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn static_prefix(prefix: &str) -> &'static str {
let mut cache = PREFIX_CACHE.lock().unwrap();
if let Some(&s) = cache.get(prefix) {
return s;
}
let leaked: &'static str = Box::leak(prefix.to_string().into_boxed_str());
cache.insert(prefix.to_string(), leaked);
leaked
}
pub fn highlight_code(code: &str, lang: &str, class_prefix: &str) -> HighlightResult {
let syntax = if lang.is_empty() {
None
} else {
SYNTAX_SET
.find_syntax_by_token(lang)
.or_else(|| SYNTAX_SET.find_syntax_by_name(lang))
};
let Some(syntax) = syntax else {
return HighlightResult {
html: html_escape(code),
language_matched: false,
};
};
let class_style = if class_prefix.is_empty() {
syntect::html::ClassStyle::Spaced
} else {
syntect::html::ClassStyle::SpacedPrefixed {
prefix: static_prefix(class_prefix),
}
};
let mut generator =
ClassedHTMLGenerator::new_with_class_style(syntax, &SYNTAX_SET, class_style);
for line in syntect::util::LinesWithEndings::from(code) {
if generator
.parse_html_for_line_which_includes_newline(line)
.is_err()
{
return HighlightResult {
html: html_escape(code),
language_matched: false,
};
}
}
HighlightResult {
html: generator.finalize(),
language_matched: true,
}
}
pub fn generate_theme_css(theme_name: &str, class_prefix: &str) -> Option<String> {
let theme = THEME_SET.themes.get(theme_name)?;
let class_style = if class_prefix.is_empty() {
syntect::html::ClassStyle::Spaced
} else {
syntect::html::ClassStyle::SpacedPrefixed {
prefix: static_prefix(class_prefix),
}
};
syntect::html::css_for_theme_with_class_style(theme, class_style).ok()
}
pub fn supported_languages() -> Vec<&'static str> {
SYNTAX_SET
.syntaxes()
.iter()
.flat_map(|s| s.file_extensions.iter().map(|e| e.as_str()))
.collect()
}
fn html_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(ch),
}
}
out
}
}
#[cfg(not(feature = "syntax-highlighting"))]
mod inner {
use super::HighlightResult;
pub fn highlight_code(code: &str, _lang: &str, _class_prefix: &str) -> HighlightResult {
HighlightResult {
html: html_escape(code),
language_matched: false,
}
}
pub fn generate_theme_css(_theme_name: &str, _class_prefix: &str) -> Option<String> {
None
}
pub fn supported_languages() -> Vec<&'static str> {
Vec::new()
}
fn html_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(ch),
}
}
out
}
}
pub use inner::{generate_theme_css, highlight_code, supported_languages};
pub fn wrap_with_line_numbers(html: &str) -> String {
let lines: Vec<&str> = html.split('\n').collect();
let line_count = if lines.last() == Some(&"") && lines.len() > 1 {
lines.len() - 1
} else {
lines.len()
};
let mut out = String::with_capacity(html.len() + line_count * 100);
for (i, line) in lines.iter().take(line_count).enumerate() {
let num = i + 1;
out.push_str(&format!(
"<span class=\"code-line\" data-line-number=\"{num}\"><span data-md-line-gutter aria-hidden=\"true\" style=\"user-select:none\">{num}</span>{line}</span>\n",
));
}
if out.ends_with('\n') {
out.pop();
}
out
}