diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index bb62a0bc9cc..b5d8c819418 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 a25a6f7d35d..6bd4e775c0e 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> { + 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> { + 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 dd3d0ebac0c..b0b4de65cca 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 00000000000..57d9038cb0c --- /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 00000000000..7432af19360 --- /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 +