Clean up highlight <span> merge code

This commit is contained in:
Guillaume Gomez 2022-08-19 21:35:09 +02:00
parent f5b5d867d5
commit 4c89c2886d

View File

@ -111,65 +111,6 @@ fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>) {
write!(out, "<code>");
}
/// Write all the pending elements sharing a same (or at mergeable) `Class`.
///
/// If there is a "parent" (if a `EnterSpan` event was encountered) and the parent can be merged
/// with the elements' class, then we simply write the elements since the `ExitSpan` event will
/// close the tag.
///
/// Otherwise, if there is only one pending element, we let the `string` function handle both
/// opening and closing the tag, otherwise we do it into this function.
fn write_pending_elems(
out: &mut Buffer,
href_context: &Option<HrefContext<'_, '_, '_>>,
pending_elems: &mut Vec<(&str, Option<Class>)>,
current_class: &mut Option<Class>,
closing_tags: &[(&str, Class)],
) {
if pending_elems.is_empty() {
return;
}
let mut done = false;
if let Some((_, parent_class)) = closing_tags.last() {
if can_merge(*current_class, Some(*parent_class), "") {
for (text, class) in pending_elems.iter() {
string(out, Escape(text), *class, &href_context, false);
}
done = true;
}
}
if !done {
// We only want to "open" the tag ourselves if we have more than one pending and if the current
// parent tag is not the same as our pending content.
let open_tag_ourselves = pending_elems.len() > 1 && current_class.is_some();
let close_tag = if open_tag_ourselves {
enter_span(out, current_class.unwrap(), &href_context)
} else {
""
};
for (text, class) in pending_elems.iter() {
string(out, Escape(text), *class, &href_context, !open_tag_ourselves);
}
if open_tag_ourselves {
exit_span(out, close_tag);
}
}
pending_elems.clear();
*current_class = None;
}
fn handle_exit_span(
out: &mut Buffer,
href_context: &Option<HrefContext<'_, '_, '_>>,
pending_elems: &mut Vec<(&str, Option<Class>)>,
closing_tags: &mut Vec<(&str, Class)>,
) {
let class = closing_tags.last().expect("ExitSpan without EnterSpan").1;
// We flush everything just in case...
write_pending_elems(out, href_context, pending_elems, &mut Some(class), closing_tags);
exit_span(out, closing_tags.pop().expect("ExitSpan without EnterSpan").0);
}
/// Check if two `Class` can be merged together. In the following rules, "unclassified" means `None`
/// basically (since it's `Option<Class>`). The following rules apply:
///
@ -187,6 +128,87 @@ fn can_merge(class1: Option<Class>, class2: Option<Class>, text: &str) -> bool {
}
}
/// This type is used as a conveniency to prevent having to pass all its fields as arguments into
/// the various functions (which became its methods).
struct TokenHandler<'a, 'b, 'c, 'd, 'e> {
out: &'a mut Buffer,
/// It contains the closing tag and the associated `Class`.
closing_tags: Vec<(&'static str, Class)>,
/// This is used because we don't automatically generate the closing tag on `ExitSpan` in
/// case an `EnterSpan` event with the same class follows.
pending_exit_span: Option<Class>,
/// `current_class` and `pending_elems` are used to group HTML elements with same `class`
/// attributes to reduce the DOM size.
current_class: Option<Class>,
/// We need to keep the `Class` for each element because it could contain a `Span` which is
/// used to generate links.
pending_elems: Vec<(&'b str, Option<Class>)>,
href_context: Option<HrefContext<'c, 'd, 'e>>,
}
impl<'a, 'b, 'c, 'd, 'e> TokenHandler<'a, 'b, 'c, 'd, 'e> {
fn handle_exit_span(&mut self) {
// We can't get the last `closing_tags` element using `pop()` because `closing_tags` is
// being used in `write_pending_elems`.
let class = self.closing_tags.last().expect("ExitSpan without EnterSpan").1;
// We flush everything just in case...
self.write_pending_elems(Some(class));
exit_span(self.out, self.closing_tags.pop().expect("ExitSpan without EnterSpan").0);
self.pending_exit_span = None;
}
/// Write all the pending elements sharing a same (or at mergeable) `Class`.
///
/// If there is a "parent" (if a `EnterSpan` event was encountered) and the parent can be merged
/// with the elements' class, then we simply write the elements since the `ExitSpan` event will
/// close the tag.
///
/// Otherwise, if there is only one pending element, we let the `string` function handle both
/// opening and closing the tag, otherwise we do it into this function.
///
/// It returns `true` if `current_class` must be set to `None` afterwards.
fn write_pending_elems(&mut self, current_class: Option<Class>) -> bool {
if self.pending_elems.is_empty() {
return false;
}
if let Some((_, parent_class)) = self.closing_tags.last() &&
can_merge(current_class, Some(*parent_class), "")
{
for (text, class) in self.pending_elems.iter() {
string(self.out, Escape(text), *class, &self.href_context, false);
}
} else {
// We only want to "open" the tag ourselves if we have more than one pending and if the
// current parent tag is not the same as our pending content.
let close_tag = if self.pending_elems.len() > 1 && current_class.is_some() {
Some(enter_span(self.out, current_class.unwrap(), &self.href_context))
} else {
None
};
for (text, class) in self.pending_elems.iter() {
string(self.out, Escape(text), *class, &self.href_context, close_tag.is_none());
}
if let Some(close_tag) = close_tag {
exit_span(self.out, close_tag);
}
}
self.pending_elems.clear();
true
}
}
impl<'a, 'b, 'c, 'd, 'e> Drop for TokenHandler<'a, 'b, 'c, 'd, 'e> {
/// When leaving, we need to flush all pending data to not have missing content.
fn drop(&mut self) {
if self.pending_exit_span.is_some() {
self.handle_exit_span();
} else {
self.write_pending_elems(self.current_class);
}
}
}
/// Convert the given `src` source code into HTML by adding classes for highlighting.
///
/// This code is used to render code blocks (in the documentation) as well as the source code pages.
@ -206,21 +228,18 @@ fn write_code(
) {
// This replace allows to fix how the code source with DOS backline characters is displayed.
let src = src.replace("\r\n", "\n");
// It contains the closing tag and the associated `Class`.
let mut closing_tags: Vec<(&'static str, Class)> = Vec::new();
// This is used because we don't automatically generate the closing tag on `ExitSpan` in
// case an `EnterSpan` event with the same class follows.
let mut pending_exit_span: Option<Class> = None;
// The following two variables are used to group HTML elements with same `class` attributes
// to reduce the DOM size.
let mut current_class: Option<Class> = None;
// We need to keep the `Class` for each element because it could contain a `Span` which is
// used to generate links.
let mut pending_elems: Vec<(&str, Option<Class>)> = Vec::new();
let mut token_handler = TokenHandler {
out,
closing_tags: Vec::new(),
pending_exit_span: None,
current_class: None,
pending_elems: Vec::new(),
href_context,
};
Classifier::new(
&src,
href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
token_handler.href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
decoration_info,
)
.highlight(&mut |highlight| {
@ -228,75 +247,53 @@ fn write_code(
Highlight::Token { text, class } => {
// If we received a `ExitSpan` event and then have a non-compatible `Class`, we
// need to close the `<span>`.
if let Some(pending) = pending_exit_span &&
let need_current_class_update = if let Some(pending) = token_handler.pending_exit_span &&
!can_merge(Some(pending), class, text) {
handle_exit_span(
out,
&href_context,
&mut pending_elems,
&mut closing_tags,
);
pending_exit_span = None;
current_class = class.map(Class::dummy);
token_handler.handle_exit_span();
true
// If the two `Class` are different, time to flush the current content and start
// a new one.
} else if !can_merge(current_class, class, text) {
write_pending_elems(
out,
&href_context,
&mut pending_elems,
&mut current_class,
&closing_tags,
);
current_class = class.map(Class::dummy);
} else if current_class.is_none() {
current_class = class.map(Class::dummy);
} else if !can_merge(token_handler.current_class, class, text) {
token_handler.write_pending_elems(token_handler.current_class);
true
} else {
token_handler.current_class.is_none()
};
if need_current_class_update {
token_handler.current_class = class.map(Class::dummy);
}
pending_elems.push((text, class));
token_handler.pending_elems.push((text, class));
}
Highlight::EnterSpan { class } => {
let mut should_add = true;
if pending_exit_span.is_some() {
if !can_merge(Some(class), pending_exit_span, "") {
handle_exit_span(out, &href_context, &mut pending_elems, &mut closing_tags);
} else {
if let Some(pending_exit_span) = token_handler.pending_exit_span {
if class.is_equal_to(pending_exit_span) {
should_add = false;
} else {
token_handler.handle_exit_span();
}
} else {
// We flush everything just in case...
write_pending_elems(
out,
&href_context,
&mut pending_elems,
&mut current_class,
&closing_tags,
);
if token_handler.write_pending_elems(token_handler.current_class) {
token_handler.current_class = None;
}
}
current_class = None;
pending_exit_span = None;
if should_add {
let closing_tag = enter_span(out, class, &href_context);
closing_tags.push((closing_tag, class));
let closing_tag = enter_span(token_handler.out, class, &token_handler.href_context);
token_handler.closing_tags.push((closing_tag, class));
}
token_handler.current_class = None;
token_handler.pending_exit_span = None;
}
Highlight::ExitSpan => {
current_class = None;
pending_exit_span =
Some(closing_tags.last().as_ref().expect("ExitSpan without EnterSpan").1);
token_handler.current_class = None;
token_handler.pending_exit_span =
Some(token_handler.closing_tags.last().as_ref().expect("ExitSpan without EnterSpan").1);
}
};
});
if pending_exit_span.is_some() {
handle_exit_span(out, &href_context, &mut pending_elems, &mut closing_tags);
} else {
write_pending_elems(
out,
&href_context,
&mut pending_elems,
&mut current_class,
&closing_tags,
);
}
}
fn write_footer(out: &mut Buffer, playground_button: Option<&str>) {