jsslint_core/rules/
mod.rs1pub mod abbreviations;
11pub mod bibtex;
12pub mod capitalization;
13pub mod citations;
14pub mod code_style;
15pub mod code_width;
16pub mod crossrefs;
17pub mod house_style;
18pub mod markup;
19pub mod naming;
20pub mod operators;
21pub mod preamble;
22pub mod references;
23pub mod structure;
24pub mod tex_common;
25pub mod typography;
26
27use crate::bib::{Entry, Library};
28use crate::catalogue;
29use crate::report::{Fix, Violation};
30use crate::tex::extract;
31use crate::tex::node::Node as TexNode;
32use crate::tex::prose::{Slot, CITE_MACROS_FOR_SCOPE};
33use std::collections::HashSet;
34
35pub fn py_repr(s: &str) -> String {
44 let has_single = s.contains('\'');
45 let has_double = s.contains('"');
46 let (quote, escape_quote) = if has_single && !has_double {
47 ('"', '"')
48 } else {
49 ('\'', '\'')
50 };
51 let mut out = String::with_capacity(s.len() + 2);
52 out.push(quote);
53 for c in s.chars() {
54 match c {
55 '\\' => out.push_str("\\\\"),
56 '\n' => out.push_str("\\n"),
57 '\r' => out.push_str("\\r"),
58 '\t' => out.push_str("\\t"),
59 c if c == escape_quote => {
60 out.push('\\');
61 out.push(c);
62 }
63 c => out.push(c),
64 }
65 }
66 out.push(quote);
67 out
68}
69
70pub fn entry_line(entry: &Entry) -> u32 {
73 entry.start_line + 1
74}
75
76pub fn entry_violation(
79 file: &str,
80 entry: &Entry,
81 rule_id: &str,
82 suggestion: Option<String>,
83) -> Violation {
84 let meta = catalogue::lookup(rule_id).unwrap_or_else(|| panic!("unknown rule_id {rule_id}"));
85 Violation {
86 file: file.to_string(),
87 line: entry_line(entry),
88 column: None,
89 rule_id: rule_id.to_string(),
90 severity: meta.severity,
91 message: meta.message_template.to_string(),
92 suggestion,
93 fix: None,
94 }
95}
96
97pub fn entry_violation_with_fix(
99 file: &str,
100 entry: &Entry,
101 rule_id: &str,
102 suggestion: Option<String>,
103 fix: Option<Fix>,
104) -> Violation {
105 Violation {
106 fix,
107 ..entry_violation(file, entry, rule_id, suggestion)
108 }
109}
110
111pub fn collect_cited_keys(tex_like: &[&[TexNode]]) -> (HashSet<String>, bool) {
117 let mut keys = HashSet::new();
118 let mut include_all = false;
119 for &nodes in tex_like {
120 extract::iter_with_parent_visit(nodes, &mut |parent: &[Slot], idx, node| {
121 let TexNode::Macro(m) = node else { return };
122 if !CITE_MACROS_FOR_SCOPE.contains(&m.macroname.as_str()) {
123 return;
124 }
125 let text = extract::macro_args_text(m, parent, idx);
126 for raw in text.split(',') {
127 let key = raw.trim();
128 if key == "*" {
129 include_all = true;
130 } else if !key.is_empty() {
131 keys.insert(key.to_string());
132 }
133 }
134 });
135 }
136 (keys, include_all)
137}
138
139pub fn referenced_entries<'a>(library: &'a Library, tex_like: &[&[TexNode]]) -> Vec<&'a Entry> {
144 if tex_like.is_empty() {
145 return library.entries.iter().collect();
146 }
147 let (cited, include_all) = collect_cited_keys(tex_like);
148 let cited_lower: HashSet<String> = cited.iter().map(|k| k.to_lowercase()).collect();
149 library
150 .entries
151 .iter()
152 .filter(|e| include_all || cited_lower.contains(&e.key.to_lowercase()))
153 .collect()
154}
155
156pub fn entry_source_span(source_chars: &[char], entry: &Entry) -> (usize, usize) {
162 let start = line_start_offset(source_chars, entry.start_line);
163 let mut end = source_chars.len();
164 let mut i = start + 1;
165 while i < source_chars.len() {
166 let at_line_start = i == start + 1 || source_chars[i - 1] == '\n';
167 if at_line_start && source_chars[i] == '@' {
168 end = i;
169 break;
170 }
171 i += 1;
172 }
173 (start, end)
174}
175
176fn line_start_offset(chars: &[char], line: u32) -> usize {
177 let mut offset = 0usize;
178 let mut cur = 0u32;
179 while cur < line {
180 match chars[offset..].iter().position(|&c| c == '\n') {
181 Some(rel) => {
182 offset += rel + 1;
183 cur += 1;
184 }
185 None => return chars.len(),
186 }
187 }
188 offset
189}
190
191pub fn field_value_span(
201 source_chars: &[char],
202 entry: &Entry,
203 field_name: &str,
204 expected_value: &str,
205) -> Option<(usize, usize)> {
206 let (es, ee) = entry_source_span(source_chars, entry);
207 let name_chars: Vec<char> = field_name.chars().collect();
208 let mut i = es;
209 while i < ee {
210 let boundary_ok =
211 i == es || !(source_chars[i - 1].is_ascii_alphanumeric() || source_chars[i - 1] == '_');
212 let name_matches = boundary_ok
213 && i + name_chars.len() <= ee
214 && source_chars[i..i + name_chars.len()]
215 .iter()
216 .zip(&name_chars)
217 .all(|(a, b)| a.eq_ignore_ascii_case(b));
218 if !name_matches {
219 i += 1;
220 continue;
221 }
222 let mut j = skip_ws(source_chars, i + name_chars.len(), ee);
223 if j >= ee || source_chars[j] != '=' {
224 i += 1;
225 continue;
226 }
227 j = skip_ws(source_chars, j + 1, ee);
228 if j >= ee {
229 break;
230 }
231 let (mut v_start, mut v_end) = match source_chars[j] {
232 '{' => match find_char(source_chars, j + 1, ee, '}') {
233 Some(close) => (j + 1, close),
234 None => {
235 i += 1;
236 continue;
237 }
238 },
239 '"' => match find_char(source_chars, j + 1, ee, '"') {
240 Some(close) => (j + 1, close),
241 None => {
242 i += 1;
243 continue;
244 }
245 },
246 _ => {
247 i += 1;
248 continue;
249 }
250 };
251 let literal: String = source_chars[v_start..v_end].iter().collect();
252 if literal.trim() != expected_value {
253 i += 1;
254 continue;
255 }
256 let inner = &source_chars[v_start..v_end];
261 let lead_ws = inner.iter().take_while(|c| c.is_whitespace()).count();
262 let trail_ws = inner.iter().rev().take_while(|c| c.is_whitespace()).count();
263 v_start += lead_ws;
264 v_end -= trail_ws.min(inner.len() - lead_ws);
265 return Some((v_start, v_end));
266 }
267 None
268}
269
270fn skip_ws(chars: &[char], mut i: usize, end: usize) -> usize {
271 while i < end && chars[i].is_whitespace() {
272 i += 1;
273 }
274 i
275}
276
277fn find_char(chars: &[char], start: usize, end: usize, needle: char) -> Option<usize> {
278 (start..end).find(|&i| chars[i] == needle)
279}