From 5515fc88dc45c274f0574d381a17d4f72dfd5047 Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Tue, 25 Apr 2023 15:04:22 +0200
Subject: [PATCH 01/13] Implement custom classes for rustdoc code blocks with
 `custom_code_classes_in_docs` feature

---
 compiler/rustc_feature/src/active.rs          |   2 +
 compiler/rustc_span/src/symbol.rs             |   1 +
 src/librustdoc/html/highlight.rs              |  25 +-
 src/librustdoc/html/markdown.rs               | 236 ++++++++++++++----
 src/librustdoc/html/markdown/tests.rs         |  24 ++
 .../passes/check_custom_code_classes.rs       |  77 ++++++
 src/librustdoc/passes/mod.rs                  |   5 +
 7 files changed, 321 insertions(+), 49 deletions(-)
 create mode 100644 src/librustdoc/passes/check_custom_code_classes.rs

diff --git a/compiler/rustc_feature/src/active.rs b/compiler/rustc_feature/src/active.rs
index fcb112eadfedb..6c338be99b61e 100644
--- a/compiler/rustc_feature/src/active.rs
+++ b/compiler/rustc_feature/src/active.rs
@@ -401,6 +401,8 @@ declare_features! (
     /// Allows function attribute `#[coverage(on/off)]`, to control coverage
     /// instrumentation of that function.
     (active, coverage_attribute, "CURRENT_RUSTC_VERSION", Some(84605), None),
+    /// Allows users to provide classes for fenced code block using `class:classname`.
+    (active, custom_code_classes_in_docs, "CURRENT_RUSTC_VERSION", Some(79483), None),
     /// Allows non-builtin attributes in inner attribute position.
     (active, custom_inner_attributes, "1.30.0", Some(54726), None),
     /// Allows custom test frameworks with `#![test_runner]` and `#[test_case]`.
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index 448314cd9e113..68ce64bc8c026 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -592,6 +592,7 @@ symbols! {
         cttz,
         cttz_nonzero,
         custom_attribute,
+        custom_code_classes_in_docs,
         custom_derive,
         custom_inner_attributes,
         custom_mir,
diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs
index 039e8cdb98738..d8e36139a7804 100644
--- a/src/librustdoc/html/highlight.rs
+++ b/src/librustdoc/html/highlight.rs
@@ -52,8 +52,9 @@ pub(crate) fn render_example_with_highlighting(
     out: &mut Buffer,
     tooltip: Tooltip,
     playground_button: Option<&str>,
+    extra_classes: &[String],
 ) {
-    write_header(out, "rust-example-rendered", None, tooltip);
+    write_header(out, "rust-example-rendered", None, tooltip, extra_classes);
     write_code(out, src, None, None);
     write_footer(out, playground_button);
 }
@@ -65,7 +66,13 @@ pub(crate) fn render_item_decl_with_highlighting(src: &str, out: &mut Buffer) {
     write!(out, "</pre>");
 }
 
-fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>, tooltip: Tooltip) {
+fn write_header(
+    out: &mut Buffer,
+    class: &str,
+    extra_content: Option<Buffer>,
+    tooltip: Tooltip,
+    extra_classes: &[String],
+) {
     write!(
         out,
         "<div class=\"example-wrap{}\">",
@@ -100,9 +107,19 @@ fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>, to
         out.push_buffer(extra);
     }
     if class.is_empty() {
-        write!(out, "<pre class=\"rust\">");
+        write!(
+            out,
+            "<pre class=\"rust{}{}\">",
+            if extra_classes.is_empty() { "" } else { " " },
+            extra_classes.join(" "),
+        );
     } else {
-        write!(out, "<pre class=\"rust {class}\">");
+        write!(
+            out,
+            "<pre class=\"rust {class}{}{}\">",
+            if extra_classes.is_empty() { "" } else { " " },
+            extra_classes.join(" "),
+        );
     }
     write!(out, "<code>");
 }
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index b28019e3f91b1..a25a6f7d35d10 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -37,8 +37,9 @@ use once_cell::sync::Lazy;
 use std::borrow::Cow;
 use std::collections::VecDeque;
 use std::fmt::Write;
+use std::iter::Peekable;
 use std::ops::{ControlFlow, Range};
-use std::str;
+use std::str::{self, CharIndices};
 
 use crate::clean::RenderedLink;
 use crate::doctest;
@@ -243,11 +244,21 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
                 let parse_result =
                     LangString::parse_without_check(lang, self.check_error_codes, false);
                 if !parse_result.rust {
+                    let added_classes = parse_result.added_classes;
+                    let lang_string = if let Some(lang) = parse_result.unknown.first() {
+                        format!("language-{}", lang)
+                    } else {
+                        String::new()
+                    };
+                    let whitespace = if added_classes.is_empty() { "" } else { " " };
                     return Some(Event::Html(
                         format!(
                             "<div class=\"example-wrap\">\
-                                 <pre class=\"language-{lang}\"><code>{text}</code></pre>\
+                                 <pre class=\"{lang_string}{whitespace}{added_classes}\">\
+                                     <code>{text}</code>\
+                                 </pre>\
                              </div>",
+                            added_classes = added_classes.join(" "),
                             text = Escape(&original_text),
                         )
                         .into(),
@@ -258,6 +269,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
             CodeBlockKind::Indented => Default::default(),
         };
 
+        let added_classes = parse_result.added_classes;
         let lines = original_text.lines().filter_map(|l| map_line(l).for_html());
         let text = lines.intersperse("\n".into()).collect::<String>();
 
@@ -315,6 +327,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
             &mut s,
             tooltip,
             playground_button.as_deref(),
+            &added_classes,
         );
         Some(Event::Html(s.into_inner().into()))
     }
@@ -711,6 +724,17 @@ pub(crate) fn find_testable_code<T: doctest::Tester>(
     error_codes: ErrorCodes,
     enable_per_target_ignores: bool,
     extra_info: Option<&ExtraInfo<'_>>,
+) {
+    find_codes(doc, tests, error_codes, enable_per_target_ignores, extra_info, false)
+}
+
+pub(crate) fn find_codes<T: doctest::Tester>(
+    doc: &str,
+    tests: &mut T,
+    error_codes: ErrorCodes,
+    enable_per_target_ignores: bool,
+    extra_info: Option<&ExtraInfo<'_>>,
+    include_non_rust: bool,
 ) {
     let mut parser = Parser::new(doc).into_offset_iter();
     let mut prev_offset = 0;
@@ -734,7 +758,7 @@ pub(crate) fn find_testable_code<T: doctest::Tester>(
                     }
                     CodeBlockKind::Indented => Default::default(),
                 };
-                if !block_info.rust {
+                if !include_non_rust && !block_info.rust {
                     continue;
                 }
 
@@ -784,7 +808,19 @@ impl<'tcx> ExtraInfo<'tcx> {
         ExtraInfo { def_id, sp, tcx }
     }
 
-    fn error_invalid_codeblock_attr(&self, msg: String, help: &'static str) {
+    fn error_invalid_codeblock_attr(&self, msg: &str) {
+        if let Some(def_id) = self.def_id.as_local() {
+            self.tcx.struct_span_lint_hir(
+                crate::lint::INVALID_CODEBLOCK_ATTRIBUTES,
+                self.tcx.hir().local_def_id_to_hir_id(def_id),
+                self.sp,
+                msg,
+                |l| l,
+            );
+        }
+    }
+
+    fn error_invalid_codeblock_attr_with_help(&self, msg: &str, help: &str) {
         if let Some(def_id) = self.def_id.as_local() {
             self.tcx.struct_span_lint_hir(
                 crate::lint::INVALID_CODEBLOCK_ATTRIBUTES,
@@ -808,6 +844,8 @@ pub(crate) struct LangString {
     pub(crate) compile_fail: bool,
     pub(crate) error_codes: Vec<String>,
     pub(crate) edition: Option<Edition>,
+    pub(crate) added_classes: Vec<String>,
+    pub(crate) unknown: Vec<String>,
 }
 
 #[derive(Eq, PartialEq, Clone, Debug)]
@@ -817,6 +855,109 @@ pub(crate) enum Ignore {
     Some(Vec<String>),
 }
 
+pub(crate) struct TagIterator<'a, 'tcx> {
+    inner: Peekable<CharIndices<'a>>,
+    data: &'a str,
+    is_in_attribute_block: bool,
+    extra: Option<&'a ExtraInfo<'tcx>>,
+}
+
+#[derive(Debug, PartialEq)]
+pub(crate) enum TokenKind<'a> {
+    Token(&'a str),
+    Attribute(&'a str),
+}
+
+fn is_separator(c: char) -> bool {
+    c == ' ' || c == ',' || c == '\t'
+}
+
+impl<'a, 'tcx> TagIterator<'a, 'tcx> {
+    pub(crate) fn new(data: &'a str, extra: Option<&'a ExtraInfo<'tcx>>) -> Self {
+        Self { inner: data.char_indices().peekable(), data, extra, is_in_attribute_block: false }
+    }
+
+    fn skip_separators(&mut self) -> Option<usize> {
+        while let Some((pos, c)) = self.inner.peek() {
+            if !is_separator(*c) {
+                return Some(*pos);
+            }
+            self.inner.next();
+        }
+        None
+    }
+
+    fn emit_error(&self, err: &str) {
+        if let Some(extra) = self.extra {
+            extra.error_invalid_codeblock_attr(err);
+        }
+    }
+}
+
+impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> {
+    type Item = TokenKind<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let Some(start) = self.skip_separators() else {
+            if self.is_in_attribute_block {
+                self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
+            }
+            return None;
+        };
+        if self.is_in_attribute_block {
+            while let Some((pos, c)) = self.inner.next() {
+                if is_separator(c) {
+                    return Some(TokenKind::Attribute(&self.data[start..pos]));
+                } else if c == '{' {
+                    // There shouldn't be a nested block!
+                    self.emit_error("unexpected `{` inside attribute block (`{}`)");
+                    let attr = &self.data[start..pos];
+                    if attr.is_empty() {
+                        return self.next();
+                    }
+                    self.inner.next();
+                    return Some(TokenKind::Attribute(attr));
+                } else if c == '}' {
+                    self.is_in_attribute_block = false;
+                    let attr = &self.data[start..pos];
+                    if attr.is_empty() {
+                        return self.next();
+                    }
+                    return Some(TokenKind::Attribute(attr));
+                }
+            }
+            // Unclosed attribute block!
+            self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
+            let token = &self.data[start..];
+            if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) }
+        } else {
+            while let Some((pos, c)) = self.inner.next() {
+                if is_separator(c) {
+                    return Some(TokenKind::Token(&self.data[start..pos]));
+                } else if c == '{' {
+                    self.is_in_attribute_block = true;
+                    let token = &self.data[start..pos];
+                    if token.is_empty() {
+                        return self.next();
+                    }
+                    return Some(TokenKind::Token(token));
+                } else if c == '}' {
+                    // We're not in a block so it shouldn't be there!
+                    self.emit_error("unexpected `}` outside attribute block (`{}`)");
+                    let token = &self.data[start..pos];
+                    if token.is_empty() {
+                        return self.next();
+                    }
+                    self.inner.next();
+                    return Some(TokenKind::Attribute(token));
+                }
+            }
+            let token = &self.data[start..];
+            if token.is_empty() { None } else { Some(TokenKind::Token(token)) }
+        }
+    }
+}
+
 impl Default for LangString {
     fn default() -> Self {
         Self {
@@ -829,50 +970,37 @@ impl Default for LangString {
             compile_fail: false,
             error_codes: Vec::new(),
             edition: None,
+            added_classes: Vec::new(),
+            unknown: Vec::new(),
         }
     }
 }
 
+fn handle_class(class: &str, after: &str, data: &mut LangString, extra: Option<&ExtraInfo<'_>>) {
+    if class.is_empty() {
+        if let Some(extra) = extra {
+            extra.error_invalid_codeblock_attr(&format!("missing class name after `{after}`"));
+        }
+    } else {
+        data.added_classes.push(class.to_owned());
+    }
+}
+
 impl LangString {
     fn parse_without_check(
         string: &str,
         allow_error_code_check: ErrorCodes,
         enable_per_target_ignores: bool,
-    ) -> LangString {
+    ) -> Self {
         Self::parse(string, allow_error_code_check, enable_per_target_ignores, None)
     }
 
-    fn tokens(string: &str) -> impl Iterator<Item = &str> {
-        // Pandoc, which Rust once used for generating documentation,
-        // expects lang strings to be surrounded by `{}` and for each token
-        // to be proceeded by a `.`. Since some of these lang strings are still
-        // loose in the wild, we strip a pair of surrounding `{}` from the lang
-        // string and a leading `.` from each token.
-
-        let string = string.trim();
-
-        let first = string.chars().next();
-        let last = string.chars().last();
-
-        let string = if first == Some('{') && last == Some('}') {
-            &string[1..string.len() - 1]
-        } else {
-            string
-        };
-
-        string
-            .split(|c| c == ',' || c == ' ' || c == '\t')
-            .map(str::trim)
-            .map(|token| token.strip_prefix('.').unwrap_or(token))
-            .filter(|token| !token.is_empty())
-    }
-
     fn parse(
         string: &str,
         allow_error_code_check: ErrorCodes,
         enable_per_target_ignores: bool,
         extra: Option<&ExtraInfo<'_>>,
-    ) -> LangString {
+    ) -> Self {
         let allow_error_code_check = allow_error_code_check.as_bool();
         let mut seen_rust_tags = false;
         let mut seen_other_tags = false;
@@ -881,43 +1009,45 @@ impl LangString {
 
         data.original = string.to_owned();
 
-        for token in Self::tokens(string) {
+        for token in TagIterator::new(string, extra) {
             match token {
-                "should_panic" => {
+                TokenKind::Token("should_panic") => {
                     data.should_panic = true;
                     seen_rust_tags = !seen_other_tags;
                 }
-                "no_run" => {
+                TokenKind::Token("no_run") => {
                     data.no_run = true;
                     seen_rust_tags = !seen_other_tags;
                 }
-                "ignore" => {
+                TokenKind::Token("ignore") => {
                     data.ignore = Ignore::All;
                     seen_rust_tags = !seen_other_tags;
                 }
-                x if x.starts_with("ignore-") => {
+                TokenKind::Token(x) if x.starts_with("ignore-") => {
                     if enable_per_target_ignores {
                         ignores.push(x.trim_start_matches("ignore-").to_owned());
                         seen_rust_tags = !seen_other_tags;
                     }
                 }
-                "rust" => {
+                TokenKind::Token("rust") => {
                     data.rust = true;
                     seen_rust_tags = true;
                 }
-                "test_harness" => {
+                TokenKind::Token("test_harness") => {
                     data.test_harness = true;
                     seen_rust_tags = !seen_other_tags || seen_rust_tags;
                 }
-                "compile_fail" => {
+                TokenKind::Token("compile_fail") => {
                     data.compile_fail = true;
                     seen_rust_tags = !seen_other_tags || seen_rust_tags;
                     data.no_run = true;
                 }
-                x if x.starts_with("edition") => {
+                TokenKind::Token(x) if x.starts_with("edition") => {
                     data.edition = x[7..].parse::<Edition>().ok();
                 }
-                x if allow_error_code_check && x.starts_with('E') && x.len() == 5 => {
+                TokenKind::Token(x)
+                    if allow_error_code_check && x.starts_with('E') && x.len() == 5 =>
+                {
                     if x[1..].parse::<u32>().is_ok() {
                         data.error_codes.push(x.to_owned());
                         seen_rust_tags = !seen_other_tags || seen_rust_tags;
@@ -925,7 +1055,7 @@ impl LangString {
                         seen_other_tags = true;
                     }
                 }
-                x if extra.is_some() => {
+                TokenKind::Token(x) if extra.is_some() => {
                     let s = x.to_lowercase();
                     if let Some((flag, help)) = if s == "compile-fail"
                         || s == "compile_fail"
@@ -958,15 +1088,31 @@ impl LangString {
                         None
                     } {
                         if let Some(extra) = extra {
-                            extra.error_invalid_codeblock_attr(
-                                format!("unknown attribute `{x}`. Did you mean `{flag}`?"),
+                            extra.error_invalid_codeblock_attr_with_help(
+                                &format!("unknown attribute `{}`. Did you mean `{}`?", x, flag),
                                 help,
                             );
                         }
                     }
                     seen_other_tags = true;
+                    data.unknown.push(x.to_owned());
+                }
+                TokenKind::Token(x) => {
+                    seen_other_tags = true;
+                    data.unknown.push(x.to_owned());
+                }
+                TokenKind::Attribute(attr) => {
+                    seen_other_tags = true;
+                    if let Some(class) = attr.strip_prefix('.') {
+                        handle_class(class, ".", &mut data, extra);
+                    } else if let Some(class) = attr.strip_prefix("class=") {
+                        handle_class(class, "class=", &mut data, extra);
+                    } else if let Some(extra) = extra {
+                        extra.error_invalid_codeblock_attr(&format!(
+                            "unsupported attribute `{attr}`"
+                        ));
+                    }
                 }
-                _ => seen_other_tags = true,
             }
         }
 
diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index db8504d15c753..2c9c95590acc5 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -117,6 +117,30 @@ fn test_lang_string_parse() {
         edition: Some(Edition::Edition2018),
         ..Default::default()
     });
+    t(LangString {
+        original: "class:test".into(),
+        added_classes: vec!["test".into()],
+        rust: false,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "rust,class:test".into(),
+        added_classes: vec!["test".into()],
+        rust: true,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "class:test:with:colon".into(),
+        added_classes: vec!["test:with:colon".into()],
+        rust: false,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "class:first,class:second".into(),
+        added_classes: vec!["first".into(), "second".into()],
+        rust: false,
+        ..Default::default()
+    });
 }
 
 #[test]
diff --git a/src/librustdoc/passes/check_custom_code_classes.rs b/src/librustdoc/passes/check_custom_code_classes.rs
new file mode 100644
index 0000000000000..246e7f8f3316b
--- /dev/null
+++ b/src/librustdoc/passes/check_custom_code_classes.rs
@@ -0,0 +1,77 @@
+//! NIGHTLY & UNSTABLE CHECK: custom_code_classes_in_docs
+//!
+//! This pass will produce errors when finding custom classes outside of
+//! nightly + relevant feature active.
+
+use super::Pass;
+use crate::clean::{Crate, Item};
+use crate::core::DocContext;
+use crate::fold::DocFolder;
+use crate::html::markdown::{find_codes, ErrorCodes, LangString};
+
+use rustc_session::parse::feature_err;
+use rustc_span::symbol::sym;
+
+pub(crate) const CHECK_CUSTOM_CODE_CLASSES: Pass = Pass {
+    name: "check-custom-code-classes",
+    run: check_custom_code_classes,
+    description: "check for custom code classes without the feature-gate enabled",
+};
+
+pub(crate) fn check_custom_code_classes(krate: Crate, cx: &mut DocContext<'_>) -> Crate {
+    let mut coll = CustomCodeClassLinter { cx };
+
+    coll.fold_crate(krate)
+}
+
+struct CustomCodeClassLinter<'a, 'tcx> {
+    cx: &'a DocContext<'tcx>,
+}
+
+impl<'a, 'tcx> DocFolder for CustomCodeClassLinter<'a, 'tcx> {
+    fn fold_item(&mut self, item: Item) -> Option<Item> {
+        look_for_custom_classes(&self.cx, &item);
+        Some(self.fold_item_recur(item))
+    }
+}
+
+#[derive(Debug)]
+struct TestsWithCustomClasses {
+    custom_classes_found: Vec<String>,
+}
+
+impl crate::doctest::Tester for TestsWithCustomClasses {
+    fn add_test(&mut self, _: String, config: LangString, _: usize) {
+        self.custom_classes_found.extend(config.added_classes.into_iter());
+    }
+}
+
+pub(crate) fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, item: &Item) {
+    if !item.item_id.is_local() {
+        // If non-local, no need to check anything.
+        return;
+    }
+
+    let mut tests = TestsWithCustomClasses { custom_classes_found: vec![] };
+
+    let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
+    find_codes(&dox, &mut tests, ErrorCodes::No, false, None, true);
+
+    if !tests.custom_classes_found.is_empty() && !cx.tcx.features().custom_code_classes_in_docs {
+        feature_err(
+            &cx.tcx.sess.parse_sess,
+            sym::custom_code_classes_in_docs,
+            item.attr_span(cx.tcx),
+            "custom classes in code blocks are unstable",
+        )
+        .note(
+            // This will list the wrong items to make them more easily searchable.
+            // To ensure the most correct hits, it adds back the 'class:' that was stripped.
+            &format!(
+                "found these custom classes: class={}",
+                tests.custom_classes_found.join(",class=")
+            ),
+        )
+        .emit();
+    }
+}
diff --git a/src/librustdoc/passes/mod.rs b/src/librustdoc/passes/mod.rs
index bb678e3388880..4eeaaa2bb70a9 100644
--- a/src/librustdoc/passes/mod.rs
+++ b/src/librustdoc/passes/mod.rs
@@ -35,6 +35,9 @@ pub(crate) use self::calculate_doc_coverage::CALCULATE_DOC_COVERAGE;
 mod lint;
 pub(crate) use self::lint::RUN_LINTS;
 
+mod check_custom_code_classes;
+pub(crate) use self::check_custom_code_classes::CHECK_CUSTOM_CODE_CLASSES;
+
 /// A single pass over the cleaned documentation.
 ///
 /// Runs in the compiler context, so it has access to types and traits and the like.
@@ -66,6 +69,7 @@ pub(crate) enum Condition {
 
 /// The full list of passes.
 pub(crate) const PASSES: &[Pass] = &[
+    CHECK_CUSTOM_CODE_CLASSES,
     CHECK_DOC_TEST_VISIBILITY,
     STRIP_HIDDEN,
     STRIP_PRIVATE,
@@ -79,6 +83,7 @@ pub(crate) const PASSES: &[Pass] = &[
 
 /// The list of passes run by default.
 pub(crate) const DEFAULT_PASSES: &[ConditionalPass] = &[
+    ConditionalPass::always(CHECK_CUSTOM_CODE_CLASSES),
     ConditionalPass::always(COLLECT_TRAIT_IMPLS),
     ConditionalPass::always(CHECK_DOC_TEST_VISIBILITY),
     ConditionalPass::new(STRIP_HIDDEN, WhenNotDocumentHidden),

From f5561842e3dfc9adc8da4ba12b95514da4d99f00 Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Tue, 25 Apr 2023 15:04:46 +0200
Subject: [PATCH 02/13] Add tests for `custom_code_classes_in_docs` feature

---
 src/librustdoc/html/markdown/tests.rs         | 131 ++++++++++++++----
 .../custom_code_classes_in_docs-warning.rs    |  19 +++
 ...custom_code_classes_in_docs-warning.stderr |  65 +++++++++
 .../custom_code_classes_in_docs-warning2.rs   |  13 ++
 ...ustom_code_classes_in_docs-warning2.stderr |  17 +++
 ...eature-gate-custom_code_classes_in_docs.rs |   5 +
 ...re-gate-custom_code_classes_in_docs.stderr |  15 ++
 tests/rustdoc-ui/issues/issue-91713.stdout    |   2 +
 tests/rustdoc/custom_code_classes.rs          |  28 ++++
 9 files changed, 268 insertions(+), 27 deletions(-)
 create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs
 create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr
 create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs
 create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr
 create mode 100644 tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs
 create mode 100644 tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr
 create mode 100644 tests/rustdoc/custom_code_classes.rs

diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index 2c9c95590acc5..dd3d0ebac0cd4 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -1,5 +1,8 @@
 use super::{find_testable_code, plain_text_summary, short_markdown_summary};
-use super::{ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, Markdown, MarkdownItemInfo};
+use super::{
+    ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, Markdown, MarkdownItemInfo, TagIterator,
+    TokenKind,
+};
 use rustc_span::edition::{Edition, DEFAULT_EDITION};
 
 #[test]
@@ -51,10 +54,25 @@ fn test_lang_string_parse() {
 
     t(Default::default());
     t(LangString { original: "rust".into(), ..Default::default() });
-    t(LangString { original: ".rust".into(), ..Default::default() });
-    t(LangString { original: "{rust}".into(), ..Default::default() });
-    t(LangString { original: "{.rust}".into(), ..Default::default() });
-    t(LangString { original: "sh".into(), rust: false, ..Default::default() });
+    t(LangString {
+        original: ".rust".into(),
+        rust: false,
+        unknown: vec![".rust".into()],
+        ..Default::default()
+    });
+    t(LangString { original: "{rust}".into(), rust: false, ..Default::default() });
+    t(LangString {
+        original: "{.rust}".into(),
+        rust: false,
+        added_classes: vec!["rust".into()],
+        ..Default::default()
+    });
+    t(LangString {
+        original: "sh".into(),
+        rust: false,
+        unknown: vec!["sh".into()],
+        ..Default::default()
+    });
     t(LangString { original: "ignore".into(), ignore: Ignore::All, ..Default::default() });
     t(LangString {
         original: "ignore-foo".into(),
@@ -70,41 +88,56 @@ fn test_lang_string_parse() {
         compile_fail: true,
         ..Default::default()
     });
-    t(LangString { original: "no_run,example".into(), no_run: true, ..Default::default() });
+    t(LangString {
+        original: "no_run,example".into(),
+        no_run: true,
+        unknown: vec!["example".into()],
+        ..Default::default()
+    });
     t(LangString {
         original: "sh,should_panic".into(),
         should_panic: true,
         rust: false,
+        unknown: vec!["sh".into()],
+        ..Default::default()
+    });
+    t(LangString {
+        original: "example,rust".into(),
+        unknown: vec!["example".into()],
         ..Default::default()
     });
-    t(LangString { original: "example,rust".into(), ..Default::default() });
     t(LangString {
         original: "test_harness,.rust".into(),
         test_harness: true,
+        unknown: vec![".rust".into()],
         ..Default::default()
     });
     t(LangString {
         original: "text, no_run".into(),
         no_run: true,
         rust: false,
+        unknown: vec!["text".into()],
         ..Default::default()
     });
     t(LangString {
         original: "text,no_run".into(),
         no_run: true,
         rust: false,
+        unknown: vec!["text".into()],
         ..Default::default()
     });
     t(LangString {
         original: "text,no_run, ".into(),
         no_run: true,
         rust: false,
+        unknown: vec!["text".into()],
         ..Default::default()
     });
     t(LangString {
         original: "text,no_run,".into(),
         no_run: true,
         rust: false,
+        unknown: vec!["text".into()],
         ..Default::default()
     });
     t(LangString {
@@ -118,52 +151,96 @@ fn test_lang_string_parse() {
         ..Default::default()
     });
     t(LangString {
-        original: "class:test".into(),
+        original: "{class=test}".into(),
         added_classes: vec!["test".into()],
         rust: false,
         ..Default::default()
     });
     t(LangString {
-        original: "rust,class:test".into(),
+        original: "{.test}".into(),
         added_classes: vec!["test".into()],
+        rust: false,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "rust,{class=test,.test2}".into(),
+        added_classes: vec!["test".into(), "test2".into()],
         rust: true,
         ..Default::default()
     });
     t(LangString {
-        original: "class:test:with:colon".into(),
-        added_classes: vec!["test:with:colon".into()],
+        original: "{class=test:with:colon .test1}".into(),
+        added_classes: vec!["test:with:colon".into(), "test1".into()],
         rust: false,
         ..Default::default()
     });
     t(LangString {
-        original: "class:first,class:second".into(),
+        original: "{class=first,class=second}".into(),
         added_classes: vec!["first".into(), "second".into()],
         rust: false,
         ..Default::default()
     });
+    t(LangString {
+        original: "{class=first,.second},unknown".into(),
+        added_classes: vec!["first".into(), "second".into()],
+        rust: false,
+        unknown: vec!["unknown".into()],
+        ..Default::default()
+    });
+    t(LangString {
+        original: "{class=first .second} unknown".into(),
+        added_classes: vec!["first".into(), "second".into()],
+        rust: false,
+        unknown: vec!["unknown".into()],
+        ..Default::default()
+    });
+    t(LangString {
+        original: "{.first.second}".into(),
+        added_classes: vec!["first.second".into()],
+        rust: false,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "{class=first=second}".into(),
+        added_classes: vec!["first=second".into()],
+        rust: false,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "{class=first.second}".into(),
+        added_classes: vec!["first.second".into()],
+        rust: false,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "{class=.first}".into(),
+        added_classes: vec![".first".into()],
+        rust: false,
+        ..Default::default()
+    });
 }
 
 #[test]
 fn test_lang_string_tokenizer() {
-    fn case(lang_string: &str, want: &[&str]) {
-        let have = LangString::tokens(lang_string).collect::<Vec<&str>>();
+    fn case(lang_string: &str, want: &[TokenKind<'_>]) {
+        let have = TagIterator::new(lang_string, None).collect::<Vec<_>>();
         assert_eq!(have, want, "Unexpected lang string split for `{}`", lang_string);
     }
 
     case("", &[]);
-    case("foo", &["foo"]);
-    case("foo,bar", &["foo", "bar"]);
-    case(".foo,.bar", &["foo", "bar"]);
-    case("{.foo,.bar}", &["foo", "bar"]);
-    case("  {.foo,.bar}  ", &["foo", "bar"]);
-    case("foo bar", &["foo", "bar"]);
-    case("foo\tbar", &["foo", "bar"]);
-    case("foo\t, bar", &["foo", "bar"]);
-    case(" foo , bar ", &["foo", "bar"]);
-    case(",,foo,,bar,,", &["foo", "bar"]);
-    case("foo=bar", &["foo=bar"]);
-    case("a-b-c", &["a-b-c"]);
-    case("a_b_c", &["a_b_c"]);
+    case("foo", &[TokenKind::Token("foo")]);
+    case("foo,bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
+    case(".foo,.bar", &[TokenKind::Token(".foo"), TokenKind::Token(".bar")]);
+    case("{.foo,.bar}", &[TokenKind::Attribute(".foo"), TokenKind::Attribute(".bar")]);
+    case("  {.foo,.bar}  ", &[TokenKind::Attribute(".foo"), TokenKind::Attribute(".bar")]);
+    case("foo bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
+    case("foo\tbar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
+    case("foo\t, bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
+    case(" foo , bar ", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
+    case(",,foo,,bar,,", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
+    case("foo=bar", &[TokenKind::Token("foo=bar")]);
+    case("a-b-c", &[TokenKind::Token("a-b-c")]);
+    case("a_b_c", &[TokenKind::Token("a_b_c")]);
 }
 
 #[test]
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs
new file mode 100644
index 0000000000000..c28921b01f133
--- /dev/null
+++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs
@@ -0,0 +1,19 @@
+// This test ensures that warnings are working as expected for "custom_code_classes_in_docs"
+// feature.
+
+#![feature(custom_code_classes_in_docs)]
+#![deny(warnings)]
+#![feature(no_core)]
+#![no_core]
+
+/// ```{. class= whatever=hehe #id} } {{
+/// main;
+/// ```
+//~^^^ ERROR missing class name after `.`
+//~| ERROR missing class name after `class=`
+//~| ERROR unsupported attribute `whatever=hehe`
+//~| ERROR unsupported attribute `#id`
+//~| ERROR unexpected `}` outside attribute block (`{}`)
+//~| ERROR unclosed attribute block (`{}`): missing `}` at the end
+//~| ERROR unexpected `{` inside attribute block (`{}`)
+pub fn foo() {}
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr
new file mode 100644
index 0000000000000..f19b62914dbfb
--- /dev/null
+++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr
@@ -0,0 +1,65 @@
+error: missing class name after `.`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+   |
+LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+   |
+note: the lint level is defined here
+  --> $DIR/custom_code_classes_in_docs-warning.rs:5:9
+   |
+LL | #![deny(warnings)]
+   |         ^^^^^^^^
+   = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]`
+
+error: missing class name after `class=`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+   |
+LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unsupported attribute `whatever=hehe`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+   |
+LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unsupported attribute `#id`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+   |
+LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unexpected `}` outside attribute block (`{}`)
+  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+   |
+LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unexpected `{` inside attribute block (`{}`)
+  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+   |
+LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unclosed attribute block (`{}`): missing `}` at the end
+  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+   |
+LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: aborting due to 7 previous errors
+
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs
new file mode 100644
index 0000000000000..b2ce7407ec6d8
--- /dev/null
+++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs
@@ -0,0 +1,13 @@
+// This test ensures that warnings are working as expected for "custom_code_classes_in_docs"
+// feature.
+
+#![feature(custom_code_classes_in_docs)]
+#![deny(warnings)]
+#![feature(no_core)]
+#![no_core]
+
+/// ```{class=}
+/// main;
+/// ```
+//~^^^ ERROR missing class name after `class=`
+pub fn foo() {}
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr
new file mode 100644
index 0000000000000..52bb1dae9f61f
--- /dev/null
+++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr
@@ -0,0 +1,17 @@
+error: missing class name after `class=`
+  --> $DIR/custom_code_classes_in_docs-warning2.rs:9:1
+   |
+LL | / /// ```{class=}
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+   |
+note: the lint level is defined here
+  --> $DIR/custom_code_classes_in_docs-warning2.rs:5:9
+   |
+LL | #![deny(warnings)]
+   |         ^^^^^^^^
+   = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]`
+
+error: aborting due to previous error
+
diff --git a/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs b/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs
new file mode 100644
index 0000000000000..8aa13b2d5d10c
--- /dev/null
+++ b/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.rs
@@ -0,0 +1,5 @@
+/// ```{class=language-c}
+/// int main(void) { return 0; }
+/// ```
+//~^^^ ERROR 1:1: 3:8: custom classes in code blocks are unstable [E0658]
+pub struct Bar;
diff --git a/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr b/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr
new file mode 100644
index 0000000000000..c41ebfc807334
--- /dev/null
+++ b/tests/rustdoc-ui/feature-gate-custom_code_classes_in_docs.stderr
@@ -0,0 +1,15 @@
+error[E0658]: custom classes in code blocks are unstable
+  --> $DIR/feature-gate-custom_code_classes_in_docs.rs:1:1
+   |
+LL | / /// ```{class=language-c}
+LL | | /// int main(void) { return 0; }
+LL | | /// ```
+   | |_______^
+   |
+   = note: see issue #79483 <https://github.com/rust-lang/rust/issues/79483> for more information
+   = help: add `#![feature(custom_code_classes_in_docs)]` to the crate attributes to enable
+   = note: found these custom classes: class=language-c
+
+error: aborting due to previous error
+
+For more information about this error, try `rustc --explain E0658`.
diff --git a/tests/rustdoc-ui/issues/issue-91713.stdout b/tests/rustdoc-ui/issues/issue-91713.stdout
index 1678352436315..bbea7e5c212ef 100644
--- a/tests/rustdoc-ui/issues/issue-91713.stdout
+++ b/tests/rustdoc-ui/issues/issue-91713.stdout
@@ -1,4 +1,5 @@
 Available passes for running rustdoc:
+check-custom-code-classes - check for custom code classes without the feature-gate enabled
 check_doc_test_visibility - run various visibility-related lints on doctests
         strip-hidden - strips all `#[doc(hidden)]` items from the output
        strip-private - strips all private items from a crate which cannot be seen externally, implies strip-priv-imports
@@ -10,6 +11,7 @@ calculate-doc-coverage - counts the number of items with and without documentati
            run-lints - runs some of rustdoc's lints
 
 Default passes for rustdoc:
+check-custom-code-classes
  collect-trait-impls
 check_doc_test_visibility
         strip-hidden  (when not --document-hidden-items)
diff --git a/tests/rustdoc/custom_code_classes.rs b/tests/rustdoc/custom_code_classes.rs
new file mode 100644
index 0000000000000..f110721c5a715
--- /dev/null
+++ b/tests/rustdoc/custom_code_classes.rs
@@ -0,0 +1,28 @@
+// Test for `custom_code_classes_in_docs` feature.
+
+#![feature(custom_code_classes_in_docs)]
+#![crate_name = "foo"]
+#![feature(no_core)]
+#![no_core]
+
+// @has 'foo/struct.Bar.html'
+// @has - '//*[@id="main-content"]//pre[@class="language-whatever hoho-c"]' 'main;'
+// @has - '//*[@id="main-content"]//pre[@class="language-whatever2 haha-c"]' 'main;'
+// @has - '//*[@id="main-content"]//pre[@class="language-whatever4 huhu-c"]' 'main;'
+
+/// ```{class=hoho-c},whatever
+/// main;
+/// ```
+///
+/// Testing multiple kinds of orders.
+///
+/// ```whatever2 {class=haha-c}
+/// main;
+/// ```
+///
+/// Testing with multiple "unknown". Only the first should be used.
+///
+/// ```whatever4{.huhu-c} whatever5
+/// main;
+/// ```
+pub struct Bar;

From d829fee6b5859de516dadaaba30db758bb567268 Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Tue, 25 Apr 2023 16:37:43 +0200
Subject: [PATCH 03/13] Add documentation for `custom_code_classes_in_docs`
 feature

---
 src/doc/rustdoc/src/unstable-features.md | 29 ++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md
index f69156b7c05e2..bb62a0bc9cc83 100644
--- a/src/doc/rustdoc/src/unstable-features.md
+++ b/src/doc/rustdoc/src/unstable-features.md
@@ -625,3 +625,32 @@ and check the values of `feature`: `foo` and `bar`.
 
 This flag enables the generation of links in the source code pages which allow the reader
 to jump to a type definition.
+
+### Custom CSS classes for code blocks
+
+```rust
+#![feature(custom_code_classes_in_docs)]
+
+/// ```{class=language-c}
+/// int main(void) { return 0; }
+/// ```
+pub struct Bar;
+```
+
+The text `int main(void) { return 0; }` is rendered without highlighting in a code block
+with the class `language-c`. This can be used to highlight other languages through JavaScript
+libraries for example.
+
+To be noted that you can replace `class=` with `.` to achieve the same result:
+
+```rust
+#![feature(custom_code_classes_in_docs)]
+
+/// ```{.language-c}
+/// int main(void) { return 0; }
+/// ```
+pub struct Bar;
+```
+
+To be noted, `rust` and `.rust`/`class=rust` have different effects: `rust` indicates that this is
+a Rust code block whereas the two others add a "rust" CSS class on the code block.

From 4ce17fa30eeef32235e9b305f56b5651c6d35276 Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Thu, 27 Apr 2023 15:09:43 +0200
Subject: [PATCH 04/13] Add support for double quotes in markdown codeblock
 attributes

---
 src/doc/rustdoc/src/unstable-features.md      |  11 ++
 src/librustdoc/html/markdown.rs               | 121 +++++++++++-------
 src/librustdoc/html/markdown/tests.rs         |  12 ++
 .../custom_code_classes_in_docs-warning3.rs   |  17 +++
 ...ustom_code_classes_in_docs-warning3.stderr |  33 +++++
 5 files changed, 145 insertions(+), 49 deletions(-)
 create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning3.rs
 create mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr

diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md
index bb62a0bc9cc83..b5d8c8194188a 100644
--- a/src/doc/rustdoc/src/unstable-features.md
+++ b/src/doc/rustdoc/src/unstable-features.md
@@ -654,3 +654,14 @@ pub struct Bar;
 
 To be noted, `rust` and `.rust`/`class=rust` have different effects: `rust` indicates that this is
 a Rust code block whereas the two others add a "rust" CSS class on the code block.
+
+You can also use double quotes:
+
+```rust
+#![feature(custom_code_classes_in_docs)]
+
+/// ```"not rust" {."hello everyone"}
+/// int main(void) { return 0; }
+/// ```
+pub struct Bar;
+```
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index a25a6f7d35d10..6bd4e775c0e76 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -892,6 +892,75 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
             extra.error_invalid_codeblock_attr(err);
         }
     }
+
+    /// Returns false if the string is unfinished.
+    fn skip_string(&mut self) -> bool {
+        while let Some((_, c)) = self.inner.next() {
+            if c == '"' {
+                return true;
+            }
+        }
+        self.emit_error("unclosed quote string: missing `\"` at the end");
+        false
+    }
+
+    fn parse_in_attribute_block(&mut self, start: usize) -> Option<TokenKind<'a>> {
+        while let Some((pos, c)) = self.inner.next() {
+            if is_separator(c) {
+                return Some(TokenKind::Attribute(&self.data[start..pos]));
+            } else if c == '{' {
+                // There shouldn't be a nested block!
+                self.emit_error("unexpected `{` inside attribute block (`{}`)");
+                let attr = &self.data[start..pos];
+                if attr.is_empty() {
+                    return self.next();
+                }
+                self.inner.next();
+                return Some(TokenKind::Attribute(attr));
+            } else if c == '}' {
+                self.is_in_attribute_block = false;
+                let attr = &self.data[start..pos];
+                if attr.is_empty() {
+                    return self.next();
+                }
+                return Some(TokenKind::Attribute(attr));
+            } else if c == '"' && !self.skip_string() {
+                return None;
+            }
+        }
+        // Unclosed attribute block!
+        self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
+        let token = &self.data[start..];
+        if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) }
+    }
+
+    fn parse_outside_attribute_block(&mut self, start: usize) -> Option<TokenKind<'a>> {
+        while let Some((pos, c)) = self.inner.next() {
+            if is_separator(c) {
+                return Some(TokenKind::Token(&self.data[start..pos]));
+            } else if c == '{' {
+                self.is_in_attribute_block = true;
+                let token = &self.data[start..pos];
+                if token.is_empty() {
+                    return self.next();
+                }
+                return Some(TokenKind::Token(token));
+            } else if c == '}' {
+                // We're not in a block so it shouldn't be there!
+                self.emit_error("unexpected `}` outside attribute block (`{}`)");
+                let token = &self.data[start..pos];
+                if token.is_empty() {
+                    return self.next();
+                }
+                self.inner.next();
+                return Some(TokenKind::Attribute(token));
+            } else if c == '"' && !self.skip_string() {
+                return None;
+            }
+        }
+        let token = &self.data[start..];
+        if token.is_empty() { None } else { Some(TokenKind::Token(token)) }
+    }
 }
 
 impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> {
@@ -905,55 +974,9 @@ impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> {
             return None;
         };
         if self.is_in_attribute_block {
-            while let Some((pos, c)) = self.inner.next() {
-                if is_separator(c) {
-                    return Some(TokenKind::Attribute(&self.data[start..pos]));
-                } else if c == '{' {
-                    // There shouldn't be a nested block!
-                    self.emit_error("unexpected `{` inside attribute block (`{}`)");
-                    let attr = &self.data[start..pos];
-                    if attr.is_empty() {
-                        return self.next();
-                    }
-                    self.inner.next();
-                    return Some(TokenKind::Attribute(attr));
-                } else if c == '}' {
-                    self.is_in_attribute_block = false;
-                    let attr = &self.data[start..pos];
-                    if attr.is_empty() {
-                        return self.next();
-                    }
-                    return Some(TokenKind::Attribute(attr));
-                }
-            }
-            // Unclosed attribute block!
-            self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
-            let token = &self.data[start..];
-            if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) }
+            self.parse_in_attribute_block(start)
         } else {
-            while let Some((pos, c)) = self.inner.next() {
-                if is_separator(c) {
-                    return Some(TokenKind::Token(&self.data[start..pos]));
-                } else if c == '{' {
-                    self.is_in_attribute_block = true;
-                    let token = &self.data[start..pos];
-                    if token.is_empty() {
-                        return self.next();
-                    }
-                    return Some(TokenKind::Token(token));
-                } else if c == '}' {
-                    // We're not in a block so it shouldn't be there!
-                    self.emit_error("unexpected `}` outside attribute block (`{}`)");
-                    let token = &self.data[start..pos];
-                    if token.is_empty() {
-                        return self.next();
-                    }
-                    self.inner.next();
-                    return Some(TokenKind::Attribute(token));
-                }
-            }
-            let token = &self.data[start..];
-            if token.is_empty() { None } else { Some(TokenKind::Token(token)) }
+            self.parse_outside_attribute_block(start)
         }
     }
 }
@@ -982,7 +1005,7 @@ fn handle_class(class: &str, after: &str, data: &mut LangString, extra: Option<&
             extra.error_invalid_codeblock_attr(&format!("missing class name after `{after}`"));
         }
     } else {
-        data.added_classes.push(class.to_owned());
+        data.added_classes.push(class.replace('"', ""));
     }
 }
 
diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index dd3d0ebac0cd4..b0b4de65cca40 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -218,6 +218,18 @@ fn test_lang_string_parse() {
         rust: false,
         ..Default::default()
     });
+    t(LangString {
+        original: r#"{class="first"}"#.into(),
+        added_classes: vec!["first".into()],
+        rust: false,
+        ..Default::default()
+    });
+    t(LangString {
+        original: r#"{class=f"irst"}"#.into(),
+        added_classes: vec!["first".into()],
+        rust: false,
+        ..Default::default()
+    });
 }
 
 #[test]
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.rs
new file mode 100644
index 0000000000000..57d9038cb0ce1
--- /dev/null
+++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.rs
@@ -0,0 +1,17 @@
+// This test ensures that warnings are working as expected for "custom_code_classes_in_docs"
+// feature.
+
+#![feature(custom_code_classes_in_docs)]
+#![deny(warnings)]
+#![feature(no_core)]
+#![no_core]
+
+/// ```{class="}
+/// main;
+/// ```
+//~^^^ ERROR unclosed quote string
+//~| ERROR unclosed quote string
+/// ```"
+/// main;
+/// ```
+pub fn foo() {}
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr
new file mode 100644
index 0000000000000..7432af19360bc
--- /dev/null
+++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr
@@ -0,0 +1,33 @@
+error: unclosed quote string: missing `"` at the end
+  --> $DIR/custom_code_classes_in_docs-warning3.rs:9:1
+   |
+LL | / /// ```{class="}
+LL | | /// main;
+LL | | /// ```
+LL | |
+...  |
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+   |
+note: the lint level is defined here
+  --> $DIR/custom_code_classes_in_docs-warning3.rs:5:9
+   |
+LL | #![deny(warnings)]
+   |         ^^^^^^^^
+   = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]`
+
+error: unclosed quote string: missing `"` at the end
+  --> $DIR/custom_code_classes_in_docs-warning3.rs:9:1
+   |
+LL | / /// ```{class="}
+LL | | /// main;
+LL | | /// ```
+LL | |
+...  |
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: aborting due to 2 previous errors
+

From 7681f63cab88109dd8d445cf7b49cbbd63f81e75 Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Tue, 2 May 2023 22:01:28 +0200
Subject: [PATCH 05/13] Implement new eBNF for codeblock attributes

---
 src/librustdoc/html/markdown.rs               | 272 ++++++++++++------
 src/librustdoc/html/markdown/tests.rs         |  89 +++---
 .../custom_code_classes_in_docs-warning.rs    |  82 +++++-
 ...custom_code_classes_in_docs-warning.stderr |  90 ++++--
 .../custom_code_classes_in_docs-warning2.rs   |  13 -
 ...ustom_code_classes_in_docs-warning2.stderr |  17 --
 ...ustom_code_classes_in_docs-warning3.stderr |   4 +-
 tests/rustdoc/custom_code_classes.rs          |   2 +-
 8 files changed, 373 insertions(+), 196 deletions(-)
 delete mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs
 delete mode 100644 tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr

diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 6bd4e775c0e76..0a741f7815b89 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -862,19 +862,34 @@ pub(crate) struct TagIterator<'a, 'tcx> {
     extra: Option<&'a ExtraInfo<'tcx>>,
 }
 
-#[derive(Debug, PartialEq)]
-pub(crate) enum TokenKind<'a> {
-    Token(&'a str),
-    Attribute(&'a str),
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub(crate) enum LangStringToken<'a> {
+    LangToken(&'a str),
+    ClassAttribute(&'a str),
+    KeyValueAttribute(&'a str, &'a str),
 }
 
+fn is_bareword_char(c: char) -> bool {
+    c == '_' || c == '-' || c == ':' || c.is_ascii_alphabetic() || c.is_ascii_digit()
+}
 fn is_separator(c: char) -> bool {
     c == ' ' || c == ',' || c == '\t'
 }
 
+struct Indices {
+    start: usize,
+    end: usize,
+}
+
 impl<'a, 'tcx> TagIterator<'a, 'tcx> {
     pub(crate) fn new(data: &'a str, extra: Option<&'a ExtraInfo<'tcx>>) -> Self {
-        Self { inner: data.char_indices().peekable(), data, extra, is_in_attribute_block: false }
+        Self { inner: data.char_indices().peekable(), data, is_in_attribute_block: false, extra }
+    }
+
+    fn emit_error(&self, err: &str) {
+        if let Some(extra) = self.extra {
+            extra.error_invalid_codeblock_attr(err);
+        }
     }
 
     fn skip_separators(&mut self) -> Option<usize> {
@@ -887,84 +902,183 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
         None
     }
 
-    fn emit_error(&self, err: &str) {
-        if let Some(extra) = self.extra {
-            extra.error_invalid_codeblock_attr(err);
+    fn parse_string(&mut self, start: usize) -> Option<Indices> {
+        while let Some((pos, c)) = self.inner.next() {
+            if c == '"' {
+                return Some(Indices { start: start + 1, end: pos });
+            }
         }
+        self.emit_error("unclosed quote string `\"`");
+        None
     }
 
-    /// Returns false if the string is unfinished.
-    fn skip_string(&mut self) -> bool {
-        while let Some((_, c)) = self.inner.next() {
-            if c == '"' {
-                return true;
+    fn parse_class(&mut self, start: usize) -> Option<LangStringToken<'a>> {
+        while let Some((pos, c)) = self.inner.peek().copied() {
+            if is_bareword_char(c) {
+                self.inner.next();
+            } else {
+                let class = &self.data[start + 1..pos];
+                if class.is_empty() {
+                    self.emit_error(&format!("unexpected `{c}` character after `.`"));
+                    return None;
+                } else if self.check_after_token() {
+                    return Some(LangStringToken::ClassAttribute(class));
+                } else {
+                    return None;
+                }
             }
         }
-        self.emit_error("unclosed quote string: missing `\"` at the end");
-        false
+        let class = &self.data[start + 1..];
+        if class.is_empty() {
+            self.emit_error("missing character after `.`");
+            None
+        } else if self.check_after_token() {
+            Some(LangStringToken::ClassAttribute(class))
+        } else {
+            None
+        }
+    }
+
+    fn parse_token(&mut self, start: usize) -> Option<Indices> {
+        while let Some((pos, c)) = self.inner.peek() {
+            if !is_bareword_char(*c) {
+                return Some(Indices { start, end: *pos });
+            }
+            self.inner.next();
+        }
+        self.emit_error("unexpected end");
+        None
+    }
+
+    fn parse_key_value(&mut self, c: char, start: usize) -> Option<LangStringToken<'a>> {
+        let key_indices =
+            if c == '"' { self.parse_string(start)? } else { self.parse_token(start)? };
+        if key_indices.start == key_indices.end {
+            self.emit_error("unexpected empty string as key");
+            return None;
+        }
+
+        if let Some((_, c)) = self.inner.next() {
+            if c != '=' {
+                self.emit_error(&format!("expected `=`, found `{}`", c));
+                return None;
+            }
+        } else {
+            self.emit_error("unexpected end");
+            return None;
+        }
+        let value_indices = match self.inner.next() {
+            Some((pos, '"')) => self.parse_string(pos)?,
+            Some((pos, c)) if is_bareword_char(c) => self.parse_token(pos)?,
+            Some((_, c)) => {
+                self.emit_error(&format!("unexpected `{c}` character after `=`"));
+                return None;
+            }
+            None => {
+                self.emit_error("expected value after `=`");
+                return None;
+            }
+        };
+        if value_indices.start == value_indices.end {
+            self.emit_error("unexpected empty string as value");
+            None
+        } else if self.check_after_token() {
+            Some(LangStringToken::KeyValueAttribute(
+                &self.data[key_indices.start..key_indices.end],
+                &self.data[value_indices.start..value_indices.end],
+            ))
+        } else {
+            None
+        }
     }
 
-    fn parse_in_attribute_block(&mut self, start: usize) -> Option<TokenKind<'a>> {
+    /// Returns `false` if an error was emitted.
+    fn check_after_token(&mut self) -> bool {
+        if let Some((_, c)) = self.inner.peek().copied() {
+            if c == '}' || is_separator(c) || c == '(' {
+                true
+            } else {
+                self.emit_error(&format!("unexpected `{c}` character"));
+                false
+            }
+        } else {
+            // The error will be caught on the next iteration.
+            true
+        }
+    }
+
+    fn parse_in_attribute_block(&mut self) -> Option<LangStringToken<'a>> {
         while let Some((pos, c)) = self.inner.next() {
-            if is_separator(c) {
-                return Some(TokenKind::Attribute(&self.data[start..pos]));
-            } else if c == '{' {
-                // There shouldn't be a nested block!
-                self.emit_error("unexpected `{` inside attribute block (`{}`)");
-                let attr = &self.data[start..pos];
-                if attr.is_empty() {
-                    return self.next();
-                }
-                self.inner.next();
-                return Some(TokenKind::Attribute(attr));
-            } else if c == '}' {
+            if c == '}' {
                 self.is_in_attribute_block = false;
-                let attr = &self.data[start..pos];
-                if attr.is_empty() {
-                    return self.next();
-                }
-                return Some(TokenKind::Attribute(attr));
-            } else if c == '"' && !self.skip_string() {
+                return self.next();
+            } else if c == '.' {
+                return self.parse_class(pos);
+            } else if c == '"' || is_bareword_char(c) {
+                return self.parse_key_value(c, pos);
+            } else {
+                self.emit_error(&format!("unexpected character `{c}`"));
                 return None;
             }
         }
-        // Unclosed attribute block!
         self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
-        let token = &self.data[start..];
-        if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) }
+        None
     }
 
-    fn parse_outside_attribute_block(&mut self, start: usize) -> Option<TokenKind<'a>> {
+    /// Returns `false` if an error was emitted.
+    fn skip_paren_block(&mut self) -> bool {
+        while let Some((_, c)) = self.inner.next() {
+            if c == ')' {
+                return true;
+            }
+        }
+        self.emit_error("unclosed comment: missing `)` at the end");
+        false
+    }
+
+    fn parse_outside_attribute_block(&mut self, start: usize) -> Option<LangStringToken<'a>> {
         while let Some((pos, c)) = self.inner.next() {
-            if is_separator(c) {
-                return Some(TokenKind::Token(&self.data[start..pos]));
+            if c == '"' {
+                if pos != start {
+                    self.emit_error("expected ` `, `{` or `,` found `\"`");
+                    return None;
+                }
+                let indices = self.parse_string(pos)?;
+                if let Some((_, c)) = self.inner.peek().copied() && c != '{' && !is_separator(c) && c != '(' {
+                    self.emit_error(&format!("expected ` `, `{{` or `,` after `\"`, found `{c}`"));
+                    return None;
+                }
+                return Some(LangStringToken::LangToken(&self.data[indices.start..indices.end]));
             } else if c == '{' {
                 self.is_in_attribute_block = true;
-                let token = &self.data[start..pos];
-                if token.is_empty() {
-                    return self.next();
+                return self.next();
+            } else if is_bareword_char(c) {
+                continue;
+            } else if is_separator(c) {
+                if pos != start {
+                    return Some(LangStringToken::LangToken(&self.data[start..pos]));
                 }
-                return Some(TokenKind::Token(token));
-            } else if c == '}' {
-                // We're not in a block so it shouldn't be there!
-                self.emit_error("unexpected `}` outside attribute block (`{}`)");
-                let token = &self.data[start..pos];
-                if token.is_empty() {
-                    return self.next();
+                return self.next();
+            } else if c == '(' {
+                if !self.skip_paren_block() {
+                    return None;
                 }
-                self.inner.next();
-                return Some(TokenKind::Attribute(token));
-            } else if c == '"' && !self.skip_string() {
+                if pos != start {
+                    return Some(LangStringToken::LangToken(&self.data[start..pos]));
+                }
+                return self.next();
+            } else {
+                self.emit_error(&format!("unexpected character `{c}`"));
                 return None;
             }
         }
         let token = &self.data[start..];
-        if token.is_empty() { None } else { Some(TokenKind::Token(token)) }
+        if token.is_empty() { None } else { Some(LangStringToken::LangToken(&self.data[start..])) }
     }
 }
 
 impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> {
-    type Item = TokenKind<'a>;
+    type Item = LangStringToken<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
         let Some(start) = self.skip_separators() else {
@@ -974,7 +1088,7 @@ impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> {
             return None;
         };
         if self.is_in_attribute_block {
-            self.parse_in_attribute_block(start)
+            self.parse_in_attribute_block()
         } else {
             self.parse_outside_attribute_block(start)
         }
@@ -999,16 +1113,6 @@ impl Default for LangString {
     }
 }
 
-fn handle_class(class: &str, after: &str, data: &mut LangString, extra: Option<&ExtraInfo<'_>>) {
-    if class.is_empty() {
-        if let Some(extra) = extra {
-            extra.error_invalid_codeblock_attr(&format!("missing class name after `{after}`"));
-        }
-    } else {
-        data.added_classes.push(class.replace('"', ""));
-    }
-}
-
 impl LangString {
     fn parse_without_check(
         string: &str,
@@ -1034,41 +1138,41 @@ impl LangString {
 
         for token in TagIterator::new(string, extra) {
             match token {
-                TokenKind::Token("should_panic") => {
+                LangStringToken::LangToken("should_panic") => {
                     data.should_panic = true;
                     seen_rust_tags = !seen_other_tags;
                 }
-                TokenKind::Token("no_run") => {
+                LangStringToken::LangToken("no_run") => {
                     data.no_run = true;
                     seen_rust_tags = !seen_other_tags;
                 }
-                TokenKind::Token("ignore") => {
+                LangStringToken::LangToken("ignore") => {
                     data.ignore = Ignore::All;
                     seen_rust_tags = !seen_other_tags;
                 }
-                TokenKind::Token(x) if x.starts_with("ignore-") => {
+                LangStringToken::LangToken(x) if x.starts_with("ignore-") => {
                     if enable_per_target_ignores {
                         ignores.push(x.trim_start_matches("ignore-").to_owned());
                         seen_rust_tags = !seen_other_tags;
                     }
                 }
-                TokenKind::Token("rust") => {
+                LangStringToken::LangToken("rust") => {
                     data.rust = true;
                     seen_rust_tags = true;
                 }
-                TokenKind::Token("test_harness") => {
+                LangStringToken::LangToken("test_harness") => {
                     data.test_harness = true;
                     seen_rust_tags = !seen_other_tags || seen_rust_tags;
                 }
-                TokenKind::Token("compile_fail") => {
+                LangStringToken::LangToken("compile_fail") => {
                     data.compile_fail = true;
                     seen_rust_tags = !seen_other_tags || seen_rust_tags;
                     data.no_run = true;
                 }
-                TokenKind::Token(x) if x.starts_with("edition") => {
+                LangStringToken::LangToken(x) if x.starts_with("edition") => {
                     data.edition = x[7..].parse::<Edition>().ok();
                 }
-                TokenKind::Token(x)
+                LangStringToken::LangToken(x)
                     if allow_error_code_check && x.starts_with('E') && x.len() == 5 =>
                 {
                     if x[1..].parse::<u32>().is_ok() {
@@ -1078,7 +1182,7 @@ impl LangString {
                         seen_other_tags = true;
                     }
                 }
-                TokenKind::Token(x) if extra.is_some() => {
+                LangStringToken::LangToken(x) if extra.is_some() => {
                     let s = x.to_lowercase();
                     if let Some((flag, help)) = if s == "compile-fail"
                         || s == "compile_fail"
@@ -1120,22 +1224,24 @@ impl LangString {
                     seen_other_tags = true;
                     data.unknown.push(x.to_owned());
                 }
-                TokenKind::Token(x) => {
+                LangStringToken::LangToken(x) => {
                     seen_other_tags = true;
                     data.unknown.push(x.to_owned());
                 }
-                TokenKind::Attribute(attr) => {
+                LangStringToken::KeyValueAttribute(key, value) => {
                     seen_other_tags = true;
-                    if let Some(class) = attr.strip_prefix('.') {
-                        handle_class(class, ".", &mut data, extra);
-                    } else if let Some(class) = attr.strip_prefix("class=") {
-                        handle_class(class, "class=", &mut data, extra);
+                    if key == "class" {
+                        data.added_classes.push(value.to_owned());
                     } else if let Some(extra) = extra {
                         extra.error_invalid_codeblock_attr(&format!(
-                            "unsupported attribute `{attr}`"
+                            "unsupported attribute `{key}`"
                         ));
                     }
                 }
+                LangStringToken::ClassAttribute(class) => {
+                    seen_other_tags = true;
+                    data.added_classes.push(class.to_owned());
+                }
             }
         }
 
diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index b0b4de65cca40..35b243d3d292e 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -1,7 +1,7 @@
 use super::{find_testable_code, plain_text_summary, short_markdown_summary};
 use super::{
-    ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, Markdown, MarkdownItemInfo, TagIterator,
-    TokenKind,
+    ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, LangStringToken, Markdown,
+    MarkdownItemInfo, TagIterator,
 };
 use rustc_span::edition::{Edition, DEFAULT_EDITION};
 
@@ -55,12 +55,13 @@ fn test_lang_string_parse() {
     t(Default::default());
     t(LangString { original: "rust".into(), ..Default::default() });
     t(LangString {
-        original: ".rust".into(),
+        original: "rusta".into(),
         rust: false,
-        unknown: vec![".rust".into()],
+        unknown: vec!["rusta".into()],
         ..Default::default()
     });
-    t(LangString { original: "{rust}".into(), rust: false, ..Default::default() });
+    // error
+    t(LangString { original: "{rust}".into(), rust: true, ..Default::default() });
     t(LangString {
         original: "{.rust}".into(),
         rust: false,
@@ -107,9 +108,9 @@ fn test_lang_string_parse() {
         ..Default::default()
     });
     t(LangString {
-        original: "test_harness,.rust".into(),
+        original: "test_harness,rusta".into(),
         test_harness: true,
-        unknown: vec![".rust".into()],
+        unknown: vec!["rusta".into()],
         ..Default::default()
     });
     t(LangString {
@@ -194,65 +195,51 @@ fn test_lang_string_parse() {
         unknown: vec!["unknown".into()],
         ..Default::default()
     });
-    t(LangString {
-        original: "{.first.second}".into(),
-        added_classes: vec!["first.second".into()],
-        rust: false,
-        ..Default::default()
-    });
-    t(LangString {
-        original: "{class=first=second}".into(),
-        added_classes: vec!["first=second".into()],
-        rust: false,
-        ..Default::default()
-    });
-    t(LangString {
-        original: "{class=first.second}".into(),
-        added_classes: vec!["first.second".into()],
-        rust: false,
-        ..Default::default()
-    });
-    t(LangString {
-        original: "{class=.first}".into(),
-        added_classes: vec![".first".into()],
-        rust: false,
-        ..Default::default()
-    });
+    // error
+    t(LangString { original: "{.first.second}".into(), rust: true, ..Default::default() });
+    // error
+    t(LangString { original: "{class=first=second}".into(), rust: true, ..Default::default() });
+    // error
+    t(LangString { original: "{class=first.second}".into(), rust: true, ..Default::default() });
+    // error
+    t(LangString { original: "{class=.first}".into(), rust: true, ..Default::default() });
     t(LangString {
         original: r#"{class="first"}"#.into(),
         added_classes: vec!["first".into()],
         rust: false,
         ..Default::default()
     });
-    t(LangString {
-        original: r#"{class=f"irst"}"#.into(),
-        added_classes: vec!["first".into()],
-        rust: false,
-        ..Default::default()
-    });
+    // error
+    t(LangString { original: r#"{class=f"irst"}"#.into(), rust: true, ..Default::default() });
 }
 
 #[test]
 fn test_lang_string_tokenizer() {
-    fn case(lang_string: &str, want: &[TokenKind<'_>]) {
+    fn case(lang_string: &str, want: &[LangStringToken<'_>]) {
         let have = TagIterator::new(lang_string, None).collect::<Vec<_>>();
         assert_eq!(have, want, "Unexpected lang string split for `{}`", lang_string);
     }
 
     case("", &[]);
-    case("foo", &[TokenKind::Token("foo")]);
-    case("foo,bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
-    case(".foo,.bar", &[TokenKind::Token(".foo"), TokenKind::Token(".bar")]);
-    case("{.foo,.bar}", &[TokenKind::Attribute(".foo"), TokenKind::Attribute(".bar")]);
-    case("  {.foo,.bar}  ", &[TokenKind::Attribute(".foo"), TokenKind::Attribute(".bar")]);
-    case("foo bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
-    case("foo\tbar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
-    case("foo\t, bar", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
-    case(" foo , bar ", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
-    case(",,foo,,bar,,", &[TokenKind::Token("foo"), TokenKind::Token("bar")]);
-    case("foo=bar", &[TokenKind::Token("foo=bar")]);
-    case("a-b-c", &[TokenKind::Token("a-b-c")]);
-    case("a_b_c", &[TokenKind::Token("a_b_c")]);
+    case("foo", &[LangStringToken::LangToken("foo")]);
+    case("foo,bar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
+    case(".foo,.bar", &[]);
+    case(
+        "{.foo,.bar}",
+        &[LangStringToken::ClassAttribute("foo"), LangStringToken::ClassAttribute("bar")],
+    );
+    case(
+        "  {.foo,.bar}  ",
+        &[LangStringToken::ClassAttribute("foo"), LangStringToken::ClassAttribute("bar")],
+    );
+    case("foo bar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
+    case("foo\tbar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
+    case("foo\t, bar", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
+    case(" foo , bar ", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
+    case(",,foo,,bar,,", &[LangStringToken::LangToken("foo"), LangStringToken::LangToken("bar")]);
+    case("foo=bar", &[]);
+    case("a-b-c", &[LangStringToken::LangToken("a-b-c")]);
+    case("a_b_c", &[LangStringToken::LangToken("a_b_c")]);
 }
 
 #[test]
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs
index c28921b01f133..dd8759b7e378a 100644
--- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs
+++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.rs
@@ -6,14 +6,80 @@
 #![feature(no_core)]
 #![no_core]
 
-/// ```{. class= whatever=hehe #id} } {{
+/// ```{. }
 /// main;
 /// ```
-//~^^^ ERROR missing class name after `.`
-//~| ERROR missing class name after `class=`
-//~| ERROR unsupported attribute `whatever=hehe`
-//~| ERROR unsupported attribute `#id`
-//~| ERROR unexpected `}` outside attribute block (`{}`)
-//~| ERROR unclosed attribute block (`{}`): missing `}` at the end
-//~| ERROR unexpected `{` inside attribute block (`{}`)
+//~^^^ ERROR unexpected ` ` character after `.`
 pub fn foo() {}
+
+/// ```{class= a}
+/// main;
+/// ```
+//~^^^ ERROR unexpected ` ` character after `=`
+pub fn foo2() {}
+
+/// ```{#id}
+/// main;
+/// ```
+//~^^^ ERROR unexpected character `#`
+pub fn foo3() {}
+
+/// ```{{
+/// main;
+/// ```
+//~^^^ ERROR unexpected character `{`
+pub fn foo4() {}
+
+/// ```}
+/// main;
+/// ```
+//~^^^ ERROR unexpected character `}`
+pub fn foo5() {}
+
+/// ```)
+/// main;
+/// ```
+//~^^^ ERROR unexpected character `)`
+pub fn foo6() {}
+
+/// ```{class=}
+/// main;
+/// ```
+//~^^^ ERROR unexpected `}` character after `=`
+pub fn foo7() {}
+
+/// ```(
+/// main;
+/// ```
+//~^^^ ERROR unclosed comment: missing `)` at the end
+pub fn foo8() {}
+
+/// ```{class=one=two}
+/// main;
+/// ```
+//~^^^ ERROR unexpected `=`
+pub fn foo9() {}
+
+/// ```{.one.two}
+/// main;
+/// ```
+//~^^^ ERROR unexpected `.` character
+pub fn foo10() {}
+
+/// ```{class=.one}
+/// main;
+/// ```
+//~^^^ ERROR unexpected `.` character after `=`
+pub fn foo11() {}
+
+/// ```{class=one.two}
+/// main;
+/// ```
+//~^^^ ERROR unexpected `.` character
+pub fn foo12() {}
+
+/// ```{(comment)}
+/// main;
+/// ```
+//~^^^ ERROR unexpected character `(`
+pub fn foo13() {}
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr
index f19b62914dbfb..3e0dc4b2f7a6b 100644
--- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr
+++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning.stderr
@@ -1,7 +1,7 @@
-error: missing class name after `.`
+error: unexpected ` ` character after `.`
   --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
    |
-LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | / /// ```{. }
 LL | | /// main;
 LL | | /// ```
    | |_______^
@@ -13,53 +13,101 @@ LL | #![deny(warnings)]
    |         ^^^^^^^^
    = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]`
 
-error: missing class name after `class=`
-  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+error: unexpected ` ` character after `=`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:15:1
    |
-LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | / /// ```{class= a}
 LL | | /// main;
 LL | | /// ```
    | |_______^
 
-error: unsupported attribute `whatever=hehe`
-  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+error: unexpected character `#`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:21:1
    |
-LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | / /// ```{#id}
 LL | | /// main;
 LL | | /// ```
    | |_______^
 
-error: unsupported attribute `#id`
-  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+error: unexpected character `{`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:27:1
    |
-LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | / /// ```{{
 LL | | /// main;
 LL | | /// ```
    | |_______^
 
-error: unexpected `}` outside attribute block (`{}`)
-  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+error: unexpected character `}`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:33:1
    |
-LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | / /// ```}
 LL | | /// main;
 LL | | /// ```
    | |_______^
 
-error: unexpected `{` inside attribute block (`{}`)
-  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+error: unexpected character `)`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:39:1
    |
-LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | / /// ```)
 LL | | /// main;
 LL | | /// ```
    | |_______^
 
-error: unclosed attribute block (`{}`): missing `}` at the end
-  --> $DIR/custom_code_classes_in_docs-warning.rs:9:1
+error: unexpected `}` character after `=`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:45:1
+   |
+LL | / /// ```{class=}
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unclosed comment: missing `)` at the end
+  --> $DIR/custom_code_classes_in_docs-warning.rs:51:1
+   |
+LL | / /// ```(
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unexpected `=` character
+  --> $DIR/custom_code_classes_in_docs-warning.rs:57:1
+   |
+LL | / /// ```{class=one=two}
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unexpected `.` character
+  --> $DIR/custom_code_classes_in_docs-warning.rs:63:1
+   |
+LL | / /// ```{.one.two}
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unexpected `.` character after `=`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:69:1
+   |
+LL | / /// ```{class=.one}
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unexpected `.` character
+  --> $DIR/custom_code_classes_in_docs-warning.rs:75:1
+   |
+LL | / /// ```{class=one.two}
+LL | | /// main;
+LL | | /// ```
+   | |_______^
+
+error: unexpected character `(`
+  --> $DIR/custom_code_classes_in_docs-warning.rs:81:1
    |
-LL | / /// ```{. class= whatever=hehe #id} } {{
+LL | / /// ```{(comment)}
 LL | | /// main;
 LL | | /// ```
    | |_______^
 
-error: aborting due to 7 previous errors
+error: aborting due to 13 previous errors
 
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs
deleted file mode 100644
index b2ce7407ec6d8..0000000000000
--- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.rs
+++ /dev/null
@@ -1,13 +0,0 @@
-// This test ensures that warnings are working as expected for "custom_code_classes_in_docs"
-// feature.
-
-#![feature(custom_code_classes_in_docs)]
-#![deny(warnings)]
-#![feature(no_core)]
-#![no_core]
-
-/// ```{class=}
-/// main;
-/// ```
-//~^^^ ERROR missing class name after `class=`
-pub fn foo() {}
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr
deleted file mode 100644
index 52bb1dae9f61f..0000000000000
--- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning2.stderr
+++ /dev/null
@@ -1,17 +0,0 @@
-error: missing class name after `class=`
-  --> $DIR/custom_code_classes_in_docs-warning2.rs:9:1
-   |
-LL | / /// ```{class=}
-LL | | /// main;
-LL | | /// ```
-   | |_______^
-   |
-note: the lint level is defined here
-  --> $DIR/custom_code_classes_in_docs-warning2.rs:5:9
-   |
-LL | #![deny(warnings)]
-   |         ^^^^^^^^
-   = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]`
-
-error: aborting due to previous error
-
diff --git a/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr
index 7432af19360bc..4f2ded78c29fa 100644
--- a/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr
+++ b/tests/rustdoc-ui/custom_code_classes_in_docs-warning3.stderr
@@ -1,4 +1,4 @@
-error: unclosed quote string: missing `"` at the end
+error: unclosed quote string `"`
   --> $DIR/custom_code_classes_in_docs-warning3.rs:9:1
    |
 LL | / /// ```{class="}
@@ -17,7 +17,7 @@ LL | #![deny(warnings)]
    |         ^^^^^^^^
    = note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]`
 
-error: unclosed quote string: missing `"` at the end
+error: unclosed quote string `"`
   --> $DIR/custom_code_classes_in_docs-warning3.rs:9:1
    |
 LL | / /// ```{class="}
diff --git a/tests/rustdoc/custom_code_classes.rs b/tests/rustdoc/custom_code_classes.rs
index f110721c5a715..cd20d8b7d6c9e 100644
--- a/tests/rustdoc/custom_code_classes.rs
+++ b/tests/rustdoc/custom_code_classes.rs
@@ -22,7 +22,7 @@
 ///
 /// Testing with multiple "unknown". Only the first should be used.
 ///
-/// ```whatever4{.huhu-c} whatever5
+/// ```whatever4,{.huhu-c} whatever5
 /// main;
 /// ```
 pub struct Bar;

From 5f3002ebeb4afd1dff8bc2dff24c658ac19ee1fd Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Tue, 2 May 2023 22:04:15 +0200
Subject: [PATCH 06/13] Add eBNF and documentation on TagIterator

---
 src/librustdoc/html/markdown.rs | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 0a741f7815b89..482e7b7f260f2 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -855,6 +855,36 @@ pub(crate) enum Ignore {
     Some(Vec<String>),
 }
 
+/// This is the parser for fenced codeblocks attributes. It implements the following eBNF:
+///
+/// ```eBNF
+/// lang-string = *(token-list / delimited-attribute-list / comment)
+///
+/// bareword = CHAR *(CHAR)
+/// quoted-string = QUOTE *(NONQUOTE) QUOTE
+/// token = bareword / quoted-string
+/// sep = COMMA/WS *(COMMA/WS)
+/// attribute = (DOT token)/(token EQUAL token)
+/// attribute-list = [sep] attribute *(sep attribute) [sep]
+/// delimited-attribute-list = OPEN-CURLY-BRACKET attribute-list CLOSE-CURLY-BRACKET
+/// token-list = [sep] token *(sep token) [sep]
+/// comment = OPEN_PAREN *(all characters) CLOSE_PAREN
+///
+/// OPEN_PAREN = "("
+/// CLOSE_PARENT = ")"
+/// OPEN-CURLY-BRACKET = "{"
+/// CLOSE-CURLY-BRACKET = "}"
+/// CHAR = ALPHA / DIGIT / "_" / "-" / ":"
+/// QUOTE = %x22
+/// NONQUOTE = %x09 / %x20 / %x21 / %x23-7E ; TAB / SPACE / all printable characters except `"`
+/// COMMA = ","
+/// DOT = "."
+/// EQUAL = "="
+///
+/// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
+/// DIGIT = %x30-39
+/// WS = %x09 / " "
+/// ```
 pub(crate) struct TagIterator<'a, 'tcx> {
     inner: Peekable<CharIndices<'a>>,
     data: &'a str,

From 6e21e4823d4bc8f8004271e0b327c7ae4ac9103f Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Wed, 3 May 2023 14:02:36 +0200
Subject: [PATCH 07/13] Fix incorrect codeblock attributes in docs

---
 compiler/rustc_middle/src/ty/typeck_results.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/compiler/rustc_middle/src/ty/typeck_results.rs b/compiler/rustc_middle/src/ty/typeck_results.rs
index 7ecc7e6014dfe..159cbb72a3b72 100644
--- a/compiler/rustc_middle/src/ty/typeck_results.rs
+++ b/compiler/rustc_middle/src/ty/typeck_results.rs
@@ -165,7 +165,7 @@ pub struct TypeckResults<'tcx> {
     /// reading places that are mentioned in a closure (because of _ patterns). However,
     /// to ensure the places are initialized, we introduce fake reads.
     /// Consider these two examples:
-    /// ``` (discriminant matching with only wildcard arm)
+    /// ```ignore (discriminant matching with only wildcard arm)
     /// let x: u8;
     /// let c = || match x { _ => () };
     /// ```
@@ -173,7 +173,7 @@ pub struct TypeckResults<'tcx> {
     /// want to capture it. However, we do still want an error here, because `x` should have
     /// to be initialized at the point where c is created. Therefore, we add a "fake read"
     /// instead.
-    /// ``` (destructured assignments)
+    /// ```ignore (destructured assignments)
     /// let c = || {
     ///     let (t1, t2) = t;
     /// }

From bbaa930b35667947fb6791527cb8e3a273c8b087 Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Wed, 3 May 2023 14:44:45 +0200
Subject: [PATCH 08/13] Fix compilation error "the trait bound
 `SubdiagnosticMessage: From<&std::string::String>` is not satisfied"

---
 src/librustdoc/passes/check_custom_code_classes.rs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/librustdoc/passes/check_custom_code_classes.rs b/src/librustdoc/passes/check_custom_code_classes.rs
index 246e7f8f3316b..eb32e79643113 100644
--- a/src/librustdoc/passes/check_custom_code_classes.rs
+++ b/src/librustdoc/passes/check_custom_code_classes.rs
@@ -67,10 +67,11 @@ pub(crate) fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, item: &Item)
         .note(
             // This will list the wrong items to make them more easily searchable.
             // To ensure the most correct hits, it adds back the 'class:' that was stripped.
-            &format!(
+            format!(
                 "found these custom classes: class={}",
                 tests.custom_classes_found.join(",class=")
-            ),
+            )
+            .as_str(),
         )
         .emit();
     }

From 87d2aa5fd34ab96f755fe69aed64083e8a246e09 Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Thu, 4 May 2023 11:17:56 +0200
Subject: [PATCH 09/13] Improve error emitting code

---
 src/librustdoc/html/markdown.rs | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 482e7b7f260f2..61dc4357c41b0 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -26,6 +26,7 @@
 //! ```
 
 use rustc_data_structures::fx::FxHashMap;
+use rustc_errors::{DiagnosticMessage, SubdiagnosticMessage};
 use rustc_hir::def_id::DefId;
 use rustc_middle::ty::TyCtxt;
 pub(crate) use rustc_resolve::rustdoc::main_body_opts;
@@ -808,7 +809,7 @@ impl<'tcx> ExtraInfo<'tcx> {
         ExtraInfo { def_id, sp, tcx }
     }
 
-    fn error_invalid_codeblock_attr(&self, msg: &str) {
+    fn error_invalid_codeblock_attr(&self, msg: impl Into<DiagnosticMessage>) {
         if let Some(def_id) = self.def_id.as_local() {
             self.tcx.struct_span_lint_hir(
                 crate::lint::INVALID_CODEBLOCK_ATTRIBUTES,
@@ -820,7 +821,11 @@ impl<'tcx> ExtraInfo<'tcx> {
         }
     }
 
-    fn error_invalid_codeblock_attr_with_help(&self, msg: &str, help: &str) {
+    fn error_invalid_codeblock_attr_with_help(
+        &self,
+        msg: impl Into<DiagnosticMessage>,
+        help: impl Into<SubdiagnosticMessage>,
+    ) {
         if let Some(def_id) = self.def_id.as_local() {
             self.tcx.struct_span_lint_hir(
                 crate::lint::INVALID_CODEBLOCK_ATTRIBUTES,
@@ -1246,7 +1251,7 @@ impl LangString {
                     } {
                         if let Some(extra) = extra {
                             extra.error_invalid_codeblock_attr_with_help(
-                                &format!("unknown attribute `{}`. Did you mean `{}`?", x, flag),
+                                format!("unknown attribute `{x}`. Did you mean `{flag}`?"),
                                 help,
                             );
                         }
@@ -1263,9 +1268,8 @@ impl LangString {
                     if key == "class" {
                         data.added_classes.push(value.to_owned());
                     } else if let Some(extra) = extra {
-                        extra.error_invalid_codeblock_attr(&format!(
-                            "unsupported attribute `{key}`"
-                        ));
+                        extra
+                            .error_invalid_codeblock_attr(format!("unsupported attribute `{key}`"));
                     }
                 }
                 LangStringToken::ClassAttribute(class) => {

From 113220b970bcecd4463288c459eeeae9ca315bb1 Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Mon, 28 Aug 2023 14:32:01 +0200
Subject: [PATCH 10/13] Update to new `emit_error` API

---
 src/librustdoc/html/markdown.rs                  | 16 ++++++++--------
 .../passes/check_custom_code_classes.rs          |  5 ++---
 2 files changed, 10 insertions(+), 11 deletions(-)

diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 61dc4357c41b0..db30836b43f1d 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -921,7 +921,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
         Self { inner: data.char_indices().peekable(), data, is_in_attribute_block: false, extra }
     }
 
-    fn emit_error(&self, err: &str) {
+    fn emit_error(&self, err: impl Into<DiagnosticMessage>) {
         if let Some(extra) = self.extra {
             extra.error_invalid_codeblock_attr(err);
         }
@@ -954,7 +954,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
             } else {
                 let class = &self.data[start + 1..pos];
                 if class.is_empty() {
-                    self.emit_error(&format!("unexpected `{c}` character after `.`"));
+                    self.emit_error(format!("unexpected `{c}` character after `.`"));
                     return None;
                 } else if self.check_after_token() {
                     return Some(LangStringToken::ClassAttribute(class));
@@ -995,7 +995,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
 
         if let Some((_, c)) = self.inner.next() {
             if c != '=' {
-                self.emit_error(&format!("expected `=`, found `{}`", c));
+                self.emit_error(format!("expected `=`, found `{}`", c));
                 return None;
             }
         } else {
@@ -1006,7 +1006,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
             Some((pos, '"')) => self.parse_string(pos)?,
             Some((pos, c)) if is_bareword_char(c) => self.parse_token(pos)?,
             Some((_, c)) => {
-                self.emit_error(&format!("unexpected `{c}` character after `=`"));
+                self.emit_error(format!("unexpected `{c}` character after `=`"));
                 return None;
             }
             None => {
@@ -1033,7 +1033,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
             if c == '}' || is_separator(c) || c == '(' {
                 true
             } else {
-                self.emit_error(&format!("unexpected `{c}` character"));
+                self.emit_error(format!("unexpected `{c}` character"));
                 false
             }
         } else {
@@ -1052,7 +1052,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
             } else if c == '"' || is_bareword_char(c) {
                 return self.parse_key_value(c, pos);
             } else {
-                self.emit_error(&format!("unexpected character `{c}`"));
+                self.emit_error(format!("unexpected character `{c}`"));
                 return None;
             }
         }
@@ -1080,7 +1080,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
                 }
                 let indices = self.parse_string(pos)?;
                 if let Some((_, c)) = self.inner.peek().copied() && c != '{' && !is_separator(c) && c != '(' {
-                    self.emit_error(&format!("expected ` `, `{{` or `,` after `\"`, found `{c}`"));
+                    self.emit_error(format!("expected ` `, `{{` or `,` after `\"`, found `{c}`"));
                     return None;
                 }
                 return Some(LangStringToken::LangToken(&self.data[indices.start..indices.end]));
@@ -1103,7 +1103,7 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
                 }
                 return self.next();
             } else {
-                self.emit_error(&format!("unexpected character `{c}`"));
+                self.emit_error(format!("unexpected character `{c}`"));
                 return None;
             }
         }
diff --git a/src/librustdoc/passes/check_custom_code_classes.rs b/src/librustdoc/passes/check_custom_code_classes.rs
index eb32e79643113..73e80372e4a41 100644
--- a/src/librustdoc/passes/check_custom_code_classes.rs
+++ b/src/librustdoc/passes/check_custom_code_classes.rs
@@ -54,7 +54,7 @@ pub(crate) fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, item: &Item)
 
     let mut tests = TestsWithCustomClasses { custom_classes_found: vec![] };
 
-    let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
+    let dox = item.attrs.doc_value();
     find_codes(&dox, &mut tests, ErrorCodes::No, false, None, true);
 
     if !tests.custom_classes_found.is_empty() && !cx.tcx.features().custom_code_classes_in_docs {
@@ -70,8 +70,7 @@ pub(crate) fn look_for_custom_classes<'tcx>(cx: &DocContext<'tcx>, item: &Item)
             format!(
                 "found these custom classes: class={}",
                 tests.custom_classes_found.join(",class=")
-            )
-            .as_str(),
+            ),
         )
         .emit();
     }

From f038f180fd5c6a70a6018c2609862d5b6912d761 Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Mon, 28 Aug 2023 15:03:40 +0200
Subject: [PATCH 11/13] Add `custom` tag for markdown codeblocks

---
 src/librustdoc/html/markdown.rs       |  8 +++---
 src/librustdoc/html/markdown/tests.rs | 36 +++++++++++++++++++++++++++
 2 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index db30836b43f1d..177fb1a9426f9 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -1166,6 +1166,7 @@ impl LangString {
         let allow_error_code_check = allow_error_code_check.as_bool();
         let mut seen_rust_tags = false;
         let mut seen_other_tags = false;
+        let mut seen_custom_tag = false;
         let mut data = LangString::default();
         let mut ignores = vec![];
 
@@ -1195,6 +1196,9 @@ impl LangString {
                     data.rust = true;
                     seen_rust_tags = true;
                 }
+                LangStringToken::LangToken("custom") => {
+                    seen_custom_tag = true;
+                }
                 LangStringToken::LangToken("test_harness") => {
                     data.test_harness = true;
                     seen_rust_tags = !seen_other_tags || seen_rust_tags;
@@ -1264,7 +1268,6 @@ impl LangString {
                     data.unknown.push(x.to_owned());
                 }
                 LangStringToken::KeyValueAttribute(key, value) => {
-                    seen_other_tags = true;
                     if key == "class" {
                         data.added_classes.push(value.to_owned());
                     } else if let Some(extra) = extra {
@@ -1273,7 +1276,6 @@ impl LangString {
                     }
                 }
                 LangStringToken::ClassAttribute(class) => {
-                    seen_other_tags = true;
                     data.added_classes.push(class.to_owned());
                 }
             }
@@ -1284,7 +1286,7 @@ impl LangString {
             data.ignore = Ignore::Some(ignores);
         }
 
-        data.rust &= !seen_other_tags || seen_rust_tags;
+        data.rust &= !seen_custom_tag && (!seen_other_tags || seen_rust_tags);
 
         data
     }
diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index 35b243d3d292e..7d89cb0c4e618 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -64,6 +64,12 @@ fn test_lang_string_parse() {
     t(LangString { original: "{rust}".into(), rust: true, ..Default::default() });
     t(LangString {
         original: "{.rust}".into(),
+        rust: true,
+        added_classes: vec!["rust".into()],
+        ..Default::default()
+    });
+    t(LangString {
+        original: "custom,{.rust}".into(),
         rust: false,
         added_classes: vec!["rust".into()],
         ..Default::default()
@@ -154,12 +160,24 @@ fn test_lang_string_parse() {
     t(LangString {
         original: "{class=test}".into(),
         added_classes: vec!["test".into()],
+        rust: true,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "custom,{class=test}".into(),
+        added_classes: vec!["test".into()],
         rust: false,
         ..Default::default()
     });
     t(LangString {
         original: "{.test}".into(),
         added_classes: vec!["test".into()],
+        rust: true,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "custom,{.test}".into(),
+        added_classes: vec!["test".into()],
         rust: false,
         ..Default::default()
     });
@@ -172,12 +190,24 @@ fn test_lang_string_parse() {
     t(LangString {
         original: "{class=test:with:colon .test1}".into(),
         added_classes: vec!["test:with:colon".into(), "test1".into()],
+        rust: true,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "custom,{class=test:with:colon .test1}".into(),
+        added_classes: vec!["test:with:colon".into(), "test1".into()],
         rust: false,
         ..Default::default()
     });
     t(LangString {
         original: "{class=first,class=second}".into(),
         added_classes: vec!["first".into(), "second".into()],
+        rust: true,
+        ..Default::default()
+    });
+    t(LangString {
+        original: "custom,{class=first,class=second}".into(),
+        added_classes: vec!["first".into(), "second".into()],
         rust: false,
         ..Default::default()
     });
@@ -206,6 +236,12 @@ fn test_lang_string_parse() {
     t(LangString {
         original: r#"{class="first"}"#.into(),
         added_classes: vec!["first".into()],
+        rust: true,
+        ..Default::default()
+    });
+    t(LangString {
+        original: r#"custom,{class="first"}"#.into(),
+        added_classes: vec!["first".into()],
         rust: false,
         ..Default::default()
     });

From 7c72edf19fa72a1c9596a79ee9221342a27289ad Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Mon, 28 Aug 2023 15:07:54 +0200
Subject: [PATCH 12/13] Update documentation for `custom_code_classes_in_docs`
 feature

---
 src/doc/rustdoc/src/unstable-features.md | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md
index b5d8c8194188a..0d3f6338af45d 100644
--- a/src/doc/rustdoc/src/unstable-features.md
+++ b/src/doc/rustdoc/src/unstable-features.md
@@ -631,7 +631,7 @@ to jump to a type definition.
 ```rust
 #![feature(custom_code_classes_in_docs)]
 
-/// ```{class=language-c}
+/// ```custom,{class=language-c}
 /// int main(void) { return 0; }
 /// ```
 pub struct Bar;
@@ -641,12 +641,16 @@ The text `int main(void) { return 0; }` is rendered without highlighting in a co
 with the class `language-c`. This can be used to highlight other languages through JavaScript
 libraries for example.
 
+Without the `custom` attribute, it would be generated as a Rust code example with an additional
+`language-C` CSS class. Therefore, if you specifically don't want it to be a Rust code example,
+don't forget to add the `custom` attribute.
+
 To be noted that you can replace `class=` with `.` to achieve the same result:
 
 ```rust
 #![feature(custom_code_classes_in_docs)]
 
-/// ```{.language-c}
+/// ```custom,{.language-c}
 /// int main(void) { return 0; }
 /// ```
 pub struct Bar;

From e39c39346b8ff10e57fa93563029b4cc39fb2b2a Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume.gomez@huawei.com>
Date: Sat, 16 Sep 2023 11:37:41 +0200
Subject: [PATCH 13/13] Fix invalid markdown codeblock label

---
 .../rustc_builtin_macros/src/deriving/generic/mod.rs | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs b/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
index 6597ee3cf1b6c..edc6f9f098ea7 100644
--- a/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
+++ b/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
@@ -88,7 +88,7 @@
 //!
 //! When generating the `expr` for the `A` impl, the `SubstructureFields` is
 //!
-//! ```{.text}
+//! ```text
 //! Struct(vec![FieldInfo {
 //!            span: <span of x>
 //!            name: Some(<ident of x>),
@@ -99,7 +99,7 @@
 //!
 //! For the `B` impl, called with `B(a)` and `B(b)`,
 //!
-//! ```{.text}
+//! ```text
 //! Struct(vec![FieldInfo {
 //!           span: <span of `i32`>,
 //!           name: None,
@@ -113,7 +113,7 @@
 //! When generating the `expr` for a call with `self == C0(a)` and `other
 //! == C0(b)`, the SubstructureFields is
 //!
-//! ```{.text}
+//! ```text
 //! EnumMatching(0, <ast::Variant for C0>,
 //!              vec![FieldInfo {
 //!                 span: <span of i32>
@@ -125,7 +125,7 @@
 //!
 //! For `C1 {x}` and `C1 {x}`,
 //!
-//! ```{.text}
+//! ```text
 //! EnumMatching(1, <ast::Variant for C1>,
 //!              vec![FieldInfo {
 //!                 span: <span of x>
@@ -137,7 +137,7 @@
 //!
 //! For the tags,
 //!
-//! ```{.text}
+//! ```text
 //! EnumTag(
 //!     &[<ident of self tag>, <ident of other tag>], <expr to combine with>)
 //! ```
@@ -149,7 +149,7 @@
 //!
 //! A static method on the types above would result in,
 //!
-//! ```{.text}
+//! ```text
 //! StaticStruct(<ast::VariantData of A>, Named(vec![(<ident of x>, <span of x>)]))
 //!
 //! StaticStruct(<ast::VariantData of B>, Unnamed(vec![<span of x>]))