diff --git a/crates/mdbook-html/src/html/print.rs b/crates/mdbook-html/src/html/print.rs index 5996ef2b..3688a979 100644 --- a/crates/mdbook-html/src/html/print.rs +++ b/crates/mdbook-html/src/html/print.rs @@ -8,7 +8,7 @@ use super::Node; use crate::html::{ChapterTree, Element, serialize}; use crate::utils::{ToUrlPath, id_from_content, normalize_path, unique_id}; use mdbook_core::static_regex; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::{Component, PathBuf}; /// Takes all the chapter trees, modifies them to be suitable to render for @@ -42,12 +42,9 @@ pub(crate) fn render_print_page(mut chapter_trees: Vec>) -> Stri /// been seen. This is used to generate unique IDs. fn make_ids_unique( chapter_trees: &mut [ChapterTree<'_>], -) -> ( - HashMap>, - HashMap, -) { +) -> (HashMap>, HashSet) { let mut id_remap = HashMap::new(); - let mut id_counter = HashMap::new(); + let mut id_counter = HashSet::new(); for ChapterTree { html_path, tree, .. } in chapter_trees @@ -76,7 +73,7 @@ fn make_ids_unique( /// print output has something to link to. fn make_root_id_map( chapter_trees: &mut [ChapterTree<'_>], - id_counter: &mut HashMap, + id_counter: &mut HashSet, ) -> HashMap { let mut path_to_root_id = HashMap::new(); for ChapterTree { diff --git a/crates/mdbook-html/src/html/tree.rs b/crates/mdbook-html/src/html/tree.rs index 802a215e..cd7a570b 100644 --- a/crates/mdbook-html/src/html/tree.rs +++ b/crates/mdbook-html/src/html/tree.rs @@ -820,7 +820,7 @@ where /// to all header elements, and to also add an `` tag so that clicking /// the header will set the current URL to that header's fragment. fn add_header_links(&mut self) { - let mut id_counter = HashMap::new(); + let mut id_counter = HashSet::new(); let headings = self.node_ids_for_tag(&|name| matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")); for heading in headings { diff --git a/crates/mdbook-html/src/utils.rs b/crates/mdbook-html/src/utils.rs index 0fb54613..6c17b8d5 100644 --- a/crates/mdbook-html/src/utils.rs +++ b/crates/mdbook-html/src/utils.rs @@ -1,7 +1,6 @@ //! Utilities for processing HTML. -use std::collections::HashMap; -use std::fmt::Write; +use std::collections::HashSet; use std::path::{Component, Path, PathBuf}; /// Utility function to normalize path elements like `..`. @@ -54,18 +53,23 @@ impl ToUrlPath for Path { /// Make sure an HTML id is unique. /// -/// The `id_counter` map is used to ensure the ID is globally unique. If the -/// same id appears more than once, then it will have a number added to make -/// it unique. -pub(crate) fn unique_id(id: &str, id_counter: &mut HashMap) -> String { - let mut id = id.to_string(); - let id_count = id_counter.entry(id.to_string()).or_insert(0); - if *id_count != 0 { - // FIXME: This should be a loop to ensure that the new ID is also unique. - write!(id, "-{id_count}").unwrap(); +/// Keeps a set of all previously returned IDs; if the requested id is already +/// used, numeric suffixes (-1, -2, ...) are tried until an unused one is found. +pub(crate) fn unique_id(id: &str, used: &mut HashSet) -> String { + if used.insert(id.to_string()) { + return id.to_string(); + } + + // This ID is already in use. Generate one that is not by appending a + // numeric suffix. + let mut counter: u32 = 1; + loop { + let candidate = format!("{id}-{counter}"); + if used.insert(candidate.clone()) { + return candidate; + } + counter += 1; } - *id_count += 1; - id } /// Generates an HTML id from the given text. diff --git a/tests/testsuite/rendering/header_links/expected/header_links.html b/tests/testsuite/rendering/header_links/expected/header_links.html index ae823201..29c35a53 100644 --- a/tests/testsuite/rendering/header_links/expected/header_links.html +++ b/tests/testsuite/rendering/header_links/expected/header_links.html @@ -6,5 +6,5 @@

Repeat

Repeat

Repeat

-

Repeat 1

+

Repeat 1

\ No newline at end of file