use std::sync::LazyLock;
use regex::Regex;
use textlane_core::hast::{Element, Node, PropertyItem, PropertyValue};
use textlane_core::{Allocator, Data, Error};
use crate::actions::code::highlight::highlight_code;
use crate::actions::code::tokenize::is_plain_language;
use crate::actions::theme::resolve::resolve_themes;
use crate::helpers::registry::{INJECTION_TARGETS, load_registry};
use crate::models::highlight::Themes;
use crate::models::options::{Highlight, InlineCode};
pub fn highlight_hast<'a>(
node: &mut Node<'a>,
options: &Highlight,
alloc: &'a Allocator,
) -> Result<(), Error> {
let mut themes: Option<Themes> = None;
let mut stack: Vec<&mut Node<'a>> = vec![node];
while let Some(node) = stack.pop() {
let children = match node {
Node::Root(root) => &mut root.children,
Node::Element(element) => &mut element.children,
_ => continue,
};
for child in children.iter_mut() {
let found = match child {
Node::Element(element) => read_block(element, options)
.map(|block| (block, false))
.or_else(|| read_inline(element, options).map(|block| (block, true))),
_ => None,
};
let Some(((code, language, meta), inline)) = found else {
stack.push(child);
continue;
};
if themes.is_none() {
themes = Some(resolve_themes(options)?);
}
if let Some(themes) = &themes {
let mut highlighted =
highlight_code(&code, &language, &meta, themes, options, alloc)?;
if let (true, Node::Element(element)) = (inline, &mut highlighted) {
element.tag_name = "span";
}
*child = highlighted;
}
}
}
Ok(())
}
fn read_inline(code: &Element, options: &Highlight) -> Option<(String, String, String)> {
if code.tag_name != "code" || options.inline != Some(InlineCode::TailingCurlyColon) {
return None;
}
let mut text = String::new();
collect_text(&code.children, &mut text);
let captures = TAILING_CURLY_COLON.as_ref()?.captures(&text)?;
let named = captures.get(2)?.as_str();
let language = match is_known_language(named) {
true => named.to_owned(),
false => options.fallback_language.clone()?,
};
Some((
captures.get(1)?.as_str().to_owned(),
language,
String::new(),
))
}
static TAILING_CURLY_COLON: LazyLock<Option<Regex>> =
LazyLock::new(|| Regex::new(r"^(.+)\{:([A-Za-z0-9_-]+)\}$").ok());
fn read_block(pre: &Element, options: &Highlight) -> Option<(String, String, String)> {
if pre.tag_name != "pre" {
return None;
}
let Some(Node::Element(code)) = pre.children.first() else {
return None;
};
if code.tag_name != "code" {
return None;
}
let named = read_language(code).filter(|language| !language.is_empty());
let language = match named {
None => options.default_language.clone()?,
Some(language) if is_known_language(language) => language.to_owned(),
Some(_) => options.fallback_language.clone()?,
};
let mut text = String::new();
collect_text(&code.children, &mut text);
if text.ends_with('\n') {
text.pop();
}
Some((text, language, read_meta(code)))
}
fn read_meta(code: &Element) -> String {
if let Some(Data::Object(entries)) = code.data
&& let Some((_, Data::String(meta))) =
entries.iter().find(|(key, _)| *key == "meta")
{
return (*meta).to_owned();
}
let property = code
.properties
.iter()
.find(|property| property.name == "metastring");
match property.map(|property| &property.value) {
Some(PropertyValue::String(meta)) => (*meta).to_owned(),
_ => String::new(),
}
}
fn read_language<'a>(code: &Element<'a>) -> Option<&'a str> {
let property = code
.properties
.iter()
.find(|property| property.name == "className")?;
let PropertyValue::List(items) = &property.value else {
return None;
};
items.iter().find_map(|item| match item {
PropertyItem::String(name) => name.strip_prefix("language-"),
PropertyItem::Number(_) => None,
})
}
fn is_known_language(language: &str) -> bool {
is_plain_language(language)
|| language == "ansi"
|| (!language.chars().any(char::is_uppercase)
&& !INJECTION_TARGETS
.iter()
.any(|(scope, _)| *scope == language)
&& load_registry()
.is_some_and(|registry| registry.contains_grammar(language)))
}
fn collect_text(children: &[Node], text: &mut String) {
let mut stack: Vec<std::slice::Iter<Node>> = vec![children.iter()];
while let Some(iter) = stack.last_mut() {
match iter.next() {
Some(Node::Text(node)) => text.push_str(node.value),
Some(Node::Element(element)) => stack.push(element.children.iter()),
Some(Node::Root(root)) => stack.push(root.children.iter()),
Some(_) => {}
None => {
stack.pop();
}
}
}
}
#[cfg(test)]
#[path = "highlight_unit.rs"]
mod unit;