Skip to content

Commit 4a9a517

Browse files
authored
Merge pull request #2927 from ehuss/fix-unbalanced-html-in-header
Handle unclosed HTML tags inside a markdown element
2 parents 9a5e8db + 700839f commit 4a9a517

File tree

2 files changed

+62
-19
lines changed

2 files changed

+62
-19
lines changed

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

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -306,25 +306,7 @@ where
306306
trace!("event={event:?}");
307307
match event {
308308
Event::Start(tag) => self.start_tag(tag),
309-
Event::End(tag) => {
310-
// TODO: This should validate that the event stack is
311-
// properly synchronized with the tag stack.
312-
self.pop();
313-
match tag {
314-
TagEnd::TableHead => {
315-
self.table_state = TableState::Body;
316-
self.push(Node::Element(Element::new("tbody")));
317-
}
318-
TagEnd::TableCell => {
319-
self.table_cell_index += 1;
320-
}
321-
TagEnd::Table => {
322-
// Pop tbody or thead
323-
self.pop();
324-
}
325-
_ => {}
326-
}
327-
}
309+
Event::End(tag) => self.end_tag(tag),
328310
Event::Text(text) => {
329311
self.append_text(text.into_tendril());
330312
}
@@ -600,6 +582,46 @@ where
600582
self.push(Node::Element(element));
601583
}
602584

585+
fn end_tag(&mut self, tag: TagEnd) {
586+
// TODO: This should validate that the event stack is properly
587+
// synchronized with the tag stack. That, would likely require keeping
588+
// a parallel "expected end tag" with the tag stack, since mapping a
589+
// pulldown-cmark event tag to an HTML tag isn't always clear.
590+
//
591+
// Check for unclosed HTML tags when exiting a markdown event.
592+
while let Some(node_id) = self.tag_stack.last() {
593+
let node = self.tree.get(*node_id).unwrap().value();
594+
let Node::Element(el) = node else {
595+
break;
596+
};
597+
if !el.was_raw {
598+
break;
599+
}
600+
warn!(
601+
"unclosed HTML tag `<{}>` found in `{}` while exiting {tag:?}\n\
602+
HTML tags must be closed before exiting a markdown element.",
603+
el.name.local,
604+
self.options.path.display(),
605+
);
606+
self.pop();
607+
}
608+
self.pop();
609+
match tag {
610+
TagEnd::TableHead => {
611+
self.table_state = TableState::Body;
612+
self.push(Node::Element(Element::new("tbody")));
613+
}
614+
TagEnd::TableCell => {
615+
self.table_cell_index += 1;
616+
}
617+
TagEnd::Table => {
618+
// Pop tbody or thead
619+
self.pop();
620+
}
621+
_ => {}
622+
}
623+
}
624+
603625
/// Given some HTML, parse it into [`Node`] elements and append them to
604626
/// the current node.
605627
fn append_html(&mut self, html: &str) {

tests/testsuite/rendering.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,24 @@ Check that the HTML tags are properly balanced.
283283
})
284284
.check_main_file("book/chapter_1.html", str!["<div>x<span>foo</span></div>"]);
285285
}
286+
287+
// Test for bug with unbalanced HTML handling in the heading.
288+
#[test]
289+
fn heading_with_unbalanced_html() {
290+
BookTest::init(|_| {})
291+
.change_file("src/chapter_1.md", "### Option<T>")
292+
.run("build", |cmd| {
293+
cmd.expect_stderr(str![[r#"
294+
INFO Book building has started
295+
INFO Running the html backend
296+
WARN unclosed HTML tag `<t>` found in `chapter_1.md` while exiting Heading(H3)
297+
HTML tags must be closed before exiting a markdown element.
298+
INFO HTML book written to `[ROOT]/book`
299+
300+
"#]]);
301+
})
302+
.check_main_file(
303+
"book/chapter_1.html",
304+
str![[r##"<h3 id="option"><a class="header" href="#option">Option<t></t></a></h3>"##]],
305+
);
306+
}

0 commit comments

Comments
 (0)