use super::*;
fn is_head_content_tag(tag_id: Option<u8>) -> bool {
matches!(
tag_id,
Some(
TAG_TITLE
| TAG_META
| TAG_LINK
| TAG_BASE
| TAG_STYLE
| TAG_SCRIPT
| TAG_NOSCRIPT
| TAG_TEMPLATE
)
)
}
const CLOSES_P: [bool; MAX_TAG_ID] = {
let mut t = [false; MAX_TAG_ID];
t[TAG_DIV as usize] = true;
t[TAG_P as usize] = true;
t[TAG_UL as usize] = true;
t[TAG_OL as usize] = true;
t[TAG_DL as usize] = true;
t[TAG_LI as usize] = true;
t[TAG_DD as usize] = true;
t[TAG_DT as usize] = true;
t[TAG_TABLE as usize] = true;
t[TAG_H1 as usize] = true;
t[TAG_H2 as usize] = true;
t[TAG_H3 as usize] = true;
t[TAG_H4 as usize] = true;
t[TAG_H5 as usize] = true;
t[TAG_H6 as usize] = true;
t[TAG_BLOCKQUOTE as usize] = true;
t[TAG_SECTION as usize] = true;
t[TAG_ARTICLE as usize] = true;
t[TAG_HEADER as usize] = true;
t[TAG_FOOTER as usize] = true;
t[TAG_NAV as usize] = true;
t[TAG_ASIDE as usize] = true;
t[TAG_PRE as usize] = true;
t[TAG_HR as usize] = true;
t[TAG_FORM as usize] = true;
t[TAG_FIELDSET as usize] = true;
t[TAG_FIGURE as usize] = true;
t[TAG_FIGCAPTION as usize] = true;
t[TAG_ADDRESS as usize] = true;
t[TAG_MAIN as usize] = true;
t[TAG_CENTER as usize] = true;
t[TAG_DETAILS as usize] = true;
t[TAG_SUMMARY as usize] = true;
t[TAG_DIALOG as usize] = true;
t
};
const NEEDS_IMPLIED_END_RECOVERY: [bool; MAX_TAG_ID] = {
let mut t = CLOSES_P;
t[TAG_A as usize] = true;
t[TAG_TD as usize] = true;
t[TAG_TH as usize] = true;
t[TAG_TR as usize] = true;
t[TAG_THEAD as usize] = true;
t[TAG_TBODY as usize] = true;
t[TAG_TFOOT as usize] = true;
t
};
#[inline]
fn closes_p(tag_id: u8) -> bool {
CLOSES_P[tag_id as usize]
}
#[inline]
fn needs_implied_end_recovery(tag_id: u8) -> bool {
(tag_id as usize) < MAX_TAG_ID && NEEDS_IMPLIED_END_RECOVERY[tag_id as usize]
}
fn is_p_scope_boundary(tag_id: u8) -> bool {
matches!(
tag_id,
TAG_BUTTON
| TAG_TD
| TAG_TH
| TAG_CAPTION
| TAG_TABLE
| TAG_TEMPLATE
| TAG_HTML
| TAG_UL
| TAG_OL
| TAG_DL
| TAG_LI
)
}
fn is_li_scope_boundary(tag_id: u8) -> bool {
matches!(
tag_id,
TAG_UL | TAG_OL | TAG_TABLE | TAG_TD | TAG_TH | TAG_CAPTION | TAG_TEMPLATE | TAG_HTML
)
}
fn is_dl_scope_boundary(tag_id: u8) -> bool {
matches!(
tag_id,
TAG_DL
| TAG_UL
| TAG_OL
| TAG_LI
| TAG_TABLE
| TAG_TD
| TAG_TH
| TAG_CAPTION
| TAG_TEMPLATE
| TAG_HTML
)
}
fn is_cell_scope_boundary(tag_id: u8) -> bool {
matches!(
tag_id,
TAG_TR | TAG_THEAD | TAG_TBODY | TAG_TFOOT | TAG_TABLE | TAG_CAPTION | TAG_TEMPLATE | TAG_HTML
)
}
fn is_a_scope_boundary(tag_id: u8) -> bool {
matches!(
tag_id,
TAG_P
| TAG_DIV
| TAG_LI
| TAG_UL
| TAG_OL
| TAG_DL
| TAG_DD
| TAG_DT
| TAG_TABLE
| TAG_TD
| TAG_TH
| TAG_TR
| TAG_CAPTION
| TAG_BLOCKQUOTE
| TAG_SECTION
| TAG_ARTICLE
| TAG_HEADER
| TAG_FOOTER
| TAG_NAV
| TAG_ASIDE
| TAG_MAIN
| TAG_FORM
| TAG_FIELDSET
| TAG_FIGURE
| TAG_BUTTON
| TAG_H1
| TAG_H2
| TAG_H3
| TAG_H4
| TAG_H5
| TAG_H6
| TAG_TEMPLATE
| TAG_HTML
)
}
fn is_hidden(node: &ElementNode) -> bool {
if let Some(style) = node.attributes.get("style")
&& (style.contains("display:none")
|| style.contains("display: none")
|| style.contains("visibility:hidden")
|| style.contains("visibility: hidden")
|| style.contains("position:absolute")
|| style.contains("position: absolute")
|| style.contains("position:fixed")
|| style.contains("position: fixed"))
{
return true;
}
matches!(node.attributes.get("hidden"), Some(v) if !v.eq_ignore_ascii_case("until-found"))
}
impl ConvertState {
pub(crate) fn process_text_buffer(&mut self, text_buffer: &mut String) {
let contains_non_whitespace = self.text_buffer_contains_non_whitespace;
let contains_whitespace = self.text_buffer_contains_whitespace;
self.text_buffer_contains_non_whitespace = false;
self.text_buffer_contains_whitespace = false;
let mut excludes_text_nodes = self
.stack
.last()
.is_some_and(|parent| parent.excludes_text_nodes || parent.excluded_from_markdown);
if self.has_isolate_main {
if self.isolate_main_found {
if self.isolate_main_closed {
excludes_text_nodes = true;
}
} else if self.isolate_first_header_depth.is_none() {
if self.depth_map[TAG_HEAD as usize] == 0 {
excludes_text_nodes = true;
}
} else if self.isolate_after_footer {
excludes_text_nodes = true;
}
}
if self.has_frontmatter
&& self.frontmatter_in_head
&& self
.stack
.last()
.is_some_and(|p| p.tag_id == Some(TAG_TITLE))
{
let val = text_buffer.trim().to_string();
if !val.is_empty() {
self.frontmatter_title = Some(val);
}
text_buffer.clear();
return;
}
let in_pre_tag = self.in_pre;
let child_text_node_index = self.stack.last().map_or(0, |n| n.child_text_node_index);
if !in_pre_tag && !contains_non_whitespace && child_text_node_index == 0 {
return;
}
if text_buffer.is_empty() {
return;
}
let first_block_parent_index = self.first_block_parent_index;
let first_block_child_text_count =
first_block_parent_index.map_or(0, |idx| self.stack[idx].child_text_node_index);
let mut text = std::mem::take(text_buffer);
let is_first_text_in_block = first_block_child_text_count == 0
&& (first_block_parent_index.is_some()
|| self.buffer.is_empty()
|| self.buffer.as_bytes().last() == Some(&b'\n'));
if contains_whitespace && is_first_text_in_block {
let mut start = 0;
let bytes = text.as_bytes();
while start < bytes.len()
&& (if in_pre_tag {
bytes[start] == NEWLINE_CHAR || bytes[start] == CARRIAGE_RETURN_CHAR
} else {
is_whitespace(bytes[start])
})
{
start += 1;
}
if start > 0 {
text.drain(..start);
}
}
if self.has_encoded_html_entity {
if let Cow::Owned(decoded) = decode_html_entities(&text) {
text = decoded;
}
self.has_encoded_html_entity = false;
}
if self.has_tailwind
&& let Some(parent) = self.stack.last()
&& let Some(tw) = &parent.tailwind
{
if tw.hidden {
excludes_text_nodes = true;
} else if !excludes_text_nodes {
let mut modified = false;
let mut new_text = String::new();
if let Some(p) = &tw.prefix {
new_text.push_str(p);
modified = true;
}
new_text.push_str(&text);
if let Some(s) = &tw.suffix {
new_text.push_str(s);
modified = true;
}
if modified {
text = fix_redundant_delimiters(&new_text);
}
}
}
if !self.extraction_tracked.is_empty() {
let current_depth = self.stack.len();
for tracked in &mut self.extraction_tracked {
if tracked.stack_depth <= current_depth {
tracked.text_content.push_str(&text);
}
}
}
if !excludes_text_nodes {
let depth = self.depth;
let index = self.stack.last().map_or(0, |n| n.current_walk_index);
self.emit_text(&text, contains_whitespace, depth, index);
}
text.clear();
*text_buffer = text;
if let Some(parent) = self.stack.last_mut() {
parent.current_walk_index += 1;
}
let up_to = first_block_parent_index.unwrap_or(0);
for idx in up_to..self.stack.len() {
self.stack[idx].child_text_node_index += 1;
}
}
fn close_implied_to(&mut self, target: fn(u8) -> bool, boundary: fn(u8) -> bool) {
let mut close_count = 0usize;
let mut found = false;
for node in self.stack.iter().rev() {
match node.tag_id {
Some(id) if target(id) => {
close_count += 1;
found = true;
break;
}
Some(id) if boundary(id) => break,
_ => close_count += 1,
}
}
if found {
for _ in 0..close_count {
self.close_node();
}
}
}
fn close_table_context(&mut self, closeable: fn(u8) -> bool) {
while let Some(top) = self.stack.last() {
match top.tag_id {
Some(id) if closeable(id) => self.close_node(),
_ => break,
}
}
}
pub(crate) fn process_opening_tag(
&mut self,
tag_name: &str,
tag_id: Option<u8>,
is_builtin: bool,
html_chunk: &str,
position: usize,
) -> OpeningTagResult {
let tag_handler = tag_id.and_then(get_tag_handler);
let needs_attrs = tag_handler.is_some_and(|h| h.needs_attributes)
|| self.has_tailwind
|| self.has_filter
|| self.has_extraction
|| self.has_tag_overrides
|| self.has_frontmatter;
let (complete, new_position, attributes, self_closing) =
process_tag_attributes(html_chunk, position, tag_handler, !needs_attrs);
if !complete {
return OpeningTagResult {
complete: false,
new_position: position,
self_closing: false,
skip: false,
};
}
if self.depth_map[TAG_HEAD as usize] > 0 && !is_head_content_tag(tag_id) {
while self
.stack
.last()
.is_some_and(|n| n.tag_id != Some(TAG_HEAD))
{
self.close_node();
}
if self
.stack
.last()
.is_some_and(|n| n.tag_id == Some(TAG_HEAD))
{
self.close_node();
}
}
if let Some(id) = tag_id
&& needs_implied_end_recovery(id)
{
match id {
TAG_A if self.depth_map[TAG_A as usize] > 0 => {
self.close_implied_to(|t| t == TAG_A, is_a_scope_boundary);
}
TAG_TD | TAG_TH | TAG_TR | TAG_THEAD | TAG_TBODY | TAG_TFOOT
if self.depth_map[TAG_TABLE as usize] > 0 =>
{
match id {
TAG_TD | TAG_TH
if self.depth_map[TAG_TD as usize] > 0 || self.depth_map[TAG_TH as usize] > 0 =>
{
self.close_implied_to(|t| t == TAG_TD || t == TAG_TH, is_cell_scope_boundary);
}
TAG_TR if self.depth_map[TAG_TR as usize] > 0 => {
self.close_table_context(|t| matches!(t, TAG_TD | TAG_TH | TAG_TR));
}
TAG_THEAD | TAG_TBODY | TAG_TFOOT => {
self.close_table_context(|t| {
matches!(
t,
TAG_TD | TAG_TH | TAG_TR | TAG_THEAD | TAG_TBODY | TAG_TFOOT | TAG_CAPTION
)
});
}
_ => {}
}
}
TAG_A | TAG_TD | TAG_TH | TAG_TR | TAG_THEAD | TAG_TBODY | TAG_TFOOT => {}
_ => {
if self.depth_map[TAG_P as usize] > 0 {
debug_assert!(closes_p(id));
self.close_implied_to(|t| t == TAG_P, is_p_scope_boundary);
}
match id {
TAG_H1 | TAG_H2 | TAG_H3 | TAG_H4 | TAG_H5 | TAG_H6
if self.stack.last().is_some_and(|n| {
matches!(
n.tag_id,
Some(TAG_H1 | TAG_H2 | TAG_H3 | TAG_H4 | TAG_H5 | TAG_H6)
)
}) =>
{
self.close_node();
}
TAG_LI if self.depth_map[TAG_LI as usize] > 0 => {
self.close_implied_to(|t| t == TAG_LI, is_li_scope_boundary);
}
TAG_DT | TAG_DD
if self.depth_map[TAG_DT as usize] > 0 || self.depth_map[TAG_DD as usize] > 0 =>
{
self.close_implied_to(|t| t == TAG_DT || t == TAG_DD, is_dl_scope_boundary);
}
_ => {}
}
}
}
}
if let Some(id) = tag_id {
debug_assert!(
(id as usize) < MAX_TAG_ID,
"tag_id {id} exceeds MAX_TAG_ID {MAX_TAG_ID}"
);
if (id as usize) < MAX_TAG_ID {
self.depth_map[id as usize] = self.depth_map[id as usize].saturating_add(1);
}
match id {
TAG_TABLE if !self.plain_text => self.escape_ctx |= ESC_TABLE,
TAG_CODE | TAG_PRE => {
if !self.plain_text {
self.escape_ctx |= ESC_CODE_PRE;
}
if id == TAG_PRE {
self.in_pre = true;
}
}
TAG_A if !self.plain_text => self.escape_ctx |= ESC_LINK,
TAG_BLOCKQUOTE if !self.plain_text => self.escape_ctx |= ESC_BLOCKQUOTE,
_ => {}
}
}
self.depth += 1;
let current_walk_index = self.stack.last().map_or(0, |n| n.current_walk_index);
let custom_name = if is_builtin {
None
} else {
Some(tag_name.to_string())
};
let (h_inline, h_excludes, h_non_nesting, h_collapses, h_spacing) = if let Some(h) = tag_handler
{
(
h.is_inline,
h.excludes_text_nodes,
h.is_non_nesting,
h.collapses_inner_white_space,
h.spacing,
)
} else if tag_id.is_none() {
(true, false, false, false, Some(NO_SPACING))
} else {
(false, false, false, false, None)
};
let mut tag = if let Some(mut pooled) = self.node_pool.pop() {
pooled.custom_name = custom_name;
pooled.attributes = attributes;
pooled.tag_id = tag_id;
pooled.depth = self.depth;
pooled.index = current_walk_index;
pooled.current_walk_index = 0;
pooled.child_text_node_index = 0;
pooled.contains_whitespace = false;
pooled.excluded_from_markdown = false;
pooled.tailwind = None;
pooled.is_inline = h_inline;
pooled.excludes_text_nodes = h_excludes;
pooled.is_non_nesting = h_non_nesting;
pooled.collapses_inner_white_space = h_collapses;
pooled.spacing = h_spacing;
pooled
} else {
ElementNode {
custom_name,
attributes,
tag_id,
depth: self.depth,
index: current_walk_index,
current_walk_index: 0,
child_text_node_index: 0,
contains_whitespace: false,
excluded_from_markdown: false,
tailwind: None,
is_inline: h_inline,
excludes_text_nodes: h_excludes,
is_non_nesting: h_non_nesting,
collapses_inner_white_space: h_collapses,
spacing: h_spacing,
}
};
let mut skip_node = false;
let mut filter_excluded = false;
if self.has_plugins {
if self.has_tailwind {
let parent_hidden = self
.stack
.last()
.and_then(|p| p.tailwind.as_ref())
.is_some_and(|tw| tw.hidden);
if let Some(class_attr) = tag.attributes.get("class") {
let (mut prefix, mut suffix, hidden) = process_tailwind_classes(class_attr);
if self.plain_text {
prefix = None;
suffix = None;
}
let hidden = hidden || parent_hidden;
if prefix.is_some() || suffix.is_some() || hidden {
tag.tailwind = Some(Box::new(TailwindData {
prefix,
suffix,
hidden,
}));
if hidden {
skip_node = true;
}
}
} else if parent_hidden {
tag.tailwind = Some(Box::new(TailwindData {
prefix: None,
suffix: None,
hidden: true,
}));
skip_node = true;
}
}
if self.has_filter {
if self.hidden_since_depth.is_some() {
skip_node = true;
filter_excluded = true;
} else if is_hidden(&tag) {
skip_node = true;
filter_excluded = true;
self.hidden_since_depth = Some(self.depth);
}
if !skip_node {
for (_, parsed) in &self.filter_exclude_parsed {
if matches_selector(&tag, parsed) {
skip_node = true;
filter_excluded = true;
break;
}
}
}
if !skip_node {
for parent in &self.stack {
for (_, parsed) in &self.filter_exclude_parsed {
if matches_selector(parent, parsed) {
skip_node = true;
filter_excluded = true;
break;
}
}
if skip_node {
break;
}
}
}
if !skip_node && !self.filter_include_parsed.is_empty() {
let mut match_found = false;
for (_, parsed) in &self.filter_include_parsed {
if matches_selector(&tag, parsed) {
match_found = true;
break;
}
}
if !match_found && self.filter_process_children {
for parent in &self.stack {
for (_, parsed) in &self.filter_include_parsed {
if matches_selector(parent, parsed) {
match_found = true;
break;
}
}
if match_found {
break;
}
}
}
if !match_found {
skip_node = true;
filter_excluded = true;
}
}
}
if self.has_isolate_main {
let is_main = tag_id == Some(TAG_MAIN);
if !self.isolate_main_found && is_main && self.depth <= 50 {
self.isolate_main_found = true;
}
if self.isolate_main_found {
if self.isolate_main_closed {
skip_node = true;
}
} else {
let is_header = tag_id.is_some_and(|id| (TAG_H1..=TAG_H6).contains(&id));
if self.isolate_first_header_depth.is_none()
&& is_header
&& self.depth_map[TAG_HEADER as usize] == 0
{
self.isolate_first_header_depth = Some(self.depth);
}
if let Some(header_depth) = self.isolate_first_header_depth
&& !self.isolate_after_footer
&& tag_id == Some(TAG_FOOTER)
&& self.depth.saturating_sub(header_depth) <= 5
{
self.isolate_after_footer = true;
skip_node = true;
}
if self.isolate_first_header_depth.is_none() {
if tag_id != Some(TAG_HEAD) && self.depth_map[TAG_HEAD as usize] == 0 {
skip_node = true;
}
} else if self.isolate_after_footer {
skip_node = true;
}
}
}
if self.has_frontmatter {
if tag_id == Some(TAG_HEAD) {
self.frontmatter_in_head = true;
} else if self.frontmatter_in_head && tag_id == Some(TAG_META) {
let name = tag
.attributes
.get("name")
.or_else(|| tag.attributes.get("property"));
let content = tag.attributes.get("content");
if let (Some(n), Some(c)) = (name, content) {
let n_str = n.as_str();
let is_allowed = match n_str {
"description"
| "keywords"
| "author"
| "date"
| "og:title"
| "og:description"
| "twitter:title"
| "twitter:description" => true,
_ => self
.options
.plugins
.as_ref()
.and_then(|p| p.frontmatter.as_ref())
.and_then(|f| f.meta_fields.as_ref())
.is_some_and(|allowed| allowed.iter().any(|a| a == n_str)),
};
if is_allowed {
if let Some(entry) = self.frontmatter_meta.iter_mut().find(|(k, _)| k == n) {
entry.1.clone_from(c);
} else {
self.frontmatter_meta.push((n.clone(), c.clone()));
}
}
}
}
}
}
tag.excluded_from_markdown =
filter_excluded || (skip_node && (!self.has_isolate_main || self.isolate_main_found));
if tag.collapses_inner_white_space && !filter_excluded {
if tag.tag_id == Some(TAG_SPAN) {
self.collapse_span_depth = self.collapse_span_depth.saturating_add(1);
} else {
self.collapse_non_span_depth = self.collapse_non_span_depth.saturating_add(1);
}
}
if let Some(last) = self.stack.last_mut() {
last.current_walk_index += 1;
}
if !tag.is_inline {
let idx = self.stack.len();
self.first_block_parent_index = Some(idx);
self.block_parent_indices.push(idx);
}
self.stack.push(tag);
if !self.extraction_parsed_selectors.is_empty()
&& let Some(element) = self.stack.last()
{
let stack_depth = self.stack.len();
for (selector, parsed) in &self.extraction_parsed_selectors {
if matches_selector(element, parsed) {
let attrs: Vec<(String, String)> = element
.attributes
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
self.extraction_tracked.push(TrackedExtraction {
selector: selector.clone(),
stack_depth,
text_content: String::new(),
tag_name: element.name().to_string(),
attributes: attrs,
});
}
}
}
if !skip_node {
self.emit_enter_element();
}
if tag_id == Some(TAG_LI)
&& let Some(li) = self.stack.last()
{
let width: usize = if !skip_node && !self.in_table_cell() && !self.plain_text {
let stack_len = self.stack.len();
let parent_is_ordered = stack_len >= 2 && self.stack[stack_len - 2].tag_id == Some(TAG_OL);
if parent_is_ordered {
let n = li.index + 1;
let digits = (n.ilog10() + 1) as usize;
digits + 2
} else {
2
}
} else {
0
};
self
.list_indent_widths
.push(u8::try_from(width).unwrap_or(u8::MAX));
for _ in 0..width {
self.list_indent.push(' ');
}
}
self.has_encoded_html_entity = false;
if self.stack.last().is_some_and(|n| n.is_non_nesting) && !self_closing {
self.in_non_nesting = true;
self.in_rawtext_quote_aware = matches!(tag_id, Some(TAG_SCRIPT | TAG_STYLE));
self.rawtext_quote = 0;
self.rawtext_escaped = false;
}
if !self_closing {
self.just_closed_tag = false;
}
OpeningTagResult {
complete: true,
new_position,
self_closing,
skip: false,
}
}
pub(crate) fn close_node(&mut self) {
if self.stack.is_empty() {
return;
}
if !self.extraction_tracked.is_empty() {
let current_depth = self.stack.len();
let mut i = 0;
while i < self.extraction_tracked.len() {
if self.extraction_tracked[i].stack_depth == current_depth {
let tracked = self.extraction_tracked.swap_remove(i);
self.extraction_results.push(ExtractedElement {
selector: tracked.selector,
tag_name: tracked.tag_name,
text_content: tracked.text_content.trim().to_string(),
attributes: tracked.attributes,
});
} else {
i += 1;
}
}
}
let popping_index = self.stack.len() - 1;
let Some(node) = self.stack.pop() else { return };
if self.hidden_since_depth == Some(node.depth) {
self.hidden_since_depth = None;
}
if self.first_block_parent_index == Some(popping_index) {
self.block_parent_indices.pop();
self.first_block_parent_index = self
.block_parent_indices
.last()
.copied()
.or(if self.stack.is_empty() { None } else { Some(0) });
}
if node.collapses_inner_white_space && !node.excluded_from_markdown {
if node.tag_id == Some(TAG_SPAN) {
self.collapse_span_depth = self.collapse_span_depth.saturating_sub(1);
} else {
self.collapse_non_span_depth = self.collapse_non_span_depth.saturating_sub(1);
}
}
if self.has_isolate_main
&& node.tag_id == Some(TAG_MAIN)
&& self.isolate_main_found
&& !self.isolate_main_closed
{
self.isolate_main_closed = true;
}
if self.has_frontmatter && node.tag_id == Some(TAG_HEAD) && self.frontmatter_in_head {
self.frontmatter_in_head = false;
self.generate_frontmatter_yaml();
}
if node.tag_id == Some(TAG_A) && node.child_text_node_index == 0 && !node.excluded_from_markdown
{
let prefix = node
.attributes
.get("title")
.or_else(|| node.attributes.get("aria-label"))
.cloned()
.unwrap_or_default();
if !prefix.is_empty() {
let node_depth = node.depth;
let node_tag_id = node.tag_id;
let mut modified_node = node;
modified_node.child_text_node_index = 1;
let text_depth = node_depth + 1;
self.stack.push(modified_node);
self.emit_text(&prefix, false, text_depth, 0);
for prev in &mut self.stack {
prev.child_text_node_index += 1;
}
let Some(modified_node2) = self.stack.pop() else {
return;
};
self.emit_exit_element(&modified_node2);
self.recycle_node(modified_node2);
if let Some(id) = node_tag_id {
debug_assert!(
(id as usize) < MAX_TAG_ID,
"tag_id {id} exceeds MAX_TAG_ID {MAX_TAG_ID}"
);
if (id as usize) < MAX_TAG_ID {
self.depth_map[id as usize] = self.depth_map[id as usize].saturating_sub(1);
}
self.update_escape_ctx_on_close(id);
if id == TAG_LI
&& let Some(w) = self.list_indent_widths.pop()
{
let new_len = self.list_indent.len().saturating_sub(w as usize);
self.list_indent.truncate(new_len);
}
}
self.depth -= 1;
self.has_encoded_html_entity = false;
self.just_closed_tag = true;
self.in_non_nesting = self.stack.last().is_some_and(|n| n.is_non_nesting);
self.reset_rawtext_quote_state();
return;
}
}
self.emit_exit_element(&node);
let node_tag_id = node.tag_id;
self.recycle_node(node);
if let Some(id) = node_tag_id {
debug_assert!(
(id as usize) < MAX_TAG_ID,
"tag_id {id} exceeds MAX_TAG_ID {MAX_TAG_ID}"
);
if (id as usize) < MAX_TAG_ID {
self.depth_map[id as usize] = self.depth_map[id as usize].saturating_sub(1);
}
self.update_escape_ctx_on_close(id);
if id == TAG_LI
&& let Some(w) = self.list_indent_widths.pop()
{
let new_len = self.list_indent.len().saturating_sub(w as usize);
self.list_indent.truncate(new_len);
}
}
self.in_non_nesting = self.stack.last().is_some_and(|n| n.is_non_nesting);
self.reset_rawtext_quote_state();
self.depth -= 1;
self.has_encoded_html_entity = false;
self.just_closed_tag = true;
}
#[inline]
fn reset_rawtext_quote_state(&mut self) {
if !self.in_non_nesting {
self.in_rawtext_quote_aware = false;
self.rawtext_quote = 0;
self.rawtext_escaped = false;
}
}
pub(crate) fn process_closing_tag(
&mut self,
html_chunk: &str,
position: usize,
) -> CloseTagResult {
let mut i = position + 2;
let tag_name_start = i;
let bytes = html_chunk.as_bytes();
let chunk_length = bytes.len();
let mut found_close = false;
while i < chunk_length {
if bytes[i] == GT_CHAR {
found_close = true;
break;
}
i += 1;
}
if !found_close {
return CloseTagResult {
complete: false,
new_position: position,
remaining_start: position,
};
}
let tag_name_raw = html_chunk[tag_name_start..i].trim();
let builtin_tag_id = crate::consts::get_tag_id_ci_bytes(tag_name_raw.as_bytes());
let tag_name: Cow<str> = if builtin_tag_id.is_some() {
Cow::Borrowed(tag_name_raw)
} else if tag_name_raw.bytes().any(|b| b.is_ascii_uppercase()) {
Cow::Owned(tag_name_raw.to_ascii_lowercase())
} else {
Cow::Borrowed(tag_name_raw)
};
let tag_id = if builtin_tag_id.is_some() {
builtin_tag_id
} else {
self
.options
.plugins
.as_ref()
.and_then(|p| p.tag_overrides.as_ref())
.and_then(|ovs| {
ovs
.iter()
.find(|(k, _)| k == tag_name.as_ref())
.map(|(_, v)| v)
})
.and_then(|ov| ov.alias_tag_id)
};
if let Some(curr) = self.stack.last()
&& curr.is_non_nesting
&& curr.tag_id != tag_id
{
return CloseTagResult {
complete: false,
new_position: position,
remaining_start: position,
};
}
let close_name: &str = tag_name.as_ref();
let needs_name_match = builtin_tag_id.is_none() && tag_id.is_some();
let matches = |node: &ElementNode| -> bool {
if node.tag_id != tag_id {
return false;
}
if !needs_name_match {
return true;
}
node.custom_name.as_deref() == Some(close_name)
};
if let Some(top) = self.stack.last() {
if matches(top) {
self.close_node();
} else {
let mut pop_count = 0;
for j in (0..self.stack.len()).rev() {
pop_count += 1;
if matches(&self.stack[j]) {
break;
}
}
for _ in 0..pop_count {
if !self.stack.is_empty() {
self.close_node();
}
}
}
}
self.just_closed_tag = true;
CloseTagResult {
complete: true,
new_position: i + 1,
remaining_start: 0,
}
}
pub(crate) fn process_cdata_section(&mut self, content: &str) {
if !self.has_tag_overrides {
return;
}
let Some(tag_id) = self
.options
.plugins
.as_ref()
.and_then(|p| p.tag_overrides.as_ref())
.and_then(|ovs| ovs.iter().find(|(k, _)| k == "#cdata-section"))
.map(|(_, ov)| ov.alias_tag_id)
else {
return;
};
let result = self.process_opening_tag("#cdata-section", tag_id, false, ">", 0);
if !result.complete {
return;
}
if !result.self_closing && !content.is_empty() {
let excluded = self
.stack
.last()
.is_some_and(|n| n.excluded_from_markdown || n.excludes_text_nodes);
if !excluded {
let depth = self.depth;
let index = self.stack.last().map_or(0, |n| n.current_walk_index);
self.emit_text(content, false, depth, index);
}
if let Some(parent) = self.stack.last_mut() {
parent.current_walk_index += 1;
parent.child_text_node_index += 1;
}
}
if !result.self_closing {
self.close_node();
}
}
#[inline]
pub(crate) fn recycle_node(&mut self, mut node: ElementNode) {
node.attributes.clear();
node.custom_name = None;
node.tailwind = None;
self.node_pool.push(node);
}
#[inline]
pub(crate) fn update_escape_ctx_on_close(&mut self, id: u8) {
match id {
TAG_TABLE if self.depth_map[id as usize] == 0 => self.escape_ctx &= !ESC_TABLE,
TAG_CODE
if self.depth_map[TAG_CODE as usize] == 0 && self.depth_map[TAG_PRE as usize] == 0 =>
{
self.escape_ctx &= !ESC_CODE_PRE;
}
TAG_PRE if self.depth_map[TAG_PRE as usize] == 0 => {
self.in_pre = false;
if self.depth_map[TAG_CODE as usize] == 0 {
self.escape_ctx &= !ESC_CODE_PRE;
}
}
TAG_A if self.depth_map[id as usize] == 0 => self.escape_ctx &= !ESC_LINK,
TAG_BLOCKQUOTE if self.depth_map[id as usize] == 0 => self.escape_ctx &= !ESC_BLOCKQUOTE,
_ => {}
}
}
}