pub mod abbreviations;
pub mod bibtex;
pub mod capitalization;
pub mod citations;
pub mod code_style;
pub mod code_width;
pub mod crossrefs;
pub mod house_style;
pub mod markup;
pub mod naming;
pub mod operators;
pub mod preamble;
pub mod references;
pub mod structure;
pub mod tex_common;
pub mod typography;
use crate::bib::{Entry, Library};
use crate::catalogue;
use crate::report::{Fix, Violation};
use crate::tex::extract;
use crate::tex::node::Node as TexNode;
use crate::tex::prose::{Slot, CITE_MACROS_FOR_SCOPE};
use std::collections::HashSet;
pub fn py_repr(s: &str) -> String {
let has_single = s.contains('\'');
let has_double = s.contains('"');
let (quote, escape_quote) = if has_single && !has_double {
('"', '"')
} else {
('\'', '\'')
};
let mut out = String::with_capacity(s.len() + 2);
out.push(quote);
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c == escape_quote => {
out.push('\\');
out.push(c);
}
c => out.push(c),
}
}
out.push(quote);
out
}
pub fn entry_line(entry: &Entry) -> u32 {
entry.start_line + 1
}
pub fn entry_violation(
file: &str,
entry: &Entry,
rule_id: &str,
suggestion: Option<String>,
) -> Violation {
let meta = catalogue::lookup(rule_id).unwrap_or_else(|| panic!("unknown rule_id {rule_id}"));
Violation {
file: file.to_string(),
line: entry_line(entry),
column: None,
rule_id: rule_id.to_string(),
severity: meta.severity,
message: meta.message_template.to_string(),
suggestion,
fix: None,
}
}
pub fn entry_violation_with_fix(
file: &str,
entry: &Entry,
rule_id: &str,
suggestion: Option<String>,
fix: Option<Fix>,
) -> Violation {
Violation {
fix,
..entry_violation(file, entry, rule_id, suggestion)
}
}
pub fn collect_cited_keys(tex_like: &[&[TexNode]]) -> (HashSet<String>, bool) {
let mut keys = HashSet::new();
let mut include_all = false;
for &nodes in tex_like {
extract::iter_with_parent_visit(nodes, &mut |parent: &[Slot], idx, node| {
let TexNode::Macro(m) = node else { return };
if !CITE_MACROS_FOR_SCOPE.contains(&m.macroname.as_str()) {
return;
}
let text = extract::macro_args_text(m, parent, idx);
for raw in text.split(',') {
let key = raw.trim();
if key == "*" {
include_all = true;
} else if !key.is_empty() {
keys.insert(key.to_string());
}
}
});
}
(keys, include_all)
}
pub fn referenced_entries<'a>(library: &'a Library, tex_like: &[&[TexNode]]) -> Vec<&'a Entry> {
if tex_like.is_empty() {
return library.entries.iter().collect();
}
let (cited, include_all) = collect_cited_keys(tex_like);
let cited_lower: HashSet<String> = cited.iter().map(|k| k.to_lowercase()).collect();
library
.entries
.iter()
.filter(|e| include_all || cited_lower.contains(&e.key.to_lowercase()))
.collect()
}
pub fn entry_source_span(source_chars: &[char], entry: &Entry) -> (usize, usize) {
let start = line_start_offset(source_chars, entry.start_line);
let mut end = source_chars.len();
let mut i = start + 1;
while i < source_chars.len() {
let at_line_start = i == start + 1 || source_chars[i - 1] == '\n';
if at_line_start && source_chars[i] == '@' {
end = i;
break;
}
i += 1;
}
(start, end)
}
fn line_start_offset(chars: &[char], line: u32) -> usize {
let mut offset = 0usize;
let mut cur = 0u32;
while cur < line {
match chars[offset..].iter().position(|&c| c == '\n') {
Some(rel) => {
offset += rel + 1;
cur += 1;
}
None => return chars.len(),
}
}
offset
}
pub fn field_value_span(
source_chars: &[char],
entry: &Entry,
field_name: &str,
expected_value: &str,
) -> Option<(usize, usize)> {
let (es, ee) = entry_source_span(source_chars, entry);
let name_chars: Vec<char> = field_name.chars().collect();
let mut i = es;
while i < ee {
let boundary_ok =
i == es || !(source_chars[i - 1].is_ascii_alphanumeric() || source_chars[i - 1] == '_');
let name_matches = boundary_ok
&& i + name_chars.len() <= ee
&& source_chars[i..i + name_chars.len()]
.iter()
.zip(&name_chars)
.all(|(a, b)| a.eq_ignore_ascii_case(b));
if !name_matches {
i += 1;
continue;
}
let mut j = skip_ws(source_chars, i + name_chars.len(), ee);
if j >= ee || source_chars[j] != '=' {
i += 1;
continue;
}
j = skip_ws(source_chars, j + 1, ee);
if j >= ee {
break;
}
let (mut v_start, mut v_end) = match source_chars[j] {
'{' => match find_char(source_chars, j + 1, ee, '}') {
Some(close) => (j + 1, close),
None => {
i += 1;
continue;
}
},
'"' => match find_char(source_chars, j + 1, ee, '"') {
Some(close) => (j + 1, close),
None => {
i += 1;
continue;
}
},
_ => {
i += 1;
continue;
}
};
let literal: String = source_chars[v_start..v_end].iter().collect();
if literal.trim() != expected_value {
i += 1;
continue;
}
let inner = &source_chars[v_start..v_end];
let lead_ws = inner.iter().take_while(|c| c.is_whitespace()).count();
let trail_ws = inner.iter().rev().take_while(|c| c.is_whitespace()).count();
v_start += lead_ws;
v_end -= trail_ws.min(inner.len() - lead_ws);
return Some((v_start, v_end));
}
None
}
fn skip_ws(chars: &[char], mut i: usize, end: usize) -> usize {
while i < end && chars[i].is_whitespace() {
i += 1;
}
i
}
fn find_char(chars: &[char], start: usize, end: usize, needle: char) -> Option<usize> {
(start..end).find(|&i| chars[i] == needle)
}