1use std::borrow::Cow;
4use std::iter::Peekable;
5use std::ops::Range;
6use std::str::CharIndices;
7
8use itertools::Itertools as _;
9use rustc_ast::attr::AttributeExt;
10use rustc_ast::token::{CommentKind, DocFragmentKind};
11use rustc_hir::HirId;
12use rustc_resolve::rustdoc::pulldown_cmark::{BrokenLink, Event, LinkType, Parser, Tag, TagEnd};
13use rustc_resolve::rustdoc::source_span_for_markdown_range;
14
15use crate::clean::*;
16use crate::core::DocContext;
17use crate::html::markdown::main_body_opts;
18
19pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
20 let tcx = cx.tcx;
21 let report_diag = |msg: String, range: &Range<usize>, mode: HtmlDiagMode| {
22 let sp = match source_span_for_markdown_range(tcx, dox, range, &item.attrs.doc_strings) {
23 Some((sp, _)) => sp,
24 None => item.attr_span(tcx),
25 };
26 tcx.emit_node_span_lint(
27 crate::lint::INVALID_HTML_TAGS,
28 hir_id,
29 sp,
30 rustc_errors::DiagDecorator(|lint| {
31 use rustc_lint::Applicability;
32
33 lint.primary_message(msg);
34
35 let mut generics_end = range.end;
39 if mode == HtmlDiagMode::Unclosed
40 && dox[..generics_end].ends_with('>')
41 && let Some(mut generics_start) = extract_path_backwards(dox, range.start)
42 {
43 while generics_start != 0
44 && generics_end < dox.len()
45 && dox.as_bytes()[generics_start - 1] == b'<'
46 && dox.as_bytes()[generics_end] == b'>'
47 {
48 generics_end += 1;
49 generics_start -= 1;
50 if let Some(new_start) = extract_path_backwards(dox, generics_start) {
51 generics_start = new_start;
52 }
53 if let Some(new_end) = extract_path_forward(dox, generics_end) {
54 generics_end = new_end;
55 }
56 }
57 if let Some(new_end) = extract_path_forward(dox, generics_end) {
58 generics_end = new_end;
59 }
60 let generics_sp = match source_span_for_markdown_range(
61 tcx,
62 dox,
63 &(generics_start..generics_end),
64 &item.attrs.doc_strings,
65 ) {
66 Some((sp, _)) => sp,
67 None => item.attr_span(tcx),
68 };
69 if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<')
95 || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>')
96 {
97 return;
98 }
99 lint.multipart_suggestion(
101 "try marking as source code",
102 vec![
103 (generics_sp.shrink_to_lo(), String::from("`")),
104 (generics_sp.shrink_to_hi(), String::from("`")),
105 ],
106 Applicability::MaybeIncorrect,
107 );
108 } else if let HtmlDiagMode::Unopened { possible_pair: Some(possible_pair) } = mode {
109 let (reason_display_text, reason_range) = match possible_pair.reason {
110 HtmlOrMarkdownTag::Markdown(tag @ TagEnd::Paragraph, range)
111 if dox.as_bytes().get(range.end) == Some(&b'>') =>
112 {
113 (
114 format!(
115 "because the Markdown {} is interrupted by this block quote",
116 markdown_tag_name(tag)
117 ),
118 range.end..range.end,
119 )
120 }
121 HtmlOrMarkdownTag::Markdown(
122 tag @ (TagEnd::Paragraph | TagEnd::TableCell),
123 range,
124 ) => (
125 format!("because the Markdown {} ends here", markdown_tag_name(tag)),
126 range.end..range.end,
127 ),
128 HtmlOrMarkdownTag::Markdown(tag, range) => {
129 (format!("because of this Markdown {}", markdown_tag_name(tag)), range)
130 }
131 HtmlOrMarkdownTag::Html(name, range) => {
132 (format!("because of this HTML `{name}`"), range)
133 }
134 };
135 if let HtmlOrMarkdownTag::Html(_, unclosed_tag_range) =
136 possible_pair.unclosed_tag
137 && let Some((unclosed_tag_span, _)) = source_span_for_markdown_range(
138 tcx,
139 dox,
140 &unclosed_tag_range,
141 &item.attrs.doc_strings,
142 )
143 {
144 lint.span_label(sp, "this unopened tag");
145 lint.span_label(unclosed_tag_span, "does not match this unclosed tag");
146 }
147 if let Some((reason_span, _)) = source_span_for_markdown_range(
148 tcx,
149 dox,
150 &reason_range,
151 &item.attrs.doc_strings,
152 ) {
153 lint.span_label(reason_span, reason_display_text);
154 }
155 } else if let HtmlDiagMode::MarkdownNestedInRawText(html_tag_range, html_tag) = mode {
156 lint.span_label(sp, format!("Markdown translates this into HTML, but the browser parses it as {language}", language = html_tag.language()));
157 if
158 let Some((html_tag_span, _)) = source_span_for_markdown_range(
160 tcx,
161 dox,
162 &html_tag_range,
163 &item.attrs.doc_strings,
164 ) &&
165 item.attrs.doc_strings.iter().all(|f| f.kind == DocFragmentKind::Sugared(CommentKind::Line)) &&
167 let Some(def_id) = item.def_id() &&
169 let mut style_iter = inline::load_attrs(cx.tcx, def_id).iter().filter_map(|attr| attr.doc_resolution_scope()) &&
170 let Some(doc_attr_style) = style_iter.next() &&
171 style_iter.all(|style| style == doc_attr_style)
172 {
173 lint.span_suggestion(
174 html_tag_span,
175 "to turn off Markdown parsing, put the tag at the start of the line",
176 format!("\n{mark} {doc}", mark=doc_attr_style.line_doc_comment_prefix(), doc=&dox[html_tag_range.clone()]),
177 Applicability::MachineApplicable,
178 );
179 }
180 }
181 }),
182 );
183 };
184
185 let mut tagp = TagParser::new();
186 let mut is_in_comment = None;
187
188 let link_names = item.link_names(&cx.cache);
189
190 let mut replacer = |broken_link: BrokenLink<'_>| {
191 if let Some(link) =
192 link_names.iter().find(|link| *link.original_text == *broken_link.reference)
193 {
194 Some((link.href.as_str().into(), link.new_text.to_string().into()))
195 } else if matches!(&broken_link.link_type, LinkType::Reference | LinkType::ReferenceUnknown)
196 {
197 Some((
200 broken_link.reference.to_string().into(),
201 broken_link.reference.to_string().into(),
202 ))
203 } else {
204 None
205 }
206 };
207
208 let p = Parser::new_with_broken_link_callback(dox, main_body_opts(), Some(&mut replacer))
209 .into_offset_iter()
210 .coalesce(|a, b| {
211 match (a, b) {
214 ((Event::Html(_), ra), (Event::Html(_), rb)) if ra.end == rb.start => {
215 let merged = ra.start..rb.end;
216 Ok((Event::Html(Cow::Borrowed(&dox[merged.clone()]).into()), merged))
217 }
218 x => Err(x),
219 }
220 });
221
222 for (event, range) in p {
223 match event {
224 Event::Html(text) | Event::InlineHtml(text) => {
225 tagp.extract_tags(&text, range, &mut is_in_comment, &report_diag)
226 }
227 Event::Start(Tag::HtmlBlock) | Event::End(TagEnd::HtmlBlock) => {}
228 Event::Start(tag) => {
229 tagp.push_markdown_tag(tag.into(), range, &report_diag);
230 }
231 Event::End(tag) => {
232 tagp.pop_markdown_tag(tag, range, &report_diag);
233 }
234 _ => {}
235 }
236 }
237
238 if let Some(range) = is_in_comment {
239 report_diag("Unclosed HTML comment".to_string(), &range, HtmlDiagMode::Incomplete);
240 } else if let &Some(quote_pos) = &tagp.quote_pos {
241 let qr = Range { start: quote_pos, end: quote_pos };
242 report_diag(
243 format!("unclosed quoted HTML attribute on tag `{}`", &tagp.tag_name),
244 &qr,
245 HtmlDiagMode::Incomplete,
246 );
247 } else {
248 if !tagp.tag_name.is_empty() {
249 report_diag(
250 format!("incomplete HTML tag `{}`", &tagp.tag_name),
251 &(tagp.tag_start_pos..dox.len()),
252 HtmlDiagMode::Incomplete,
253 );
254 }
255 for tag in tagp.tags.iter().chain(
256 tagp.unclosed_tag_buf
257 .iter()
258 .map(|buffered_unclosed_tag| &buffered_unclosed_tag.unclosed_tag),
259 ) {
260 match tag {
261 HtmlOrMarkdownTag::Html(tag, range) => {
262 if !is_implicitly_self_closing(&tag.to_ascii_lowercase()) {
263 report_diag(
264 format!("unclosed HTML tag `{tag}`"),
265 range,
266 HtmlDiagMode::Unclosed,
267 );
268 }
269 }
270 HtmlOrMarkdownTag::Markdown(tag, range) => {
271 report_diag(
272 format!(
273 "invalid tree with Markdown delimiter `{tag_name}`",
274 tag_name = markdown_tag_name(*tag)
275 ),
276 range,
277 HtmlDiagMode::Unclosed,
278 );
279 }
280 }
281 }
282 }
283}
284
285const ALLOWED_UNCLOSED: &[&str] = &[
287 "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
288 "source", "track", "wbr",
289];
290
291fn is_implicitly_self_closing(tag_name: &str) -> bool {
293 ALLOWED_UNCLOSED.contains(&tag_name)
294}
295
296fn extract_path_backwards(text: &str, end_pos: usize) -> Option<usize> {
297 use rustc_lexer::{is_id_continue, is_id_start};
298 let mut current_pos = end_pos;
299 loop {
300 if current_pos >= 2 && text[..current_pos].ends_with("::") {
301 current_pos -= 2;
302 }
303 let new_pos = text[..current_pos]
304 .char_indices()
305 .rev()
306 .take_while(|(_, c)| is_id_start(*c) || is_id_continue(*c))
307 .reduce(|_accum, item| item)
308 .and_then(|(new_pos, c)| is_id_start(c).then_some(new_pos));
309 if let Some(new_pos) = new_pos
310 && current_pos != new_pos
311 {
312 current_pos = new_pos;
313 continue;
314 }
315 break;
316 }
317 if current_pos == end_pos { None } else { Some(current_pos) }
318}
319
320fn extract_path_forward(text: &str, start_pos: usize) -> Option<usize> {
321 use rustc_lexer::{is_id_continue, is_id_start};
322 let mut current_pos = start_pos;
323 loop {
324 if current_pos < text.len() && text[current_pos..].starts_with("::") {
325 current_pos += 2;
326 } else {
327 break;
328 }
329 let mut chars = text[current_pos..].chars();
330 if let Some(c) = chars.next() {
331 if is_id_start(c) {
332 current_pos += c.len_utf8();
333 } else {
334 break;
335 }
336 }
337 for c in chars {
338 if is_id_continue(c) {
339 current_pos += c.len_utf8();
340 } else {
341 break;
342 }
343 }
344 }
345 if current_pos == start_pos { None } else { Some(current_pos) }
346}
347
348fn is_valid_for_html_tag_name(c: char, is_empty: bool) -> bool {
349 c.is_ascii_alphabetic() || !is_empty && (c == '-' || c.is_ascii_digit())
354}
355
356#[derive(Eq, PartialEq, Debug, Clone)]
357enum HtmlOrMarkdownTag {
358 Html(String, Range<usize>),
359 Markdown(TagEnd, Range<usize>),
360}
361
362impl HtmlOrMarkdownTag {
363 fn range(&self) -> Range<usize> {
364 match self {
365 HtmlOrMarkdownTag::Html(_, range) => range.clone(),
366 HtmlOrMarkdownTag::Markdown(_, range) => range.clone(),
367 }
368 }
369}
370
371#[derive(Eq, PartialEq, Debug, Clone)]
372struct BufferedUnclosedTag {
373 unclosed_tag: HtmlOrMarkdownTag,
374 reason: HtmlOrMarkdownTag,
375}
376
377#[derive(Eq, PartialEq, Debug, Clone, Copy)]
378enum HtmlRawTextTag {
379 Script,
380 Style,
381}
382
383impl HtmlRawTextTag {
384 fn name(self) -> &'static str {
385 match self {
386 HtmlRawTextTag::Script => "script",
387 HtmlRawTextTag::Style => "style",
388 }
389 }
390 fn language(self) -> &'static str {
391 match self {
392 HtmlRawTextTag::Script => "JavaScript",
393 HtmlRawTextTag::Style => "CSS",
394 }
395 }
396 fn from_tag(tag: &str) -> Option<HtmlRawTextTag> {
397 match &tag.to_ascii_lowercase() {
398 "script" => Some(HtmlRawTextTag::Script),
399 "style" => Some(HtmlRawTextTag::Style),
400 _ => None,
401 }
402 }
403}
404
405#[derive(Eq, PartialEq, Debug, Clone)]
406enum HtmlDiagMode {
407 Unclosed,
408 Unopened { possible_pair: Option<BufferedUnclosedTag> },
409 Incomplete,
410 MarkdownNestedInRawText(Range<usize>, HtmlRawTextTag),
411}
412
413#[derive(Debug, Clone)]
415struct TagParser {
416 tags: Vec<HtmlOrMarkdownTag>,
417 unclosed_tag_buf: Vec<BufferedUnclosedTag>,
418 tag_name: String,
423 tag_start_pos: usize,
424 is_closing: bool,
425 in_attrs: bool,
427 quote: Option<char>,
431 quote_pos: Option<usize>,
432 after_eq: bool,
433}
434
435impl TagParser {
436 fn new() -> Self {
437 Self {
438 tags: Vec::new(),
439 unclosed_tag_buf: Vec::new(),
440 tag_name: String::with_capacity(8),
441 tag_start_pos: 0,
442 is_closing: false,
443 in_attrs: false,
444 quote: None,
445 quote_pos: None,
446 after_eq: false,
447 }
448 }
449
450 fn drop_tag(&mut self, range: Range<usize>, f: &impl Fn(String, &Range<usize>, HtmlDiagMode)) {
451 let tag_name_low = self.tag_name.to_ascii_lowercase();
452 let tag_name_is_match = |tag: &HtmlOrMarkdownTag| match tag {
453 HtmlOrMarkdownTag::Html(name, _span) => name.to_ascii_lowercase() == tag_name_low,
454 HtmlOrMarkdownTag::Markdown(..) => false,
455 };
456 if let Some(pos) = self.tags.iter().rposition(tag_name_is_match) {
457 let should_not_warn = self.tags.iter().take(pos + 1).any(|tag| match tag {
460 HtmlOrMarkdownTag::Html(at, _span) => HtmlRawTextTag::from_tag(at).is_some(),
461 HtmlOrMarkdownTag::Markdown(..) => false,
462 });
463 if should_not_warn {
464 self.tags
470 .extract_if(pos.., |tag| matches!(tag, HtmlOrMarkdownTag::Html(..)))
471 .for_each(|_| ());
472 } else {
473 let (HtmlOrMarkdownTag::Html(_, start_range)
474 | HtmlOrMarkdownTag::Markdown(_, start_range)) = &self.tags[pos];
475 let range = start_range.start..range.end;
476 self.unclosed_tag_buf.extend(self.tags.drain(pos + 1..).map(|unclosed_tag| {
480 BufferedUnclosedTag {
481 unclosed_tag,
482 reason: HtmlOrMarkdownTag::Html(self.tag_name.clone(), range.clone()),
483 }
484 }));
485 self.tags.pop();
487 }
488 } else if !self.tags.iter().any(|tag| match tag {
489 HtmlOrMarkdownTag::Html(at, _span) => HtmlRawTextTag::from_tag(at).is_some(),
490 HtmlOrMarkdownTag::Markdown(..) => false,
491 }) {
492 let mode = HtmlDiagMode::Unopened {
495 possible_pair: self
496 .unclosed_tag_buf
497 .iter()
498 .rposition(|buf| tag_name_is_match(&buf.unclosed_tag))
499 .map(|pos| self.unclosed_tag_buf.remove(pos)),
500 };
501 f(format!("unopened HTML tag `{}`", &self.tag_name), &range, mode);
502 }
503 }
504
505 fn handle_lt_in_tag(
507 &mut self,
508 range: Range<usize>,
509 lt_pos: usize,
510 f: &impl Fn(String, &Range<usize>, HtmlDiagMode),
511 ) {
512 let global_pos = range.start + lt_pos;
513 if global_pos == self.tag_start_pos {
515 return;
517 }
518 f(
520 format!("incomplete HTML tag `{}`", &self.tag_name),
521 &(self.tag_start_pos..global_pos),
522 HtmlDiagMode::Incomplete,
523 );
524 self.tag_parsed();
525 }
526
527 fn extract_html_tag(
528 &mut self,
529 text: &str,
530 range: &Range<usize>,
531 start_pos: usize,
532 iter: &mut Peekable<CharIndices<'_>>,
533 f: &impl Fn(String, &Range<usize>, HtmlDiagMode),
534 ) {
535 let mut prev_pos = start_pos;
536
537 'outer_loop: loop {
538 let (pos, c) = match iter.peek() {
539 Some((pos, c)) => (*pos, *c),
540 None if self.tag_name.is_empty() => (prev_pos, '\0'),
543 None => break,
544 };
545 prev_pos = pos;
546 if c == '/' && self.tag_name.is_empty() {
547 self.is_closing = true;
549 } else if !self.in_attrs && is_valid_for_html_tag_name(c, self.tag_name.is_empty()) {
550 self.tag_name.push(c);
551 } else {
552 if !self.tag_name.is_empty() {
553 self.in_attrs = true;
554 let mut r = Range { start: range.start + start_pos, end: range.start + pos };
556 if c == '>' {
557 r.end += 1;
560 }
561 if self.is_closing {
562 if c != '>' {
564 if !c.is_whitespace() {
565 break;
567 }
568 let mut found = false;
569 for (new_pos, c) in text[pos..].char_indices() {
570 if !c.is_whitespace() {
571 if c == '>' {
572 r.end = range.start + pos + new_pos + 1;
573 found = true;
574 } else if c == '<' {
575 self.handle_lt_in_tag(range.clone(), pos + new_pos, f);
576 }
577 break;
578 }
579 }
580 if !found {
581 break 'outer_loop;
582 }
583 }
584 self.drop_tag(r, f);
585 self.tag_parsed();
586 } else {
587 self.extract_opening_tag(text, range, r, pos, c, iter, f)
588 }
589 }
590 break;
591 }
592 iter.next();
593 }
594 }
595
596 fn extract_opening_tag(
597 &mut self,
598 text: &str,
599 range: &Range<usize>,
600 r: Range<usize>,
601 pos: usize,
602 c: char,
603 iter: &mut Peekable<CharIndices<'_>>,
604 f: &impl Fn(String, &Range<usize>, HtmlDiagMode),
605 ) {
606 let mut is_self_closing = false;
609 if c != '>' {
610 'parse_til_gt: {
611 for (i, c) in text[pos..].char_indices() {
612 if !c.is_whitespace() {
613 debug_assert_eq!(self.quote_pos.is_some(), self.quote.is_some());
614 if let Some(q) = self.quote {
615 if c == q {
616 self.quote = None;
617 self.quote_pos = None;
618 self.after_eq = false;
619 }
620 } else if c == '>' {
621 break 'parse_til_gt;
622 } else if c == '<' {
623 self.handle_lt_in_tag(range.clone(), pos + i, f);
624 } else if c == '/' && !self.after_eq {
625 is_self_closing = true;
626 } else {
627 if is_self_closing {
628 is_self_closing = false;
629 }
630 if (c == '"' || c == '\'') && self.after_eq {
631 self.quote = Some(c);
632 self.quote_pos = Some(pos + i);
633 } else if c == '=' {
634 self.after_eq = true;
635 }
636 }
637 } else if self.quote.is_none() {
638 self.after_eq = false;
639 }
640 if !is_self_closing && !self.tag_name.is_empty() {
641 iter.next();
642 }
643 }
644 return;
649 }
650 }
651 if is_self_closing {
652 let valid = ALLOWED_UNCLOSED.contains(&&self.tag_name[..])
654 || self.tags.iter().take(pos + 1).any(|tag| match tag {
655 HtmlOrMarkdownTag::Html(at, _) => {
656 let at = at.to_ascii_lowercase();
657 at == "svg" || at == "math"
658 }
659 HtmlOrMarkdownTag::Markdown(..) => false,
660 });
661 if !valid {
662 f(
663 format!("invalid self-closing HTML tag `{}`", self.tag_name),
664 &r,
665 HtmlDiagMode::Incomplete,
666 );
667 }
668 } else if !self.tag_name.is_empty() {
669 self.tags.push(HtmlOrMarkdownTag::Html(std::mem::take(&mut self.tag_name), r));
670 }
671 self.tag_parsed();
672 }
673 fn tag_parsed(&mut self) {
675 self.tag_name.clear();
676 self.is_closing = false;
677 self.in_attrs = false;
678 }
679
680 fn extract_tags(
681 &mut self,
682 text: &str,
683 range: Range<usize>,
684 is_in_comment: &mut Option<Range<usize>>,
685 f: &impl Fn(String, &Range<usize>, HtmlDiagMode),
686 ) {
687 let mut iter = text.char_indices().peekable();
688 let mut prev_pos = 0;
689 loop {
690 if self.quote.is_some() {
691 debug_assert!(self.in_attrs && self.quote_pos.is_some());
692 }
693 if self.in_attrs
694 && let Some(&(start_pos, _)) = iter.peek()
695 {
696 self.extract_html_tag(text, &range, start_pos, &mut iter, f);
697 if prev_pos == start_pos {
699 iter.next();
700 }
701 prev_pos = start_pos;
702 continue;
703 }
704 let Some((start_pos, c)) = iter.next() else { break };
705 if is_in_comment.is_some() {
706 if text[start_pos..].starts_with("-->") {
707 *is_in_comment = None;
708 }
709 } else if c == '<' {
710 if self.tag_name.is_empty() && text[start_pos..].starts_with("<!--") {
712 iter.next();
714 iter.next();
715 iter.next();
716 *is_in_comment = Some(Range {
717 start: range.start + start_pos,
718 end: range.start + start_pos + 4,
719 });
720 } else {
721 if self.tag_name.is_empty() {
722 self.tag_start_pos = range.start + start_pos;
723 }
724 self.extract_html_tag(text, &range, start_pos, &mut iter, f);
725 }
726 } else if !self.tag_name.is_empty() {
727 self.extract_html_tag(text, &range, start_pos, &mut iter, f);
729 }
730 }
731 }
732
733 fn push_markdown_tag(
734 &mut self,
735 tag: TagEnd,
736 range: Range<usize>,
737 f: &impl Fn(String, &Range<usize>, HtmlDiagMode),
738 ) {
739 let script_or_style_tag = self.tags.iter().find_map(|tag| match tag {
741 HtmlOrMarkdownTag::Html(at, _span) => {
742 Some((HtmlRawTextTag::from_tag(at)?, tag.range()))
743 }
744 HtmlOrMarkdownTag::Markdown(..) => None,
745 });
746 if let Some((html_tag, tag_range)) = script_or_style_tag {
747 f(
748 format!(
749 "nested Markdown {} in HTML `{}` tag",
750 markdown_tag_name(tag),
751 html_tag.name()
752 ),
753 &range,
754 HtmlDiagMode::MarkdownNestedInRawText(tag_range.clone(), html_tag),
755 );
756 }
757 self.tags.push(HtmlOrMarkdownTag::Markdown(tag, range));
758 }
759
760 fn pop_markdown_tag(
761 &mut self,
762 tag_end: TagEnd,
763 range: Range<usize>,
764 f: &impl Fn(String, &Range<usize>, HtmlDiagMode),
765 ) {
766 let tag_is_match = |tag: &HtmlOrMarkdownTag| match tag {
767 HtmlOrMarkdownTag::Html(..) => false,
768 HtmlOrMarkdownTag::Markdown(last_tag, _span) => *last_tag == tag_end,
769 };
770 if let Some(pos) = self.tags.iter().rposition(tag_is_match) {
771 let script_or_style_tag = self.tags.iter().skip(pos + 1).find_map(|tag| match tag {
778 HtmlOrMarkdownTag::Html(at, _span) => {
779 Some((HtmlRawTextTag::from_tag(at)?, tag.range()))
780 }
781 HtmlOrMarkdownTag::Markdown(..) => None,
782 });
783 if let Some((tag, tag_range)) = script_or_style_tag {
784 f(
785 format!(
786 "improperly nested Markdown {} in HTML `{}` tag",
787 markdown_tag_name(tag_end),
788 tag.name()
789 ),
790 &range,
791 HtmlDiagMode::MarkdownNestedInRawText(tag_range.clone(), tag),
792 );
793 self.tags.truncate(pos);
794 self.tags.push(HtmlOrMarkdownTag::Html(tag.name().to_owned(), tag_range));
799 } else {
800 self.unclosed_tag_buf.extend(self.tags.drain(pos + 1..).map(|unclosed_tag| {
804 BufferedUnclosedTag {
805 unclosed_tag,
806 reason: HtmlOrMarkdownTag::Markdown(tag_end, range.clone()),
807 }
808 }));
809 self.tags.pop();
811 }
812 } else {
813 let mode = HtmlDiagMode::Unopened {
816 possible_pair: self
817 .unclosed_tag_buf
818 .iter()
819 .rposition(|buf| tag_is_match(&buf.unclosed_tag))
820 .map(|pos| self.unclosed_tag_buf.remove(pos)),
821 };
822 f(format!("improperly nested Markdown {}", markdown_tag_name(tag_end)), &range, mode);
823 }
824 }
825}
826
827fn markdown_tag_name(tag: TagEnd) -> &'static str {
828 match tag {
829 TagEnd::Paragraph => "paragraph",
830 TagEnd::Heading(..) => "heading",
831 TagEnd::BlockQuote => "block quote `>`",
832 TagEnd::CodeBlock => "code block",
833 TagEnd::HtmlBlock => "HTML",
834 TagEnd::List(true) => "numbered list",
835 TagEnd::List(false) => "bulleted list",
836 TagEnd::Item => "list item",
837 TagEnd::FootnoteDefinition => "footnote definition",
838 TagEnd::Table => "table",
839 TagEnd::TableHead => "table head",
840 TagEnd::TableRow => "table row",
841 TagEnd::TableCell => "table cell",
842 TagEnd::Emphasis => "emphasis",
843 TagEnd::Strong => "strong emphasis",
844 TagEnd::Strikethrough => "strikethrough",
845 TagEnd::Link => "link",
846 TagEnd::Image => "image",
847 TagEnd::MetadataBlock(..) => "front matter",
848 }
849}
850
851#[cfg(test)]
852mod tests;