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