use textlane_core::hast::{Element, Node, Property, PropertyItem, PropertyValue, Text};
use textlane_core::{Allocator, ArenaVec, Error};
use crate::actions::code::decorate::{apply_decorations, read_decorations, split_tokens};
use crate::actions::code::tokenize::{replace_color, tokenize_code};
use crate::actions::code::transform::transform_code;
use crate::models::highlight::{CodeBlock, CodeLine, Themes, add_classes};
use crate::models::options::Highlight;
pub(crate) fn highlight_code<'a>(
code: &str,
language: &str,
meta: &str,
themes: &Themes,
options: &Highlight,
alloc: &'a Allocator,
) -> Result<Node<'a>, Error> {
let max_line_length = options.tokenize_max_line_length.unwrap_or(2_000);
let lines = tokenize_code(code, language, themes, max_line_length)?;
let mut block = CodeBlock {
lines: lines
.into_iter()
.map(|tokens| CodeLine {
classes: vec!["line".to_owned()],
tokens: tokens.into_iter().map(Some).collect(),
removed: false,
})
.collect(),
language: language.to_owned(),
meta: meta.to_owned(),
pre_classes: Vec::new(),
code_classes: Vec::new(),
};
let mut decorations = read_decorations(code, meta, &options.transformers)?;
if !decorations.is_empty() {
split_tokens(&mut block, code, &decorations);
}
transform_code(&mut block, &options.transformers);
if options.add_language_class {
add_classes(&mut block.code_classes, &[format!("language-{language}")]);
}
let last = block.lines.len().saturating_sub(1);
let mut children = ArenaVec::with_capacity_in(block.lines.len() * 2, alloc);
for (index, line) in block.lines.iter().enumerate() {
if line.removed {
continue;
}
let tokens = line.tokens.iter().flatten();
let mut spans = ArenaVec::with_capacity_in(line.tokens.len(), alloc);
for token in tokens {
let mut properties = ArenaVec::new_in(alloc);
if !token.style.is_empty() {
properties.push(create_property("style", &token.style, alloc));
}
if !token.classes.is_empty() {
properties.push(create_class(&token.classes, alloc));
}
let mut text = ArenaVec::with_capacity_in(1, alloc);
text.push(create_text(&token.text, alloc));
spans.push(create_element("span", properties, text));
}
let mut properties = ArenaVec::with_capacity_in(1, alloc);
properties.push(create_class(&line.classes, alloc));
children.push(create_element("span", properties, spans));
if index < last {
children.push(create_text("\n", alloc));
}
}
let mut code_properties = ArenaVec::new_in(alloc);
if !block.code_classes.is_empty() {
code_properties.push(create_class(&block.code_classes, alloc));
}
if !decorations.is_empty() {
apply_decorations(&mut children, code, &mut decorations, alloc)?;
}
let mut code_element = ArenaVec::with_capacity_in(1, alloc);
code_element.push(create_element("code", code_properties, children));
let mut properties = ArenaVec::with_capacity_in(4, alloc);
let names = themes.list.iter().map(|theme| theme.colors.name.to_owned());
let mut class: Vec<String> = match themes.single {
true => ["shiki".to_owned()].into_iter().chain(names).collect(),
false => ["shiki".to_owned(), "shiki-themes".to_owned()]
.into_iter()
.chain(names)
.collect(),
};
add_classes(&mut class, &block.pre_classes);
properties.push(create_class(&class, alloc));
properties.push(create_property(
"style",
&stringify_root_style(themes),
alloc,
));
properties.push(Property {
name: "tabIndex",
value: PropertyValue::Number(0.0),
});
if let Some(name) = options
.meta_property
.as_deref()
.filter(|_| !meta.is_empty())
{
properties.push(create_property(alloc.alloc_str(name), meta, alloc));
}
Ok(create_element("pre", properties, code_element))
}
fn stringify_root_style(themes: &Themes) -> String {
let mut style = String::new();
for (property, variable) in [("background-color", "-bg"), ("color", "")] {
for (index, theme) in themes.list.iter().enumerate() {
let value = match variable {
"-bg" => theme.colors.bg,
_ => theme.colors.fg,
};
let value = replace_color(value.to_owned(), theme);
if !style.is_empty() {
style.push(';');
}
match index {
0 => style.push_str(&format!("{property}:{value}")),
_ => style.push_str(&format!("--shiki-{}{variable}:{value}", theme.mode)),
}
}
}
style
}
fn create_element<'a>(
tag_name: &'a str,
properties: ArenaVec<'a, Property<'a>>,
children: ArenaVec<'a, Node<'a>>,
) -> Node<'a> {
Node::Element(Element {
tag_name,
properties,
children,
position: None,
data: None,
content: None,
})
}
fn create_text<'a>(value: &str, alloc: &'a Allocator) -> Node<'a> {
Node::Text(Text {
value: alloc.alloc_str(value),
position: None,
data: None,
})
}
fn create_property<'a>(name: &'a str, value: &str, alloc: &'a Allocator) -> Property<'a> {
Property {
name,
value: PropertyValue::String(alloc.alloc_str(value)),
}
}
fn create_class<'a>(names: &[String], alloc: &'a Allocator) -> Property<'a> {
let mut items = ArenaVec::with_capacity_in(names.len(), alloc);
for name in names {
items.push(PropertyItem::String(alloc.alloc_str(name)));
}
Property {
name: "className",
value: PropertyValue::List(items),
}
}