diff --git a/src/doc/rustdoc/src/how-to-write-documentation.md b/src/doc/rustdoc/src/how-to-write-documentation.md
index 38fd1db5c21e8..f1eb54d687dab 100644
--- a/src/doc/rustdoc/src/how-to-write-documentation.md
+++ b/src/doc/rustdoc/src/how-to-write-documentation.md
@@ -254,6 +254,131 @@ characters:
 
 So, no need to manually enter those Unicode characters!
 
+### Inline HTML
+
+As a standard Commonmark parser with no special restrictions, rustdoc allows
+you to write HTML whenever the regular markup isn't sufficient, such as
+advanced table layouts:
+
+```html
+
+    
+        
+            | Name+ | Description+ | Category+ | 
+    
+    
+        
+            | traits+ | static and dynamic dispatch+ | generics+ | 
+        
+            | enums+ | 
+        
+            | doc comments+ | reference documentation+ | 
+        
+            | markdown comments+ | attributes with custom syntax+ | 
+    
+
+```
+
+This will render the same way Markdown tables do, even though Markdown tables
+don't support `colspan`:
+
+
+    
+        
+            | Name+ | Description+ | Category+ | 
+    
+    
+        
+            | traits+ | static and dynamic dispatch+ | generics+ | 
+        
+            | enums+ | 
+        
+            | doc comments+ | reference documentation+ | 
+        
+            | markdown comments+ | attributes with custom syntax+ | 
+    
+
+
+HTML is not sanitized when included in the documentation, though
+improperly-nested tags will produce a build-time warning.
+
+```text
+warning: unclosed HTML tag `h2`
+  --> $DIR/invalid-html-tags.rs:19:7
+   |
+LL | ///   
+   |       ^^^^
+
+warning: unclosed quoted HTML attribute on tag `p`
+  --> $DIR/invalid-html-self-closing-tag.rs:19:14
+   |
+LL | /// 
+   |                           -^^^
+   |                           |
+   |                           help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+warning: unprefixed HTML `id` attribute
+  --> $DIR/unprefixed-html-id.rs:4:24
+   |
+LL | /// Test with 
+   |                        -^^^
+   |                        |
+   |                        help: add prefix: `unprefixed_html_id_`
+   |
+
+warning: 2 warnings emitted
+```
+
+We recommend the following additional restrictions:
+
+  * Start all doc comments with a one-sentence summary that doesn't use
+    inline HTML. This summary will be used in contexts where arbitrary HTML
+    cannot, such as tooltips.
+  * Though JavaScript is allowed, many viewers won't run it. Ensure your docs
+    are readable without JavaScript.
+  * Do not embed CSS or JavaScript in doc comments to customize rustdoc's
+    UI. If you want to publish documentation with a customized UI, invoke
+    rustdoc with the `--html-in-header` [command-line parameter] to generate it
+    with your custom stylesheet or script, then publish the result as
+    pre-built HTML.
+
 [`backtrace`]: https://docs.rs/backtrace/0.3.50/backtrace/
 [commonmark markdown specification]: https://commonmark.org/
 [commonmark quick reference]: https://commonmark.org/help/
@@ -268,3 +393,4 @@ So, no need to manually enter those Unicode characters!
 [strikethrough]: https://github.github.com/gfm/#strikethrough-extension-
 [tables]: https://github.github.com/gfm/#tables-extension-
 [task list extension]: https://github.github.com/gfm/#task-list-items-extension-
+[command-line parameter]: command-line-arguments.md#--html-in-header-include-more-html-in
diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs
index 3aad97bc296fb..73d870c9f8709 100644
--- a/src/librustdoc/lint.rs
+++ b/src/librustdoc/lint.rs
@@ -152,6 +152,26 @@ declare_rustdoc_lint! {
     "detects invalid HTML tags in doc comments"
 }
 
+declare_rustdoc_lint! {
+    /// The `unprefixed_html_id` lint detects potential HTML ID conflicts. This is a
+    /// `rustdoc` only lint, see the documentation in the [rustdoc book].
+    ///
+    /// [rustdoc book]: ../../../rustdoc/lints.html#unprefixed_html_ids
+    UNPREFIXED_HTML_ID,
+    Warn,
+    "detects HTML id attributes that do not start with the crate name"
+}
+
+declare_rustdoc_lint! {
+    /// The `unprefixed_html_class` lint detects potential HTML class conflicts. This is a
+    /// `rustdoc` only lint, see the documentation in the [rustdoc book].
+    ///
+    /// [rustdoc book]: ../../../rustdoc/lints.html#unprefixed_html_ids
+    UNPREFIXED_HTML_CLASS,
+    Warn,
+    "detects HTML class attributes that do not start with the crate name"
+}
+
 declare_rustdoc_lint! {
     /// The `bare_urls` lint detects when a URL is not a hyperlink.
     /// This is a `rustdoc` only lint, see the documentation in the [rustdoc book].
@@ -185,6 +205,8 @@ pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| {
         INVALID_HTML_TAGS,
         BARE_URLS,
         MISSING_CRATE_LEVEL_DOCS,
+        UNPREFIXED_HTML_ID,
+        UNPREFIXED_HTML_CLASS,
     ]
 });
 
