1# /=====================================================================\ #
2# |  LaTeXML::Core::Rewrite                                             | #
3# | Rewrite Rules that modify the Constructed Document                  | #
4# |=====================================================================| #
5# | Part of LaTeXML:                                                    | #
6# |  Public domain software, produced as part of work done by the       | #
7# |  United States Government & not subject to copyright in the US.     | #
8# |---------------------------------------------------------------------| #
9# | Bruce Miller <bruce.miller@nist.gov>                        #_#     | #
10# | http://dlmf.nist.gov/LaTeXML/                              (o o)    | #
11# \=========================================================ooo==U==ooo=/ #
12package LaTeXML::Core::Rewrite;
13use strict;
14use warnings;
15use LaTeXML::Global;
16use LaTeXML::Common::Object;
17use LaTeXML::Common::Error;
18use LaTeXML::Common::XML;
19
20sub new {
21  my ($class, $mode, @specs) = @_;
22  my @clauses = ();
23  while (@specs) {
24    my ($op, $pattern) = (shift(@specs), shift(@specs));
25    push(@clauses, ['uncompiled', $op, $pattern]); }
26  return bless {
27    mode => $mode, math => ($mode eq 'math'), clauses => [@clauses], labels => {}
28  }, $class; }
29
30sub clauses {
31  my ($self) = @_;
32  return @{ $$self{clauses} }; }
33
34DebuggableFeature('rewrite', 'Debug rewritting operations (LaTeXML::Core::Rewrite)');
35
36sub rewrite {
37  my ($self, $document, $node) = @_;
38  foreach my $node ($document->findnodes('//*[@labels]')) {
39    my $labels = $node->getAttribute('labels');
40    if (my $id = $node->getAttribute('xml:id')) {
41      foreach my $label (split(/ /, $labels)) {
42        $$self{labels}{$label} = $id; } }
43    else {
44      Error('malformed', 'label', $node, "Node has labels but no xml:id"); } }
45  Debug(('=' x 40)) if $LaTeXML::DEBUG{rewrite};
46  $self->applyClause($document, $node, 0, $self->clauses);
47  return; }
48
49sub getLabelID {
50  my ($self, $label) = @_;
51  if (my $id = $$self{labels}{ LaTeXML::Package::CleanLabel($label) }) {
52    return $id; }
53  else {
54    Error('misdefined', '<rewrite>', undef, "No id for label $label in Rewrite");
55    return; } }
56
57# Rewrite spec as input
58#   scope  => $scope  : a scope like "section:1.2.3" or "label:eq.one"; translated to xpath
59#   select => $xpath  : selects subtrees based on xpath expression.
60#   match  => $code   : called on $document and current $node: tests current node, returns $nnodes, if match
61#   match  => $string : Treats as TeX, converts Box, then DOM tree, to xpath
62#                      (The matching top-level nodes will be replaced, if replace is the next op.)
63#   replace=> $code   : removes the current $nnodes, calls $code with $document and removed nodes
64#   replace=> $string : removes $nnodes
65#                       Treats $string as TeX, converts to Box and inserts to replace
66#                       the removed nodes.
67#   attributes=>$hash : adds data from hash as attributes to the current node.
68#   regexp  => $string: apply regexp (subst) to all text nodes in/under the current node.
69
70# Compiled rewrite spec:
71#   select => $xpath  : operate on nodes selected by $xpath.
72#   test   => $code   : Calls $code on $document and current $node.
73#                       Returns number of nodes matched.
74#   replace=> $code   : removes the current $nnodes, calls $code on them.
75#   action => $code   : invoke $code on current $node, without removing them.
76#   regexp  => $string: apply regexp (subst) to all text nodes in/under the current node.
77
78sub applyClause {
79  my ($self, $document, $tree, $nmatched, $clause, @more_clauses) = @_;
80  if (!$clause) {
81    markSeen($tree, $nmatched);
82    return; }
83  return unless $clause;
84  if ($$clause[0] eq 'uncompiled') {
85    $self->compileClause($document, $clause); }
86  my ($ignore, $op, $pattern) = @$clause;
87  if ($op eq 'trace') {
88    local $LaTeXML::DEBUG{rewrite} = 1;
89    $self->applyClause($document, $tree, $nmatched, @more_clauses); }
90  elsif ($op eq 'ignore') {
91    $self->applyClause($document, $tree, $nmatched, @more_clauses); }
92  elsif ($op eq 'select') {
93    my ($xpath, $nnodes, @wilds) = @$pattern;
94    my @matches = $document->findnodes($xpath, $tree);
95    Debug("Rewrite selecting \"$xpath\" => " . scalar(@matches) . " matches")
96      if $LaTeXML::DEBUG{rewrite};
97    foreach my $node (@matches) {
98      next unless $node->ownerDocument->isSameNode($tree->ownerDocument); # If still attached to original document!
99      next if $node->getAttribute('_matched');
100      my @w = markWildcards($node, @wilds);
101      $self->applyClause($document, $node, $nnodes, @more_clauses);
102      unmarkWildcards($node, @w); } }
103  elsif ($op eq 'multi_select') {
104    foreach my $subpattern (@$pattern) {
105      my ($xpath, $nnodes, @wilds) = @$subpattern;
106      my @matches = $document->findnodes($xpath, $tree);
107      Debug("Rewrite selecting \"$xpath\" => " . scalar(@matches) . " matches")
108        if $LaTeXML::DEBUG{rewrite};
109      foreach my $node (@matches) {
110        next unless $node->ownerDocument->isSameNode($tree->ownerDocument); # If still attached to original document!
111        my @w = markWildcards($node, @wilds);
112        $self->applyClause($document, $node, $nnodes, @more_clauses);
113        unmarkWildcards($node, @w); } } }
114  elsif ($op eq 'test') {
115    my $nnodes = &$pattern($document, $tree);
116    Debug("Rewrite test at " . $tree->toString . ": " . ($nnodes ? $nnodes . " to replace" : "failed"))
117      if $LaTeXML::DEBUG{rewrite};
118    $self->applyClause($document, $tree, $nnodes, @more_clauses) if $nnodes; }
119  elsif ($op eq 'replace') {
120    Debug("Rewrite replace at " . $tree->toString . " using $pattern")
121      if $LaTeXML::DEBUG{rewrite};
122    my $parent = $tree->parentNode;
123    # Remove & separate nodes to be replaced, and sibling nodes following them.
124    my @following = ();    # Collect the matching and following nodes
125    while (my $sib = $parent->lastChild) {
126      $parent->removeChild($sib);
127      unshift(@following, $sib);
128      last if $tree->isSameNode($sib); }
129    my @replaced = map { shift(@following) } 1 .. $nmatched;    # Remove the nodes to be replaced
130    map { $document->unRecordNodeIDs($_) } @replaced;
131    # Carry out the operation, inserting whatever nodes.
132    $document->setNode($parent);
133    my $point = $parent->lastChild;
134    &$pattern($document, @replaced);                            # Carry out the insertion.
135
136    # Now collect the newly inserted nodes for any needed patching
137    my @inserted = ();                                          # Collect the newly added nodes.
138    if ($point) {
139      my @sibs = $parent->childNodes;
140      while (my $sib = pop(@sibs)) {
141        last if $$sib == $$point;
142        unshift(@inserted, $sib); } }
143    else {
144      @inserted = $parent->childNodes; }
145
146    # Now make any adjustments to the new nodes
147    map { $document->recordNodeIDs($_) } @inserted;
148    my $font = $document->getNodeFont($tree);   # the font of the matched node
149    foreach my $ins (@inserted) {               # Copy the non-semantic parts of font to the replacement
150      $document->mergeNodeFontRec($ins => $font); }
151    # Now, replace the following nodes.
152    map { $parent->appendChild($_) } @following; }
153  elsif ($op eq 'action') {
154    Debug("Rewrite action at " . $tree->toString . " using $pattern")
155      if $LaTeXML::DEBUG{rewrite};
156    &$pattern($tree); }
157  elsif ($op eq 'attributes') {
158    my @nodes = ();
159    my $n     = $tree;
160    for (my $i = 0 ; $n && ($i < $nmatched) ; $i++) {
161      push(@nodes, $n);
162      $n = $n->nextSibling; }
163    if ($tree->hasAttribute('_has_wildcards')) {
164      setAttributes_wild($document, $pattern, @nodes); }
165    else {
166      setAttributes_encapsulate($document, $pattern, @nodes); }
167
168    Debug("Rewrite attributes (deep $nmatched) " . join(',', sort keys %$pattern) . " for " . Stringify($tree))
169      if $LaTeXML::DEBUG{rewrite};
170    $self->applyClause($document, $tree, $nmatched, @more_clauses); }
171  elsif ($op eq 'regexp') {
172    my @matches = $document->findnodes('descendant-or-self::text()', $tree);
173    Debug("Rewrite regexp => " . scalar(@matches) . " matches")
174      if $LaTeXML::DEBUG{rewrite};
175    foreach my $text (@matches) {
176      my $string = $text->textContent;
177      if (&$pattern($string)) {
178        $text->setData($string); } } }
179  else {
180    Error('misdefined', '<rewrite>', undef, "Unknown directive '$op' in Compiled Rewrite spec"); }
181  return; }
182
183# Set attributes for an encapsulated tree (ie. a decorated symbol as symbol itself)
184sub setAttributes_encapsulate {
185  my ($document, $attributes, @nodes) = @_;
186  return unless grep { !$_->getAttribute('_matched'); } @nodes;
187  my $node = $nodes[0];
188  if (!$$attributes{_nowrap} && (scalar(@nodes) > 1)) {
189    $node = $document->wrapNodes('ltx:XMWrap', @nodes); }
190  map { $node->setAttribute($_, $$attributes{$_}) } keys %$attributes;    #}
191  return; }
192
193# Set attributes for a subtree w/wildcards
194# Presumably only on tokens which are not in the wildcard?
195sub setAttributes_wild {
196  my ($document, $attributes, @nodes) = @_;
197  my $node = $nodes[0];
198  return unless grep { !$_->getAttribute('_matched'); } @nodes;
199  if ($$attributes{_nowrap}    # No wrapping requested, or already is an XMDual
200    || ((scalar(@nodes) == 1) && ($document->getNodeQName($nodes[0]) eq 'ltx:XMDual'))) {
201    my ($nonwild) = grep { !$_->getAttribute('_wildcard'); } @nodes;
202    if ($nonwild) {
203      map { $nonwild->setAttribute($_ => $$attributes{$_}); } keys %$attributes; } }
204  else {
205    # Do this slightly clunky, in order to keep the SAME xml @nodes in the result
206    my $wrapper = $document->wrapNodes('ltx:XMWrap', @nodes);
207    my @wildids = set_wildcard_ids($document, $wrapper);
208    $node = $document->wrapNodes('ltx:XMDual', $wrapper);
209    $node->setAttribute(role => $$attributes{role}) if defined $$attributes{role};
210    $node->removeChild($wrapper);
211    my $app = $document->openElementAt($node, 'ltx:XMApp');
212    my $op  = $document->openElementAt($app, 'ltx:XMTok',
213      map { ($_ eq 'role' ? () : ($_ => $$attributes{$_})); } keys %$attributes);
214    foreach my $rid (@wildids) {
215      $document->openElementAt($app, 'ltx:XMRef', idref => $rid); }
216    $node->appendChild($wrapper); }
217  return; }
218
219sub set_wildcard_ids {
220  my ($document, $node) = @_;
221  if (($node->nodeType != XML_ELEMENT_NODE)
222    || $node->getAttribute('_matched')) {
223    return (); }
224  elsif ($node->hasAttribute('_wildcard')) {
225    my $id = $node->getAttribute('xml:id');
226    if (!$id) {
227      LaTeXML::Package::GenerateID($document, $node, undef, '');
228      $id = $node->getAttribute('xml:id'); }
229    return ($id); }
230  else {
231    return map { set_wildcard_ids($document, $_); } $node->childNodes; } }
232
233sub markSeen {
234  my ($node, $nsibs) = @_;
235  for (my $i = 0 ; $node && ($i < $nsibs) ; $i++) {
236    markSeen_rec($node);
237    $node = $node->nextSibling; }
238  return; }
239
240sub markSeen_rec {
241  my ($node) = @_;
242  return if $node->getAttribute('_wildcard');    # Not even children?
243  $node->setAttribute('_matched' => 1);
244  foreach my $child ($node->childNodes) {
245    if ($child->nodeType == XML_ELEMENT_NODE) {
246      markSeen_rec($child); } }
247  return; }
248
249sub markWildcards {
250  my ($node, @wilds) = @_;
251  $node->setAttribute(_has_wildcards => 1) if @wilds;
252  my @n = ();
253  foreach my $wild (@wilds) {
254    my $n     = $node;
255    my $start = 1;
256    foreach my $i (@$wild) {
257      last unless $n;
258      $n     = ($start ? nth_sibling($n, $i) : nth_child($n, $i));
259      $start = 0; }
260    if ($n && ($n->nodeType == XML_ELEMENT_NODE)) {
261      $n->setAttribute('_wildcard' => 1);
262      push(@n, $n); } }
263  return @n; }
264
265sub unmarkWildcards {
266  my (@nodes) = @_;
267  foreach my $n (@nodes) {
268    if ($n && ($n->nodeType == XML_ELEMENT_NODE)) {
269      $n->removeAttribute('_has_wildcards');
270      $n->removeAttribute('_wildcard'); } }
271  return; }
272
273sub nth_sibling {
274  my ($node, $n) = @_;
275  my $nn = $node;
276  while ($nn && ($n > 1)) { $nn = $nn->nextSibling; $n--; }
277  return $nn; }
278
279sub nth_child {
280  my ($node, $n) = @_;
281  my @c = $node->childNodes;
282  return $c[$n - 1]; }
283#**********************************************************************
284sub compileClause {
285  my ($self,   $document, $clause)  = @_;
286  my ($ignore, $op,       $pattern) = @$clause;
287  my ($oop, $opattern) = ($op, $pattern);
288  if ($op eq 'label') {
289    if (ref $pattern eq 'ARRAY') {
290      #      $op='multi_select'; $pattern = [map(["descendant-or-self::*[\@label='$_']",1], @$pattern)]; }
291
292      $op = 'multi_select'; $pattern = [map { ["descendant-or-self::*[\@xml:id='$_']", 1] }
293          map { $self->getLabelID($_) } @$pattern]; }
294    else {
295      #      $op='select'; $pattern=["descendant-or-self::*[\@label='$pattern']",1]; }}
296      $op      = 'select';
297      $pattern = ["descendant-or-self::*[\@xml:id='" . $self->getLabelID($pattern) . "']", 1]; } }
298  elsif ($op eq 'scope') {
299    $op = 'select';
300    if ($pattern =~ /^label:(.*)$/) {
301      #      $pattern=["descendant-or-self::*[\@label='$1']",1]; }
302      $pattern = ["descendant-or-self::*[\@xml:id='" . $self->getLabelID($1) . "']", 1]; }
303    elsif ($pattern =~ /^id:(.*)$/) {
304      $pattern = ["descendant-or-self::*[\@xml:id='$1']", 1]; }
305### Is this pattern ever used? <elementname>:<refnum> expects attribute!!!
306###    elsif ($pattern =~ /^(.*):(.*)$/) {
307###      $pattern = ["descendant-or-self::*[local-name()='$1' and \@refnum='$2']", 1]; }
308    else {
309      Error('misdefined', '<rewrite>', undef,
310        "Unrecognized scope pattern in Rewrite clause: \"$pattern\"; Ignoring it.");
311      $op = 'ignore'; $pattern = []; } }
312  elsif ($op eq 'xpath') {
313    $op = 'select'; $pattern = [$pattern, 1]; }
314  elsif ($op eq 'match') {
315    if (ref $pattern eq 'CODE') {
316      $op = 'test'; }
317    elsif (ref $pattern eq 'ARRAY') {    # Multiple patterns!
318      $op      = 'multi_select';
319      $pattern = [map { $self->compile_match($document, $_) } @$pattern]; }
320    else {
321      $op = 'select'; $pattern = $self->compile_match($document, $pattern); } }
322  elsif ($op eq 'replace') {
323    if (ref $pattern eq 'CODE') { }
324    else {
325      $pattern = $self->compile_replacement($document, $pattern); } }
326  elsif ($op eq 'regexp') {
327    $pattern = $self->compile_regexp($pattern); }
328  Debug("Compiled clause $oop=>" . ToString($opattern) . "  ==> $op=>" . ToString($pattern))
329    if $LaTeXML::DEBUG{rewrite};
330  $$clause[0] = 'compiled'; $$clause[1] = $op; $$clause[2] = $pattern;
331  return; }
332
333#**********************************************************************
334sub compile_match {
335  my ($self, $document, $pattern) = @_;
336###  if (!ref $pattern) {
337###    return $self->compile_match1($document,
338###      digest_rewrite(($$self{math} ? '$' . $pattern . '$' : $pattern))); }
339###  els
340  if ($pattern->isaBox) {
341    return $self->compile_match1($document, $pattern); }
342  elsif (ref $pattern) {    # Is tokens????
343    return $self->compile_match1($document, digest_rewrite($pattern)); }
344  else {
345    Error('misdefined', '<rewrite>', undef,
346      "Don't know what to do with match=>\"" . Stringify($pattern) . "\"");
347    return; } }
348
349sub compile_match1 {
350  my ($self, $document, $patternbox) = @_;
351  # Create a temporary document
352  my $capdocument = LaTeXML::Core::Document->new($document->getModel);
353  my $capture     = $capdocument->openElement('_Capture_', font => LaTeXML::Common::Font->new());
354  $capdocument->absorb($patternbox);
355  my @nodes = ($$self{mode} eq 'math'
356    ? $capdocument->findnodes("//ltx:XMath/*", $capture)
357    : $capture->childNodes);
358  my $frag = $capdocument->getDocument->createDocumentFragment;
359  map { $frag->appendChild($_) } @nodes;
360  # Convert the captured nodes to an XPath that would match them.
361  my ($xpath, $nnodes, @wilds) = domToXPath($capdocument, $frag);
362  # The branches of an XMDual can contain "decorations", nodes that are ONLY visible
363  # from either presentation or content, but not both.
364  # [See LaTeXML::Core::Document->markXMNodeVisibility]
365  # These decorations should NOT have rewrite rules applied
366  $xpath .= '[@_pvis and @_cvis]' if $$self{math};
367
368  if ($LaTeXML::DEBUG{rewrite}) {
369    Debug("Converting \"" . ToString($patternbox) . "\"\n  $nnodes nodes\n  => xpath= \"$xpath\"");
370    foreach my $w (@wilds) {
371      Debug('with wildcard \@' . join(',', @$w)); } }
372  return [$xpath, $nnodes, @wilds]; }
373
374# Reworked to do digestion at replacement time.
375sub compile_replacement {
376  my ($self, $document, $pattern) = @_;
377
378  if ((ref $pattern) && $pattern->isaBox) {
379    $pattern = $pattern->getBody if $$self{math};
380    return sub { $_[0]->absorb($pattern); } }
381  else {
382    return sub {
383      my $stomach = $STATE->getStomach;
384      $stomach->bgroup;
385      $STATE->assignValue(font     => LaTeXML::Common::Font->new(), 'local');
386      $STATE->assignValue(mathfont => LaTeXML::Common::Font->new(), 'local');
387      my $box = $stomach->digest($pattern, 0);
388      $stomach->egroup;
389      $box = $box->getBody if $$self{math};
390      $_[0]->absorb($box); }
391} }
392
393sub compile_regexp {
394  my ($self, $pattern) = @_;
395  my $code = "sub { \$_[0] =~ s${pattern}g; }";
396  ## no critic
397  my $fcn = eval $code;
398  ## use critic
399  Error('misdefined', '<rewrite>', undef,
400    "Failed to compile regexp pattern \"$pattern\" into \"$code\": $!") if $@;
401  return $fcn; }
402
403#**********************************************************************
404
405sub digest_rewrite {
406  my ($string) = @_;
407  my $stomach = $STATE->getStomach;
408  $stomach->bgroup;
409  $STATE->assignValue(font => LaTeXML::Common::Font->new(), 'local'); # Use empty font, so eventual insertion merges.
410  $STATE->assignValue(mathfont => LaTeXML::Common::Font->new(), 'local');
411  my $box = $stomach->digest($string, 0);
412  $stomach->egroup;
413  return $box; }
414
415#**********************************************************************
416sub domToXPath {
417  my ($document, $node) = @_;
418  my ($xpath, $nnodes, $nwilds, @wilds)
419    = domToXPath_rec($document, $node, 'descendant-or-self', undef);
420  return ($xpath, $nnodes, @wilds); }
421
422# May need some work here;
423my %EXCLUDED_MATCH_ATTRIBUTES = (scriptpos => 1, 'xml:id' => 1, fontsize => 1);    # [CONSTANT]
424
425sub domToXPath_rec {
426  my ($document, $node, $axis, $pos) = @_;
427  my $type = (ref $node eq 'XML::LibXML::NodeList' ? 999 : $node->nodeType);
428  if (($type == 999) || ($type == XML_DOCUMENT_FRAG_NODE)) {
429    my @nodes = ($type == 999 ? $node->get_nodelist() : $node->childNodes);
430    my ($xpath, $nnodes, @wilds) = domToXPath_seq($document, $axis, $pos, @nodes);
431    return ($xpath, $nnodes, 0, @wilds); }
432  elsif ($type == XML_ELEMENT_NODE) {
433    my $qname      = $document->getNodeQName($node);
434    my @children   = $node->childNodes;
435    my @predicates = ();
436    my @wilds      = ();
437    if ($qname eq '_WildCard_') {
438      my $tomatch = $node->childNodes;    # or all children!
439      if ($tomatch) {
440        my ($xpath, $nnodes, $nwilds, @wilds) = domToXPath_rec($document, $tomatch, $axis, $pos);
441        my $n = (scalar(@children) || 1);
442        return ($xpath, $n, $n); }
443      else {
444        return ($axis . '::*', 1, 1); } }
445    # Also, an XMRef pointing to a wildcard is a wildcard!
446    # (or pointing to an XMArg|XMWrap of a wildcard!)
447    elsif ($qname eq 'ltx:XMRef') {
448      my $id = $node->getAttribute('idref');
449      my $r  = $id && $document->lookupID($id);
450      my $rq = $r  && $document->getNodeQName($r);    # eq '_WildCard_')
451      if ($rq && ($rq =~ /ltx:(?:XMArg|XMWrap)$/)) {
452        my @rc = $r->childNodes;
453        if ((scalar(@rc) == 1)) {
454          $r  = $r->firstChild;
455          $rq = $document->getNodeQName($r); } }
456      if ($rq && ($rq eq '_WildCard_')) {
457        return ($axis . '::*', 1, 1); } }
458    # Also treat XMArg or XMWrap with single wildcard child as a wildcard (w/o children)
459    elsif (($qname =~ /ltx:(?:XMArg|XMWrap)$/)
460      && (scalar(@children) == 1) && ($document->getNodeQName($children[0]) eq '_WildCard_')) {
461      my $tomatch = $children[0]->childNodes;
462      if ($tomatch) {
463        my ($xpath, $nnodes, $nwilds, @wilds) = domToXPath_rec($document, $tomatch, 'child', 1);
464        return ($axis . '::' . $qname . '[' .
465            join(' and ', ($pos ? ('position()=' . $pos) : undef), $xpath) . ']',
466          1, 1); }
467      else {
468        return ($axis . '::*', 1, 1); } }
469    # Order the predicates so as to put most quickly restrictive first.
470    if ($node->hasAttributes) {
471      foreach my $attribute (grep { $_->nodeType == XML_ATTRIBUTE_NODE } $node->attributes) {
472        my $key = $attribute->nodeName;
473        next if ($key =~ /^_/) || $EXCLUDED_MATCH_ATTRIBUTES{$key};
474        push(@predicates, "\@" . $key . "='" . $attribute->getValue . "'"); } }
475    if (@children) {
476      if (!grep { $_->nodeType != XML_TEXT_NODE } @children) {    # All are text nodes:
477        push(@predicates, "text()=" . quoteXPathLiteral($node->textContent)); }
478      elsif (!grep { $_->nodeType != XML_ELEMENT_NODE } @children) {
479        my ($xp, $n, @w) = domToXPath_seq($document, 'child', 1, @children);
480        push(@predicates, $xp);
481        push(@wilds,      @w); }
482      else {
483        Fatal('misdefined', '<rewrite>', $node,
484          "Can't generate XPath for mixed content"); } }
485    if ($document->canHaveAttribute($qname, 'font')) {
486      if (my $font = $node->getAttribute('_font')) {
487        my $pred = LaTeXML::Common::Font::font_match_xpaths($font);
488        push(@predicates, $pred); } }
489
490    if ($pos) {
491      unshift(@predicates, 'self::' . $qname);
492      $qname = '*';
493      unshift(@predicates, 'position()=' . $pos); }
494    my $preds = join(' and ', grep { $_ } @predicates);
495    return ($axis . '::' . $qname . ($preds ? "[" . $preds . "]" : ''), 1, 0, @wilds);
496  }
497
498  elsif ($type == XML_TEXT_NODE) {
499    return ("*[text()=" . quoteXPathLiteral($node->textContent) . "]", 1, 0); } }
500
501# Return quoted string, but note: XPath doesn't provide sensible way to slashify ' or "
502sub quoteXPathLiteral {
503  my ($string) = @_;
504  if    ($string !~ /'/) { return "'" . $string . "'"; }
505  elsif ($string !~ /"/) { return '"' . $string . '"'; }
506  else { return 'concat(' . join(',"\'",', map { "'" . $_ . "'"; } split(/'/, $string)) . ')'; } }
507
508sub domToXPath_seq {
509  my ($document, $axis, $pos, @nodes) = @_;
510  my $i         = 1;
511  my @sibxpaths = ();
512  my @wilds     = ();
513  my ($xpath, $n, $nwilds, @w0) = domToXPath_rec($document, shift(@nodes), $axis, $pos);
514  if ($nwilds) {
515    for (my $j = 0 ; $j < $nwilds ; $j++) {
516      push(@wilds, [$i]); $i++; } }
517  else {
518    push(@wilds, (map { [1, @$_]; } @w0));
519    $i++; }
520  foreach my $sib (@nodes) {
521    my ($xp, $nn, $nw, @w) = domToXPath_rec($document, $sib, 'following-sibling', $i - 1);
522    push(@sibxpaths, $xp);
523    if ($nw) {
524      for (my $j = 0 ; $j < $nw ; $j++) {
525        push(@wilds, [$i]); $i++; } }
526    else {
527      push(@wilds, (map { [$i, @$_]; } @w));
528      $i++; } }
529  return ($xpath . (scalar(@sibxpaths) ? join('', map { '[' . $_ . ']'; } @sibxpaths) : ''),
530    $i - 1, @wilds); }
531
532#**********************************************************************
5331;
534
535__END__
536
537=pod
538
539=head1 NAME
540
541C<LaTeXML::Core::Rewrite> - rewrite rules for modifying the XML document.
542
543=head1 DESCRIPTION
544
545C<LaTeXML::Core::Rewrite> implements rewrite rules for modifying the XML document.
546See L<LaTeXML::Package> for declarations which create the rewrite rules.
547Further documentation needed.
548
549=head1 AUTHOR
550
551Bruce Miller <bruce.miller@nist.gov>
552
553=head1 COPYRIGHT
554
555Public domain software, produced as part of work done by the
556United States Government & not subject to copyright in the US.
557
558=cut
559