1use std::{collections::VecDeque, fmt, rc::Rc};
2
3use libxml::tree::Node;
4use rustc_hash::FxHashMap as HashMap;
5
6use crate::{
7 common::{arena, error::*},
8 document::{Document, get_node_qname},
9 state::Scope,
10 tokens::Tokens,
11};
12
13pub mod declare;
14
15pub type RewriteReplaceClosure = Rc<dyn Fn(&mut Document, Vec<&mut Node>) -> Result<()>>;
16pub type RewriteTestClosure = Rc<dyn Fn(&mut Document, &Node) -> Result<usize>>;
18pub type RewriteRegexpClosure = Rc<dyn Fn(&str) -> Option<String>>;
20
21#[derive(Debug, Clone)]
24pub struct MultiSelectEntry {
25 pub xpath: String,
26 pub nnodes: usize,
27 pub wilds: Vec<WildPath>,
28}
29
30#[derive(Clone, Default)]
34pub struct RewriteOptions {
35 pub label: Option<String>,
36 pub scope: Option<Scope>,
37 pub xpath: Option<String>,
38 pub on_match: Option<Tokens>,
39 pub attributes: Option<String>,
40 pub attributes_map: Option<HashMap<String, String>>,
41 pub replace: Option<RewriteReplaceClosure>,
42 pub regexp: Option<String>,
43 pub select: Option<String>,
44 pub select_count: Option<usize>,
45 pub is_math: bool,
46 pub wildcard_paths: Option<Vec<WildPath>>,
47 pub declare_filter: Option<declare::DeclarePattern>,
52}
53impl fmt::Debug for RewriteOptions {
54 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "<RewriteOptions>") }
55}
56impl PartialEq for RewriteOptions {
57 fn eq(&self, other: &RewriteOptions) -> bool { self.select == other.select }
58}
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum RewriteOperator {
61 Label,
63 Scope,
64 Xpath,
65 Match,
66 Regexp,
68 Attributes,
69 Action,
70 Replace,
71 Test,
72 MultiSelect,
73 Select,
74 Ignore,
75 Trace,
76}
77#[derive(Clone)]
78pub enum RewritePattern {
79 String(String),
80 Scope(Scope),
81 Tokens(Tokens),
82 Closure(RewriteReplaceClosure),
83 TestClosure(RewriteTestClosure),
85 RegexpClosure(RewriteRegexpClosure),
87 MultiSelectPatterns(Vec<MultiSelectEntry>),
89 NodeList(Vec<Node>),
91}
92impl fmt::Debug for RewritePattern {
93 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94 match self {
95 RewritePattern::String(x) => write!(f, "{x:?}"),
96 RewritePattern::Scope(x) => write!(f, "{x:?}"),
97 RewritePattern::Tokens(x) => write!(f, "{x:?}"),
98 RewritePattern::Closure(_) => write!(f, "<Rewrite Replacement Closure>"),
99 RewritePattern::TestClosure(_) => write!(f, "<Rewrite Test Closure>"),
100 RewritePattern::RegexpClosure(_) => write!(f, "<Rewrite Regexp Closure>"),
101 RewritePattern::MultiSelectPatterns(v) => write!(f, "<MultiSelect {} patterns>", v.len()),
102 RewritePattern::NodeList(v) => write!(f, "<NodeList {} nodes>", v.len()),
103 }
104 }
105}
106#[derive(Debug, Clone)]
107pub struct RewriteClause {
108 compiled: bool,
109 pub op: RewriteOperator,
110 pub pattern: RewritePattern,
111}
112impl RewriteClause {
113 pub fn new_uncompiled(op: RewriteOperator, pattern: RewritePattern) -> Self {
114 RewriteClause { compiled: false, op, pattern }
115 }
116
117 pub fn new_compiled(op: RewriteOperator, pattern: RewritePattern) -> Self {
118 RewriteClause { compiled: true, op, pattern }
119 }
120}
121
122#[derive(Debug, Clone, Default)]
123pub struct Rewrite {
124 pub options: RewriteOptions,
125 pub clauses: Vec<RewriteClause>,
126}
127impl PartialEq for Rewrite {
128 fn eq(&self, other: &Rewrite) -> bool { self.options == other.options }
129}
130
131impl Rewrite {
132 pub fn new(_kind: &str, mut options: RewriteOptions) -> Self {
133 use RewriteOperator::*;
134 let mut clauses = Vec::new();
135 if let Some(xpath) = options.select.take() {
137 clauses.push(RewriteClause {
138 compiled: true,
139 op: Select,
140 pattern: RewritePattern::String(xpath),
141 })
142 }
143 if let Some(label) = options.label.take() {
145 clauses.push(RewriteClause {
146 compiled: false,
147 op: Label,
148 pattern: RewritePattern::String(label),
149 });
150 }
151 if let Some(scope) = options.scope.take() {
152 let scope_str = match scope {
155 crate::state::Scope::Named(s) => arena::with(s, |r| r.to_string()),
156 crate::state::Scope::Global => String::from("global"),
157 crate::state::Scope::Local => String::from("local"),
158 crate::state::Scope::InPlace => String::from("inplace"),
161 };
162 clauses.push(RewriteClause {
163 compiled: false,
164 op: Scope,
165 pattern: RewritePattern::String(scope_str),
166 });
167 }
168 if let Some(xpath) = options.xpath.take() {
169 clauses.push(RewriteClause {
170 compiled: false,
171 op: Xpath,
172 pattern: RewritePattern::String(xpath),
173 });
174 }
175 if let Some(tokens) = options.on_match.take() {
176 clauses.push(RewriteClause {
177 compiled: false,
178 op: Match,
179 pattern: RewritePattern::Tokens(tokens),
180 });
181 }
182 if let Some(replace) = options.replace.take() {
183 clauses.push(RewriteClause {
184 compiled: false,
185 op: Replace,
186 pattern: RewritePattern::Closure(replace),
187 });
188 }
189 if let Some(r) = options.regexp.take() {
190 clauses.push(RewriteClause {
191 compiled: false,
192 op: Regexp,
193 pattern: RewritePattern::String(r),
194 });
195 }
196 if options.attributes_map.is_none()
199 && let Some(ref attrs_str) = options.attributes
200 {
201 let mut map = HashMap::default();
202 for part in attrs_str.split(',') {
203 let part = part.trim();
204 if let Some((key, val)) = part.split_once('=') {
205 let val = val.trim().trim_matches('\'').trim_matches('"');
206 map.insert(key.trim().to_string(), val.to_string());
207 }
208 }
209 if !map.is_empty() {
210 options.attributes_map = Some(map);
211 }
212 }
213 if options.attributes_map.is_some() {
214 clauses.push(RewriteClause {
215 compiled: true,
216 op: Attributes,
217 pattern: RewritePattern::String(String::new()), });
219 }
220 Rewrite { options, clauses }
221 }
222
223 pub fn compile_clauses(&mut self, document: &mut Document) {
224 let current_clauses: Vec<RewriteClause> = std::mem::take(&mut self.clauses);
225 let mut new_clauses: Vec<RewriteClause> = Vec::new();
226 for clause in current_clauses {
227 if !clause.compiled {
228 new_clauses.push(self.compile_clause(document, clause));
229 } else {
230 new_clauses.push(clause);
231 }
232 }
233 self.clauses = new_clauses;
234 }
235
236 pub fn compile_clause(
237 &mut self,
238 document: &mut Document,
239 clause: RewriteClause,
240 ) -> RewriteClause {
241 let op = clause.op;
242 let pattern = clause.pattern;
243
244 if op == RewriteOperator::Xpath {
245 if self.options.select_count.is_none() {
246 self.options.select_count = Some(1);
247 }
248 return RewriteClause {
249 compiled: true,
250 op: RewriteOperator::Select,
251 pattern,
252 };
253 }
254 if op == RewriteOperator::Label
262 && let RewritePattern::String(label_str) = &pattern
263 {
264 let as_scope = RewriteClause {
265 compiled: false,
266 op: RewriteOperator::Scope,
267 pattern: RewritePattern::String(format!("label:{label_str}")),
268 };
269 return self.compile_clause(document, as_scope);
270 }
271 if op == RewriteOperator::Scope
274 && let RewritePattern::String(scope_str) = &pattern
275 {
276 let whole_fragment_scope = |document: &Document| RewriteClause {
281 compiled: true,
282 op: RewriteOperator::Select,
283 pattern: RewritePattern::NodeList(
284 document
285 .get_document()
286 .get_root_element()
287 .into_iter()
288 .collect(),
289 ),
290 };
291 if let Some(label_part) = scope_str.strip_prefix("label:") {
292 if let Some(id) = document.lookup_rewrite_label(label_part) {
293 if self.options.select_count.is_none() {
294 self.options.select_count = Some(1);
295 }
296 if document.fragment_ancestor_ids.contains(&id) {
297 return whole_fragment_scope(document);
298 }
299 let xpath = format!("descendant-or-self::*[@xml:id='{}']", id);
300 return RewriteClause {
301 compiled: true,
302 op: RewriteOperator::Select,
303 pattern: RewritePattern::String(xpath),
304 };
305 }
306 let clean_key = format!("LABEL:{}", label_part);
308 if let Some(id) = document.lookup_rewrite_label(&clean_key) {
309 if self.options.select_count.is_none() {
310 self.options.select_count = Some(1);
311 }
312 if document.fragment_ancestor_ids.contains(&id) {
313 return whole_fragment_scope(document);
314 }
315 let xpath = format!("descendant-or-self::*[@xml:id='{}']", id);
316 return RewriteClause {
317 compiled: true,
318 op: RewriteOperator::Select,
319 pattern: RewritePattern::String(xpath),
320 };
321 }
322 if document.scoped_rules_strict {
327 return RewriteClause {
328 compiled: true,
329 op: RewriteOperator::Select,
330 pattern: RewritePattern::NodeList(Vec::new()),
331 };
332 }
333 return RewriteClause {
334 compiled: true,
335 op: RewriteOperator::Ignore,
336 pattern: RewritePattern::String(String::new()),
337 };
338 } else if let Some(id_part) = scope_str.strip_prefix("id:") {
339 if self.options.select_count.is_none() {
340 self.options.select_count = Some(1);
341 }
342 let target_id = id_part.to_string();
345 if document.fragment_ancestor_ids.contains(&target_id) {
346 return whole_fragment_scope(document);
347 }
348 let scope_nodes: Vec<Node> = document
349 .findnodes("descendant-or-self::*", None)
350 .into_iter()
351 .filter(|n| {
352 n.get_property("id").as_deref() == Some(&target_id)
353 || n.get_attribute("xml:id").as_deref() == Some(&target_id)
354 })
355 .collect();
356 if !scope_nodes.is_empty() {
357 return RewriteClause {
359 compiled: true,
360 op: RewriteOperator::Select,
361 pattern: RewritePattern::NodeList(scope_nodes),
362 };
363 }
364 if document.scoped_rules_strict {
366 return RewriteClause {
367 compiled: true,
368 op: RewriteOperator::Select,
369 pattern: RewritePattern::NodeList(Vec::new()),
370 };
371 }
372 return RewriteClause {
373 compiled: true,
374 op: RewriteOperator::Ignore,
375 pattern: RewritePattern::String(String::new()),
376 };
377 }
378 return RewriteClause {
379 compiled: true,
380 op: RewriteOperator::Ignore,
381 pattern: RewritePattern::String(String::new()),
382 };
383 }
384 if op == RewriteOperator::Match {
390 match pattern {
391 RewritePattern::String(xpath) => {
392 if self.options.select_count.is_none() {
394 self.options.select_count = Some(1);
395 }
396 return RewriteClause {
397 compiled: true,
398 op: RewriteOperator::Select,
399 pattern: RewritePattern::String(xpath),
400 };
401 },
402 RewritePattern::TestClosure(_) => {
403 return RewriteClause {
405 compiled: true,
406 op: RewriteOperator::Test,
407 pattern,
408 };
409 },
410 RewritePattern::MultiSelectPatterns(_) => {
411 return RewriteClause {
413 compiled: true,
414 op: RewriteOperator::MultiSelect,
415 pattern,
416 };
417 },
418 _ => {},
419 }
420 }
421 RewriteClause { compiled: true, op, pattern }
422 }
423
424 pub fn invoke(&mut self, document: &mut Document, root: &Node) -> Result<()> {
425 let clauses = self.clauses.iter().collect();
426 self.apply_clause(document, root, 0, clauses)?;
427 Ok(())
428 }
429 fn apply_clause(
451 &self,
452 document: &mut Document,
453 tree: &Node,
454 nmatched: usize,
455 mut clauses: VecDeque<&RewriteClause>,
456 ) -> Result<()> {
457 use RewriteOperator::*;
458 if let Some(RewriteClause { compiled: _, op, pattern }) = clauses.pop_front() {
459 match op {
460 Select => {
461 if let RewritePattern::NodeList(nodes) = pattern {
463 for node in nodes {
464 self.apply_clause(document, node, 1, clauses.clone())?;
465 }
466 return Ok(());
467 }
468 if let RewritePattern::String(xpath) = pattern {
469 let mut matches = document.findnodes(xpath, Some(tree));
473 if matches.is_empty() && !xpath.contains("xml:id") && !xpath.contains("@id=") {
474 let all = document.findnodes(xpath, None);
475 if !all.is_empty() {
476 let tree_ptr = tree.node_ptr();
477 matches = all
478 .into_iter()
479 .filter(|n| {
480 let mut cur = n.get_parent();
481 while let Some(p) = cur {
482 if std::ptr::eq(p.node_ptr(), tree_ptr) {
483 return true;
484 }
485 cur = p.get_parent();
486 }
487 false
488 })
489 .collect();
490 }
491 }
492 let is_content_select = !xpath.contains("xml:id") && !xpath.contains("@id=");
494 let wilds = if is_content_select {
495 self.options.wildcard_paths.clone()
496 } else {
497 None
498 };
499 let pattern_filter = if is_content_select {
503 self.options.declare_filter.as_ref()
504 } else {
505 None
506 };
507 for node in matches {
508 if node.has_attribute("_matched") {
509 continue;
510 }
511 if let Some(pat) = pattern_filter
512 && !declare::declare_node_matches(document, &node, pat)
513 {
514 continue;
515 }
516 let marked = if let Some(ref wpaths) = wilds {
517 mark_wildcards(&node, wpaths)
518 } else {
519 vec![]
520 };
521 let nmatched_for_clause = if is_content_select {
523 self.options.select_count.unwrap_or(1)
524 } else {
525 1
526 };
527 self.apply_clause(document, &node, nmatched_for_clause, clauses.clone())?;
528 if !marked.is_empty() {
529 unmark_wildcards(&marked);
530 }
531 }
532 }
533 },
534 Replace => {
535 let Some(mut parent) = tree.get_parent() else {
543 return Ok(());
544 };
545 let mut following = VecDeque::new(); while let Some(mut sib) = parent.get_last_child() {
548 sib.unbind_node();
549 if *tree == sib {
550 following.push_front(sib);
551 break;
552 } else {
553 following.push_front(sib);
554 }
555 }
556 let mut replaced = Vec::new();
557 for _idx in 0..nmatched {
558 match following.pop_front() {
560 Some(popped) => {
561 replaced.push(popped);
562 },
563 _ => {
564 break; },
566 }
567 }
568 for rnode in replaced.iter() {
569 document.unrecord_node_ids(rnode);
570 }
571 document.set_node(&parent);
573 let point_opt = parent.get_last_child();
574 if let RewritePattern::Closure(closure) = pattern {
575 closure(document, replaced.iter_mut().collect())?; }
577
578 let inserted = if let Some(point) = point_opt {
580 let mut ins_queue = VecDeque::new();
581 let mut sibs = parent.get_child_nodes();
582 while let Some(sib) = sibs.pop() {
583 if sib == point {
584 break;
585 }
586 ins_queue.push_front(sib);
587 }
588 ins_queue.into_iter().collect::<Vec<Node>>()
589 } else {
590 parent.get_child_nodes()
591 };
592
593 for ins in inserted.iter() {
595 document.record_node_ids(ins)?;
596 }
597 let font = document.get_node_font(tree).clone();
599 for ins in inserted.iter() {
601 document.merge_node_font_rec(ins, &font)?;
603 }
604 for mut follow_node in following {
606 parent.add_child(&mut follow_node)?;
607 }
608 },
609 Attributes => {
610 if let Some(ref attrs) = self.options.attributes_map {
611 let has_wc = tree.has_attribute("_has_wildcards");
612 if has_wc {
613 let mut nodes = vec![tree.clone()];
615 let mut cur = tree.clone();
617 for _ in 1..nmatched {
618 match cur.get_next_sibling() {
619 Some(sib) => {
620 cur = sib.clone();
621 nodes.push(sib);
622 },
623 _ => {
624 break;
625 },
626 }
627 }
628 set_attributes_wild(document, attrs, nodes, nmatched)?;
629 } else if nmatched > 1 {
630 let mut nodes = vec![tree.clone()];
632 let mut cur = tree.clone();
633 for _ in 1..nmatched {
634 while let Some(sib) = cur.get_next_sibling() {
635 cur = sib.clone();
636 if sib.get_type() == Some(libxml::tree::NodeType::ElementNode) {
637 nodes.push(sib);
638 break;
639 }
640 }
641 }
642 if nodes.iter().any(|n| !n.has_attribute("_matched"))
644 && let Ok(Some(mut wrapper)) = document.wrap_nodes("ltx:XMWrap", nodes)
645 {
646 for (key, value) in attrs {
647 if !key.starts_with('_') {
648 let _ = wrapper.set_attribute(key, value);
649 }
650 }
651 let _ = wrapper.set_attribute("_rewrite", "1");
652 }
653 } else if !tree.has_attribute("_matched") {
654 let mut node = tree.clone();
656 for (key, value) in attrs {
657 if !key.starts_with('_') {
658 let _ = node.set_attribute(key, value);
659 }
660 }
661 if node.get_name() == "XMApp" && attrs.contains_key("role") {
662 let _ = node.set_attribute("_rewrite", "1");
663 }
664 }
665 }
666 mark_seen(tree, nmatched);
667 self.apply_clause(document, tree, nmatched, clauses)?;
668 },
669 Ignore => {
670 self.apply_clause(document, tree, nmatched, clauses)?;
672 },
673 Regexp => {
674 if let RewritePattern::RegexpClosure(closure) = pattern {
677 let text_nodes = document.findnodes("descendant-or-self::text()", Some(tree));
678 for mut text_node in text_nodes {
679 let content = text_node.get_content();
680 if let Some(modified) = closure(&content) {
681 let _ = text_node.set_content(&modified);
682 }
683 }
684 } else if let RewritePattern::String(regex_str) = pattern {
685 let re =
687 regex::Regex::new(regex_str).unwrap_or_else(|_| regex::Regex::new("$^").unwrap());
688 let text_nodes = document.findnodes("descendant-or-self::text()", Some(tree));
689 for mut text_node in text_nodes {
690 let content = text_node.get_content();
691 let result = re.replace_all(&content, "");
692 if result != content {
693 let _ = text_node.set_content(&result);
694 }
695 }
696 }
697 },
698 Label => {
699 self.apply_clause(document, tree, nmatched, clauses)?;
703 },
704 Trace => {
705 self.apply_clause(document, tree, nmatched, clauses)?;
707 },
708 Action => {
709 if let RewritePattern::Closure(closure) = pattern {
712 let mut node = tree.clone();
713 closure(document, vec![&mut node])?;
714 }
715 self.apply_clause(document, tree, nmatched, clauses)?;
717 },
718 Test => {
719 if let RewritePattern::TestClosure(closure) = pattern {
723 let nnodes = closure(document, tree)?;
724 if nnodes > 0 {
725 self.apply_clause(document, tree, nnodes, clauses)?;
726 }
727 } else if let RewritePattern::Closure(closure) = pattern {
728 let mut node = tree.clone();
730 closure(document, vec![&mut node])?;
731 self.apply_clause(document, tree, nmatched, clauses)?;
732 }
733 },
734 MultiSelect => {
735 if let RewritePattern::MultiSelectPatterns(entries) = pattern {
743 for entry in entries {
744 let matches = document.findnodes(&entry.xpath, Some(tree));
745 for node in matches {
746 if node.has_attribute("_matched") {
747 continue;
748 }
749 let marked = if !entry.wilds.is_empty() {
750 mark_wildcards(&node, &entry.wilds)
751 } else {
752 vec![]
753 };
754 self.apply_clause(document, &node, entry.nnodes, clauses.clone())?;
755 if !marked.is_empty() {
756 unmark_wildcards(&marked);
757 }
758 }
759 }
760 } else if let RewritePattern::String(xpath) = pattern {
761 let count = self.options.select_count.unwrap_or(1);
763 let matches = document.findnodes(xpath, Some(tree));
764 for node in matches {
765 if node.has_attribute("_matched") {
766 continue;
767 }
768 self.apply_clause(document, &node, count, clauses.clone())?;
769 }
770 }
771 },
772 _ => {
773 self.apply_clause(document, tree, nmatched, clauses)?;
775 },
776 }
777 } else {
778 mark_seen(tree, nmatched);
781 }
782
783 Ok(())
784 }
785}
786
787pub type WildPath = Vec<usize>;
794
795pub type CompiledMatch = (String, usize, Vec<WildPath>);
797
798pub fn dom_to_xpath(document: &Document, node: &Node) -> CompiledMatch {
801 let (xpath, nnodes, _nwilds, wilds) =
802 dom_to_xpath_rec(document, node, "descendant-or-self", None);
803 (xpath, nnodes, wilds)
804}
805
806fn is_excluded_match_attr(key: &str) -> bool {
808 matches!(
809 key,
810 "scriptpos" | "mathstyle" | "xml:id" | "fontsize" | "_font" | "_pvis" | "_cvis"
811 ) || key.starts_with('_')
812}
813
814fn dom_to_xpath_rec(
817 document: &Document,
818 node: &Node,
819 axis: &str,
820 pos: Option<usize>,
821) -> (String, usize, usize, Vec<WildPath>) {
822 let node_type = node.get_type();
823 if node_type == Some(libxml::tree::NodeType::DocumentFragNode) {
825 let children = node.get_child_nodes();
826 let (xpath, nnodes, wilds) = dom_to_xpath_seq(document, axis, pos, &children);
827 return (xpath, nnodes, 0, wilds);
828 }
829 if node_type == Some(libxml::tree::NodeType::ElementNode) {
830 let qname = arena::with(get_node_qname(node), |s| s.to_string());
831 let children = node.get_child_nodes();
832
833 if qname == "_WildCard_" {
835 if !children.is_empty() {
836 let child_list = node.get_child_nodes();
838 let (xpath, _nnodes, _nwilds, _wilds) =
840 dom_to_xpath_rec(document, &child_list[0], axis, pos);
841 let n = children.len().max(1);
842 return (xpath, n, n, vec![]);
843 } else {
844 return (format!("{axis}::*"), 1, 1, vec![]);
845 }
846 }
847 if qname == "ltx:XMRef"
849 && let Some(idref) = node.get_property("idref")
850 && let Some(target) = document.lookup_id(&idref).cloned()
851 {
852 let tqname = arena::with(get_node_qname(&target), |s| s.to_string());
853 let is_wild = if tqname.ends_with("XMArg") || tqname.ends_with("XMWrap") {
855 let tc = target.get_child_nodes();
856 tc.len() == 1 && arena::with(get_node_qname(&tc[0]), |s| s == "_WildCard_")
857 } else {
858 tqname == "_WildCard_"
859 };
860 if is_wild {
861 return (format!("{axis}::*"), 1, 1, vec![]);
862 }
863 }
864 if (qname.ends_with("XMArg") || qname.ends_with("XMWrap"))
866 && children.len() == 1
867 && arena::with(get_node_qname(&children[0]), |s| s.to_string()) == "_WildCard_"
868 {
869 let wc_children = children[0].get_child_nodes();
870 if !wc_children.is_empty() {
871 let (child_xpath, _nn, _nw, _w) =
872 dom_to_xpath_rec(document, &wc_children[0], "child", Some(1));
873 let mut preds = vec![];
874 if let Some(p) = pos {
875 preds.push(format!("position()={p}"));
876 }
877 preds.push(child_xpath);
878 return (
879 format!("{axis}::{qname}[{}]", preds.join(" and ")),
880 1,
881 1,
882 vec![],
883 );
884 } else {
885 return (format!("{axis}::*"), 1, 1, vec![]);
886 }
887 }
888
889 let mut predicates = Vec::new();
891 let mut wilds = Vec::new();
892
893 let attrs = node.get_attributes();
895 for (key, value) in &attrs {
896 if !is_excluded_match_attr(key) {
897 predicates.push(format!("@{key}='{}'", value.replace('\'', "'")));
898 }
899 }
900 if !children.is_empty() {
902 let all_text = children
903 .iter()
904 .all(|c| c.get_type() == Some(libxml::tree::NodeType::TextNode));
905 let all_elem = children
906 .iter()
907 .all(|c| c.get_type() == Some(libxml::tree::NodeType::ElementNode));
908 if all_text {
909 let text = node.get_content();
910 predicates.push(format!("text()='{}'", text.replace('\'', "'")));
911 } else if all_elem {
912 let (xp, _nn, w) = dom_to_xpath_seq(document, "child", Some(1), &children);
913 predicates.push(xp);
914 wilds.extend(w);
915 }
916 }
918
919 let tag = if let Some(p) = pos {
921 predicates.insert(0, format!("self::{qname}"));
922 predicates.insert(0, format!("position()={p}"));
923 "*".to_string()
924 } else {
925 qname
926 };
927 let preds = predicates.join(" and ");
928 let xpath = if preds.is_empty() {
929 format!("{axis}::{tag}")
930 } else {
931 format!("{axis}::{tag}[{preds}]")
932 };
933 return (xpath, 1, 0, wilds);
934 }
935 if node_type == Some(libxml::tree::NodeType::TextNode) {
936 let text = node.get_content();
937 return (
938 format!("*[text()='{}']", text.replace('\'', "'")),
939 1,
940 0,
941 vec![],
942 );
943 }
944 (String::new(), 0, 0, vec![])
945}
946
947fn dom_to_xpath_seq(
950 document: &Document,
951 axis: &str,
952 pos: Option<usize>,
953 nodes: &[Node],
954) -> (String, usize, Vec<WildPath>) {
955 if nodes.is_empty() {
956 return (String::new(), 0, vec![]);
957 }
958 let mut i: usize = 1;
959 let mut sib_xpaths = Vec::new();
960 let mut wilds = Vec::new();
961
962 let (xpath, _nn, nwilds, w0) = dom_to_xpath_rec(document, &nodes[0], axis, pos);
964 if nwilds > 0 {
965 for _ in 0..nwilds {
966 wilds.push(vec![i]);
967 i += 1;
968 }
969 } else {
970 for w in &w0 {
971 let mut path = vec![1usize];
972 path.extend(w);
973 wilds.push(path);
974 }
975 i += 1;
976 }
977 for sib in &nodes[1..] {
979 let (xp, _nn, nw, w) = dom_to_xpath_rec(document, sib, "following-sibling", Some(i - 1));
980 sib_xpaths.push(xp);
981 if nw > 0 {
982 for _ in 0..nw {
983 wilds.push(vec![i]);
984 i += 1;
985 }
986 } else {
987 for ww in &w {
988 let mut path = vec![i];
989 path.extend(ww);
990 wilds.push(path);
991 }
992 i += 1;
993 }
994 }
995 let mut result = xpath;
996 for sp in &sib_xpaths {
997 result = format!("{result}[{sp}]");
998 }
999 (result, i - 1, wilds)
1000}
1001
1002fn nth_sibling(node: &Node, n: usize) -> Option<Node> {
1004 let mut current = Some(node.clone());
1005 for _ in 1..n {
1006 current = current.and_then(|n| {
1007 let mut next = n.get_next_sibling();
1008 while let Some(ref s) = next {
1010 if s.get_type() == Some(libxml::tree::NodeType::ElementNode) {
1011 break;
1012 }
1013 next = s.get_next_sibling();
1014 }
1015 next
1016 });
1017 }
1018 current
1019}
1020
1021fn nth_child(node: &Node, n: usize) -> Option<Node> {
1023 node.get_child_nodes().into_iter().nth(n - 1)
1024}
1025
1026pub fn mark_wildcards(node: &Node, wilds: &[WildPath]) -> Vec<Node> {
1029 if wilds.is_empty() {
1030 return vec![];
1031 }
1032 let mut n = node.clone();
1033 let _ = n.set_attribute("_has_wildcards", "1");
1034 let mut marked = Vec::new();
1035 for wild in wilds {
1036 let mut current = Some(node.clone());
1037 let mut first = true;
1038 for &idx in wild {
1039 if current.is_none() {
1040 break;
1041 }
1042 current = if first {
1043 first = false;
1044 nth_sibling(current.as_ref().unwrap(), idx)
1045 } else {
1046 nth_child(current.as_ref().unwrap(), idx)
1047 };
1048 }
1049 if let Some(ref c) = current
1050 && c.get_type() == Some(libxml::tree::NodeType::ElementNode)
1051 {
1052 let mut mc = c.clone();
1053 let _ = mc.set_attribute("_wildcard", "1");
1054 marked.push(mc);
1055 }
1056 }
1057 marked
1058}
1059
1060pub fn unmark_wildcards(nodes: &[Node]) {
1062 for n in nodes {
1063 if n.get_type() == Some(libxml::tree::NodeType::ElementNode) {
1064 let mut mc = n.clone();
1065 let _ = mc.remove_attribute("_has_wildcards");
1066 let _ = mc.remove_attribute("_wildcard");
1067 }
1068 }
1069}
1070
1071pub fn set_wildcard_ids(document: &mut Document, node: &Node) -> Vec<String> {
1078 if node.get_type() != Some(libxml::tree::NodeType::ElementNode) {
1079 return vec![];
1080 }
1081 if node.has_attribute("_matched") {
1082 return vec![];
1083 }
1084 if node.has_attribute("_wildcard") {
1085 let id = if let Some(existing) = node
1089 .get_property("xml:id")
1090 .or_else(|| node.get_property("id"))
1091 {
1092 existing
1093 } else {
1094 let mut n = node.clone();
1096 let _ = document.generate_id(&mut n, "");
1097 node
1098 .get_property("xml:id")
1099 .or_else(|| node.get_property("id"))
1100 .unwrap_or_default()
1101 };
1102 return vec![id];
1103 }
1104 let mut ids = Vec::new();
1106 for child in node.get_child_nodes() {
1107 ids.extend(set_wildcard_ids(document, &child));
1108 }
1109 ids
1110}
1111
1112pub fn set_attributes_wild(
1128 document: &mut Document,
1129 attrs: &HashMap<String, String>,
1130 nodes: Vec<Node>,
1131 _nmatched: usize,
1132) -> Result<()> {
1133 if nodes.iter().all(|n| n.has_attribute("_matched")) {
1135 return Ok(());
1136 }
1137 let nowrap = attrs.contains_key("_nowrap");
1138 if nowrap || (nodes.len() == 1 && nodes[0].get_name() == "XMDual") {
1140 if let Some(nonwild) = nodes.iter().find(|n| !n.has_attribute("_wildcard")) {
1141 let mut n = nonwild.clone();
1142 for (key, value) in attrs {
1143 if !key.starts_with('_') {
1144 let _ = n.set_attribute(key, value);
1145 }
1146 }
1147 }
1148 return Ok(());
1149 }
1150
1151 let Some(wrap_node) = document.wrap_nodes("ltx:XMWrap", nodes)? else {
1159 return Ok(());
1160 };
1161 let wild_ids = set_wildcard_ids(document, &wrap_node);
1162 let Some(mut dual_node) = document.wrap_nodes("ltx:XMDual", vec![wrap_node.clone()])? else {
1163 return Ok(());
1164 };
1165
1166 if let Some(role) = attrs.get("role") {
1168 let _ = dual_node.set_attribute("role", role);
1169 }
1170
1171 let doc = document.get_document();
1173 let mut content_app = Node::new("XMApp", None, doc)?;
1174 let mut content_op = Node::new("XMTok", None, doc)?;
1175 for (key, value) in attrs {
1176 if key != "role" && !key.starts_with('_') {
1177 let _ = content_op.set_attribute(key, value);
1178 }
1179 }
1180 content_app.add_child(&mut content_op)?;
1181 for rid in &wild_ids {
1182 let mut xmref = Node::new("XMRef", None, doc)?;
1183 let _ = xmref.set_attribute("idref", rid);
1184 content_app.add_child(&mut xmref)?;
1185 }
1186
1187 let mut wrap_mut = wrap_node;
1190 wrap_mut.add_prev_sibling(&mut content_app)?;
1191 Ok(())
1192}
1193
1194fn mark_seen(node: &Node, nsibs: usize) {
1197 let mut current = Some(node.clone());
1198 for _i in 0..nsibs {
1199 if let Some(n) = current {
1200 mark_seen_rec(&n);
1201 current = n.get_next_sibling();
1202 } else {
1203 break;
1204 }
1205 }
1206}
1207
1208fn mark_seen_rec(node: &Node) {
1209 if node.has_attribute("_wildcard") {
1210 return;
1211 }
1212 let mut n = node.clone();
1213 let _ = n.set_attribute("_matched", "1");
1214 for child in node.get_child_nodes() {
1215 if child.get_type() == Some(libxml::tree::NodeType::ElementNode) {
1216 mark_seen_rec(&child);
1217 }
1218 }
1219}
1220
1221#[cfg(test)]
1222mod tests {
1223 use super::*;
1224 use crate::document::Document;
1225
1226 #[test]
1238 fn label_clause_compiles_to_a_select_on_the_labeled_id() {
1239 let mut document = Document::new();
1240 document
1241 .rewrite_labels
1242 .insert("LABEL:eq.one".to_string(), "S1.E2".to_string());
1243
1244 let mut rule = Rewrite::new("text", RewriteOptions {
1245 label: Some("eq.one".to_string()),
1246 ..RewriteOptions::default()
1247 });
1248 assert!(
1249 matches!(
1250 rule.clauses.first().map(|c| c.op),
1251 Some(RewriteOperator::Label)
1252 ),
1253 "a label option must start life as a Label clause"
1254 );
1255 rule.compile_clauses(&mut document);
1256
1257 match rule.clauses.first() {
1258 Some(RewriteClause {
1259 op: RewriteOperator::Select,
1260 pattern: RewritePattern::String(xpath),
1261 ..
1262 }) => assert_eq!(xpath, "descendant-or-self::*[@xml:id='S1.E2']"),
1263 other => panic!("label must lower to a Select on the labeled id, got {other:?}"),
1264 }
1265 assert_eq!(rule.options.select_count, Some(1));
1267 }
1268
1269 #[test]
1274 fn unknown_label_falls_through_to_ignore() {
1275 let mut document = Document::new();
1276 let mut rule = Rewrite::new("text", RewriteOptions {
1277 label: Some("nope".to_string()),
1278 ..RewriteOptions::default()
1279 });
1280 rule.compile_clauses(&mut document);
1281 assert!(
1282 matches!(
1283 rule.clauses.first().map(|c| c.op),
1284 Some(RewriteOperator::Ignore)
1285 ),
1286 "an unresolvable label must not silently restrict the rule"
1287 );
1288 }
1289}