@@ -111,6 +111,69 @@ 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+ /// * If `Class` is `Ident`, then it can be merged with all unclassified elements.
168+ fn can_merge ( class1 : Option < Class > , class2 : Option < Class > , text : & str ) -> bool {
169+ match ( class1, class2) {
170+ ( Some ( c1) , Some ( c2) ) => c1. is_equal_to ( c2) ,
171+ ( Some ( Class :: Ident ( _) ) , None ) | ( None , Some ( Class :: Ident ( _) ) ) => true ,
172+ ( Some ( _) , None ) | ( None , Some ( _) ) => text. trim ( ) . is_empty ( ) ,
173+ _ => false ,
174+ }
175+ }
176+
114177/// Convert the given `src` source code into HTML by adding classes for highlighting.
115178///
116179/// This code is used to render code blocks (in the documentation) as well as the source code pages.
@@ -130,23 +193,64 @@ fn write_code(
130193) {
131194 // This replace allows to fix how the code source with DOS backline characters is displayed.
132195 let src = src. replace ( "\r \n " , "\n " ) ;
133- let mut closing_tags: Vec < & ' static str > = Vec :: new ( ) ;
196+ // It contains the closing tag and the associated `Class`.
197+ let mut closing_tags: Vec < ( & ' static str , Class ) > = Vec :: new ( ) ;
198+ // The following two variables are used to group HTML elements with same `class` attributes
199+ // to reduce the DOM size.
200+ let mut current_class: Option < Class > = None ;
201+ // We need to keep the `Class` for each element because it could contain a `Span` which is
202+ // used to generate links.
203+ let mut pending_elems: Vec < ( & str , Option < Class > ) > = Vec :: new ( ) ;
204+
134205 Classifier :: new (
135206 & src,
136207 href_context. as_ref ( ) . map ( |c| c. file_span ) . unwrap_or ( DUMMY_SP ) ,
137208 decoration_info,
138209 )
139210 . highlight ( & mut |highlight| {
140211 match highlight {
141- Highlight :: Token { text, class } => string ( out, Escape ( text) , class, & href_context) ,
212+ Highlight :: Token { text, class } => {
213+ // If the two `Class` are different, time to flush the current content and start
214+ // a new one.
215+ if !can_merge ( current_class, class, text) {
216+ write_pending_elems (
217+ out,
218+ & href_context,
219+ & mut pending_elems,
220+ & mut current_class,
221+ & closing_tags,
222+ ) ;
223+ current_class = class. map ( Class :: dummy) ;
224+ } else if current_class. is_none ( ) {
225+ current_class = class. map ( Class :: dummy) ;
226+ }
227+ pending_elems. push ( ( text, class) ) ;
228+ }
142229 Highlight :: EnterSpan { class } => {
143- closing_tags. push ( enter_span ( out, class, & href_context) )
230+ // We flush everything just in case...
231+ write_pending_elems (
232+ out,
233+ & href_context,
234+ & mut pending_elems,
235+ & mut current_class,
236+ & closing_tags,
237+ ) ;
238+ closing_tags. push ( ( enter_span ( out, class, & href_context) , class) )
144239 }
145240 Highlight :: ExitSpan => {
146- exit_span ( out, closing_tags. pop ( ) . expect ( "ExitSpan without EnterSpan" ) )
241+ // We flush everything just in case...
242+ write_pending_elems (
243+ out,
244+ & href_context,
245+ & mut pending_elems,
246+ & mut current_class,
247+ & closing_tags,
248+ ) ;
249+ exit_span ( out, closing_tags. pop ( ) . expect ( "ExitSpan without EnterSpan" ) . 0 )
147250 }
148251 } ;
149252 } ) ;
253+ write_pending_elems ( out, & href_context, & mut pending_elems, & mut current_class, & closing_tags) ;
150254}
151255
152256fn write_footer ( out : & mut Buffer , playground_button : Option < & str > ) {
@@ -177,6 +281,31 @@ enum Class {
177281}
178282
179283impl Class {
284+ /// It is only looking at the variant, not the variant content.
285+ ///
286+ /// It is used mostly to group multiple similar HTML elements into one `<span>` instead of
287+ /// multiple ones.
288+ fn is_equal_to ( self , other : Self ) -> bool {
289+ match ( self , other) {
290+ ( Self :: Self_ ( _) , Self :: Self_ ( _) )
291+ | ( Self :: Macro ( _) , Self :: Macro ( _) )
292+ | ( Self :: Ident ( _) , Self :: Ident ( _) )
293+ | ( Self :: Decoration ( _) , Self :: Decoration ( _) ) => true ,
294+ ( x, y) => x == y,
295+ }
296+ }
297+
298+ /// If `self` contains a `Span`, it'll be replaced with `DUMMY_SP` to prevent creating links
299+ /// on "empty content" (because of the attributes merge).
300+ fn dummy ( self ) -> Self {
301+ match self {
302+ Self :: Self_ ( _) => Self :: Self_ ( DUMMY_SP ) ,
303+ Self :: Macro ( _) => Self :: Macro ( DUMMY_SP ) ,
304+ Self :: Ident ( _) => Self :: Ident ( DUMMY_SP ) ,
305+ s => s,
306+ }
307+ }
308+
180309 /// Returns the css class expected by rustdoc for each `Class`.
181310 fn as_html ( self ) -> & ' static str {
182311 match self {
@@ -630,7 +759,7 @@ impl<'a> Classifier<'a> {
630759 TokenKind :: CloseBracket => {
631760 if self . in_attribute {
632761 self . in_attribute = false ;
633- sink ( Highlight :: Token { text : "]" , class : None } ) ;
762+ sink ( Highlight :: Token { text : "]" , class : Some ( Class :: Attribute ) } ) ;
634763 sink ( Highlight :: ExitSpan ) ;
635764 return ;
636765 }
@@ -701,7 +830,7 @@ fn enter_span(
701830 klass : Class ,
702831 href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
703832) -> & ' static str {
704- string_without_closing_tag ( out, "" , Some ( klass) , href_context) . expect (
833+ string_without_closing_tag ( out, "" , Some ( klass) , href_context, true ) . expect (
705834 "internal error: enter_span was called with Some(klass) but did not return a \
706835 closing HTML tag",
707836 )
@@ -733,8 +862,10 @@ fn string<T: Display>(
733862 text : T ,
734863 klass : Option < Class > ,
735864 href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
865+ open_tag : bool ,
736866) {
737- if let Some ( closing_tag) = string_without_closing_tag ( out, text, klass, href_context) {
867+ if let Some ( closing_tag) = string_without_closing_tag ( out, text, klass, href_context, open_tag)
868+ {
738869 out. write_str ( closing_tag) ;
739870 }
740871}
@@ -753,6 +884,7 @@ fn string_without_closing_tag<T: Display>(
753884 text : T ,
754885 klass : Option < Class > ,
755886 href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
887+ open_tag : bool ,
756888) -> Option < & ' static str > {
757889 let Some ( klass) = klass
758890 else {
@@ -761,6 +893,10 @@ fn string_without_closing_tag<T: Display>(
761893 } ;
762894 let Some ( def_span) = klass. get_span ( )
763895 else {
896+ if !open_tag {
897+ write ! ( out, "{}" , text) ;
898+ return None ;
899+ }
764900 write ! ( out, "<span class=\" {}\" >{}" , klass. as_html( ) , text) ;
765901 return Some ( "</span>" ) ;
766902 } ;
@@ -784,6 +920,7 @@ fn string_without_closing_tag<T: Display>(
784920 path
785921 } ) ;
786922 }
923+ // We don't want to generate links on empty text.
787924 if let Some ( href_context) = href_context {
788925 if let Some ( href) =
789926 href_context. context . shared . span_correspondance_map . get ( & def_span) . and_then ( |href| {
@@ -812,10 +949,20 @@ fn string_without_closing_tag<T: Display>(
812949 }
813950 } )
814951 {
815- write ! ( out, "<a class=\" {}\" href=\" {}\" >{}" , klass. as_html( ) , href, text_s) ;
952+ if !open_tag {
953+ // We're already inside an element which has the same klass, no need to give it
954+ // again.
955+ write ! ( out, "<a href=\" {}\" >{}" , href, text_s) ;
956+ } else {
957+ write ! ( out, "<a class=\" {}\" href=\" {}\" >{}" , klass. as_html( ) , href, text_s) ;
958+ }
816959 return Some ( "</a>" ) ;
817960 }
818961 }
962+ if !open_tag {
963+ write ! ( out, "{}" , text_s) ;
964+ return None ;
965+ }
819966 write ! ( out, "<span class=\" {}\" >{}" , klass. as_html( ) , text_s) ;
820967 Some ( "</span>" )
821968}
0 commit comments