Skip to content

Commit f173132

Browse files
committed
Fix ID collisions when the numeric suffix gets used
This fixes a collision with the ID generation where it a previous entry could generate a unique ID like "foo-1", but then a header with the text "Foo 1" would collide with it. This fixes it so that when generating the ID for "Foo 1", it will loop unit it finds an ID that doesn't collide (in this case, `foo-1-1`).
1 parent 1c034bd commit f173132

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
@@ -820,7 +820,7 @@ where
820820
/// to all header elements, and to also add an `<a>` tag so that clicking
821821
/// the header will set the current URL to that header's fragment.
822822
fn add_header_links(&mut self) {
823-
let mut id_counter = HashMap::new();
823+
let mut id_counter = HashSet::new();
824824
let headings =
825825
self.node_ids_for_tag(&|name| matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6"));
826826
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)