From 67b4260021845552d2f031f2dbf66a2d9cc17a5f Mon Sep 17 00:00:00 2001 From: Gabor Szabo Date: Sat, 22 Mar 2025 13:02:25 +0200 Subject: [PATCH] Avoid using the same file twice in SUMMARY.md See #2612 --- src/book/summary.rs | 108 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/book/summary.rs b/src/book/summary.rs index d6daf2a1c8..820230936a 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -3,6 +3,7 @@ use log::{debug, trace, warn}; use memchr::Memchr; use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use std::fmt::{self, Display, Formatter}; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; @@ -245,6 +246,11 @@ impl<'a> SummaryParser<'a> { .parse_affix(false) .with_context(|| "There was an error parsing the suffix chapters")?; + let mut files = HashSet::new(); + for part in [&prefix_chapters, &numbered_chapters, &suffix_chapters] { + self.check_for_duplicates(&part, &mut files)?; + } + Ok(Summary { title, prefix_chapters, @@ -253,6 +259,29 @@ impl<'a> SummaryParser<'a> { }) } + /// Recursively check for duplicate files in the summary items. + fn check_for_duplicates<'b>( + &self, + items: &'b [SummaryItem], + files: &mut HashSet<&'b PathBuf>, + ) -> Result<()> { + for item in items { + if let SummaryItem::Link(link) = item { + if let Some(location) = &link.location { + if !files.insert(location) { + bail!(anyhow::anyhow!( + "Duplicate file in SUMMARY.md: {:?}", + location + )); + } + } + // Recursively check nested items + self.check_for_duplicates(&link.nested_items, files)?; + } + } + Ok(()) + } + /// Parse the affix chapters. fn parse_affix(&mut self, is_prefix: bool) -> Result> { let mut items = Vec::new(); @@ -1127,4 +1156,83 @@ mod tests { let got = parser.parse_affix(false).unwrap(); assert_eq!(got, should_be); } + + #[test] + fn duplicate_entries_1() { + let src = r#" +# Summary +- [A](./a.md) +- [A](./a.md) +"#; + + let res = parse_summary(src); + assert!(res.is_err()); + let error_message = res.err().unwrap().to_string(); + assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#); + } + + #[test] + fn duplicate_entries_2() { + let src = r#" +# Summary +- [A](./a.md) + - [A](./a.md) +"#; + + let res = parse_summary(src); + assert!(res.is_err()); + let error_message = res.err().unwrap().to_string(); + assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#); + } + #[test] + fn duplicate_entries_3() { + let src = r#" +# Summary +- [A](./a.md) +- [B](./b.md) + - [A](./a.md) +"#; + + let res = parse_summary(src); + assert!(res.is_err()); + let error_message = res.err().unwrap().to_string(); + assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#); + } + + #[test] + fn duplicate_entries_4() { + let src = r#" +# Summary +[A](./a.md) +- [B](./b.md) +- [A](./a.md) +"#; + + let res = parse_summary(src); + assert!(res.is_err()); + let error_message = res.err().unwrap().to_string(); + assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#); + } + + #[test] + fn duplicate_entries_5() { + let src = r#" +# Summary +[A](./a.md) + +# hi +- [B](./b.md) + +# bye + +--- + +[A](./a.md) +"#; + + let res = parse_summary(src); + assert!(res.is_err()); + let error_message = res.err().unwrap().to_string(); + assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#); + } }