diff --git a/src/librustdoc/passes/lint/html_tags.rs b/src/librustdoc/passes/lint/html_tags.rs
index 070c0aab5868b..f2575d4fb39db 100644
--- a/src/librustdoc/passes/lint/html_tags.rs
+++ b/src/librustdoc/passes/lint/html_tags.rs
@@ -4,6 +4,7 @@ use crate::core::DocContext;
 use crate::html::markdown::main_body_opts;
 use crate::passes::source_span_for_markdown_range;
 
+use itertools::Itertools;
 use pulldown_cmark::{BrokenLink, Event, LinkType, Parser, Tag};
 
 use std::iter::Peekable;
@@ -17,92 +18,6 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
     else { return };
     let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
     if !dox.is_empty() {
-        let report_diag = |msg: &str, range: &Range, is_open_tag: bool| {
-            let sp = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs) {
-                Some(sp) => sp,
-                None => item.attr_span(tcx),
-            };
-            tcx.struct_span_lint_hir(crate::lint::INVALID_HTML_TAGS, hir_id, sp, msg, |lint| {
-                use rustc_lint_defs::Applicability;
-                // If a tag looks like ``, it might actually be a generic.
-                // We don't try to detect stuff `` because that's not valid HTML,
-                // and we don't try to detect stuff `` because that's not valid Rust.
-                let mut generics_end = range.end;
-                if let Some(Some(mut generics_start)) = (is_open_tag
-                    && dox[..generics_end].ends_with('>'))
-                .then(|| extract_path_backwards(&dox, range.start))
-                {
-                    while generics_start != 0
-                        && generics_end < dox.len()
-                        && dox.as_bytes()[generics_start - 1] == b'<'
-                        && dox.as_bytes()[generics_end] == b'>'
-                    {
-                        generics_end += 1;
-                        generics_start -= 1;
-                        if let Some(new_start) = extract_path_backwards(&dox, generics_start) {
-                            generics_start = new_start;
-                        }
-                        if let Some(new_end) = extract_path_forward(&dox, generics_end) {
-                            generics_end = new_end;
-                        }
-                    }
-                    if let Some(new_end) = extract_path_forward(&dox, generics_end) {
-                        generics_end = new_end;
-                    }
-                    let generics_sp = match source_span_for_markdown_range(
-                        tcx,
-                        &dox,
-                        &(generics_start..generics_end),
-                        &item.attrs,
-                    ) {
-                        Some(sp) => sp,
-                        None => item.attr_span(tcx),
-                    };
-                    // Sometimes, we only extract part of a path. For example, consider this:
-                    //
-                    //     <[u32] as IntoIter>::Item
-                    //                       ^^^^^ unclosed HTML tag `u32`
-                    //
-                    // We don't have any code for parsing fully-qualified trait paths.
-                    // In theory, we could add it, but doing it correctly would require
-                    // parsing the entire path grammar, which is problematic because of
-                    // overlap between the path grammar and Markdown.
-                    //
-                    // The example above shows that ambiguity. Is `[u32]` intended to be an
-                    // intra-doc link to the u32 primitive, or is it intended to be a slice?
-                    //
-                    // If the below conditional were removed, we would suggest this, which is
-                    // not what the user probably wants.
-                    //
-                    //     <[u32] as `IntoIter`>::Item
-                    //
-                    // We know that the user actually wants to wrap the whole thing in a code
-                    // block, but the only reason we know that is because `u32` does not, in
-                    // fact, implement IntoIter. If the example looks like this:
-                    //
-                    //     <[Vec] as IntoIter::Item
-                    //
-                    // The ideal fix would be significantly different.
-                    if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<')
-                        || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>')
-                    {
-                        return lint;
-                    }
-                    // multipart form is chosen here because ``Vec`` would be confusing.
-                    lint.multipart_suggestion(
-                        "try marking as source code",
-                        vec![
-                            (generics_sp.shrink_to_lo(), String::from("`")),
-                            (generics_sp.shrink_to_hi(), String::from("`")),
-                        ],
-                        Applicability::MaybeIncorrect,
-                    );
-                }
-
-                lint
-            });
-        };
-
         let mut tags = Vec::new();
         let mut is_in_comment = None;
         let mut in_code_block = false;
