use crate::ast::LatexNode;
use regex::Regex;
pub fn parse_latex(input: &str) -> Vec<LatexNode> {
let mut nodes = Vec::new();
let mut remaining = input;
let patterns: &[(Regex, fn(String) -> LatexNode)] = &[
(Regex::new(r"\\section\{(.*?)\}").unwrap(), |c| LatexNode::Section(c)),
(Regex::new(r"\\subsection\{(.*?)\}").unwrap(), |c| LatexNode::Subsection(c)),
(Regex::new(r"\\subsubsection\{(.*?)\}").unwrap(), |c| LatexNode::Subsubsection(c)),
(Regex::new(r"\\paragraph\{(.*?)\}").unwrap(), |c| LatexNode::Paragraph(c)),
(Regex::new(r"\\subparagraph\{(.*?)\}").unwrap(), |c| LatexNode::Subparagraph(c)),
(Regex::new(r"\\textbf\{(.*?)\}").unwrap(), |c| LatexNode::Bold(c)),
(Regex::new(r"\\textit\{(.*?)\}").unwrap(), |c| LatexNode::Italic(c)),
(Regex::new(r"\\underline\{(.*?)\}").unwrap(), |c| LatexNode::Underline(c)),
(Regex::new(r"\\emph\{(.*?)\}").unwrap(), |c| LatexNode::Emphasized(c)),
(Regex::new(r"\\url\{(.*?)\}").unwrap(), |c| LatexNode::Hyperlink { url: c.clone(), text: c }),
(Regex::new(r"\\tableofcontents").unwrap(), |_| LatexNode::TableOfContents),
(Regex::new(r"\\pagebreak").unwrap(), |_| LatexNode::PageBreak),
(Regex::new(r"\\hrule").unwrap(), |_| LatexNode::HorizontalRule),
(Regex::new(r"\$(.*?)\$").unwrap(), |c| LatexNode::InlineMath(c)),
(Regex::new(r"\\\[(.*?)\\\]").unwrap(), |c| LatexNode::DisplayMath(c)),
];
let textcolor_re = Regex::new(r"\\textcolor\{(.*?)\}\{(.*?)\}").unwrap();
let href_re = Regex::new(r"\\href\{(.*?)\}\{(.*?)\}").unwrap();
let mut pos = 0;
while pos < remaining.len() {
let slice = &remaining[pos..];
let mut matched = false;
if let Some(cap) = textcolor_re.captures(slice) {
if let Some(mat) = cap.get(0) {
let start = pos + mat.start();
let end = pos + mat.end();
if start > pos {
let text = &remaining[pos..start];
if !text.trim().is_empty() {
nodes.push(LatexNode::Text(text.to_string()));
}
}
nodes.push(LatexNode::Colored {
color: cap[1].to_string(),
content: cap[2].to_string(),
});
pos = end;
matched = true;
continue;
}
} else if let Some(cap) = href_re.captures(slice) {
if let Some(mat) = cap.get(0) {
let start = pos + mat.start();
let end = pos + mat.end();
if start > pos {
let text = &remaining[pos..start];
if !text.trim().is_empty() {
nodes.push(LatexNode::Text(text.to_string()));
}
}
nodes.push(LatexNode::Hyperlink {
url: cap[1].to_string(),
text: cap[2].to_string(),
});
pos = end;
matched = true;
continue;
}
}
for (re, constructor) in patterns {
if let Some(cap) = re.captures(slice) {
if let Some(mat) = cap.get(0) {
let start = pos + mat.start();
let end = pos + mat.end();
if start > pos {
let text = &remaining[pos..start];
if !text.trim().is_empty() {
nodes.push(LatexNode::Text(text.to_string()));
}
}
let content = cap.get(1).map_or("", |m| m.as_str()).to_string();
nodes.push(constructor(content));
pos = end;
matched = true;
break;
}
}
}
if !matched {
let rest = &remaining[pos..];
if !rest.trim().is_empty() {
nodes.push(LatexNode::Text(rest.to_string()));
}
break;
}
}
nodes
}
fn extract_brace_content<'a>(line: &'a str, command: &str) -> Option<&'a str> {
let prefix = format!("{}{{", command);
if line.starts_with(&prefix) && line.ends_with('}') {
Some(&line[prefix.len()..line.len() - 1])
} else {
None
}
}
fn extract_two_brace_args<'a>(line: &'a str, command: &str) -> Option<(&'a str, &'a str)> {
let re = Regex::new(&format!(r"{}\{{([^}}]+)\}}\{{([^}}]+)\}}", regex::escape(command))).ok()?;
let caps = re.captures(line)?;
Some((caps.get(1)?.as_str(), caps.get(2)?.as_str()))
}
fn extract_fontsize_command<'a>(line: &'a str) -> Option<(&'a str, &'a str)> {
let re = Regex::new(r"\\fontsize\{([^}]+)\}\{[^}]+\}\\selectfont\s*(.+)").ok()?;
let caps = re.captures(line)?;
Some((caps.get(1)?.as_str(), caps.get(2)?.as_str()))
}
fn parse_newcommand(line: &str) -> Option<(String, usize, String)> {
let re = Regex::new(r"\\newcommand\\(\\w+)(?:\[(\d+)\])?\{(.+)\}").ok()?;
let caps = re.captures(line)?;
let name = caps.get(1)?.as_str().to_string();
let args = caps.get(2).map_or(0, |m| m.as_str().parse().unwrap_or(0));
let def = caps.get(3)?.as_str().to_string();
Some((name, args, def))
}
fn parse_item_list(lines: &[&str], env: &str) -> (Vec<LatexNode>, usize) {
let mut items = Vec::new();
let mut i = 1;
while i < lines.len() && !lines[i].trim().starts_with(&format!("\\end{{{}}}", env)) {
let line = lines[i].trim();
if line.starts_with("\\item") {
let content = line.trim_start_matches("\\item").trim();
items.push(LatexNode::ListItem(content.to_string()));
}
i += 1;
}
(items, i + 1)
}
fn parse_environment_block(lines: &[&str], env: &str) -> (Vec<String>, usize) {
let mut content = Vec::new();
let mut i = 1;
while i < lines.len() && !lines[i].trim().starts_with(&format!("\\end{{{}}}", env)) {
content.push(lines[i].to_string());
i += 1;
}
(content, i + 1)
}
fn parse_description_list(lines: &[&str]) -> (Vec<(String, String)>, usize) {
let mut items = Vec::new();
let mut i = 1;
let re = Regex::new(r"\\item\[(.*?)\](.*)").unwrap();
while i < lines.len() && !lines[i].trim().starts_with("\\end{description}") {
let line = lines[i].trim();
if let Some(caps) = re.captures(line) {
let label = caps.get(1).unwrap().as_str().trim().to_string();
let content = caps.get(2).unwrap().as_str().trim().to_string();
items.push((label, content));
}
i += 1;
}
(items, i + 1)
}
fn parse_tabular(lines: &[&str]) -> (LatexNode, usize) {
let alignment_line = lines[0];
let re_align = Regex::new(r"\\begin\{tabular\}\{([^}]+)\}").unwrap();
let alignment = if let Some(caps) = re_align.captures(alignment_line) {
caps.get(1).unwrap().as_str().chars().map(|c| c.to_string()).collect()
} else {
vec![]
};
let mut rows = Vec::new();
let mut i = 1;
while i < lines.len() && !lines[i].trim().starts_with("\\end{tabular}") {
let line = lines[i].trim();
if !line.is_empty() {
let cols: Vec<String> = line.split('&').map(|s| s.trim().trim_end_matches("\\").to_string()).collect();
rows.push(cols);
}
i += 1;
}
(LatexNode::Table {
alignment,
rows,
caption: None,
}, i + 1)
}
fn parse_figure(lines: &[&str]) -> (LatexNode, usize) {
let mut content = Vec::new();
let mut caption = None;
let mut i = 1;
while i < lines.len() && !lines[i].trim().starts_with("\\end{figure}") {
let line = lines[i].trim();
if line.starts_with("\\caption") {
if let Some(c) = extract_brace_content(line, "\\caption") {
caption = Some(c.to_string());
}
} else if line.starts_with("\\includegraphics") {
if let Some(path) = extract_brace_content(line, "\\includegraphics") {
content.push(LatexNode::Image {
path: path.to_string(),
width: None,
height: None,
caption: None,
});
}
} else if !line.is_empty() {
content.push(LatexNode::Text(line.to_string()));
}
i += 1;
}
(LatexNode::Figure {
content,
caption,
}, i + 1)
}
fn parse_bibliography(lines: &[&str]) -> (Vec<LatexNode>, usize) {
let mut items = Vec::new();
let mut i = 1;
while i < lines.len() && !lines[i].trim().starts_with("\\end{thebibliography}") {
let line = lines[i].trim();
if line.starts_with("\\bibitem") {
let re = Regex::new(r"\\bibitem\{([^}]+)\}(.*)").unwrap();
if let Some(caps) = re.captures(line) {
let key = caps.get(1).unwrap().as_str().to_string();
let content = caps.get(2).unwrap().as_str().to_string();
items.push(LatexNode::BibItem {
key,
content: vec![LatexNode::Text(content)],
});
}
}
i += 1;
}
(items, i + 1)
}