Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion crates/mdbook-html/front-end/templates/toc.js.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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) {
Expand Down
44 changes: 38 additions & 6 deletions crates/mdbook-html/src/html/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,15 +932,26 @@ where
el.insert_attr("id", id.into());
href
};
// Insert an <a> 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 `<a>` elements (issue #221).
self.tree
.get_mut(heading)
.unwrap()
.prepend(Node::Element(a));
} else {
// Insert an `<a>` 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);
}
}
}

Expand Down Expand Up @@ -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 `<a>` 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ <h1 id="definition-lists"><a class="header" href="#definition-lists">Definition
</dl>
<h2 id="term-with-link"><a class="header" href="#term-with-link">Term with link</a></h2>
<dl>
<dt id="apple-2"><a class="header" href="#apple-2"><a href="some-page.html#apple">apple</a></a></dt>
<dt id="apple-2"><a class="header" href="#apple-2"></a><a href="some-page.html#apple">apple</a></dt>
<dd>red fruit</dd>
</dl>
<h2 id="multi-line-term"><a class="header" href="#multi-line-term">Multi-line term</a></h2>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<h1 id="header-links"><a class="header" href="#header-links">Header Links</a></h1>
<h2 id="header-with-link"><a class="header" href="#header-with-link"></a>Header with <a href="https://example.com">link</a>.</h2>
<h2 id="foobar"><a class="header" href="#foobar">Foo^bar</a></h2>
<h3 id=""><a class="header" href="#"></a></h3>
<h4 id="-1"><a class="header" href="#-1"></a></h4>
Expand All @@ -7,4 +8,4 @@ <h2 id="repeat"><a class="header" href="#repeat">Repeat</a></h2>
<h2 id="repeat-1"><a class="header" href="#repeat-1">Repeat</a></h2>
<h2 id="repeat-2"><a class="header" href="#repeat-2">Repeat</a></h2>
<h2 id="repeat-1-1"><a class="header" href="#repeat-1-1">Repeat 1</a></h2>
<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>
<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"></a><!--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></h2>
2 changes: 2 additions & 0 deletions tests/testsuite/rendering/header_links/src/header_links.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Header Links

## Header with [link](https://example.com).

## Foo^bar

###
Expand Down
Loading