Skip to content

Commit 957ca47

Browse files
Make compatible stack elements "glue" together to prevent creating more HTML tags than necessary
1 parent 817187c commit 957ca47

File tree

1 file changed

+70
-23
lines changed

1 file changed

+70
-23
lines changed

src/librustdoc/html/highlight.rs

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,17 @@ impl Element {
158158
other.content.iter().all(|c| can_merge(self.class, other.class, &c.text))
159159
}
160160

161-
fn write_elem_to<W: Write>(&self, out: &mut W, href_context: &Option<HrefContext<'_, '_>>, parent_class: Option<Class>) {
161+
fn write_elem_to<W: Write>(
162+
&self,
163+
out: &mut W,
164+
href_context: &Option<HrefContext<'_, '_>>,
165+
parent_class: Option<Class>,
166+
) {
162167
let mut prev = parent_class;
163168
let mut closing_tag = None;
164169
for part in &self.content {
165-
let text: &dyn Display = if part.needs_escape { &EscapeBodyText(&part.text) } else { &part.text };
170+
let text: &dyn Display =
171+
if part.needs_escape { &EscapeBodyText(&part.text) } else { &part.text };
166172
if part.class.is_some() {
167173
// We only try to generate links as the `<span>` should have already be generated
168174
// by the caller of `write_elem_to`.
@@ -197,11 +203,18 @@ enum ElementOrStack {
197203
Stack(ElementStack),
198204
}
199205

206+
/// This represents the stack of HTML elements. For example a macro expansion
207+
/// will contain other elements which might themselves contain other elements
208+
/// (like macros).
209+
///
210+
/// This allows to easily handle HTML tags instead of having a more complicated
211+
/// state machine to keep track of which tags are open.
200212
#[derive(Debug)]
201213
struct ElementStack {
202214
elements: Vec<ElementOrStack>,
203215
parent: Option<Box<ElementStack>>,
204216
class: Option<Class>,
217+
pending_exit: bool,
205218
}
206219

207220
impl ElementStack {
@@ -210,10 +223,15 @@ impl ElementStack {
210223
}
211224

212225
fn new_with_class(class: Option<Class>) -> Self {
213-
Self { elements: Vec::new(), parent: None, class }
226+
Self { elements: Vec::new(), parent: None, class, pending_exit: false }
214227
}
215228

216229
fn push_element(&mut self, mut elem: Element) {
230+
if self.pending_exit
231+
&& !can_merge(self.class, elem.class, elem.content.first().map_or("", |c| &c.text))
232+
{
233+
self.exit_current_stack();
234+
}
217235
if let Some(ElementOrStack::Element(last)) = self.elements.last_mut()
218236
&& last.can_merge(&elem)
219237
{
@@ -236,41 +254,61 @@ impl ElementStack {
236254
}
237255
}
238256

239-
fn enter_stack(&mut self, ElementStack { elements, parent, class }: ElementStack) {
257+
fn enter_stack(
258+
&mut self,
259+
ElementStack { elements, parent, class, pending_exit }: ElementStack,
260+
) {
261+
if self.pending_exit {
262+
if can_merge(self.class, class, "") {
263+
self.pending_exit = false;
264+
for elem in elements {
265+
self.elements.push(elem);
266+
}
267+
// Compatible stacks, nothing to be done here!
268+
return;
269+
}
270+
self.exit_current_stack();
271+
}
240272
assert!(parent.is_none(), "`enter_stack` used with a non empty parent");
241273
let parent_elements = std::mem::take(&mut self.elements);
242274
let parent_parent = std::mem::take(&mut self.parent);
243275
self.parent = Some(Box::new(ElementStack {
244276
elements: parent_elements,
245277
parent: parent_parent,
246278
class: self.class,
279+
pending_exit: self.pending_exit,
247280
}));
248281
self.class = class;
249282
self.elements = elements;
283+
self.pending_exit = pending_exit;
250284
}
251285

252-
fn enter_elem(&mut self, class: Class) {
253-
let elements = std::mem::take(&mut self.elements);
254-
let parent = std::mem::take(&mut self.parent);
255-
self.parent = Some(Box::new(ElementStack { elements, parent, class: self.class }));
256-
self.class = Some(class);
286+
/// This sets the `pending_exit` field to `true`. Meaning that if we try to push another stack
287+
/// which is not compatible with this one, it will exit the current one before adding the new
288+
/// one.
289+
fn exit_elem(&mut self) {
290+
self.pending_exit = true;
257291
}
258292

259-
fn exit_elem(&mut self) {
293+
/// Unlike `exit_elem`, this method directly exits the current stack. It is called when the
294+
/// current stack is not compatible with a new one pushed or if an expansion was ended.
295+
fn exit_current_stack(&mut self) {
260296
let Some(element) = std::mem::take(&mut self.parent) else {
261297
panic!("exiting an element where there is no parent");
262298
};
263-
let ElementStack { elements, parent, class } = Box::into_inner(element);
299+
let ElementStack { elements, parent, class, pending_exit } = Box::into_inner(element);
264300

265301
let old_elements = std::mem::take(&mut self.elements);
266302
self.elements = elements;
267303
self.elements.push(ElementOrStack::Stack(ElementStack {
268304
elements: old_elements,
269305
class: self.class,
270306
parent: None,
307+
pending_exit: false,
271308
}));
272309
self.parent = parent;
273310
self.class = class;
311+
self.pending_exit = pending_exit;
274312
}
275313

276314
fn write_content<W: Write>(&self, out: &mut W, href_context: &Option<HrefContext<'_, '_>>) {
@@ -305,16 +343,18 @@ impl ElementStack {
305343
// we generate the `<a>` directly here.
306344
//
307345
// For other elements, the links will be generated in `write_elem_to`.
308-
let href_context = if matches!(class, Class::Macro(_)) {
309-
href_context
310-
} else {
311-
&None
312-
};
313-
string_without_closing_tag(out, "", Some(class), href_context, self.class != parent_class)
314-
.expect(
315-
"internal error: enter_span was called with Some(class) but did not \
346+
let href_context = if matches!(class, Class::Macro(_)) { href_context } else { &None };
347+
string_without_closing_tag(
348+
out,
349+
"",
350+
Some(class),
351+
href_context,
352+
self.class != parent_class,
353+
)
354+
.expect(
355+
"internal error: enter_span was called with Some(class) but did not \
316356
return a closing HTML tag",
317-
)
357+
)
318358
} else {
319359
""
320360
};
@@ -426,7 +466,7 @@ impl<F: Write> TokenHandler<'_, '_, F> {
426466

427467
// We inline everything into the top-most element.
428468
while self.element_stack.parent.is_some() {
429-
self.element_stack.exit_elem();
469+
self.element_stack.exit_current_stack();
430470
if let Some(ElementOrStack::Stack(stack)) = self.element_stack.elements.last()
431471
&& let Some(class) = stack.class
432472
&& class != Class::Original
@@ -449,6 +489,11 @@ impl<F: Write> TokenHandler<'_, '_, F> {
449489
impl<F: Write> Drop for TokenHandler<'_, '_, F> {
450490
/// When leaving, we need to flush all pending data to not have missing content.
451491
fn drop(&mut self) {
492+
// We need to clean the hierarchy before displaying it, otherwise the parents won't see
493+
// the last child.
494+
while self.element_stack.parent.is_some() {
495+
self.element_stack.exit_current_stack();
496+
}
452497
self.element_stack.write_content(self.out, &self.href_context);
453498
}
454499
}
@@ -492,7 +537,7 @@ fn end_expansion<'a, W: Write>(
492537
expanded_codes: &'a [ExpandedCode],
493538
span: Span,
494539
) -> Option<&'a ExpandedCode> {
495-
token_handler.element_stack.exit_elem();
540+
token_handler.element_stack.exit_current_stack();
496541
let expansion = get_next_expansion(expanded_codes, token_handler.line, span);
497542
if expansion.is_none() {
498543
token_handler.close_expansion();
@@ -610,7 +655,9 @@ pub(super) fn write_code(
610655
}
611656
}
612657
}
613-
Highlight::EnterSpan { class } => token_handler.element_stack.enter_elem(class),
658+
Highlight::EnterSpan { class } => {
659+
token_handler.element_stack.enter_stack(ElementStack::new_with_class(Some(class)))
660+
}
614661
Highlight::ExitSpan => token_handler.element_stack.exit_elem(),
615662
});
616663
}

0 commit comments

Comments
 (0)