Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 4ce17fa

Browse files
Add support for double quotes in markdown codeblock attributes
1 parent d829fee commit 4ce17fa

File tree

5 files changed

+145
-49
lines changed

5 files changed

+145
-49
lines changed

src/doc/rustdoc/src/unstable-features.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,3 +654,14 @@ pub struct Bar;
654654

655655
To be noted, `rust` and `.rust`/`class=rust` have different effects: `rust` indicates that this is
656656
a Rust code block whereas the two others add a "rust" CSS class on the code block.
657+
658+
You can also use double quotes:
659+
660+
```rust
661+
#![feature(custom_code_classes_in_docs)]
662+
663+
/// ```"not rust" {."hello everyone"}
664+
/// int main(void) { return 0; }
665+
/// ```
666+
pub struct Bar;
667+
```

src/librustdoc/html/markdown.rs

Lines changed: 72 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,75 @@ impl<'a, 'tcx> TagIterator<'a, 'tcx> {
892892
extra.error_invalid_codeblock_attr(err);
893893
}
894894
}
895+
896+
/// Returns false if the string is unfinished.
897+
fn skip_string(&mut self) -> bool {
898+
while let Some((_, c)) = self.inner.next() {
899+
if c == '"' {
900+
return true;
901+
}
902+
}
903+
self.emit_error("unclosed quote string: missing `\"` at the end");
904+
false
905+
}
906+
907+
fn parse_in_attribute_block(&mut self, start: usize) -> Option<TokenKind<'a>> {
908+
while let Some((pos, c)) = self.inner.next() {
909+
if is_separator(c) {
910+
return Some(TokenKind::Attribute(&self.data[start..pos]));
911+
} else if c == '{' {
912+
// There shouldn't be a nested block!
913+
self.emit_error("unexpected `{` inside attribute block (`{}`)");
914+
let attr = &self.data[start..pos];
915+
if attr.is_empty() {
916+
return self.next();
917+
}
918+
self.inner.next();
919+
return Some(TokenKind::Attribute(attr));
920+
} else if c == '}' {
921+
self.is_in_attribute_block = false;
922+
let attr = &self.data[start..pos];
923+
if attr.is_empty() {
924+
return self.next();
925+
}
926+
return Some(TokenKind::Attribute(attr));
927+
} else if c == '"' && !self.skip_string() {
928+
return None;
929+
}
930+
}
931+
// Unclosed attribute block!
932+
self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
933+
let token = &self.data[start..];
934+
if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) }
935+
}
936+
937+
fn parse_outside_attribute_block(&mut self, start: usize) -> Option<TokenKind<'a>> {
938+
while let Some((pos, c)) = self.inner.next() {
939+
if is_separator(c) {
940+
return Some(TokenKind::Token(&self.data[start..pos]));
941+
} else if c == '{' {
942+
self.is_in_attribute_block = true;
943+
let token = &self.data[start..pos];
944+
if token.is_empty() {
945+
return self.next();
946+
}
947+
return Some(TokenKind::Token(token));
948+
} else if c == '}' {
949+
// We're not in a block so it shouldn't be there!
950+
self.emit_error("unexpected `}` outside attribute block (`{}`)");
951+
let token = &self.data[start..pos];
952+
if token.is_empty() {
953+
return self.next();
954+
}
955+
self.inner.next();
956+
return Some(TokenKind::Attribute(token));
957+
} else if c == '"' && !self.skip_string() {
958+
return None;
959+
}
960+
}
961+
let token = &self.data[start..];
962+
if token.is_empty() { None } else { Some(TokenKind::Token(token)) }
963+
}
895964
}
896965

