diff options
| author | Guillaume Gomez <guillaume.gomez@huawei.com> | 2022-08-11 23:03:45 +0200 |
|---|---|---|
| committer | Guillaume Gomez <guillaume.gomez@huawei.com> | 2022-08-14 11:30:52 +0200 |
| commit | 68f327bcc112ffb4261980c04e58d333b5d97b6e (patch) | |
| tree | b8703d7a311f8689ecd5cc466216dd1903dbe936 | |
| parent | 2fbc08e2ce64dee45a29cb6133da6b32366268aa (diff) | |
| download | rust-68f327bcc112ffb4261980c04e58d333b5d97b6e.tar.gz rust-68f327bcc112ffb4261980c04e58d333b5d97b6e.zip | |
Merge HTML elements in highlighting when they can be merged together
| -rw-r--r-- | src/librustdoc/html/highlight.rs | 163 |
1 files changed, 155 insertions, 8 deletions
diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index 27ccff9a276..517f441e647 100644 --- a/src/librustdoc/html/highlight.rs +++ b/src/librustdoc/html/highlight.rs @@ -111,6 +111,69 @@ fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>) { write!(out, "<code>"); } +/// 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. +fn write_pending_elems( + out: &mut Buffer, + href_context: &Option<HrefContext<'_, '_, '_>>, + pending_elems: &mut Vec<(&str, Option<Class>)>, + current_class: &mut Option<Class>, + closing_tags: &[(&str, Class)], +) { + if pending_elems.is_empty() { + return; + } + let mut done = false; + if let Some((_, parent_class)) = closing_tags.last() { + if can_merge(*current_class, Some(*parent_class), "") { + for (text, class) in pending_elems.iter() { + string(out, Escape(text), *class, &href_context, false); + } + done = true; + } + } + if !done { + // 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 open_tag_ourselves = pending_elems.len() > 1; + let close_tag = if open_tag_ourselves { + enter_span(out, current_class.unwrap(), &href_context) + } else { + "" + }; + for (text, class) in pending_elems.iter() { + string(out, Escape(text), *class, &href_context, !open_tag_ourselves); + } + if open_tag_ourselves { + exit_span(out, close_tag); + } + } + pending_elems.clear(); + *current_class = None; +} + +/// 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. +/// * If `Class` is `Ident`, then it can be merged with all unclassified elements. +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(), + _ => false, + } +} + /// 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. @@ -130,7 +193,15 @@ fn write_code( ) { // 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(); + // It contains the closing tag and the associated `Class`. + let mut closing_tags: Vec<(&'static str, Class)> = Vec::new(); + // The following two variables are used to group HTML elements with same `class` attributes + // to reduce the DOM size. + let mut current_class: Option<Class> = None; + // We need to keep the `Class` for each element because it could contain a `Span` which is + // used to generate links. + let mut pending_elems: Vec<(&str, Option<Class>)> = Vec::new(); + Classifier::new( &src, href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP), @@ -138,15 +209,48 @@ fn write_code( ) .highlight(&mut |highlight| { match highlight { - Highlight::Token { text, class } => string(out, Escape(text), class, &href_context), + Highlight::Token { text, class } => { + // If the two `Class` are different, time to flush the current content and start + // a new one. + if !can_merge(current_class, class, text) { + write_pending_elems( + out, + &href_context, + &mut pending_elems, + &mut current_class, + &closing_tags, + ); + current_class = class.map(Class::dummy); + } else if current_class.is_none() { + current_class = class.map(Class::dummy); + } + pending_elems.push((text, class)); + } Highlight::EnterSpan { class } => { - closing_tags.push(enter_span(out, class, &href_context)) + // We flush everything just in case... + write_pending_elems( + out, + &href_context, + &mut pending_elems, + &mut current_class, + &closing_tags, + ); + closing_tags.push((enter_span(out, class, &href_context), class)) } Highlight::ExitSpan => { - exit_span(out, closing_tags.pop().expect("ExitSpan without EnterSpan")) + // We flush everything just in case... + write_pending_elems( + out, + &href_context, + &mut pending_elems, + &mut current_class, + &closing_tags, + ); + exit_span(out, closing_tags.pop().expect("ExitSpan without EnterSpan").0) } }; }); + write_pending_elems(out, &href_context, &mut pending_elems, &mut current_class, &closing_tags); } fn write_footer(out: &mut Buffer, playground_button: Option<&str>) { @@ -177,6 +281,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(_)) + | (Self::Decoration(_), Self::Decoration(_)) => true, + (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 { @@ -630,7 +759,7 @@ impl<'a> Classifier<'a> { TokenKind::CloseBracket => { if self.in_attribute { self.in_attribute = false; - sink(Highlight::Token { text: "]", class: None }); + sink(Highlight::Token { text: "]", class: Some(Class::Attribute) }); sink(Highlight::ExitSpan); return; } @@ -701,7 +830,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", ) @@ -733,8 +862,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); } } @@ -753,6 +884,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 { @@ -761,6 +893,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>"); }; @@ -784,6 +920,7 @@ fn string_without_closing_tag<T: Display>( path }); } + // We don't want to generate links on empty text. if let Some(href_context) = href_context { if let Some(href) = href_context.context.shared.span_correspondance_map.get(&def_span).and_then(|href| { @@ -812,10 +949,20 @@ 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 { + write!(out, "<a class=\"{}\" href=\"{}\">{}", klass.as_html(), href, text_s); + } return Some("</a>"); } } + if !open_tag { + write!(out, "{}", text_s); + return None; + } write!(out, "<span class=\"{}\">{}", klass.as_html(), text_s); Some("</span>") } |
