Skip to main content

latexml_core/document/
helpers.rs

1use libxml::tree::Node;
2
3use super::{Document, get_node_qname};
4use crate::common::{error::*, xml::XML_NS};
5
6/// In some cases we could have e.g. a \noindent followed by a {table},
7/// in which case we end up with an empty ltx:para which we can prune.
8pub fn prune_empty_para(document: &mut Document, node: &mut Node) -> Result<()> {
9  let children = node.get_child_elements();
10  if children.is_empty() {
11    let prev_is_para = match node.get_prev_element_sibling() {
12      None => false,
13      Some(prev) => {
14        if prev.get_name() == "_spilled_" {
15          // Streaming pass 1: earlier siblings were spilled; the placeholder
16          // records the LAST spilled node's qname (see `spill_run`) so this
17          // test matches what the eager walk would have seen.
18          prev.get_attribute("last").as_deref() == Some("ltx:para")
19        } else {
20          get_node_qname(&prev) == crate::pin!("ltx:para")
21        }
22      },
23    };
24    if !prev_is_para {
25      // If `node` WAS the 1st child
26      document.add_class(&mut node.get_parent().unwrap(), "ltx_pruned_first")?;
27    }
28    // Decrement the ID counter on the ancestor that generated this node's id,
29    // so that the pruned para's id slot gets reused by the next para.
30    if let Some(id) = node.get_attribute_ns("id", XML_NS) {
31      // Extract the prefix from the id (e.g. "p7" → prefix "p", counter "7")
32      if let Some(pos) = id.rfind('.') {
33        let suffix = &id[pos + 1..];
34        let prefix: String = suffix.chars().take_while(|c| !c.is_ascii_digit()).collect();
35        // Perl `Package.pm:939` — empty prefix uses `_ID_counter_` (single
36        // trailing underscore), not `_ID_counter__`.
37        let ctrkey = if prefix.is_empty() {
38          "_ID_counter_".to_string()
39        } else {
40          format!("_ID_counter_{}_", prefix)
41        };
42        if let Some(mut ancestor) = node.get_parent()
43          && let Some(ctr_str) = ancestor.get_attribute(&ctrkey)
44          && let Ok(ctr) = ctr_str.parse::<u32>()
45          && ctr > 0
46        {
47          ancestor.set_attribute(&ctrkey, &(ctr - 1).to_string())?;
48        }
49      } else {
50        // No dot — top-level id like "p7"
51        let prefix: String = id.chars().take_while(|c| !c.is_ascii_digit()).collect();
52        // Perl `Package.pm:939` — empty prefix uses `_ID_counter_` (single
53        // trailing underscore), not `_ID_counter__`.
54        let ctrkey = if prefix.is_empty() {
55          "_ID_counter_".to_string()
56        } else {
57          format!("_ID_counter_{}_", prefix)
58        };
59        // Find the ancestor with the counter (root element)
60        if let Some(root) = document.get_document().get_root_element() {
61          let mut root = root;
62          if let Some(ctr_str) = root.get_attribute(&ctrkey)
63            && let Ok(ctr) = ctr_str.parse::<u32>()
64            && ctr > 0
65          {
66            root.set_attribute(&ctrkey, &(ctr - 1).to_string())?;
67          }
68        }
69      }
70      document.unrecord_id(&id);
71    }
72    node.unlink();
73  }
74  Ok(())
75}