use std::cmp::Ordering;
use std::mem::take;
use std::str;
use anyhow::{bail, Context, Error};
use crate::{noisy::Noisy, replacements::Replacement, Timers};
enum LabelError
{
BadNumber(String, String),
DifferentLengths(String, String),
}
fn cmp_piece(a: &str, b: &str, label_a: &str, label_b: &str, is_first_label: bool) -> Result<Ordering, LabelError>
{
if let (Ok(a), Ok(b)) = (a.parse::<f64>(), b.parse::<f64>())
{
Ok(a.partial_cmp(&b)
.ok_or_else(|| LabelError::BadNumber(label_a.to_owned(), label_b.to_owned()))?)
}
else
{
if b.parse::<f64>().is_ok()
{
if is_first_label
{
Err(LabelError::BadNumber(label_a.to_string(), a.to_string()))
}
else
{
Ok(Ordering::Less)
}
}
else
{
Err(LabelError::BadNumber(label_b.to_string(), b.to_string()))
}
}
}
fn cmp_label(a: &str, b: &str, is_first_label: bool) -> Result<Ordering, Vec<LabelError>>
{
let mut ok_return_value = None;
let mut errors = vec![];
let a_splitted = a.split(':').collect::<Vec<_>>();
let b_splitted = b.split(':').collect::<Vec<_>>();
match (a_splitted.len(), b_splitted.len())
{
(n, m) if n == m =>
{
for i in 0..n
{
match cmp_piece(a_splitted[i], b_splitted[i], a, b, is_first_label)
{
Ok(Ordering::Less) => ok_return_value = ok_return_value.or(Some(Ordering::Less)),
Ok(Ordering::Greater) => ok_return_value = ok_return_value.or(Some(Ordering::Greater)),
Ok(Ordering::Equal) =>
{}
Err(err) => errors.push(err),
}
}
}
_ => errors.push(LabelError::DifferentLengths(a.to_string(), b.to_string())),
}
if !errors.is_empty()
{
Err(errors)
}
else if let Some(rv) = ok_return_value
{
Ok(rv)
}
else
{
Ok(Ordering::Equal)
}
}
fn parse(s: &[u8], target: &[u8]) -> Result<Vec<String>, Error>
{
let mut labels = vec![];
let mut i = 0;
let mut line = 1;
while i + target.len() <= s.len()
{
if &s[i..(i + target.len())] == target
{
let mut label = vec![];
i += target.len();
if i >= s.len()
{
bail!("Unclosed label or ref immediately at the end on line {line}");
}
while s[i] != b'}'
{
label.push(s[i]);
i += 1;
if i >= s.len()
{
bail!(
"Label or ref {:?} was not closed at line {line}",
str::from_utf8(&label).expect("A partial string was not UTF-8")
);
}
}
labels.push(String::from_utf8(take(&mut label)).expect("A label or ref was not UTF-8"));
}
if s[i] == b'\n'
{
line += 1;
}
i += 1;
}
Ok(labels)
}
pub fn check_labels(s: &str, repls: &[Replacement], noisy: &mut Noisy, timers: &mut Timers) -> Result<String, Error>
{
let s_bytes = s.as_bytes();
let labels = parse(s_bytes, b"\\label{").context("Couldn't parse labels")?;
let refs = parse(s_bytes, b"\\ref{").context("Couldn't parse refs")?;
let missing_one_kind = |a: &[String], b: &[String]| {
b.iter()
.filter_map(|b_| {
if a.iter().all(|a_| a_ != b_)
{
Some(b_.clone())
}
else
{
None
}
})
.collect()
};
let mut missing_labels = missing_one_kind(&labels, &refs);
let mut duplicated_labels = labels
.iter()
.enumerate()
.filter_map(|(i, label)| {
if (0..i).any(|j| label == &labels[j])
{
Some(label.clone())
}
else
{
None
}
})
.collect();
let mut disordered_labels = vec![];
let mut malformatted_numbers_in_label = vec![];
let mut inconsistent_lengths_of_labels = vec![];
for i in 0..labels.len().saturating_sub(1)
{
match cmp_label(&labels[i], &labels[i + 1], i == 0)
{
Ok(Ordering::Greater) =>
{
disordered_labels.push((labels[i].clone(), labels[i + 1].clone()));
}
Ok(_) =>
{}
Err(errs) =>
{
for err in errs
{
match err
{
LabelError::BadNumber(label, bad_num) =>
{
malformatted_numbers_in_label.push((label, bad_num));
}
LabelError::DifferentLengths(a, b) => inconsistent_lengths_of_labels.push((a, b)),
}
}
}
}
}
noisy.append_labels(
&mut missing_labels,
&mut duplicated_labels,
&mut disordered_labels,
&mut malformatted_numbers_in_label,
&mut inconsistent_lengths_of_labels,
);
timers.start("remove_labels")?;
let missing_refs = missing_one_kind(&refs, &labels);
let mut s = s.to_string();
for label in missing_refs
{
s = s.replace(&format!("\\label{{{label}}}"), "");
}
for replacement in repls
{
if replacement.refkind
{
if let Some(condition) = &replacement.condition
{
if !s.contains(condition)
{
continue;
}
}
if s.contains(&replacement.from)
{
s = s.replace(&format!("{}\\ref", replacement.from), &replacement.to);
if replacement.noisy
{
noisy.repls.push(replacement.clone());
}
}
}
}
timers.stop("remove_labels")?;
Ok(s)
}
#[cfg(test)]
mod tests
{
use crate::{labels::check_labels, Noisy, Replacement, Timers};
#[allow(clippy::too_many_lines)]
#[allow(clippy::literal_string_with_formatting_args)]
#[test]
fn labels_test()
{
let tests = vec![
("", "", Noisy::default()),
(
r"\ref{6:2:1}\label{4:3:2}
\label{6:2:1}
AbCdEf
\ref{4:3:2}",
r"\ref{6:2:1}\label{4:3:2}
\label{6:2:1}
AbCdEf
\ref{4:3:2}",
Noisy::default(),
),
(r"abc\label{3:1}def", "abcdef", Noisy::default()),
(
r"abc\label{3:1}de\label{2:3:1}f",
"abcdef",
Noisy {
inconsistent_length_of_labels: vec![("3:1".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
),
(
r"abc\label{3:1:1}de\label{2:3:1}f",
"abcdef",
Noisy {
disordered_labels: vec![("3:1:1".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
),
(
r"\label{7}abc\label{3:1:1}de\label{2:3:1}f",
"abcdef",
Noisy {
inconsistent_length_of_labels: vec![("7".to_owned(), "3:1:1".to_owned())],
disordered_labels: vec![("3:1:1".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
),
(
r"\label{7}abc\label{3:1:1}d\ref{7}e\label{2:3:1}f",
r"\label{7}abcd\ref{7}ef",
Noisy {
inconsistent_length_of_labels: vec![("7".to_owned(), "3:1:1".to_owned())],
disordered_labels: vec![("3:1:1".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
),
(
r"\label{7}abc\label{3:1:1}d\label{abc}\ref{7}e\label{2:3:1}f",
r"\label{7}abcd\ref{7}ef",
Noisy {
inconsistent_length_of_labels: vec![
("7".to_owned(), "3:1:1".to_owned()),
("3:1:1".to_owned(), "abc".to_owned()),
("abc".to_owned(), "2:3:1".to_owned()),
],
..Noisy::default()
},
),
(
r"\label{7}abc\label{3:1:1}d\label{4:abc:22}\ref{7}e\label{2:3:1}f",
r"\label{7}abcd\ref{7}ef",
Noisy {
inconsistent_length_of_labels: vec![("7".to_owned(), "3:1:1".to_owned())],
malformatted_number_in_label: vec![("4:abc:22".to_owned(), "abc".to_owned())],
disordered_labels: vec![("4:abc:22".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
),
(
r"\label{7}abc\label{3:1:1}d\label{7}\label{4:abc:22}\ref{7}e\label{2:3:1}f",
r"\label{7}abcd\label{7}\ref{7}ef",
Noisy {
duplicated_labels: vec!["7".to_owned()],
inconsistent_length_of_labels: vec![
("7".to_owned(), "3:1:1".to_owned()),
("3:1:1".to_owned(), "7".to_owned()),
("7".to_owned(), "4:abc:22".to_owned()),
],
disordered_labels: vec![("4:abc:22".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
),
(
r"\label{7}abc\label{3:1:1}d\label{7}\ref{2:3:2}\label{4:abc:22}\ref{7}e\label{2:3:1}f",
r"\label{7}abcd\label{7}\ref{2:3:2}\ref{7}ef",
Noisy {
missing_labels: vec!["2:3:2".to_owned()],
duplicated_labels: vec!["7".to_owned()],
inconsistent_length_of_labels: vec![
("7".to_owned(), "3:1:1".to_owned()),
("3:1:1".to_owned(), "7".to_owned()),
("7".to_owned(), "4:abc:22".to_owned()),
],
disordered_labels: vec![("4:abc:22".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
),
(r"\label{7:}\ref{7:}", r"\label{7:}\ref{7:}", Noisy::default()),
(r"\ref{:7}\label{:7}", r"\ref{:7}\label{:7}", Noisy::default()),
];
for (input, output, noisy_output) in tests
{
let mut noisy = Noisy::default();
let mut timers = Timers::default();
assert_eq!(check_labels(input, &[], &mut noisy, &mut timers).unwrap(), output);
assert_eq!(noisy, noisy_output);
}
}
#[test]
fn labels_repl_tests()
{
let tests = vec![
(r"", r"", Noisy::default(), vec![]),
(r"\label{}", r"", Noisy::default(), vec![]),
(
r"\ref{}",
r"\ref{}",
Noisy {
missing_labels: vec![String::new()],
..Noisy::default()
},
vec![],
),
(
r"\label{7}abc\label{3:1:1}d\label{7}\ref{2:3:2}\label{4:abc:22}\ref{7}e\label{2:3:1}f",
r"\label{7}abcd\label{7}\ref{2:3:2}\ref{7}ef",
Noisy {
missing_labels: vec!["2:3:2".to_owned()],
duplicated_labels: vec!["7".to_owned()],
inconsistent_length_of_labels: vec![
("7".to_owned(), "3:1:1".to_owned()),
("3:1:1".to_owned(), "7".to_owned()),
("7".to_owned(), "4:abc:22".to_owned()),
],
disordered_labels: vec![("4:abc:22".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
vec![],
),
(
r"\label{7}abc\label{3:1:1}dhyper\ref{3:1:1}\label{7}\ref{2:3:2}\label{4:abc:22}\ref{7}e\label{2:3:1}f",
r"\label{7}abc\label{3:1:1}dhyper\ref{3:1:1}\label{7}\ref{2:3:2}\ref{7}ef",
Noisy {
missing_labels: vec!["2:3:2".to_owned()],
duplicated_labels: vec!["7".to_owned()],
inconsistent_length_of_labels: vec![
("7".to_owned(), "3:1:1".to_owned()),
("3:1:1".to_owned(), "7".to_owned()),
("7".to_owned(), "4:abc:22".to_owned()),
],
disordered_labels: vec![("4:abc:22".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
vec![],
),
(
r"\label{7}abc\label{3:1:1}dhyper\ref{3:1:1}\label{7}\ref{2:3:2}\label{4:abc:22}\ref{7}e\label{2:3:1}f",
r"\label{7}abc\label{3:1:1}d\hyperrefzvav{3:1:1}\label{7}\ref{2:3:2}\ref{7}ef",
Noisy {
missing_labels: vec!["2:3:2".to_owned()],
duplicated_labels: vec!["7".to_owned()],
inconsistent_length_of_labels: vec![
("7".to_owned(), "3:1:1".to_owned()),
("3:1:1".to_owned(), "7".to_owned()),
("7".to_owned(), "4:abc:22".to_owned()),
],
disordered_labels: vec![("4:abc:22".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
vec![Replacement {
from: "hyper".to_owned(),
to: "\\hyperrefzvav".to_owned(),
refkind: true,
..Replacement::default()
}],
),
(
r"\label{7}abc\label{3:1.4:1}dhyper\ref{3:1.4:1}\label{7}\ref{2:3:2}\label{4:abc:22}\ref{7}e\label{2:3:1}f",
r"\label{7}abc\label{3:1.4:1}d\hyperrefzvav{3:1.4:1}\label{7}\ref{2:3:2}\ref{7}ef",
Noisy {
missing_labels: vec!["2:3:2".to_owned()],
duplicated_labels: vec!["7".to_owned()],
inconsistent_length_of_labels: vec![
("7".to_owned(), "3:1.4:1".to_owned()),
("3:1.4:1".to_owned(), "7".to_owned()),
("7".to_owned(), "4:abc:22".to_owned()),
],
disordered_labels: vec![("4:abc:22".to_owned(), "2:3:1".to_owned())],
..Noisy::default()
},
vec![Replacement {
from: "hyper".to_owned(),
to: "\\hyperrefzvav".to_owned(),
refkind: true,
..Replacement::default()
}],
),
];
for (input, output, noisy_output, repls) in tests
{
let mut noisy = Noisy::default();
let mut timers = Timers::default();
assert_eq!(check_labels(input, &repls, &mut noisy, &mut timers).unwrap(), output);
assert_eq!(noisy, noisy_output);
}
}
#[test]
fn labels_fail_tests()
{
let tests = vec![
(r"\label{abc", "Couldn't parse labels"),
(r"\ref{abc", "Couldn't parse refs"),
(r"def\label{abc", "Couldn't parse labels"),
(r"def\ref{abc", "Couldn't parse refs"),
(r"\label{", "Couldn't parse labels"),
(r"\ref{", "Couldn't parse refs"),
(r"def\label{", "Couldn't parse labels"),
(r"def\ref{", "Couldn't parse refs"),
];
for (input, err_msg) in tests
{
assert_eq!(
check_labels(input, &[], &mut Noisy::default(), &mut Timers::default())
.unwrap_err()
.to_string(),
err_msg
);
}
}
#[test]
fn labels_timer_fail_test()
{
let mut timers = Timers::default();
check_labels("", &[], &mut Noisy::default(), &mut timers).unwrap();
assert_eq!(
check_labels("", &[], &mut Noisy::default(), &mut timers)
.unwrap_err()
.to_string(),
"Timer \"remove_labels\" already used"
);
}
}