Skip to content

Commit 801821d

Browse files
committed
Auto merge of rust-lang#100429 - GuillaumeGomez:merge-html-elements-together, r=notriddle
rustdoc: Merge source code pages HTML elements together We realized that the HTML generated for the source code pages could be improved quite a lot. This PR is a first pass toward this goal. Some explanations: it merges similar classes elements (even when there are white characters in between). There is an exception to this: if this is an ident, I also merged it with "unclassified" elements. This part is up to debate and can be very easily removed as the check is performed in one place (in the `can_merge` function). EDIT: The `ident` is now only kept in the code for the `span` it contains but it is not rendered into the HTML. So now some numbers: For these ones, on each page, I run this JS: `document.getElementsByTagName('*').length`. The goal is to count the number of DOM elements. I took some pages that seemed big, but don't hesitate to check some others. | file name | before this PR | with this PR | diff | without ident | diff | |-|-|-|-|-|-| | std/lib.rs.html (source link on std crate page) | 3455 | 2776 | 20.7% | 2387 | 31% | | alloc/vec/mod.rs.html (source on Vec type page) | 11012 | 8084 | 26.6% | 6682 | 39.4% | | alloc/string.rs.html (source on String type page) | 10800 | 8214 | 24% | 6712 | 37.9% | | std/sync/mutex.rs.html (source on Mutex type page) | 2953 | 2403 | 18.7% | 2139 | 27.6% | You can test it [here](https://rustdoc.crud.net/imperio/merge-html-elements-together/src/std/lib.rs.html). cc `@jsha` r? `@notriddle`
2 parents 4c56655 + 6e574e1 commit 801821d

File tree

12 files changed

+216
-59
lines changed

12 files changed

+216
-59
lines changed

src/librustdoc/html/highlight.rs

+172-12
Original file line numberDiff line numberDiff line change
@@ -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

152257
fn 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

