@@ -111,6 +111,70 @@ 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
+ /// * `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
+
114
178
/// Convert the given `src` source code into HTML by adding classes for highlighting.
115
179
///
116
180
/// 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(
130
194
) {
131
195
// This replace allows to fix how the code source with DOS backline characters is displayed.
132
196
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
+
134
206
Classifier :: new (
135
207
& src,
136
208
href_context. as_ref ( ) . map ( |c| c. file_span ) . unwrap_or ( DUMMY_SP ) ,
137
209
decoration_info,
138
210
)
139
211
. highlight ( & mut |highlight| {
140
212
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
+ }
142
230
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) )
144
240
}
145
241
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 )
147
251
}
148
252
} ;
149
253
} ) ;
254
+ write_pending_elems ( out, & href_context, & mut pending_elems, & mut current_class, & closing_tags) ;
150
255
}
151
256
152
257
fn write_footer ( out : & mut Buffer , playground_button : Option < & str > ) {
@@ -160,14 +265,15 @@ enum Class {
160
265
DocComment ,
161
266
Attribute ,
162
267
KeyWord ,
163
- // Keywords that do pointer/reference stuff.
268
+ /// Keywords that do pointer/reference stuff.
164
269
RefKeyWord ,
165
270
Self_ ( Span ) ,
166
271
Macro ( Span ) ,
167
272
MacroNonTerminal ,
168
273
String ,
169
274
Number ,
170
275
Bool ,
276
+ /// `Ident` isn't rendered in the HTML but we still need it for the `Span` it contains.
171
277
Ident ( Span ) ,
172
278
Lifetime ,
173
279
PreludeTy ,
@@ -177,6 +283,31 @@ enum Class {
177
283
}
178
284
179
285
impl 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
+
180
311
/// Returns the css class expected by rustdoc for each `Class`.
181
312
fn as_html ( self ) -> & ' static str {
182
313
match self {
@@ -191,7 +322,7 @@ impl Class {
191
322
Class :: String => "string" ,
192
323
Class :: Number => "number" ,
193
324
Class :: Bool => "bool-val" ,
194
- Class :: Ident ( _) => "ident " ,
325
+ Class :: Ident ( _) => "" ,
195
326
Class :: Lifetime => "lifetime" ,
196
327
Class :: PreludeTy => "prelude-ty" ,
197
328
Class :: PreludeVal => "prelude-val" ,
@@ -630,7 +761,7 @@ impl<'a> Classifier<'a> {
630
761
TokenKind :: CloseBracket => {
631
762
if self . in_attribute {
632
763
self . in_attribute = false ;
633
- sink ( Highlight :: Token { text : "]" , class : None } ) ;
764
+ sink ( Highlight :: Token { text : "]" , class : Some ( Class :: Attribute ) } ) ;
634
765
sink ( Highlight :: ExitSpan ) ;
635
766
return ;
636
767
}
@@ -701,7 +832,7 @@ fn enter_span(
701
832
klass : Class ,
702
833
href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
703
834
) -> & ' 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 (
705
836
"internal error: enter_span was called with Some(klass) but did not return a \
706
837
closing HTML tag",
707
838
)
@@ -733,8 +864,10 @@ fn string<T: Display>(
733
864
text : T ,
734
865
klass : Option < Class > ,
735
866
href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
867
+ open_tag : bool ,
736
868
) {
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
+ {
738
871
out. write_str ( closing_tag) ;
739
872
}
740
873
}
@@ -753,6 +886,7 @@ fn string_without_closing_tag<T: Display>(
753
886
text : T ,
754
887
klass : Option < Class > ,
755
888
href_context : & Option < HrefContext < ' _ , ' _ , ' _ > > ,
889
+ open_tag : bool ,
756
890
) -> Option < & ' static str > {
757
891
let Some ( klass) = klass
758
892
else {
@@ -761,6 +895,10 @@ fn string_without_closing_tag<T: Display>(
761
895
} ;
762
896
let Some ( def_span) = klass. get_span ( )
763
897
else {
898
+ if !open_tag {
899
+ write ! ( out, "{}" , text) ;
900
+ return None ;
901
+ }
764
902
write ! ( out, "<span class=\" {}\" >{}" , klass. as_html( ) , text) ;
765
903
return Some ( "</span>" ) ;
766
904
} ;
@@ -784,6 +922,7 @@ fn string_without_closing_tag<T: Display>(
784
922
path
785
923
} ) ;
786
924
}
925
+
787
926
if let Some ( href_context) = href_context {
788
927
if let Some ( href) =
789
928
href_context. context . shared . span_correspondance_map . get ( & def_span) . and_then ( |href| {
@@ -812,12 +951,33 @@ fn string_without_closing_tag<T: Display>(
812
951
}
813
952
} )
814
953
{
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
+ }
816
966
return Some ( "</a>" ) ;
817
967
}
818
968
}
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
+ }
821
981
}
822
982
823
983
#[ cfg( test) ]
0 commit comments