diff options
Diffstat (limited to 'src/librustdoc/html/highlight.rs')
-rw-r--r-- | src/librustdoc/html/highlight.rs | 355 |
1 files changed, 290 insertions, 65 deletions
diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index 05547ea15..8922bf377 100644 --- a/src/librustdoc/html/highlight.rs +++ b/src/librustdoc/html/highlight.rs @@ -29,31 +29,74 @@ pub(crate) struct HrefContext<'a, 'b, 'c> { /// This field is used to know "how far" from the top of the directory we are to link to either /// documentation pages or other source pages. pub(crate) root_path: &'c str, + /// This field is used to calculate precise local URLs. + pub(crate) current_href: &'c str, } /// Decorations are represented as a map from CSS class to vector of character ranges. /// Each range will be wrapped in a span with that class. +#[derive(Default)] pub(crate) struct DecorationInfo(pub(crate) FxHashMap<&'static str, Vec<(u32, u32)>>); -/// Highlights `src`, returning the HTML output. -pub(crate) fn render_with_highlighting( +#[derive(Eq, PartialEq, Clone, Copy)] +pub(crate) enum Tooltip { + Ignore, + CompileFail, + ShouldPanic, + Edition(Edition), + None, +} + +/// Highlights `src` as an inline example, returning the HTML output. +pub(crate) fn render_example_with_highlighting( src: &str, out: &mut Buffer, - class: Option<&str>, + tooltip: Tooltip, playground_button: Option<&str>, - tooltip: Option<(Option<Edition>, &str)>, - edition: Edition, - extra_content: Option<Buffer>, - href_context: Option<HrefContext<'_, '_, '_>>, - decoration_info: Option<DecorationInfo>, ) { - debug!("highlighting: ================\n{}\n==============", src); - if let Some((edition_info, class)) = tooltip { + write_header(out, "rust-example-rendered", None, tooltip); + write_code(out, src, None, None); + write_footer(out, playground_button); +} + +/// Highlights `src` as a macro, returning the HTML output. +pub(crate) fn render_macro_with_highlighting(src: &str, out: &mut Buffer) { + write_header(out, "macro", None, Tooltip::None); + write_code(out, src, None, None); + write_footer(out, None); +} + +/// Highlights `src` as a source code page, returning the HTML output. +pub(crate) fn render_source_with_highlighting( + src: &str, + out: &mut Buffer, + line_numbers: Buffer, + href_context: HrefContext<'_, '_, '_>, + decoration_info: DecorationInfo, +) { + write_header(out, "", Some(line_numbers), Tooltip::None); + write_code(out, src, Some(href_context), Some(decoration_info)); + write_footer(out, None); +} + +fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>, tooltip: Tooltip) { + write!( + out, + "<div class=\"example-wrap{}\">", + match tooltip { + Tooltip::Ignore => " ignore", + Tooltip::CompileFail => " compile_fail", + Tooltip::ShouldPanic => " should_panic", + Tooltip::Edition(_) => " edition", + Tooltip::None => "", + }, + ); + + if tooltip != Tooltip::None { write!( out, - "<div class='information'><div class='tooltip {}'{}>ⓘ</div></div>", - class, - if let Some(edition_info) = edition_info { + "<div class='tooltip'{}>ⓘ</div>", + if let Tooltip::Edition(edition_info) = tooltip { format!(" data-edition=\"{}\"", edition_info) } else { String::new() @@ -61,24 +104,115 @@ pub(crate) fn render_with_highlighting( ); } - write_header(out, class, extra_content); - write_code(out, src, edition, href_context, decoration_info); - write_footer(out, playground_button); -} - -fn write_header(out: &mut Buffer, class: Option<&str>, extra_content: Option<Buffer>) { - write!(out, "<div class=\"example-wrap\">"); if let Some(extra) = extra_content { out.push_buffer(extra); } - if let Some(class) = class { - write!(out, "<pre class=\"rust {}\">", class); - } else { + if class.is_empty() { write!(out, "<pre class=\"rust\">"); + } else { + write!(out, "<pre class=\"rust {class}\">"); } write!(out, "<code>"); } +/// Check if two `Class` can be merged together. In the following rules, "unclassified" means `None` +/// basically (since it's `Option<Class>`). The following rules apply: +/// +/// * If two `Class` have the same variant, then they can be merged. +/// * If the other `Class` is unclassified and only contains white characters (backline, +/// whitespace, etc), it can be merged. +/// * `Class::Ident` is considered the same as unclassified (because it doesn't have an associated +/// CSS class). +fn can_merge(class1: Option<Class>, class2: Option<Class>, text: &str) -> bool { + match (class1, class2) { + (Some(c1), Some(c2)) => c1.is_equal_to(c2), + (Some(Class::Ident(_)), None) | (None, Some(Class::Ident(_))) => true, + (Some(_), None) | (None, Some(_)) => text.trim().is_empty(), + (None, None) => true, + } +} + +/// This type is used as a conveniency to prevent having to pass all its fields as arguments into +/// the various functions (which became its methods). +struct TokenHandler<'a, 'b, 'c, 'd, 'e> { + out: &'a mut Buffer, + /// It contains the closing tag and the associated `Class`. + closing_tags: Vec<(&'static str, Class)>, + /// This is used because we don't automatically generate the closing tag on `ExitSpan` in + /// case an `EnterSpan` event with the same class follows. + pending_exit_span: Option<Class>, + /// `current_class` and `pending_elems` are used to group HTML elements with same `class` + /// attributes to reduce the DOM size. + current_class: Option<Class>, + /// We need to keep the `Class` for each element because it could contain a `Span` which is + /// used to generate links. + pending_elems: Vec<(&'b str, Option<Class>)>, + href_context: Option<HrefContext<'c, 'd, 'e>>, +} + +impl<'a, 'b, 'c, 'd, 'e> TokenHandler<'a, 'b, 'c, 'd, 'e> { + fn handle_exit_span(&mut self) { + // We can't get the last `closing_tags` element using `pop()` because `closing_tags` is + // being used in `write_pending_elems`. + let class = self.closing_tags.last().expect("ExitSpan without EnterSpan").1; + // We flush everything just in case... + self.write_pending_elems(Some(class)); + + exit_span(self.out, self.closing_tags.pop().expect("ExitSpan without EnterSpan").0); + self.pending_exit_span = None; + } + + /// Write all the pending elements sharing a same (or at mergeable) `Class`. + /// + /// If there is a "parent" (if a `EnterSpan` event was encountered) and the parent can be merged + /// with the elements' class, then we simply write the elements since the `ExitSpan` event will + /// close the tag. + /// + /// Otherwise, if there is only one pending element, we let the `string` function handle both + /// opening and closing the tag, otherwise we do it into this function. + /// + /// It returns `true` if `current_class` must be set to `None` afterwards. + fn write_pending_elems(&mut self, current_class: Option<Class>) -> bool { + if self.pending_elems.is_empty() { + return false; + } + if let Some((_, parent_class)) = self.closing_tags.last() && + can_merge(current_class, Some(*parent_class), "") + { + for (text, class) in self.pending_elems.iter() { + string(self.out, Escape(text), *class, &self.href_context, false); + } + } else { + // We only want to "open" the tag ourselves if we have more than one pending and if the + // current parent tag is not the same as our pending content. + let close_tag = if self.pending_elems.len() > 1 && current_class.is_some() { + Some(enter_span(self.out, current_class.unwrap(), &self.href_context)) + } else { + None + }; + for (text, class) in self.pending_elems.iter() { + string(self.out, Escape(text), *class, &self.href_context, close_tag.is_none()); + } + if let Some(close_tag) = close_tag { + exit_span(self.out, close_tag); + } + } + self.pending_elems.clear(); + true + } +} + +impl<'a, 'b, 'c, 'd, 'e> Drop for TokenHandler<'a, 'b, 'c, 'd, 'e> { + /// When leaving, we need to flush all pending data to not have missing content. + fn drop(&mut self) { + if self.pending_exit_span.is_some() { + self.handle_exit_span(); + } else { + self.write_pending_elems(self.current_class); + } + } +} + /// Convert the given `src` source code into HTML by adding classes for highlighting. /// /// This code is used to render code blocks (in the documentation) as well as the source code pages. @@ -93,27 +227,74 @@ fn write_header(out: &mut Buffer, class: Option<&str>, extra_content: Option<Buf fn write_code( out: &mut Buffer, src: &str, - edition: Edition, href_context: Option<HrefContext<'_, '_, '_>>, decoration_info: Option<DecorationInfo>, ) { // This replace allows to fix how the code source with DOS backline characters is displayed. let src = src.replace("\r\n", "\n"); - let mut closing_tags: Vec<&'static str> = Vec::new(); + let mut token_handler = TokenHandler { + out, + closing_tags: Vec::new(), + pending_exit_span: None, + current_class: None, + pending_elems: Vec::new(), + href_context, + }; + Classifier::new( &src, - edition, - href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP), + token_handler.href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP), decoration_info, ) .highlight(&mut |highlight| { match highlight { - Highlight::Token { text, class } => string(out, Escape(text), class, &href_context), + Highlight::Token { text, class } => { + // If we received a `ExitSpan` event and then have a non-compatible `Class`, we + // need to close the `<span>`. + let need_current_class_update = if let Some(pending) = token_handler.pending_exit_span && + !can_merge(Some(pending), class, text) { + token_handler.handle_exit_span(); + true + // If the two `Class` are different, time to flush the current content and start + // a new one. + } else if !can_merge(token_handler.current_class, class, text) { + token_handler.write_pending_elems(token_handler.current_class); + true + } else { + token_handler.current_class.is_none() + }; + + if need_current_class_update { + token_handler.current_class = class.map(Class::dummy); + } + token_handler.pending_elems.push((text, class)); + } Highlight::EnterSpan { class } => { - closing_tags.push(enter_span(out, class, &href_context)) + let mut should_add = true; + if let Some(pending_exit_span) = token_handler.pending_exit_span { + if class.is_equal_to(pending_exit_span) { + should_add = false; + } else { + token_handler.handle_exit_span(); + } + } else { + // We flush everything just in case... + if token_handler.write_pending_elems(token_handler.current_class) { + token_handler.current_class = None; + } + } + if should_add { + let closing_tag = enter_span(token_handler.out, class, &token_handler.href_context); + token_handler.closing_tags.push((closing_tag, class)); + } + + token_handler.current_class = None; + token_handler.pending_exit_span = None; } Highlight::ExitSpan => { - exit_span(out, closing_tags.pop().expect("ExitSpan without EnterSpan")) + token_handler.current_class = None; + token_handler.pending_exit_span = + Some(token_handler.closing_tags.last().as_ref().expect("ExitSpan without EnterSpan").1); } }; }); @@ -130,15 +311,15 @@ enum Class { DocComment, Attribute, KeyWord, - // Keywords that do pointer/reference stuff. + /// Keywords that do pointer/reference stuff. RefKeyWord, Self_(Span), - Op, Macro(Span), MacroNonTerminal, String, Number, Bool, + /// `Ident` isn't rendered in the HTML but we still need it for the `Span` it contains. Ident(Span), Lifetime, PreludeTy, @@ -148,6 +329,31 @@ enum Class { } impl Class { + /// It is only looking at the variant, not the variant content. + /// + /// It is used mostly to group multiple similar HTML elements into one `<span>` instead of + /// multiple ones. + fn is_equal_to(self, other: Self) -> bool { + match (self, other) { + (Self::Self_(_), Self::Self_(_)) + | (Self::Macro(_), Self::Macro(_)) + | (Self::Ident(_), Self::Ident(_)) => true, + (Self::Decoration(c1), Self::Decoration(c2)) => c1 == c2, + (x, y) => x == y, + } + } + + /// If `self` contains a `Span`, it'll be replaced with `DUMMY_SP` to prevent creating links + /// on "empty content" (because of the attributes merge). + fn dummy(self) -> Self { + match self { + Self::Self_(_) => Self::Self_(DUMMY_SP), + Self::Macro(_) => Self::Macro(DUMMY_SP), + Self::Ident(_) => Self::Ident(DUMMY_SP), + s => s, + } + } + /// Returns the css class expected by rustdoc for each `Class`. fn as_html(self) -> &'static str { match self { @@ -157,13 +363,12 @@ impl Class { Class::KeyWord => "kw", Class::RefKeyWord => "kw-2", Class::Self_(_) => "self", - Class::Op => "op", Class::Macro(_) => "macro", Class::MacroNonTerminal => "macro-nonterminal", Class::String => "string", Class::Number => "number", Class::Bool => "bool-val", - Class::Ident(_) => "ident", + Class::Ident(_) => "", Class::Lifetime => "lifetime", Class::PreludeTy => "prelude-ty", Class::PreludeVal => "prelude-val", @@ -182,7 +387,6 @@ impl Class { | Self::Attribute | Self::KeyWord | Self::RefKeyWord - | Self::Op | Self::MacroNonTerminal | Self::String | Self::Number @@ -220,7 +424,7 @@ impl<'a> Iterator for TokenIter<'a> { } /// Classifies into identifier class; returns `None` if this is a non-keyword identifier. -fn get_real_ident_class(text: &str, edition: Edition, allow_path_keywords: bool) -> Option<Class> { +fn get_real_ident_class(text: &str, allow_path_keywords: bool) -> Option<Class> { let ignore: &[&str] = if allow_path_keywords { &["self", "Self", "super", "crate"] } else { &["self", "Self"] }; if ignore.iter().any(|k| *k == text) { @@ -229,7 +433,7 @@ fn get_real_ident_class(text: &str, edition: Edition, allow_path_keywords: bool) Some(match text { "ref" | "mut" => Class::RefKeyWord, "false" | "true" => Class::Bool, - _ if Symbol::intern(text).is_reserved(|| edition) => Class::KeyWord, + _ if Symbol::intern(text).is_reserved(|| Edition::Edition2021) => Class::KeyWord, _ => return None, }) } @@ -250,7 +454,7 @@ impl<'a> PeekIter<'a> { fn new(iter: TokenIter<'a>) -> Self { Self { stored: VecDeque::new(), peek_pos: 0, iter } } - /// Returns the next item after the current one. It doesn't interfer with `peek_next` output. + /// Returns the next item after the current one. It doesn't interfere with `peek_next` output. fn peek(&mut self) -> Option<&(TokenKind, &'a str)> { if self.stored.is_empty() { if let Some(next) = self.iter.next() { @@ -259,7 +463,7 @@ impl<'a> PeekIter<'a> { } self.stored.front() } - /// Returns the next item after the last one peeked. It doesn't interfer with `peek` output. + /// Returns the next item after the last one peeked. It doesn't interfere with `peek` output. fn peek_next(&mut self) -> Option<&(TokenKind, &'a str)> { self.peek_pos += 1; if self.peek_pos - 1 < self.stored.len() { @@ -311,7 +515,6 @@ struct Classifier<'a> { in_attribute: bool, in_macro: bool, in_macro_nonterminal: bool, - edition: Edition, byte_pos: u32, file_span: Span, src: &'a str, @@ -321,12 +524,7 @@ struct Classifier<'a> { impl<'a> Classifier<'a> { /// Takes as argument the source code to HTML-ify, the rust edition to use and the source code /// file span which will be used later on by the `span_correspondance_map`. - fn new( - src: &str, - edition: Edition, - file_span: Span, - decoration_info: Option<DecorationInfo>, - ) -> Classifier<'_> { + fn new(src: &str, file_span: Span, decoration_info: Option<DecorationInfo>) -> Classifier<'_> { let tokens = PeekIter::new(TokenIter { src }); let decorations = decoration_info.map(Decorations::new); Classifier { @@ -334,7 +532,6 @@ impl<'a> Classifier<'a> { in_attribute: false, in_macro: false, in_macro_nonterminal: false, - edition, byte_pos: 0, file_span, src, @@ -354,7 +551,6 @@ impl<'a> Classifier<'a> { let start = self.byte_pos as usize; let mut pos = start; let mut has_ident = false; - let edition = self.edition; loop { let mut nb = 0; @@ -376,7 +572,7 @@ impl<'a> Classifier<'a> { if let Some((None, text)) = self.tokens.peek().map(|(token, text)| { if *token == TokenKind::Ident { - let class = get_real_ident_class(text, edition, true); + let class = get_real_ident_class(text, true); (class, text) } else { // Doesn't matter which Class we put in here... @@ -494,7 +690,7 @@ impl<'a> Classifier<'a> { // or a reference or pointer type. Unless, of course, it looks like // a logical and or a multiplication operator: `&&` or `* `. TokenKind::Star => match self.tokens.peek() { - Some((TokenKind::Whitespace, _)) => Class::Op, + Some((TokenKind::Whitespace, _)) => return no_highlight(sink), Some((TokenKind::Ident, "mut")) => { self.next(); sink(Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) }); @@ -510,15 +706,15 @@ impl<'a> Classifier<'a> { TokenKind::And => match self.tokens.peek() { Some((TokenKind::And, _)) => { self.next(); - sink(Highlight::Token { text: "&&", class: Some(Class::Op) }); + sink(Highlight::Token { text: "&&", class: None }); return; } Some((TokenKind::Eq, _)) => { self.next(); - sink(Highlight::Token { text: "&=", class: Some(Class::Op) }); + sink(Highlight::Token { text: "&=", class: None }); return; } - Some((TokenKind::Whitespace, _)) => Class::Op, + Some((TokenKind::Whitespace, _)) => return no_highlight(sink), Some((TokenKind::Ident, "mut")) => { self.next(); sink(Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) }); @@ -531,7 +727,7 @@ impl<'a> Classifier<'a> { TokenKind::Eq => match lookahead { Some(TokenKind::Eq) => { self.next(); - sink(Highlight::Token { text: "==", class: Some(Class::Op) }); + sink(Highlight::Token { text: "==", class: None }); return; } Some(TokenKind::Gt) => { @@ -539,7 +735,7 @@ impl<'a> Classifier<'a> { sink(Highlight::Token { text: "=>", class: None }); return; } - _ => Class::Op, + _ => return no_highlight(sink), }, TokenKind::Minus if lookahead == Some(TokenKind::Gt) => { self.next(); @@ -556,7 +752,7 @@ impl<'a> Classifier<'a> { | TokenKind::Percent | TokenKind::Bang | TokenKind::Lt - | TokenKind::Gt => Class::Op, + | TokenKind::Gt => return no_highlight(sink), // Miscellaneous, no highlighting. TokenKind::Dot @@ -634,7 +830,7 @@ impl<'a> Classifier<'a> { sink(Highlight::Token { text, class: None }); return; } - TokenKind::Ident => match get_real_ident_class(text, self.edition, false) { + TokenKind::Ident => match get_real_ident_class(text, false) { None => match text { "Option" | "Result" => Class::PreludeTy, "Some" | "None" | "Ok" | "Err" => Class::PreludeVal, @@ -682,7 +878,7 @@ fn enter_span( klass: Class, href_context: &Option<HrefContext<'_, '_, '_>>, ) -> &'static str { - string_without_closing_tag(out, "", Some(klass), href_context).expect( + string_without_closing_tag(out, "", Some(klass), href_context, true).expect( "internal error: enter_span was called with Some(klass) but did not return a \ closing HTML tag", ) @@ -714,8 +910,10 @@ fn string<T: Display>( text: T, klass: Option<Class>, href_context: &Option<HrefContext<'_, '_, '_>>, + open_tag: bool, ) { - if let Some(closing_tag) = string_without_closing_tag(out, text, klass, href_context) { + if let Some(closing_tag) = string_without_closing_tag(out, text, klass, href_context, open_tag) + { out.write_str(closing_tag); } } @@ -734,6 +932,7 @@ fn string_without_closing_tag<T: Display>( text: T, klass: Option<Class>, href_context: &Option<HrefContext<'_, '_, '_>>, + open_tag: bool, ) -> Option<&'static str> { let Some(klass) = klass else { @@ -742,6 +941,10 @@ fn string_without_closing_tag<T: Display>( }; let Some(def_span) = klass.get_span() else { + if !open_tag { + write!(out, "{}", text); + return None; + } write!(out, "<span class=\"{}\">{}", klass.as_html(), text); return Some("</span>"); }; @@ -765,6 +968,7 @@ fn string_without_closing_tag<T: Display>( path }); } + if let Some(href_context) = href_context { if let Some(href) = href_context.context.shared.span_correspondance_map.get(&def_span).and_then(|href| { @@ -775,9 +979,9 @@ fn string_without_closing_tag<T: Display>( // a link to their definition can be generated using this: // https://github.com/rust-lang/rust/blob/60f1a2fc4b535ead9c85ce085fdce49b1b097531/src/librustdoc/html/render/context.rs#L315-L338 match href { - LinkFromSrc::Local(span) => context - .href_from_span(*span, true) - .map(|s| format!("{}{}", href_context.root_path, s)), + LinkFromSrc::Local(span) => { + context.href_from_span_relative(*span, href_context.current_href) + } LinkFromSrc::External(def_id) => { format::href_with_root_path(*def_id, context, Some(href_context.root_path)) .ok() @@ -793,12 +997,33 @@ fn string_without_closing_tag<T: Display>( } }) { - write!(out, "<a class=\"{}\" href=\"{}\">{}", klass.as_html(), href, text_s); + if !open_tag { + // We're already inside an element which has the same klass, no need to give it + // again. + write!(out, "<a href=\"{}\">{}", href, text_s); + } else { + let klass_s = klass.as_html(); + if klass_s.is_empty() { + write!(out, "<a href=\"{}\">{}", href, text_s); + } else { + write!(out, "<a class=\"{}\" href=\"{}\">{}", klass_s, href, text_s); + } + } return Some("</a>"); } } - write!(out, "<span class=\"{}\">{}", klass.as_html(), text_s); - Some("</span>") + if !open_tag { + write!(out, "{}", text_s); + return None; + } + let klass_s = klass.as_html(); + if klass_s.is_empty() { + write!(out, "{}", text_s); + Some("") + } else { + write!(out, "<span class=\"{}\">{}", klass_s, text_s); + Some("</span>") + } } #[cfg(test)] |