diff options
| author | bors <bors@rust-lang.org> | 2023-09-16 13:10:11 +0000 |
|---|---|---|
| committer | bors <bors@rust-lang.org> | 2023-09-16 13:10:11 +0000 |
| commit | 41bafc4ff3eb6a73aa40e60c3bd4494302c7ec57 (patch) | |
| tree | a0cfa6468cc265eb55df838ec85beba13f0458c1 /src/librustdoc/html | |
| parent | 42dead4d23309234802390aca16763f353920cf6 (diff) | |
| parent | e39c39346b8ff10e57fa93563029b4cc39fb2b2a (diff) | |
| download | rust-41bafc4ff3eb6a73aa40e60c3bd4494302c7ec57.tar.gz rust-41bafc4ff3eb6a73aa40e60c3bd4494302c7ec57.zip | |
Auto merge of #110800 - GuillaumeGomez:custom_code_classes_in_docs, r=t-rustdoc
Accept additional user-defined syntax classes in fenced code blocks
Part of #79483.
This is a re-opening of https://github.com/rust-lang/rust/pull/79454 after a big update/cleanup. I also converted the syntax to pandoc as suggested by `@notriddle:` the idea is to be as compatible as possible with the existing instead of having our own syntax.
## Motivation
From the original issue: https://github.com/rust-lang/rust/issues/78917
> The technique used by `inline-c-rs` can be ported to other languages. It's just super fun to see C code inside Rust documentation that is also tested by `cargo doc`. I'm sure this technique can be used by other languages in the future.
Having custom CSS classes for syntax highlighting will allow tools like `highlight.js` to be used in order to provide highlighting for languages other than Rust while not increasing technical burden on rustdoc.
## What is the feature about?
In short, this PR changes two things, both related to codeblocks in doc comments in Rust documentation:
* Allow to disable generation of `language-*` CSS classes with the `custom` attribute.
* Add your own CSS classes to a code block so that you can use other tools to highlight them.
#### The `custom` attribute
Let's start with the new `custom` attribute: it will disable the generation of the `language-*` CSS class on the generated HTML code block. For example:
```rust
/// ```custom,c
/// int main(void) {
/// return 0;
/// }
/// ```
```
The generated HTML code block will not have `class="language-c"` because the `custom` attribute has been set. The `custom` attribute becomes especially useful with the other thing added by this feature: adding your own CSS classes.
#### Adding your own CSS classes
The second part of this feature is to allow users to add CSS classes themselves so that they can then add a JS library which will do it (like `highlight.js` or `prism.js`), allowing to support highlighting for other languages than Rust without increasing burden on rustdoc. To disable the automatic `language-*` CSS class generation, you need to use the `custom` attribute as well.
This allow users to write the following:
```rust
/// Some code block with `{class=language-c}` as the language string.
///
/// ```custom,{class=language-c}
/// int main(void) {
/// return 0;
/// }
/// ```
fn main() {}
```
This will notably produce the following HTML:
```html
<pre class="language-c">
int main(void) {
return 0;
}</pre>
```
Instead of:
```html
<pre class="rust rust-example-rendered">
<span class="ident">int</span> <span class="ident">main</span>(<span class="ident">void</span>) {
<span class="kw">return</span> <span class="number">0</span>;
}
</pre>
```
To be noted, we could have written `{.language-c}` to achieve the same result. `.` and `class=` have the same effect.
One last syntax point: content between parens (`(like this)`) is now considered as comment and is not taken into account at all.
In addition to this, I added an `unknown` field into `LangString` (the parsed code block "attribute") because of cases like this:
```rust
/// ```custom,class:language-c
/// main;
/// ```
pub fn foo() {}
```
Without this `unknown` field, it would generate in the DOM: `<pre class="language-class:language-c language-c">`, which is quite bad. So instead, it now stores all unknown tags into the `unknown` field and use the first one as "language". So in this case, since there is no unknown tag, it'll simply generate `<pre class="language-c">`. I added tests to cover this.
Finally, I added a parser for the codeblock attributes to make it much easier to maintain. It'll be pretty easy to extend.
As to why this syntax for adding attributes was picked: it's [Pandoc's syntax](https://pandoc.org/MANUAL.html#extension-fenced_code_attributes). Even if it seems clunkier in some cases, it's extensible, and most third-party Markdown renderers are smart enough to ignore Pandoc's brace-delimited attributes (from [this comment](https://github.com/rust-lang/rust/pull/110800#issuecomment-1522044456)).
## Raised concerns
#### It's not obvious when the `language-*` attribute generation will be added or not.
It is added by default. If you want to disable it, you will need to use the `custom` attribute.
#### Why not using HTML in markdown directly then?
Code examples in most languages are likely to contain `<`, `>`, `&` and `"` characters. These characters [require escaping](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre) when written inside the `<pre>` element. Using the \`\`\` code blocks allows rustdoc to take care of escaping, which means doc authors can paste code samples directly without manually converting them to HTML.
cc `@poliorcetics`
r? `@notriddle`
Diffstat (limited to 'src/librustdoc/html')
| -rw-r--r-- | src/librustdoc/html/highlight.rs | 25 | ||||
| -rw-r--r-- | src/librustdoc/html/markdown.rs | 401 | ||||
| -rw-r--r-- | src/librustdoc/html/markdown/tests.rs | 182 |
3 files changed, 536 insertions, 72 deletions
diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index 039e8cdb987..d8e36139a78 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 b28019e3f91..177fb1a9426 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; @@ -37,8 +38,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 +245,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 +270,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 +328,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())) } @@ -712,6 +726,17 @@ pub(crate) fn find_testable_code<T: doctest::Tester>( 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; let mut nb_lines = 0; @@ -734,7 +759,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 +809,23 @@ 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: impl Into<DiagnosticMessage>) { + 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: 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, @@ -808,6 +849,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 +860,276 @@ 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, + is_in_attribute_block: bool, + extra: Option<&'a ExtraInfo<'tcx>>, +} + +#[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, is_in_attribute_block: false, extra } + } + + fn emit_error(&self, err: impl Into<DiagnosticMessage>) { + if let Some(extra) = self.extra { + extra.error_invalid_codeblock_attr(err); + } + } + + 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 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 + } + + 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; + } + } + } + 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 + } + } + + /// 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 c == '}' { + self.is_in_attribute_block = false; + 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; + } + } + self.emit_error("unclosed attribute block (`{}`): missing `}` at the end"); + None + } + + /// 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 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; + 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 self.next(); + } else if c == '(' { + if !self.skip_paren_block() { + return None; + } + 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(LangStringToken::LangToken(&self.data[start..])) } + } +} + +impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> { + type Item = LangStringToken<'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 { + self.parse_in_attribute_block() + } else { + self.parse_outside_attribute_block(start) + } + } +} + impl Default for LangString { fn default() -> Self { Self { @@ -829,6 +1142,8 @@ impl Default for LangString { compile_fail: false, error_codes: Vec::new(), edition: None, + added_classes: Vec::new(), + unknown: Vec::new(), } } } @@ -838,86 +1153,67 @@ impl LangString { 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; + let mut seen_custom_tag = false; let mut data = LangString::default(); let mut ignores = vec![]; data.original = string.to_owned(); - for token in Self::tokens(string) { + for token in TagIterator::new(string, extra) { match token { - "should_panic" => { + LangStringToken::LangToken("should_panic") => { data.should_panic = true; seen_rust_tags = !seen_other_tags; } - "no_run" => { + LangStringToken::LangToken("no_run") => { data.no_run = true; seen_rust_tags = !seen_other_tags; } - "ignore" => { + LangStringToken::LangToken("ignore") => { data.ignore = Ignore::All; seen_rust_tags = !seen_other_tags; } - 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; } } - "rust" => { + LangStringToken::LangToken("rust") => { data.rust = true; seen_rust_tags = true; } - "test_harness" => { + LangStringToken::LangToken("custom") => { + seen_custom_tag = true; + } + LangStringToken::LangToken("test_harness") => { data.test_harness = true; seen_rust_tags = !seen_other_tags || seen_rust_tags; } - "compile_fail" => { + LangStringToken::LangToken("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") => { + LangStringToken::LangToken(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 => { + LangStringToken::LangToken(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 +1221,7 @@ impl LangString { seen_other_tags = true; } } - 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" @@ -958,15 +1254,30 @@ impl LangString { None } { if let Some(extra) = extra { - extra.error_invalid_codeblock_attr( + extra.error_invalid_codeblock_attr_with_help( format!("unknown attribute `{x}`. Did you mean `{flag}`?"), help, ); } } seen_other_tags = true; + data.unknown.push(x.to_owned()); + } + LangStringToken::LangToken(x) => { + seen_other_tags = true; + data.unknown.push(x.to_owned()); + } + LangStringToken::KeyValueAttribute(key, value) => { + 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}`")); + } + } + LangStringToken::ClassAttribute(class) => { + data.added_classes.push(class.to_owned()); } - _ => seen_other_tags = true, } } @@ -975,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 db8504d15c7..7d89cb0c4e6 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, LangStringToken, Markdown, + MarkdownItemInfo, TagIterator, +}; use rustc_span::edition::{Edition, DEFAULT_EDITION}; #[test] @@ -51,10 +54,32 @@ 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: "rusta".into(), + rust: false, + unknown: vec!["rusta".into()], + ..Default::default() + }); + // error + 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() + }); + 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 +95,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(), ..Default::default() }); t(LangString { - original: "test_harness,.rust".into(), + original: "example,rust".into(), + unknown: vec!["example".into()], + ..Default::default() + }); + t(LangString { + original: "test_harness,rusta".into(), test_harness: true, + unknown: vec!["rusta".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 { @@ -117,29 +157,125 @@ fn test_lang_string_parse() { edition: Some(Edition::Edition2018), ..Default::default() }); + 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() + }); + 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 .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() + }); + 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() + }); + // 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: true, + ..Default::default() + }); + t(LangString { + original: r#"custom,{class="first"}"#.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: &[&str]) { - let have = LangString::tokens(lang_string).collect::<Vec<&str>>(); + 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", &["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", &[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] |
