Skip to content

Commit 68f327b

Browse files
Merge HTML elements in highlighting when they can be merged together
1 parent 2fbc08e commit 68f327b

File tree

1 file changed

+155
-8
lines changed

1 file changed

+155
-8
lines changed

Diff for: src/librustdoc/html/highlight.rs

+155-8
Original file line numberDiff line numberDiff line change
@@ -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

152256
fn write_footer(out: &mut Buffer, playground_button: Option<&str>) {
@@ -177,6 +281,31 @@ enum Class {
177281
}
178282

179283
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+
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

Comments
 (0)