@@ -111,6 +111,69 @@ fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>) {
111
111
write ! ( out, "<code>" ) ;
112
112
}
113
113
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
+
114
177
/// Convert the given `src` source code into HTML by adding classes for highlighting.
115
178
///
116
179
/// 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(
130
193
) {
131
194
// This replace allows to fix how the code source with DOS backline characters is displayed.
132
195
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
+
134
205
Classifier :: new (
135
206
& src,
136
207
href_context. as_ref ( ) . map ( |c| c. file_span ) . unwrap_or ( DUMMY_SP ) ,
137
208
decoration_info,
138
209
)
139
210
. highlight ( & mut |highlight| {
140
211
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
+ }
142
229
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) )
144
239
}
145
240
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 )
147
250
}
148
251
} ;
149
252
} ) ;
253
+ write_pending_elems ( out, & href_context, & mut pending_elems, & mut current_class, & closing_tags) ;
150
254
}
151
255
152
256
fn write_footer ( out : & mut Buffer , playground_button : Option < & str > ) {
@@ -177,6 +281,31 @@ enum Class {
177
281
}
178
282
179
283
impl 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
+
180
309
/// Returns the css class expected by rustdoc for each `Class`.
181
310
fn as_html ( self ) -> & ' static str {
182
311
match self {
@@ -630,7 +759,7 @@ impl<'a> Classifier<'a> {
630
759
TokenKind :: CloseBracket => {
631
760
if self . in_attribute {
632
761
self . in_attribute = false ;
633
- sink ( Highlight :: Token { text : "]" , class : None } ) ;
762
+ sink ( Highlight :: Token { text : "]" , class : Some ( Class :: Attribute ) } ) ;
634
763
sink ( Highlight :: ExitSpan ) ;
635
764
return ;
636
765
}
@@ -701,7 +830,7 @@ fn enter_span(
701
830
klass : Class ,
702
831
href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
703
832
) -> & ' 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 (
705
834
"internal error: enter_span was called with Some(klass) but did not return a \
706
835
closing HTML tag",
707
836
)
@@ -733,8 +862,10 @@ fn string<T: Display>(
733
862
text : T ,
734
863
klass : Option < Class > ,
735
864
href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
865
+ open_tag : bool ,
736
866
) {
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
+ {
738
869
out. write_str ( closing_tag) ;
739
870
}
740
871
}
@@ -753,6 +884,7 @@ fn string_without_closing_tag<T: Display>(
753
884
text : T ,
754
885
klass : Option < Class > ,
755
886
href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
887
+ open_tag : bool ,
756
888
) -> Option < & ' static str > {
757
889
let Some ( klass) = klass
758
890
else {
@@ -761,6 +893,10 @@ fn string_without_closing_tag<T: Display>(
761
893
} ;
762
894
let Some ( def_span) = klass. get_span ( )
763
895
else {
896
+ if !open_tag {
897
+ write ! ( out, "{}" , text) ;
898
+ return None ;
899
+ }
764
900
write ! ( out, "<span class=\" {}\" >{}" , klass. as_html( ) , text) ;
765
901
return Some ( "</span>" ) ;
766
902
} ;
@@ -784,6 +920,7 @@ fn string_without_closing_tag<T: Display>(
784
920
path
785
921
} ) ;
786
922
}
923
+ // We don't want to generate links on empty text.
787
924
if let Some ( href_context) = href_context {
788
925
if let Some ( href) =
789
926
href_context. context . shared . span_correspondance_map . get ( & def_span) . and_then ( |href| {
@@ -812,10 +949,20 @@ fn string_without_closing_tag<T: Display>(
812
949
}
813
950
} )
814
951
{
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
+ }
816
959
return Some ( "</a>" ) ;
817
960
}
818
961
}
962
+ if !open_tag {
963
+ write ! ( out, "{}" , text_s) ;
964
+ return None ;
965
+ }
819
966
write ! ( out, "<span class=\" {}\" >{}" , klass. as_html( ) , text_s) ;
820
967
Some ( "</span>" )
821
968
}
0 commit comments