[ Index ] |
PHP Cross Reference of phpBB-3.3.2-deutsch |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @package s9e\TextFormatter 5 * @copyright Copyright (c) 2010-2020 The s9e authors 6 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7 */ 8 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; 9 10 use Closure; 11 use RuntimeException; 12 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder; 13 14 class Quick 15 { 16 /** 17 * Generate the Quick renderer's source 18 * 19 * @param array $compiledTemplates Array of tagName => compiled template 20 * @return string 21 */ 22 public static function getSource(array $compiledTemplates) 23 { 24 $map = ['dynamic' => [], 'php' => [], 'static' => []]; 25 $tagNames = []; 26 $unsupported = []; 27 28 // Ignore system tags 29 unset($compiledTemplates['br']); 30 unset($compiledTemplates['e']); 31 unset($compiledTemplates['i']); 32 unset($compiledTemplates['p']); 33 unset($compiledTemplates['s']); 34 35 foreach ($compiledTemplates as $tagName => $php) 36 { 37 $renderings = self::getRenderingStrategy($php); 38 if (empty($renderings)) 39 { 40 $unsupported[] = $tagName; 41 continue; 42 } 43 44 foreach ($renderings as $i => list($strategy, $replacement)) 45 { 46 $match = (($i) ? '/' : '') . $tagName; 47 $map[$strategy][$match] = $replacement; 48 } 49 50 // Record the names of tags whose template does not contain a passthrough 51 if (!isset($renderings[1])) 52 { 53 $tagNames[] = $tagName; 54 } 55 } 56 57 $php = []; 58 $php[] = ' /** {@inheritdoc} */'; 59 $php[] = ' public $enableQuickRenderer=true;'; 60 $php[] = ' /** {@inheritdoc} */'; 61 $php[] = ' protected $static=' . self::export($map['static']) . ';'; 62 $php[] = ' /** {@inheritdoc} */'; 63 $php[] = ' protected $dynamic=' . self::export($map['dynamic']) . ';'; 64 65 $quickSource = ''; 66 if (!empty($map['php'])) 67 { 68 $quickSource = SwitchStatement::generate('$id', $map['php']); 69 } 70 71 // Build a regexp that matches all the tags 72 $regexp = '(<(?:(?!/)('; 73 $regexp .= ($tagNames) ? RegexpBuilder::fromList($tagNames) : '(?!)'; 74 $regexp .= ')(?: [^>]*)?>.*?</\\1|(/?(?!br/|p>)[^ />]+)[^>]*?(/)?)>)s'; 75 $php[] = ' /** {@inheritdoc} */'; 76 $php[] = ' protected $quickRegexp=' . var_export($regexp, true) . ';'; 77 78 // Build a regexp that matches tags that cannot be rendered with the Quick renderer 79 if (!empty($unsupported)) 80 { 81 $regexp = '((?<=<)(?:[!?]|' . RegexpBuilder::fromList($unsupported) . '[ />]))'; 82 $php[] = ' /** {@inheritdoc} */'; 83 $php[] = ' protected $quickRenderingTest=' . var_export($regexp, true) . ';'; 84 } 85 86 $php[] = ' /** {@inheritdoc} */'; 87 $php[] = ' protected function renderQuickTemplate($id, $xml)'; 88 $php[] = ' {'; 89 $php[] = ' $attributes=$this->matchAttributes($xml);'; 90 $php[] = " \$html='';" . $quickSource; 91 $php[] = ''; 92 $php[] = ' return $html;'; 93 $php[] = ' }'; 94 95 return implode("\n", $php); 96 } 97 98 /** 99 * Export an array as PHP 100 * 101 * @param array $arr 102 * @return string 103 */ 104 protected static function export(array $arr) 105 { 106 $exportKeys = (array_keys($arr) !== range(0, count($arr) - 1)); 107 ksort($arr); 108 109 $entries = []; 110 foreach ($arr as $k => $v) 111 { 112 $entries[] = (($exportKeys) ? var_export($k, true) . '=>' : '') 113 . ((is_array($v)) ? self::export($v) : var_export($v, true)); 114 } 115 116 return '[' . implode(',', $entries) . ']'; 117 } 118 119 /** 120 * Compute the rendering strategy for a compiled template 121 * 122 * @param string $php Template compiled for the PHP renderer 123 * @return array[] An array containing 0 to 2 pairs of [<rendering type>, <replacement>] 124 */ 125 public static function getRenderingStrategy($php) 126 { 127 $phpRenderings = self::getQuickRendering($php); 128 if (empty($phpRenderings)) 129 { 130 return []; 131 } 132 $renderings = self::getStringRenderings($php); 133 134 // Keep string rendering where possible, use PHP rendering wherever else 135 foreach ($phpRenderings as $i => $phpRendering) 136 { 137 if (!isset($renderings[$i]) || strpos($phpRendering, '$this->attributes[]') !== false) 138 { 139 $renderings[$i] = ['php', $phpRendering]; 140 } 141 } 142 143 return $renderings; 144 } 145 146 /** 147 * Generate the code for rendering a compiled template with the Quick renderer 148 * 149 * Parse and record every code path that contains a passthrough. Parse every if-else structure. 150 * When the whole structure is parsed, there are 2 possible situations: 151 * - no code path contains a passthrough, in which case we discard the data 152 * - all the code paths including the mandatory "else" branch contain a passthrough, in which 153 * case we keep the data 154 * 155 * @param string $php Template compiled for the PHP renderer 156 * @return string[] An array containing one or two strings of PHP, or an empty array 157 * if the PHP cannot be converted 158 */ 159 protected static function getQuickRendering($php) 160 { 161 // xsl:apply-templates elements with a select expression are not supported 162 if (preg_match('(\\$this->at\\((?!\\$node\\);))', $php)) 163 { 164 return []; 165 } 166 167 // Tokenize the PHP and add an empty token as terminator 168 $tokens = token_get_all('<?php ' . $php); 169 $tokens[] = [0, '']; 170 171 // Remove the first token, which is a T_OPEN_TAG 172 array_shift($tokens); 173 $cnt = count($tokens); 174 175 // Prepare the main branch 176 $branch = [ 177 // We purposefully use a value that can never match 178 'braces' => -1, 179 'branches' => [], 180 'head' => '', 181 'passthrough' => 0, 182 'statement' => '', 183 'tail' => '' 184 ]; 185 186 $braces = 0; 187 $i = 0; 188 do 189 { 190 // Test whether we've reached a passthrough 191 if ($tokens[$i ][0] === T_VARIABLE 192 && $tokens[$i ][1] === '$this' 193 && $tokens[$i + 1][0] === T_OBJECT_OPERATOR 194 && $tokens[$i + 2][0] === T_STRING 195 && $tokens[$i + 2][1] === 'at' 196 && $tokens[$i + 3] === '(' 197 && $tokens[$i + 4][0] === T_VARIABLE 198 && $tokens[$i + 4][1] === '$node' 199 && $tokens[$i + 5] === ')' 200 && $tokens[$i + 6] === ';') 201 { 202 if (++$branch['passthrough'] > 1) 203 { 204 // Multiple passthroughs are not supported 205 return []; 206 } 207 208 // Skip to the semi-colon 209 $i += 6; 210 211 continue; 212 } 213 214 $key = ($branch['passthrough']) ? 'tail' : 'head'; 215 $branch[$key] .= (is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i]; 216 217 if ($tokens[$i] === '{') 218 { 219 ++$braces; 220 continue; 221 } 222 223 if ($tokens[$i] === '}') 224 { 225 --$braces; 226 227 if ($branch['braces'] === $braces) 228 { 229 // Remove the last brace from the branch's content 230 $branch[$key] = substr($branch[$key], 0, -1); 231 232 // Jump back to the parent branch 233 $branch =& $branch['parent']; 234 235 // Copy the current index to look ahead 236 $j = $i; 237 238 // Skip whitespace 239 while ($tokens[++$j][0] === T_WHITESPACE); 240 241 // Test whether this is the last brace of an if-else structure by looking for 242 // an additional elseif/else case 243 if ($tokens[$j][0] !== T_ELSEIF && $tokens[$j][0] !== T_ELSE) 244 { 245 $passthroughs = self::getBranchesPassthrough($branch['branches']); 246 if ($passthroughs === [0]) 247 { 248 // No branch was passthrough, move their PHP source back to this branch 249 // then discard the data 250 foreach ($branch['branches'] as $child) 251 { 252 $branch['head'] .= $child['statement'] . '{' . $child['head'] . '}'; 253 } 254 255 $branch['branches'] = []; 256 continue; 257 } 258 259 if ($passthroughs === [1]) 260 { 261 // All branches were passthrough, so their parent is passthrough 262 ++$branch['passthrough']; 263 264 continue; 265 } 266 267 // Mixed branches (with/out passthrough) are not supported 268 return []; 269 } 270 } 271 272 continue; 273 } 274 275 // We don't have to record child branches if we know that current branch is passthrough. 276 // If a child branch contains a passthrough, it will be treated as a multiple 277 // passthrough and we will abort 278 if ($branch['passthrough']) 279 { 280 continue; 281 } 282 283 if ($tokens[$i][0] === T_IF 284 || $tokens[$i][0] === T_ELSEIF 285 || $tokens[$i][0] === T_ELSE) 286 { 287 // Remove the statement from the branch's content 288 $branch[$key] = substr($branch[$key], 0, -strlen($tokens[$i][1])); 289 290 // Create a new branch 291 $branch['branches'][] = [ 292 'braces' => $braces, 293 'branches' => [], 294 'head' => '', 295 'parent' => &$branch, 296 'passthrough' => 0, 297 'statement' => '', 298 'tail' => '' 299 ]; 300 301 // Jump to the new branch 302 $branch =& $branch['branches'][count($branch['branches']) - 1]; 303 304 // Record the PHP statement 305 do 306 { 307 $branch['statement'] .= (is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i]; 308 } 309 while ($tokens[++$i] !== '{'); 310 311 // Account for the brace in the statement 312 ++$braces; 313 } 314 } 315 while (++$i < $cnt); 316 317 list($head, $tail) = self::buildPHP($branch['branches']); 318 $head = $branch['head'] . $head; 319 $tail .= $branch['tail']; 320 321 // Convert the PHP renderer source to the format used in the Quick renderer 322 self::convertPHP($head, $tail, (bool) $branch['passthrough']); 323 324 // Test whether any method call was left unconverted. If so, we cannot render this template 325 if (preg_match('((?<!-|\\$this)->)', $head . $tail)) 326 { 327 return []; 328 } 329 330 return ($branch['passthrough']) ? [$head, $tail] : [$head]; 331 } 332 333 /** 334 * Convert the two sides of a compiled template to quick rendering 335 * 336 * @param string &$head 337 * @param string &$tail 338 * @param bool $passthrough 339 * @return void 340 */ 341 protected static function convertPHP(&$head, &$tail, $passthrough) 342 { 343 // Test whether the attributes must be saved when rendering the head because they're needed 344 // when rendering the tail 345 $saveAttributes = (bool) preg_match('(\\$node->(?:get|has)Attribute)', $tail); 346 347 // Collect the names of all the attributes so that we can initialize them with a null value 348 // to avoid undefined variable notices. We exclude attributes that seem to be in an if block 349 // that tests its existence beforehand. This last part is not an accurate process as it 350 // would be much more expensive to do it accurately but where it fails the only consequence 351 // is we needlessly add the attribute to the list. There is no difference in functionality 352 preg_match_all( 353 "(\\\$node->getAttribute\\('([^']+)'\\))", 354 preg_replace_callback( 355 '(if\\(\\$node->hasAttribute\\(([^\\)]+)[^}]+)', 356 function ($m) 357 { 358 return str_replace('$node->getAttribute(' . $m[1] . ')', '', $m[0]); 359 }, 360 $head . $tail 361 ), 362 $matches 363 ); 364 $attrNames = array_unique($matches[1]); 365 366 // Replace the source in $head and $tail 367 self::replacePHP($head); 368 self::replacePHP($tail); 369 370 if (!$passthrough && strpos($head, '$node->textContent') !== false) 371 { 372 $head = '$textContent=$this->getQuickTextContent($xml);' . str_replace('$node->textContent', '$textContent', $head); 373 } 374 375 if (!empty($attrNames)) 376 { 377 ksort($attrNames); 378 $head = "\$attributes+=['" . implode("'=>null,'", $attrNames) . "'=>null];" . $head; 379 } 380 381 if ($saveAttributes) 382 { 383 $head .= '$this->attributes[]=$attributes;'; 384 $tail = '$attributes=array_pop($this->attributes);' . $tail; 385 } 386 } 387 388 /** 389 * Replace the PHP code used in a compiled template to be used by the Quick renderer 390 * 391 * @param string &$php 392 * @return void 393 */ 394 protected static function replacePHP(&$php) 395 { 396 // Expression that matches a $node->getAttribute() call and captures its string argument 397 $getAttribute = "\\\$node->getAttribute\\(('[^']+')\\)"; 398 399 // Expression that matches a single-quoted string literal 400 $string = "'(?:[^\\\\']|\\\\.)*+'"; 401 402 $replacements = [ 403 '$this->out' => '$html', 404 405 // An attribute value escaped as ENT_NOQUOTES. We only need to unescape quotes 406 '(htmlspecialchars\\(' . $getAttribute . ',' . ENT_NOQUOTES . '\\))' 407 => "str_replace('"','\"',\$attributes[\$1])", 408 409 // One or several attribute values escaped as ENT_COMPAT can be used as-is 410 '(htmlspecialchars\\((' . $getAttribute . '(?:\\.' . $getAttribute . ')*),' . ENT_COMPAT . '\\))' 411 => function ($m) use ($getAttribute) 412 { 413 return preg_replace('(' . $getAttribute . ')', '$attributes[$1]', $m[1]); 414 }, 415 416 // Character replacement can be performed directly on the escaped value provided that it 417 // is then escaped as ENT_COMPAT and that replacements do not interfere with the escaping 418 // of the characters &<>" or their representation &<>" 419 '(htmlspecialchars\\(strtr\\(' . $getAttribute . ",('[^\"&\\\\';<>aglmopqtu]+'),('[^\"&\\\\'<>]+')\\)," . ENT_COMPAT . '\\))' 420 => 'strtr($attributes[$1],$2,$3)', 421 422 // A comparison between two attributes. No need to unescape 423 '(' . $getAttribute . '(!?=+)' . $getAttribute . ')' 424 => '$attributes[$1]$2$attributes[$3]', 425 426 // A comparison between an attribute and a literal string. Rather than unescape the 427 // attribute value, we escape the literal. This applies to comparisons using XPath's 428 // contains() as well (translated to PHP's strpos()) 429 '(' . $getAttribute . '===(' . $string . '))s' 430 => function ($m) 431 { 432 return '$attributes[' . $m[1] . ']===' . htmlspecialchars($m[2], ENT_COMPAT); 433 }, 434 435 '((' . $string . ')===' . $getAttribute . ')s' 436 => function ($m) 437 { 438 return htmlspecialchars($m[1], ENT_COMPAT) . '===$attributes[' . $m[2] . ']'; 439 }, 440 441 '(strpos\\(' . $getAttribute . ',(' . $string . ')\\)([!=]==(?:0|false)))s' 442 => function ($m) 443 { 444 return 'strpos($attributes[' . $m[1] . "]," . htmlspecialchars($m[2], ENT_COMPAT) . ')' . $m[3]; 445 }, 446 447 '(strpos\\((' . $string . '),' . $getAttribute . '\\)([!=]==(?:0|false)))s' 448 => function ($m) 449 { 450 return 'strpos(' . htmlspecialchars($m[1], ENT_COMPAT) . ',$attributes[' . $m[2] . '])' . $m[3]; 451 }, 452 453 // An attribute value used in an arithmetic comparison or operation does not need to be 454 // unescaped. The same applies to empty(), isset() and conditionals 455 '(' . $getAttribute . '(?=(?:==|[-+*])\\d+))' => '$attributes[$1]', 456 '(\\b(\\d+(?:==|[-+*]))' . $getAttribute . ')' => '$1$attributes[$2]', 457 '(empty\\(' . $getAttribute . '\\))' => 'empty($attributes[$1])', 458 "(\\\$node->hasAttribute\\(('[^']+')\\))" => 'isset($attributes[$1])', 459 'if($node->attributes->length)' => 'if($this->hasNonNullValues($attributes))', 460 461 // In all other situations, unescape the attribute value before use 462 '(' . $getAttribute . ')' => 'htmlspecialchars_decode($attributes[$1])' 463 ]; 464 465 foreach ($replacements as $match => $replace) 466 { 467 if ($replace instanceof Closure) 468 { 469 $php = preg_replace_callback($match, $replace, $php); 470 } 471 elseif ($match[0] === '(') 472 { 473 $php = preg_replace($match, $replace, $php); 474 } 475 else 476 { 477 $php = str_replace($match, $replace, $php); 478 } 479 } 480 } 481 482 /** 483 * Build the source for the two sides of a templates based on the structure extracted from its 484 * original source 485 * 486 * @param array $branches 487 * @return string[] 488 */ 489 protected static function buildPHP(array $branches) 490 { 491 $return = ['', '']; 492 foreach ($branches as $branch) 493 { 494 $return[0] .= $branch['statement'] . '{' . $branch['head']; 495 $return[1] .= $branch['statement'] . '{'; 496 497 if ($branch['branches']) 498 { 499 list($head, $tail) = self::buildPHP($branch['branches']); 500 501 $return[0] .= $head; 502 $return[1] .= $tail; 503 } 504 505 $return[0] .= '}'; 506 $return[1] .= $branch['tail'] . '}'; 507 } 508 509 return $return; 510 } 511 512 /** 513 * Get the unique values for the "passthrough" key of given branches 514 * 515 * @param array $branches 516 * @return integer[] 517 */ 518 protected static function getBranchesPassthrough(array $branches) 519 { 520 $values = []; 521 foreach ($branches as $branch) 522 { 523 $values[] = $branch['passthrough']; 524 } 525 526 // If the last branch isn't an "else", we act as if there was an additional branch with no 527 // passthrough 528 if ($branch['statement'] !== 'else') 529 { 530 $values[] = 0; 531 } 532 533 return array_unique($values); 534 } 535 536 /** 537 * Get a string suitable as a preg_replace() replacement for given PHP code 538 * 539 * @param string $php Original code 540 * @return array|bool Array of [regexp, replacement] if possible, or FALSE otherwise 541 */ 542 protected static function getDynamicRendering($php) 543 { 544 $rendering = ''; 545 546 $literal = "(?<literal>'((?>[^'\\\\]+|\\\\['\\\\])*)')"; 547 $attribute = "(?<attribute>htmlspecialchars\\(\\\$node->getAttribute\\('([^']+)'\\),2\\))"; 548 $value = "(?<value>$literal|$attribute)"; 549 $output = "(?<output>\\\$this->out\\.=$value(?:\\.(?&value))*;)"; 550 551 $copyOfAttribute = "(?<copyOfAttribute>if\\(\\\$node->hasAttribute\\('([^']+)'\\)\\)\\{\\\$this->out\\.=' \\g-1=\"'\\.htmlspecialchars\\(\\\$node->getAttribute\\('\\g-1'\\),2\\)\\.'\"';\\})"; 552 553 $regexp = '(^(' . $output . '|' . $copyOfAttribute . ')*$)'; 554 if (!preg_match($regexp, $php, $m)) 555 { 556 return false; 557 } 558 559 // Attributes that are copied in the replacement 560 $copiedAttributes = []; 561 562 // Attributes whose value is used in the replacement 563 $usedAttributes = []; 564 565 $regexp = '(' . $output . '|' . $copyOfAttribute . ')A'; 566 $offset = 0; 567 while (preg_match($regexp, $php, $m, 0, $offset)) 568 { 569 // Test whether it's normal output or a copy of attribute 570 if ($m['output']) 571 { 572 // 12 === strlen('$this->out.=') 573 $offset += 12; 574 575 while (preg_match('(' . $value . ')A', $php, $m, 0, $offset)) 576 { 577 // Test whether it's a literal or an attribute value 578 if ($m['literal']) 579 { 580 // Unescape the literal 581 $str = stripslashes(substr($m[0], 1, -1)); 582 583 // Escape special characters 584 $rendering .= preg_replace('([\\\\$](?=\\d))', '\\\\$0', $str); 585 } 586 else 587 { 588 $attrName = end($m); 589 590 // Generate a unique ID for this attribute name, we'll use it as a 591 // placeholder until we have the full list of captures and we can replace it 592 // with the capture number 593 if (!isset($usedAttributes[$attrName])) 594 { 595 $usedAttributes[$attrName] = uniqid($attrName, true); 596 } 597 598 $rendering .= $usedAttributes[$attrName]; 599 } 600 601 // Skip the match plus the next . or ; 602 $offset += 1 + strlen($m[0]); 603 } 604 } 605 else 606 { 607 $attrName = end($m); 608 609 if (!isset($copiedAttributes[$attrName])) 610 { 611 $copiedAttributes[$attrName] = uniqid($attrName, true); 612 } 613 614 $rendering .= $copiedAttributes[$attrName]; 615 $offset += strlen($m[0]); 616 } 617 } 618 619 // Gather the names of the attributes used in the replacement either by copy or by value 620 $attrNames = array_keys($copiedAttributes + $usedAttributes); 621 622 // Sort them alphabetically 623 sort($attrNames); 624 625 // Keep a copy of the attribute names to be used in the fillter subpattern 626 $remainingAttributes = array_combine($attrNames, $attrNames); 627 628 // Prepare the final regexp 629 $regexp = '(^[^ ]+'; 630 $index = 0; 631 foreach ($attrNames as $attrName) 632 { 633 // Add a subpattern that matches (and skips) any attribute definition that is not one of 634 // the remaining attributes we're trying to match 635 $regexp .= '(?> (?!' . RegexpBuilder::fromList($remainingAttributes) . '=)[^=]+="[^"]*")*'; 636 unset($remainingAttributes[$attrName]); 637 638 $regexp .= '('; 639 640 if (isset($copiedAttributes[$attrName])) 641 { 642 self::replacePlaceholder($rendering, $copiedAttributes[$attrName], ++$index); 643 } 644 else 645 { 646 $regexp .= '?>'; 647 } 648 649 $regexp .= ' ' . $attrName . '="'; 650 651 if (isset($usedAttributes[$attrName])) 652 { 653 $regexp .= '('; 654 655 self::replacePlaceholder($rendering, $usedAttributes[$attrName], ++$index); 656 } 657 658 $regexp .= '[^"]*'; 659 660 if (isset($usedAttributes[$attrName])) 661 { 662 $regexp .= ')'; 663 } 664 665 $regexp .= '")?'; 666 } 667 668 $regexp .= '.*)s'; 669 670 return [$regexp, $rendering]; 671 } 672 673 /** 674 * Get a string suitable as a str_replace() replacement for given PHP code 675 * 676 * @param string $php Original code 677 * @return bool|string Static replacement if possible, or FALSE otherwise 678 */ 679 protected static function getStaticRendering($php) 680 { 681 if ($php === '') 682 { 683 return ''; 684 } 685 686 $regexp = "(^\\\$this->out\.='((?>[^'\\\\]|\\\\['\\\\])*+)';\$)"; 687 if (preg_match($regexp, $php, $m)) 688 { 689 return stripslashes($m[1]); 690 } 691 692 return false; 693 } 694 695 /** 696 * Get string rendering strategies for given chunks 697 * 698 * @param string $php 699 * @return array 700 */ 701 protected static function getStringRenderings($php) 702 { 703 $chunks = explode('$this->at($node);', $php); 704 if (count($chunks) > 2) 705 { 706 // Can't use string replacements if there are more than one xsl:apply-templates 707 return []; 708 } 709 710 $renderings = []; 711 foreach ($chunks as $k => $chunk) 712 { 713 // Try a static replacement first 714 $rendering = self::getStaticRendering($chunk); 715 if ($rendering !== false) 716 { 717 $renderings[$k] = ['static', $rendering]; 718 } 719 elseif ($k === 0) 720 { 721 // If this is the first chunk, we can try a dynamic replacement. This wouldn't work 722 // for the second chunk because we wouldn't have access to the attribute values 723 $rendering = self::getDynamicRendering($chunk); 724 if ($rendering !== false) 725 { 726 $renderings[$k] = ['dynamic', $rendering]; 727 } 728 } 729 } 730 731 return $renderings; 732 } 733 734 /** 735 * Replace all instances of a uniqid with a PCRE replacement in a string 736 * 737 * @param string &$str PCRE replacement 738 * @param string $uniqid Unique ID 739 * @param integer $index Capture index 740 * @return void 741 */ 742 protected static function replacePlaceholder(&$str, $uniqid, $index) 743 { 744 $str = preg_replace_callback( 745 '(' . preg_quote($uniqid) . '(.))', 746 function ($m) use ($index) 747 { 748 // Replace with $1 where unambiguous and ${1} otherwise 749 if (is_numeric($m[1])) 750 { 751 return '${' . $index . '}' . $m[1]; 752 } 753 else 754 { 755 return '$' . $index . $m[1]; 756 } 757 }, 758 $str 759 ); 760 } 761 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Nov 11 20:28:18 2020 | Cross-referenced by PHPXref 0.7.1 |