1use std::path::Path;
12
13use libxml::{
14 parser::Parser as XmlParser,
15 tree::{Document, Namespace, Node, NodeType},
16 xpath::Context as XPathContext,
17};
18use regex::Regex;
19use rustc_hash::{FxHashMap as HashMap, FxHashSet};
20use unicode_normalization::UnicodeNormalization;
21
22use crate::radix::radix_alpha;
23
24const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
25
26pub fn get_xml_id(node: &Node) -> Option<String> {
29 node
30 .get_attribute_ns("id", XML_NS)
31 .or_else(|| node.get_attribute("xml:id"))
32 .or_else(|| {
33 let props = node.get_properties();
35 props.get("id").cloned()
36 })
37}
38
39fn remap_fragid(id: &str, new_id: &str, fragid: &str) -> String {
47 let offset = id.len().saturating_sub(fragid.len());
48 new_id.get(offset..).unwrap_or(fragid).to_string()
49}
50
51pub const LTX_NSURI: &str = "http://dlmf.nist.gov/LaTeXML";
53
54fn huge_parse_options() -> libxml::parser::ParserOptions<'static> {
58 libxml::parser::ParserOptions {
59 huge: true,
60 ..Default::default()
61 }
62}
63
64pub(crate) fn is_ltx(node: &Node) -> bool {
69 node
70 .get_namespace()
71 .map(|ns| ns.get_href() == LTX_NSURI)
72 .unwrap_or(false)
73}
74
75fn scan_ids_and_pis(node: &Node, ids: &mut Vec<(String, Node)>, pis: &mut Vec<String>) {
85 let mut child = node.get_first_child();
86 while let Some(c) = child {
87 match c.get_type() {
88 Some(NodeType::ElementNode) => {
89 if let Some(id) = get_xml_id(&c) {
90 ids.push((id, c.clone()));
91 }
92 scan_ids_and_pis(&c, ids, pis);
93 },
94 Some(NodeType::PiNode) if c.get_name() == "latexml" => {
95 pis.push(c.get_content());
96 },
97 _ => {},
98 }
99 child = c.get_next_sibling();
100 }
101}
102
103pub(crate) struct SplitArm {
106 pub(crate) element: String,
108 pub(crate) any_of: Vec<SplitCond>,
110}
111
112pub(crate) enum SplitCond {
115 PrecedingSibling(String),
117 Parent(String),
119}
120
121pub(crate) fn parse_split_union(union_xpath: &str) -> Option<Vec<SplitArm>> {
127 let mut arms = Vec::new();
128 for raw in union_xpath.split('|') {
129 let arm = raw.trim();
130 let rest = arm.strip_prefix("//ltx:")?;
131 let (name, pred) = match rest.split_once('[') {
132 Some((n, p)) => (n.trim(), Some(p.strip_suffix(']')?.trim())),
133 None => (rest.trim(), None),
134 };
135 if name.is_empty()
136 || !name
137 .chars()
138 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
139 {
140 return None;
141 }
142 let mut any_of = Vec::new();
143 if let Some(pred) = pred {
144 for cond in pred.split(" or ") {
145 let cond = cond.trim();
146 if let Some(n) = cond.strip_prefix("preceding-sibling::ltx:") {
147 any_of.push(SplitCond::PrecedingSibling(n.trim().to_string()));
148 } else {
149 let n = cond.strip_prefix("parent::ltx:")?;
153 any_of.push(SplitCond::Parent(n.trim().to_string()));
154 }
155 }
156 }
157 arms.push(SplitArm {
158 element: name.to_string(),
159 any_of,
160 });
161 }
162 if arms.is_empty() { None } else { Some(arms) }
163}
164
165pub(crate) fn cond_matches(cond: &SplitCond, node: &Node) -> bool {
168 match cond {
169 SplitCond::PrecedingSibling(name) => {
170 let mut sib = node.get_prev_sibling();
171 while let Some(s) = sib {
172 if s.get_type() == Some(NodeType::ElementNode) && s.get_name() == *name && is_ltx(&s) {
173 return true;
174 }
175 sib = s.get_prev_sibling();
176 }
177 false
178 },
179 SplitCond::Parent(name) => node
180 .get_parent()
181 .map(|p| p.get_name() == *name && is_ltx(&p))
182 .unwrap_or(false),
183 }
184}
185
186#[derive(Debug)]
189struct WalkArm {
190 name: Option<String>,
192 preds: Vec<Vec<WalkPred>>,
195}
196
197#[derive(Debug)]
201enum WalkPred {
202 HasAttr(String),
203 NoAttr(String),
204 NoAncestor(String),
206 NoChild(String),
208}
209
210fn parse_walk_union(union_xpath: &str) -> Option<Vec<WalkArm>> {
223 let mut arms = Vec::new();
224 for raw in union_xpath.split('|') {
225 let arm = raw.trim().strip_prefix("//")?;
226 let (name_part, pred_part) = match arm.split_once('[') {
227 Some((n, p)) => (n.trim(), Some(p.strip_suffix(']')?.trim())),
228 None => (arm.trim(), None),
229 };
230 let name = match name_part {
231 "*" => None,
232 other => Some(other.strip_prefix("ltx:")?.to_string()),
233 };
234 let is_ncname = |n: &str| {
235 !n.is_empty()
236 && n
237 .chars()
238 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
239 };
240 if let Some(n) = &name
241 && !is_ncname(n)
242 {
243 return None;
244 }
245 let mut preds: Vec<Vec<WalkPred>> = Vec::new();
246 if let Some(pred) = pred_part {
247 if pred.contains('(') && pred.contains(" or ") && pred.contains(" and ") {
251 return None;
252 }
253 for disjunct in pred.split(" or ") {
254 let mut group = Vec::new();
255 for cond in disjunct.split(" and ") {
256 let cond = cond.trim();
257 let parsed = if let Some(attr) = cond.strip_prefix("@") {
258 is_ncname(attr).then(|| WalkPred::HasAttr(attr.to_string()))
259 } else if let Some(inner) = cond.strip_prefix("not(").and_then(|c| c.strip_suffix(")")) {
260 let inner = inner.trim();
261 if let Some(attr) = inner.strip_prefix("@") {
262 is_ncname(attr).then(|| WalkPred::NoAttr(attr.to_string()))
263 } else if let Some(n) = inner.strip_prefix("ancestor::ltx:") {
264 is_ncname(n).then(|| WalkPred::NoAncestor(n.to_string()))
265 } else if let Some(n) = inner.strip_prefix("ltx:") {
266 is_ncname(n).then(|| WalkPred::NoChild(n.to_string()))
267 } else {
268 None
269 }
270 } else {
271 None
272 };
273 group.push(parsed?);
274 }
275 preds.push(group);
276 }
277 }
278 arms.push(WalkArm { name, preds });
279 }
280 (!arms.is_empty()).then_some(arms)
281}
282
283fn walk_arm_matches(arm: &WalkArm, node: &Node) -> bool {
287 if let Some(want) = &arm.name
288 && (node.get_name() != *want || !is_ltx(node))
289 {
290 return false;
291 }
292 if arm.preds.is_empty() {
293 return true;
294 }
295 arm.preds.iter().any(|group| {
296 group.iter().all(|pred| match pred {
297 WalkPred::HasAttr(a) => node.has_attribute(a),
298 WalkPred::NoAttr(a) => !node.has_attribute(a),
299 WalkPred::NoAncestor(n) => {
300 let mut cur = node.get_parent();
301 while let Some(p) = cur {
302 if p.get_type() == Some(NodeType::ElementNode) && p.get_name() == *n && is_ltx(&p) {
303 return false;
304 }
305 cur = p.get_parent();
306 }
307 true
308 },
309 WalkPred::NoChild(n) => !node
310 .get_child_elements()
311 .iter()
312 .any(|c| c.get_name() == *n && is_ltx(c)),
313 })
314 })
315}
316
317fn collect_walk_matches(node: &Node, arms: &[WalkArm], out: &mut Vec<Node>) {
321 if node.get_type() == Some(NodeType::ElementNode)
322 && arms.iter().any(|arm| walk_arm_matches(arm, node))
323 {
324 out.push(node.clone());
325 }
326 for child in node.get_child_nodes() {
327 collect_walk_matches(&child, arms, out);
328 }
329}
330
331pub(crate) fn arm_matches(arm: &SplitArm, node: &Node) -> bool {
333 arm.any_of.is_empty() || arm.any_of.iter().any(|c| cond_matches(c, node))
334}
335
336pub(crate) fn collect_split_pages(node: &Node, arms: &[SplitArm], out: &mut Vec<Node>) {
340 let mut child = node.get_first_child();
341 while let Some(c) = child {
342 if c.get_type() == Some(NodeType::ElementNode) {
343 let name = c.get_name();
344 if arms.iter().any(|a| a.element == name)
347 && is_ltx(&c)
348 && arms.iter().any(|a| a.element == name && arm_matches(a, &c))
349 {
350 out.push(c.clone());
351 }
352 collect_split_pages(&c, arms, out);
353 }
354 child = c.get_next_sibling();
355 }
356}
357
358pub struct PostDocument {
363 document: Document,
365 pub destination: Option<String>,
367 pub destination_directory: Option<String>,
369 pub site_directory: Option<String>,
371 pub source: Option<String>,
373 pub source_directory: Option<String>,
375 pub searchpaths: Vec<String>,
377 pub namespaces: HashMap<String, String>,
379 pub namespace_uris: HashMap<String, String>,
381 idcache: HashMap<String, Node>,
383 idcache_reusable: HashMap<String, bool>,
385 idcache_reserve: HashMap<String, bool>,
387 idcache_clashes: HashMap<String, u32>,
389 pub processing_instructions: Vec<String>,
391 pub parent_document: Option<Box<PostDocument>>,
393 pub split_from_id: Option<String>,
395 pub validate: bool,
397 cache: HashMap<String, String>,
399 pub nocache: bool,
401 pending_xmath_unlinks: Vec<Node>,
414 nav_memo: Option<NavigationMemo>,
431}
432
433struct NavigationMemo {
435 element: Option<Node>,
437 refs: FxHashSet<(String, String)>,
439}
440
441impl Drop for PostDocument {
442 fn drop(&mut self) {
467 for (_, node) in std::mem::take(&mut self.idcache) {
468 node.set_linked();
475 }
476 }
477}
478
479impl PostDocument {
480 pub fn new(doc: Document, options: PostDocumentOptions) -> Self {
484 let mut pd = Self::new_internal(doc, options);
489 pd.set_document_internal();
490 pd
491 }
492
493 fn new_internal(doc: Document, options: PostDocumentOptions) -> Self {
494 let mut dest_dir = options.destination_directory.clone();
495 if options.destination.is_some() && dest_dir.is_none() {
496 if let Some(ref dest) = options.destination {
497 if let Some(parent) = Path::new(dest).parent() {
498 let parent_str = parent.to_string_lossy().to_string();
499 if parent_str.is_empty() {
501 dest_dir = Some(".".to_string());
502 } else {
503 dest_dir = Some(parent_str);
504 }
505 }
506 }
507 }
508
509 let site_dir = if let Some(ref sd) = options.site_directory {
510 Some(sd.clone())
511 } else {
512 dest_dir.clone()
513 };
514
515 let mut namespaces = HashMap::default();
516 namespaces.insert("ltx".to_string(), LTX_NSURI.to_string());
517 let mut namespace_uris = HashMap::default();
518 namespace_uris.insert(LTX_NSURI.to_string(), "ltx".to_string());
519
520 PostDocument {
521 document: doc,
522 destination: options.destination,
523 destination_directory: dest_dir,
524 site_directory: site_dir,
525 source: options.source,
526 source_directory: options.source_directory,
527 searchpaths: options.searchpaths.unwrap_or_default(),
528 namespaces,
529 namespace_uris,
530 idcache: HashMap::default(),
531 idcache_reusable: HashMap::default(),
532 idcache_reserve: HashMap::default(),
533 idcache_clashes: HashMap::default(),
534 processing_instructions: Vec::new(),
535 parent_document: None,
536 split_from_id: None,
537 validate: options.validate,
538 cache: HashMap::default(),
539 nocache: options.nocache,
540 pending_xmath_unlinks: Vec::new(),
541 nav_memo: None,
542 }
543 }
544
545 fn set_document_internal(&mut self) {
547 let mut ids: Vec<(String, Node)> = Vec::new();
554 let mut pis: Vec<String> = Vec::new();
555 scan_ids_and_pis(&self.document.as_node(), &mut ids, &mut pis);
556 for (id, node) in ids {
557 self.idcache.insert(id, node);
558 }
559 self.processing_instructions = pis;
560
561 if let Some(root) = self.document.get_root_element() {
563 let ns_decls = root.get_namespace_declarations();
564 for ns in ns_decls {
565 let prefix = ns.get_prefix();
566 if !prefix.is_empty() {
567 let href = ns.get_href();
568 self
569 .namespaces
570 .entry(prefix.clone())
571 .or_insert_with(|| href.clone());
572 self.namespace_uris.entry(href).or_insert(prefix);
573 }
574 }
575 }
576
577 let sp_re = Regex::new(r#"^\s*searchpaths\s*=\s*[\"'](.*?)[\"']\s*$"#).unwrap();
579 let mut paths = self.searchpaths.clone();
580 for pi_text in &self.processing_instructions {
581 if let Some(cap) = sp_re.captures(pi_text) {
582 for p in cap[1].split(',') {
583 paths.push(p.trim().to_string());
584 }
585 }
586 }
587 paths.push(".".to_string());
588 self.searchpaths = paths;
589 }
590
591 pub fn new_from_file(path: &str, options: PostDocumentOptions) -> Result<Self, String> {
607 let parser = XmlParser::default();
608 let doc = parser
609 .parse_file_with_options(path, huge_parse_options())
610 .map_err(|e| format!("Failed to parse '{}': {}", path, e))?;
611 let mut opts = options;
612 if opts.source.is_none() {
613 opts.source = Some(path.to_string());
614 }
615 if opts.source_directory.is_none() {
616 if let Some(parent) = Path::new(path).parent() {
617 opts.source_directory = Some(parent.to_string_lossy().to_string());
618 }
619 }
620 Ok(Self::new(doc, opts))
621 }
622
623 pub fn new_from_string(xml: &str, options: PostDocumentOptions) -> Result<Self, String> {
627 let parser = XmlParser::default();
628 let doc = parser
629 .parse_string_with_options(xml, huge_parse_options())
630 .map_err(|e| format!("Failed to parse XML string: {}", e))?;
631 let mut opts = options;
632 if opts.source_directory.is_none() {
633 opts.source_directory = Some(".".to_string());
634 }
635 Ok(Self::new(doc, opts))
636 }
637
638 pub fn new_document(&self, root: Node, destination: &str) -> Self {
644 use libxml::tree::Document as XmlDocument;
645 let new_xml_doc: XmlDocument = XmlDocument::dup_node_into_new_doc(&root)
673 .expect("dup_node_into_new_doc returned NULL while creating split sub-document");
674 let _ = root;
675
676 let opts = PostDocumentOptions {
677 destination: Some(destination.to_string()),
678 site_directory: self.site_directory.clone(),
688 source: self.source.clone(),
689 source_directory: self.source_directory.clone(),
690 searchpaths: Some(self.searchpaths.clone()),
691 ..PostDocumentOptions::default()
692 };
693 let mut subdoc = Self::new_internal(new_xml_doc, opts);
694
695 subdoc.namespaces = self.namespaces.clone();
697 subdoc.namespace_uris = self.namespace_uris.clone();
698
699 for node in subdoc.findnodes("//*[@xml:id]") {
701 if let Some(id) = get_xml_id(&node) {
702 subdoc.idcache.insert(id, node);
703 }
704 }
705
706 if let Some(ref root_el) = self.get_document_element() {
708 if let Some(root_id) = get_xml_id(root_el) {
709 subdoc.split_from_id = Some(root_id);
710 }
711 }
712
713 for mut pi in self.findnodes("//processing-instruction('latexml')") {
718 if let Ok(mut pi_clone) = subdoc.document.import_node(&mut pi) {
719 if let Some(mut doc_node) = subdoc.document.get_root_element() {
720 doc_node.add_prev_sibling(&mut pi_clone).ok();
721 }
722 }
723 }
724
725 let resources: Vec<NodeData> = self
730 .findnodes("//ltx:resource")
731 .iter()
732 .map(|r| NodeData::XmlNode(r.clone()))
733 .collect();
734 if !resources.is_empty() {
735 if let Some(mut doc_root) = subdoc.get_document_element() {
736 subdoc.add_nodes(&mut doc_root, &resources);
737 }
738 }
739
740 if let Some(sub_root) = subdoc.get_document_element() {
747 if subdoc.findnodes_at("ltx:date", Some(&sub_root)).is_empty() {
748 if let Some(parent_root) = self.get_document_element() {
749 let dates: Vec<NodeData> = self
750 .findnodes_at("ltx:date", Some(&parent_root))
751 .iter()
752 .map(|d| NodeData::XmlNode(d.clone()))
753 .collect();
754 if !dates.is_empty() {
755 let mut sub_root_mut = sub_root;
756 subdoc.add_nodes(&mut sub_root_mut, &dates);
757 }
758 }
759 }
760 }
761
762 if let Some(parent_root) = self.get_document_element() {
764 if let Some(pclass) = parent_root.get_attribute("class") {
765 if let Some(mut doc_root) = subdoc.get_document_element() {
766 let existing = doc_root.get_attribute("class").unwrap_or_default();
767 if existing.is_empty() {
768 doc_root.set_attribute("class", &pclass).ok();
769 } else {
770 doc_root
771 .set_attribute("class", &format!("{} {}", existing, pclass))
772 .ok();
773 }
774 }
775 }
776 }
777
778 subdoc
779 }
780
781 pub fn get_document(&self) -> &Document { &self.document }
786
787 pub fn get_document_mut(&mut self) -> &mut Document { &mut self.document }
789
790 pub fn get_document_element(&self) -> Option<Node> { self.document.get_root_element() }
792
793 pub fn get_source(&self) -> Option<&str> { self.source.as_deref() }
795
796 pub fn get_source_directory(&self) -> &str { self.source_directory.as_deref().unwrap_or(".") }
798
799 pub fn get_search_paths(&self) -> &[String] { &self.searchpaths }
801
802 pub fn get_destination(&self) -> Option<&str> { self.destination.as_deref() }
804
805 pub fn get_destination_directory(&self) -> Option<&str> { self.destination_directory.as_deref() }
807
808 pub fn get_site_directory(&self) -> Option<&str> { self.site_directory.as_deref() }
810
811 pub fn site_relative_destination(&self) -> Option<String> {
815 if let (Some(dest), Some(site)) = (&self.destination, &self.site_directory) {
816 Some(pathdiff(dest, site))
817 } else {
818 self.destination.clone()
819 }
820 }
821
822 pub fn site_relative_pathname(&self, pathname: &str) -> Option<String> {
824 self
825 .site_directory
826 .as_ref()
827 .map(|site| pathdiff(pathname, site))
828 }
829
830 pub fn get_destination_extension(&self) -> Option<String> {
832 self.destination.as_ref().and_then(|d| {
833 Path::new(d)
834 .extension()
835 .map(|e| e.to_string_lossy().to_string())
836 })
837 }
838
839 pub fn to_xml_string(&self) -> String { self.document.to_string() }
841
842 pub fn node_to_string(&self, node: &Node) -> String { self.document.node_to_string(node) }
846
847 pub fn processing_instructions(&self) -> &[String] { &self.processing_instructions }
852
853 pub fn stringify(&self) -> String {
854 format!(
855 "Post::Document[{}]",
856 self
857 .site_relative_destination()
858 .unwrap_or_else(|| "?".to_string())
859 )
860 }
861
862 pub fn findnodes(&self, xpath: &str) -> Vec<Node> { self.findnodes_at(xpath, None) }
869
870 pub fn findnodes_at(&self, xpath: &str, context_node: Option<&Node>) -> Vec<Node> {
872 let ctx = match context_node {
886 Some(node) => XPathContext::from_node(node),
887 None => XPathContext::new(&self.document),
888 };
889 let ctx = match ctx {
890 Ok(c) => c,
891 Err(_) => return vec![],
892 };
893
894 for (prefix, uri) in &self.namespaces {
896 let _ = ctx.register_namespace(prefix, uri);
897 }
898
899 if context_node.is_none()
911 && let Some(arms) = parse_walk_union(xpath)
912 && let Some(root) = self.document.get_root_element()
913 {
914 let mut out = Vec::new();
915 collect_walk_matches(&root, &arms, &mut out);
916 return out;
917 }
918
919 let result = if let Some(node) = context_node {
920 ctx.node_evaluate_checked(xpath, node)
921 } else {
922 match self.document.get_root_element() {
937 Some(root) => ctx.node_evaluate_checked(xpath, &root),
938 None => ctx.evaluate_checked(xpath),
939 }
940 };
941
942 match result {
943 Ok(obj) => obj.get_nodes_as_vec(),
944 Err(e) => {
945 Error!(
951 "post",
952 "xpath",
953 "XPath evaluation failed for `{}`: {} — results are INCOMPLETE for this pass",
954 xpath,
955 e
956 );
957 vec![]
958 },
959 }
960 }
961
962 pub fn find_split_pages(&self, union_xpath: &str) -> Vec<Node> {
976 let arms = match parse_split_union(union_xpath) {
977 Some(a) => a,
978 None => return self.findnodes(union_xpath),
979 };
980 let mut pages = Vec::new();
981 collect_split_pages(&self.document.as_node(), &arms, &mut pages);
982 pages
983 }
984
985 pub fn findnode(&self, xpath: &str) -> Option<Node> { self.findnodes(xpath).into_iter().next() }
987
988 pub fn findnode_at(&self, xpath: &str, context_node: &Node) -> Option<Node> {
990 self
991 .findnodes_at(xpath, Some(context_node))
992 .into_iter()
993 .next()
994 }
995
996 pub fn findvalue(&self, xpath: &str) -> Option<String> {
998 let ctx = XPathContext::new(&self.document).ok()?;
999 for (prefix, uri) in &self.namespaces {
1000 let _ = ctx.register_namespace(prefix, uri);
1001 }
1002 ctx.evaluate(xpath).ok().map(|obj| obj.to_string())
1003 }
1004
1005 pub fn findnodes_foreign(xpath: &str, node: &Node) -> Vec<Node> {
1008 let mut current = node.clone();
1010 while let Some(parent) = current.get_parent() {
1011 current = parent;
1012 }
1013 if let Some(doc) = current.get_parent() {
1016 let _ = doc; }
1019 #[allow(unused_imports)]
1022 use libxml::xpath::Context as XPathContext;
1023 Self::findnodes_by_traversal(xpath, node)
1029 }
1030
1031 fn findnodes_by_traversal(xpath: &str, parent: &Node) -> Vec<Node> {
1035 let xpath = xpath.trim_start_matches('!').trim();
1036 let mut results = Vec::new();
1037
1038 let parts: Vec<&str> = split_steps(xpath);
1042 if parts.is_empty() {
1043 return results;
1044 }
1045
1046 fn split_steps(xpath: &str) -> Vec<&str> {
1048 let mut steps = Vec::new();
1049 let mut depth = 0i32;
1050 let mut start = 0;
1051 for (i, b) in xpath.bytes().enumerate() {
1052 match b {
1053 b'[' => depth += 1,
1054 b']' => depth -= 1,
1055 b'/' if depth == 0 => {
1056 steps.push(&xpath[start..i]);
1057 start = i + 1;
1058 },
1059 _ => {},
1060 }
1061 }
1062 steps.push(&xpath[start..]);
1063 steps
1064 }
1065
1066 fn extract_predicates(s: &str) -> Vec<&str> {
1070 let mut preds = Vec::new();
1071 let mut depth = 0i32;
1072 let mut start = 0;
1073 for (i, b) in s.bytes().enumerate() {
1074 match b {
1075 b'[' => {
1076 if depth == 0 {
1077 start = i + 1;
1078 }
1079 depth += 1;
1080 },
1081 b']' => {
1082 depth -= 1;
1083 if depth == 0 {
1084 preds.push(&s[start..i]);
1085 }
1086 },
1087 _ => {},
1088 }
1089 }
1090 preds
1091 }
1092
1093 fn match_element(node: &Node, pattern: &str) -> bool {
1094 let pattern = pattern.trim().trim_start_matches("ltx:");
1095 let bracket_pos = match pattern.find('[') {
1096 None => return node.get_name() == pattern,
1097 Some(p) => p,
1098 };
1099 let elem_name = &pattern[..bracket_pos];
1100 if node.get_name() != elem_name {
1101 return false;
1102 }
1103 for pred in extract_predicates(&pattern[bracket_pos..]) {
1108 let pred = pred.trim();
1109 if pred.contains('(') {
1110 continue;
1111 }
1112 if let Some(attr) = pred.strip_prefix('@') {
1113 if let Some(eq) = attr.find('=') {
1114 let name = attr[..eq].trim();
1115 let val = attr[eq + 1..].trim().trim_matches('\'').trim_matches('"');
1116 if node.get_attribute(name).as_deref() != Some(val) {
1117 return false;
1118 }
1119 } else if node.get_attribute(attr.trim()).is_none() {
1120 return false;
1121 }
1122 }
1123 }
1124 true
1125 }
1126
1127 fn collect_matching(node: &Node, parts: &[&str], results: &mut Vec<Node>) {
1128 if parts.is_empty() {
1129 return;
1130 }
1131 let pattern = parts[0];
1132 let alternatives: Vec<&str> = pattern.split('|').map(|s| s.trim()).collect();
1134 let mut child = node.get_first_child();
1135 while let Some(c) = child {
1136 for alt in &alternatives {
1137 if match_element(&c, alt) {
1138 if parts.len() == 1 {
1139 results.push(c.clone());
1140 } else {
1141 collect_matching(&c, &parts[1..], results);
1142 }
1143 }
1144 }
1145 child = c.get_next_sibling();
1146 }
1147 }
1148
1149 collect_matching(parent, &parts, &mut results);
1150 results
1151 }
1152
1153 pub fn add_namespace(&mut self, prefix: &str, nsuri: &str) {
1160 let dominated = self
1161 .namespaces
1162 .get(prefix)
1163 .map(|u| u == nsuri)
1164 .unwrap_or(false);
1165 if !dominated {
1166 self
1167 .namespaces
1168 .insert(prefix.to_string(), nsuri.to_string());
1169 self
1170 .namespace_uris
1171 .insert(nsuri.to_string(), prefix.to_string());
1172 if let Some(mut root) = self.document.get_root_element() {
1176 let _ = Namespace::new(prefix, nsuri, &mut root);
1177 }
1178 }
1179 }
1180
1181 pub fn get_qname(&self, node: &Node) -> Option<String> {
1185 if node.get_type() != Some(NodeType::ElementNode) {
1186 return None;
1187 }
1188 let localname = node.get_name();
1189 if let Some(ns) = node.get_namespace() {
1190 let nsuri = ns.get_href();
1191 if let Some(prefix) = self.namespace_uris.get(&nsuri) {
1192 Some(format!("{}:{}", prefix, localname))
1193 } else {
1194 let n = self
1196 .namespaces
1197 .keys()
1198 .filter(|k| k.starts_with("_ns"))
1199 .count()
1200 + 1;
1201 Some(format!("_ns{}:{}", n, localname))
1202 }
1203 } else {
1204 Some(localname)
1205 }
1206 }
1207
1208 pub fn qname_prefix(&self, node: &Node) -> Option<String> {
1214 if node.get_type() != Some(NodeType::ElementNode) {
1215 return None;
1216 }
1217 node.get_namespace().and_then(|ns| {
1218 let nsuri = ns.get_href();
1219 self.namespace_uris.get(&nsuri).cloned()
1220 })
1221 }
1222
1223 pub fn is_qname(&self, node: &Node, expected: &str) -> bool {
1230 if node.get_type() != Some(NodeType::ElementNode) {
1231 return false;
1232 }
1233 let (expected_prefix, expected_local) = match expected.split_once(':') {
1234 Some((p, l)) => (Some(p), l),
1235 None => (None, expected),
1236 };
1237 let localname = node.get_name();
1238 if localname != expected_local {
1239 return false;
1240 }
1241 match (node.get_namespace(), expected_prefix) {
1242 (Some(ns), Some(ep)) => {
1243 let nsuri = ns.get_href();
1244 self
1245 .namespace_uris
1246 .get(&nsuri)
1247 .map(|p| p == ep)
1248 .unwrap_or(false)
1249 },
1250 (None, None) => true,
1251 _ => false,
1252 }
1253 }
1254
1255 pub fn record_id(&mut self, id: &str, node: Node) {
1262 self.idcache.insert(id.to_string(), node);
1263 self.idcache_reserve.remove(id);
1264 self.idcache_reusable.remove(id);
1265 }
1266
1267 pub fn find_node_by_id(&self, id: &str) -> Option<&Node> { self.idcache.get(id) }
1271
1272 pub fn idcache_len(&self) -> usize { self.idcache.len() }
1275
1276 pub fn idcache_iter(&self) -> impl Iterator<Item = (&String, &Node)> { self.idcache.iter() }
1280
1281 pub fn uniquify_id(&mut self, baseid: &str, suffix: Option<&str>) -> String {
1288 let apply_suffix = |id: &str, sfx: Option<&str>| -> String {
1289 if let Some(s) = sfx {
1290 format!("{}{}", id, s)
1291 } else {
1292 id.to_string()
1293 }
1294 };
1295
1296 let mut id = apply_suffix(baseid, suffix);
1297 let cachekey = id.clone();
1298
1299 while (self.idcache.contains_key(&id) || self.idcache_reserve.contains_key(&id))
1300 && !self.idcache_reusable.contains_key(&id)
1301 {
1302 let clash_count = self.idcache_clashes.entry(cachekey.clone()).or_insert(0);
1303 *clash_count += 1;
1304 id = apply_suffix(&format!("{}{}", baseid, radix_alpha(*clash_count)), suffix);
1305 }
1306
1307 self.idcache_reusable.remove(&id);
1308 self.idcache_reserve.insert(id.clone(), true);
1309 id
1310 }
1311
1312 pub fn generate_node_id(
1318 &mut self,
1319 node: &mut Node,
1320 prefix: &str,
1321 reusable: bool,
1322 ) -> Option<String> {
1323 if let Some(id) = get_xml_id(node) {
1324 return Some(id);
1325 }
1326
1327 let mut parent_node = node.get_parent();
1331 let mut pid = String::new();
1332 while let Some(ref p) = parent_node {
1333 if let Some(id) = get_xml_id(p) {
1334 pid = id;
1335 break;
1336 }
1337 parent_node = p.get_parent();
1338 }
1339
1340 if !pid.is_empty() {
1341 pid.push('.');
1342 }
1343
1344 let mut n = 1u32;
1346 let id = loop {
1347 let candidate = format!("{}{}{}", pid, prefix, n);
1348 if !self.idcache.contains_key(&candidate) && !self.idcache_reserve.contains_key(&candidate) {
1349 break candidate;
1350 }
1351 n += 1;
1352 };
1353
1354 node.set_attribute("xml:id", &id).ok();
1355 let node_copy = node.clone();
1356 self.idcache.insert(id.clone(), node_copy);
1357 if reusable {
1358 self.idcache_reusable.insert(id.clone(), true);
1359 }
1360
1361 if let Some(ref p) = parent_node {
1363 if p.get_attribute("fragid").is_some() {
1364 let new_fragid = format!("{}.{}{}", p.get_attribute("fragid").unwrap(), prefix, n);
1365 node.set_attribute("fragid", &new_fragid).ok();
1366 }
1367 }
1368
1369 Some(id)
1370 }
1371
1372 pub fn add_nodes(&mut self, parent: &mut Node, data: &[NodeData]) {
1379 for child in data {
1380 match child {
1381 NodeData::Text(text) => {
1382 parent.append_text(text).ok();
1383 },
1384 NodeData::Element { tag, attributes, children } => {
1385 debug_assert!(
1394 !((tag == "m:mi" || tag == "mi") && children.is_empty()),
1395 "Empty <mi></mi> detected at materialization — use <mrow></mrow> \
1396 scaffolding instead; see task #264 in docs/SYNC_STATUS.md"
1397 );
1398 if tag == "_Fragment_" {
1399 self.add_nodes(parent, children);
1400 } else if let Some((prefix, localname)) = tag.split_once(':') {
1401 let nsuri = self.namespaces.get(prefix).cloned();
1402 if nsuri.is_none() {
1403 Warn!("malformed", "namespace", "No namespace on '{}'", tag);
1404 }
1405 let ns = nsuri.and_then(|uri| {
1409 parent
1411 .get_namespace_declarations()
1412 .into_iter()
1413 .find(|ns| ns.get_prefix().is_empty() && ns.get_href() == uri)
1414 .or_else(|| {
1415 parent
1416 .get_namespaces(&self.document)
1417 .into_iter()
1418 .find(|ns| ns.get_prefix().is_empty() && ns.get_href() == uri)
1419 })
1420 .or_else(|| {
1422 parent
1423 .get_namespace_declarations()
1424 .into_iter()
1425 .find(|ns| ns.get_prefix() == prefix)
1426 })
1427 .or_else(|| {
1428 parent
1429 .get_namespaces(&self.document)
1430 .into_iter()
1431 .find(|ns| ns.get_prefix() == prefix)
1432 })
1433 .or_else(|| {
1434 Namespace::new(prefix, &uri, parent).ok()
1436 })
1437 });
1438 if let Ok(mut new_node) = parent.new_child(ns, localname) {
1439 if let Some(attrs) = attributes {
1441 let mut sorted_keys: Vec<_> = attrs.keys().collect();
1442 sorted_keys.sort();
1443 for key in sorted_keys {
1444 let value = &attrs[key];
1445 if key.starts_with('_') {
1446 continue;
1447 }
1448 if key == "xml:id" {
1449 let id = if self.idcache.contains_key(value.as_str()) {
1450 self.uniquify_id(value, None)
1451 } else {
1452 value.clone()
1453 };
1454 self.record_id(&id, new_node.clone());
1455 new_node.set_attribute("xml:id", &id).ok();
1456 } else {
1457 new_node.set_attribute(key, value).ok();
1458 }
1459 }
1460 }
1461 self.add_nodes(&mut new_node, children);
1462 }
1463 } else {
1464 Warn!(
1465 "malformed",
1466 "namespace",
1467 "Tag '{}' has no namespace prefix",
1468 tag
1469 );
1470 }
1471 },
1472 NodeData::XmlNode(source_node) => {
1473 self.append_clone(parent, source_node);
1474 },
1475 }
1476 }
1477 }
1478
1479 fn append_clone(&mut self, parent: &mut Node, source: &Node) {
1493 let mut idmap: HashMap<String, String> = HashMap::default();
1494 let Some(root) = self.clone_subtree(parent, source, &mut idmap) else {
1495 return;
1496 };
1497 if !idmap.is_empty() {
1498 for mut n in self.findnodes_at("descendant-or-self::*[@idref]", Some(&root)) {
1499 if let Some(idref) = n.get_attribute("idref") {
1500 if let Some(newid) = idmap.get(&idref) {
1501 n.set_attribute("idref", newid).ok();
1502 }
1503 }
1504 }
1505 }
1506 for mut n in self.findnodes_at("descendant-or-self::*[@labels]", Some(&root)) {
1507 let _ = n.remove_attribute("labels");
1508 }
1509 }
1510
1511 fn clone_subtree(
1518 &mut self,
1519 parent: &mut Node,
1520 source: &Node,
1521 idmap: &mut HashMap<String, String>,
1522 ) -> Option<Node> {
1523 match source.get_type() {
1524 Some(NodeType::ElementNode) => {
1525 let localname = source.get_name();
1526 let ns = source.get_namespace().and_then(|src_ns| {
1535 let uri = src_ns.get_href();
1536 let prefix = src_ns.get_prefix();
1537 parent
1538 .get_namespace_declarations()
1539 .into_iter()
1540 .find(|n| n.get_prefix().is_empty() && n.get_href() == uri)
1541 .or_else(|| {
1542 parent
1543 .get_namespaces(&self.document)
1544 .into_iter()
1545 .find(|n| n.get_prefix().is_empty() && n.get_href() == uri)
1546 })
1547 .or_else(|| {
1548 parent
1549 .get_namespace_declarations()
1550 .into_iter()
1551 .find(|n| n.get_prefix() == prefix)
1552 })
1553 .or_else(|| {
1554 parent
1555 .get_namespaces(&self.document)
1556 .into_iter()
1557 .find(|n| n.get_prefix() == prefix)
1558 })
1559 .or_else(|| Namespace::new(&prefix, &uri, parent).ok())
1560 });
1561 let mut new_node = parent.new_child(ns, &localname).ok()?;
1562
1563 let src_xmlid = get_xml_id(source);
1567 let src_fragid = source.get_attribute("fragid");
1568 let new_xmlid = src_xmlid.as_ref().map(|id| {
1569 let newid = if self.idcache.contains_key(id.as_str()) {
1570 self.uniquify_id(id, None)
1571 } else {
1572 id.clone()
1573 };
1574 idmap.insert(id.clone(), newid.clone());
1575 self.record_id(&newid, new_node.clone());
1576 newid
1577 });
1578 let new_fragid = match (&src_xmlid, &new_xmlid, &src_fragid) {
1580 (Some(id), Some(newid), Some(fragid)) => Some(remap_fragid(id, newid, fragid)),
1581 _ => src_fragid.clone(),
1582 };
1583
1584 for (key, value) in &source.get_properties() {
1587 if key.starts_with('_') || key == "fragid" {
1588 continue;
1589 }
1590 let is_xmlid = key == "xml:id" || (key == "id" && src_xmlid.as_deref() == Some(value));
1591 if is_xmlid {
1592 continue;
1593 }
1594 new_node.set_attribute(key, value).ok();
1595 }
1596 if let Some(newid) = &new_xmlid {
1597 new_node.set_attribute("xml:id", newid).ok();
1598 }
1599 if let Some(fragid) = &new_fragid {
1600 new_node.set_attribute("fragid", fragid).ok();
1601 }
1602
1603 let mut child = source.get_first_child();
1605 while let Some(c) = child {
1606 self.clone_subtree(&mut new_node, &c, idmap);
1607 child = c.get_next_sibling();
1608 }
1609 Some(new_node)
1610 },
1611 Some(NodeType::TextNode) => {
1612 parent.append_text(&source.get_content()).ok();
1613 None
1614 },
1615 Some(NodeType::DocumentFragNode) => {
1616 let mut child = source.get_first_child();
1617 while let Some(c) = child {
1618 self.clone_subtree(parent, &c, idmap);
1619 child = c.get_next_sibling();
1620 }
1621 None
1622 },
1623 _ => None,
1624 }
1625 }
1626
1627 pub fn remove_nodes(&mut self, nodes: &[Node]) {
1631 fn collect_ids_of_subtree(node: &Node, out: &mut Vec<String>) {
1632 if node.get_type() != Some(NodeType::ElementNode) {
1633 return;
1634 }
1635 if let Some(id) = get_xml_id(node) {
1636 out.push(id);
1637 }
1638 let mut child = node.get_first_child();
1639 while let Some(c) = child {
1640 collect_ids_of_subtree(&c, out);
1641 child = c.get_next_sibling();
1642 }
1643 }
1644
1645 for node in nodes {
1646 if node.get_type() == Some(NodeType::ElementNode) {
1647 let mut ids = Vec::new();
1649 collect_ids_of_subtree(node, &mut ids);
1650 for id in ids {
1651 self.idcache.remove(&id);
1652 }
1653 }
1654 let mut n = node.clone();
1655 n.unlink_node();
1656 }
1657 }
1658
1659 pub fn preremove_nodes(&mut self, nodes: &[Node]) {
1663 for node in nodes {
1664 if node.get_type() == Some(NodeType::ElementNode) {
1665 for idd in self.findnodes_at("descendant-or-self::*[@xml:id]", Some(node)) {
1666 if let Some(id) = get_xml_id(&idd) {
1669 self.idcache_reusable.insert(id, true);
1670 }
1671 }
1672 }
1673 }
1674 }
1675
1676 pub fn defer_xmath_unlink(&mut self, node: Node) { self.pending_xmath_unlinks.push(node); }
1689
1690 pub fn drain_pending_xmath_unlinks(&mut self) {
1699 let pending = std::mem::take(&mut self.pending_xmath_unlinks);
1700 for node in pending {
1701 node.free_subtree();
1709 }
1710 }
1711
1712 pub fn remove_blank_nodes(&self, node: &Node) -> u32 {
1716 let mut count = 0;
1717 if let Some(child) = node.get_first_child() {
1718 let mut current = Some(child);
1719 while let Some(ref mut c) = current {
1720 let next = c.get_next_sibling();
1721 if c.get_type() == Some(NodeType::TextNode) {
1722 let text = c.get_content();
1723 if text.trim().is_empty() {
1724 c.unlink_node();
1725 count += 1;
1726 }
1727 }
1728 current = next;
1729 }
1730 }
1731 count
1732 }
1733
1734 pub fn replace_node(&mut self, old_node: &Node, replacements: &[NodeData]) {
1738 if let Some(mut parent) = old_node.get_parent() {
1739 let mut save = Vec::new();
1741 while let Some(mut last) = parent.get_last_child() {
1742 if last == *old_node {
1743 break;
1744 }
1745 last.unlink_node();
1746 save.insert(0, last);
1747 }
1748
1749 self.remove_nodes(&[old_node.clone()]);
1751
1752 self.add_nodes(&mut parent, replacements);
1754
1755 for mut s in save {
1757 parent.add_child(&mut s).ok();
1758 }
1759 }
1760 }
1761
1762 pub fn prepend_nodes(&mut self, parent: &mut Node, nodes: &[NodeData]) {
1766 let mut save = Vec::new();
1768 while let Some(mut last) = parent.get_last_child() {
1769 last.unlink_node();
1770 save.insert(0, last);
1771 }
1772
1773 self.add_nodes(parent, nodes);
1775
1776 for mut s in save {
1778 parent.add_child(&mut s).ok();
1779 }
1780 }
1781
1782 pub fn add_ss_values(node: &mut Node, key: &str, values: &str) {
1794 if values.is_empty() {
1795 return;
1796 }
1797 let new_values: Vec<&str> = values.split_whitespace().collect();
1798 if let Some(old_values_str) = node.get_attribute(key) {
1799 let mut all: Vec<String> = old_values_str
1800 .split_whitespace()
1801 .map(String::from)
1802 .collect();
1803 for v in &new_values {
1804 if !all.iter().any(|o| o == v) {
1805 all.push(v.to_string());
1806 }
1807 }
1808 all.sort();
1809 node.set_attribute(key, &all.join(" ")).ok();
1810 } else {
1811 let mut sorted: Vec<&str> = new_values;
1812 sorted.sort_unstable();
1813 node.set_attribute(key, &sorted.join(" ")).ok();
1814 }
1815 }
1816
1817 pub fn add_class(node: &mut Node, class: &str) { Self::add_ss_values(node, "class", class); }
1821
1822 pub fn mark_xm_node_visibility(&self) {
1829 for mut math_child in self.findnodes("//ltx:XMath/*") {
1830 self.mark_xm_node_visibility_aux(&mut math_child, true, true);
1831 }
1832 }
1833
1834 fn mark_xm_node_visibility_aux(&self, node: &mut Node, cvis: bool, pvis: bool) {
1835 let qname = match self.get_qname(node) {
1836 Some(q) => q,
1837 None => return,
1838 };
1839
1840 let has_cvis = node.get_attribute("_cvis").is_some();
1841 let has_pvis = node.get_attribute("_pvis").is_some();
1842 if (!cvis || has_cvis) && (!pvis || has_pvis) {
1843 return;
1844 }
1845
1846 if cvis {
1847 node.set_attribute("_cvis", "1").ok();
1848 }
1849 if pvis {
1850 node.set_attribute("_pvis", "1").ok();
1851 }
1852
1853 if qname == "ltx:XMDual" {
1854 let mut children = element_children(node);
1855 if children.len() >= 2 {
1856 if cvis {
1857 self.mark_xm_node_visibility_aux(&mut children[0], true, false);
1858 }
1859 if pvis {
1860 self.mark_xm_node_visibility_aux(&mut children[1], false, true);
1861 }
1862 }
1863 } else if qname == "ltx:XMRef" {
1864 if let Some(idref) = node.get_attribute("idref") {
1865 if let Some(target) = self.find_node_by_id(&idref) {
1866 let mut target_mut = target.clone();
1867 self.mark_xm_node_visibility_aux(&mut target_mut, cvis, pvis);
1868 } else {
1869 Error!(
1872 "expected",
1873 "id",
1874 "Cannot find a node with xml:id='{}'",
1875 idref
1876 );
1877 }
1878 }
1879 } else {
1880 for mut child in element_children(node) {
1881 self.mark_xm_node_visibility_aux(&mut child, cvis, pvis);
1882 }
1883 }
1884 }
1885
1886 pub fn realize_xm_node_branch(&self, node: &Node, branch: XMBranch) -> Option<Node> {
1895 let mut node = node.clone();
1896 loop {
1897 if self.is_qname(&node, "ltx:XMRef") {
1898 let idref = node.get_attribute("idref")?;
1899 match self.find_node_by_id(&idref) {
1900 Some(target) => node = target.clone(),
1901 None => {
1902 Error!(
1903 "expected",
1904 "id",
1905 "Cannot find a node with xml:id='{}'",
1906 idref
1907 );
1908 return None;
1909 },
1910 }
1911 } else if self.is_qname(&node, "ltx:XMDual") {
1912 let children = element_children(&node);
1916 node = children.get(branch as usize)?.clone();
1917 } else {
1918 return Some(node);
1919 }
1920 }
1921 }
1922
1923 pub fn realize_xm_node(&self, node: &Node) -> Option<Node> {
1929 if self.is_qname(node, "ltx:XMRef") {
1930 let idref = node.get_attribute("idref")?;
1931 let realized = self.find_node_by_id(&idref).cloned();
1932 if realized.is_none() {
1933 Error!(
1936 "expected",
1937 "id",
1938 "Cannot find a node with xml:id='{}'",
1939 idref
1940 );
1941 }
1942 realized
1943 } else {
1944 Some(node.clone())
1945 }
1946 }
1947
1948 pub fn conjoin(conjunction: Conjunction, nodes: Vec<NodeData>) -> Vec<NodeData> {
1955 let n = nodes.len();
1956 if n < 2 {
1957 return nodes;
1958 }
1959
1960 let (comma, and) = match conjunction {
1961 Conjunction::Simple(s) => (s.clone(), s),
1962 Conjunction::Pair(c, a) => (c, a),
1963 };
1964
1965 let mut result = Vec::new();
1966 let mut iter = nodes.into_iter();
1967 result.push(iter.next().unwrap());
1968
1969 let mut remaining: Vec<_> = iter.collect();
1970 while remaining.len() > 1 {
1971 result.push(NodeData::Text(comma.clone()));
1972 result.push(remaining.remove(0));
1973 }
1974 result.push(NodeData::Text(and));
1975 result.push(remaining.remove(0));
1976 result
1977 }
1978
1979 pub fn initial(string: &str, force: bool) -> String {
1983 let decomposed: String = string.nfd().collect();
1984 let trimmed = decomposed.trim_start();
1985 let s = if force {
1986 trimmed.trim_start_matches(|c: char| !c.is_ascii_alphabetic())
1987 } else {
1988 trimmed
1989 };
1990 match s.chars().next() {
1991 Some(c) if c.is_ascii_alphabetic() => c.to_uppercase().to_string(),
1992 _ => "*".to_string(),
1993 }
1994 }
1995
1996 pub fn trim_child_nodes(node: &Node) -> Vec<Node> {
2000 let mut children: Vec<Node> = Vec::new();
2001 if let Some(child) = node.get_first_child() {
2002 let mut current = Some(child);
2003 while let Some(ref c) = current {
2004 children.push(c.clone());
2005 current = c.get_next_sibling();
2006 }
2007 }
2008
2009 if children.is_empty() {
2010 return children;
2011 }
2012
2013 if let Some(first) = children.first_mut() {
2015 if first.get_type() == Some(NodeType::TextNode) {
2016 let text = first.get_content();
2017 let trimmed = text.trim_start();
2018 if trimmed.is_empty() {
2019 children.remove(0);
2020 } else if trimmed != text {
2021 first.set_content(trimmed).ok();
2022 }
2023 }
2024 }
2025
2026 if let Some(last) = children.last_mut() {
2028 if last.get_type() == Some(NodeType::TextNode) {
2029 let text = last.get_content();
2030 let trimmed = text.trim_end();
2031 if trimmed.is_empty() {
2032 children.pop();
2033 } else if trimmed != text {
2034 last.set_content(trimmed).ok();
2035 }
2036 }
2037 }
2038
2039 children
2040 }
2041
2042 pub fn add_navigation(&mut self, relation: &str, id: &str) {
2046 if self.navigation_ref_present(relation, id) {
2052 return;
2053 }
2054
2055 let ref_node = NodeData::Element {
2056 tag: "ltx:ref".to_string(),
2057 attributes: Some(HashMap::from_iter([
2058 ("idref".to_string(), id.to_string()),
2059 ("rel".to_string(), relation.to_string()),
2060 ("show".to_string(), "toctitle".to_string()),
2061 ])),
2062 children: vec![],
2063 };
2064
2065 match self.navigation_element() {
2066 Some(mut nav) => {
2067 self.add_nodes(&mut nav, &[ref_node]);
2068 self.record_navigation_ref(relation, id);
2069 },
2070 _ => {
2071 if let Some(mut root) = self.get_document_element() {
2072 let nav_node = NodeData::Element {
2073 tag: "ltx:navigation".to_string(),
2074 attributes: None,
2075 children: vec![ref_node],
2076 };
2077 self.add_nodes(&mut root, &[nav_node]);
2078 let found = self.findnode("//ltx:navigation");
2081 if let Some(memo) = self.nav_memo.as_mut() {
2082 memo.element = found;
2083 }
2084 self.record_navigation_ref(relation, id);
2085 }
2086 },
2090 }
2091 }
2092
2093 fn navigation_element(&mut self) -> Option<Node> {
2101 if let Some(memo) = self.nav_memo.as_ref()
2102 && let Some(nav) = memo.element.as_ref()
2103 && nav.get_parent().is_some()
2104 {
2105 return Some(nav.clone());
2106 }
2107 let found = self.findnode("//ltx:navigation");
2108 if let Some(memo) = self.nav_memo.as_mut() {
2109 memo.element = found.clone();
2110 }
2111 found
2112 }
2113
2114 fn seed_navigation_memo(&mut self) {
2128 if self.nav_memo.is_some() {
2129 return;
2130 }
2131 let mut refs = FxHashSet::default();
2132 let elements = self.findnodes("//ltx:navigation");
2133 for nav in &elements {
2134 for child in nav.get_child_nodes() {
2135 if child.get_name() != "ref" {
2136 continue;
2137 }
2138 let in_ltx = child
2141 .get_namespace()
2142 .is_some_and(|ns| ns.get_href() == LTX_NSURI);
2143 if !in_ltx {
2144 continue;
2145 }
2146 if let (Some(rel), Some(idref)) = (child.get_attribute("rel"), child.get_attribute("idref"))
2147 {
2148 refs.insert((rel, idref));
2149 }
2150 }
2151 }
2152 self.nav_memo = Some(NavigationMemo {
2153 element: elements.into_iter().next(),
2154 refs,
2155 });
2156 }
2157
2158 fn navigation_ref_present(&mut self, relation: &str, id: &str) -> bool {
2159 self.seed_navigation_memo();
2160 self
2161 .nav_memo
2162 .as_ref()
2163 .is_some_and(|memo| memo.refs.contains(&(relation.to_string(), id.to_string())))
2164 }
2165
2166 fn record_navigation_ref(&mut self, relation: &str, id: &str) {
2168 if let Some(memo) = self.nav_memo.as_mut() {
2169 memo.refs.insert((relation.to_string(), id.to_string()));
2170 }
2171 }
2172
2173 pub fn validate(&self) -> Result<(), String> {
2180 let rng_re = Regex::new(r#"^\s*RelaxNGSchema\s*=\s*[\"'](.*?)[\"']\s*$"#).unwrap();
2181 for pi_text in &self.processing_instructions {
2182 if let Some(cap) = rng_re.captures(pi_text) {
2183 let schema = &cap[1];
2184 Info!(
2185 "validate",
2186 "schema",
2187 "Would validate against RelaxNG schema: {}",
2188 schema
2189 );
2190 return Ok(());
2191 }
2192 }
2193 Warn!(
2198 "missing_file",
2199 "schema",
2200 "No schema found for document validation"
2201 );
2202 Ok(())
2203 }
2204
2205 pub fn idcheck(&self) {
2209 let mut doc_ids: HashMap<String, bool> = HashMap::default();
2210 let mut dups = Vec::new();
2211
2212 for node in self.findnodes("//*[@xml:id]") {
2213 if let Some(id) = get_xml_id(&node) {
2214 if doc_ids.contains_key(&id) {
2215 dups.push(id.clone());
2216 }
2217 doc_ids.insert(id, true);
2218 }
2219 }
2220
2221 let mut missing = Vec::new();
2222 for id in self.idcache.keys() {
2223 if !doc_ids.contains_key(id) {
2224 missing.push(id.clone());
2225 }
2226 }
2227
2228 if !dups.is_empty() {
2229 Warn!(
2230 "malformed",
2231 "id",
2232 "Duplicate IDs for {}: {}",
2233 self.site_relative_destination().unwrap_or_default(),
2234 dups.join(", ")
2235 );
2236 }
2237 if !missing.is_empty() {
2238 Warn!(
2239 "expected",
2240 "id",
2241 "Cached IDs not in document for {}: {}",
2242 self.site_relative_destination().unwrap_or_default(),
2243 missing.join(", ")
2244 );
2245 }
2246 }
2247
2248 pub fn cache_lookup(&self, key: &str) -> Option<String> { self.cache.get(key).cloned() }
2253
2254 pub fn cache_store(&mut self, key: &str, value: &str) {
2256 self.cache.insert(key.to_string(), value.to_string());
2257 }
2258
2259 pub fn cache_remove(&mut self, key: &str) { self.cache.remove(key); }
2261}
2262
2263#[derive(Debug, Default, Clone)]
2268pub struct PostDocumentOptions {
2269 pub destination: Option<String>,
2270 pub destination_directory: Option<String>,
2271 pub site_directory: Option<String>,
2272 pub source: Option<String>,
2273 pub source_directory: Option<String>,
2274 pub searchpaths: Option<Vec<String>>,
2275 pub validate: bool,
2276 pub nocache: bool,
2277}
2278
2279#[derive(Debug, Clone)]
2283pub enum NodeData {
2284 Text(String),
2286 Element {
2288 tag: String,
2289 attributes: Option<HashMap<String, String>>,
2290 children: Vec<NodeData>,
2291 },
2292 XmlNode(Node),
2294}
2295
2296#[derive(Clone, Copy, PartialEq, Eq, Debug)]
2303pub enum XMBranch {
2304 Content = 0,
2306 Presentation = 1,
2308}
2309
2310pub enum Conjunction {
2312 Simple(String),
2314 Pair(String, String),
2316}
2317
2318pub fn element_children(node: &Node) -> Vec<Node> {
2323 let mut result = Vec::new();
2324 if let Some(child) = node.get_first_child() {
2325 let mut current = Some(child);
2326 while let Some(ref c) = current {
2327 if c.get_type() == Some(NodeType::ElementNode) {
2328 result.push(c.clone());
2329 }
2330 current = c.get_next_sibling();
2331 }
2332 }
2333 result
2334}
2335
2336pub fn element_children_iter(node: &Node) -> impl Iterator<Item = Node> + use<> {
2341 let first = node.get_first_child();
2342 std::iter::successors(first, |c| c.get_next_sibling())
2343 .filter(|c| c.get_type() == Some(NodeType::ElementNode))
2344}
2345
2346pub fn escape_xml(s: &str) -> String {
2367 s.replace('&', "&")
2368 .replace('<', "<")
2369 .replace('>', ">")
2370 .replace('"', """)
2371}
2372
2373fn pathdiff(path: &str, base: &str) -> String {
2375 let p = Path::new(path);
2376 let b = Path::new(base);
2377 if let Ok(rel) = p.strip_prefix(b) {
2378 rel.to_string_lossy().to_string()
2379 } else {
2380 path.to_string()
2381 }
2382}
2383
2384#[cfg(test)]
2385mod tests {
2386 use super::*;
2387
2388 fn make_test_doc(xml: &str) -> PostDocument {
2389 PostDocument::new_from_string(xml, PostDocumentOptions::default()).unwrap()
2390 }
2391
2392 #[test]
2403 fn add_navigation_dedupes_including_preexisting_refs() {
2404 let mut doc = make_test_doc(
2405 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2406 <navigation>\
2407 <ref rel='chapter' idref='Ch1' show='toctitle'/>\
2408 <title>ignored — not an ltx:ref</title>\
2409 </navigation>\
2410 </document>",
2411 );
2412
2413 doc.add_navigation("chapter", "Ch1"); doc.add_navigation("section", "S1"); doc.add_navigation("section", "S1"); doc.add_navigation("sidebar", "Ch1"); let refs = doc.findnodes("//ltx:navigation/ltx:ref");
2419 let mut pairs: Vec<(String, String)> = refs
2420 .iter()
2421 .map(|n| {
2422 (
2423 n.get_attribute("rel").unwrap_or_default(),
2424 n.get_attribute("idref").unwrap_or_default(),
2425 )
2426 })
2427 .collect();
2428 pairs.sort();
2429 assert_eq!(pairs, vec![
2430 ("chapter".to_string(), "Ch1".to_string()),
2431 ("section".to_string(), "S1".to_string()),
2432 ("sidebar".to_string(), "Ch1".to_string()),
2433 ]);
2434 assert_eq!(
2435 doc.findnodes("//ltx:navigation").len(),
2436 1,
2437 "the existing navigation element must be reused, not duplicated"
2438 );
2439 }
2440
2441 #[test]
2448 fn add_navigation_sees_refs_under_every_navigation_element() {
2449 let mut doc = make_test_doc(
2450 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2451 <navigation><ref rel='chapter' idref='Ch1'/></navigation>\
2452 <section><navigation><ref rel='section' idref='S9'/></navigation></section>\
2453 </document>",
2454 );
2455
2456 doc.add_navigation("section", "S9"); doc.add_navigation("chapter", "Ch1"); assert_eq!(
2460 doc.findnodes("//ltx:navigation/ltx:ref").len(),
2461 2,
2462 "neither pair may be re-added; `//` spans both navigation elements"
2463 );
2464
2465 doc.add_navigation("appendix", "A1"); let first_nav_refs = doc.findnodes("//ltx:navigation").first().map(|n| {
2467 n.get_child_nodes()
2468 .iter()
2469 .filter(|c| c.get_name() == "ref")
2470 .count()
2471 });
2472 assert_eq!(
2473 first_nav_refs,
2474 Some(2),
2475 "a new ref lands under the FIRST navigation element (Perl's findnode)"
2476 );
2477 }
2478
2479 #[test]
2482 fn add_navigation_ignores_a_foreign_namespace_ref_when_seeding() {
2483 let mut doc = make_test_doc(
2484 "<document xmlns='http://dlmf.nist.gov/LaTeXML' xmlns:other='http://example.org/other'>\
2485 <navigation><other:ref rel='chapter' idref='Ch1'/></navigation>\
2486 </document>",
2487 );
2488
2489 doc.add_navigation("chapter", "Ch1");
2490
2491 assert_eq!(
2492 doc.findnodes("//ltx:navigation/ltx:ref").len(),
2493 1,
2494 "the foreign-namespace ref is not an ltx:ref, so the real one must be added"
2495 );
2496 }
2497
2498 #[test]
2502 fn navigation_memo_agrees_with_the_original_xpath_probe() {
2503 let mut doc = make_test_doc(
2504 "<document xmlns='http://dlmf.nist.gov/LaTeXML' xmlns:other='http://example.org/other'>\
2505 <navigation>\
2506 <ref rel='chapter' idref='Ch1'/>\
2507 <title>t</title>\
2508 <other:ref rel='section' idref='S1'/>\
2509 </navigation>\
2510 </document>",
2511 );
2512
2513 for (rel, id) in [
2514 ("chapter", "Ch1"), ("section", "S1"), ("title", "t"), ("section", "S2"), ] {
2519 let probe = format!("//ltx:navigation/ltx:ref[@rel='{}'][@idref='{}']", rel, id);
2521 let xpath_says_present = doc.findnode(&probe).is_some();
2522 let memo_says_present = doc.navigation_ref_present(rel, id);
2523 assert_eq!(
2524 memo_says_present, xpath_says_present,
2525 "memo and XPath disagree about ({rel}, {id})"
2526 );
2527 }
2528 }
2529
2530 #[test]
2531 fn add_navigation_creates_then_reuses_the_navigation_element() {
2532 let mut doc = make_test_doc("<document xmlns='http://dlmf.nist.gov/LaTeXML'><p/></document>");
2533
2534 doc.add_navigation("section", "S1");
2535 doc.add_navigation("section", "S2");
2536 doc.add_navigation("section", "S1"); assert_eq!(
2539 doc.findnodes("//ltx:navigation").len(),
2540 1,
2541 "exactly one navigation element must be created"
2542 );
2543 assert_eq!(doc.findnodes("//ltx:navigation/ltx:ref").len(), 2);
2544 }
2545
2546 #[test]
2547 fn walk_union_agrees_with_xpath_on_every_supported_shape() {
2548 let doc = make_test_doc(
2549 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2550 <ref href='u1'/>\
2551 <ref href='u2' idref='i1'/>\
2552 <ref labelref='L1'/>\
2553 <graphics/>\
2554 <graphics imagesrc='a.png'/>\
2555 <Math id='m1'><XMath><Math id='inner'/></XMath></Math>\
2556 <index/>\
2557 <index><indexlist/></index>\
2558 <glossary/>\
2559 <p idref='i2'/>\
2560 <XMDual _cvis='1'/>\
2561 <XMDual _pvis='1'/>\
2562 <XMDual _cvis='1' _pvis='1'/>\
2563 <XMDual/>\
2564 </document>",
2565 );
2566 for xpath in [
2567 "//*[@idref]",
2568 "//*[@labelref]",
2569 "//ltx:ref[@href and not(@idref) and not(@labelref)]",
2570 "//ltx:graphics[not(@imagesrc)]",
2571 "//ltx:Math[not(ancestor::ltx:Math)]",
2572 "//ltx:index[not(ltx:indexlist)] | //ltx:glossary[not(ltx:glossarylist)]",
2573 "//ltx:ref",
2574 "//*[@_cvis or @_pvis]",
2576 "//ltx:ref[@href or @idref]",
2577 ] {
2578 let arms = parse_walk_union(xpath)
2579 .unwrap_or_else(|| panic!("`{xpath}` must be inside the walk grammar"));
2580 let mut walked = Vec::new();
2581 collect_walk_matches(&doc.get_document_element().unwrap(), &arms, &mut walked);
2582 let ctx = libxml::xpath::Context::new(&doc.document).expect("ctx");
2586 ctx.register_namespace("ltx", LTX_NSURI).expect("ns");
2587 let root = doc.get_document_element().unwrap();
2588 let expected: Vec<String> = ctx
2589 .node_evaluate(xpath, &root)
2590 .expect("xpath evaluates on a small doc")
2591 .get_nodes_as_vec()
2592 .iter()
2593 .map(|n| format!("{}#{:?}", n.get_name(), n.get_attribute("id")))
2594 .collect();
2595 let got: Vec<String> = walked
2596 .iter()
2597 .map(|n| format!("{}#{:?}", n.get_name(), n.get_attribute("id")))
2598 .collect();
2599 assert_eq!(got, expected, "walk disagrees with XPath for `{xpath}`");
2600 }
2601 }
2602
2603 #[test]
2606 fn unsupported_shapes_are_not_claimed_by_the_walk() {
2607 for xpath in [
2608 "//ltx:section/ltx:title", "//ltx:ref[position()=1]", "descendant::ltx:navigation", "//svg:svg", "//ltx:Math[not(ancestor::svg:svg)]", "//ltx:ref[(@a or @b) and @c]", ] {
2615 assert!(
2616 parse_walk_union(xpath).is_none(),
2617 "`{xpath}` must fall through to XPath, not be walked"
2618 );
2619 }
2620 }
2621
2622 #[test]
2623 fn test_new_from_string() {
2624 let doc = make_test_doc("<document xmlns='http://dlmf.nist.gov/LaTeXML'/>");
2625 assert!(doc.get_document_element().is_some());
2626 }
2627
2628 #[test]
2629 fn test_findnodes() {
2630 let doc = make_test_doc(
2631 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2632 <section xml:id='s1'/>\
2633 <section xml:id='s2'/>\
2634 </document>",
2635 );
2636 let sections = doc.findnodes("//ltx:section");
2637 assert_eq!(sections.len(), 2);
2638 }
2639
2640 #[test]
2648 fn findnodes_resolves_relative_axes_without_context_node() {
2649 let doc = make_test_doc(
2650 "<?latexml class='book'?>\
2651 <document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2652 <resource src='a.css' type='text/css'/>\
2653 <section xml:id='s1'/>\
2654 </document>",
2655 );
2656 assert_eq!(doc.findnodes("//ltx:resource").len(), 1, "absolute");
2657 assert_eq!(
2658 doc.findnodes("descendant::ltx:resource").len(),
2659 1,
2660 "descendant:: axis must resolve without an explicit context node"
2661 );
2662 assert_eq!(
2663 doc.findnodes(".//ltx:resource").len(),
2664 1,
2665 ".// axis must resolve without an explicit context node"
2666 );
2667 assert_eq!(
2669 doc.findnodes("//processing-instruction('latexml')").len(),
2670 1,
2671 "absolute PI query finds the before-root <?latexml?>"
2672 );
2673 }
2674
2675 const SECTION_UNION: &str = "//ltx:section | \
2678 //ltx:bibliography[preceding-sibling::ltx:section or parent::ltx:part or parent::ltx:chapter] | \
2679 //ltx:appendix[preceding-sibling::ltx:section or parent::ltx:part or parent::ltx:chapter] | \
2680 //ltx:index[preceding-sibling::ltx:section or parent::ltx:part or parent::ltx:chapter] | \
2681 //ltx:part | \
2682 //ltx:bibliography[preceding-sibling::ltx:part] | \
2683 //ltx:appendix[preceding-sibling::ltx:part] | \
2684 //ltx:index[preceding-sibling::ltx:part] | \
2685 //ltx:chapter | \
2686 //ltx:bibliography[preceding-sibling::ltx:chapter or parent::ltx:part] | \
2687 //ltx:appendix[preceding-sibling::ltx:chapter or parent::ltx:part] | \
2688 //ltx:index[preceding-sibling::ltx:chapter or parent::ltx:part]";
2689
2690 fn split_doc() -> PostDocument {
2691 make_test_doc(
2692 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2693 <part xml:id='P1'>\
2694 <chapter xml:id='C1'>\
2695 <section xml:id='S1'/>\
2696 <section xml:id='S2'/>\
2697 <index xml:id='I1'/>\
2698 </chapter>\
2699 <bibliography xml:id='B1'/>\
2700 </part>\
2701 <section xml:id='S3'/>\
2702 <index xml:id='I2'/>\
2703 </document>",
2704 )
2705 }
2706
2707 #[test]
2710 fn test_find_split_pages_matches_xpath() {
2711 let doc = split_doc();
2712 let ids = |nodes: Vec<Node>| -> Vec<String> { nodes.iter().filter_map(get_xml_id).collect() };
2713 let via_walk = ids(doc.find_split_pages(SECTION_UNION));
2714 let via_xpath = ids(doc.findnodes(SECTION_UNION));
2715 assert_eq!(
2716 via_walk,
2717 vec!["P1", "C1", "S1", "S2", "I1", "B1", "S3", "I2"],
2718 "walk selected the wrong pages / order"
2719 );
2720 assert_eq!(via_walk, via_xpath, "walk diverged from XPath union");
2721 }
2722
2723 #[test]
2726 fn test_find_split_pages_fallback() {
2727 let doc = split_doc();
2728 let via = doc.find_split_pages("//ltx:chapter/descendant::ltx:section");
2730 let ids: Vec<String> = via.iter().filter_map(get_xml_id).collect();
2731 assert_eq!(ids, vec!["S1", "S2"]);
2732 }
2733
2734 #[test]
2737 fn test_scan_ids_populates_idcache() {
2738 let doc = split_doc();
2739 for id in ["P1", "C1", "S1", "S2", "I1", "B1", "S3", "I2"] {
2740 assert!(
2741 doc.find_node_by_id(id).is_some(),
2742 "missing id {id} in idcache"
2743 );
2744 }
2745 assert!(doc.find_node_by_id("nope").is_none());
2746 }
2747
2748 #[test]
2751 fn test_scan_collects_doclevel_pi_searchpaths() {
2752 let doc = make_test_doc(
2753 "<?latexml searchpaths=\"alpha,beta\"?>\
2754 <document xmlns='http://dlmf.nist.gov/LaTeXML'><section xml:id='s1'/></document>",
2755 );
2756 let paths = doc.get_search_paths();
2757 assert!(paths.iter().any(|p| p == "alpha"), "searchpaths: {paths:?}");
2758 assert!(paths.iter().any(|p| p == "beta"), "searchpaths: {paths:?}");
2759 }
2760
2761 #[test]
2762 fn test_uniquify_id() {
2763 let doc = make_test_doc(
2764 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2765 <p xml:id='p1'/>\
2766 </document>",
2767 );
2768 let mut doc = doc;
2769 let id1 = doc.uniquify_id("p1", None);
2771 let id2 = doc.uniquify_id("p1", None);
2773 assert_ne!(id1, id2);
2774 assert!(id1.starts_with("p1"));
2776 assert!(id2.starts_with("p1"));
2777 }
2778
2779 #[test]
2780 fn test_initial() {
2781 assert_eq!(PostDocument::initial("Hello", false), "H");
2782 assert_eq!(PostDocument::initial(" world", false), "W");
2783 assert_eq!(PostDocument::initial("123abc", true), "A");
2784 assert_eq!(PostDocument::initial("!@#", false), "*");
2785 assert_eq!(PostDocument::initial("\u{00E9}cole", false), "E"); }
2787
2788 #[test]
2789 fn test_add_class() {
2790 let doc = make_test_doc(
2791 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2792 <p xml:id='p1'/>\
2793 </document>",
2794 );
2795 let mut node = doc.findnode("//ltx:p").unwrap();
2796 PostDocument::add_class(&mut node, "foo bar");
2797 let class = node.get_attribute("class").unwrap();
2798 assert!(class.contains("bar"));
2799 assert!(class.contains("foo"));
2800
2801 PostDocument::add_class(&mut node, "foo");
2803 let class = node.get_attribute("class").unwrap();
2804 let count = class.matches("foo").count();
2805 assert_eq!(count, 1);
2806 }
2807}