@@ -132,6 +47,8 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
         let p = Parser::new_with_broken_link_callback(&dox, main_body_opts(), Some(&mut replacer))
             .into_offset_iter();
 
+        let report_diag = ReportDiag { cx, dox: dox.clone(), hir_id, item };
+
         for (event, range) in p {
             match event {
                 Event::Start(Tag::CodeBlock(_)) => in_code_block = true,
@@ -147,11 +64,11 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
             let t = t.to_lowercase();
             !ALLOWED_UNCLOSED.contains(&t.as_str())
         }) {
-            report_diag(&format!("unclosed HTML tag `{}`", tag), range, true);
+            report_diag.invalid_html_tags(&format!("unclosed HTML tag `{}`", tag), range, true);
         }
 
         if let Some(range) = is_in_comment {
-            report_diag("Unclosed HTML comment", &range, false);
+            report_diag.invalid_html_tags("Unclosed HTML comment", &range, false);
         }
     }
 }
@@ -165,7 +82,7 @@ fn drop_tag(
     tags: &mut Vec<(String, Range)>,
     tag_name: String,
     range: Range,
-    f: &impl Fn(&str, &Range, bool),
+    report_diag: &ReportDiag<'_, '_>,
 ) {
     let tag_name_low = tag_name.to_lowercase();
     if let Some(pos) = tags.iter().rposition(|(t, _)| t.to_lowercase() == tag_name_low) {
@@ -186,14 +103,18 @@ fn drop_tag(
             // `tags` is used as a queue, meaning that everything after `pos` is included inside it.
             // So `
` will look like `["h2", "h3"]`. So when closing `h2`, we will still
             // have `h3`, meaning the tag wasn't closed as it should have.
-            f(&format!("unclosed HTML tag `{}`", last_tag_name), &last_tag_span, true);
+            report_diag.invalid_html_tags(
+                &format!("unclosed HTML tag `{}`", last_tag_name),
+                &last_tag_span,
+                true,
+            );
         }
         // Remove the `tag_name` that was originally closed
         tags.pop();
     } else {
         // It can happen for example in this case: `` (the `h2` tag isn't required
         // but it helps for the visualization).
-        f(&format!("unopened HTML tag `{}`", tag_name), &range, false);
+        report_diag.invalid_html_tags(&format!("unopened HTML tag `{}`", tag_name), &range, false);
     }
 }
 
@@ -257,13 +178,59 @@ fn is_valid_for_html_tag_name(c: char, is_empty: bool) -> bool {
     c.is_ascii_alphabetic() || !is_empty && (c == '-' || c.is_ascii_digit())
 }
 
+/// These class names are provided by rustdoc to doc authors as a stable API.
+///
+/// Structure: `[(class, [depends_on_other_class])]`
+const STABLE_CLASSES: &[(&str, &[&str])] =
+    &[("stab", &[]), ("deprecated", &["stab"]), ("portability", &["stab"])];
+
+fn check_html_attr(name: &str, value: &str, range: Range, report_diag: &ReportDiag<'_, '_>) {
+    match name {
+        "class" => {
+            let prefix = format!("{}_", report_diag.crate_name());
+            let has_prefix = |name: &str| name.starts_with(&prefix);
+            let is_stable_class = |name: &str| {
+                STABLE_CLASSES.iter().any(|&(class, depends_on_other_classes)| {
+                    class == name
+                        && depends_on_other_classes.iter().all(|&class| {
+                            value.split_ascii_whitespace().any(|class2| class2 == class)
+                        })
+                })
+            };
+            // Can't use `split_ascii_whitespace()`, because I need byte offsets to report suggestions.
+            let mut start = None;
+            for (i, c) in value.char_indices() {
+                if c.is_ascii_whitespace() {
+                    if let Some(start) = start && let class_name = &value[start..i] && !has_prefix(class_name) && !is_stable_class(class_name) {
+                        let range = (start + range.start)..(i + range.start);
+                        report_diag.unprefixed_html_class(&range);
+                    }
+                    start = None;
+                } else if start.is_none() {
+                    start = Some(i);
+                }
+            }
+            if let Some(start) = start && let class_name = &value[start..] && !has_prefix(class_name) && !is_stable_class(class_name) {
+                let range = (start + range.start)..range.end;
+                report_diag.unprefixed_html_class(&range);
+            }
+        }
+        "id" => {
+            if !value.starts_with(&format!("{}_", report_diag.crate_name())) {
+                report_diag.unprefixed_html_id(&range);
+            }
+        }
+        _ => {}
+    }
+}
+
 fn extract_html_tag(
     tags: &mut Vec<(String, Range)>,
     text: &str,
     range: &Range,
     start_pos: usize,
     iter: &mut Peekable>,
-    f: &impl Fn(&str, &Range, bool),
+    report_diag: &ReportDiag<'_, '_>,
 ) {
     let mut tag_name = String::new();
     let mut is_closing = false;
@@ -293,13 +260,13 @@ fn extract_html_tag(
                 if is_closing {
                     // In case we have "" or even "".
                     if c != '>' {
-                        if !c.is_whitespace() {
+                        if !c.is_ascii_whitespace() {
                             // It seems like it's not a valid HTML tag.
                             break;
                         }
                         let mut found = false;
                         for (new_pos, c) in text[pos..].char_indices() {
-                            if !c.is_whitespace() {
+                            if !c.is_ascii_whitespace() {
                                 if c == '>' {
                                     r.end = range.start + new_pos + 1;
                                     found = true;
@@ -311,61 +278,119 @@ fn extract_html_tag(
                             break;
                         }
                     }
-                    drop_tag(tags, tag_name, r, f);
+                    drop_tag(tags, tag_name, r, report_diag);
                 } else {
-                    let mut is_self_closing = false;
-                    let mut quote_pos = None;
-                    if c != '>' {
-                        let mut quote = None;
-                        let mut after_eq = false;
-                        for (i, c) in text[pos..].char_indices() {
-                            if !c.is_whitespace() {
-                                if let Some(q) = quote {
-                                    if c == q {
-                                        quote = None;
-                                        quote_pos = None;
-                                        after_eq = false;
-                                    }
-                                } else if c == '>' {
+                    #[derive(Clone, Copy, Eq, PartialEq)]
+                    enum HtmlAttrParseState {
+                        Start { start_pos: usize },
+                        PotentiallySelfClosing,
+                        AfterEq { start_pos: usize, eq_pos: usize },
+                        Quoted { start_pos: usize, eq_pos: usize, quote_pos: usize, quote: char },
+                        Unquoted { start_pos: usize, eq_pos: usize },
+                    }
+                    let mut state = HtmlAttrParseState::Start { start_pos: pos };
+                    for (i, c) in text[pos..].char_indices() {
+                        let cur_pos = pos + i;
+                        match state {
+                            HtmlAttrParseState::Start { start_pos } => {
+                                if c.is_ascii_whitespace() {
+                                    state = HtmlAttrParseState::Start { start_pos: cur_pos + 1 };
+                                } else if c == '/' {
+                                    state = HtmlAttrParseState::PotentiallySelfClosing;
+                                } else if c == '=' {
+                                    state =
+                                        HtmlAttrParseState::AfterEq { start_pos, eq_pos: cur_pos }
+                                }
+                            }
+                            HtmlAttrParseState::PotentiallySelfClosing => {
+                                if c == '>' {
                                     break;
-                                } else if c == '/' && !after_eq {
-                                    is_self_closing = true;
+                                } else if !c.is_ascii_whitespace() {
+                                    state = HtmlAttrParseState::Start { start_pos: cur_pos };
+                                }
+                            }
+                            HtmlAttrParseState::AfterEq { start_pos, eq_pos } => {
+                                if c == '>' {
+                                    break;
+                                } else if c == '"' || c == '\'' {
+                                    state = HtmlAttrParseState::Quoted {
+                                        start_pos,
+                                        eq_pos,
+                                        quote_pos: cur_pos,
+                                        quote: c,
+                                    };
+                                } else if c.is_ascii_whitespace() {
+                                    check_html_attr(
+                                        &text[start_pos..eq_pos],
+                                        "",
+                                        (r.start + eq_pos)..(r.start + eq_pos),
+                                        report_diag,
+                                    );
+                                    state = HtmlAttrParseState::Start { start_pos: cur_pos + 1 };
                                 } else {
-                                    if is_self_closing {
-                                        is_self_closing = false;
-                                    }
-                                    if (c == '"' || c == '\'') && after_eq {
-                                        quote = Some(c);
-                                        quote_pos = Some(pos + i);
-                                    } else if c == '=' {
-                                        after_eq = true;
-                                    }
+                                    state = HtmlAttrParseState::Unquoted { start_pos, eq_pos }
+                                }
+                            }
+                            HtmlAttrParseState::Quoted { start_pos, eq_pos, quote_pos, quote } => {
+                                if c == quote {
+                                    check_html_attr(
+                                        &text[start_pos..eq_pos],
+                                        &text[(quote_pos + 1)..cur_pos],
+                                        (quote_pos + 1 + r.start)..(cur_pos + r.start),
+                                        report_diag,
+                                    );
+                                    state = HtmlAttrParseState::Start { start_pos: cur_pos + 1 };
+                                }
+                            }
+                            HtmlAttrParseState::Unquoted { start_pos, eq_pos } => {
+                                if c == '>' {
+                                    check_html_attr(
+                                        &text[start_pos..eq_pos],
+                                        &text[(eq_pos + 1)..cur_pos],
+                                        (eq_pos + 1 + r.start)..(cur_pos + r.start),
+                                        report_diag,
+                                    );
+                                    break;
+                                } else if c.is_ascii_whitespace() {
+                                    check_html_attr(
+                                        &text[start_pos..eq_pos],
+                                        &text[(eq_pos + 1)..cur_pos],
+                                        (eq_pos + 1 + r.start)..(cur_pos + r.start),
+                                        report_diag,
+                                    );
+                                    state = HtmlAttrParseState::Start { start_pos: cur_pos + 1 };
                                 }
-                            } else if quote.is_none() {
-                                after_eq = false;
                             }
                         }
                     }
-                    if let Some(quote_pos) = quote_pos {
-                        let qr = Range { start: quote_pos, end: quote_pos };
-                        f(
-                            &format!("unclosed quoted HTML attribute on tag `{}`", tag_name),
-                            &qr,
-                            false,
-                        );
-                    }
-                    if is_self_closing {
-                        // https://html.spec.whatwg.org/#parse-error-non-void-html-element-start-tag-with-trailing-solidus
-                        let valid = ALLOWED_UNCLOSED.contains(&&tag_name[..])
-                            || tags.iter().take(pos + 1).any(|(at, _)| {
-                                let at = at.to_lowercase();
-                                at == "svg" || at == "math"
-                            });
-                        if !valid {
-                            f(&format!("invalid self-closing HTML tag `{}`", tag_name), &r, false);
+                    match state {
+                        HtmlAttrParseState::PotentiallySelfClosing => {
+                            // https://html.spec.whatwg.org/#parse-error-non-void-html-element-start-tag-with-trailing-solidus
+                            let valid = ALLOWED_UNCLOSED.contains(&&tag_name[..])
+                                || tags.iter().take(pos + 1).any(|(at, _)| {
+                                    let at = at.to_lowercase();
+                                    at == "svg" || at == "math"
+                                });
+                            if !valid {
+                                report_diag.invalid_html_tags(
+                                    &format!("invalid self-closing HTML tag `{}`", tag_name),
+                                    &r,
+                                    false,
+                                );
+                            }
+                        }
+                        HtmlAttrParseState::Quoted { quote_pos, .. } => {
+                            let qr = Range { start: quote_pos, end: quote_pos };
+                            report_diag.invalid_html_tags(
+                                &format!("unclosed quoted HTML attribute on tag `{}`", tag_name),
+                                &qr,
+                                false,
+                            );
+                            tags.push((tag_name, r));
+                        }
+                        _ => {
+                            tags.push((tag_name, r));
                         }
-                    } else {
-                        tags.push((tag_name, r));
                     }
                 }
             }
@@ -380,7 +405,7 @@ fn extract_tags(
     text: &str,
     range: Range,
     is_in_comment: &mut Option>,
-    f: &impl Fn(&str, &Range, bool),
+    report_diag: &ReportDiag<'_, '_>,
 ) {
     let mut iter = text.char_indices().peekable();
 
@@ -400,8 +425,162 @@ fn extract_tags(
                     end: range.start + start_pos + 3,
                 });
             } else {
-                extract_html_tag(tags, text, &range, start_pos, &mut iter, f);
+                extract_html_tag(tags, text, &range, start_pos, &mut iter, report_diag);
             }
         }
     }
 }
+
+struct ReportDiag<'cx, 'item> {
+    cx: &'item DocContext<'cx>,
+    dox: String,
+    hir_id: rustc_hir::HirId,
+    item: &'item Item,
+}
+
+impl<'cx, 'item> ReportDiag<'cx, 'item> {
+    fn invalid_html_tags(&self, msg: &str, range: &Range, is_open_tag: bool) {
+        let ReportDiag { cx, ref dox, hir_id, item } = *self;
+        let tcx = cx.tcx;
+        let sp = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs) {
+            Some(sp) => sp,
+            None => item.attr_span(tcx),
+        };
+        tcx.struct_span_lint_hir(crate::lint::INVALID_HTML_TAGS, hir_id, sp, msg, |lint| {
+            use rustc_lint_defs::Applicability;
+            // If a tag looks like ``, it might actually be a generic.
+            // We don't try to detect stuff `` because that's not valid HTML,
+            // and we don't try to detect stuff `` because that's not valid Rust.
+            let mut generics_end = range.end;
+            if let Some(Some(mut generics_start)) = (is_open_tag
+                && dox[..generics_end].ends_with('>'))
+            .then(|| extract_path_backwards(&dox, range.start))
+            {
+                while generics_start != 0
+                    && generics_end < dox.len()
+                    && dox.as_bytes()[generics_start - 1] == b'<'
+                    && dox.as_bytes()[generics_end] == b'>'
+                {
+                    generics_end += 1;
+                    generics_start -= 1;
+                    if let Some(new_start) = extract_path_backwards(&dox, generics_start) {
+                        generics_start = new_start;
+                    }
+                    if let Some(new_end) = extract_path_forward(&dox, generics_end) {
+                        generics_end = new_end;
+                    }
+                }
+                if let Some(new_end) = extract_path_forward(&dox, generics_end) {
+                    generics_end = new_end;
+                }
+                let generics_sp = match source_span_for_markdown_range(
+                    tcx,
+                    &dox,
+                    &(generics_start..generics_end),
+                    &item.attrs,
+                ) {
+                    Some(sp) => sp,
+                    None => item.attr_span(tcx),
+                };
+                // Sometimes, we only extract part of a path. For example, consider this:
+                //
+                //     <[u32] as IntoIter>::Item
+                //                       ^^^^^ unclosed HTML tag `u32`
+                //
+                // We don't have any code for parsing fully-qualified trait paths.
+                // In theory, we could add it, but doing it correctly would require
+                // parsing the entire path grammar, which is problematic because of
+                // overlap between the path grammar and Markdown.
+                //
+                // The example above shows that ambiguity. Is `[u32]` intended to be an
+                // intra-doc link to the u32 primitive, or is it intended to be a slice?
+                //
+                // If the below conditional were removed, we would suggest this, which is
+                // not what the user probably wants.
+                //
+                //     <[u32] as `IntoIter`>::Item
+                //
+                // We know that the user actually wants to wrap the whole thing in a code
+                // block, but the only reason we know that is because `u32` does not, in
+                // fact, implement IntoIter. If the example looks like this:
+                //
+                //     <[Vec] as IntoIter::Item
+                //
+                // The ideal fix would be significantly different.
+                if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<')
+                    || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>')
+                {
+                    return lint;
+                }
+                // multipart form is chosen here because ``Vec`` would be confusing.
+                lint.multipart_suggestion(
+                    "try marking as source code",
+                    vec![
+                        (generics_sp.shrink_to_lo(), String::from("`")),
+                        (generics_sp.shrink_to_hi(), String::from("`")),
+                    ],
+                    Applicability::MaybeIncorrect,
+                );
+            }
+
+            lint
+        });
+    }
+    fn crate_name(&self) -> rustc_span::Symbol {
+        self.cx.tcx.crate_name(self.item.item_id.krate())
+    }
+    fn unprefixed_html_class(&self, range: &Range) {
+        let ReportDiag { cx, ref dox, hir_id, item } = *self;
+        let tcx = cx.tcx;
+        let span = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs) {
+            Some(span) => span,
+            None => item.attr_span(tcx),
+        };
+        let msg = "unprefixed HTML `class` attribute";
+        tcx.struct_span_lint_hir(crate::lint::UNPREFIXED_HTML_CLASS, hir_id, span, msg, |lint| {
+            use rustc_lint_defs::Applicability;
+            lint.multipart_suggestion(
+                "add prefix",
+                vec![(span.shrink_to_lo(), format!("{}_", self.crate_name()))],
+                Applicability::MaybeIncorrect,
+            );
+            lint.help(format!(
+                "classes should start with `{{cratename}}_`, or be: {}",
+                STABLE_CLASSES
+                    .iter()
+                    .enumerate()
+                    .map(|(i, &(class, depends_on_other_classes))| {
+                        let mut result =
+                            if i == STABLE_CLASSES.len() - 1 { "or `" } else { "`" }.to_string();
+                        for other in depends_on_other_classes {
+                            result.push_str(other);
+                            result.push_str(" ");
+                        }
+                        result.push_str(class);
+                        result.push_str("`");
+                        result
+                    })
+                    .join(", ")
+            ));
+            lint
+        });
+    }
+    fn unprefixed_html_id(&self, range: &Range) {
+        let ReportDiag { cx, ref dox, hir_id, item } = *self;
+        let tcx = cx.tcx;
+        let span = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs) {
+            Some(span) => span,
+            None => item.attr_span(tcx),
+        };
+        let msg = "unprefixed HTML `id` attribute";
+        tcx.struct_span_lint_hir(crate::lint::UNPREFIXED_HTML_ID, hir_id, span, msg, |lint| {
+            use rustc_lint_defs::Applicability;
+            lint.multipart_suggestion(
+                "add prefix",
+                vec![(span.shrink_to_lo(), format!("{}_", self.crate_name()))],
+                Applicability::MachineApplicable,
+            );
+            lint
+        });
+    }
+}
diff --git a/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.rs b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.rs
index 476e3b2d43e4a..180a544d94949 100644
--- a/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.rs
+++ b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.rs
@@ -50,7 +50,7 @@ pub struct FullyQualifiedPathsDoNotCount3;
 // out if this is valid would require parsing the entire path grammar.
 pub struct FullyQualifiedPathsDoNotCount4;
 