179285
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+
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)]
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
<span class="example"><span class="kw">let</span> <span class="ident">x</span> = <span class="number">1</span>;</span>
2-
<span class="kw">let</span> <span class="ident">y</span> = <span class="number">2</span>;
1+
<span class="example"><span class="kw">let </span>x = <span class="number">1</span>;</span>
2+
<span class="kw">let </span>y = <span class="number">2</span>;
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
<span class="kw">pub</span> <span class="kw">fn</span> <span class="ident">foo</span>() {
1+
<span class="kw">pub fn </span>foo() {
22
<span class="macro">println!</span>(<span class="string">&quot;foo&quot;</span>);
33
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<span class="kw">use</span> <span class="ident"><span class="kw">crate</span>::a::foo</span>;
2-
<span class="kw">use</span> <span class="ident"><span class="self">self</span>::whatever</span>;
3-
<span class="kw">let</span> <span class="ident">x</span> = <span class="ident"><span class="kw">super</span>::b::foo</span>;
4-
<span class="kw">let</span> <span class="ident">y</span> = <span class="ident"><span class="self">Self</span>::whatever</span>;
1+
<span class="kw">use </span><span class="kw">crate</span>::a::foo;
2+
<span class="kw">use </span><span class="self">self</span>::whatever;
3+
<span class="kw">let </span>x = <span class="kw">super</span>::b::foo;
4+
<span class="kw">let </span>y = <span class="self">Self</span>::whatever;

src/librustdoc/html/highlight/fixtures/sample.html

+19-19
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,30 @@
88
.lifetime { color: #B76514; }
99
.question-mark { color: #ff9011; }
1010
</style>
11-
<pre><code><span class="attribute">#![<span class="ident">crate_type</span> = <span class="string">&quot;lib&quot;</span>]</span>
11+
<pre><code><span class="attribute">#![crate_type = <span class="string">&quot;lib&quot;</span>]</span>
1212

13-
<span class="kw">use</span> <span class="ident">std::path</span>::{<span class="ident">Path</span>, <span class="ident">PathBuf</span>};
13+
<span class="kw">use </span>std::path::{Path, PathBuf};
1414

15-
<span class="attribute">#[<span class="ident">cfg</span>(<span class="ident">target_os</span> = <span class="string">&quot;linux&quot;</span>)]</span>
16-
<span class="kw">fn</span> <span class="ident">main</span>() -&gt; () {
17-
<span class="kw">let</span> <span class="ident">foo</span> = <span class="bool-val">true</span> &amp;&amp; <span class="bool-val">false</span> || <span class="bool-val">true</span>;
18-
<span class="kw">let</span> <span class="kw">_</span>: <span class="kw-2">*const</span> () = <span class="number">0</span>;
19-
<span class="kw">let</span> <span class="kw">_</span> = <span class="kw-2">&amp;</span><span class="ident">foo</span>;
20-
<span class="kw">let</span> <span class="kw">_</span> = &amp;&amp;<span class="ident">foo</span>;
21-
<span class="kw">let</span> <span class="kw">_</span> = <span class="kw-2">*</span><span class="ident">foo</span>;
22-
<span class="macro">mac!</span>(<span class="ident">foo</span>, <span class="kw-2">&amp;mut</span> <span class="ident">bar</span>);
23-
<span class="macro">assert!</span>(<span class="self">self</span>.<span class="ident">length</span> &lt; <span class="ident">N</span> &amp;&amp; <span class="ident">index</span> &lt;= <span class="self">self</span>.<span class="ident">length</span>);
24-
<span class="ident">::std::env::var</span>(<span class="string">&quot;gateau&quot;</span>).<span class="ident">is_ok</span>();
25-
<span class="attribute">#[<span class="ident">rustfmt::skip</span>]</span>
26-
<span class="kw">let</span> <span class="ident">s</span>:<span class="ident">std::path::PathBuf</span> = <span class="ident">std::path::PathBuf::new</span>();
27-
<span class="kw">let</span> <span class="kw-2">mut</span> <span class="ident">s</span> = <span class="ident">String::new</span>();
15+
<span class="attribute">#[cfg(target_os = <span class="string">&quot;linux&quot;</span>)]</span>
16+
<span class="kw">fn </span>main() -&gt; () {
17+
<span class="kw">let </span>foo = <span class="bool-val">true </span>&amp;&amp; <span class="bool-val">false </span>|| <span class="bool-val">true</span>;
18+
<span class="kw">let _</span>: <span class="kw-2">*const </span>() = <span class="number">0</span>;
19+
<span class="kw">let _ </span>= <span class="kw-2">&amp;</span>foo;
20+
<span class="kw">let _ </span>= &amp;&amp;foo;
21+
<span class="kw">let _ </span>= <span class="kw-2">*</span>foo;
22+
<span class="macro">mac!</span>(foo, <span class="kw-2">&amp;mut </span>bar);
23+
<span class="macro">assert!</span>(<span class="self">self</span>.length &lt; N &amp;&amp; index &lt;= <span class="self">self</span>.length);
24+
::std::env::var(<span class="string">&quot;gateau&quot;</span>).is_ok();
25+
<span class="attribute">#[rustfmt::skip]</span>
26+
<span class="kw">let </span>s:std::path::PathBuf = std::path::PathBuf::new();
27+
<span class="kw">let </span><span class="kw-2">mut </span>s = String::new();
2828

29-
<span class="kw">match</span> <span class="kw-2">&amp;</span><span class="ident">s</span> {
30-
<span class="kw-2">ref</span> <span class="kw-2">mut</span> <span class="ident">x</span> =&gt; {}
29+
<span class="kw">match </span><span class="kw-2">&amp;</span>s {
30+
<span class="kw-2">ref mut </span>x =&gt; {}
3131
}
3232
}
3333

34-
<span class="macro">macro_rules!</span> <span class="ident">bar</span> {
35-
(<span class="macro-nonterminal">$</span><span class="macro-nonterminal">foo</span>:<span class="ident">tt</span>) =&gt; {};
34+
<span class="macro">macro_rules!</span> bar {
35+
(<span class="macro-nonterminal">$foo</span>:tt) =&gt; {};
3636
}
3737
</code></pre>
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<span class="kw">union</span> <span class="ident">Foo</span> {
2-
<span class="ident">i</span>: <span class="ident">i8</span>,
3-
<span class="ident">u</span>: <span class="ident">i8</span>,
1+
<span class="kw">union </span>Foo {
2+
i: i8,
3+
u: i8,
44
}
55

6-
<span class="kw">fn</span> <span class="ident">main</span>() {
7-
<span class="kw">let</span> <span class="ident">union</span> = <span class="number">0</span>;
6+
<span class="kw">fn </span>main() {
7+
<span class="kw">let </span>union = <span class="number">0</span>;
88
}

src/librustdoc/html/static/css/themes/ayu.css

+1-5
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,6 @@ pre.rust .self {
250250
pre.rust .attribute {
251251
color: #e6e1cf;
252252
}
253-
pre.rust .attribute .ident {
254-
color: #e6e1cf;
255-
}
256253

257254
.example-wrap > pre.line-number {
258255
color: #5c67736e;
@@ -398,8 +395,7 @@ pre.rust .comment {}
398395
.block a.current.method,.content span.tymethod,.content a.tymethod,.block a.current.tymethod,
399396
.content .fnname {}
400397
pre.rust .kw {}
401-
pre.rust .self,pre.rust .bool-val,pre.rust .prelude-val,pre.rust .attribute,
402-
pre.rust .attribute .ident {}
398+
pre.rust .self,pre.rust .bool-val,pre.rust .prelude-val,pre.rust .attribute {}
403399
.content span.foreigntype,.content a.foreigntype,.block a.current.foreigntype {}
404400
pre.rust .doccomment {}
405401
.stab.deprecated {}

0 commit comments

Comments
 (0)