use crate::args::{Args, TabChar};
use crate::ignore::{get_ignore, Ignore};
use crate::indent::{apply_indent, calculate_indent, Indent};
use crate::logging::{record_file_log, Log};
use crate::read::{read, read_stdin};
use crate::regexes::{ENV_BEGIN, ENV_END, ITEM, RE_SPLITTING, VERBS};
use crate::subs;
use crate::table::{format_tables, is_inside_table, Table};
use crate::verbatim::{get_verbatim, Verbatim};
use crate::wrap::{apply_wrap, needs_wrap};
use crate::write::process_output;
use crate::LINE_END;
use log::Level::{Info, Warn};
use std::iter::zip;
use std::path::{Path, PathBuf};
#[allow(clippy::too_many_lines)]
pub fn format_file(
old_text: &str,
file: &Path,
args: &Args,
logs: &mut Vec<Log>,
) -> String {
record_file_log(logs, Info, file, "Formatting started.");
let old_text = clean_text(old_text, args);
let mut old_lines = zip(1.., old_text.lines());
let mut state = State::new();
let mut queue: Vec<(usize, String)> = vec![];
let mut new_text = String::with_capacity(2 * old_text.len());
let indent_char = match args.tabchar {
TabChar::Tab => "\t",
TabChar::Space => " ",
};
let lists_begin = get_begins(&args.lists);
let lists_end = get_ends(&args.lists);
let verbatims_begin = get_begins(&args.verbatims);
let verbatims_end = get_ends(&args.verbatims);
let no_indent_envs_begin = get_begins(&args.no_indent_envs);
let no_indent_envs_end = get_ends(&args.no_indent_envs);
loop {
if let Some((linum_old, mut line)) = queue.pop() {
let pattern = Pattern::new(&line);
let mut temp_state = state.clone();
temp_state.linum_old = linum_old;
if !set_ignore_and_report(
&line,
&mut temp_state,
logs,
file,
&pattern,
&verbatims_begin,
&verbatims_end,
) {
if subs::needs_split(&line, &pattern) {
let (this_line, next_line) =
subs::split_line(&line, &temp_state, file, args, logs);
queue.push((linum_old, next_line.to_string()));
line = this_line.to_string();
}
let indent = calculate_indent(
&line,
&mut temp_state,
logs,
file,
args,
&pattern,
&lists_begin,
&lists_end,
&no_indent_envs_begin,
&no_indent_envs_end,
);
#[allow(clippy::cast_possible_wrap)]
let indent_length =
usize::try_from(indent.visual * args.tabsize as i8)
.expect("Visual indent is non-negative.");
if needs_wrap(
line.trim_start(),
indent_length,
args,
&temp_state,
) {
let wrapped_lines = apply_wrap(
line.trim_start(),
indent_length,
&temp_state,
file,
args,
logs,
&pattern,
);
if let Some([this_line, next_line_start, next_line]) =
wrapped_lines
{
queue.push((
linum_old,
[next_line_start, next_line].concat(),
));
queue.push((linum_old, this_line.to_string()));
continue;
}
}
line = apply_indent(&line, &indent, args, indent_char);
}
state = temp_state;
new_text.push_str(&line);
new_text.push_str(LINE_END);
state.linum_new += 1;
} else if let Some((linum_old, line)) = old_lines.next() {
queue.push((linum_old, line.to_string()));
} else {
break;
}
}
if !indents_return_to_zero(&state) {
let msg = format!(
"Indent does not return to zero. Last non-indented line is line {}",
state.linum_last_zero_indent
);
record_file_log(logs, Warn, file, &msg);
}
if let Some(n) = state.linum_first_negative_indent {
let msg = format!(
"Negative indents. First negatively indented line is line {n}",
);
record_file_log(logs, Warn, file, &msg);
}
if args.format_tables {
new_text = format_tables(&new_text);
}
new_text = subs::remove_trailing_spaces(&new_text);
new_text = subs::remove_trailing_blank_lines(&new_text);
record_file_log(logs, Info, file, "Formatting complete.");
new_text
}
fn get_begins(v: &[String]) -> Vec<String> {
v.iter().map(|l| format!("\\begin{{{l}}}")).collect()
}
fn get_ends(v: &[String]) -> Vec<String> {
v.iter().map(|l| format!("\\end{{{l}}}")).collect()
}
fn set_ignore_and_report(
line: &str,
temp_state: &mut State,
logs: &mut Vec<Log>,
file: &Path,
pattern: &Pattern,
verbatims_begin: &[String],
verbatims_end: &[String],
) -> bool {
temp_state.ignore = get_ignore(line, temp_state, logs, file, true);
temp_state.verbatim = get_verbatim(
line,
temp_state,
logs,
file,
true,
pattern,
verbatims_begin,
verbatims_end,
);
temp_state.table = is_inside_table(line, temp_state, pattern);
temp_state.verbatim.visual || temp_state.ignore.visual
}
fn clean_text(text: &str, args: &Args) -> String {
let mut text = subs::remove_extra_newlines(text);
if args.tabchar != TabChar::Tab {
text = subs::remove_tabs(&text, args);
}
text = subs::remove_trailing_spaces(&text);
text
}
#[derive(Clone, Debug)]
pub struct State {
pub linum_old: usize,
pub linum_new: usize,
pub ignore: Ignore,
pub indent: Indent,
pub verbatim: Verbatim,
pub linum_last_zero_indent: usize,
pub linum_first_negative_indent: Option<usize>,
pub table: Table,
}
impl State {
#[must_use]
pub const fn new() -> Self {
Self {
linum_old: 1,
linum_new: 1,
ignore: Ignore::new(),
indent: Indent::new(),
verbatim: Verbatim::new(),
linum_last_zero_indent: 1,
linum_first_negative_indent: None,
table: Table::new(),
}
}
}
impl Default for State {
fn default() -> Self {
Self::new()
}
}
#[allow(clippy::struct_excessive_bools)]
pub struct Pattern {
pub contains_env_begin: bool,
pub contains_env_end: bool,
pub contains_item: bool,
pub contains_splitting: bool,
pub contains_comment: bool,
pub contains_verb: bool,
}
impl Pattern {
#[must_use]
pub fn new(s: &str) -> Self {
let contains_comment = s.contains('%');
let contains_verb = VERBS.iter().any(|x| s.contains(x));
if RE_SPLITTING.is_match(s) {
Self {
contains_env_begin: s.contains(ENV_BEGIN),
contains_env_end: s.contains(ENV_END),
contains_item: s.contains(ITEM),
contains_splitting: true,
contains_comment,
contains_verb,
}
} else {
Self {
contains_env_begin: false,
contains_env_end: false,
contains_item: false,
contains_splitting: false,
contains_comment,
contains_verb,
}
}
}
}
const fn indents_return_to_zero(state: &State) -> bool {
state.indent.actual == 0
}
pub fn run(args: &Args, logs: &mut Vec<Log>) -> u8 {
let mut exit_code = 0;
if args.stdin {
let stdin_path = PathBuf::from("<stdin>");
if let Some(text) = read_stdin(logs) {
let new_text = format_file(&text, &stdin_path, args, logs);
exit_code =
process_output(args, &stdin_path, &text, &new_text, logs);
} else {
exit_code = 1;
}
} else {
for file in &args.files {
if let Some(text) = read(file, logs) {
let new_text = format_file(&text, file, args, logs);
exit_code |= process_output(args, file, &text, &new_text, logs);
} else {
exit_code = 1;
}
}
}
exit_code
}