Skip to content

Commit cf5d133

Browse files
authored
Rollup merge of rust-lang#100775 - GuillaumeGomez:reduce-span-v2, r=notriddle
rustdoc: Merge source code pages HTML elements together v2 This is the follow-up of rust-lang#100429. I strongly recommend to review it one commit at a time because otherwise it's a lot at once. 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. I also added the "starting point" because it's quite nice to see how much the page was reduced thanks to these two PRs. | file name | before rust-lang#100429 | before this PR | with this PR | diff | |-|-|-|-|-| | std/lib.rs.html (source link on std crate page) | 3455 | 2332 | 1772 | 24% | | alloc/vec/mod.rs.html (source on Vec type page) | 11012 | 5982 | 5833 | 2.5% | | alloc/string.rs.html (source on String type page) | 10800 | 6010 | 5822 | 3.2% | | std/sync/mutex.rs.html (source on Mutex type page) | 2953 | 2041 | 2038 | 0.1% | So unsurprisingly, the more attributes you have, the bigger the difference. You can test it [here](https://rustdoc.crud.net/imperio/reduce-span-v2/src/std/lib.rs.html). cc ```@jsha``` r? ```@notriddle```
2 parents b182a90 + 7ab8e0c commit cf5d133

File tree

7 files changed

+157
-106
lines changed

7 files changed

+157
-106
lines changed

src/librustdoc/html/highlight.rs