-/// This Vec thing!
+/// This Vec thing!
 //~^ERROR unclosed HTML tag `i32`
 // HTML attributes shouldn't be treated as Rust syntax, so no suggestions.
 pub struct TagWithAttributes;
diff --git a/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.stderr b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.stderr
index 3856a251321b2..81bc0aac02ca5 100644
--- a/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.stderr
+++ b/src/test/rustdoc-ui/suggestions/html-as-generics-no-suggestions.stderr
@@ -37,7 +37,7 @@ LL | /// This Vec as IntoIter> thing!
 error: unclosed HTML tag `i32`
   --> $DIR/html-as-generics-no-suggestions.rs:53:13
    |
-LL | /// This Vec thing!
+LL | /// This Vec thing!
    |             ^^^^
 
 error: unopened HTML tag `i32`
diff --git a/src/test/rustdoc-ui/unprefixed-html-class.fixed b/src/test/rustdoc-ui/unprefixed-html-class.fixed
new file mode 100644
index 0000000000000..a54f5cf41e496
--- /dev/null
+++ b/src/test/rustdoc-ui/unprefixed-html-class.fixed
@@ -0,0 +1,56 @@
+// run-rustfix
+#![deny(rustdoc::unprefixed_html_class)]
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct BasicBad;
+
+/// Test with 
+pub struct BasicGood;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct MixedBadAndGood1;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct MixedBadAndGood2;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+//~^^ ERROR unprefixed HTML `class` attribute
+pub struct DoubleBad;
+
+// `stab` is currently the only class name that's allowed unprefixed.
+
+/// Test with 
+/// Test with 
+/// Test with 
+/// Test with 
+/// Test with 
+/// Test with 
+/// Test with 
+pub struct Stab;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct StandaloneDeprecatedIsBad;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct StandalonePortabilityIsBad;
+
+/// Test unquoted: 
+pub struct UnquotedStab;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct UnquotedBad;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct UnquotedBadMixed1;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct UnquotedBadMixed2;
diff --git a/src/test/rustdoc-ui/unprefixed-html-class.rs b/src/test/rustdoc-ui/unprefixed-html-class.rs
new file mode 100644
index 0000000000000..c2b56d1b9dc47
--- /dev/null
+++ b/src/test/rustdoc-ui/unprefixed-html-class.rs
@@ -0,0 +1,56 @@
+// run-rustfix
+#![deny(rustdoc::unprefixed_html_class)]
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct BasicBad;
+
+/// Test with 
+pub struct BasicGood;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct MixedBadAndGood1;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct MixedBadAndGood2;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+//~^^ ERROR unprefixed HTML `class` attribute
+pub struct DoubleBad;
+
+// `stab` is currently the only class name that's allowed unprefixed.
+
+/// Test with 
+/// Test with 
+/// Test with 
+/// Test with 
+/// Test with 
+/// Test with 
+/// Test with 
+pub struct Stab;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct StandaloneDeprecatedIsBad;
+
+/// Test with 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct StandalonePortabilityIsBad;
+
+/// Test unquoted: 
+pub struct UnquotedStab;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct UnquotedBad;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct UnquotedBadMixed1;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `class` attribute
+pub struct UnquotedBadMixed2;
diff --git a/src/test/rustdoc-ui/unprefixed-html-class.stderr b/src/test/rustdoc-ui/unprefixed-html-class.stderr
new file mode 100644
index 0000000000000..f9f0c91f7c3b6
--- /dev/null
+++ b/src/test/rustdoc-ui/unprefixed-html-class.stderr
@@ -0,0 +1,107 @@
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:4:27
+   |
+LL | /// Test with 
+   |                           -^^^
+   |                           |
+   |                           help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+note: the lint level is defined here
+  --> $DIR/unprefixed-html-class.rs:2:9
+   |
+LL | #![deny(rustdoc::unprefixed_html_class)]
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:11:54
+   |
+LL | /// Test with 
+   |                                                      -^^^
+   |                                                      |
+   |                                                      help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:15:27
+   |
+LL | /// Test with 
+   |                           -^^^
+   |                           |
+   |                           help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:19:27
+   |
+LL | /// Test with 
+   |                           -^^^
+   |                           |
+   |                           help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:19:32
+   |
+LL | /// Test with 
+   |                                -^^^^^
+   |                                |
+   |                                help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:35:27
+   |
+LL | /// Test with 
+   |                           -^^^^^^^^^
+   |                           |
+   |                           help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:39:27
+   |
+LL | /// Test with 
+   |                           -^^^^^^^^^^
+   |                           |
+   |                           help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:46:31
+   |
+LL | /// Test unquoted: 
+   |                               -^^
+   |                               |
+   |                               help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:50:31
+   |
+LL | /// Test unquoted: 
+   |                               -^^
+   |                               |
+   |                               help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+error: unprefixed HTML `class` attribute
+  --> $DIR/unprefixed-html-class.rs:54:40
+   |
+LL | /// Test unquoted: 
+   |                                        -^^
+   |                                        |
+   |                                        help: add prefix: `unprefixed_html_class_`
+   |
+   = help: classes should start with `{cratename}_`, or be: `stab`, `stab deprecated`, or `stab portability`
+
+error: aborting due to 10 previous errors
+
diff --git a/src/test/rustdoc-ui/unprefixed-html-id.fixed b/src/test/rustdoc-ui/unprefixed-html-id.fixed
new file mode 100644
index 0000000000000..bf3f3ef1a56f0
--- /dev/null
+++ b/src/test/rustdoc-ui/unprefixed-html-id.fixed
@@ -0,0 +1,27 @@
+// run-rustfix
+#![deny(rustdoc::unprefixed_html_id)]
+
+/// Test with 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct BasicBad;
+
+/// Test with 
+pub struct BasicGood;
+
+// `stab` is not allowed as a special ID.
+
+/// Test with 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct StabIsNotAnId;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct UnquotedStab;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct UnquotedBadMixed1;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct UnquotedBadMixed2;
diff --git a/src/test/rustdoc-ui/unprefixed-html-id.rs b/src/test/rustdoc-ui/unprefixed-html-id.rs
new file mode 100644
index 0000000000000..71d4cba7d2b73
--- /dev/null
+++ b/src/test/rustdoc-ui/unprefixed-html-id.rs
@@ -0,0 +1,27 @@
+// run-rustfix
+#![deny(rustdoc::unprefixed_html_id)]
+
+/// Test with 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct BasicBad;
+
+/// Test with 
+pub struct BasicGood;
+
+// `stab` is not allowed as a special ID.
+
+/// Test with 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct StabIsNotAnId;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct UnquotedStab;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct UnquotedBadMixed1;
+
+/// Test unquoted: 
+//~^ ERROR unprefixed HTML `id` attribute
+pub struct UnquotedBadMixed2;
diff --git a/src/test/rustdoc-ui/unprefixed-html-id.stderr b/src/test/rustdoc-ui/unprefixed-html-id.stderr
new file mode 100644
index 0000000000000..0c27a3bd4b7d2
--- /dev/null
+++ b/src/test/rustdoc-ui/unprefixed-html-id.stderr
@@ -0,0 +1,48 @@
+error: unprefixed HTML `id` attribute
+  --> $DIR/unprefixed-html-id.rs:4:24
+   |
+LL | /// Test with 
+   |                        -^^^
+   |                        |
+   |                        help: add prefix: `unprefixed_html_id_`
+   |
+note: the lint level is defined here
+  --> $DIR/unprefixed-html-id.rs:2:9
+   |
+LL | #![deny(rustdoc::unprefixed_html_id)]
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: unprefixed HTML `id` attribute
+  --> $DIR/unprefixed-html-id.rs:13:24
+   |
+LL | /// Test with 
+   |                        -^^^
+   |                        |
+   |                        help: add prefix: `unprefixed_html_id_`
+
+error: unprefixed HTML `id` attribute
+  --> $DIR/unprefixed-html-id.rs:17:28
+   |
+LL | /// Test unquoted: 
+   |                            -^^^
+   |                            |
+   |                            help: add prefix: `unprefixed_html_id_`
+
+error: unprefixed HTML `id` attribute
+  --> $DIR/unprefixed-html-id.rs:21:28
+   |
+LL | /// Test unquoted: 
+   |                            -^^
+   |                            |
+   |                            help: add prefix: `unprefixed_html_id_`
+
+error: unprefixed HTML `id` attribute
+  --> $DIR/unprefixed-html-id.rs:25:37
+   |
+LL | /// Test unquoted: 
+   |                                     -^^
+   |                                     |
+   |                                     help: add prefix: `unprefixed_html_id_`
+
+error: aborting due to 5 previous errors
+