Skip to content

Commit c6e26f9

Browse files
Make compatible stack elements "glue" together to prevent creating more HTML tags than necessary
1 parent a5346c6 commit c6e26f9

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
@@ -159,11 +159,17 @@ impl Element {
159159
other.content.iter().all(|c| can_merge(self.class, other.class, &c.text))
160160
}
161161

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

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

208221
impl ElementStack {
@@ -211,10 +224,15 @@ impl ElementStack {
211224
}
212225

213226
fn new_with_class(class: Option<Class>) -> Self {
214-
Self { elements: Vec::new(), parent: None, class }
227+
Self { elements: Vec::new(), parent: None, class, pending_exit: false }
215228
}
216229

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

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

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

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

266302
let old_elements = std::mem::take(&mut self.elements);
267303
self.elements = elements;
268304
self.elements.push(ElementOrStack::Stack(ElementStack {
269305
elements: old_elements,
270306
class: self.class,
271307
parent: None,
308+
pending_exit: false,
272309
}));
273310
self.parent = parent;
274311
self.class = class;
312+
self.pending_exit = pending_exit;
275313
}
276314

277315
fn write_content<W: Write>(&self, out: &mut W, href_context: &Option<HrefContext<'_, '_>>) {
@@ -306,16 +344,18 @@ impl ElementStack {
306344
// we generate the `<a>` directly here.
307345
//
308346
// For other elements, the links will be generated in `write_elem_to`.
309-
let href_context = if matches!(class, Class::Macro(_)) {
310-
href_context
311-
} else {
312-
&None
313-
};
314-
string_without_closing_tag(out, "", Some(class), href_context, self.class != parent_class)
315-
.expect(
316-
"internal error: enter_span was called with Some(class) but did not \
347+
let href_context = if matches!(class, Class::Macro(_)) { href_context } else { &None };
348+
string_without_closing_tag(
349+
out,
350+
"",
351+
Some(class),
352+
href_context,
353+
self.class != parent_class,
354+
)
355+
.expect(
356+
"internal error: enter_span was called with Some(class) but did not \
317357
return a closing HTML tag",
318-
)
358+
)
319359
} else {
320360
""
321361
};
@@ -427,7 +467,7 @@ impl<F: Write> TokenHandler<'_, '_, F> {
427467

428468
// We inline everything into the top-most element.
429469
while self.element_stack.parent.is_some() {
430-
self.element_stack.exit_elem();
470+
self.element_stack.exit_current_stack();
431471
if let Some(ElementOrStack::Stack(stack)) = self.element_stack.elements.last()
432472
&& let Some(class) = stack.class
433473
&& class != Class::Original
@@ -450,6 +490,11 @@ impl<F: Write> TokenHandler<'_, '_, F> {
450490
impl<F: Write> Drop for TokenHandler<'_, '_, F> {
451491
/// When leaving, we need to flush all pending data to not have missing content.
452492
fn drop(&mut self) {
493+
// We need to clean the hierarchy before displaying it, otherwise the parents won't see
494+
// the last child.
495+
while self.element_stack.parent.is_some() {
496+
self.element_stack.exit_current_stack();
497+
}
453498
self.element_stack.write_content(self.out, &self.href_context);
454499
}
455500
}
@@ -493,7 +538,7 @@ fn end_expansion<'a, W: Write>(
493538
expanded_codes: &'a [ExpandedCode],
494539
span: Span,
495540
) -> Option<&'a ExpandedCode> {
496-
token_handler.element_stack.exit_elem();
541+
token_handler.element_stack.exit_current_stack();
497542
let expansion = get_next_expansion(expanded_codes, token_handler.line, span);
498543
if expansion.is_none() {
499544
token_handler.close_expansion();
@@ -617,7 +662,9 @@ pub(super) fn write_code(
617662
}
618663
}
619664
}
620-
Highlight::EnterSpan { class } => token_handler.element_stack.enter_elem(class),
665+
Highlight::EnterSpan { class } => {
666+
token_handler.element_stack.enter_stack(ElementStack::new_with_class(Some(class)))
667+
}
621668
Highlight::ExitSpan => token_handler.element_stack.exit_elem(),
622669
});
623670
}

0 commit comments

Comments
 (0)