+133-91
Original file line numberDiff line numberDiff line change
@@ -111,53 +111,6 @@ 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-
161114
/// Check if two `Class` can be merged together. In the following rules, "unclassified" means `None`
162115
/// basically (since it's `Option<Class>`). The following rules apply:
163116
///
@@ -171,7 +124,88 @@ fn can_merge(class1: Option<Class>, class2: Option<Class>, text: &str) -> bool {
171124
(Some(c1), Some(c2)) => c1.is_equal_to(c2),
172125
(Some(Class::Ident(_)), None) | (None, Some(Class::Ident(_))) => true,
173126
(Some(_), None) | (None, Some(_)) => text.trim().is_empty(),
174-
_ => false,
127+
(None, None) => true,
128+
}
129+
}
130+
131+
/// This type is used as a conveniency to prevent having to pass all its fields as arguments into
132+
/// the various functions (which became its methods).
133+
struct TokenHandler<'a, 'b, 'c, 'd, 'e> {
134+
out: &'a mut Buffer,
135+
/// It contains the closing tag and the associated `Class`.
136+
closing_tags: Vec<(&'static str, Class)>,
137+
/// This is used because we don't automatically generate the closing tag on `ExitSpan` in
138+
/// case an `EnterSpan` event with the same class follows.
139+
pending_exit_span: Option<Class>,
140+
/// `current_class` and `pending_elems` are used to group HTML elements with same `class`
141+
/// attributes to reduce the DOM size.
142+
current_class: Option<Class>,
143+
/// We need to keep the `Class` for each element because it could contain a `Span` which is
144+
/// used to generate links.
145+
pending_elems: Vec<(&'b str, Option<Class>)>,
146+
href_context: Option<HrefContext<'c, 'd, 'e>>,
147+
}
148+
149+
impl<'a, 'b, 'c, 'd, 'e> TokenHandler<'a, 'b, 'c, 'd, 'e> {
150+
fn handle_exit_span(&mut self) {
151+
// We can't get the last `closing_tags` element using `pop()` because `closing_tags` is
152+
// being used in `write_pending_elems`.
153+
let class = self.closing_tags.last().expect("ExitSpan without EnterSpan").1;
154+
// We flush everything just in case...
155+
self.write_pending_elems(Some(class));
156+
157+
exit_span(self.out, self.closing_tags.pop().expect("ExitSpan without EnterSpan").0);
158+
self.pending_exit_span = None;
159+
}
160+
161+
/// Write all the pending elements sharing a same (or at mergeable) `Class`.
162+
///
163+
/// If there is a "parent" (if a `EnterSpan` event was encountered) and the parent can be merged
164+
/// with the elements' class, then we simply write the elements since the `ExitSpan` event will
165+
/// close the tag.
166+
///
167+
/// Otherwise, if there is only one pending element, we let the `string` function handle both
168+
/// opening and closing the tag, otherwise we do it into this function.
169+
///
170+
/// It returns `true` if `current_class` must be set to `None` afterwards.
171+
fn write_pending_elems(&mut self, current_class: Option<Class>) -> bool {
172+
if self.pending_elems.is_empty() {
173+
return false;
174+
}
175+
if let Some((_, parent_class)) = self.closing_tags.last() &&
176+
can_merge(current_class, Some(*parent_class), "")
177+
{
178+
for (text, class) in self.pending_elems.iter() {
179+
string(self.out, Escape(text), *class, &self.href_context, false);
180+
}
181+
} else {
182+
// We only want to "open" the tag ourselves if we have more than one pending and if the
183+
// current parent tag is not the same as our pending content.
184+
let close_tag = if self.pending_elems.len() > 1 && current_class.is_some() {
185+
Some(enter_span(self.out, current_class.unwrap(), &self.href_context))
186+
} else {
187+
None
188+
};
189+
for (text, class) in self.pending_elems.iter() {
190+
string(self.out, Escape(text), *class, &self.href_context, close_tag.is_none());
191+
}
192+
if let Some(close_tag) = close_tag {
193+
exit_span(self.out, close_tag);
194+
}
195+
}
196+
self.pending_elems.clear();
197+
true
198+
}
199+
}
200+
201+
impl<'a, 'b, 'c, 'd, 'e> Drop for TokenHandler<'a, 'b, 'c, 'd, 'e> {
202+
/// When leaving, we need to flush all pending data to not have missing content.
203+
fn drop(&mut self) {
204+
if self.pending_exit_span.is_some() {
205+
self.handle_exit_span();
206+
} else {
207+
self.write_pending_elems(self.current_class);
208+
}
175209
}
176210
}
177211

@@ -194,64 +228,72 @@ fn write_code(
194228
) {
195229
// This replace allows to fix how the code source with DOS backline characters is displayed.
196230
let src = src.replace("\r\n", "\n");
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();
231+
let mut token_handler = TokenHandler {
232+
out,
233+
closing_tags: Vec::new(),
234+
pending_exit_span: None,
235+
current_class: None,
236+
pending_elems: Vec::new(),
237+
href_context,
238+
};
205239

206240
Classifier::new(
207241
&src,
208-
href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
242+
token_handler.href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
209243
decoration_info,
210244
)
211245
.highlight(&mut |highlight| {
212246
match highlight {
213247
Highlight::Token { text, class } => {
248+
// If we received a `ExitSpan` event and then have a non-compatible `Class`, we
249+
// need to close the `<span>`.
250+
let need_current_class_update = if let Some(pending) = token_handler.pending_exit_span &&
251+
!can_merge(Some(pending), class, text) {
252+
token_handler.handle_exit_span();
253+
true
214254
// If the two `Class` are different, time to flush the current content and start
215255
// 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);
256+
} else if !can_merge(token_handler.current_class, class, text) {
257+
token_handler.write_pending_elems(token_handler.current_class);
258+
true
259+
} else {
260+
token_handler.current_class.is_none()
261+
};
262+
263+
if need_current_class_update {
264+
token_handler.current_class = class.map(Class::dummy);
227265
}
228-
pending_elems.push((text, class));
266+
token_handler.pending_elems.push((text, class));
229267
}
230268
Highlight::EnterSpan { class } => {
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))
269+
let mut should_add = true;
270+
if let Some(pending_exit_span) = token_handler.pending_exit_span {
271+
if class.is_equal_to(pending_exit_span) {
272+
should_add = false;
273+
} else {
274+
token_handler.handle_exit_span();
275+
}
276+
} else {
277+
// We flush everything just in case...
278+
if token_handler.write_pending_elems(token_handler.current_class) {
279+
token_handler.current_class = None;
280+
}
281+
}
282+
if should_add {
283+
let closing_tag = enter_span(token_handler.out, class, &token_handler.href_context);
284+
token_handler.closing_tags.push((closing_tag, class));
285+
}
286+
287+
token_handler.current_class = None;
288+
token_handler.pending_exit_span = None;
240289
}
241290
Highlight::ExitSpan => {
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)
291+
token_handler.current_class = None;
292+
token_handler.pending_exit_span =
293+
Some(token_handler.closing_tags.last().as_ref().expect("ExitSpan without EnterSpan").1);
251294
}
252295
};
253296
});
254-
write_pending_elems(out, &href_context, &mut pending_elems, &mut current_class, &closing_tags);
255297
}
256298

