use mitex_parser::syntax::{FormulaItem, SyntaxElement, SyntaxKind, SyntaxNode};
use rowan::ast::AstNode;
use std::fmt::Write;
use super::context::{ConversionMode, LatexConverter};
use super::environment::{convert_array_with_delim, convert_matrix_with_delim};
pub fn convert_formula(conv: &mut LatexConverter, elem: SyntaxElement, output: &mut String) {
if let SyntaxElement::Node(n) = elem {
if let Some(formula) = FormulaItem::cast(n.clone()) {
let is_inline = formula.is_inline();
let prev_mode = conv.state.mode;
conv.state.mode = ConversionMode::Math;
let mut math_content = String::new();
conv.visit_node(&n, &mut math_content);
let cleaned = conv.cleanup_math_spacing(&math_content);
if is_inline {
output.push('$');
output.push_str(&cleaned);
output.push('$');
} else {
output.push_str("$ ");
output.push_str(&cleaned);
output.push_str(" $");
}
conv.state.mode = prev_mode;
}
}
}
pub fn convert_curly(conv: &mut LatexConverter, elem: SyntaxElement, output: &mut String) {
if conv.state.in_preamble {
return;
}
let node = match elem {
SyntaxElement::Node(n) => n,
_ => return,
};
if let Some(pending) = conv.state.pending_citation.take() {
super::markup::emit_pending_citation_from_curly(&node, pending, output);
return;
}
if let Some(op) = conv.state.pending_op.take() {
let mut content = String::new();
for child in node.children_with_tokens() {
match child.kind() {
SyntaxKind::TokenWhiteSpace
| SyntaxKind::TokenLineBreak
| SyntaxKind::TokenLBrace
| SyntaxKind::TokenRBrace => {}
_ => conv.visit_element(child, &mut content),
}
}
let text = content.trim();
let normalized = text.replace("thin", "").replace(" ", "");
let final_text = if normalized == "argmin" {
"argmin"
} else if normalized == "argmax" {
"argmax"
} else {
text
};
let op_content = if final_text
.chars()
.all(|c| c.is_alphanumeric() || c.is_whitespace())
{
format!("\"{}\"", final_text)
} else {
format!("[{}]", final_text)
};
if op.is_limits {
let _ = write!(output, "limits(op({}))", op_content);
} else {
let _ = write!(output, "op({})", op_content);
}
return;
}
let mut has_content = false;
for child in node.children_with_tokens() {
match child.kind() {
SyntaxKind::TokenWhiteSpace
| SyntaxKind::TokenLineBreak
| SyntaxKind::TokenLBrace
| SyntaxKind::TokenRBrace => {}
_ => has_content = true,
}
conv.visit_element(child, output);
}
if !has_content && matches!(conv.state.mode, ConversionMode::Math) {
output.push_str("zws ");
}
}
pub fn convert_lr(conv: &mut LatexConverter, elem: SyntaxElement, output: &mut String) {
let node = match elem {
SyntaxElement::Node(n) => n,
_ => return,
};
let children: Vec<_> = node.children_with_tokens().collect();
let mut left_delim: Option<String> = None;
let mut right_delim: Option<String> = None;
let mut body_start = 0;
let mut body_end = children.len();
for (i, child) in children.iter().enumerate() {
match child {
SyntaxElement::Node(cn) if cn.kind() == SyntaxKind::ClauseLR => {
let text = cn.text().to_string();
if text.starts_with("\\left") && left_delim.is_none() {
if let Some(delim_text) = text.strip_prefix("\\left") {
let delim_text = delim_text.trim();
if !delim_text.is_empty() {
let delim = extract_delimiter_from_text(delim_text);
left_delim = Some(convert_delimiter(delim));
}
}
body_start = i + 1;
}
}
SyntaxElement::Token(t) => {
let name = t.text();
if let Some(stripped) = name.strip_prefix("\\left") {
left_delim = Some(convert_delimiter(stripped));
body_start = i + 1;
}
}
_ => {}
}
}
for (i, child) in children.iter().enumerate().rev() {
match child {
SyntaxElement::Node(cn) if cn.kind() == SyntaxKind::ClauseLR => {
let text = cn.text().to_string();
if text.starts_with("\\right") && right_delim.is_none() {
if let Some(delim_text) = text.strip_prefix("\\right") {
let delim_text = delim_text.trim();
if !delim_text.is_empty() {
let delim = extract_delimiter_from_text(delim_text);
right_delim = Some(convert_delimiter(delim));
}
}
body_end = i;
break;
}
}
SyntaxElement::Token(t) => {
let name = t.text();
if let Some(stripped) = name.strip_prefix("\\right") {
right_delim = Some(convert_delimiter(stripped));
body_end = i;
break;
}
}
_ => {}
}
}
let (use_lr, is_valid_pair) = match (left_delim.as_deref(), right_delim.as_deref()) {
(Some("("), Some(")")) | (Some("["), Some("]")) | (Some("{"), Some("}")) => (false, true),
(Some(l), Some(r)) if l == r => (true, true),
(Some("("), Some("]"))
| (Some("["), Some(")"))
| (Some("chevron.l"), Some("chevron.r"))
| (Some("floor.l"), Some("floor.r"))
| (Some("ceil.l"), Some("ceil.r")) => (true, true),
(Some("."), Some(_)) | (Some(_), Some(".")) => (true, true),
(None, _) | (_, None) => (false, false),
_ => (true, true),
};
if let Some(matrix_like) = classify_lr_matrix_like(children.as_slice(), body_start, body_end) {
match (
&matrix_like.kind,
left_delim.as_deref(),
right_delim.as_deref(),
) {
(LrMatrixKind::NoIntrinsicDelim, Some("("), Some(")")) => {
emit_lr_matrix_like(conv, &matrix_like.node, &matrix_like.env_name, "(", output);
return;
}
(LrMatrixKind::NoIntrinsicDelim, Some("["), Some("]")) => {
emit_lr_matrix_like(conv, &matrix_like.node, &matrix_like.env_name, "[", output);
return;
}
(LrMatrixKind::NoIntrinsicDelim, Some("{"), Some("}")) => {
emit_lr_matrix_like(conv, &matrix_like.node, &matrix_like.env_name, "{", output);
return;
}
(LrMatrixKind::NoIntrinsicDelim, Some("bar.v"), Some("bar.v")) => {
emit_lr_matrix_like(conv, &matrix_like.node, &matrix_like.env_name, "|", output);
return;
}
(LrMatrixKind::NoIntrinsicDelim, Some("bar.v.double"), Some("bar.v.double")) => {
emit_lr_matrix_like(conv, &matrix_like.node, &matrix_like.env_name, "‖", output);
return;
}
(LrMatrixKind::WithIntrinsicDelim, Some("bar.v"), Some("bar.v")) => {
emit_nested_lr_matrix_like(
conv,
&matrix_like.node,
&matrix_like.env_name,
"bar.v",
output,
);
return;
}
(LrMatrixKind::WithIntrinsicDelim, Some("bar.v.double"), Some("bar.v.double")) => {
emit_nested_lr_matrix_like(
conv,
&matrix_like.node,
&matrix_like.env_name,
"bar.v.double",
output,
);
return;
}
_ => {}
}
}
if left_delim.as_deref() == Some("bar.v.double")
&& right_delim.as_deref() == Some("bar.v.double")
{
let mut content = String::new();
for child in children.iter().take(body_end).skip(body_start) {
match child {
SyntaxElement::Token(t) if t.text() == "." => {}
SyntaxElement::Token(t) if t.text().starts_with("\\right") => {}
_ => conv.visit_element(child.clone(), &mut content),
}
}
output.push_str("norm(");
if content.contains(',') {
output.push('{');
output.push_str(content.trim());
output.push('}');
} else {
output.push_str(&content);
}
output.push_str(") ");
return;
}
if left_delim.as_deref() == Some("bar.v") && right_delim.as_deref() == Some("bar.v") {
let mut content = String::new();
for child in children.iter().take(body_end).skip(body_start) {
match child {
SyntaxElement::Token(t) if t.text() == "." => {}
SyntaxElement::Token(t) if t.text().starts_with("\\right") => {}
_ => conv.visit_element(child.clone(), &mut content),
}
}
output.push_str("abs(");
if content.contains(',') {
output.push('{');
output.push_str(content.trim());
output.push('}');
} else {
output.push_str(&content);
}
output.push_str(") ");
return;
}
if !is_valid_pair {
if let Some(ref delim) = left_delim {
if delim != "." && !delim.is_empty() {
output.push_str(delim);
output.push(' ');
}
}
for child in children.iter().take(body_end).skip(body_start) {
match child {
SyntaxElement::Token(t) if t.text() == "." => {}
SyntaxElement::Token(t) if t.text().starts_with("\\right") => {}
_ => conv.visit_element(child.clone(), output),
}
}
if let Some(ref delim) = right_delim {
if delim != "." && !delim.is_empty() {
output.push_str(delim);
output.push(' ');
}
}
return;
}
if use_lr {
output.push_str("lr(");
}
if let Some(ref delim) = left_delim {
if delim != "." && !delim.is_empty() {
output.push_str(delim);
output.push(' ');
}
}
for child in children.iter().take(body_end).skip(body_start) {
match child {
SyntaxElement::Token(t) if t.text() == "." => {}
SyntaxElement::Token(t) if t.text().starts_with("\\right") => {}
_ => conv.visit_element(child.clone(), output),
}
}
if let Some(ref delim) = right_delim {
if delim != "." && !delim.is_empty() {
output.push(' ');
output.push_str(delim);
}
}
if use_lr {
output.push_str(") ");
} else {
output.push(' ');
}
}
enum LrMatrixKind {
NoIntrinsicDelim,
WithIntrinsicDelim,
}
struct LrMatrixBody {
env_name: String,
node: SyntaxNode,
kind: LrMatrixKind,
}
fn classify_lr_matrix_like(
children: &[SyntaxElement],
body_start: usize,
body_end: usize,
) -> Option<LrMatrixBody> {
let mut significant = Vec::new();
for child in children.iter().take(body_end).skip(body_start) {
match child {
SyntaxElement::Token(t)
if matches!(
t.kind(),
SyntaxKind::TokenWhiteSpace | SyntaxKind::TokenLineBreak
) => {}
SyntaxElement::Token(t) if t.text() == "." => {}
SyntaxElement::Node(n) if n.kind() == SyntaxKind::ClauseLR => {}
_ => significant.push(child),
}
}
if significant.len() != 1 {
return None;
}
let node = unwrap_trivial_matrix_wrapper(significant[0].clone())?;
let env = mitex_parser::syntax::EnvItem::cast(node.clone())?;
let env_name = env.name_tok()?.text().to_string();
let kind = match env_name.as_str() {
"array" | "matrix" | "smallmatrix" => LrMatrixKind::NoIntrinsicDelim,
"pmatrix" | "bmatrix" | "Bmatrix" | "vmatrix" | "Vmatrix" => {
LrMatrixKind::WithIntrinsicDelim
}
_ => return None,
};
Some(LrMatrixBody {
env_name,
node,
kind,
})
}
fn unwrap_trivial_matrix_wrapper(elem: SyntaxElement) -> Option<SyntaxNode> {
match elem {
SyntaxElement::Node(node) => match node.kind() {
SyntaxKind::ItemCurly | SyntaxKind::ItemText | SyntaxKind::ClauseArgument => {
let mut significant = Vec::new();
for child in node.children_with_tokens() {
match child.kind() {
SyntaxKind::TokenWhiteSpace
| SyntaxKind::TokenLineBreak
| SyntaxKind::TokenLBrace
| SyntaxKind::TokenRBrace => {}
_ => significant.push(child),
}
}
if significant.len() == 1 {
unwrap_trivial_matrix_wrapper(significant[0].clone())
} else {
Some(node)
}
}
_ => Some(node),
},
SyntaxElement::Token(_) => None,
}
}
fn emit_lr_matrix_like(
conv: &mut LatexConverter,
node: &SyntaxNode,
env_name: &str,
delim: &str,
output: &mut String,
) {
match env_name {
"array" => convert_array_with_delim(conv, node, Some(delim), output),
_ => convert_matrix_with_delim(conv, node, env_name, Some(delim), output),
}
}
fn emit_nested_lr_matrix_like(
conv: &mut LatexConverter,
node: &SyntaxNode,
env_name: &str,
outer_delim: &str,
output: &mut String,
) {
output.push_str("lr(");
output.push_str(outer_delim);
output.push(' ');
convert_matrix_with_delim(conv, node, env_name, None, output);
output.push(' ');
output.push_str(outer_delim);
output.push_str(") ");
}
pub fn convert_attachment(conv: &mut LatexConverter, elem: SyntaxElement, output: &mut String) {
let node = match elem {
SyntaxElement::Node(n) => n,
_ => return,
};
let mut is_script = false;
for child in node.children_with_tokens() {
let kind = child.kind();
if kind == SyntaxKind::TokenUnderscore {
output.push('_');
is_script = true;
continue;
}
if kind == SyntaxKind::TokenCaret {
output.push('^');
is_script = true;
continue;
}
if kind == SyntaxKind::TokenWhiteSpace || kind == SyntaxKind::TokenLineBreak {
continue;
}
if is_script {
output.push('(');
conv.visit_element(child, output);
output.push(')');
is_script = false;
} else {
conv.visit_element(child, output);
}
}
}
fn extract_delimiter_from_text(text: &str) -> &str {
if text.is_empty() {
return ".";
}
if let Some(after_backslash) = text.strip_prefix('\\') {
if after_backslash.is_empty() {
return "\\";
}
let first_char = after_backslash.chars().next().unwrap();
if first_char.is_ascii_alphabetic() {
let end = after_backslash
.find(|c: char| !c.is_ascii_alphabetic())
.unwrap_or(after_backslash.len());
&text[..end + 1] } else {
let char_len = first_char.len_utf8();
&text[..1 + char_len]
}
} else {
let first_char = text.chars().next().unwrap();
&text[..first_char.len_utf8()]
}
}
fn convert_delimiter(delim: &str) -> String {
match delim.trim() {
"." => ".".to_string(), "(" => "(".to_string(),
")" => ")".to_string(),
"[" => "[".to_string(),
"]" => "]".to_string(),
"\\{" | "\\lbrace" => "{".to_string(),
"\\}" | "\\rbrace" => "}".to_string(),
"|" | "\\vert" | "\\lvert" | "\\rvert" => "bar.v".to_string(),
"\\|" | "\\Vert" | "\\lVert" | "\\rVert" => "bar.v.double".to_string(),
"\\langle" => "chevron.l".to_string(),
"\\rangle" => "chevron.r".to_string(),
"\\lfloor" => "floor.l".to_string(),
"\\rfloor" => "floor.r".to_string(),
"\\lceil" => "ceil.l".to_string(),
"\\rceil" => "ceil.r".to_string(),
"\\lgroup" => "paren.l.flat".to_string(),
"\\rgroup" => "paren.r.flat".to_string(),
other => other.to_string(),
}
}