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/// Does an `ltx:para` element immediately precede `node`? Both `prune_empty_para`
7/// (to decide `ltx_pruned_first`) and the first-paragraph `ltx_noindent` stamp
8/// (`tex_paragraph.rs`, OXIDIZED_DESIGN #143) ask this: a paragraph with no
9/// preceding `ltx:para` sibling is the first paragraph of its parent, structurally
10/// — a signal robust to how many stray `\par`s fired before it (which varies by
11/// TeX Live year / dump vs no-dump).
12pub fn preceding_para_sibling(node: &Node) -> bool {
13  match node.get_prev_element_sibling() {
14    None => false,
15    Some(prev) => {
16      if prev.get_name() == "_spilled_" {
17        // Streaming pass 1: earlier siblings were spilled; the placeholder
18        // records the LAST spilled node's qname (see `spill_run`) so this
19        // test matches what the eager walk would have seen.
20        prev.get_attribute("last").as_deref() == Some("ltx:para")
21      } else {
22        get_node_qname(&prev) == crate::pin!("ltx:para")
23      }
24    },
25  }
26}
27
28/// In some cases we could have e.g. a \noindent followed by a {table},
29/// in which case we end up with an empty ltx:para which we can prune.
30pub fn prune_empty_para(document: &mut Document, node: &mut Node) -> Result<()> {
31  let children = node.get_child_elements();
32  if children.is_empty() {
33    let prev_is_para = preceding_para_sibling(node);
34    if !prev_is_para {
35      // If `node` WAS the 1st child
36      document.add_class(&mut node.get_parent().unwrap(), "ltx_pruned_first")?;
37    }
38    // Decrement the ID counter on the ancestor that generated this node's id,
39    // so that the pruned para's id slot gets reused by the next para.
40    if let Some(id) = node.get_attribute_ns("id", XML_NS) {
41      // Extract the prefix from the id (e.g. "p7" → prefix "p", counter "7")
42      if let Some(pos) = id.rfind('.') {
43        let suffix = &id[pos + 1..];
44        let prefix: String = suffix.chars().take_while(|c| !c.is_ascii_digit()).collect();
45        // Perl `Package.pm:939` — empty prefix uses `_ID_counter_` (single
46        // trailing underscore), not `_ID_counter__`.
47        let ctrkey = if prefix.is_empty() {
48          "_ID_counter_".to_string()
49        } else {
50          format!("_ID_counter_{}_", prefix)
51        };
52        if let Some(mut ancestor) = node.get_parent()
53          && let Some(ctr_str) = ancestor.get_attribute(&ctrkey)
54          && let Ok(ctr) = ctr_str.parse::<u32>()
55          && ctr > 0
56        {
57          ancestor.set_attribute(&ctrkey, &(ctr - 1).to_string())?;
58        }
59      } else {
60        // No dot — top-level id like "p7"
61        let prefix: String = id.chars().take_while(|c| !c.is_ascii_digit()).collect();
62        // Perl `Package.pm:939` — empty prefix uses `_ID_counter_` (single
63        // trailing underscore), not `_ID_counter__`.
64        let ctrkey = if prefix.is_empty() {
65          "_ID_counter_".to_string()
66        } else {
67          format!("_ID_counter_{}_", prefix)
68        };
69        // Find the ancestor with the counter (root element)
70        if let Some(root) = document.get_document().get_root_element() {
71          let mut root = root;
72          if let Some(ctr_str) = root.get_attribute(&ctrkey)
73            && let Ok(ctr) = ctr_str.parse::<u32>()
74            && ctr > 0
75          {
76            root.set_attribute(&ctrkey, &(ctr - 1).to_string())?;
77          }
78        }
79      }
80      document.unrecord_id(&id);
81    }
82    node.unlink();
83  }
84  Ok(())
85}