@@ -111,6 +111,70 @@ fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>) {
111111 write ! ( out, "<code>" ) ;
112112}
113113
114+ /// Write all the pending elements sharing a same (or at mergeable) `Class`.
115+ ///
116+ /// If there is a "parent" (if a `EnterSpan` event was encountered) and the parent can be merged
117+ /// with the elements' class, then we simply write the elements since the `ExitSpan` event will
118+ /// close the tag.
119+ ///
120+ /// Otherwise, if there is only one pending element, we let the `string` function handle both
121+ /// opening and closing the tag, otherwise we do it into this function.
122+ fn write_pending_elems (
123+ out : & mut Buffer ,
124+ href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
125+ pending_elems : & mut Vec < ( & str , Option < Class > ) > ,
126+ current_class : & mut Option < Class > ,
127+ closing_tags : & [ ( & str , Class ) ] ,
128+ ) {
129+ if pending_elems. is_empty ( ) {
130+ return ;
131+ }
132+ let mut done = false ;
133+ if let Some ( ( _, parent_class) ) = closing_tags. last ( ) {
134+ if can_merge ( * current_class, Some ( * parent_class) , "" ) {
135+ for ( text, class) in pending_elems. iter ( ) {
136+ string ( out, Escape ( text) , * class, & href_context, false ) ;
137+ }
138+ done = true ;
139+ }
140+ }
141+ if !done {
142+ // We only want to "open" the tag ourselves if we have more than one pending and if the current
143+ // parent tag is not the same as our pending content.
144+ let open_tag_ourselves = pending_elems. len ( ) > 1 ;
145+ let close_tag = if open_tag_ourselves {
146+ enter_span ( out, current_class. unwrap ( ) , & href_context)
147+ } else {
148+ ""
149+ } ;
150+ for ( text, class) in pending_elems. iter ( ) {
151+ string ( out, Escape ( text) , * class, & href_context, !open_tag_ourselves) ;
152+ }
153+ if open_tag_ourselves {
154+ exit_span ( out, close_tag) ;
155+ }
156+ }
157+ pending_elems. clear ( ) ;
158+ * current_class = None ;
159+ }
160+
161+ /// Check if two `Class` can be merged together. In the following rules, "unclassified" means `None`
162+ /// basically (since it's `Option<Class>`). The following rules apply:
163+ ///
164+ /// * If two `Class` have the same variant, then they can be merged.
165+ /// * If the other `Class` is unclassified and only contains white characters (backline,
166+ /// whitespace, etc), it can be merged.
167+ /// * `Class::Ident` is considered the same as unclassified (because it doesn't have an associated
168+ /// CSS class).
169+ fn can_merge ( class1 : Option < Class > , class2 : Option < Class > , text : & str ) -> bool {
170+ match ( class1, class2) {
171+ ( Some ( c1) , Some ( c2) ) => c1. is_equal_to ( c2) ,
172+ ( Some ( Class :: Ident ( _) ) , None ) | ( None , Some ( Class :: Ident ( _) ) ) => true ,
173+ ( Some ( _) , None ) | ( None , Some ( _) ) => text. trim ( ) . is_empty ( ) ,
174+ _ => false ,
175+ }
176+ }
177+
114178/// Convert the given `src` source code into HTML by adding classes for highlighting.
115179///
116180/// This code is used to render code blocks (in the documentation) as well as the source code pages.
@@ -130,23 +194,64 @@ fn write_code(
130194) {
131195 // This replace allows to fix how the code source with DOS backline characters is displayed.
132196 let src = src. replace ( "\r \n " , "\n " ) ;
133- let mut closing_tags: Vec < & ' static str > = Vec :: new ( ) ;
197+ // It contains the closing tag and the associated `Class`.
198+ let mut closing_tags: Vec < ( & ' static str , Class ) > = Vec :: new ( ) ;
199+ // The following two variables are used to group HTML elements with same `class` attributes
200+ // to reduce the DOM size.
201+ let mut current_class: Option < Class > = None ;
202+ // We need to keep the `Class` for each element because it could contain a `Span` which is
203+ // used to generate links.
204+ let mut pending_elems: Vec < ( & str , Option < Class > ) > = Vec :: new ( ) ;
205+
134206 Classifier :: new (
135207 & src,
136208 href_context. as_ref ( ) . map ( |c| c. file_span ) . unwrap_or ( DUMMY_SP ) ,
137209 decoration_info,
138210 )
139211 . highlight ( & mut |highlight| {
140212 match highlight {
141- Highlight :: Token { text, class } => string ( out, Escape ( text) , class, & href_context) ,
213+ Highlight :: Token { text, class } => {
214+ // If the two `Class` are different, time to flush the current content and start
215+ // a new one.
216+ if !can_merge ( current_class, class, text) {
217+ write_pending_elems (
218+ out,
219+ & href_context,
220+ & mut pending_elems,
221+ & mut current_class,
222+ & closing_tags,
223+ ) ;
224+ current_class = class. map ( Class :: dummy) ;
225+ } else if current_class. is_none ( ) {
226+ current_class = class. map ( Class :: dummy) ;
227+ }
228+ pending_elems. push ( ( text, class) ) ;
229+ }
142230 Highlight :: EnterSpan { class } => {
143- closing_tags. push ( enter_span ( out, class, & href_context) )
231+ // We flush everything just in case...
232+ write_pending_elems (
233+ out,
234+ & href_context,
235+ & mut pending_elems,
236+ & mut current_class,
237+ & closing_tags,
238+ ) ;
239+ closing_tags. push ( ( enter_span ( out, class, & href_context) , class) )
144240 }
145241 Highlight :: ExitSpan => {
146- exit_span ( out, closing_tags. pop ( ) . expect ( "ExitSpan without EnterSpan" ) )
242+ // We flush everything just in case...
243+ write_pending_elems (
244+ out,
245+ & href_context,
246+ & mut pending_elems,
247+ & mut current_class,
248+ & closing_tags,
249+ ) ;
250+ exit_span ( out, closing_tags. pop ( ) . expect ( "ExitSpan without EnterSpan" ) . 0 )
147251 }
148252 } ;
149253 } ) ;
254+ write_pending_elems ( out, & href_context, & mut pending_elems, & mut current_class, & closing_tags) ;
150255}
151256
152257fn write_footer ( out : & mut Buffer , playground_button : Option < & str > ) {
@@ -160,14 +265,15 @@ enum Class {
160265 DocComment ,
161266 Attribute ,
162267 KeyWord ,
163- // Keywords that do pointer/reference stuff.
268+ /// Keywords that do pointer/reference stuff.
164269 RefKeyWord ,
165270 Self_ ( Span ) ,
166271 Macro ( Span ) ,
167272 MacroNonTerminal ,
168273 String ,
169274 Number ,
170275 Bool ,
276+ /// `Ident` isn't rendered in the HTML but we still need it for the `Span` it contains.
171277 Ident ( Span ) ,
172278 Lifetime ,
173279 PreludeTy ,
@@ -177,6 +283,31 @@ enum Class {
177283}
178284
179285impl Class {
286+ /// It is only looking at the variant, not the variant content.
287+ ///
288+ /// It is used mostly to group multiple similar HTML elements into one `<span>` instead of
289+ /// multiple ones.
290+ fn is_equal_to ( self , other : Self ) -> bool {
291+ match ( self , other) {
292+ ( Self :: Self_ ( _) , Self :: Self_ ( _) )
293+ | ( Self :: Macro ( _) , Self :: Macro ( _) )
294+ | ( Self :: Ident ( _) , Self :: Ident ( _) )
295+ | ( Self :: Decoration ( _) , Self :: Decoration ( _) ) => true ,
296+ ( x, y) => x == y,
297+ }
298+ }
299+
300+ /// If `self` contains a `Span`, it'll be replaced with `DUMMY_SP` to prevent creating links
301+ /// on "empty content" (because of the attributes merge).
302+ fn dummy ( self ) -> Self {
303+ match self {
304+ Self :: Self_ ( _) => Self :: Self_ ( DUMMY_SP ) ,
305+ Self :: Macro ( _) => Self :: Macro ( DUMMY_SP ) ,
306+ Self :: Ident ( _) => Self :: Ident ( DUMMY_SP ) ,
307+ s => s,
308+ }
309+ }
310+
180311 /// Returns the css class expected by rustdoc for each `Class`.
181312 fn as_html ( self ) -> & ' static str {
182313 match self {
@@ -191,7 +322,7 @@ impl Class {
191322 Class :: String => "string" ,
192323 Class :: Number => "number" ,
193324 Class :: Bool => "bool-val" ,
194- Class :: Ident ( _) => "ident " ,
325+ Class :: Ident ( _) => "" ,
195326 Class :: Lifetime => "lifetime" ,
196327 Class :: PreludeTy => "prelude-ty" ,
197328 Class :: PreludeVal => "prelude-val" ,
@@ -630,7 +761,7 @@ impl<'a> Classifier<'a> {
630761 TokenKind :: CloseBracket => {
631762 if self . in_attribute {
632763 self . in_attribute = false ;
633- sink ( Highlight :: Token { text : "]" , class : None } ) ;
764+ sink ( Highlight :: Token { text : "]" , class : Some ( Class :: Attribute ) } ) ;
634765 sink ( Highlight :: ExitSpan ) ;
635766 return ;
636767 }
@@ -701,7 +832,7 @@ fn enter_span(
701832 klass : Class ,
702833 href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
703834) -> & ' static str {
704- string_without_closing_tag ( out, "" , Some ( klass) , href_context) . expect (
835+ string_without_closing_tag ( out, "" , Some ( klass) , href_context, true ) . expect (
705836 "internal error: enter_span was called with Some(klass) but did not return a \
706837 closing HTML tag",
707838 )
@@ -733,8 +864,10 @@ fn string<T: Display>(
733864 text : T ,
734865 klass : Option < Class > ,
735866 href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
867+ open_tag : bool ,
736868) {
737- if let Some ( closing_tag) = string_without_closing_tag ( out, text, klass, href_context) {
869+ if let Some ( closing_tag) = string_without_closing_tag ( out, text, klass, href_context, open_tag)
870+ {
738871 out. write_str ( closing_tag) ;
739872 }
740873}
@@ -753,6 +886,7 @@ fn string_without_closing_tag<T: Display>(
753886 text : T ,
754887 klass : Option < Class > ,
755888 href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
889+ open_tag : bool ,
756890) -> Option < & ' static str > {
757891 let Some ( klass) = klass
758892 else {
@@ -761,6 +895,10 @@ fn string_without_closing_tag<T: Display>(
761895 } ;
762896 let Some ( def_span) = klass. get_span ( )
763897 else {
898+ if !open_tag {
899+ write ! ( out, "{}" , text) ;
900+ return None ;
901+ }
764902 write ! ( out, "<span class=\" {}\" >{}" , klass. as_html( ) , text) ;
765903 return Some ( "</span>" ) ;
766904 } ;
@@ -784,6 +922,7 @@ fn string_without_closing_tag<T: Display>(
784922 path
785923 } ) ;
786924 }
925+
787926 if let Some ( href_context) = href_context {
788927 if let Some ( href) =
789928 href_context. context . shared . span_correspondance_map . get ( & def_span) . and_then ( |href| {
@@ -812,12 +951,33 @@ fn string_without_closing_tag<T: Display>(
812951 }
813952 } )
814953 {
815- write ! ( out, "<a class=\" {}\" href=\" {}\" >{}" , klass. as_html( ) , href, text_s) ;
954+ if !open_tag {
955+ // We're already inside an element which has the same klass, no need to give it
956+ // again.
957+ write ! ( out, "<a href=\" {}\" >{}" , href, text_s) ;
958+ } else {
959+ let klass_s = klass. as_html ( ) ;
960+ if klass_s. is_empty ( ) {
961+ write ! ( out, "<a href=\" {}\" >{}" , href, text_s) ;
962+ } else {
963+ write ! ( out, "<a class=\" {}\" href=\" {}\" >{}" , klass_s, href, text_s) ;
964+ }
965+ }
816966 return Some ( "</a>" ) ;
817967 }
818968 }
819- write ! ( out, "<span class=\" {}\" >{}" , klass. as_html( ) , text_s) ;
820- Some ( "</span>" )
969+ if !open_tag {
970+ write ! ( out, "{}" , text_s) ;
971+ return None ;
972+ }
973+ let klass_s = klass. as_html ( ) ;
974+ if klass_s. is_empty ( ) {
975+ write ! ( out, "{}" , text_s) ;
976+ Some ( "" )
977+ } else {
978+ write ! ( out, "<span class=\" {}\" >{}" , klass_s, text_s) ;
979+ Some ( "</span>" )
980+ }
821981}
822982
823983#[ cfg( test) ]
0 commit comments