diff --git a/crates/mdbook-html/front-end/templates/toc.js.hbs b/crates/mdbook-html/front-end/templates/toc.js.hbs
index 1a9f751ccf..c0ebbb6587 100644
--- a/crates/mdbook-html/front-end/templates/toc.js.hbs
+++ b/crates/mdbook-html/front-end/templates/toc.js.hbs
@@ -23,10 +23,14 @@ class MDBookSidebarScrollbox extends HTMLElement {
link.href = path_to_root + href;
}
// The 'index' page is supposed to alias the first chapter in the book.
- if (link.href === current_page
- || i === 0
- && path_to_root === ''
- && current_page.endsWith('/index.html')) {
+ const on_book_index = path_to_root === ''
+ && current_page.endsWith('/index.html');
+ const is_index_alias = i === 0 && on_book_index;
+ // When the book index displays the first chapter, don't also highlight
+ // other chapters that incorrectly resolve to `index.html` (issue #2883).
+ const is_exact_match = link.href === current_page
+ && !(on_book_index && i !== 0);
+ if (is_exact_match || is_index_alias) {
link.classList.add('active');
let parent = link.parentElement;
while (parent) {
@@ -39,11 +43,19 @@ class MDBookSidebarScrollbox extends HTMLElement {
}
// Track and set sidebar scroll position
this.addEventListener('click', e => {
- if (e.target.tagName === 'A') {
- const clientRect = e.target.getBoundingClientRect();
- const sidebarRect = this.getBoundingClientRect();
- sessionStorage.setItem('sidebar-scroll-offset', clientRect.top - sidebarRect.top);
+ const link = e.target.closest('a');
+ if (!link || !this.contains(link)) {
+ return;
+ }
+ const href = link.getAttribute('href');
+ // Ignore fold toggles and in-page header links; chapter links may
+ // contain child elements (e.g. ) so e.target is not always .
+ if (!href || href.startsWith('#') || link.classList.contains('chapter-fold-toggle')) {
+ return;
}
+ const clientRect = link.getBoundingClientRect();
+ const sidebarRect = this.getBoundingClientRect();
+ sessionStorage.setItem('sidebar-scroll-offset', clientRect.top - sidebarRect.top);
}, { passive: true });
const sidebarScrollOffset = sessionStorage.getItem('sidebar-scroll-offset');
sessionStorage.removeItem('sidebar-scroll-offset');
@@ -335,10 +347,25 @@ window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox)
});
}
+ // Returns the node to copy header text from. When the heading uses an
+ // empty `.header` link as its first child (see issue #221), use the full
+ // heading so the sidebar still shows the title text.
+ function headerContentRoot(header) {
+ const first = header.children[0];
+ if (first?.tagName === 'A' && first.classList.contains('header')) {
+ if (first.textContent.trim() === '') {
+ return header;
+ }
+ return first;
+ }
+ return first;
+ }
+
// Takes the nodes from the given head and copies them over to the
// destination, along with some filtering.
function filterHeader(source, dest) {
const clone = source.cloneNode(true);
+ clone.querySelectorAll('a.header').forEach(a => a.remove());
clone.querySelectorAll('mark').forEach(mark => {
mark.replaceWith(...mark.childNodes);
});
@@ -417,7 +444,7 @@ window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox)
span.appendChild(a);
a.href = '#' + header.id;
a.classList.add('header-in-summary');
- filterHeader(header.children[0], a);
+ filterHeader(headerContentRoot(header), a);
a.addEventListener('click', headerThresholdClick);
const nextHeader = headers[i + 1];
if (nextHeader !== undefined) {
diff --git a/tests/gui/books/readme-not-first/book.toml b/tests/gui/books/readme-not-first/book.toml
new file mode 100644
index 0000000000..6c847796d8
--- /dev/null
+++ b/tests/gui/books/readme-not-first/book.toml
@@ -0,0 +1,2 @@
+[book]
+title = "readme-not-first"
diff --git a/tests/gui/books/readme-not-first/src/README.md b/tests/gui/books/readme-not-first/src/README.md
new file mode 100644
index 0000000000..e10b99d013
--- /dev/null
+++ b/tests/gui/books/readme-not-first/src/README.md
@@ -0,0 +1 @@
+# Introduction
diff --git a/tests/gui/books/readme-not-first/src/SUMMARY.md b/tests/gui/books/readme-not-first/src/SUMMARY.md
new file mode 100644
index 0000000000..2b55b91b2e
--- /dev/null
+++ b/tests/gui/books/readme-not-first/src/SUMMARY.md
@@ -0,0 +1,6 @@
+# Summary
+
+[Prefix 1](prefix-1.md)
+[Prefix 2](prefix-2.md)
+
+- [Introduction](README.md)
diff --git a/tests/gui/books/readme-not-first/src/prefix-1.md b/tests/gui/books/readme-not-first/src/prefix-1.md
new file mode 100644
index 0000000000..4ddb4143d8
--- /dev/null
+++ b/tests/gui/books/readme-not-first/src/prefix-1.md
@@ -0,0 +1 @@
+# Prefix 1
diff --git a/tests/gui/books/readme-not-first/src/prefix-2.md b/tests/gui/books/readme-not-first/src/prefix-2.md
new file mode 100644
index 0000000000..11ff4958c7
--- /dev/null
+++ b/tests/gui/books/readme-not-first/src/prefix-2.md
@@ -0,0 +1 @@
+# Prefix 2
diff --git a/tests/gui/sidebar-readme-not-first.goml b/tests/gui/sidebar-readme-not-first.goml
new file mode 100644
index 0000000000..37180a1218
--- /dev/null
+++ b/tests/gui/sidebar-readme-not-first.goml
@@ -0,0 +1,6 @@
+// README.md not listed first should not double-highlight the sidebar on index.html.
+
+go-to: |DOC_PATH| + "readme-not-first/index.html"
+
+assert-count: (".sidebar-scrollbox a.active", 1)
+assert-text: (".sidebar-scrollbox a.active", "Prefix 1")