use katex::macros::MacroDefinition;
use katex::{
KatexContext, OutputFormat, Settings, StrictMode, StrictSetting, TrustSetting,
};
use textlane_core::hast::{Element, Node, Property, PropertyItem, PropertyValue, Text};
use textlane_core::{Allocator, ArenaVec, File, Message, UnistPosition};
use crate::actions::html::parse::parse_html;
use crate::models::options::{Katex, KatexOutput, KatexStrict, KatexStrictMode, Parse};
const LEVEL_STACK: usize = 64 * 1024;
const MAX_LEVELS: usize = 4096;
const DEFINITIONS: [&str; 8] = [
"def",
"gdef",
"edef",
"xdef",
"newcommand",
"renewcommand",
"providecommand",
"DeclareMathOperator",
];
pub(crate) fn create_settings(options: &Katex) -> Settings {
let macros = options
.macros
.iter()
.map(|(name, body)| (name.clone(), MacroDefinition::String(body.clone())))
.collect();
Settings::builder()
.maybe_output(options.output.map(|output| match output {
KatexOutput::Html => OutputFormat::Html,
KatexOutput::Mathml => OutputFormat::Mathml,
KatexOutput::HtmlAndMathml => OutputFormat::HtmlAndMathml,
}))
.leqno(options.leqno)
.fleqn(options.fleqn)
.throw_on_error(true)
.maybe_error_color(options.error_color.clone())
.macros(macros)
.maybe_min_rule_thickness(options.min_rule_thickness)
.color_is_text_color(options.color_is_text_color)
.maybe_strict(options.strict.as_ref().map(|strict| match strict {
KatexStrict::Boolean(strict) => StrictSetting::Bool(*strict),
KatexStrict::Mode(mode) => StrictSetting::Mode(match mode {
KatexStrictMode::Ignore => StrictMode::Ignore,
KatexStrictMode::Warn => StrictMode::Warn,
KatexStrictMode::Error => StrictMode::Error,
}),
}))
.trust(TrustSetting::Bool(options.trust))
.maybe_max_size(options.max_size)
.maybe_max_expand(options.max_expand)
.global_group(options.global_group)
.build()
}
pub(crate) fn typeset_math<'a>(
value: &str,
display: bool,
context: &KatexContext,
settings: &mut Settings,
place: Option<UnistPosition>,
file: &mut File,
alloc: &'a Allocator,
) -> ArenaVec<'a, Node<'a>> {
let message = |rule: &str| Message {
reason: "Could not render math with KaTeX".to_string(),
source: Some("rehype-katex".to_string()),
rule: Some(rule.to_string()),
place,
};
let Some((levels, max_expand)) = measure_levels(value, settings) else {
file.messages.push(message("rangeerror"));
let title = "RangeError: Maximum call stack size exceeded";
return create_error(value, title, settings, alloc);
};
let expand = std::mem::replace(&mut settings.max_expand, max_expand);
settings.display_mode = display;
let render = |settings: &Settings| {
let stack = levels * LEVEL_STACK;
stacker::maybe_grow(stack, MAX_LEVELS * LEVEL_STACK, || {
katex::render_to_string(context, value, settings)
})
};
let result = match render(settings) {
Ok(html) => Ok(html),
Err(_) => {
file.messages.push(message("parseerror"));
let strict = std::mem::replace(
&mut settings.strict,
StrictSetting::Mode(StrictMode::Ignore),
);
settings.throw_on_error = false;
let fallback = render(settings);
settings.throw_on_error = true;
let fallback = match fallback {
Ok(html) if html.starts_with("<span class=\"katex-error\"") => {
render(settings).map_err(|error| error.to_string())
}
fallback => fallback.map_err(|error| error.to_string()),
};
settings.strict = strict;
fallback
}
};
settings.max_expand = expand;
match result {
Ok(html) => parse_markup(&html, alloc),
Err(error) => {
create_error(value, &format!("ParseError: {error}"), settings, alloc)
}
}
}
fn measure_levels(value: &str, settings: &Settings) -> Option<(usize, usize)> {
let (depth, defines) = measure_depth(value);
let levels = depth + 64;
let room = MAX_LEVELS.checked_sub(levels)?;
let macros = settings.macros.borrow();
if !defines && macros.is_empty() {
return Some((levels, settings.max_expand));
}
let body = macros
.values()
.filter_map(|definition| match definition {
MacroDefinition::String(body) => Some(measure_depth(body).0),
MacroDefinition::Expansion(expansion) => {
let tokens: Vec<&str> = expansion
.tokens
.iter()
.rev()
.map(|token| &*token.text)
.collect();
Some(measure_depth(&tokens.join(" ")).0)
}
_ => None,
})
.fold(depth, usize::max)
.max(1);
let max_expand = settings.max_expand.min(room / body);
Some((levels + max_expand * body, max_expand))
}
#[derive(Clone, Copy, PartialEq)]
enum Group {
Brace,
Left,
Begin,
}
fn measure_depth(source: &str) -> (usize, bool) {
let bytes = source.as_bytes();
let mut groups: Vec<(Group, usize)> = Vec::new();
let (mut level, mut deepest, mut defines) = (0usize, 0usize, false);
let open = |groups: &mut Vec<(Group, usize)>, level: &mut usize, group| {
groups.push((group, *level));
*level += 4;
};
let close = |groups: &mut Vec<(Group, usize)>, level: &mut usize, group| {
let Some(at) = groups.iter().rposition(|(open, _)| *open == group) else {
return;
};
if group == Group::Brace || at + 1 == groups.len() {
*level = groups[at].1;
groups.truncate(at);
}
};
let mut hidden = 0;
let mut ends = [(0, 0); 2];
let mut find_end = |from: usize, verb: bool| {
let (start, end) = &mut ends[usize::from(verb)];
if from >= *end || from < *start {
let found = match verb {
true => memchr::memchr2(b'\n', b'\r', &bytes[from..]),
false => memchr::memchr(b'\n', &bytes[from..]),
};
(*start, *end) = (from, found.map_or(bytes.len(), |found| from + found));
}
*end
};
let mut index = 0;
while let Some(&byte) = bytes.get(index) {
let shown = index >= hidden;
index += 1;
match byte {
b'{' => open(&mut groups, &mut level, Group::Brace),
b'[' => level += 4,
b'}' if shown => close(&mut groups, &mut level, Group::Brace),
b'%' => hidden = hidden.max(find_end(index, false)),
b'\\' => {
let start = index;
while bytes.get(index).is_some_and(u8::is_ascii_alphabetic) {
index += 1;
}
match &source[start..index] {
"left" => open(&mut groups, &mut level, Group::Left),
"begin" => open(&mut groups, &mut level, Group::Begin),
"bgroup" | "begingroup" => {
open(&mut groups, &mut level, Group::Brace)
}
"right" if shown => close(&mut groups, &mut level, Group::Left),
"end" if shown => close(&mut groups, &mut level, Group::Begin),
"egroup" | "endgroup" if shown => {
close(&mut groups, &mut level, Group::Brace)
}
"verb" => {
let after = index + usize::from(bytes.get(index) == Some(&b'*'));
if let Some(delimiter) = source[after..].chars().next() {
let from = after + delimiter.len_utf8();
let end = find_end(from, true);
if let Some(at) = source[from..end].find(delimiter) {
hidden = hidden.max(from + at + delimiter.len_utf8());
}
}
level += 1;
}
"" => {
index += source[index..].chars().next().map_or(0, char::len_utf8)
}
word => {
defines |= DEFINITIONS.contains(&word);
level += 1;
}
}
}
_ => {}
}
deepest = deepest.max(level);
}
(deepest, defines)
}
fn parse_markup<'a>(html: &str, alloc: &'a Allocator) -> ArenaVec<'a, Node<'a>> {
let parse = Parse {
fragment: true,
..Default::default()
};
let Node::Root(root) = parse_html(html, &parse, &mut File::new(), alloc) else {
return ArenaVec::new_in(alloc);
};
let mut children = root.children;
let mut stack: Vec<&mut Node<'a>> = children.iter_mut().collect();
while let Some(node) = stack.pop() {
match node {
Node::Element(element) => {
element.position = None;
sort_attributes(element);
stack.extend(element.children.iter_mut());
}
Node::Text(text) => text.position = None,
_ => {}
}
}
children
}
fn sort_attributes(element: &mut Element<'_>) {
element
.properties
.sort_by_key(|property| match property.name {
"className" => (0, ""),
"style" => (1, ""),
name => (2, name),
});
}
fn create_error<'a>(
value: &str,
title: &str,
settings: &Settings,
alloc: &'a Allocator,
) -> ArenaVec<'a, Node<'a>> {
let mut classes = ArenaVec::with_capacity_in(1, alloc);
classes.push(PropertyItem::String("katex-error"));
let style = alloc.alloc_str(&format!("color:{}", settings.error_color));
let mut properties = ArenaVec::with_capacity_in(3, alloc);
properties.push(Property {
name: "className",
value: PropertyValue::List(classes),
});
properties.push(Property {
name: "style",
value: PropertyValue::String(style),
});
properties.push(Property {
name: "title",
value: PropertyValue::String(alloc.alloc_str(title)),
});
let mut children = ArenaVec::with_capacity_in(1, alloc);
children.push(Node::Text(Text {
value: alloc.alloc_str(value),
position: None,
data: None,
}));
let mut nodes = ArenaVec::with_capacity_in(1, alloc);
nodes.push(Node::Element(Element {
tag_name: "span",
properties,
children,
position: None,
data: None,
content: None,
}));
nodes
}