Skip to content

Commit 53d39a8

Browse files
authored
Merge pull request #2846 from ehuss/fix-unique-id-loop
Fix ID collisions when the numeric suffix gets used
2 parents 51d7998 + f173132 commit 53d39a8

File tree

4 files changed

+23
-22
lines changed

4 files changed

+23
-22
lines changed

crates/mdbook-html/src/html/print.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use super::Node;
88
use crate::html::{ChapterTree, Element, serialize};
99
use crate::utils::{ToUrlPath, id_from_content, normalize_path, unique_id};
1010
use mdbook_core::static_regex;
11-
use std::collections::HashMap;
11+
use std::collections::{HashMap, HashSet};
1212
use std::path::{Component, PathBuf};
1313

1414
/// 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<ChapterTree<'_>>) -> Stri
4242
/// been seen. This is used to generate unique IDs.
4343
fn make_ids_unique(
4444
chapter_trees: &mut [ChapterTree<'_>],
45-
) -> (
46-
HashMap<PathBuf, HashMap<String, String>>,
47-
HashMap<String, u32>,
48-
) {
45+
) -> (HashMap<PathBuf, HashMap<String, String>>, HashSet<String>) {
4946
let mut id_remap = HashMap::new();
50-
let mut id_counter = HashMap::new();
47+
let mut id_counter = HashSet::new();
5148
for ChapterTree {
5249
html_path, tree, ..
5350
} in chapter_trees
@@ -76,7 +73,7 @@ fn make_ids_unique(
7673
/// print output has something to link to.
7774
fn make_root_id_map(
7875
chapter_trees: &mut [ChapterTree<'_>],
79-
id_counter: &mut HashMap<String, u32>,
76+
id_counter: &mut HashSet<String>,
8077
) -> HashMap<PathBuf, String> {
8178
let mut path_to_root_id = HashMap::new();
8279
for ChapterTree {

crates/mdbook-html/src/html/tree.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ where
821821
/// to all header elements, and to also add an `<a>` tag so that clicking
822822
/// the header will set the current URL to that header's fragment.
823823
fn add_header_links(&mut self) {
824-
let mut id_counter = HashMap::new();
824+
let mut id_counter = HashSet::new();
825825
let headings =
826826
self.node_ids_for_tag(&|name| matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6"));
827827
for heading in headings {

crates/mdbook-html/src/utils.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! Utilities for processing HTML.
22
3-
use std::collections::HashMap;
4-
use std::fmt::Write;
3+
use std::collections::HashSet;
54
use std::path::{Component, Path, PathBuf};
65

76
/// Utility function to normalize path elements like `..`.
@@ -54,18 +53,23 @@ impl ToUrlPath for Path {
5453

5554
/// Make sure an HTML id is unique.
5655
///
57-
/// The `id_counter` map is used to ensure the ID is globally unique. If the
58-
/// same id appears more than once, then it will have a number added to make
59-
/// it unique.
60-
pub(crate) fn unique_id(id: &str, id_counter: &mut HashMap<String, u32>) -> String {
61-
let mut id = id.to_string();
62-
let id_count = id_counter.entry(id.to_string()).or_insert(0);
63-
if *id_count != 0 {
64-
// FIXME: This should be a loop to ensure that the new ID is also unique.
65-
write!(id, "-{id_count}").unwrap();
56+
/// Keeps a set of all previously returned IDs; if the requested id is already
57+
/// used, numeric suffixes (-1, -2, ...) are tried until an unused one is found.
58+
pub(crate) fn unique_id(id: &str, used: &mut HashSet<String>) -> String {
59+
if used.insert(id.to_string()) {
60+
return id.to_string();
61+
}
62+
63+
// This ID is already in use. Generate one that is not by appending a
64+
// numeric suffix.
65+
let mut counter: u32 = 1;
66+
loop {
67+
let candidate = format!("{id}-{counter}");
68+
if used.insert(candidate.clone()) {
69+
return candidate;
70+
}
71+
counter += 1;
6672
}
67-
*id_count += 1;
68-
id
6973
}
7074

7175
/// Generates an HTML id from the given text.

tests/testsuite/rendering/header_links/expected/header_links.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ <h2 id="hï"><a class="header" href="#hï">Hï</a></h2>
66
<h2 id="repeat"><a class="header" href="#repeat">Repeat</a></h2>
77
<h2 id="repeat-1"><a class="header" href="#repeat-1">Repeat</a></h2>
88
<h2 id="repeat-2"><a class="header" href="#repeat-2">Repeat</a></h2>
9-
<h2 id="repeat-1"><a class="header" href="#repeat-1">Repeat 1</a></h2>
9+
<h2 id="repeat-1-1"><a class="header" href="#repeat-1-1">Repeat 1</a></h2>
1010
<h2 id="with-emphasis-bold-bold_emphasis-code-escaped-html-link-httpsexamplecom"><a class="header" href="#with-emphasis-bold-bold_emphasis-code-escaped-html-link-httpsexamplecom"><!--comment--> With <em>emphasis</em> <strong>bold</strong> <strong><em>bold_emphasis</em></strong> <code>code</code> &lt;escaped&gt; <span>html</span> <a href="https://example.com/link">link</a> <a href="https://example.com/">https://example.com/</a></a></h2>

0 commit comments

Comments
 (0)