const INDENT: &str = " ";
const VERBATIM_ENVS: &[&str] = &["verbatim", "lstlisting", "minted", "Verbatim"];
pub fn format(source: &str) -> String {
let mut output: Vec<String> = Vec::new();
let mut depth: usize = 0;
let mut prev_blank = false;
let mut verbatim: Option<String> = None;
for line in source.lines() {
if let Some(ref env) = verbatim {
let end_tag = format!("\\end{{{}}}", env);
if line.trim_start().starts_with(&end_tag) {
depth = depth.saturating_sub(1);
output.push(indent_line(depth, line.trim()));
verbatim = None;
} else {
output.push(line.trim_end().to_string());
}
continue;
}
let trimmed = line.trim();
if trimmed.is_empty() {
if !prev_blank && !output.is_empty() {
output.push(String::new());
}
prev_blank = true;
continue;
}
prev_blank = false;
let display_depth = depth.saturating_sub(leading_dedent(trimmed));
output.push(indent_line(display_depth, trimmed));
if let Some(env) = verbatim_opener(trimmed) {
depth += 1;
verbatim = Some(env);
continue;
}
let delta = nesting_delta(trimmed);
depth = (depth as i32 + delta).max(0) as usize;
}
while output.last().is_some_and(|l| l.is_empty()) {
output.pop();
}
let mut result = output.join("\n");
result.push('\n');
result
}
fn indent_line(depth: usize, content: &str) -> String {
if content.is_empty() {
String::new()
} else {
format!("{}{}", INDENT.repeat(depth), content)
}
}
fn leading_dedent(line: &str) -> usize {
if line.starts_with("\\end{") {
return 1;
}
line.chars().take_while(|&c| c == '}' || c == ']').count()
}
fn nesting_delta(line: &str) -> i32 {
let mut delta = count_occurrences(line, "\\begin{") as i32;
delta -= count_occurrences(line, "\\end{") as i32;
let mut escaped = false;
for ch in line.chars() {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' => escaped = true,
'%' => break, '{' | '[' => delta += 1,
'}' | ']' => delta -= 1,
_ => {}
}
}
delta
}
fn verbatim_opener(line: &str) -> Option<String> {
if !line.starts_with("\\begin{") {
return None;
}
let env = extract_env_name(line)?;
VERBATIM_ENVS.contains(&env.as_str()).then_some(env)
}
fn count_occurrences(haystack: &str, needle: &str) -> usize {
haystack.matches(needle).count()
}
fn extract_env_name(line: &str) -> Option<String> {
let start = line.find("\\begin{")? + 7;
let end = line[start..].find('}')?;
Some(line[start..start + end].to_string())
}
pub fn format_bib(source: &str) -> String {
match try_format_bib(source) {
Some(formatted) => formatted,
None => source.to_string(),
}
}
struct BibEntry {
kind: String,
key: Option<String>,
fields: Vec<(String, String)>,
}
fn try_format_bib(source: &str) -> Option<String> {
let chars: Vec<char> = source.chars().collect();
let n = chars.len();
let mut i = 0;
let mut entries = Vec::new();
while i < n {
while i < n && chars[i].is_whitespace() {
i += 1;
}
if i >= n {
break;
}
if chars[i] != '@' {
return None;
}
i += 1;
let kind_start = i;
while i < n && chars[i].is_alphanumeric() {
i += 1;
}
let kind: String = chars[kind_start..i].iter().collect();
if kind.is_empty() {
return None;
}
while i < n && chars[i].is_whitespace() {
i += 1;
}
if i >= n {
return None;
}
let open = match chars[i] {
'{' => '{',
'(' => '(',
_ => return None,
};
i += 1;
let body_start = i;
let mut brace_depth = 0i32;
let mut in_quote = false;
loop {
if i >= n {
return None; }
let c = chars[i];
if in_quote {
if c == '"' {
in_quote = false;
}
} else {
match c {
'"' => in_quote = true,
'{' => brace_depth += 1,
'}' => {
if open == '{' && brace_depth == 0 {
break;
}
brace_depth -= 1;
}
')' if open == '(' && brace_depth == 0 => break,
_ => {}
}
}
i += 1;
}
let body: String = chars[body_start..i].iter().collect();
i += 1;
entries.push(parse_bib_body(&kind, &body)?);
}
if entries.is_empty() {
return None;
}
let mut blocks: Vec<String> = Vec::new();
for entry in &entries {
blocks.push(render_bib_entry(entry));
}
let mut out = blocks.join("\n\n");
out.push('\n');
Some(out)
}
fn parse_bib_body(kind: &str, body: &str) -> Option<BibEntry> {
let segments = split_top_level(body);
let kind_l = kind.to_lowercase();
let key_less = matches!(kind_l.as_str(), "string" | "preamble");
let mut iter = segments.into_iter();
let mut key = None;
if !key_less {
let first = iter.next()?.trim().to_string();
if first.is_empty() || first.contains('=') {
return None;
}
key = Some(first);
}
let mut fields = Vec::new();
for seg in iter {
let seg = seg.trim();
if seg.is_empty() {
continue; }
let eq = top_level_eq(seg)?;
let name = seg[..eq].trim().to_lowercase();
let value = collapse_ws(seg[eq + 1..].trim());
if name.is_empty() {
return None;
}
fields.push((name, value));
}
Some(BibEntry {
kind: kind_l,
key,
fields,
})
}
fn render_bib_entry(entry: &BibEntry) -> String {
let mut s = String::new();
s.push('@');
s.push_str(&entry.kind);
s.push('{');
let width = entry.fields.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
match &entry.key {
Some(key) => s.push_str(key),
None => {
if let Some((name, value)) = entry.fields.first() {
s.push_str(&format!("{} = {}", name, value));
}
s.push('}');
return s;
}
}
for (name, value) in &entry.fields {
s.push_str(",\n");
s.push_str(INDENT);
s.push_str(&format!("{:width$} = {}", name, value, width = width));
}
if !entry.fields.is_empty() {
s.push(',');
}
s.push('\n');
s.push('}');
s
}
fn split_top_level(body: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut depth = 0i32;
let mut in_quote = false;
for c in body.chars() {
match c {
'"' if depth == 0 => {
in_quote = !in_quote;
cur.push(c);
}
'{' if !in_quote => {
depth += 1;
cur.push(c);
}
'}' if !in_quote => {
depth -= 1;
cur.push(c);
}
',' if depth == 0 && !in_quote => {
out.push(std::mem::take(&mut cur));
}
_ => cur.push(c),
}
}
out.push(cur);
out
}
fn top_level_eq(seg: &str) -> Option<usize> {
let mut depth = 0i32;
let mut in_quote = false;
for (idx, c) in seg.char_indices() {
match c {
'"' if depth == 0 => in_quote = !in_quote,
'{' if !in_quote => depth += 1,
'}' if !in_quote => depth -= 1,
'=' if depth == 0 && !in_quote => return Some(idx),
_ => {}
}
}
None
}
fn collapse_ws(value: &str) -> String {
value.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_returns_single_newline() {
assert_eq!(format(""), "\n");
}
#[test]
fn trailing_newline_always_present() {
let out = format("hello");
assert!(out.ends_with('\n'));
}
#[test]
fn indentation_inside_environment() {
let src = "\\begin{document}\nhello\n\\end{document}";
let out = format(src);
assert_eq!(out, "\\begin{document}\n hello\n\\end{document}\n");
}
#[test]
fn nested_environments_indent_begin_and_end() {
let src = "\\begin{a}\n\\begin{b}\nx\n\\end{b}\n\\end{a}";
let out = format(src);
assert_eq!(
out,
"\\begin{a}\n \\begin{b}\n x\n \\end{b}\n\\end{a}\n"
);
}
#[test]
fn multiline_brace_argument_indents() {
let src = "\\hypersetup{\npdftitle={X},\ncolorlinks\n}";
let out = format(src);
assert_eq!(out, "\\hypersetup{\n pdftitle={X},\n colorlinks\n}\n");
}
#[test]
fn escaped_braces_do_not_affect_depth() {
let src = "\\begin{document}\na \\{ b \\} c\nd\n\\end{document}";
let out = format(src);
assert_eq!(
out,
"\\begin{document}\n a \\{ b \\} c\n d\n\\end{document}\n"
);
}
#[test]
fn comment_braces_ignored() {
let src = "\\begin{document}\nx % this { is not counted\ny\n\\end{document}";
let out = format(src);
assert_eq!(
out,
"\\begin{document}\n x % this { is not counted\n y\n\\end{document}\n"
);
}
#[test]
fn multiple_blank_lines_collapsed() {
let src = "a\n\n\n\nb";
let out = format(src);
assert_eq!(out, "a\n\nb\n");
}
#[test]
fn verbatim_content_preserved() {
let src = "\\begin{verbatim}\n raw content\n\\end{verbatim}";
let out = format(src);
assert_eq!(out, "\\begin{verbatim}\n raw content\n\\end{verbatim}\n");
}
#[test]
fn bib_entry_formatted_and_aligned() {
let src = "@Article{key, author={A. B.},title = {T},year=2020}";
let out = format_bib(src);
assert_eq!(
out,
"@article{key,\n author = {A. B.},\n title = {T},\n year = 2020,\n}\n"
);
}
#[test]
fn bib_multiline_value_collapsed() {
let src = "@book{k,\n title = {A\n long title},\n}";
let out = format_bib(src);
assert_eq!(out, "@book{k,\n title = {A long title},\n}\n");
}
#[test]
fn bib_invalid_left_untouched() {
let src = "not a bib file at all";
assert_eq!(format_bib(src), src);
}
#[test]
fn bib_braced_comma_not_split() {
let src = "@misc{k, title={Hello, World}}";
let out = format_bib(src);
assert_eq!(out, "@misc{k,\n title = {Hello, World},\n}\n");
}
}