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 let mut child = node.get_first_child();
331 while let Some(c) = child {
332 collect_walk_matches(&c, arms, out);
333 child = c.get_next_sibling();
334 }
335}
336
337pub(crate) fn arm_matches(arm: &SplitArm, node: &Node) -> bool {
339 arm.any_of.is_empty() || arm.any_of.iter().any(|c| cond_matches(c, node))
340}
341
342pub(crate) fn collect_split_pages(node: &Node, arms: &[SplitArm], out: &mut Vec<Node>) {
346 let mut child = node.get_first_child();
347 while let Some(c) = child {
348 if c.get_type() == Some(NodeType::ElementNode) {
349 let name = c.get_name();
350 if arms.iter().any(|a| a.element == name)
353 && is_ltx(&c)
354 && arms.iter().any(|a| a.element == name && arm_matches(a, &c))
355 {
356 out.push(c.clone());
357 }
358 collect_split_pages(&c, arms, out);
359 }
360 child = c.get_next_sibling();
361 }
362}
363
364pub struct PostDocument {
369 document: Document,
371 pub destination: Option<String>,
373 pub destination_directory: Option<String>,
375 pub site_directory: Option<String>,
377 pub source: Option<String>,
379 pub source_directory: Option<String>,
381 pub searchpaths: Vec<String>,
383 pub namespaces: HashMap<String, String>,
385 pub namespace_uris: HashMap<String, String>,
387 idcache: HashMap<String, Node>,
389 idcache_reusable: HashMap<String, bool>,
391 idcache_reserve: HashMap<String, bool>,
393 idcache_clashes: HashMap<String, u32>,
395 pub processing_instructions: Vec<String>,
397 pub parent_document: Option<Box<PostDocument>>,
399 pub split_from_id: Option<String>,
401 pub validate: bool,
403 cache: HashMap<String, String>,
405 pub nocache: bool,
407 pending_xmath_unlinks: Vec<Node>,
420 nav_memo: Option<NavigationMemo>,
437}
438
439struct NavigationMemo {
441 element: Option<Node>,
443 refs: FxHashSet<(String, String)>,
445}
446
447impl Drop for PostDocument {
448 fn drop(&mut self) {
473 for (_, node) in std::mem::take(&mut self.idcache) {
474 node.set_linked();
481 }
482 }
483}
484
485impl PostDocument {
486 pub fn new(doc: Document, options: PostDocumentOptions) -> Self {
490 let mut pd = Self::new_internal(doc, options);
495 pd.set_document_internal();
496 pd
497 }
498
499 fn new_internal(doc: Document, options: PostDocumentOptions) -> Self {
500 let mut dest_dir = options.destination_directory.clone();
501 if options.destination.is_some() && dest_dir.is_none() {
502 if let Some(ref dest) = options.destination {
503 if let Some(parent) = Path::new(dest).parent() {
504 let parent_str = parent.to_string_lossy().to_string();
505 if parent_str.is_empty() {
507 dest_dir = Some(".".to_string());
508 } else {
509 dest_dir = Some(parent_str);
510 }
511 }
512 }
513 }
514
515 let site_dir = if let Some(ref sd) = options.site_directory {
516 Some(sd.clone())
517 } else {
518 dest_dir.clone()
519 };
520
521 let mut namespaces = HashMap::default();
522 namespaces.insert("ltx".to_string(), LTX_NSURI.to_string());
523 let mut namespace_uris = HashMap::default();
524 namespace_uris.insert(LTX_NSURI.to_string(), "ltx".to_string());
525
526 PostDocument {
527 document: doc,
528 destination: options.destination,
529 destination_directory: dest_dir,
530 site_directory: site_dir,
531 source: options.source,
532 source_directory: options.source_directory,
533 searchpaths: options.searchpaths.unwrap_or_default(),
534 namespaces,
535 namespace_uris,
536 idcache: HashMap::default(),
537 idcache_reusable: HashMap::default(),
538 idcache_reserve: HashMap::default(),
539 idcache_clashes: HashMap::default(),
540 processing_instructions: Vec::new(),
541 parent_document: None,
542 split_from_id: None,
543 validate: options.validate,
544 cache: HashMap::default(),
545 nocache: options.nocache,
546 pending_xmath_unlinks: Vec::new(),
547 nav_memo: None,
548 }
549 }
550
551 fn set_document_internal(&mut self) {
553 let mut ids: Vec<(String, Node)> = Vec::new();
560 let mut pis: Vec<String> = Vec::new();
561 scan_ids_and_pis(&self.document.as_node(), &mut ids, &mut pis);
562 for (id, node) in ids {
563 self.idcache.insert(id, node);
564 }
565 self.processing_instructions = pis;
566
567 if let Some(root) = self.document.get_root_element() {
569 let ns_decls = root.get_namespace_declarations();
570 for ns in ns_decls {
571 let prefix = ns.get_prefix();
572 if !prefix.is_empty() {
573 let href = ns.get_href();
574 self
575 .namespaces
576 .entry(prefix.clone())
577 .or_insert_with(|| href.clone());
578 self.namespace_uris.entry(href).or_insert(prefix);
579 }
580 }
581 }
582
583 let sp_re = Regex::new(r#"^\s*searchpaths\s*=\s*[\"'](.*?)[\"']\s*$"#).unwrap();
585 let mut paths = self.searchpaths.clone();
586 for pi_text in &self.processing_instructions {
587 if let Some(cap) = sp_re.captures(pi_text) {
588 for p in cap[1].split(',') {
589 paths.push(p.trim().to_string());
590 }
591 }
592 }
593 paths.push(".".to_string());
594 self.searchpaths = paths;
595 }
596
597 pub fn new_from_file(path: &str, options: PostDocumentOptions) -> Result<Self, String> {
613 let parser = XmlParser::default();
614 let doc = parser
615 .parse_file_with_options(path, huge_parse_options())
616 .map_err(|e| format!("Failed to parse '{}': {}", path, e))?;
617 let mut opts = options;
618 if opts.source.is_none() {
619 opts.source = Some(path.to_string());
620 }
621 if opts.source_directory.is_none() {
622 if let Some(parent) = Path::new(path).parent() {
623 opts.source_directory = Some(parent.to_string_lossy().to_string());
624 }
625 }
626 Ok(Self::new(doc, opts))
627 }
628
629 pub fn new_from_string(xml: &str, options: PostDocumentOptions) -> Result<Self, String> {
633 let parser = XmlParser::default();
634 let doc = parser
635 .parse_string_with_options(xml, huge_parse_options())
636 .map_err(|e| format!("Failed to parse XML string: {}", e))?;
637 let mut opts = options;
638 if opts.source_directory.is_none() {
639 opts.source_directory = Some(".".to_string());
640 }
641 Ok(Self::new(doc, opts))
642 }
643
644 pub fn new_document(&self, root: Node, destination: &str) -> Self {
650 use libxml::tree::Document as XmlDocument;
651 let new_xml_doc: XmlDocument = XmlDocument::dup_node_into_new_doc(&root)
679 .expect("dup_node_into_new_doc returned NULL while creating split sub-document");
680 let _ = root;
681
682 let opts = PostDocumentOptions {
683 destination: Some(destination.to_string()),
684 site_directory: self.site_directory.clone(),
694 source: self.source.clone(),
695 source_directory: self.source_directory.clone(),
696 searchpaths: Some(self.searchpaths.clone()),
697 ..PostDocumentOptions::default()
698 };
699 let mut subdoc = Self::new_internal(new_xml_doc, opts);
700
701 subdoc.namespaces = self.namespaces.clone();
703 subdoc.namespace_uris = self.namespace_uris.clone();
704
705 for node in subdoc.findnodes("//*[@xml:id]") {
707 if let Some(id) = get_xml_id(&node) {
708 subdoc.idcache.insert(id, node);
709 }
710 }
711
712 if let Some(ref root_el) = self.get_document_element() {
714 if let Some(root_id) = get_xml_id(root_el) {
715 subdoc.split_from_id = Some(root_id);
716 }
717 }
718
719 for mut pi in self.findnodes("//processing-instruction('latexml')") {
724 if let Ok(mut pi_clone) = subdoc.document.import_node(&mut pi) {
725 if let Some(mut doc_node) = subdoc.document.get_root_element() {
726 doc_node.add_prev_sibling(&mut pi_clone).ok();
727 }
728 }
729 }
730
731 let resources: Vec<NodeData> = self
736 .findnodes("//ltx:resource")
737 .iter()
738 .map(|r| NodeData::XmlNode(r.clone()))
739 .collect();
740 if !resources.is_empty() {
741 if let Some(mut doc_root) = subdoc.get_document_element() {
742 subdoc.add_nodes(&mut doc_root, &resources);
743 }
744 }
745
746 if let Some(sub_root) = subdoc.get_document_element() {
753 if subdoc.findnodes_at("ltx:date", Some(&sub_root)).is_empty() {
754 if let Some(parent_root) = self.get_document_element() {
755 let dates: Vec<NodeData> = self
756 .findnodes_at("ltx:date", Some(&parent_root))
757 .iter()
758 .map(|d| NodeData::XmlNode(d.clone()))
759 .collect();
760 if !dates.is_empty() {
761 let mut sub_root_mut = sub_root;
762 subdoc.add_nodes(&mut sub_root_mut, &dates);
763 }
764 }
765 }
766 }
767
768 if let Some(parent_root) = self.get_document_element() {
770 if let Some(pclass) = parent_root.get_attribute("class") {
771 if let Some(mut doc_root) = subdoc.get_document_element() {
772 let existing = doc_root.get_attribute("class").unwrap_or_default();
773 if existing.is_empty() {
774 doc_root.set_attribute("class", &pclass).ok();
775 } else {
776 doc_root
777 .set_attribute("class", &format!("{} {}", existing, pclass))
778 .ok();
779 }
780 }
781 }
782 }
783
784 subdoc
785 }
786
787 pub fn get_document(&self) -> &Document { &self.document }
792
793 pub fn get_document_mut(&mut self) -> &mut Document { &mut self.document }
795
796 pub fn get_document_element(&self) -> Option<Node> { self.document.get_root_element() }
798
799 pub fn get_source(&self) -> Option<&str> { self.source.as_deref() }
801
802 pub fn get_source_directory(&self) -> &str { self.source_directory.as_deref().unwrap_or(".") }
804
805 pub fn get_search_paths(&self) -> &[String] { &self.searchpaths }
807
808 pub fn get_destination(&self) -> Option<&str> { self.destination.as_deref() }
810
811 pub fn get_destination_directory(&self) -> Option<&str> { self.destination_directory.as_deref() }
813
814 pub fn get_site_directory(&self) -> Option<&str> { self.site_directory.as_deref() }
816
817 pub fn site_relative_destination(&self) -> Option<String> {
821 if let (Some(dest), Some(site)) = (&self.destination, &self.site_directory) {
822 Some(pathdiff(dest, site))
823 } else {
824 self.destination.clone()
825 }
826 }
827
828 pub fn site_relative_pathname(&self, pathname: &str) -> Option<String> {
830 self
831 .site_directory
832 .as_ref()
833 .map(|site| pathdiff(pathname, site))
834 }
835
836 pub fn get_destination_extension(&self) -> Option<String> {
838 self.destination.as_ref().and_then(|d| {
839 Path::new(d)
840 .extension()
841 .map(|e| e.to_string_lossy().to_string())
842 })
843 }
844
845 pub fn to_xml_string(&self) -> String { self.document.to_string() }
847
848 pub fn node_to_string(&self, node: &Node) -> String { self.document.node_to_string(node) }
852
853 pub fn processing_instructions(&self) -> &[String] { &self.processing_instructions }
858
859 pub fn stringify(&self) -> String {
860 format!(
861 "Post::Document[{}]",
862 self
863 .site_relative_destination()
864 .unwrap_or_else(|| "?".to_string())
865 )
866 }
867
868 pub fn findnodes(&self, xpath: &str) -> Vec<Node> { self.findnodes_at(xpath, None) }
875
876 pub fn findnodes_at(&self, xpath: &str, context_node: Option<&Node>) -> Vec<Node> {
878 let ctx = match context_node {
892 Some(node) => XPathContext::from_node(node),
893 None => XPathContext::new(&self.document),
894 };
895 let ctx = match ctx {
896 Ok(c) => c,
897 Err(_) => return vec![],
898 };
899
900 for (prefix, uri) in &self.namespaces {
902 let _ = ctx.register_namespace(prefix, uri);
903 }
904
905 if context_node.is_none()
917 && let Some(arms) = parse_walk_union(xpath)
918 && let Some(root) = self.document.get_root_element()
919 {
920 let mut out = Vec::new();
921 collect_walk_matches(&root, &arms, &mut out);
922 return out;
923 }
924
925 let result = if let Some(node) = context_node {
926 ctx.node_evaluate_checked(xpath, node)
927 } else {
928 match self.document.get_root_element() {
943 Some(root) => ctx.node_evaluate_checked(xpath, &root),
944 None => ctx.evaluate_checked(xpath),
945 }
946 };
947
948 match result {
949 Ok(obj) => obj.get_nodes_as_vec(),
950 Err(e) => {
951 Error!(
957 "post",
958 "xpath",
959 "XPath evaluation failed for `{}`: {} — results are INCOMPLETE for this pass",
960 xpath,
961 e
962 );
963 vec![]
964 },
965 }
966 }
967
968 pub fn find_split_pages(&self, union_xpath: &str) -> Vec<Node> {
982 let arms = match parse_split_union(union_xpath) {
983 Some(a) => a,
984 None => return self.findnodes(union_xpath),
985 };
986 let mut pages = Vec::new();
987 collect_split_pages(&self.document.as_node(), &arms, &mut pages);
988 pages
989 }
990
991 pub fn findnode(&self, xpath: &str) -> Option<Node> { self.findnodes(xpath).into_iter().next() }
993
994 pub fn findnode_at(&self, xpath: &str, context_node: &Node) -> Option<Node> {
996 self
997 .findnodes_at(xpath, Some(context_node))
998 .into_iter()
999 .next()
1000 }
1001
1002 pub fn findvalue(&self, xpath: &str) -> Option<String> {
1004 let ctx = XPathContext::new(&self.document).ok()?;
1005 for (prefix, uri) in &self.namespaces {
1006 let _ = ctx.register_namespace(prefix, uri);
1007 }
1008 ctx.evaluate(xpath).ok().map(|obj| obj.to_string())
1009 }
1010
1011 pub fn findnodes_foreign(xpath: &str, node: &Node) -> Vec<Node> {
1014 let mut current = node.clone();
1016 while let Some(parent) = current.get_parent() {
1017 current = parent;
1018 }
1019 if let Some(doc) = current.get_parent() {
1022 let _ = doc; }
1025 #[allow(unused_imports)]
1028 use libxml::xpath::Context as XPathContext;
1029 Self::findnodes_by_traversal(xpath, node)
1035 }
1036
1037 fn findnodes_by_traversal(xpath: &str, parent: &Node) -> Vec<Node> {
1041 let xpath = xpath.trim_start_matches('!').trim();
1042 let mut results = Vec::new();
1043
1044 let parts: Vec<&str> = split_steps(xpath);
1048 if parts.is_empty() {
1049 return results;
1050 }
1051
1052 fn split_steps(xpath: &str) -> Vec<&str> {
1054 let mut steps = Vec::new();
1055 let mut depth = 0i32;
1056 let mut start = 0;
1057 for (i, b) in xpath.bytes().enumerate() {
1058 match b {
1059 b'[' => depth += 1,
1060 b']' => depth -= 1,
1061 b'/' if depth == 0 => {
1062 steps.push(&xpath[start..i]);
1063 start = i + 1;
1064 },
1065 _ => {},
1066 }
1067 }
1068 steps.push(&xpath[start..]);
1069 steps
1070 }
1071
1072 fn extract_predicates(s: &str) -> Vec<&str> {
1076 let mut preds = Vec::new();
1077 let mut depth = 0i32;
1078 let mut start = 0;
1079 for (i, b) in s.bytes().enumerate() {
1080 match b {
1081 b'[' => {
1082 if depth == 0 {
1083 start = i + 1;
1084 }
1085 depth += 1;
1086 },
1087 b']' => {
1088 depth -= 1;
1089 if depth == 0 {
1090 preds.push(&s[start..i]);
1091 }
1092 },
1093 _ => {},
1094 }
1095 }
1096 preds
1097 }
1098
1099 fn match_element(node: &Node, pattern: &str) -> bool {
1100 let pattern = pattern.trim().trim_start_matches("ltx:");
1101 let bracket_pos = match pattern.find('[') {
1102 None => return node.get_name() == pattern,
1103 Some(p) => p,
1104 };
1105 let elem_name = &pattern[..bracket_pos];
1106 if node.get_name() != elem_name {
1107 return false;
1108 }
1109 for pred in extract_predicates(&pattern[bracket_pos..]) {
1114 let pred = pred.trim();
1115 if pred.contains('(') {
1116 continue;
1117 }
1118 if let Some(attr) = pred.strip_prefix('@') {
1119 if let Some(eq) = attr.find('=') {
1120 let name = attr[..eq].trim();
1121 let val = attr[eq + 1..].trim().trim_matches('\'').trim_matches('"');
1122 if node.get_attribute(name).as_deref() != Some(val) {
1123 return false;
1124 }
1125 } else if node.get_attribute(attr.trim()).is_none() {
1126 return false;
1127 }
1128 }
1129 }
1130 true
1131 }
1132
1133 fn collect_matching(node: &Node, parts: &[&str], results: &mut Vec<Node>) {
1134 if parts.is_empty() {
1135 return;
1136 }
1137 let pattern = parts[0];
1138 let alternatives: Vec<&str> = pattern.split('|').map(|s| s.trim()).collect();
1140 let mut child = node.get_first_child();
1141 while let Some(c) = child {
1142 for alt in &alternatives {
1143 if match_element(&c, alt) {
1144 if parts.len() == 1 {
1145 results.push(c.clone());
1146 } else {
1147 collect_matching(&c, &parts[1..], results);
1148 }
1149 }
1150 }
1151 child = c.get_next_sibling();
1152 }
1153 }
1154
1155 collect_matching(parent, &parts, &mut results);
1156 results
1157 }
1158
1159 pub fn add_namespace(&mut self, prefix: &str, nsuri: &str) {
1166 let dominated = self
1167 .namespaces
1168 .get(prefix)
1169 .map(|u| u == nsuri)
1170 .unwrap_or(false);
1171 if !dominated {
1172 self
1173 .namespaces
1174 .insert(prefix.to_string(), nsuri.to_string());
1175 self
1176 .namespace_uris
1177 .insert(nsuri.to_string(), prefix.to_string());
1178 if let Some(mut root) = self.document.get_root_element() {
1182 let _ = Namespace::new(prefix, nsuri, &mut root);
1183 }
1184 }
1185 }
1186
1187 pub fn get_qname(&self, node: &Node) -> Option<String> {
1191 if node.get_type() != Some(NodeType::ElementNode) {
1192 return None;
1193 }
1194 let localname = node.get_name();
1195 if let Some(ns) = node.get_namespace() {
1196 let nsuri = ns.get_href();
1197 if let Some(prefix) = self.namespace_uris.get(&nsuri) {
1198 Some(format!("{}:{}", prefix, localname))
1199 } else {
1200 let n = self
1202 .namespaces
1203 .keys()
1204 .filter(|k| k.starts_with("_ns"))
1205 .count()
1206 + 1;
1207 Some(format!("_ns{}:{}", n, localname))
1208 }
1209 } else {
1210 Some(localname)
1211 }
1212 }
1213
1214 pub fn qname_prefix(&self, node: &Node) -> Option<String> {
1220 if node.get_type() != Some(NodeType::ElementNode) {
1221 return None;
1222 }
1223 node.get_namespace().and_then(|ns| {
1224 let nsuri = ns.get_href();
1225 self.namespace_uris.get(&nsuri).cloned()
1226 })
1227 }
1228
1229 pub fn is_qname(&self, node: &Node, expected: &str) -> bool {
1236 if node.get_type() != Some(NodeType::ElementNode) {
1237 return false;
1238 }
1239 let (expected_prefix, expected_local) = match expected.split_once(':') {
1240 Some((p, l)) => (Some(p), l),
1241 None => (None, expected),
1242 };
1243 let localname = node.get_name();
1244 if localname != expected_local {
1245 return false;
1246 }
1247 match (node.get_namespace(), expected_prefix) {
1248 (Some(ns), Some(ep)) => {
1249 let nsuri = ns.get_href();
1250 self
1251 .namespace_uris
1252 .get(&nsuri)
1253 .map(|p| p == ep)
1254 .unwrap_or(false)
1255 },
1256 (None, None) => true,
1257 _ => false,
1258 }
1259 }
1260
1261 pub fn record_id(&mut self, id: &str, node: Node) {
1268 self.idcache.insert(id.to_string(), node);
1269 self.idcache_reserve.remove(id);
1270 self.idcache_reusable.remove(id);
1271 }
1272
1273 pub fn find_node_by_id(&self, id: &str) -> Option<&Node> { self.idcache.get(id) }
1277
1278 pub fn idcache_len(&self) -> usize { self.idcache.len() }
1281
1282 pub fn idcache_iter(&self) -> impl Iterator<Item = (&String, &Node)> { self.idcache.iter() }
1286
1287 pub fn uniquify_id(&mut self, baseid: &str, suffix: Option<&str>) -> String {
1294 let apply_suffix = |id: &str, sfx: Option<&str>| -> String {
1295 if let Some(s) = sfx {
1296 format!("{}{}", id, s)
1297 } else {
1298 id.to_string()
1299 }
1300 };
1301
1302 let mut id = apply_suffix(baseid, suffix);
1303 let cachekey = id.clone();
1304
1305 while (self.idcache.contains_key(&id) || self.idcache_reserve.contains_key(&id))
1306 && !self.idcache_reusable.contains_key(&id)
1307 {
1308 let clash_count = self.idcache_clashes.entry(cachekey.clone()).or_insert(0);
1309 *clash_count += 1;
1310 id = apply_suffix(&format!("{}{}", baseid, radix_alpha(*clash_count)), suffix);
1311 }
1312
1313 self.idcache_reusable.remove(&id);
1314 self.idcache_reserve.insert(id.clone(), true);
1315 id
1316 }
1317
1318 pub fn generate_node_id(
1324 &mut self,
1325 node: &mut Node,
1326 prefix: &str,
1327 reusable: bool,
1328 ) -> Option<String> {
1329 if let Some(id) = get_xml_id(node) {
1330 return Some(id);
1331 }
1332
1333 let mut parent_node = node.get_parent();
1337 let mut pid = String::new();
1338 while let Some(ref p) = parent_node {
1339 if let Some(id) = get_xml_id(p) {
1340 pid = id;
1341 break;
1342 }
1343 parent_node = p.get_parent();
1344 }
1345
1346 if !pid.is_empty() {
1347 pid.push('.');
1348 }
1349
1350 let mut n = 1u32;
1352 let id = loop {
1353 let candidate = format!("{}{}{}", pid, prefix, n);
1354 if !self.idcache.contains_key(&candidate) && !self.idcache_reserve.contains_key(&candidate) {
1355 break candidate;
1356 }
1357 n += 1;
1358 };
1359
1360 node.set_attribute("xml:id", &id).ok();
1361 let node_copy = node.clone();
1362 self.idcache.insert(id.clone(), node_copy);
1363 if reusable {
1364 self.idcache_reusable.insert(id.clone(), true);
1365 }
1366
1367 if let Some(ref p) = parent_node {
1369 if p.get_attribute("fragid").is_some() {
1370 let new_fragid = format!("{}.{}{}", p.get_attribute("fragid").unwrap(), prefix, n);
1371 node.set_attribute("fragid", &new_fragid).ok();
1372 }
1373 }
1374
1375 Some(id)
1376 }
1377
1378 pub fn add_nodes(&mut self, parent: &mut Node, data: &[NodeData]) {
1385 for child in data {
1386 match child {
1387 NodeData::Text(text) => {
1388 parent.append_text(text).ok();
1389 },
1390 NodeData::Element { tag, attributes, children } => {
1391 debug_assert!(
1400 !((tag == "m:mi" || tag == "mi") && children.is_empty()),
1401 "Empty <mi></mi> detected at materialization — use <mrow></mrow> \
1402 scaffolding instead; see task #264 in docs/SYNC_STATUS.md"
1403 );
1404 if tag == "_Fragment_" {
1405 self.add_nodes(parent, children);
1406 } else if let Some((prefix, localname)) = tag.split_once(':') {
1407 let nsuri = self.namespaces.get(prefix).cloned();
1408 if nsuri.is_none() {
1409 Warn!("malformed", "namespace", "No namespace on '{}'", tag);
1410 }
1411 let ns = nsuri.and_then(|uri| {
1415 parent
1417 .get_namespace_declarations()
1418 .into_iter()
1419 .find(|ns| ns.get_prefix().is_empty() && ns.get_href() == uri)
1420 .or_else(|| {
1421 parent
1422 .get_namespaces(&self.document)
1423 .into_iter()
1424 .find(|ns| ns.get_prefix().is_empty() && ns.get_href() == uri)
1425 })
1426 .or_else(|| {
1428 parent
1429 .get_namespace_declarations()
1430 .into_iter()
1431 .find(|ns| ns.get_prefix() == prefix)
1432 })
1433 .or_else(|| {
1434 parent
1435 .get_namespaces(&self.document)
1436 .into_iter()
1437 .find(|ns| ns.get_prefix() == prefix)
1438 })
1439 .or_else(|| {
1440 Namespace::new(prefix, &uri, parent).ok()
1442 })
1443 });
1444 if let Ok(mut new_node) = parent.new_child(ns, localname) {
1445 if let Some(attrs) = attributes {
1447 let mut sorted_keys: Vec<_> = attrs.keys().collect();
1448 sorted_keys.sort();
1449 for key in sorted_keys {
1450 let value = &attrs[key];
1451 if key.starts_with('_') {
1452 continue;
1453 }
1454 if key == "xml:id" {
1455 let id = if self.idcache.contains_key(value.as_str()) {
1456 self.uniquify_id(value, None)
1457 } else {
1458 value.clone()
1459 };
1460 self.record_id(&id, new_node.clone());
1461 new_node.set_attribute("xml:id", &id).ok();
1462 } else {
1463 new_node.set_attribute(key, value).ok();
1464 }
1465 }
1466 }
1467 self.add_nodes(&mut new_node, children);
1468 }
1469 } else {
1470 Warn!(
1471 "malformed",
1472 "namespace",
1473 "Tag '{}' has no namespace prefix",
1474 tag
1475 );
1476 }
1477 },
1478 NodeData::XmlNode(source_node) => {
1479 self.append_clone(parent, source_node);
1480 },
1481 }
1482 }
1483 }
1484
1485 fn append_clone(&mut self, parent: &mut Node, source: &Node) {
1499 let mut idmap: HashMap<String, String> = HashMap::default();
1500 let Some(root) = self.clone_subtree(parent, source, &mut idmap) else {
1501 return;
1502 };
1503 if !idmap.is_empty() {
1504 for mut n in self.findnodes_at("descendant-or-self::*[@idref]", Some(&root)) {
1505 if let Some(idref) = n.get_attribute("idref") {
1506 if let Some(newid) = idmap.get(&idref) {
1507 n.set_attribute("idref", newid).ok();
1508 }
1509 }
1510 }
1511 }
1512 for mut n in self.findnodes_at("descendant-or-self::*[@labels]", Some(&root)) {
1513 let _ = n.remove_attribute("labels");
1514 }
1515 }
1516
1517 fn clone_subtree(
1524 &mut self,
1525 parent: &mut Node,
1526 source: &Node,
1527 idmap: &mut HashMap<String, String>,
1528 ) -> Option<Node> {
1529 match source.get_type() {
1530 Some(NodeType::ElementNode) => {
1531 let localname = source.get_name();
1532 let ns = source.get_namespace().and_then(|src_ns| {
1541 let uri = src_ns.get_href();
1542 let prefix = src_ns.get_prefix();
1543 parent
1544 .get_namespace_declarations()
1545 .into_iter()
1546 .find(|n| n.get_prefix().is_empty() && n.get_href() == uri)
1547 .or_else(|| {
1548 parent
1549 .get_namespaces(&self.document)
1550 .into_iter()
1551 .find(|n| n.get_prefix().is_empty() && n.get_href() == uri)
1552 })
1553 .or_else(|| {
1554 parent
1555 .get_namespace_declarations()
1556 .into_iter()
1557 .find(|n| n.get_prefix() == prefix)
1558 })
1559 .or_else(|| {
1560 parent
1561 .get_namespaces(&self.document)
1562 .into_iter()
1563 .find(|n| n.get_prefix() == prefix)
1564 })
1565 .or_else(|| Namespace::new(&prefix, &uri, parent).ok())
1566 });
1567 let mut new_node = parent.new_child(ns, &localname).ok()?;
1568
1569 let src_xmlid = get_xml_id(source);
1573 let src_fragid = source.get_attribute("fragid");
1574 let new_xmlid = src_xmlid.as_ref().map(|id| {
1575 let newid = if self.idcache.contains_key(id.as_str()) {
1576 self.uniquify_id(id, None)
1577 } else {
1578 id.clone()
1579 };
1580 idmap.insert(id.clone(), newid.clone());
1581 self.record_id(&newid, new_node.clone());
1582 newid
1583 });
1584 let new_fragid = match (&src_xmlid, &new_xmlid, &src_fragid) {
1586 (Some(id), Some(newid), Some(fragid)) => Some(remap_fragid(id, newid, fragid)),
1587 _ => src_fragid.clone(),
1588 };
1589
1590 for (key, value) in &source.get_properties() {
1593 if key.starts_with('_') || key == "fragid" {
1594 continue;
1595 }
1596 let is_xmlid = key == "xml:id" || (key == "id" && src_xmlid.as_deref() == Some(value));
1597 if is_xmlid {
1598 continue;
1599 }
1600 new_node.set_attribute(key, value).ok();
1601 }
1602 if let Some(newid) = &new_xmlid {
1603 new_node.set_attribute("xml:id", newid).ok();
1604 }
1605 if let Some(fragid) = &new_fragid {
1606 new_node.set_attribute("fragid", fragid).ok();
1607 }
1608
1609 let mut child = source.get_first_child();
1611 while let Some(c) = child {
1612 self.clone_subtree(&mut new_node, &c, idmap);
1613 child = c.get_next_sibling();
1614 }
1615 Some(new_node)
1616 },
1617 Some(NodeType::TextNode) => {
1618 parent.append_text(&source.get_content()).ok();
1619 None
1620 },
1621 Some(NodeType::DocumentFragNode) => {
1622 let mut child = source.get_first_child();
1623 while let Some(c) = child {
1624 self.clone_subtree(parent, &c, idmap);
1625 child = c.get_next_sibling();
1626 }
1627 None
1628 },
1629 _ => None,
1630 }
1631 }
1632
1633 pub fn remove_nodes(&mut self, nodes: &[Node]) {
1637 fn collect_ids_of_subtree(node: &Node, out: &mut Vec<String>) {
1638 if node.get_type() != Some(NodeType::ElementNode) {
1639 return;
1640 }
1641 if let Some(id) = get_xml_id(node) {
1642 out.push(id);
1643 }
1644 let mut child = node.get_first_child();
1645 while let Some(c) = child {
1646 collect_ids_of_subtree(&c, out);
1647 child = c.get_next_sibling();
1648 }
1649 }
1650
1651 for node in nodes {
1652 if node.get_type() == Some(NodeType::ElementNode) {
1653 let mut ids = Vec::new();
1655 collect_ids_of_subtree(node, &mut ids);
1656 for id in ids {
1657 self.idcache.remove(&id);
1658 }
1659 }
1660 let mut n = node.clone();
1661 n.unlink_node();
1662 }
1663 }
1664
1665 pub fn preremove_nodes(&mut self, nodes: &[Node]) {
1669 for node in nodes {
1670 if node.get_type() == Some(NodeType::ElementNode) {
1671 for idd in self.findnodes_at("descendant-or-self::*[@xml:id]", Some(node)) {
1672 if let Some(id) = get_xml_id(&idd) {
1675 self.idcache_reusable.insert(id, true);
1676 }
1677 }
1678 }
1679 }
1680 }
1681
1682 pub fn defer_xmath_unlink(&mut self, node: Node) { self.pending_xmath_unlinks.push(node); }
1695
1696 pub fn drain_pending_xmath_unlinks(&mut self) {
1705 let pending = std::mem::take(&mut self.pending_xmath_unlinks);
1706 for node in pending {
1707 node.free_subtree();
1715 }
1716 }
1717
1718 pub fn remove_blank_nodes(&self, node: &Node) -> u32 {
1722 let mut count = 0;
1723 if let Some(child) = node.get_first_child() {
1724 let mut current = Some(child);
1725 while let Some(ref mut c) = current {
1726 let next = c.get_next_sibling();
1727 if c.get_type() == Some(NodeType::TextNode) {
1728 let text = c.get_content();
1729 if text.trim().is_empty() {
1730 c.unlink_node();
1731 count += 1;
1732 }
1733 }
1734 current = next;
1735 }
1736 }
1737 count
1738 }
1739
1740 pub fn replace_node(&mut self, old_node: &Node, replacements: &[NodeData]) {
1744 if let Some(mut parent) = old_node.get_parent() {
1745 let mut save = Vec::new();
1747 while let Some(mut last) = parent.get_last_child() {
1748 if last == *old_node {
1749 break;
1750 }
1751 last.unlink_node();
1752 save.insert(0, last);
1753 }
1754
1755 self.remove_nodes(&[old_node.clone()]);
1757
1758 self.add_nodes(&mut parent, replacements);
1760
1761 for mut s in save {
1763 parent.add_child(&mut s).ok();
1764 }
1765 }
1766 }
1767
1768 pub fn prepend_nodes(&mut self, parent: &mut Node, nodes: &[NodeData]) {
1772 let mut save = Vec::new();
1774 while let Some(mut last) = parent.get_last_child() {
1775 last.unlink_node();
1776 save.insert(0, last);
1777 }
1778
1779 self.add_nodes(parent, nodes);
1781
1782 for mut s in save {
1784 parent.add_child(&mut s).ok();
1785 }
1786 }
1787
1788 pub fn add_ss_values(node: &mut Node, key: &str, values: &str) {
1800 if values.is_empty() {
1801 return;
1802 }
1803 let new_values: Vec<&str> = values.split_whitespace().collect();
1804 if let Some(old_values_str) = node.get_attribute(key) {
1805 let mut all: Vec<String> = old_values_str
1806 .split_whitespace()
1807 .map(String::from)
1808 .collect();
1809 for v in &new_values {
1810 if !all.iter().any(|o| o == v) {
1811 all.push(v.to_string());
1812 }
1813 }
1814 all.sort();
1815 node.set_attribute(key, &all.join(" ")).ok();
1816 } else {
1817 let mut sorted: Vec<&str> = new_values;
1818 sorted.sort_unstable();
1819 node.set_attribute(key, &sorted.join(" ")).ok();
1820 }
1821 }
1822
1823 pub fn add_class(node: &mut Node, class: &str) { Self::add_ss_values(node, "class", class); }
1827
1828 pub fn mark_xm_node_visibility(&self) {
1835 for mut math_child in self.findnodes("//ltx:XMath/*") {
1836 self.mark_xm_node_visibility_aux(&mut math_child, true, true);
1837 }
1838 }
1839
1840 fn mark_xm_node_visibility_aux(&self, node: &mut Node, cvis: bool, pvis: bool) {
1841 let qname = match self.get_qname(node) {
1842 Some(q) => q,
1843 None => return,
1844 };
1845
1846 let has_cvis = node.get_attribute("_cvis").is_some();
1847 let has_pvis = node.get_attribute("_pvis").is_some();
1848 if (!cvis || has_cvis) && (!pvis || has_pvis) {
1849 return;
1850 }
1851
1852 if cvis {
1853 node.set_attribute("_cvis", "1").ok();
1854 }
1855 if pvis {
1856 node.set_attribute("_pvis", "1").ok();
1857 }
1858
1859 if qname == "ltx:XMDual" {
1860 let mut children = element_children(node);
1861 if children.len() >= 2 {
1862 if cvis {
1863 self.mark_xm_node_visibility_aux(&mut children[0], true, false);
1864 }
1865 if pvis {
1866 self.mark_xm_node_visibility_aux(&mut children[1], false, true);
1867 }
1868 }
1869 } else if qname == "ltx:XMRef" {
1870 if let Some(idref) = node.get_attribute("idref") {
1871 if let Some(target) = self.find_node_by_id(&idref) {
1872 let mut target_mut = target.clone();
1873 self.mark_xm_node_visibility_aux(&mut target_mut, cvis, pvis);
1874 } else {
1875 Error!(
1878 "expected",
1879 "id",
1880 "Cannot find a node with xml:id='{}'",
1881 idref
1882 );
1883 }
1884 }
1885 } else {
1886 for mut child in element_children(node) {
1887 self.mark_xm_node_visibility_aux(&mut child, cvis, pvis);
1888 }
1889 }
1890 }
1891
1892 pub fn realize_xm_node_branch(&self, node: &Node, branch: XMBranch) -> Option<Node> {
1901 let mut node = node.clone();
1902 loop {
1903 if self.is_qname(&node, "ltx:XMRef") {
1904 let idref = node.get_attribute("idref")?;
1905 match self.find_node_by_id(&idref) {
1906 Some(target) => node = target.clone(),
1907 None => {
1908 Error!(
1909 "expected",
1910 "id",
1911 "Cannot find a node with xml:id='{}'",
1912 idref
1913 );
1914 return None;
1915 },
1916 }
1917 } else if self.is_qname(&node, "ltx:XMDual") {
1918 let children = element_children(&node);
1922 node = children.get(branch as usize)?.clone();
1923 } else {
1924 return Some(node);
1925 }
1926 }
1927 }
1928
1929 pub fn realize_xm_node(&self, node: &Node) -> Option<Node> {
1935 if self.is_qname(node, "ltx:XMRef") {
1936 let idref = node.get_attribute("idref")?;
1937 let realized = self.find_node_by_id(&idref).cloned();
1938 if realized.is_none() {
1939 Error!(
1942 "expected",
1943 "id",
1944 "Cannot find a node with xml:id='{}'",
1945 idref
1946 );
1947 }
1948 realized
1949 } else {
1950 Some(node.clone())
1951 }
1952 }
1953
1954 pub fn conjoin(conjunction: Conjunction, nodes: Vec<NodeData>) -> Vec<NodeData> {
1961 let n = nodes.len();
1962 if n < 2 {
1963 return nodes;
1964 }
1965
1966 let (comma, and) = match conjunction {
1967 Conjunction::Simple(s) => (s.clone(), s),
1968 Conjunction::Pair(c, a) => (c, a),
1969 };
1970
1971 let mut result = Vec::new();
1972 let mut iter = nodes.into_iter();
1973 result.push(iter.next().unwrap());
1974
1975 let mut remaining: Vec<_> = iter.collect();
1976 while remaining.len() > 1 {
1977 result.push(NodeData::Text(comma.clone()));
1978 result.push(remaining.remove(0));
1979 }
1980 result.push(NodeData::Text(and));
1981 result.push(remaining.remove(0));
1982 result
1983 }
1984
1985 pub fn initial(string: &str, force: bool) -> String {
1989 let decomposed: String = string.nfd().collect();
1990 let trimmed = decomposed.trim_start();
1991 let s = if force {
1992 trimmed.trim_start_matches(|c: char| !c.is_ascii_alphabetic())
1993 } else {
1994 trimmed
1995 };
1996 match s.chars().next() {
1997 Some(c) if c.is_ascii_alphabetic() => c.to_uppercase().to_string(),
1998 _ => "*".to_string(),
1999 }
2000 }
2001
2002 pub fn trim_child_nodes(node: &Node) -> Vec<Node> {
2006 let mut children: Vec<Node> = Vec::new();
2007 if let Some(child) = node.get_first_child() {
2008 let mut current = Some(child);
2009 while let Some(ref c) = current {
2010 children.push(c.clone());
2011 current = c.get_next_sibling();
2012 }
2013 }
2014
2015 if children.is_empty() {
2016 return children;
2017 }
2018
2019 if let Some(first) = children.first_mut() {
2021 if first.get_type() == Some(NodeType::TextNode) {
2022 let text = first.get_content();
2023 let trimmed = text.trim_start();
2024 if trimmed.is_empty() {
2025 children.remove(0);
2026 } else if trimmed != text {
2027 first.set_content(trimmed).ok();
2028 }
2029 }
2030 }
2031
2032 if let Some(last) = children.last_mut() {
2034 if last.get_type() == Some(NodeType::TextNode) {
2035 let text = last.get_content();
2036 let trimmed = text.trim_end();
2037 if trimmed.is_empty() {
2038 children.pop();
2039 } else if trimmed != text {
2040 last.set_content(trimmed).ok();
2041 }
2042 }
2043 }
2044
2045 children
2046 }
2047
2048 pub fn add_navigation(&mut self, relation: &str, id: &str) {
2052 if self.navigation_ref_present(relation, id) {
2058 return;
2059 }
2060
2061 let ref_node = NodeData::Element {
2062 tag: "ltx:ref".to_string(),
2063 attributes: Some(HashMap::from_iter([
2064 ("idref".to_string(), id.to_string()),
2065 ("rel".to_string(), relation.to_string()),
2066 ("show".to_string(), "toctitle".to_string()),
2067 ])),
2068 children: vec![],
2069 };
2070
2071 match self.navigation_element() {
2072 Some(mut nav) => {
2073 self.add_nodes(&mut nav, &[ref_node]);
2074 self.record_navigation_ref(relation, id);
2075 },
2076 _ => {
2077 if let Some(mut root) = self.get_document_element() {
2078 let nav_node = NodeData::Element {
2079 tag: "ltx:navigation".to_string(),
2080 attributes: None,
2081 children: vec![ref_node],
2082 };
2083 self.add_nodes(&mut root, &[nav_node]);
2084 let found = self.findnode("//ltx:navigation");
2087 if let Some(memo) = self.nav_memo.as_mut() {
2088 memo.element = found;
2089 }
2090 self.record_navigation_ref(relation, id);
2091 }
2092 },
2096 }
2097 }
2098
2099 fn navigation_element(&mut self) -> Option<Node> {
2107 if let Some(memo) = self.nav_memo.as_ref()
2108 && let Some(nav) = memo.element.as_ref()
2109 && nav.get_parent().is_some()
2110 {
2111 return Some(nav.clone());
2112 }
2113 let found = self.findnode("//ltx:navigation");
2114 if let Some(memo) = self.nav_memo.as_mut() {
2115 memo.element = found.clone();
2116 }
2117 found
2118 }
2119
2120 fn seed_navigation_memo(&mut self) {
2134 if self.nav_memo.is_some() {
2135 return;
2136 }
2137 let mut refs = FxHashSet::default();
2138 let elements = self.findnodes("//ltx:navigation");
2139 for nav in &elements {
2140 for child in nav.get_child_nodes() {
2141 if child.get_name() != "ref" {
2142 continue;
2143 }
2144 let in_ltx = child
2147 .get_namespace()
2148 .is_some_and(|ns| ns.get_href() == LTX_NSURI);
2149 if !in_ltx {
2150 continue;
2151 }
2152 if let (Some(rel), Some(idref)) = (child.get_attribute("rel"), child.get_attribute("idref"))
2153 {
2154 refs.insert((rel, idref));
2155 }
2156 }
2157 }
2158 self.nav_memo = Some(NavigationMemo {
2159 element: elements.into_iter().next(),
2160 refs,
2161 });
2162 }
2163
2164 fn navigation_ref_present(&mut self, relation: &str, id: &str) -> bool {
2165 self.seed_navigation_memo();
2166 self
2167 .nav_memo
2168 .as_ref()
2169 .is_some_and(|memo| memo.refs.contains(&(relation.to_string(), id.to_string())))
2170 }
2171
2172 fn record_navigation_ref(&mut self, relation: &str, id: &str) {
2174 if let Some(memo) = self.nav_memo.as_mut() {
2175 memo.refs.insert((relation.to_string(), id.to_string()));
2176 }
2177 }
2178
2179 pub fn validate(&self) -> Result<(), String> {
2186 let rng_re = Regex::new(r#"^\s*RelaxNGSchema\s*=\s*[\"'](.*?)[\"']\s*$"#).unwrap();
2187 for pi_text in &self.processing_instructions {
2188 if let Some(cap) = rng_re.captures(pi_text) {
2189 let schema = &cap[1];
2190 Info!(
2191 "validate",
2192 "schema",
2193 "Would validate against RelaxNG schema: {}",
2194 schema
2195 );
2196 return Ok(());
2197 }
2198 }
2199 Warn!(
2204 "missing_file",
2205 "schema",
2206 "No schema found for document validation"
2207 );
2208 Ok(())
2209 }
2210
2211 pub fn idcheck(&self) {
2215 let mut doc_ids: HashMap<String, bool> = HashMap::default();
2216 let mut dups = Vec::new();
2217
2218 for node in self.findnodes("//*[@xml:id]") {
2219 if let Some(id) = get_xml_id(&node) {
2220 if doc_ids.contains_key(&id) {
2221 dups.push(id.clone());
2222 }
2223 doc_ids.insert(id, true);
2224 }
2225 }
2226
2227 let mut missing = Vec::new();
2228 for id in self.idcache.keys() {
2229 if !doc_ids.contains_key(id) {
2230 missing.push(id.clone());
2231 }
2232 }
2233
2234 if !dups.is_empty() {
2235 Warn!(
2236 "malformed",
2237 "id",
2238 "Duplicate IDs for {}: {}",
2239 self.site_relative_destination().unwrap_or_default(),
2240 dups.join(", ")
2241 );
2242 }
2243 if !missing.is_empty() {
2244 Warn!(
2245 "expected",
2246 "id",
2247 "Cached IDs not in document for {}: {}",
2248 self.site_relative_destination().unwrap_or_default(),
2249 missing.join(", ")
2250 );
2251 }
2252 }
2253
2254 pub fn cache_lookup(&self, key: &str) -> Option<String> { self.cache.get(key).cloned() }
2259
2260 pub fn cache_store(&mut self, key: &str, value: &str) {
2262 self.cache.insert(key.to_string(), value.to_string());
2263 }
2264
2265 pub fn cache_remove(&mut self, key: &str) { self.cache.remove(key); }
2267}
2268
2269#[derive(Debug, Default, Clone)]
2274pub struct PostDocumentOptions {
2275 pub destination: Option<String>,
2276 pub destination_directory: Option<String>,
2277 pub site_directory: Option<String>,
2278 pub source: Option<String>,
2279 pub source_directory: Option<String>,
2280 pub searchpaths: Option<Vec<String>>,
2281 pub validate: bool,
2282 pub nocache: bool,
2283}
2284
2285#[derive(Debug, Clone)]
2289pub enum NodeData {
2290 Text(String),
2292 Element {
2294 tag: String,
2295 attributes: Option<HashMap<String, String>>,
2296 children: Vec<NodeData>,
2297 },
2298 XmlNode(Node),
2300}
2301
2302#[derive(Clone, Copy, PartialEq, Eq, Debug)]
2309pub enum XMBranch {
2310 Content = 0,
2312 Presentation = 1,
2314}
2315
2316pub enum Conjunction {
2318 Simple(String),
2320 Pair(String, String),
2322}
2323
2324pub fn element_children(node: &Node) -> Vec<Node> {
2329 let mut result = Vec::new();
2330 if let Some(child) = node.get_first_child() {
2331 let mut current = Some(child);
2332 while let Some(ref c) = current {
2333 if c.get_type() == Some(NodeType::ElementNode) {
2334 result.push(c.clone());
2335 }
2336 current = c.get_next_sibling();
2337 }
2338 }
2339 result
2340}
2341
2342pub fn element_children_iter(node: &Node) -> impl Iterator<Item = Node> + use<> {
2347 let first = node.get_first_child();
2348 std::iter::successors(first, |c| c.get_next_sibling())
2349 .filter(|c| c.get_type() == Some(NodeType::ElementNode))
2350}
2351
2352pub fn escape_xml(s: &str) -> String {
2373 s.replace('&', "&")
2374 .replace('<', "<")
2375 .replace('>', ">")
2376 .replace('"', """)
2377}
2378
2379fn pathdiff(path: &str, base: &str) -> String {
2381 let p = Path::new(path);
2382 let b = Path::new(base);
2383 if let Ok(rel) = p.strip_prefix(b) {
2384 rel.to_string_lossy().to_string()
2385 } else {
2386 path.to_string()
2387 }
2388}
2389
2390#[cfg(test)]
2391mod tests {
2392 use super::*;
2393
2394 fn make_test_doc(xml: &str) -> PostDocument {
2395 PostDocument::new_from_string(xml, PostDocumentOptions::default()).unwrap()
2396 }
2397
2398 #[test]
2409 fn add_navigation_dedupes_including_preexisting_refs() {
2410 let mut doc = make_test_doc(
2411 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2412 <navigation>\
2413 <ref rel='chapter' idref='Ch1' show='toctitle'/>\
2414 <title>ignored — not an ltx:ref</title>\
2415 </navigation>\
2416 </document>",
2417 );
2418
2419 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");
2425 let mut pairs: Vec<(String, String)> = refs
2426 .iter()
2427 .map(|n| {
2428 (
2429 n.get_attribute("rel").unwrap_or_default(),
2430 n.get_attribute("idref").unwrap_or_default(),
2431 )
2432 })
2433 .collect();
2434 pairs.sort();
2435 assert_eq!(pairs, vec![
2436 ("chapter".to_string(), "Ch1".to_string()),
2437 ("section".to_string(), "S1".to_string()),
2438 ("sidebar".to_string(), "Ch1".to_string()),
2439 ]);
2440 assert_eq!(
2441 doc.findnodes("//ltx:navigation").len(),
2442 1,
2443 "the existing navigation element must be reused, not duplicated"
2444 );
2445 }
2446
2447 #[test]
2454 fn add_navigation_sees_refs_under_every_navigation_element() {
2455 let mut doc = make_test_doc(
2456 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2457 <navigation><ref rel='chapter' idref='Ch1'/></navigation>\
2458 <section><navigation><ref rel='section' idref='S9'/></navigation></section>\
2459 </document>",
2460 );
2461
2462 doc.add_navigation("section", "S9"); doc.add_navigation("chapter", "Ch1"); assert_eq!(
2466 doc.findnodes("//ltx:navigation/ltx:ref").len(),
2467 2,
2468 "neither pair may be re-added; `//` spans both navigation elements"
2469 );
2470
2471 doc.add_navigation("appendix", "A1"); let first_nav_refs = doc.findnodes("//ltx:navigation").first().map(|n| {
2473 n.get_child_nodes()
2474 .iter()
2475 .filter(|c| c.get_name() == "ref")
2476 .count()
2477 });
2478 assert_eq!(
2479 first_nav_refs,
2480 Some(2),
2481 "a new ref lands under the FIRST navigation element (Perl's findnode)"
2482 );
2483 }
2484
2485 #[test]
2488 fn add_navigation_ignores_a_foreign_namespace_ref_when_seeding() {
2489 let mut doc = make_test_doc(
2490 "<document xmlns='http://dlmf.nist.gov/LaTeXML' xmlns:other='http://example.org/other'>\
2491 <navigation><other:ref rel='chapter' idref='Ch1'/></navigation>\
2492 </document>",
2493 );
2494
2495 doc.add_navigation("chapter", "Ch1");
2496
2497 assert_eq!(
2498 doc.findnodes("//ltx:navigation/ltx:ref").len(),
2499 1,
2500 "the foreign-namespace ref is not an ltx:ref, so the real one must be added"
2501 );
2502 }
2503
2504 #[test]
2508 fn navigation_memo_agrees_with_the_original_xpath_probe() {
2509 let mut doc = make_test_doc(
2510 "<document xmlns='http://dlmf.nist.gov/LaTeXML' xmlns:other='http://example.org/other'>\
2511 <navigation>\
2512 <ref rel='chapter' idref='Ch1'/>\
2513 <title>t</title>\
2514 <other:ref rel='section' idref='S1'/>\
2515 </navigation>\
2516 </document>",
2517 );
2518
2519 for (rel, id) in [
2520 ("chapter", "Ch1"), ("section", "S1"), ("title", "t"), ("section", "S2"), ] {
2525 let probe = format!("//ltx:navigation/ltx:ref[@rel='{}'][@idref='{}']", rel, id);
2527 let xpath_says_present = doc.findnode(&probe).is_some();
2528 let memo_says_present = doc.navigation_ref_present(rel, id);
2529 assert_eq!(
2530 memo_says_present, xpath_says_present,
2531 "memo and XPath disagree about ({rel}, {id})"
2532 );
2533 }
2534 }
2535
2536 #[test]
2537 fn add_navigation_creates_then_reuses_the_navigation_element() {
2538 let mut doc = make_test_doc("<document xmlns='http://dlmf.nist.gov/LaTeXML'><p/></document>");
2539
2540 doc.add_navigation("section", "S1");
2541 doc.add_navigation("section", "S2");
2542 doc.add_navigation("section", "S1"); assert_eq!(
2545 doc.findnodes("//ltx:navigation").len(),
2546 1,
2547 "exactly one navigation element must be created"
2548 );
2549 assert_eq!(doc.findnodes("//ltx:navigation/ltx:ref").len(), 2);
2550 }
2551
2552 #[test]
2553 fn walk_union_agrees_with_xpath_on_every_supported_shape() {
2554 let doc = make_test_doc(
2555 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2556 <ref href='u1'/>\
2557 <ref href='u2' idref='i1'/>\
2558 <ref labelref='L1'/>\
2559 <graphics/>\
2560 <graphics imagesrc='a.png'/>\
2561 <Math id='m1'><XMath><Math id='inner'/></XMath></Math>\
2562 <index/>\
2563 <index><indexlist/></index>\
2564 <glossary/>\
2565 <p idref='i2'/>\
2566 <XMDual _cvis='1'/>\
2567 <XMDual _pvis='1'/>\
2568 <XMDual _cvis='1' _pvis='1'/>\
2569 <XMDual/>\
2570 </document>",
2571 );
2572 for xpath in [
2573 "//*[@idref]",
2574 "//*[@labelref]",
2575 "//ltx:ref[@href and not(@idref) and not(@labelref)]",
2576 "//ltx:graphics[not(@imagesrc)]",
2577 "//ltx:Math[not(ancestor::ltx:Math)]",
2578 "//ltx:index[not(ltx:indexlist)] | //ltx:glossary[not(ltx:glossarylist)]",
2579 "//ltx:ref",
2580 "//*[@_cvis or @_pvis]",
2582 "//ltx:ref[@href or @idref]",
2583 ] {
2584 let arms = parse_walk_union(xpath)
2585 .unwrap_or_else(|| panic!("`{xpath}` must be inside the walk grammar"));
2586 let mut walked = Vec::new();
2587 collect_walk_matches(&doc.get_document_element().unwrap(), &arms, &mut walked);
2588 let ctx = libxml::xpath::Context::new(&doc.document).expect("ctx");
2592 ctx.register_namespace("ltx", LTX_NSURI).expect("ns");
2593 let root = doc.get_document_element().unwrap();
2594 let expected: Vec<String> = ctx
2595 .node_evaluate(xpath, &root)
2596 .expect("xpath evaluates on a small doc")
2597 .get_nodes_as_vec()
2598 .iter()
2599 .map(|n| format!("{}#{:?}", n.get_name(), n.get_attribute("id")))
2600 .collect();
2601 let got: Vec<String> = walked
2602 .iter()
2603 .map(|n| format!("{}#{:?}", n.get_name(), n.get_attribute("id")))
2604 .collect();
2605 assert_eq!(got, expected, "walk disagrees with XPath for `{xpath}`");
2606 }
2607 }
2608
2609 #[test]
2612 fn unsupported_shapes_are_not_claimed_by_the_walk() {
2613 for xpath in [
2614 "//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]", ] {
2621 assert!(
2622 parse_walk_union(xpath).is_none(),
2623 "`{xpath}` must fall through to XPath, not be walked"
2624 );
2625 }
2626 }
2627
2628 #[test]
2629 fn test_new_from_string() {
2630 let doc = make_test_doc("<document xmlns='http://dlmf.nist.gov/LaTeXML'/>");
2631 assert!(doc.get_document_element().is_some());
2632 }
2633
2634 #[test]
2635 fn test_findnodes() {
2636 let doc = make_test_doc(
2637 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2638 <section xml:id='s1'/>\
2639 <section xml:id='s2'/>\
2640 </document>",
2641 );
2642 let sections = doc.findnodes("//ltx:section");
2643 assert_eq!(sections.len(), 2);
2644 }
2645
2646 #[test]
2654 fn findnodes_resolves_relative_axes_without_context_node() {
2655 let doc = make_test_doc(
2656 "<?latexml class='book'?>\
2657 <document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2658 <resource src='a.css' type='text/css'/>\
2659 <section xml:id='s1'/>\
2660 </document>",
2661 );
2662 assert_eq!(doc.findnodes("//ltx:resource").len(), 1, "absolute");
2663 assert_eq!(
2664 doc.findnodes("descendant::ltx:resource").len(),
2665 1,
2666 "descendant:: axis must resolve without an explicit context node"
2667 );
2668 assert_eq!(
2669 doc.findnodes(".//ltx:resource").len(),
2670 1,
2671 ".// axis must resolve without an explicit context node"
2672 );
2673 assert_eq!(
2675 doc.findnodes("//processing-instruction('latexml')").len(),
2676 1,
2677 "absolute PI query finds the before-root <?latexml?>"
2678 );
2679 }
2680
2681 const SECTION_UNION: &str = "//ltx:section | \
2684 //ltx:bibliography[preceding-sibling::ltx:section or parent::ltx:part or parent::ltx:chapter] | \
2685 //ltx:appendix[preceding-sibling::ltx:section or parent::ltx:part or parent::ltx:chapter] | \
2686 //ltx:index[preceding-sibling::ltx:section or parent::ltx:part or parent::ltx:chapter] | \
2687 //ltx:part | \
2688 //ltx:bibliography[preceding-sibling::ltx:part] | \
2689 //ltx:appendix[preceding-sibling::ltx:part] | \
2690 //ltx:index[preceding-sibling::ltx:part] | \
2691 //ltx:chapter | \
2692 //ltx:bibliography[preceding-sibling::ltx:chapter or parent::ltx:part] | \
2693 //ltx:appendix[preceding-sibling::ltx:chapter or parent::ltx:part] | \
2694 //ltx:index[preceding-sibling::ltx:chapter or parent::ltx:part]";
2695
2696 fn split_doc() -> PostDocument {
2697 make_test_doc(
2698 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2699 <part xml:id='P1'>\
2700 <chapter xml:id='C1'>\
2701 <section xml:id='S1'/>\
2702 <section xml:id='S2'/>\
2703 <index xml:id='I1'/>\
2704 </chapter>\
2705 <bibliography xml:id='B1'/>\
2706 </part>\
2707 <section xml:id='S3'/>\
2708 <index xml:id='I2'/>\
2709 </document>",
2710 )
2711 }
2712
2713 #[test]
2716 fn test_find_split_pages_matches_xpath() {
2717 let doc = split_doc();
2718 let ids = |nodes: Vec<Node>| -> Vec<String> { nodes.iter().filter_map(get_xml_id).collect() };
2719 let via_walk = ids(doc.find_split_pages(SECTION_UNION));
2720 let via_xpath = ids(doc.findnodes(SECTION_UNION));
2721 assert_eq!(
2722 via_walk,
2723 vec!["P1", "C1", "S1", "S2", "I1", "B1", "S3", "I2"],
2724 "walk selected the wrong pages / order"
2725 );
2726 assert_eq!(via_walk, via_xpath, "walk diverged from XPath union");
2727 }
2728
2729 #[test]
2732 fn test_find_split_pages_fallback() {
2733 let doc = split_doc();
2734 let via = doc.find_split_pages("//ltx:chapter/descendant::ltx:section");
2736 let ids: Vec<String> = via.iter().filter_map(get_xml_id).collect();
2737 assert_eq!(ids, vec!["S1", "S2"]);
2738 }
2739
2740 #[test]
2743 fn test_scan_ids_populates_idcache() {
2744 let doc = split_doc();
2745 for id in ["P1", "C1", "S1", "S2", "I1", "B1", "S3", "I2"] {
2746 assert!(
2747 doc.find_node_by_id(id).is_some(),
2748 "missing id {id} in idcache"
2749 );
2750 }
2751 assert!(doc.find_node_by_id("nope").is_none());
2752 }
2753
2754 #[test]
2757 fn test_scan_collects_doclevel_pi_searchpaths() {
2758 let doc = make_test_doc(
2759 "<?latexml searchpaths=\"alpha,beta\"?>\
2760 <document xmlns='http://dlmf.nist.gov/LaTeXML'><section xml:id='s1'/></document>",
2761 );
2762 let paths = doc.get_search_paths();
2763 assert!(paths.iter().any(|p| p == "alpha"), "searchpaths: {paths:?}");
2764 assert!(paths.iter().any(|p| p == "beta"), "searchpaths: {paths:?}");
2765 }
2766
2767 #[test]
2768 fn test_uniquify_id() {
2769 let doc = make_test_doc(
2770 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2771 <p xml:id='p1'/>\
2772 </document>",
2773 );
2774 let mut doc = doc;
2775 let id1 = doc.uniquify_id("p1", None);
2777 let id2 = doc.uniquify_id("p1", None);
2779 assert_ne!(id1, id2);
2780 assert!(id1.starts_with("p1"));
2782 assert!(id2.starts_with("p1"));
2783 }
2784
2785 #[test]
2786 fn test_initial() {
2787 assert_eq!(PostDocument::initial("Hello", false), "H");
2788 assert_eq!(PostDocument::initial(" world", false), "W");
2789 assert_eq!(PostDocument::initial("123abc", true), "A");
2790 assert_eq!(PostDocument::initial("!@#", false), "*");
2791 assert_eq!(PostDocument::initial("\u{00E9}cole", false), "E"); }
2793
2794 #[test]
2795 fn test_add_class() {
2796 let doc = make_test_doc(
2797 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
2798 <p xml:id='p1'/>\
2799 </document>",
2800 );
2801 let mut node = doc.findnode("//ltx:p").unwrap();
2802 PostDocument::add_class(&mut node, "foo bar");
2803 let class = node.get_attribute("class").unwrap();
2804 assert!(class.contains("bar"));
2805 assert!(class.contains("foo"));
2806
2807 PostDocument::add_class(&mut node, "foo");
2809 let class = node.get_attribute("class").unwrap();
2810 let count = class.matches("foo").count();
2811 assert_eq!(count, 1);
2812 }
2813}