diff --git a/crates/mdbook-html/front-end/templates/toc.js.hbs b/crates/mdbook-html/front-end/templates/toc.js.hbs index 1a9f751ccf..ae61a45d64 100644 --- a/crates/mdbook-html/front-end/templates/toc.js.hbs +++ b/crates/mdbook-html/front-end/templates/toc.js.hbs @@ -335,10 +335,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 +432,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/crates/mdbook-html/src/html/tree.rs b/crates/mdbook-html/src/html/tree.rs index 5cb97ce378..a085b4f81b 100644 --- a/crates/mdbook-html/src/html/tree.rs +++ b/crates/mdbook-html/src/html/tree.rs @@ -932,15 +932,26 @@ where el.insert_attr("id", id.into()); href }; - // Insert an element between the heading and its children. let mut a = Element::new("a"); a.insert_attr("class", "header".into()); a.insert_attr("href", href.into()); - let mut a = self.tree.orphan(Node::Element(a)); - a.reparent_from_id_append(heading); - let a_id = a.id(); - let mut node = self.tree.get_mut(heading).unwrap(); - node.append_id(a_id); + + let heading_node = self.tree.get(heading).unwrap(); + if heading_contains_nested_anchor(heading_node) { + // Prepend an empty header link so headings may contain other + // links without nesting `` elements (issue #221). + self.tree + .get_mut(heading) + .unwrap() + .prepend(Node::Element(a)); + } else { + // Insert an `` element between the heading and its children. + let mut a = self.tree.orphan(Node::Element(a)); + a.reparent_from_id_append(heading); + let a_id = a.id(); + let mut node = self.tree.get_mut(heading).unwrap(); + node.append_id(a_id); + } } } @@ -1074,6 +1085,27 @@ where /// Traverse the given node, emitting any plain text into the output. /// /// This is used to generate the `id` of a header. +/// Returns true if the heading contains an `` element from the markdown source. +fn heading_contains_nested_anchor(node: NodeRef<'_, Node>) -> bool { + for child in node.children() { + match child.value() { + Node::Element(el) if el.name() == "a" => return true, + Node::Element(_) => { + if heading_contains_nested_anchor(child) { + return true; + } + } + Node::Fragment => { + if heading_contains_nested_anchor(child) { + return true; + } + } + Node::Text(_) | Node::Comment(_) | Node::RawData(_) => {} + } + } + false +} + fn text_in_node(node: NodeRef<'_, Node>, output: &mut String) { for child in node.children() { match child.value() { diff --git a/tests/testsuite/markdown/definition_lists/expected/definition_lists.html b/tests/testsuite/markdown/definition_lists/expected/definition_lists.html index c647ccb4dd..bcc03a40b4 100644 --- a/tests/testsuite/markdown/definition_lists/expected/definition_lists.html +++ b/tests/testsuite/markdown/definition_lists/expected/definition_lists.html @@ -22,7 +22,7 @@

Definition
-
apple
+
apple
red fruit

Multi-line term

diff --git a/tests/testsuite/rendering/header_links/expected/header_links.html b/tests/testsuite/rendering/header_links/expected/header_links.html index 29c35a530d..101ed49951 100644 --- a/tests/testsuite/rendering/header_links/expected/header_links.html +++ b/tests/testsuite/rendering/header_links/expected/header_links.html @@ -1,4 +1,5 @@

Header Links

+

Foo^bar

@@ -7,4 +8,4 @@

Repeat

Repeat

Repeat

Repeat 1

- \ No newline at end of file + \ No newline at end of file diff --git a/tests/testsuite/rendering/header_links/src/header_links.md b/tests/testsuite/rendering/header_links/src/header_links.md index 8ad556f7a7..d747ce997d 100644 --- a/tests/testsuite/rendering/header_links/src/header_links.md +++ b/tests/testsuite/rendering/header_links/src/header_links.md @@ -1,5 +1,7 @@ # Header Links +## Header with [link](https://example.com). + ## Foo^bar ###