Skip to content

Commit dfcb725

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

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
@@ -176,11 +176,17 @@ impl Element {
176176
other.content.iter().all(|c| can_merge(self.class, other.class, &c.text))
177177
}
178178

179-
fn write_elem_to<W: Write>(&self, out: &mut W, href_context: &Option<HrefContext<'_, '_>>, parent_class: Option<Class>) {
179+
fn write_elem_to<W: Write>(
180+
&self,
181+
out: &mut W,
182+
href_context: &Option<HrefContext<'_, '_>>,
183+
parent_class: Option<Class>,
184+
) {
180185
let mut prev = parent_class;
181186
let mut closing_tag = None;
182187
for part in &self.content {
183-
let text: &dyn Display = if part.needs_escape { &EscapeBodyText(&part.text) } else { &part.text };
188+
let text: &dyn Display =
189+
if part.needs_escape { &EscapeBodyText(&part.text) } else { &part.text };
184190
if part.class.is_some() {
185191
// We only try to generate links as the `<span>` should have already be generated
186192
// by the caller of `write_elem_to`.
@@ -215,11 +221,18 @@ enum ElementOrStack {
215221
Stack(ElementStack),
216222
}
217223

224+
/// This represents the stack of HTML elements. For example a macro expansion
225+
/// will contain other elements which might themselves contain other elements
226+
/// (like macros).
227+
///
228+
/// This allows to easily handle HTML tags instead of having a more complicated
229+
/// state machine to keep track of which tags are open.
218230
#[derive(Debug)]
219231
struct ElementStack {
220232
elements: Vec<ElementOrStack>,
221233
parent: Option<Box<ElementStack>>,
222234
class: Option<Class>,
235+
pending_exit: bool,
223236
}
224237

225238
impl ElementStack {
@@ -228,10 +241,15 @@ impl ElementStack {
228241
}
229242

230243
fn new_with_class(class: Option<Class>) -> Self {
231-
Self { elements: Vec::new(), parent: None, class }
244+
Self { elements: Vec::new(), parent: None, class, pending_exit: false }
232245
}
233246

234247
fn push_element(&mut self, mut elem: Element) {
248+
if self.pending_exit
249+
&& !can_merge(self.class, elem.class, elem.content.first().map_or("", |c| &c.text))
250+
{
251+
self.exit_current_stack();
252+
}
235253
if let Some(ElementOrStack::Element(last)) = self.elements.last_mut()
236254
&& last.can_merge(&elem)
237255
{
@@ -254,41 +272,61 @@ impl ElementStack {
254272
}
255273
}
256274

257-
fn enter_stack(&mut self, ElementStack { elements, parent, class }: ElementStack) {
275+
fn enter_stack(
276+
&mut self,
277+
ElementStack { elements, parent, class, pending_exit }: ElementStack,
278+
) {
279+
if self.pending_exit {
280+
if can_merge(self.class, class, "") {
281+
self.pending_exit = false;
282+
for elem in elements {
283+
self.elements.push(elem);
284+
}
285+
// Compatible stacks, nothing to be done here!
286+
return;
287+
}
288+
self.exit_current_stack();
289+
}
258290
assert!(parent.is_none(), "`enter_stack` used with a non empty parent");
259291
let parent_elements = std::mem::take(&mut self.elements);
260292
let parent_parent = std::mem::take(&mut self.parent);
261293
self.parent = Some(Box::new(ElementStack {
262294
elements: parent_elements,
263295
parent: parent_parent,
264296
class: self.class,
297+
pending_exit: self.pending_exit,
265298
}));
266299
self.class = class;
267300
self.elements = elements;
301+
self.pending_exit = pending_exit;
268302
}
269303

270-
fn enter_elem(&mut self, class: Class) {
271-
let elements = std::mem::take(&mut self.elements);
272-
let parent = std::mem::take(&mut self.parent);
273-
self.parent = Some(Box::new(ElementStack { elements, parent, class: self.class }));
274-
self.class = Some(class);
304+
/// This sets the `pending_exit` field to `true`. Meaning that if we try to push another stack
305+
/// which is not compatible with this one, it will exit the current one before adding the new
306+
/// one.
307+
fn exit_elem(&mut self) {
308+
self.pending_exit = true;
275309
}
276310

277-
fn exit_elem(&mut self) {
311+
/// Unlike `exit_elem`, this method directly exits the current stack. It is called when the
312+
/// current stack is not compatible with a new one pushed or if an expansion was ended.
313+
fn exit_current_stack(&mut self) {
278314
let Some(element) = std::mem::take(&mut self.parent) else {
279315
panic!("exiting an element where there is no parent");
280316
};
281-
let ElementStack { elements, parent, class } = Box::into_inner(element);
317+
let ElementStack { elements, parent, class, pending_exit } = Box::into_inner(element);
282318

283319
let old_elements = std::mem::take(&mut self.elements);
284320
self.elements = elements;
285321
self.elements.push(ElementOrStack::Stack(ElementStack {
286322
elements: old_elements,
287323
class: self.class,
288324
parent: None,
325+
pending_exit: false,
289326
}));
290327
self.parent = parent;
291328
self.class = class;
329+
self.pending_exit = pending_exit;
292330
}
293331

294332
fn write_content<W: Write>(&self, out: &mut W, href_context: &Option<HrefContext<'_, '_>>) {
@@ -323,16 +361,18 @@ impl ElementStack {
323361
// we generate the `<a>` directly here.
324362
//
325363
// For other elements, the links will be generated in `write_elem_to`.
326-
let href_context = if matches!(class, Class::Macro(_)) {
327-
href_context
328-
} else {
329-
&None
330-
};
331-
string_without_closing_tag(out, "", Some(class), href_context, self.class != parent_class)
332-
.expect(
333-
"internal error: enter_span was called with Some(class) but did not \
364+
let href_context = if matches!(class, Class::Macro(_)) { href_context } else { &None };
365+
string_without_closing_tag(
366+
out,
367+
"",
368+
Some(class),
369+
href_context,
370+
self.class != parent_class,
371+
)
372+
.expect(
373+
"internal error: enter_span was called with Some(class) but did not \
334374
return a closing HTML tag",
335-
)
375+
)
336376
} else {
337377
""
338378
};
@@ -444,7 +484,7 @@ impl<F: Write> TokenHandler<'_, '_, F> {
444484

445485
// We inline everything into the top-most element.
446486
while self.element_stack.parent.is_some() {
447-
self.element_stack.exit_elem();
487+
self.element_stack.exit_current_stack();
448488
if let Some(ElementOrStack::Stack(stack)) = self.element_stack.elements.last()
449489
&& let Some(class) = stack.class
450490
&& class != Class::Original
@@ -467,6 +507,11 @@ impl<F: Write> TokenHandler<'_, '_, F> {
467507
impl<F: Write> Drop for TokenHandler<'_, '_, F> {
468508
/// When leaving, we need to flush all pending data to not have missing content.
469509
fn drop(&mut self) {
510+
// We need to clean the hierarchy before displaying it, otherwise the parents won't see
511+
// the last child.
512+
while self.element_stack.parent.is_some() {
513+
self.element_stack.exit_current_stack();
514+
}
470515
self.element_stack.write_content(self.out, &self.href_context);
471516
}
472517
}
@@ -510,7 +555,7 @@ fn end_expansion<'a, W: Write>(
510555
expanded_codes: &'a [ExpandedCode],
511556
span: Span,
512557
) -> Option<&'a ExpandedCode> {
513-
token_handler.element_stack.exit_elem();
558+
token_handler.element_stack.exit_current_stack();
514559
let expansion = get_next_expansion(expanded_codes, token_handler.line, span);
515560
if expansion.is_none() {
516561
token_handler.close_expansion();
@@ -628,7 +673,9 @@ pub(super) fn write_code(
628673
}
629674
}
630675
}
631-
Highlight::EnterSpan { class } => token_handler.element_stack.enter_elem(class),
676+
Highlight::EnterSpan { class } => {
677+
token_handler.element_stack.enter_stack(ElementStack::new_with_class(Some(class)))
678+
}
632679
Highlight::ExitSpan => token_handler.element_stack.exit_elem(),
633680
});
634681
}

0 commit comments

Comments
 (0)