1use std::path::{Path, PathBuf};
24
25use once_cell::sync::Lazy;
26use regex::Regex;
27
28pub fn find_main_tex(dir: &Path) -> Result<PathBuf, String> {
34 if let Some(filename) = parse_readme_json(dir) {
37 let main_path = dir.join(&filename);
38 if main_path.exists() {
39 return Ok(main_path);
40 }
41 }
42
43 let mut ignored_names: rustc_hash::FxHashSet<PathBuf> = rustc_hash::FxHashSet::default();
52 let readme_xxx = dir.join("00README.XXX");
53 if readme_xxx.exists()
54 && let Ok(content) = std::fs::read_to_string(&readme_xxx)
55 {
56 for line in content.lines() {
57 let parts: Vec<&str> = line.split_whitespace().collect();
58 if parts.len() < 2 {
59 continue;
60 }
61 match parts[1] {
62 "toplevelfile" => {
63 let main_path = dir.join(parts[0]);
64 if main_path.exists() {
65 return Ok(main_path);
66 }
67 },
68 "ignore" => {
69 ignored_names.insert(dir.join(parts[0]));
70 },
71 _ => {},
72 }
73 }
74 }
75
76 let mut tex_files: Vec<PathBuf> = Vec::new();
78 collect_tex_files(dir, &mut tex_files, false);
79 if !ignored_names.is_empty() {
80 tex_files.retain(|p| !ignored_names.contains(p));
81 }
82 let candidates_before_pdf_filter = tex_files.len();
83 tex_files.retain(|p| !is_pdf_magic(p));
84 if tex_files.is_empty() && candidates_before_pdf_filter > 0 {
85 return Err(s(
86 "Fatal:invalid:not_tex_source PDF magic detected in source file (no TeX-format files in archive)",
87 ));
88 }
89 if tex_files.is_empty() {
90 collect_tex_files(dir, &mut tex_files, true);
91 tex_files.retain(|p| !is_pdf_magic(p));
92 if !ignored_names.is_empty() {
93 tex_files.retain(|p| !ignored_names.contains(p));
94 }
95 }
96 if tex_files.is_empty() {
97 return Err(s("No .tex files found in directory"));
98 }
99
100 let mut likelihood: rustc_hash::FxHashMap<PathBuf, f32> = rustc_hash::FxHashMap::default();
102 let mut vetoed: Vec<(PathBuf, PathBuf)> = Vec::new();
107 let mut had_auto_ignore = false;
108
109 for tex_file in &tex_files {
110 if !tex_file.exists() {
111 continue;
112 }
113 let Ok(raw) = std::fs::read(tex_file) else {
114 continue;
115 };
116 let content = String::from_utf8_lossy(&raw);
117 let mut maybe_tex = false;
118 let mut maybe_tex_priority = false;
119 let mut maybe_tex_priority2 = false;
120 let mut determined = false;
121
122 for (lineno, raw_line) in content.lines().enumerate() {
123 let lineno1 = lineno + 1;
124 if lineno1 <= 10
126 && (RE_AUTOIGNORE.is_match(raw_line)
127 || RE_TEXINFO.is_match(raw_line)
128 || RE_AUTOINCLUDE.is_match(raw_line))
129 {
130 likelihood.insert(tex_file.clone(), 0.0);
131 if RE_AUTOIGNORE.is_match(raw_line) {
132 had_auto_ignore = true;
133 }
134 determined = true;
135 break;
136 }
137 if lineno1 <= 12
138 && let Some(cap) = RE_FORMAT_HINT.captures(raw_line)
139 {
140 let fmt = &cap[1];
141 if fmt == "latex209" || fmt == "biglatex" || fmt == "latex" || fmt == "LaTeX" {
142 likelihood.insert(tex_file.clone(), 3.0);
143 } else {
144 likelihood.insert(tex_file.clone(), 1.0);
145 }
146 determined = true;
147 break;
148 }
149 let stripped: std::borrow::Cow<str> = RE_STRIP_COMMENT.replacen(raw_line, 1, "");
153 let line: &str = &stripped;
154
155 if RE_DOCCLASS.is_match(line) {
156 likelihood.insert(tex_file.clone(), 3.0);
157 determined = true;
158 break;
159 }
160 if RE_MAYBE_TEX.is_match(line) {
161 maybe_tex = true;
162 }
163 if let Some(cap) = RE_INPUT_INCLUDE.captures(line) {
165 maybe_tex = true;
166 let mut vetoed_name = cap[1].to_string();
167 if RE_AMSTEX.is_match(&vetoed_name) {
168 likelihood.insert(tex_file.clone(), 2.0);
169 determined = true;
170 break;
171 }
172 if !vetoed_name.contains('.') {
173 vetoed_name = vetoed_name.trim_end().to_string() + ".tex";
174 }
175 let base_dir = tex_file.parent().unwrap_or(dir);
176 vetoed.push((base_dir.join(&vetoed_name), tex_file.clone()));
177 }
178 if RE_END_BYE.is_match(line) {
179 maybe_tex_priority = true;
180 }
181 if RE_END_BYE2.is_match(line) {
182 maybe_tex_priority2 = true;
183 }
184 if RE_MAC_TEX.is_match(line) {
185 likelihood.insert(tex_file.clone(), 1.0);
186 determined = true;
187 break;
188 }
189 if RE_METAFONT.is_match(line) {
190 likelihood.insert(tex_file.clone(), 0.0);
191 determined = true;
192 break;
193 }
194 if RE_BIBTEX.is_match(raw_line) {
195 likelihood.insert(tex_file.clone(), 0.0);
196 determined = true;
197 break;
198 }
199 if RE_UUENCODE.is_match(raw_line) {
200 if maybe_tex_priority {
201 likelihood.insert(tex_file.clone(), 2.0);
202 } else if maybe_tex {
203 likelihood.insert(tex_file.clone(), 1.0);
204 } else {
205 likelihood.insert(tex_file.clone(), 0.0);
206 }
207 determined = true;
208 break;
209 }
210 if RE_WITHDRAWN.is_match(line) {
211 likelihood.insert(tex_file.clone(), 0.0);
212 determined = true;
213 break;
214 }
215 }
216 if !determined {
217 let score = if maybe_tex_priority {
218 2.0
219 } else if maybe_tex_priority2 {
220 1.5
221 } else if maybe_tex {
222 1.0
223 } else {
224 0.0
225 };
226 likelihood.insert(tex_file.clone(), score);
227 }
228 }
229
230 for (vetee, vetoer) in &vetoed {
231 let vetee_score = likelihood.get(vetee).copied().unwrap_or(0.0);
232 let vetoer_score = likelihood.get(vetoer).copied().unwrap_or(0.0);
233 if vetoer_score >= vetee_score {
234 likelihood.remove(vetee);
235 }
236 }
237
238 let mut candidates: Vec<PathBuf> = likelihood
239 .keys()
240 .filter(|f| likelihood[*f] > 0.0)
241 .cloned()
242 .collect();
243 candidates.sort_by(|a, b| likelihood[b].partial_cmp(&likelihood[a]).unwrap());
244
245 if candidates.is_empty() {
246 if had_auto_ignore {
247 let dir_name = dir.file_name().and_then(|s| s.to_str()).unwrap_or_default();
256 let auto_ignore_main = tex_files
257 .iter()
258 .find(|p| {
259 p.file_stem()
260 .and_then(|s| s.to_str())
261 .is_some_and(|stem| stem == dir_name)
262 })
263 .cloned()
264 .or_else(|| tex_files.first().cloned());
265 if let Some(p) = auto_ignore_main {
266 return Ok(p);
267 }
268 }
269 return Err(s("No viable .tex files found in directory"));
270 }
271
272 let max_score = likelihood[&candidates[0]];
273 candidates.retain(|f| (likelihood[f] - max_score).abs() < f32::EPSILON);
274
275 if candidates.len() > 1 {
276 let min_depth = candidates
277 .iter()
278 .map(|f| f.strip_prefix(dir).unwrap_or(f).components().count())
279 .min()
280 .unwrap_or(0);
281 candidates.retain(|f| f.strip_prefix(dir).unwrap_or(f).components().count() == min_depth);
282 }
283
284 if candidates.len() > 1 {
304 let bbl_candidates: Vec<PathBuf> = candidates
305 .iter()
306 .filter(|f| f.with_extension("bbl").exists())
307 .cloned()
308 .collect();
309 if !bbl_candidates.is_empty() {
310 candidates = bbl_candidates;
311 }
312 }
313
314 if candidates.len() > 1 {
315 let pdf_candidates: Vec<PathBuf> = candidates
316 .iter()
317 .filter(|f| has_pdftex_marker(f))
318 .cloned()
319 .collect();
320 if !pdf_candidates.is_empty() {
321 candidates = pdf_candidates;
322 }
323 }
324
325 if candidates.len() > 1 {
326 let common: Vec<PathBuf> = candidates
327 .iter()
328 .filter(|f| {
329 f.file_name().is_some_and(|n| {
330 let n = n.to_str().unwrap_or("");
331 n == "main.tex" || n == "ms.tex" || n == "paper.tex"
332 })
333 })
334 .cloned()
335 .collect();
336 if !common.is_empty() {
337 candidates = common;
338 }
339 }
340
341 candidates.sort();
342 Ok(
343 candidates
344 .into_iter()
345 .next()
346 .expect("non-empty after filtering"),
347 )
348}
349
350pub fn find_top_level_texs(dir: &Path) -> Result<Vec<PathBuf>, String> {
371 let explicit = explicit_top_levels(dir);
373 if explicit.len() >= 2 {
374 return Ok(order_main_first(explicit, dir));
375 }
376 let main = find_main_tex(dir)?;
377 let mut set = top_level_bbl_docs(dir, &main);
381 if !set.contains(&main) {
382 set.push(main.clone());
383 }
384 let (mut supps, mains): (Vec<PathBuf>, Vec<PathBuf>) =
389 set.into_iter().partition(|f| is_supplement(f));
390 if mains.len() == 1 && !supps.is_empty() {
391 supps.sort();
392 supps.dedup();
393 let mut tops = mains;
394 tops.extend(supps);
395 return Ok(tops);
396 }
397 Ok(vec![main])
399}
400
401fn s(msg: &str) -> String { msg.to_string() }
406
407fn explicit_top_levels(dir: &Path) -> Vec<PathBuf> {
411 let mut out: Vec<PathBuf> = Vec::new();
412 for name in parse_readme_json_all(dir) {
413 let p = dir.join(&name);
414 if p.exists() && !out.contains(&p) {
415 out.push(p);
416 }
417 }
418 let readme_xxx = dir.join("00README.XXX");
419 if let Ok(content) = std::fs::read_to_string(&readme_xxx) {
420 for line in content.lines() {
421 let parts: Vec<&str> = line.split_whitespace().collect();
422 if parts.len() >= 2 && parts[1] == "toplevelfile" {
423 let p = dir.join(parts[0]);
424 if p.exists() && !out.contains(&p) {
425 out.push(p);
426 }
427 }
428 }
429 }
430 out
431}
432
433fn top_level_bbl_docs(dir: &Path, main: &Path) -> Vec<PathBuf> {
438 let main_depth = main.strip_prefix(dir).unwrap_or(main).components().count();
439 let ignored = readme_ignored(dir);
440 let mut tex_files: Vec<PathBuf> = Vec::new();
441 collect_tex_files(dir, &mut tex_files, false);
442 tex_files
443 .into_iter()
444 .filter(|f| f.strip_prefix(dir).unwrap_or(f).components().count() == main_depth)
445 .filter(|f| !ignored.contains(f))
446 .filter(|f| !has_auto_ignore(f))
447 .filter(|f| f.with_extension("bbl").exists())
448 .filter(|f| file_has_documentclass(f))
449 .collect()
450}
451
452fn readme_ignored(dir: &Path) -> rustc_hash::FxHashSet<PathBuf> {
455 let mut ignored = rustc_hash::FxHashSet::default();
456 if let Ok(content) = std::fs::read_to_string(dir.join("00README.XXX")) {
457 for line in content.lines() {
458 let parts: Vec<&str> = line.split_whitespace().collect();
459 if parts.len() >= 2 && parts[1] == "ignore" {
460 ignored.insert(dir.join(parts[0]));
461 }
462 }
463 }
464 ignored
465}
466
467fn has_auto_ignore(path: &Path) -> bool {
470 let Ok(raw) = std::fs::read(path) else {
471 return false;
472 };
473 String::from_utf8_lossy(&raw)
474 .lines()
475 .take(10)
476 .any(|l| RE_AUTOIGNORE.is_match(l))
477}
478
479fn order_main_first(mut files: Vec<PathBuf>, _dir: &Path) -> Vec<PathBuf> {
483 files.sort();
484 files.dedup();
485 let (supps, mut mains): (Vec<PathBuf>, Vec<PathBuf>) =
486 files.into_iter().partition(|f| is_supplement(f));
487 if let Some(pos) = mains.iter().position(|f| is_common_main_name(f)) {
488 let m = mains.remove(pos);
489 mains.insert(0, m);
490 }
491 mains.extend(supps);
492 mains
493}
494
495fn is_common_main_name(path: &Path) -> bool {
498 path
499 .file_name()
500 .and_then(|n| n.to_str())
501 .is_some_and(|n| n == "main.tex" || n == "ms.tex" || n == "paper.tex")
502}
503
504fn is_supplement(path: &Path) -> bool {
510 if let Some(stem) = path.file_stem().and_then(|s| s.to_str())
511 && RE_SUPP_NAME.is_match(stem)
512 {
513 return true;
514 }
515 if let Some(title) = first_title(path)
516 && RE_SUPP_TITLE.is_match(&title)
517 {
518 return true;
519 }
520 false
521}
522
523fn file_has_documentclass(path: &Path) -> bool {
526 let Ok(raw) = std::fs::read(path) else {
527 return false;
528 };
529 let content = String::from_utf8_lossy(&raw);
530 content.lines().any(|l| {
531 let stripped = RE_STRIP_COMMENT.replacen(l, 1, "");
532 RE_DOCCLASS.is_match(&stripped)
533 })
534}
535
536fn first_title(path: &Path) -> Option<String> {
539 let raw = std::fs::read(path).ok()?;
540 let content = String::from_utf8_lossy(&raw);
541 let stripped: String = content
544 .lines()
545 .map(|l| RE_STRIP_COMMENT.replacen(l, 1, "").into_owned())
546 .collect::<Vec<_>>()
547 .join("\n");
548 RE_TITLE_ARG
549 .captures(&stripped)
550 .map(|c| c[1].chars().take(120).collect())
551}
552
553fn parse_readme_json_all(dir: &Path) -> Vec<String> {
557 let mut out = Vec::new();
558 let Ok(content) = std::fs::read_to_string(dir.join("00README.json")) else {
559 return out;
560 };
561 let Some(sources_start) = content.find("\"sources\"") else {
562 return out;
563 };
564 let rest = &content[sources_start..];
565 let (Some(arr_start), Some(arr_end)) = (rest.find('['), rest.find(']')) else {
566 return out;
567 };
568 let arr = &rest[arr_start + 1..arr_end];
569 for obj_str in arr.split('}') {
570 if !obj_str.contains("\"toplevel\"") {
571 continue;
572 }
573 if let Some(fn_pos) = obj_str.find("\"filename\"") {
574 let after = obj_str[fn_pos + 10..].trim_start();
575 if let Some(after) = after.strip_prefix(':') {
576 let after = after.trim_start();
577 if let Some(after) = after.strip_prefix('"') {
578 let name: String = after
579 .chars()
580 .take_while(|&c| c != '"')
581 .filter(|&c| c != '\\')
582 .collect();
583 if !name.is_empty() {
584 out.push(name);
585 }
586 }
587 }
588 }
589 }
590 out
591}
592
593fn collect_tex_files(dir: &Path, files: &mut Vec<PathBuf>, fallback: bool) {
598 if let Ok(entries) = std::fs::read_dir(dir) {
599 for entry in entries.flatten() {
600 let path = entry.path();
601 if path.is_dir() {
602 collect_tex_files(&path, files, fallback);
603 } else if !fallback {
604 if path.extension().is_some_and(|e| {
605 let e = e.to_ascii_lowercase();
606 e == "tex" || e == "txt" || e == "ltx"
607 }) {
608 files.push(path);
609 }
610 } else {
611 let ext_opt = path.extension().and_then(|e| e.to_str());
612 let keep = match ext_opt {
613 None => true,
614 Some(ext) => ext.len() >= 4,
615 };
616 if keep {
617 files.push(path);
618 }
619 }
620 }
621 }
622}
623
624fn has_pdftex_marker(path: &Path) -> bool {
642 let Ok(raw) = std::fs::read(path) else {
643 return false;
644 };
645 let content = String::from_utf8_lossy(&raw);
646 content
647 .lines()
648 .any(|line| RE_PDF_INCLUDE.is_match(line) || RE_PDFOUTPUT.is_match(line))
649}
650
651pub fn is_pdf_magic(path: &Path) -> bool {
654 let mut buf = [0u8; 5];
655 if let Ok(mut f) = std::fs::File::open(path) {
656 use std::io::Read;
657 if f.read(&mut buf).is_ok_and(|n| n == 5) {
658 return &buf == b"%PDF-";
659 }
660 }
661 false
662}
663
664fn parse_readme_json(dir: &Path) -> Option<String> { parse_readme_json_all(dir).into_iter().next() }
669
670static RE_AUTOIGNORE: Lazy<Regex> = Lazy::new(|| Regex::new(r"%auto-ignore").unwrap());
677static RE_TEXINFO: Lazy<Regex> = Lazy::new(|| Regex::new(r"\\input texinfo").unwrap());
678static RE_AUTOINCLUDE: Lazy<Regex> = Lazy::new(|| Regex::new(r"%auto-include").unwrap());
679static RE_FORMAT_HINT: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\r?%&(\S+)").unwrap());
680static RE_DOCCLASS: Lazy<Regex> =
681 Lazy::new(|| Regex::new(r"(?:^|\r)\s*\\document(?:style|class)").unwrap());
682static RE_MAYBE_TEX: Lazy<Regex> = Lazy::new(|| {
683 Regex::new(r"(?:^|\r)\s*\\(?:font|magnification|input|def|special|baselineskip|begin)").unwrap()
684});
685static RE_INPUT_INCLUDE: Lazy<Regex> =
686 Lazy::new(|| Regex::new(r"\\(?:input|include)(?:\s+|\{)([^ \}]+)").unwrap());
687static RE_END_BYE: Lazy<Regex> =
688 Lazy::new(|| Regex::new(r"(?:^|\r)\s*\\(?:end|bye)(?:\s|$)").unwrap());
689static RE_END_BYE2: Lazy<Regex> = Lazy::new(|| Regex::new(r"\\(?:end|bye)(?:\s|$)").unwrap());
690static RE_MAC_TEX: Lazy<Regex> =
691 Lazy::new(|| Regex::new(r"\\input *(?:harv|lanl)mac|\\input\s+phyzzx").unwrap());
692static RE_METAFONT: Lazy<Regex> = Lazy::new(|| Regex::new(r"beginchar\(").unwrap());
693static RE_BIBTEX: Lazy<Regex> =
694 Lazy::new(|| Regex::new(r"(?i)(?:^|\r)@(?:book|article|inbook|unpublished)\{").unwrap());
695static RE_UUENCODE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^begin \d{1,4}\s+\S+\r?$").unwrap());
696static RE_WITHDRAWN: Lazy<Regex> =
697 Lazy::new(|| Regex::new(r"paper deliberately replaced by what little").unwrap());
698static RE_AMSTEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^amstex$").unwrap());
699static RE_PDF_INCLUDE: Lazy<Regex> =
702 Lazy::new(|| Regex::new(r"(?i)^[^%]*\\includegraphics[^%]*\.(?:pdf|png|gif|jpg)\s?\}").unwrap());
703static RE_PDFOUTPUT: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[^%]*\\pdfoutput\s*=\s*1").unwrap());
705static RE_SUPP_NAME: Lazy<Regex> = Lazy::new(|| {
709 Regex::new(r"(?i)(^|[_-])(supp(lement(ary|al)?)?|supporting|supplemental|appendix|si|sm)([_-]|$)")
710 .unwrap()
711});
712static RE_SUPP_TITLE: Lazy<Regex> = Lazy::new(|| {
715 Regex::new(r"(?i)(supplement|supporting\s+information|supplemental|appendix)").unwrap()
716});
717static RE_TITLE_ARG: Lazy<Regex> =
719 Lazy::new(|| Regex::new(r"\\(?:icml)?title\s*(?:\[[^\]]*\])?\s*\{([^}]*)").unwrap());
720static RE_STRIP_COMMENT: Lazy<Regex> = Lazy::new(|| Regex::new(r"%[^\r]*").unwrap());
723
724#[cfg(test)]
725mod tests {
726 use tempfile::tempdir;
727
728 use super::*;
729
730 fn write(dir: &Path, name: &str, body: &str) { std::fs::write(dir.join(name), body).unwrap(); }
731
732 #[test]
733 fn picks_documentclass_over_input_only_file() {
734 let d = tempdir().unwrap();
735 write(
736 d.path(),
737 "main.tex",
738 "\\documentclass{article}\n\\begin{document}x\\end{document}",
739 );
740 write(d.path(), "intro.tex", "\\input{intro_body}\n");
741 let pick = find_main_tex(d.path()).unwrap();
742 assert_eq!(pick.file_name().unwrap(), "main.tex");
743 }
744
745 #[test]
746 fn errors_when_directory_has_no_tex() {
747 let d = tempdir().unwrap();
748 let err = find_main_tex(d.path()).unwrap_err();
749 assert!(err.contains("No .tex files"));
750 }
751
752 #[test]
753 fn prefers_main_over_other_documentclass_files() {
754 let d = tempdir().unwrap();
755 write(d.path(), "draft.tex", "\\documentclass{article}\n");
756 write(d.path(), "main.tex", "\\documentclass{article}\n");
757 let pick = find_main_tex(d.path()).unwrap();
758 assert_eq!(pick.file_name().unwrap(), "main.tex");
759 }
760
761 #[test]
762 fn readme_xxx_toplevelfile_directive_short_circuits() {
763 let d = tempdir().unwrap();
764 write(d.path(), "draft.tex", "\\documentclass{article}\n");
767 write(d.path(), "main.tex", "\\documentclass{article}\n");
768 write(d.path(), "00README.XXX", "draft.tex toplevelfile\n");
769 let pick = find_main_tex(d.path()).unwrap();
770 assert_eq!(pick.file_name().unwrap(), "draft.tex");
771 }
772
773 #[test]
774 fn readme_xxx_ignore_directive_excludes_candidate() {
775 let d = tempdir().unwrap();
776 write(d.path(), "old.tex", "\\documentclass{article}\n");
781 write(d.path(), "main.tex", "\\documentclass{article}\n");
782 write(d.path(), "00README.XXX", "main.tex ignore\n");
783 let pick = find_main_tex(d.path()).unwrap();
784 assert_eq!(pick.file_name().unwrap(), "old.tex");
785 }
786
787 #[test]
788 fn readme_xxx_mixed_directives() {
789 let d = tempdir().unwrap();
790 write(d.path(), "intro.tex", "\\documentclass{article}\n");
791 write(d.path(), "paper.tex", "\\documentclass{article}\n");
792 write(d.path(), "junk.tex", "\\documentclass{article}\n");
793 write(
797 d.path(),
798 "00README.XXX",
799 "junk.tex ignore\npaper.tex toplevelfile\n",
800 );
801 let pick = find_main_tex(d.path()).unwrap();
802 assert_eq!(pick.file_name().unwrap(), "paper.tex");
803 }
804
805 #[test]
812 fn bbl_sibling_outranks_pdf_include_marker() {
813 let d = tempdir().unwrap();
814 write(
815 d.path(),
816 "main.tex",
817 "\\documentclass{IEEEtran}\n\\input{sections/intro}\n\\begin{document}\\title{Real}\\end{document}",
818 );
819 write(
820 d.path(),
821 "New_IEEEtran_how-to.tex",
822 "\\documentclass{IEEEtran}\n\\includegraphics[width=1in]{fig1.png}\n\\begin{document}How to.\\end{document}",
823 );
824 write(
825 d.path(),
826 "main.bbl",
827 "\\begin{thebibliography}{1}\\end{thebibliography}\n",
828 );
829 let pick = find_main_tex(d.path()).unwrap();
830 assert_eq!(pick.file_name().unwrap(), "main.tex");
831 }
832
833 #[test]
837 fn reorder_preserves_common_name_when_multiple_bbl() {
838 let d = tempdir().unwrap();
839 write(
840 d.path(),
841 "aaa.tex",
842 "\\documentclass{article}\n\\includegraphics{x.png}\n",
843 );
844 write(
845 d.path(),
846 "main.tex",
847 "\\documentclass{article}\n\\includegraphics{y.png}\n",
848 );
849 write(d.path(), "aaa.bbl", "");
850 write(d.path(), "main.bbl", "");
851 let pick = find_main_tex(d.path()).unwrap();
852 assert_eq!(pick.file_name().unwrap(), "main.tex");
853 }
854
855 #[test]
861 fn strict_pdf_marker_rejects_extensionless_eps_and_comments() {
862 let d = tempdir().unwrap();
863 write(
865 d.path(),
866 "example_paper.tex",
867 "\\centerline{\\includegraphics[width=\\columnwidth]{icml_numpapers}}\nResults saved as results.png.\n",
868 );
869 write(
871 d.path(),
872 "rebuttal.tex",
873 "\\includegraphics[width=0.8\\linewidth]{egfigure.eps}\n",
874 );
875 write(d.path(), "commented.tex", "%\\includegraphics{fig.png}\n");
877 assert!(!has_pdftex_marker(&d.path().join("example_paper.tex")));
878 assert!(!has_pdftex_marker(&d.path().join("rebuttal.tex")));
879 assert!(!has_pdftex_marker(&d.path().join("commented.tex")));
880 }
881
882 #[test]
883 fn strict_pdf_marker_accepts_arg_extension_and_pdfoutput() {
884 let d = tempdir().unwrap();
885 write(
886 d.path(),
887 "fig.tex",
888 "\\includegraphics[width=2in]{diagram.pdf}\n",
889 );
890 write(d.path(), "gif.tex", "\\includegraphics{anim.gif}\n"); write(
892 d.path(),
893 "pdfout.tex",
894 "\\pdfoutput=1\n\\includegraphics{noext}\n",
895 );
896 assert!(has_pdftex_marker(&d.path().join("fig.tex")));
897 assert!(has_pdftex_marker(&d.path().join("gif.tex")));
898 assert!(has_pdftex_marker(&d.path().join("pdfout.tex")));
899 }
900
901 #[test]
905 fn top_level_texs_appends_supplement() {
906 let d = tempdir().unwrap();
907 write(
908 d.path(),
909 "main.tex",
910 "\\documentclass{article}\n\\title{Quantum error correction}\n",
911 );
912 write(
913 d.path(),
914 "supplement.tex",
915 "\\documentclass{article}\n\\title{Supplementary Information for Quantum error correction}\n",
916 );
917 write(d.path(), "main.bbl", "");
918 write(d.path(), "supplement.bbl", "");
919 let tops = find_top_level_texs(d.path()).unwrap();
920 let names: Vec<_> = tops
921 .iter()
922 .map(|p| p.file_name().unwrap().to_str().unwrap())
923 .collect();
924 assert_eq!(names, vec!["main.tex", "supplement.tex"]);
925 assert_eq!(find_main_tex(d.path()).unwrap(), tops[0]);
927 }
928
929 #[test]
932 fn top_level_texs_orders_main_first_despite_alpha() {
933 let d = tempdir().unwrap();
934 write(
935 d.path(),
936 "Note.tex",
937 "\\documentclass{article}\n\\title{Bayesian Inference of the Landau Parameter}\n",
938 );
939 write(
940 d.path(),
941 "Appendix.tex",
942 "\\documentclass{article}\n\\title{SUPPLEMENTAL MATERIALS}\n",
943 );
944 write(d.path(), "Note.bbl", "");
945 write(d.path(), "Appendix.bbl", "");
946 let tops = find_top_level_texs(d.path()).unwrap();
947 let names: Vec<_> = tops
948 .iter()
949 .map(|p| p.file_name().unwrap().to_str().unwrap())
950 .collect();
951 assert_eq!(names, vec!["Note.tex", "Appendix.tex"]);
952 }
953
954 #[test]
957 fn top_level_texs_excludes_template_without_bbl() {
958 let d = tempdir().unwrap();
959 write(
960 d.path(),
961 "main.tex",
962 "\\documentclass{IEEEtran}\n\\input{sec}\n\\title{Real Paper}\n",
963 );
964 write(
965 d.path(),
966 "New_IEEEtran_how-to.tex",
967 "\\documentclass{IEEEtran}\n\\includegraphics{fig1.png}\n\\title{How to Use the IEEEtran Templates}\n",
968 );
969 write(d.path(), "main.bbl", "");
970 let tops = find_top_level_texs(d.path()).unwrap();
971 let names: Vec<_> = tops
972 .iter()
973 .map(|p| p.file_name().unwrap().to_str().unwrap())
974 .collect();
975 assert_eq!(names, vec!["main.tex"]);
976 }
977
978 #[test]
981 fn supplement_without_bbl_is_not_detected() {
982 let d = tempdir().unwrap();
983 write(
984 d.path(),
985 "main.tex",
986 "\\documentclass{article}\n\\title{Real Paper}\n",
987 );
988 write(
989 d.path(),
990 "supp.tex",
991 "\\documentclass{article}\n\\title{Supplementary Information}\n",
992 );
993 write(d.path(), "main.bbl", "");
994 let tops = find_top_level_texs(d.path()).unwrap();
995 assert_eq!(tops.len(), 1);
996 assert_eq!(tops[0].file_name().unwrap(), "main.tex");
997 }
998
999 #[test]
1002 fn readme_two_toplevelfiles_ordered_main_first() {
1003 let d = tempdir().unwrap();
1004 write(
1005 d.path(),
1006 "main.tex",
1007 "\\documentclass{article}\n\\title{Paper}\n",
1008 );
1009 write(
1010 d.path(),
1011 "si.tex",
1012 "\\documentclass{article}\n\\title{Supporting Information}\n",
1013 );
1014 write(
1015 d.path(),
1016 "00README.XXX",
1017 "si.tex toplevelfile\nmain.tex toplevelfile\n",
1018 );
1019 let tops = find_top_level_texs(d.path()).unwrap();
1020 let names: Vec<_> = tops
1021 .iter()
1022 .map(|p| p.file_name().unwrap().to_str().unwrap())
1023 .collect();
1024 assert_eq!(names, vec!["main.tex", "si.tex"]);
1025 }
1026}