257299
fn write_footer(out: &mut Buffer, playground_button: Option<&str>) {
@@ -291,8 +333,8 @@ impl Class {
291333
match (self, other) {
292334
(Self::Self_(_), Self::Self_(_))
293335
| (Self::Macro(_), Self::Macro(_))
294-
| (Self::Ident(_), Self::Ident(_))
295-
| (Self::Decoration(_), Self::Decoration(_)) => true,
336+
| (Self::Ident(_), Self::Ident(_)) => true,
337+
(Self::Decoration(c1), Self::Decoration(c2)) => c1 == c2,
296338
(x, y) => x == y,
297339
}
298340
}
@@ -761,7 +803,7 @@ impl<'a> Classifier<'a> {
761803
TokenKind::CloseBracket => {
762804
if self.in_attribute {
763805
self.in_attribute = false;
764-
sink(Highlight::Token { text: "]", class: Some(Class::Attribute) });
806+
sink(Highlight::Token { text: "]", class: None });
765807
sink(Highlight::ExitSpan);
766808
return;
767809
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
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>;
1+
<span class="example"><span class="kw">let </span>x = <span class="number">1</span>;
2+
<span class="kw">let </span>y = <span class="number">2</span>;
3+
</span><span class="example2"><span class="kw">let </span>z = <span class="number">3</span>;
4+
</span><span class="kw">let </span>a = <span class="number">4</span>;

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

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

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

15-
<span class="attribute">#[cfg(target_os = <span class="string">&quot;linux&quot;</span>)]</span>
16-
<span class="kw">fn </span>main() -&gt; () {
15+
<span class="attribute">#[cfg(target_os = <span class="string">&quot;linux&quot;</span>)]
16+
#[cfg(target_os = <span class="string">&quot;windows&quot;</span>)]
17+
</span><span class="kw">fn </span>main() -&gt; () {
1718
<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>;
1819
<span class="kw">let _</span>: <span class="kw-2">*const </span>() = <span class="number">0</span>;
1920
<span class="kw">let _ </span>= <span class="kw-2">&amp;</span>foo;
@@ -22,16 +23,16 @@
2223
<span class="macro">mac!</span>(foo, <span class="kw-2">&amp;mut </span>bar);
2324
<span class="macro">assert!</span>(<span class="self">self</span>.length &lt; N &amp;&amp; index &lt;= <span class="self">self</span>.length);
2425
::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();
26+
<span class="attribute">#[rustfmt::skip]
27+
</span><span class="kw">let </span>s:std::path::PathBuf = std::path::PathBuf::new();
2728
<span class="kw">let </span><span class="kw-2">mut </span>s = String::new();
2829

2930
<span class="kw">match </span><span class="kw-2">&amp;</span>s {
3031
<span class="kw-2">ref mut </span>x =&gt; {}
3132
}
3233
}
3334

34-
<span class="macro">macro_rules!</span> bar {
35+
<span class="macro">macro_rules! </span>bar {
3536
(<span class="macro-nonterminal">$foo</span>:tt) =&gt; {};
3637
}
3738
</code></pre>

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

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::path::{Path, PathBuf};
44

55
#[cfg(target_os = "linux")]
6+
#[cfg(target_os = "windows")]
67
fn main() -> () {
78
let foo = true && false || true;
89
let _: *const () = 0;

src/librustdoc/html/highlight/tests.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,12 @@ fn test_union_highlighting() {
6969
fn test_decorations() {
7070
create_default_session_globals_then(|| {
7171
let src = "let x = 1;
72-
let y = 2;";
72+
let y = 2;
73+
let z = 3;
74+
let a = 4;";
7375
let mut decorations = FxHashMap::default();
74-
decorations.insert("example", vec![(0, 10)]);
76+
decorations.insert("example", vec![(0, 10), (11, 21)]);
77+
decorations.insert("example2", vec![(22, 32)]);
7578

7679
let mut html = Buffer::new();
7780
write_code(&mut html, src, None, Some(DecorationInfo(decorations)));
+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<code># single
22
## double
33
### triple
4-
<span class="attribute">#[outer]</span>
5-
<span class="attribute">#![inner]</span></code>
4+
<span class="attribute">#[outer]
5+
#![inner]</span></code>

src/test/rustdoc/issue-41783.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// @has issue_41783/struct.Foo.html
22
// @!hasraw - 'space'
33
// @!hasraw - 'comment'
4-
// @hasraw - '<span class="attribute">#[outer]</span>'
5-
// @hasraw - '<span class="attribute">#![inner]</span>'
4+
// @hasraw - '<span class="attribute">#[outer]'
5+
// @!hasraw - '<span class="attribute">#[outer]</span>'
6+
// @hasraw - '#![inner]</span>'
7+
// @!hasraw - '<span class="attribute">#![inner]</span>'
68
// @snapshot 'codeblock' - '//*[@class="rustdoc-toggle top-doc"]/*[@class="docblock"]//pre/code'
79

810
/// ```no_run

0 commit comments

Comments
 (0)