897966
impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> {
@@ -905,55 +974,9 @@ impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> {
905974
return None;
906975
};
907976
if self.is_in_attribute_block {
908-
while let Some((pos, c)) = self.inner.next() {
909-
if is_separator(c) {
910-
return Some(TokenKind::Attribute(&self.data[start..pos]));
911-
} else if c == '{' {
912-
// There shouldn't be a nested block!
913-
self.emit_error("unexpected `{` inside attribute block (`{}`)");
914-
let attr = &self.data[start..pos];
915-
if attr.is_empty() {
916-
return self.next();
917-
}
918-
self.inner.next();
919-
return Some(TokenKind::Attribute(attr));
920-
} else if c == '}' {
921-
self.is_in_attribute_block = false;
922-
let attr = &self.data[start..pos];
923-
if attr.is_empty() {
924-
return self.next();
925-
}
926-
return Some(TokenKind::Attribute(attr));
927-
}
928-
}
929-
// Unclosed attribute block!
930-
self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
931-
let token = &self.data[start..];
932-
if token.is_empty() { None } else { Some(TokenKind::Attribute(token)) }
977+
self.parse_in_attribute_block(start)
933978
} else {
934-
while let Some((pos, c)) = self.inner.next() {
935-
if is_separator(c) {
936-
return Some(TokenKind::Token(&self.data[start..pos]));
937-
} else if c == '{' {
938-
self.is_in_attribute_block = true;
939-
let token = &self.data[start..pos];
940-
if token.is_empty() {
941-
return self.next();
942-
}
943-
return Some(TokenKind::Token(token));
944-
} else if c == '}' {
945-
// We're not in a block so it shouldn't be there!
946-
self.emit_error("unexpected `}` outside attribute block (`{}`)");
947-
let token = &self.data[start..pos];
948-
if token.is_empty() {
949-
return self.next();
950-
}
951-
self.inner.next();
952-
return Some(TokenKind::Attribute(token));
953-
}
954-
}
955-
let token = &self.data[start..];
956-
if token.is_empty() { None } else { Some(TokenKind::Token(token)) }
979+
self.parse_outside_attribute_block(start)
957980
}
958981
}
959982
}
@@ -982,7 +1005,7 @@ fn handle_class(class: &str, after: &str, data: &mut LangString, extra: Option<&
9821005
extra.error_invalid_codeblock_attr(&format!("missing class name after `{after}`"));
9831006
}
9841007
} else {
985-
data.added_classes.push(class.to_owned());
1008+
data.added_classes.push(class.replace('"', ""));
9861009
}
9871010
}
9881011

src/librustdoc/html/markdown/tests.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,18 @@ fn test_lang_string_parse() {
218218
rust: false,
219219
..Default::default()
220220
});
221+
t(LangString {
222+
original: r#"{class="first"}"#.into(),
223+
added_classes: vec!["first".into()],
224+
rust: false,
225+
..Default::default()
226+
});
227+
t(LangString {
228+
original: r#"{class=f"irst"}"#.into(),
229+
added_classes: vec!["first".into()],
230+
rust: false,
231+
..Default::default()
232+
});
221233
}
222234

223235
#[test]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// This test ensures that warnings are working as expected for "custom_code_classes_in_docs"
2+
// feature.
3+
4+
#![feature(custom_code_classes_in_docs)]
5+
#![deny(warnings)]
6+
#![feature(no_core)]
7+
#![no_core]
8+
9+
/// ```{class="}
10+
/// main;
11+
/// ```
12+
//~^^^ ERROR unclosed quote string
13+
//~| ERROR unclosed quote string
14+
/// ```"
15+
/// main;
16+
/// ```
17+
pub fn foo() {}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
error: unclosed quote string: missing `"` at the end
2+
--> $DIR/custom_code_classes_in_docs-warning3.rs:9:1
3+
|
4+
LL | / /// ```{class="}
5+
LL | | /// main;
6+
LL | | /// ```
7+
LL | |
8+
... |
9+
LL | | /// main;
10+
LL | | /// ```
11+
| |_______^
12+
|
13+
note: the lint level is defined here
14+
--> $DIR/custom_code_classes_in_docs-warning3.rs:5:9
15+
|
16+
LL | #![deny(warnings)]
17+
| ^^^^^^^^
18+
= note: `#[deny(rustdoc::invalid_codeblock_attributes)]` implied by `#[deny(warnings)]`
19+
20+
error: unclosed quote string: missing `"` at the end
21+
--> $DIR/custom_code_classes_in_docs-warning3.rs:9:1
22+
|
23+
LL | / /// ```{class="}
24+
LL | | /// main;
25+
LL | | /// ```
26+
LL | |
27+
... |
28+
LL | | /// main;
29+
LL | | /// ```
30+
| |_______^
31+
32+
error: aborting due to 2 previous errors
33+

0 commit comments

Comments
 (0)