[ Index ] |
PHP Cross Reference of phpBB-3.2.11-deutsch |
[Summary view] [Print] [Text view]
1 <?php 2 3 /* 4 * @package s9e\TextFormatter 5 * @copyright Copyright (c) 2010-2019 The s9e Authors 6 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7 */ 8 namespace s9e\TextFormatter; 9 use InvalidArgumentException; 10 use RuntimeException; 11 use s9e\TextFormatter\Configurator\BundleGenerator; 12 use s9e\TextFormatter\Configurator\Collections\AttributeFilterCollection; 13 use s9e\TextFormatter\Configurator\Collections\PluginCollection; 14 use s9e\TextFormatter\Configurator\Collections\Ruleset; 15 use s9e\TextFormatter\Configurator\Collections\TagCollection; 16 use s9e\TextFormatter\Configurator\ConfigProvider; 17 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper; 18 use s9e\TextFormatter\Configurator\Helpers\RulesHelper; 19 use s9e\TextFormatter\Configurator\JavaScript; 20 use s9e\TextFormatter\Configurator\JavaScript\Dictionary; 21 use s9e\TextFormatter\Configurator\Rendering; 22 use s9e\TextFormatter\Configurator\RulesGenerator; 23 use s9e\TextFormatter\Configurator\TemplateChecker; 24 use s9e\TextFormatter\Configurator\TemplateNormalizer; 25 use s9e\TextFormatter\Configurator\UrlConfig; 26 class Configurator implements ConfigProvider 27 { 28 public $attributeFilters; 29 public $bundleGenerator; 30 public $javascript; 31 public $plugins; 32 public $registeredVars; 33 public $rendering; 34 public $rootRules; 35 public $rulesGenerator; 36 public $tags; 37 public $templateChecker; 38 public $templateNormalizer; 39 public function __construct() 40 { 41 $this->attributeFilters = new AttributeFilterCollection; 42 $this->bundleGenerator = new BundleGenerator($this); 43 $this->plugins = new PluginCollection($this); 44 $this->registeredVars = ['urlConfig' => new UrlConfig]; 45 $this->rendering = new Rendering($this); 46 $this->rootRules = new Ruleset; 47 $this->rulesGenerator = new RulesGenerator; 48 $this->tags = new TagCollection; 49 $this->templateChecker = new TemplateChecker; 50 $this->templateNormalizer = new TemplateNormalizer; 51 } 52 public function __get($k) 53 { 54 if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k)) 55 return (isset($this->plugins[$k])) 56 ? $this->plugins[$k] 57 : $this->plugins->load($k); 58 if (isset($this->registeredVars[$k])) 59 return $this->registeredVars[$k]; 60 throw new RuntimeException("Undefined property '" . __CLASS__ . '::$' . $k . "'"); 61 } 62 public function __isset($k) 63 { 64 if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k)) 65 return isset($this->plugins[$k]); 66 return isset($this->registeredVars[$k]); 67 } 68 public function __set($k, $v) 69 { 70 if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k)) 71 $this->plugins[$k] = $v; 72 else 73 $this->registeredVars[$k] = $v; 74 } 75 public function __unset($k) 76 { 77 if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k)) 78 unset($this->plugins[$k]); 79 else 80 unset($this->registeredVars[$k]); 81 } 82 public function enableJavaScript() 83 { 84 if (!isset($this->javascript)) 85 $this->javascript = new JavaScript($this); 86 } 87 public function finalize() 88 { 89 $return = []; 90 $this->plugins->finalize(); 91 foreach ($this->tags as $tag) 92 $this->templateNormalizer->normalizeTag($tag); 93 $return['renderer'] = $this->rendering->getRenderer(); 94 $this->addTagRules(); 95 $config = $this->asConfig(); 96 if (isset($this->javascript)) 97 $return['js'] = $this->javascript->getParser(ConfigHelper::filterConfig($config, 'JS')); 98 $config = ConfigHelper::filterConfig($config, 'PHP'); 99 ConfigHelper::optimizeArray($config); 100 $return['parser'] = new Parser($config); 101 return $return; 102 } 103 public function loadBundle($bundleName) 104 { 105 if (!\preg_match('#^[A-Z][A-Za-z0-9]+$#D', $bundleName)) 106 throw new InvalidArgumentException("Invalid bundle name '" . $bundleName . "'"); 107 $className = __CLASS__ . '\\Bundles\\' . $bundleName; 108 $bundle = new $className; 109 $bundle->configure($this); 110 } 111 public function saveBundle($className, $filepath, array $options = []) 112 { 113 $file = "<?php\n\n" . $this->bundleGenerator->generate($className, $options); 114 return (\file_put_contents($filepath, $file) !== \false); 115 } 116 public function asConfig() 117 { 118 $properties = \get_object_vars($this); 119 unset($properties['attributeFilters']); 120 unset($properties['bundleGenerator']); 121 unset($properties['javascript']); 122 unset($properties['rendering']); 123 unset($properties['rulesGenerator']); 124 unset($properties['registeredVars']); 125 unset($properties['templateChecker']); 126 unset($properties['templateNormalizer']); 127 unset($properties['stylesheet']); 128 $config = ConfigHelper::toArray($properties); 129 $bitfields = RulesHelper::getBitfields($this->tags, $this->rootRules); 130 $config['rootContext'] = $bitfields['root']; 131 $config['rootContext']['flags'] = $config['rootRules']['flags']; 132 $config['registeredVars'] = ConfigHelper::toArray($this->registeredVars, \true); 133 $config += [ 134 'plugins' => [], 135 'tags' => [] 136 ]; 137 $config['tags'] = \array_intersect_key($config['tags'], $bitfields['tags']); 138 foreach ($bitfields['tags'] as $tagName => $tagBitfields) 139 $config['tags'][$tagName] += $tagBitfields; 140 unset($config['rootRules']); 141 return $config; 142 } 143 protected function addTagRules() 144 { 145 $rules = $this->rulesGenerator->getRules($this->tags); 146 $this->rootRules->merge($rules['root'], \false); 147 foreach ($rules['tags'] as $tagName => $tagRules) 148 $this->tags[$tagName]->rules->merge($tagRules, \false); 149 } 150 } 151 152 /* 153 * @package s9e\TextFormatter 154 * @copyright Copyright (c) 2010-2019 The s9e Authors 155 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 156 */ 157 namespace s9e\TextFormatter\Configurator; 158 use s9e\TextFormatter\Configurator; 159 use s9e\TextFormatter\Configurator\RendererGenerators\PHP; 160 class BundleGenerator 161 { 162 protected $configurator; 163 public $serializer = 'serialize'; 164 public $unserializer = 'unserialize'; 165 public function __construct(Configurator $configurator) 166 { 167 $this->configurator = $configurator; 168 } 169 public function generate($className, array $options = []) 170 { 171 $options += ['autoInclude' => \true]; 172 $objects = $this->configurator->finalize(); 173 $parser = $objects['parser']; 174 $renderer = $objects['renderer']; 175 $namespace = ''; 176 if (\preg_match('#(.*)\\\\([^\\\\]+)$#', $className, $m)) 177 { 178 $namespace = $m[1]; 179 $className = $m[2]; 180 } 181 $php = []; 182 $php[] = '/**'; 183 $php[] = '* @package s9e\TextFormatter'; 184 $php[] = '* @copyright Copyright (c) 2010-2019 The s9e Authors'; 185 $php[] = '* @license http://www.opensource.org/licenses/mit-license.php The MIT License'; 186 $php[] = '*/'; 187 if ($namespace) 188 { 189 $php[] = 'namespace ' . $namespace . ';'; 190 $php[] = ''; 191 } 192 $php[] = 'abstract class ' . $className . ' extends \\s9e\\TextFormatter\\Bundle'; 193 $php[] = '{'; 194 $php[] = ' /**'; 195 $php[] = ' * @var s9e\\TextFormatter\\Parser Singleton instance used by parse()'; 196 $php[] = ' */'; 197 $php[] = ' protected static $parser;'; 198 $php[] = ''; 199 $php[] = ' /**'; 200 $php[] = ' * @var s9e\\TextFormatter\\Renderer Singleton instance used by render()'; 201 $php[] = ' */'; 202 $php[] = ' protected static $renderer;'; 203 $php[] = ''; 204 $events = [ 205 'beforeParse' 206 => 'Callback executed before parse(), receives the original text as argument', 207 'afterParse' 208 => 'Callback executed after parse(), receives the parsed text as argument', 209 'beforeRender' 210 => 'Callback executed before render(), receives the parsed text as argument', 211 'afterRender' 212 => 'Callback executed after render(), receives the output as argument', 213 'beforeUnparse' 214 => 'Callback executed before unparse(), receives the parsed text as argument', 215 'afterUnparse' 216 => 'Callback executed after unparse(), receives the original text as argument' 217 ]; 218 foreach ($events as $eventName => $eventDesc) 219 if (isset($options[$eventName])) 220 { 221 $php[] = ' /**'; 222 $php[] = ' * @var ' . $eventDesc; 223 $php[] = ' */'; 224 $php[] = ' public static $' . $eventName . ' = ' . \var_export($options[$eventName], \true) . ';'; 225 $php[] = ''; 226 } 227 $php[] = ' /**'; 228 $php[] = ' * Return a new instance of s9e\\TextFormatter\\Parser'; 229 $php[] = ' *'; 230 $php[] = ' * @return s9e\\TextFormatter\\Parser'; 231 $php[] = ' */'; 232 $php[] = ' public static function getParser()'; 233 $php[] = ' {'; 234 if (isset($options['parserSetup'])) 235 { 236 $php[] = ' $parser = ' . $this->exportObject($parser) . ';'; 237 $php[] = ' ' . $this->exportCallback($namespace, $options['parserSetup'], '$parser') . ';'; 238 $php[] = ''; 239 $php[] = ' return $parser;'; 240 } 241 else 242 $php[] = ' return ' . $this->exportObject($parser) . ';'; 243 $php[] = ' }'; 244 $php[] = ''; 245 $php[] = ' /**'; 246 $php[] = ' * Return a new instance of s9e\\TextFormatter\\Renderer'; 247 $php[] = ' *'; 248 $php[] = ' * @return s9e\\TextFormatter\\Renderer'; 249 $php[] = ' */'; 250 $php[] = ' public static function getRenderer()'; 251 $php[] = ' {'; 252 if (!empty($options['autoInclude']) 253 && $this->configurator->rendering->engine instanceof PHP 254 && isset($this->configurator->rendering->engine->lastFilepath)) 255 { 256 $className = \get_class($renderer); 257 $filepath = \realpath($this->configurator->rendering->engine->lastFilepath); 258 $php[] = ' if (!class_exists(' . \var_export($className, \true) . ', false)'; 259 $php[] = ' && file_exists(' . \var_export($filepath, \true) . '))'; 260 $php[] = ' {'; 261 $php[] = ' include ' . \var_export($filepath, \true) . ';'; 262 $php[] = ' }'; 263 $php[] = ''; 264 } 265 if (isset($options['rendererSetup'])) 266 { 267 $php[] = ' $renderer = ' . $this->exportObject($renderer) . ';'; 268 $php[] = ' ' . $this->exportCallback($namespace, $options['rendererSetup'], '$renderer') . ';'; 269 $php[] = ''; 270 $php[] = ' return $renderer;'; 271 } 272 else 273 $php[] = ' return ' . $this->exportObject($renderer) . ';'; 274 $php[] = ' }'; 275 $php[] = '}'; 276 return \implode("\n", $php); 277 } 278 protected function exportCallback($namespace, callable $callback, $argument) 279 { 280 if (\is_array($callback) && \is_string($callback[0])) 281 $callback = $callback[0] . '::' . $callback[1]; 282 if (!\is_string($callback)) 283 return 'call_user_func(' . \var_export($callback, \true) . ', ' . $argument . ')'; 284 if ($callback[0] !== '\\') 285 $callback = '\\' . $callback; 286 if (\substr($callback, 0, 2 + \strlen($namespace)) === '\\' . $namespace . '\\') 287 $callback = \substr($callback, 2 + \strlen($namespace)); 288 return $callback . '(' . $argument . ')'; 289 } 290 protected function exportObject($obj) 291 { 292 $str = \call_user_func($this->serializer, $obj); 293 $str = \var_export($str, \true); 294 return $this->unserializer . '(' . $str . ')'; 295 } 296 } 297 298 /* 299 * @package s9e\TextFormatter 300 * @copyright Copyright (c) 2010-2019 The s9e Authors 301 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 302 */ 303 namespace s9e\TextFormatter\Configurator; 304 interface ConfigProvider 305 { 306 public function asConfig(); 307 } 308 309 /* 310 * @package s9e\TextFormatter 311 * @copyright Copyright (c) 2010-2019 The s9e Authors 312 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 313 */ 314 namespace s9e\TextFormatter\Configurator; 315 interface FilterableConfigValue 316 { 317 public function filterConfig($target); 318 } 319 320 /* 321 * @package s9e\TextFormatter 322 * @copyright Copyright (c) 2010-2019 The s9e Authors 323 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 324 */ 325 namespace s9e\TextFormatter\Configurator\Helpers; 326 use DOMAttr; 327 use RuntimeException; 328 abstract class AVTHelper 329 { 330 public static function parse($attrValue) 331 { 332 \preg_match_all('(\\{\\{|\\{(?:[^\'"}]|\'[^\']*\'|"[^"]*")+\\}|\\{|[^{]++)', $attrValue, $matches); 333 $tokens = []; 334 foreach ($matches[0] as $str) 335 if ($str === '{{' || $str === '{') 336 $tokens[] = ['literal', '{']; 337 elseif ($str[0] === '{') 338 $tokens[] = ['expression', \substr($str, 1, -1)]; 339 else 340 $tokens[] = ['literal', \str_replace('}}', '}', $str)]; 341 return $tokens; 342 } 343 public static function replace(DOMAttr $attribute, callable $callback) 344 { 345 $tokens = self::parse($attribute->value); 346 foreach ($tokens as $k => $token) 347 $tokens[$k] = $callback($token); 348 $attribute->value = \htmlspecialchars(self::serialize($tokens), \ENT_NOQUOTES, 'UTF-8'); 349 } 350 public static function serialize(array $tokens) 351 { 352 $attrValue = ''; 353 foreach ($tokens as $token) 354 if ($token[0] === 'literal') 355 $attrValue .= \preg_replace('([{}])', '$0$0', $token[1]); 356 elseif ($token[0] === 'expression') 357 $attrValue .= '{' . $token[1] . '}'; 358 else 359 throw new RuntimeException('Unknown token type'); 360 return $attrValue; 361 } 362 public static function toXSL($attrValue) 363 { 364 $xsl = ''; 365 foreach (self::parse($attrValue) as $_f6b3b659) 366 { 367 list($type, $content) = $_f6b3b659; 368 if ($type === 'expression') 369 $xsl .= '<xsl:value-of select="' . \htmlspecialchars($content, \ENT_COMPAT, 'UTF-8') . '"/>'; 370 elseif (\trim($content) !== $content) 371 $xsl .= '<xsl:text>' . \htmlspecialchars($content, \ENT_NOQUOTES, 'UTF-8') . '</xsl:text>'; 372 else 373 $xsl .= \htmlspecialchars($content, \ENT_NOQUOTES, 'UTF-8'); 374 } 375 return $xsl; 376 } 377 } 378 379 /* 380 * @package s9e\TextFormatter 381 * @copyright Copyright (c) 2010-2019 The s9e Authors 382 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 383 */ 384 namespace s9e\TextFormatter\Configurator\Helpers; 385 class CharacterClassBuilder 386 { 387 protected $chars; 388 public $delimiter = '/'; 389 protected $ranges; 390 public function fromList(array $chars) 391 { 392 $this->chars = $chars; 393 $this->unescapeLiterals(); 394 \sort($this->chars); 395 $this->storeRanges(); 396 $this->reorderDash(); 397 $this->fixCaret(); 398 $this->escapeSpecialChars(); 399 return $this->buildCharacterClass(); 400 } 401 protected function buildCharacterClass() 402 { 403 $str = '['; 404 foreach ($this->ranges as $_b7914274) 405 { 406 list($start, $end) = $_b7914274; 407 if ($end > $start + 2) 408 $str .= $this->chars[$start] . '-' . $this->chars[$end]; 409 else 410 $str .= \implode('', \array_slice($this->chars, $start, $end + 1 - $start)); 411 } 412 $str .= ']'; 413 return $str; 414 } 415 protected function escapeSpecialChars() 416 { 417 $specialChars = ['\\', ']', $this->delimiter]; 418 foreach (\array_intersect($this->chars, $specialChars) as $k => $v) 419 $this->chars[$k] = '\\' . $v; 420 } 421 protected function fixCaret() 422 { 423 $k = \array_search('^', $this->chars, \true); 424 if ($this->ranges[0][0] !== $k) 425 return; 426 if (isset($this->ranges[1])) 427 { 428 $range = $this->ranges[0]; 429 $this->ranges[0] = $this->ranges[1]; 430 $this->ranges[1] = $range; 431 } 432 else 433 $this->chars[$k] = '\\^'; 434 } 435 protected function reorderDash() 436 { 437 $dashIndex = \array_search('-', $this->chars, \true); 438 if ($dashIndex === \false) 439 return; 440 $k = \array_search([$dashIndex, $dashIndex], $this->ranges, \true); 441 if ($k > 0) 442 { 443 unset($this->ranges[$k]); 444 \array_unshift($this->ranges, [$dashIndex, $dashIndex]); 445 } 446 $commaIndex = \array_search(',', $this->chars); 447 $range = [$commaIndex, $dashIndex]; 448 $k = \array_search($range, $this->ranges, \true); 449 if ($k !== \false) 450 { 451 $this->ranges[$k] = [$commaIndex, $commaIndex]; 452 \array_unshift($this->ranges, [$dashIndex, $dashIndex]); 453 } 454 } 455 protected function storeRanges() 456 { 457 $values = []; 458 foreach ($this->chars as $char) 459 if (\strlen($char) === 1) 460 $values[] = \ord($char); 461 else 462 $values[] = \false; 463 $i = \count($values) - 1; 464 $ranges = []; 465 while ($i >= 0) 466 { 467 $start = $i; 468 $end = $i; 469 while ($start > 0 && $values[$start - 1] === $values[$end] - ($end + 1 - $start)) 470 --$start; 471 $ranges[] = [$start, $end]; 472 $i = $start - 1; 473 } 474 $this->ranges = \array_reverse($ranges); 475 } 476 protected function unescapeLiterals() 477 { 478 foreach ($this->chars as $k => $char) 479 if ($char[0] === '\\' && \preg_match('(^\\\\[^a-z]$)Di', $char)) 480 $this->chars[$k] = \substr($char, 1); 481 } 482 } 483 484 /* 485 * @package s9e\TextFormatter 486 * @copyright Copyright (c) 2010-2019 The s9e Authors 487 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 488 */ 489 namespace s9e\TextFormatter\Configurator\Helpers; 490 use RuntimeException; 491 use Traversable; 492 use s9e\TextFormatter\Configurator\ConfigProvider; 493 use s9e\TextFormatter\Configurator\FilterableConfigValue; 494 use s9e\TextFormatter\Configurator\JavaScript\Dictionary; 495 abstract class ConfigHelper 496 { 497 public static function filterConfig(array $config, $target = 'PHP') 498 { 499 $filteredConfig = []; 500 foreach ($config as $name => $value) 501 { 502 if ($value instanceof FilterableConfigValue) 503 { 504 $value = $value->filterConfig($target); 505 if (!isset($value)) 506 continue; 507 } 508 if (\is_array($value)) 509 $value = self::filterConfig($value, $target); 510 $filteredConfig[$name] = $value; 511 } 512 return $filteredConfig; 513 } 514 public static function generateQuickMatchFromList(array $strings) 515 { 516 foreach ($strings as $string) 517 { 518 $stringLen = \strlen($string); 519 $substrings = []; 520 for ($len = $stringLen; $len; --$len) 521 { 522 $pos = $stringLen - $len; 523 do 524 { 525 $substrings[\substr($string, $pos, $len)] = 1; 526 } 527 while (--$pos >= 0); 528 } 529 if (isset($goodStrings)) 530 { 531 $goodStrings = \array_intersect_key($goodStrings, $substrings); 532 if (empty($goodStrings)) 533 break; 534 } 535 else 536 $goodStrings = $substrings; 537 } 538 if (empty($goodStrings)) 539 return \false; 540 return \strval(\key($goodStrings)); 541 } 542 public static function optimizeArray(array &$config, array &$cache = []) 543 { 544 foreach ($config as $k => &$v) 545 { 546 if (!\is_array($v)) 547 continue; 548 self::optimizeArray($v, $cache); 549 $cacheKey = \serialize($v); 550 if (!isset($cache[$cacheKey])) 551 $cache[$cacheKey] = $v; 552 $config[$k] =& $cache[$cacheKey]; 553 } 554 unset($v); 555 } 556 public static function toArray($value, $keepEmpty = \false, $keepNull = \false) 557 { 558 $array = []; 559 foreach ($value as $k => $v) 560 { 561 $isDictionary = $v instanceof Dictionary; 562 if ($v instanceof ConfigProvider) 563 $v = $v->asConfig(); 564 elseif ($v instanceof Traversable || \is_array($v)) 565 $v = self::toArray($v, $keepEmpty, $keepNull); 566 elseif (\is_scalar($v) || \is_null($v)) 567 ; 568 else 569 { 570 $type = (\is_object($v)) 571 ? 'an instance of ' . \get_class($v) 572 : 'a ' . \gettype($v); 573 throw new RuntimeException('Cannot convert ' . $type . ' to array'); 574 } 575 if (!isset($v) && !$keepNull) 576 continue; 577 if (!$keepEmpty && $v === []) 578 continue; 579 $array[$k] = ($isDictionary) ? new Dictionary($v) : $v; 580 } 581 return $array; 582 } 583 } 584 585 /* 586 * @package s9e\TextFormatter 587 * @copyright Copyright (c) 2010-2019 The s9e Authors 588 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 589 */ 590 namespace s9e\TextFormatter\Configurator\Helpers; 591 use DOMElement; 592 use DOMXPath; 593 class ElementInspector 594 { 595 protected static $htmlElements = [ 596 'a'=>['c'=>"\17\0\0\0\0\1",'c3'=>'@href','ac'=>"\0",'dd'=>"\10\0\0\0\0\1",'t'=>1,'fe'=>1], 597 'abbr'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 598 'address'=>['c'=>"\3\40",'ac'=>"\1",'dd'=>"\200\44",'b'=>1,'cp'=>['p']], 599 'article'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 600 'aside'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 601 'audio'=>['c'=>"\57",'c3'=>'@controls','c1'=>'@controls','ac'=>"\0\0\0\104",'ac26'=>'not(@src)','dd'=>"\0\0\0\0\0\2",'dd41'=>'@src','t'=>1], 602 'b'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 603 'base'=>['c'=>"\100",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 604 'bdi'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 605 'bdo'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 606 'blockquote'=>['c'=>"\23",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['p']], 607 'body'=>['c'=>"\20\0\2",'ac'=>"\1",'dd'=>"\0",'b'=>1], 608 'br'=>['c'=>"\5",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 609 'button'=>['c'=>"\17\1",'ac'=>"\4",'dd'=>"\10"], 610 'caption'=>['c'=>"\0\2",'ac'=>"\1",'dd'=>"\0\0\0\200",'b'=>1], 611 'cite'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 612 'code'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 613 'col'=>['c'=>"\0\0\10",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 614 'colgroup'=>['c'=>"\0\2",'ac'=>"\0\0\10",'ac19'=>'not(@span)','dd'=>"\0",'nt'=>1,'e'=>1,'e?'=>'@span','b'=>1], 615 'data'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 616 'datalist'=>['c'=>"\5",'ac'=>"\4\0\200\10",'dd'=>"\0"], 617 'dd'=>['c'=>"\0\100\100",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['dd','dt']], 618 'del'=>['c'=>"\5",'ac'=>"\0",'dd'=>"\0",'t'=>1], 619 'details'=>['c'=>"\33",'ac'=>"\1\0\0\2",'dd'=>"\0",'b'=>1,'cp'=>['p']], 620 'dfn'=>['c'=>"\7\0\0\0\40",'ac'=>"\4",'dd'=>"\0\0\0\0\40"], 621 'dialog'=>['c'=>"\21",'ac'=>"\1",'dd'=>"\0",'b'=>1], 622 'div'=>['c'=>"\3\100",'ac'=>"\1\0\300",'ac0'=>'not(ancestor::dl)','dd'=>"\0",'b'=>1,'cp'=>['p']], 623 'dl'=>['c'=>"\3",'c1'=>'dt and dd','ac'=>"\0\100\200",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['p']], 624 'dt'=>['c'=>"\0\100\100",'ac'=>"\1",'dd'=>"\200\4\0\40",'b'=>1,'cp'=>['dd','dt']], 625 'em'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 626 'embed'=>['c'=>"\57",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 627 'fieldset'=>['c'=>"\23\1",'ac'=>"\1\0\0\20",'dd'=>"\0",'b'=>1,'cp'=>['p']], 628 'figcaption'=>['c'=>"\0\0\0\0\0\4",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['p']], 629 'figure'=>['c'=>"\23",'ac'=>"\1\0\0\0\0\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 630 'footer'=>['c'=>"\3\40",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 631 'form'=>['c'=>"\3\0\0\0\20",'ac'=>"\1",'dd'=>"\0\0\0\0\20",'b'=>1,'cp'=>['p']], 632 'h1'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 633 'h2'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 634 'h3'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 635 'h4'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 636 'h5'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 637 'h6'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 638 'head'=>['c'=>"\0\0\2",'ac'=>"\100",'dd'=>"\0",'nt'=>1,'b'=>1], 639 'header'=>['c'=>"\3\40\0\40",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 640 'hr'=>['c'=>"\1",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1,'cp'=>['p']], 641 'html'=>['c'=>"\0",'ac'=>"\0\0\2",'dd'=>"\0",'nt'=>1,'b'=>1], 642 'i'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 643 'iframe'=>['c'=>"\57",'ac'=>"\4",'dd'=>"\0"], 644 'img'=>['c'=>"\57\20\4",'c3'=>'@usemap','ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 645 'input'=>['c'=>"\17\20",'c3'=>'@type!="hidden"','c12'=>'@type!="hidden" or @type="hidden"','c1'=>'@type!="hidden"','ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 646 'ins'=>['c'=>"\7",'ac'=>"\0",'dd'=>"\0",'t'=>1], 647 'kbd'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 648 'label'=>['c'=>"\17\20\0\0\4",'ac'=>"\4",'dd'=>"\0\200\0\0\4"], 649 'legend'=>['c'=>"\0\0\0\20",'ac'=>"\204",'dd'=>"\0",'b'=>1], 650 'li'=>['c'=>"\0\0\0\0\200",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['li']], 651 'link'=>['c'=>"\105",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 652 'main'=>['c'=>"\3\0\0\0\10",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['p']], 653 'mark'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 654 'media element'=>['c'=>"\0\0\0\0\0\2",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'b'=>1], 655 'meta'=>['c'=>"\100",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 656 'meter'=>['c'=>"\7\200\0\0\2",'ac'=>"\4",'dd'=>"\0\0\0\0\2"], 657 'nav'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 658 'object'=>['c'=>"\47\1",'ac'=>"\0\0\0\0\1",'dd'=>"\0",'t'=>1], 659 'ol'=>['c'=>"\3",'c1'=>'li','ac'=>"\0\0\200\0\200",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['p']], 660 'optgroup'=>['c'=>"\0\0\1",'ac'=>"\0\0\200\10",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['optgroup','option']], 661 'option'=>['c'=>"\0\0\1\10",'ac'=>"\0",'dd'=>"\0",'b'=>1,'cp'=>['option']], 662 'output'=>['c'=>"\7\1",'ac'=>"\4",'dd'=>"\0"], 663 'p'=>['c'=>"\3",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 664 'param'=>['c'=>"\0\0\0\0\1",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 665 'picture'=>['c'=>"\45",'ac'=>"\0\0\204",'dd'=>"\0",'nt'=>1], 666 'pre'=>['c'=>"\3",'ac'=>"\4",'dd'=>"\0",'pre'=>1,'b'=>1,'cp'=>['p']], 667 'progress'=>['c'=>"\7\200\0\1",'ac'=>"\4",'dd'=>"\0\0\0\1"], 668 'q'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 669 'rb'=>['c'=>"\0\10",'ac'=>"\4",'dd'=>"\0",'b'=>1], 670 'rp'=>['c'=>"\0\10\40",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['rp','rt']], 671 'rt'=>['c'=>"\0\10\40",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['rp','rt']], 672 'rtc'=>['c'=>"\0\10",'ac'=>"\4\0\40",'dd'=>"\0",'b'=>1], 673 'ruby'=>['c'=>"\7",'ac'=>"\4\10",'dd'=>"\0"], 674 's'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 675 'samp'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 676 'script'=>['c'=>"\105\0\200",'ac'=>"\0",'dd'=>"\0",'to'=>1], 677 'section'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['p']], 678 'select'=>['c'=>"\17\1",'ac'=>"\0\0\201",'dd'=>"\0",'nt'=>1], 679 'small'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 680 'source'=>['c'=>"\0\0\4\4",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 681 'span'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 682 'strong'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 683 'style'=>['c'=>"\101",'ac'=>"\0",'dd'=>"\0",'to'=>1,'b'=>1], 684 'sub'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 685 'summary'=>['c'=>"\0\0\0\2",'ac'=>"\204",'dd'=>"\0",'b'=>1], 686 'sup'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 687 'table'=>['c'=>"\3\0\0\200",'ac'=>"\0\2\200",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['p']], 688 'tbody'=>['c'=>"\0\2",'ac'=>"\0\0\200\0\100",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['tbody','td','th','thead','tr']], 689 'td'=>['c'=>"\20\0\20",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['td','th']], 690 'template'=>['c'=>"\0\0\10",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'b'=>1], 691 'textarea'=>['c'=>"\17\1",'ac'=>"\0",'dd'=>"\0",'pre'=>1,'to'=>1], 692 'tfoot'=>['c'=>"\0\2",'ac'=>"\0\0\200\0\100",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['tbody','td','th','thead','tr']], 693 'th'=>['c'=>"\0\0\20",'ac'=>"\1",'dd'=>"\200\4\0\40",'b'=>1,'cp'=>['td','th']], 694 'thead'=>['c'=>"\0\2",'ac'=>"\0\0\200\0\100",'dd'=>"\0",'nt'=>1,'b'=>1], 695 'time'=>['c'=>"\7",'ac'=>"\4",'ac2'=>'@datetime','dd'=>"\0"], 696 'title'=>['c'=>"\100",'ac'=>"\0",'dd'=>"\0",'to'=>1,'b'=>1], 697 'tr'=>['c'=>"\0\2\0\0\100",'ac'=>"\0\0\220",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['td','th','tr']], 698 'track'=>['c'=>"\0\0\0\100",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 699 'u'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 700 'ul'=>['c'=>"\3",'c1'=>'li','ac'=>"\0\0\200\0\200",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['p']], 701 'var'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 702 'video'=>['c'=>"\57",'c3'=>'@controls','ac'=>"\0\0\0\104",'ac26'=>'not(@src)','dd'=>"\0\0\0\0\0\2",'dd41'=>'@src','t'=>1], 703 'wbr'=>['c'=>"\5",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1] 704 ]; 705 public static function closesParent(DOMElement $child, DOMElement $parent) 706 { 707 $parentName = $parent->nodeName; 708 $childName = $child->nodeName; 709 return !empty(self::$htmlElements[$childName]['cp']) && \in_array($parentName, self::$htmlElements[$childName]['cp'], \true); 710 } 711 public static function disallowsText(DOMElement $element) 712 { 713 return self::hasProperty($element, 'nt'); 714 } 715 public static function getAllowChildBitfield(DOMElement $element) 716 { 717 return self::getBitfield($element, 'ac'); 718 } 719 public static function getCategoryBitfield(DOMElement $element) 720 { 721 return self::getBitfield($element, 'c'); 722 } 723 public static function getDenyDescendantBitfield(DOMElement $element) 724 { 725 return self::getBitfield($element, 'dd'); 726 } 727 public static function isBlock(DOMElement $element) 728 { 729 return self::hasProperty($element, 'b'); 730 } 731 public static function isEmpty(DOMElement $element) 732 { 733 return self::hasProperty($element, 'e'); 734 } 735 public static function isFormattingElement(DOMElement $element) 736 { 737 return self::hasProperty($element, 'fe'); 738 } 739 public static function isTextOnly(DOMElement $element) 740 { 741 return self::hasProperty($element, 'to'); 742 } 743 public static function isTransparent(DOMElement $element) 744 { 745 return self::hasProperty($element, 't'); 746 } 747 public static function isVoid(DOMElement $element) 748 { 749 return self::hasProperty($element, 'v'); 750 } 751 public static function preservesWhitespace(DOMElement $element) 752 { 753 return self::hasProperty($element, 'pre'); 754 } 755 protected static function evaluate($query, DOMElement $element) 756 { 757 $xpath = new DOMXPath($element->ownerDocument); 758 return $xpath->evaluate('boolean(' . $query . ')', $element); 759 } 760 protected static function getBitfield(DOMElement $element, $name) 761 { 762 $props = self::getProperties($element); 763 $bitfield = self::toBin($props[$name]); 764 foreach (\array_keys(\array_filter(\str_split($bitfield, 1))) as $bitNumber) 765 { 766 $conditionName = $name . $bitNumber; 767 if (isset($props[$conditionName]) && !self::evaluate($props[$conditionName], $element)) 768 $bitfield[$bitNumber] = '0'; 769 } 770 return self::toRaw($bitfield); 771 } 772 protected static function getProperties(DOMElement $element) 773 { 774 return (isset(self::$htmlElements[$element->nodeName])) ? self::$htmlElements[$element->nodeName] : self::$htmlElements['span']; 775 } 776 protected static function hasProperty(DOMElement $element, $propName) 777 { 778 $props = self::getProperties($element); 779 return !empty($props[$propName]) && (!isset($props[$propName . '?']) || self::evaluate($props[$propName . '?'], $element)); 780 } 781 protected static function toBin($raw) 782 { 783 $bin = ''; 784 foreach (\str_split($raw, 1) as $char) 785 $bin .= \strrev(\substr('0000000' . \decbin(\ord($char)), -8)); 786 return $bin; 787 } 788 protected static function toRaw($bin) 789 { 790 return \implode('', \array_map('chr', \array_map('bindec', \array_map('strrev', \str_split($bin, 8))))); 791 } 792 } 793 794 /* 795 * @package s9e\TextFormatter 796 * @copyright Copyright (c) 2010-2019 The s9e Authors 797 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 798 */ 799 namespace s9e\TextFormatter\Configurator\Helpers; 800 use DOMDocument; 801 use DOMXPath; 802 abstract class NodeLocator 803 { 804 public static function getAttributesByRegexp(DOMDocument $dom, $regexp) 805 { 806 return self::getNodesByRegexp($dom, $regexp, 'attribute'); 807 } 808 public static function getCSSNodes(DOMDocument $dom) 809 { 810 $regexp = '/^style$/i'; 811 $nodes = \array_merge( 812 self::getAttributesByRegexp($dom, $regexp), 813 self::getElementsByRegexp($dom, '/^style$/i') 814 ); 815 return $nodes; 816 } 817 public static function getElementsByRegexp(DOMDocument $dom, $regexp) 818 { 819 return self::getNodesByRegexp($dom, $regexp, 'element'); 820 } 821 public static function getJSNodes(DOMDocument $dom) 822 { 823 $regexp = '/^(?:data-s9e-livepreview-postprocess$|on)/i'; 824 $nodes = \array_merge( 825 self::getAttributesByRegexp($dom, $regexp), 826 self::getElementsByRegexp($dom, '/^script$/i') 827 ); 828 return $nodes; 829 } 830 public static function getObjectParamsByRegexp(DOMDocument $dom, $regexp) 831 { 832 $xpath = new DOMXPath($dom); 833 $nodes = []; 834 foreach (self::getAttributesByRegexp($dom, $regexp) as $attribute) 835 if ($attribute->nodeType === \XML_ATTRIBUTE_NODE) 836 { 837 if (\strtolower($attribute->parentNode->localName) === 'embed') 838 $nodes[] = $attribute; 839 } 840 elseif ($xpath->evaluate('count(ancestor::embed)', $attribute)) 841 $nodes[] = $attribute; 842 foreach ($xpath->query('//object//param') as $param) 843 if (\preg_match($regexp, $param->getAttribute('name'))) 844 $nodes[] = $param; 845 return $nodes; 846 } 847 public static function getURLNodes(DOMDocument $dom) 848 { 849 $regexp = '/(?:^(?:action|background|c(?:ite|lassid|odebase)|data|formaction|href|icon|longdesc|manifest|p(?:ing|luginspage|oster|rofile)|usemap)|src)$/i'; 850 $nodes = self::getAttributesByRegexp($dom, $regexp); 851 foreach (self::getObjectParamsByRegexp($dom, '/^(?:dataurl|movie)$/i') as $param) 852 { 853 $node = $param->getAttributeNode('value'); 854 if ($node) 855 $nodes[] = $node; 856 } 857 return $nodes; 858 } 859 protected static function getNodes(DOMDocument $dom, $type) 860 { 861 $nodes = []; 862 $prefix = ($type === 'attribute') ? '@' : ''; 863 $xpath = new DOMXPath($dom); 864 foreach ($xpath->query('//' . $prefix . '*') as $node) 865 $nodes[] = [$node, $node->nodeName]; 866 foreach ($xpath->query('//xsl:' . $type) as $node) 867 $nodes[] = [$node, $node->getAttribute('name')]; 868 foreach ($xpath->query('//xsl:copy-of') as $node) 869 if (\preg_match('/^' . $prefix . '(\\w+)$/', $node->getAttribute('select'), $m)) 870 $nodes[] = [$node, $m[1]]; 871 return $nodes; 872 } 873 protected static function getNodesByRegexp(DOMDocument $dom, $regexp, $type) 874 { 875 $nodes = []; 876 foreach (self::getNodes($dom, $type) as $_13697a20) 877 { 878 list($node, $name) = $_13697a20; 879 if (\preg_match($regexp, $name)) 880 $nodes[] = $node; 881 } 882 return $nodes; 883 } 884 } 885 886 /* 887 * @package s9e\TextFormatter 888 * @copyright Copyright (c) 2010-2019 The s9e Authors 889 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 890 */ 891 namespace s9e\TextFormatter\Configurator\Helpers; 892 use RuntimeException; 893 abstract class RegexpBuilder 894 { 895 protected static $characterClassBuilder; 896 public static function fromList(array $words, array $options = []) 897 { 898 if (empty($words)) 899 return ''; 900 $options += [ 901 'delimiter' => '/', 902 'caseInsensitive' => \false, 903 'specialChars' => [], 904 'unicode' => \true, 905 'useLookahead' => \false 906 ]; 907 if ($options['caseInsensitive']) 908 { 909 foreach ($words as &$word) 910 $word = \strtr( 911 $word, 912 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 913 'abcdefghijklmnopqrstuvwxyz' 914 ); 915 unset($word); 916 } 917 $words = \array_unique($words); 918 \sort($words); 919 $initials = []; 920 $esc = $options['specialChars']; 921 $esc += [$options['delimiter'] => '\\' . $options['delimiter']]; 922 $esc += [ 923 '!' => '!', 924 '-' => '-', 925 ':' => ':', 926 '<' => '<', 927 '=' => '=', 928 '>' => '>', 929 '}' => '}' 930 ]; 931 $splitWords = []; 932 foreach ($words as $word) 933 { 934 $regexp = ($options['unicode']) ? '(.)us' : '(.)s'; 935 if (\preg_match_all($regexp, $word, $matches) === \false) 936 throw new RuntimeException("Invalid UTF-8 string '" . $word . "'"); 937 $splitWord = []; 938 foreach ($matches[0] as $pos => $c) 939 { 940 if (!isset($esc[$c])) 941 $esc[$c] = \preg_quote($c); 942 if ($pos === 0) 943 $initials[] = $esc[$c]; 944 $splitWord[] = $esc[$c]; 945 } 946 $splitWords[] = $splitWord; 947 } 948 self::$characterClassBuilder = new CharacterClassBuilder; 949 self::$characterClassBuilder->delimiter = $options['delimiter']; 950 $regexp = self::assemble([self::mergeChains($splitWords)]); 951 if ($options['useLookahead'] 952 && \count($initials) > 1 953 && $regexp[0] !== '[') 954 { 955 $useLookahead = \true; 956 foreach ($initials as $initial) 957 if (!self::canBeUsedInCharacterClass($initial)) 958 { 959 $useLookahead = \false; 960 break; 961 } 962 if ($useLookahead) 963 $regexp = '(?=' . self::generateCharacterClass($initials) . ')' . $regexp; 964 } 965 return $regexp; 966 } 967 protected static function mergeChains(array $chains, $preventRemerge = \false) 968 { 969 if (!isset($chains[1])) 970 return $chains[0]; 971 $mergedChain = self::removeLongestCommonPrefix($chains); 972 if (!isset($chains[0][0]) 973 && !\array_filter($chains)) 974 return $mergedChain; 975 $suffix = self::removeLongestCommonSuffix($chains); 976 if (isset($chains[1])) 977 { 978 self::optimizeDotChains($chains); 979 self::optimizeCatchallChains($chains); 980 } 981 $endOfChain = \false; 982 $remerge = \false; 983 $groups = []; 984 foreach ($chains as $chain) 985 { 986 if (!isset($chain[0])) 987 { 988 $endOfChain = \true; 989 continue; 990 } 991 $head = $chain[0]; 992 if (isset($groups[$head])) 993 $remerge = \true; 994 $groups[$head][] = $chain; 995 } 996 $characterClass = []; 997 foreach ($groups as $head => $groupChains) 998 { 999 $head = (string) $head; 1000 if ($groupChains === [[$head]] 1001 && self::canBeUsedInCharacterClass($head)) 1002 $characterClass[$head] = $head; 1003 } 1004 \sort($characterClass); 1005 if (isset($characterClass[1])) 1006 { 1007 foreach ($characterClass as $char) 1008 unset($groups[$char]); 1009 $head = self::generateCharacterClass($characterClass); 1010 $groups[$head][] = [$head]; 1011 $groups = [$head => $groups[$head]] 1012 + $groups; 1013 } 1014 if ($remerge && !$preventRemerge) 1015 { 1016 $mergedChains = []; 1017 foreach ($groups as $head => $groupChains) 1018 $mergedChains[] = self::mergeChains($groupChains); 1019 self::mergeTails($mergedChains); 1020 $regexp = \implode('', self::mergeChains($mergedChains, \true)); 1021 if ($endOfChain) 1022 $regexp = self::makeRegexpOptional($regexp); 1023 $mergedChain[] = $regexp; 1024 } 1025 else 1026 { 1027 self::mergeTails($chains); 1028 $mergedChain[] = self::assemble($chains); 1029 } 1030 foreach ($suffix as $atom) 1031 $mergedChain[] = $atom; 1032 return $mergedChain; 1033 } 1034 protected static function mergeTails(array &$chains) 1035 { 1036 self::mergeTailsCC($chains); 1037 self::mergeTailsAltern($chains); 1038 $chains = \array_values($chains); 1039 } 1040 protected static function mergeTailsCC(array &$chains) 1041 { 1042 $groups = []; 1043 foreach ($chains as $k => $chain) 1044 if (isset($chain[1]) 1045 && !isset($chain[2]) 1046 && self::canBeUsedInCharacterClass($chain[0])) 1047 $groups[$chain[1]][$k] = $chain; 1048 foreach ($groups as $groupChains) 1049 { 1050 if (\count($groupChains) < 2) 1051 continue; 1052 $chains = \array_diff_key($chains, $groupChains); 1053 $chains[] = self::mergeChains(\array_values($groupChains)); 1054 } 1055 } 1056 protected static function mergeTailsAltern(array &$chains) 1057 { 1058 $groups = []; 1059 foreach ($chains as $k => $chain) 1060 if (!empty($chain)) 1061 { 1062 $tail = \array_slice($chain, -1); 1063 $groups[$tail[0]][$k] = $chain; 1064 } 1065 foreach ($groups as $tail => $groupChains) 1066 { 1067 if (\count($groupChains) < 2) 1068 continue; 1069 $mergedChain = self::mergeChains(\array_values($groupChains)); 1070 $oldLen = 0; 1071 foreach ($groupChains as $groupChain) 1072 $oldLen += \array_sum(\array_map('strlen', $groupChain)); 1073 if ($oldLen <= \array_sum(\array_map('strlen', $mergedChain))) 1074 continue; 1075 $chains = \array_diff_key($chains, $groupChains); 1076 $chains[] = $mergedChain; 1077 } 1078 } 1079 protected static function removeLongestCommonPrefix(array &$chains) 1080 { 1081 $pLen = 0; 1082 while (1) 1083 { 1084 $c = \null; 1085 foreach ($chains as $chain) 1086 { 1087 if (!isset($chain[$pLen])) 1088 break 2; 1089 if (!isset($c)) 1090 { 1091 $c = $chain[$pLen]; 1092 continue; 1093 } 1094 if ($chain[$pLen] !== $c) 1095 break 2; 1096 } 1097 ++$pLen; 1098 } 1099 if (!$pLen) 1100 return []; 1101 $prefix = \array_slice($chains[0], 0, $pLen); 1102 foreach ($chains as &$chain) 1103 $chain = \array_slice($chain, $pLen); 1104 unset($chain); 1105 return $prefix; 1106 } 1107 protected static function removeLongestCommonSuffix(array &$chains) 1108 { 1109 $chainsLen = \array_map('count', $chains); 1110 $maxLen = \min($chainsLen); 1111 if (\max($chainsLen) === $maxLen) 1112 --$maxLen; 1113 $sLen = 0; 1114 while ($sLen < $maxLen) 1115 { 1116 $c = \null; 1117 foreach ($chains as $k => $chain) 1118 { 1119 $pos = $chainsLen[$k] - ($sLen + 1); 1120 if (!isset($c)) 1121 { 1122 $c = $chain[$pos]; 1123 continue; 1124 } 1125 if ($chain[$pos] !== $c) 1126 break 2; 1127 } 1128 ++$sLen; 1129 } 1130 if (!$sLen) 1131 return []; 1132 $suffix = \array_slice($chains[0], -$sLen); 1133 foreach ($chains as &$chain) 1134 $chain = \array_slice($chain, 0, -$sLen); 1135 unset($chain); 1136 return $suffix; 1137 } 1138 protected static function assemble(array $chains) 1139 { 1140 $endOfChain = \false; 1141 $regexps = []; 1142 $characterClass = []; 1143 foreach ($chains as $chain) 1144 { 1145 if (empty($chain)) 1146 { 1147 $endOfChain = \true; 1148 continue; 1149 } 1150 if (!isset($chain[1]) 1151 && self::canBeUsedInCharacterClass($chain[0])) 1152 $characterClass[$chain[0]] = $chain[0]; 1153 else 1154 $regexps[] = \implode('', $chain); 1155 } 1156 if (!empty($characterClass)) 1157 { 1158 \sort($characterClass); 1159 $regexp = (isset($characterClass[1])) 1160 ? self::generateCharacterClass($characterClass) 1161 : $characterClass[0]; 1162 \array_unshift($regexps, $regexp); 1163 } 1164 if (empty($regexps)) 1165 return ''; 1166 if (isset($regexps[1])) 1167 { 1168 $regexp = \implode('|', $regexps); 1169 $regexp = ((self::canUseAtomicGrouping($regexp)) ? '(?>' : '(?:') . $regexp . ')'; 1170 } 1171 else 1172 $regexp = $regexps[0]; 1173 if ($endOfChain) 1174 $regexp = self::makeRegexpOptional($regexp); 1175 return $regexp; 1176 } 1177 protected static function makeRegexpOptional($regexp) 1178 { 1179 if (\preg_match('#^\\.\\+\\??$#', $regexp)) 1180 return \str_replace('+', '*', $regexp); 1181 if (\preg_match('#^(\\\\?.)((?:\\1\\?)+)$#Du', $regexp, $m)) 1182 return $m[1] . '?' . $m[2]; 1183 if (\preg_match('#^(?:[$^]|\\\\[bBAZzGQEK])$#', $regexp)) 1184 return ''; 1185 if (\preg_match('#^\\\\?.$#Dus', $regexp)) 1186 $isAtomic = \true; 1187 elseif (\preg_match('#^[^[(].#s', $regexp)) 1188 $isAtomic = \false; 1189 else 1190 { 1191 $def = RegexpParser::parse('#' . $regexp . '#'); 1192 $tokens = $def['tokens']; 1193 switch (\count($tokens)) 1194 { 1195 case 1: 1196 $startPos = $tokens[0]['pos']; 1197 $len = $tokens[0]['len']; 1198 $isAtomic = (bool) ($startPos === 0 && $len === \strlen($regexp)); 1199 if ($isAtomic && $tokens[0]['type'] === 'characterClass') 1200 { 1201 $regexp = \rtrim($regexp, '+*?'); 1202 if (!empty($tokens[0]['quantifiers']) && $tokens[0]['quantifiers'] !== '?') 1203 $regexp .= '*'; 1204 } 1205 break; 1206 case 2: 1207 if ($tokens[0]['type'] === 'nonCapturingSubpatternStart' 1208 && $tokens[1]['type'] === 'nonCapturingSubpatternEnd') 1209 { 1210 $startPos = $tokens[0]['pos']; 1211 $len = $tokens[1]['pos'] + $tokens[1]['len']; 1212 $isAtomic = (bool) ($startPos === 0 && $len === \strlen($regexp)); 1213 break; 1214 } 1215 default: 1216 $isAtomic = \false; 1217 } 1218 } 1219 if (!$isAtomic) 1220 $regexp = ((self::canUseAtomicGrouping($regexp)) ? '(?>' : '(?:') . $regexp . ')'; 1221 $regexp .= '?'; 1222 return $regexp; 1223 } 1224 protected static function generateCharacterClass(array $chars) 1225 { 1226 return self::$characterClassBuilder->fromList($chars); 1227 } 1228 protected static function canBeUsedInCharacterClass($char) 1229 { 1230 if (\preg_match('#^\\\\[aefnrtdDhHsSvVwW]$#D', $char)) 1231 return \true; 1232 if (\preg_match('#^\\\\[^A-Za-z0-9]$#Dus', $char)) 1233 return \true; 1234 if (\preg_match('#..#Dus', $char)) 1235 return \false; 1236 if (\preg_quote($char) !== $char 1237 && !\preg_match('#^[-!:<=>}]$#D', $char)) 1238 return \false; 1239 return \true; 1240 } 1241 protected static function optimizeDotChains(array &$chains) 1242 { 1243 $validAtoms = [ 1244 '\\d' => 1, '\\D' => 1, '\\h' => 1, '\\H' => 1, 1245 '\\s' => 1, '\\S' => 1, '\\v' => 1, '\\V' => 1, 1246 '\\w' => 1, '\\W' => 1, 1247 '\\^' => 1, '\\$' => 1, '\\.' => 1, '\\?' => 1, 1248 '\\[' => 1, '\\]' => 1, '\\(' => 1, '\\)' => 1, 1249 '\\+' => 1, '\\*' => 1, '\\\\' => 1 1250 ]; 1251 do 1252 { 1253 $hasMoreDots = \false; 1254 foreach ($chains as $k1 => $dotChain) 1255 { 1256 $dotKeys = \array_keys($dotChain, '.?', \true); 1257 if (!empty($dotKeys)) 1258 { 1259 $dotChain[$dotKeys[0]] = '.'; 1260 $chains[$k1] = $dotChain; 1261 \array_splice($dotChain, $dotKeys[0], 1); 1262 $chains[] = $dotChain; 1263 if (isset($dotKeys[1])) 1264 $hasMoreDots = \true; 1265 } 1266 } 1267 } 1268 while ($hasMoreDots); 1269 foreach ($chains as $k1 => $dotChain) 1270 { 1271 $dotKeys = \array_keys($dotChain, '.', \true); 1272 if (empty($dotKeys)) 1273 continue; 1274 foreach ($chains as $k2 => $tmpChain) 1275 { 1276 if ($k2 === $k1) 1277 continue; 1278 foreach ($dotKeys as $dotKey) 1279 { 1280 if (!isset($tmpChain[$dotKey])) 1281 continue 2; 1282 if (!\preg_match('#^.$#Du', \preg_quote($tmpChain[$dotKey])) 1283 && !isset($validAtoms[$tmpChain[$dotKey]])) 1284 continue 2; 1285 $tmpChain[$dotKey] = '.'; 1286 } 1287 if ($tmpChain === $dotChain) 1288 unset($chains[$k2]); 1289 } 1290 } 1291 } 1292 protected static function optimizeCatchallChains(array &$chains) 1293 { 1294 $precedence = [ 1295 '.*' => 3, 1296 '.*?' => 2, 1297 '.+' => 1, 1298 '.+?' => 0 1299 ]; 1300 $tails = []; 1301 foreach ($chains as $k => $chain) 1302 { 1303 if (!isset($chain[0])) 1304 continue; 1305 $head = $chain[0]; 1306 if (!isset($precedence[$head])) 1307 continue; 1308 $tail = \implode('', \array_slice($chain, 1)); 1309 if (!isset($tails[$tail]) 1310 || $precedence[$head] > $tails[$tail]['precedence']) 1311 $tails[$tail] = [ 1312 'key' => $k, 1313 'precedence' => $precedence[$head] 1314 ]; 1315 } 1316 $catchallChains = []; 1317 foreach ($tails as $tail => $info) 1318 $catchallChains[$info['key']] = $chains[$info['key']]; 1319 foreach ($catchallChains as $k1 => $catchallChain) 1320 { 1321 $headExpr = $catchallChain[0]; 1322 $tailExpr = \false; 1323 $match = \array_slice($catchallChain, 1); 1324 if (isset($catchallChain[1]) 1325 && isset($precedence[\end($catchallChain)])) 1326 $tailExpr = \array_pop($match); 1327 $matchCnt = \count($match); 1328 foreach ($chains as $k2 => $chain) 1329 { 1330 if ($k2 === $k1) 1331 continue; 1332 $start = 0; 1333 $end = \count($chain); 1334 if ($headExpr[1] === '+') 1335 { 1336 $found = \false; 1337 foreach ($chain as $start => $atom) 1338 if (self::matchesAtLeastOneCharacter($atom)) 1339 { 1340 $found = \true; 1341 break; 1342 } 1343 if (!$found) 1344 continue; 1345 } 1346 if ($tailExpr === \false) 1347 $end = $start; 1348 else 1349 { 1350 if ($tailExpr[1] === '+') 1351 { 1352 $found = \false; 1353 while (--$end > $start) 1354 if (self::matchesAtLeastOneCharacter($chain[$end])) 1355 { 1356 $found = \true; 1357 break; 1358 } 1359 if (!$found) 1360 continue; 1361 } 1362 $end -= $matchCnt; 1363 } 1364 while ($start <= $end) 1365 { 1366 if (\array_slice($chain, $start, $matchCnt) === $match) 1367 { 1368 unset($chains[$k2]); 1369 break; 1370 } 1371 ++$start; 1372 } 1373 } 1374 } 1375 } 1376 protected static function matchesAtLeastOneCharacter($expr) 1377 { 1378 if (\preg_match('#^[$*?^]$#', $expr)) 1379 return \false; 1380 if (\preg_match('#^.$#u', $expr)) 1381 return \true; 1382 if (\preg_match('#^.\\+#u', $expr)) 1383 return \true; 1384 if (\preg_match('#^\\\\[^bBAZzGQEK1-9](?![*?])#', $expr)) 1385 return \true; 1386 return \false; 1387 } 1388 protected static function canUseAtomicGrouping($expr) 1389 { 1390 if (\preg_match('#(?<!\\\\)(?>\\\\\\\\)*\\.#', $expr)) 1391 return \false; 1392 if (\preg_match('#(?<!\\\\)(?>\\\\\\\\)*[+*]#', $expr)) 1393 return \false; 1394 if (\preg_match('#(?<!\\\\)(?>\\\\\\\\)*\\(?(?<!\\()\\?#', $expr)) 1395 return \false; 1396 if (\preg_match('#(?<!\\\\)(?>\\\\\\\\)*\\\\[a-z0-9]#', $expr)) 1397 return \false; 1398 return \true; 1399 } 1400 } 1401 1402 /* 1403 * @package s9e\TextFormatter 1404 * @copyright Copyright (c) 2010-2019 The s9e Authors 1405 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 1406 */ 1407 namespace s9e\TextFormatter\Configurator\Helpers; 1408 use s9e\TextFormatter\Configurator\Collections\Ruleset; 1409 use s9e\TextFormatter\Configurator\Collections\TagCollection; 1410 abstract class RulesHelper 1411 { 1412 public static function getBitfields(TagCollection $tags, Ruleset $rootRules) 1413 { 1414 $rules = ['*root*' => \iterator_to_array($rootRules)]; 1415 foreach ($tags as $tagName => $tag) 1416 $rules[$tagName] = \iterator_to_array($tag->rules); 1417 $matrix = self::unrollRules($rules); 1418 self::pruneMatrix($matrix); 1419 $groupedTags = []; 1420 foreach (\array_keys($matrix) as $tagName) 1421 { 1422 if ($tagName === '*root*') 1423 continue; 1424 $k = ''; 1425 foreach ($matrix as $tagMatrix) 1426 { 1427 $k .= $tagMatrix['allowedChildren'][$tagName]; 1428 $k .= $tagMatrix['allowedDescendants'][$tagName]; 1429 } 1430 $groupedTags[$k][] = $tagName; 1431 } 1432 $bitTag = []; 1433 $bitNumber = 0; 1434 $tagsConfig = []; 1435 foreach ($groupedTags as $tagNames) 1436 { 1437 foreach ($tagNames as $tagName) 1438 { 1439 $tagsConfig[$tagName]['bitNumber'] = $bitNumber; 1440 $bitTag[$bitNumber] = $tagName; 1441 } 1442 ++$bitNumber; 1443 } 1444 foreach ($matrix as $tagName => $tagMatrix) 1445 { 1446 $allowedChildren = ''; 1447 $allowedDescendants = ''; 1448 foreach ($bitTag as $targetName) 1449 { 1450 $allowedChildren .= $tagMatrix['allowedChildren'][$targetName]; 1451 $allowedDescendants .= $tagMatrix['allowedDescendants'][$targetName]; 1452 } 1453 $tagsConfig[$tagName]['allowed'] = self::pack($allowedChildren, $allowedDescendants); 1454 } 1455 $return = [ 1456 'root' => $tagsConfig['*root*'], 1457 'tags' => $tagsConfig 1458 ]; 1459 unset($return['tags']['*root*']); 1460 return $return; 1461 } 1462 protected static function initMatrix(array $rules) 1463 { 1464 $matrix = []; 1465 $tagNames = \array_keys($rules); 1466 foreach ($rules as $tagName => $tagRules) 1467 { 1468 $matrix[$tagName]['allowedChildren'] = \array_fill_keys($tagNames, 0); 1469 $matrix[$tagName]['allowedDescendants'] = \array_fill_keys($tagNames, 0); 1470 } 1471 return $matrix; 1472 } 1473 protected static function applyTargetedRule(array &$matrix, $rules, $ruleName, $key, $value) 1474 { 1475 foreach ($rules as $tagName => $tagRules) 1476 { 1477 if (!isset($tagRules[$ruleName])) 1478 continue; 1479 foreach ($tagRules[$ruleName] as $targetName) 1480 $matrix[$tagName][$key][$targetName] = $value; 1481 } 1482 } 1483 protected static function unrollRules(array $rules) 1484 { 1485 $matrix = self::initMatrix($rules); 1486 $tagNames = \array_keys($rules); 1487 foreach ($rules as $tagName => $tagRules) 1488 { 1489 if (!empty($tagRules['ignoreTags'])) 1490 { 1491 $rules[$tagName]['denyChild'] = $tagNames; 1492 $rules[$tagName]['denyDescendant'] = $tagNames; 1493 } 1494 if (!empty($tagRules['requireParent'])) 1495 { 1496 $denyParents = \array_diff($tagNames, $tagRules['requireParent']); 1497 foreach ($denyParents as $parentName) 1498 $rules[$parentName]['denyChild'][] = $tagName; 1499 } 1500 } 1501 self::applyTargetedRule($matrix, $rules, 'allowChild', 'allowedChildren', 1); 1502 self::applyTargetedRule($matrix, $rules, 'allowDescendant', 'allowedDescendants', 1); 1503 self::applyTargetedRule($matrix, $rules, 'denyChild', 'allowedChildren', 0); 1504 self::applyTargetedRule($matrix, $rules, 'denyDescendant', 'allowedDescendants', 0); 1505 return $matrix; 1506 } 1507 protected static function pruneMatrix(array &$matrix) 1508 { 1509 $usableTags = ['*root*' => 1]; 1510 $parentTags = $usableTags; 1511 do 1512 { 1513 $nextTags = []; 1514 foreach (\array_keys($parentTags) as $tagName) 1515 $nextTags += \array_filter($matrix[$tagName]['allowedChildren']); 1516 $parentTags = \array_diff_key($nextTags, $usableTags); 1517 $parentTags = \array_intersect_key($parentTags, $matrix); 1518 $usableTags += $parentTags; 1519 } 1520 while (!empty($parentTags)); 1521 $matrix = \array_intersect_key($matrix, $usableTags); 1522 unset($usableTags['*root*']); 1523 foreach ($matrix as $tagName => &$tagMatrix) 1524 { 1525 $tagMatrix['allowedChildren'] 1526 = \array_intersect_key($tagMatrix['allowedChildren'], $usableTags); 1527 $tagMatrix['allowedDescendants'] 1528 = \array_intersect_key($tagMatrix['allowedDescendants'], $usableTags); 1529 } 1530 unset($tagMatrix); 1531 } 1532 protected static function pack($allowedChildren, $allowedDescendants) 1533 { 1534 $allowedChildren = \str_split($allowedChildren, 8); 1535 $allowedDescendants = \str_split($allowedDescendants, 8); 1536 $allowed = []; 1537 foreach (\array_keys($allowedChildren) as $k) 1538 $allowed[] = \bindec(\sprintf( 1539 '%1$08s%2$08s', 1540 \strrev($allowedDescendants[$k]), 1541 \strrev($allowedChildren[$k]) 1542 )); 1543 return $allowed; 1544 } 1545 } 1546 1547 /* 1548 * @package s9e\TextFormatter 1549 * @copyright Copyright (c) 2010-2019 The s9e Authors 1550 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 1551 */ 1552 namespace s9e\TextFormatter\Configurator\Helpers; 1553 use DOMAttr; 1554 use DOMCharacterData; 1555 use DOMDocument; 1556 use DOMElement; 1557 use DOMNode; 1558 use DOMProcessingInstruction; 1559 use DOMText; 1560 use DOMXPath; 1561 abstract class TemplateHelper 1562 { 1563 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; 1564 public static function getAttributesByRegexp(DOMDocument $dom, $regexp) 1565 { 1566 return NodeLocator::getAttributesByRegexp($dom, $regexp); 1567 } 1568 public static function getCSSNodes(DOMDocument $dom) 1569 { 1570 return NodeLocator::getCSSNodes($dom); 1571 } 1572 public static function getElementsByRegexp(DOMDocument $dom, $regexp) 1573 { 1574 return NodeLocator::getElementsByRegexp($dom, $regexp); 1575 } 1576 public static function getJSNodes(DOMDocument $dom) 1577 { 1578 return NodeLocator::getJSNodes($dom); 1579 } 1580 public static function getObjectParamsByRegexp(DOMDocument $dom, $regexp) 1581 { 1582 return NodeLocator::getObjectParamsByRegexp($dom, $regexp); 1583 } 1584 public static function getParametersFromXSL($xsl) 1585 { 1586 $paramNames = []; 1587 $xpath = new DOMXPath(TemplateLoader::load($xsl)); 1588 $query = '//xsl:*/@match | //xsl:*/@select | //xsl:*/@test'; 1589 foreach ($xpath->query($query) as $attribute) 1590 { 1591 $expr = $attribute->value; 1592 $paramNames += \array_flip(self::getParametersFromExpression($attribute, $expr)); 1593 } 1594 $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{")]'; 1595 foreach ($xpath->query($query) as $attribute) 1596 foreach (AVTHelper::parse($attribute->value) as $token) 1597 if ($token[0] === 'expression') 1598 { 1599 $expr = $token[1]; 1600 $paramNames += \array_flip(self::getParametersFromExpression($attribute, $expr)); 1601 } 1602 \ksort($paramNames); 1603 return \array_keys($paramNames); 1604 } 1605 public static function getURLNodes(DOMDocument $dom) 1606 { 1607 return NodeLocator::getURLNodes($dom); 1608 } 1609 public static function highlightNode(DOMNode $node, $prepend, $append) 1610 { 1611 $dom = $node->ownerDocument->cloneNode(\true); 1612 $dom->formatOutput = \true; 1613 $xpath = new DOMXPath($dom); 1614 $node = $xpath->query($node->getNodePath())->item(0); 1615 $uniqid = \uniqid('_'); 1616 if ($node instanceof DOMAttr) 1617 $node->value .= $uniqid; 1618 elseif ($node instanceof DOMElement) 1619 $node->setAttribute($uniqid, ''); 1620 elseif ($node instanceof DOMCharacterData || $node instanceof DOMProcessingInstruction) 1621 $node->data .= $uniqid; 1622 $docXml = TemplateLoader::innerXML($dom->documentElement); 1623 $docXml = \trim(\str_replace("\n ", "\n", $docXml)); 1624 $nodeHtml = \htmlspecialchars(\trim($dom->saveXML($node))); 1625 $docHtml = \htmlspecialchars($docXml); 1626 $html = \str_replace($nodeHtml, $prepend . $nodeHtml . $append, $docHtml); 1627 $html = \str_replace(' ' . $uniqid . '=""', '', $html); 1628 $html = \str_replace($uniqid, '', $html); 1629 return $html; 1630 } 1631 public static function loadTemplate($template) 1632 { 1633 return TemplateLoader::load($template); 1634 } 1635 public static function replaceHomogeneousTemplates(array &$templates, $minCount = 3) 1636 { 1637 $expr = 'name()'; 1638 $tagNames = []; 1639 foreach ($templates as $tagName => $template) 1640 { 1641 $elName = \strtolower(\preg_replace('/^[^:]+:/', '', $tagName)); 1642 if ($template === '<' . $elName . '><xsl:apply-templates/></' . $elName . '>') 1643 { 1644 $tagNames[] = $tagName; 1645 if (\strpos($tagName, ':') !== \false) 1646 $expr = 'local-name()'; 1647 } 1648 } 1649 if (\count($tagNames) < $minCount) 1650 return; 1651 $chars = \preg_replace('/[^A-Z]+/', '', \count_chars(\implode('', $tagNames), 3)); 1652 if ($chars > '') 1653 $expr = 'translate(' . $expr . ",'" . $chars . "','" . \strtolower($chars) . "')"; 1654 $template = '<xsl:element name="{' . $expr . '}"><xsl:apply-templates/></xsl:element>'; 1655 foreach ($tagNames as $tagName) 1656 $templates[$tagName] = $template; 1657 } 1658 public static function replaceTokens($template, $regexp, $fn) 1659 { 1660 return TemplateModifier::replaceTokens($template, $regexp, $fn); 1661 } 1662 public static function saveTemplate(DOMDocument $dom) 1663 { 1664 return TemplateLoader::save($dom); 1665 } 1666 protected static function getParametersFromExpression(DOMNode $node, $expr) 1667 { 1668 $varNames = XPathHelper::getVariables($expr); 1669 $paramNames = []; 1670 $xpath = new DOMXPath($node->ownerDocument); 1671 foreach ($varNames as $name) 1672 { 1673 $query = 'ancestor-or-self::*/preceding-sibling::xsl:variable[@name="' . $name . '"]'; 1674 if (!$xpath->query($query, $node)->length) 1675 $paramNames[] = $name; 1676 } 1677 return $paramNames; 1678 } 1679 } 1680 1681 /* 1682 * @package s9e\TextFormatter 1683 * @copyright Copyright (c) 2010-2019 The s9e Authors 1684 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 1685 */ 1686 namespace s9e\TextFormatter\Configurator\Helpers; 1687 use DOMElement; 1688 use DOMXPath; 1689 class TemplateInspector 1690 { 1691 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; 1692 protected $allowChildBitfields = []; 1693 protected $allowsChildElements; 1694 protected $allowsText; 1695 protected $branches; 1696 protected $contentBitfield = "\0"; 1697 protected $defaultBranchBitfield; 1698 protected $denyDescendantBitfield = "\0"; 1699 protected $dom; 1700 protected $hasElements = \false; 1701 protected $hasRootText; 1702 protected $isBlock = \false; 1703 protected $isEmpty; 1704 protected $isFormattingElement; 1705 protected $isPassthrough = \false; 1706 protected $isTransparent = \false; 1707 protected $isVoid; 1708 protected $leafNodes = []; 1709 protected $preservesNewLines = \false; 1710 protected $rootBitfields = []; 1711 protected $rootNodes = []; 1712 protected $xpath; 1713 public function __construct($template) 1714 { 1715 $this->dom = TemplateHelper::loadTemplate($template); 1716 $this->xpath = new DOMXPath($this->dom); 1717 $this->defaultBranchBitfield = ElementInspector::getAllowChildBitfield($this->dom->createElement('div')); 1718 $this->analyseRootNodes(); 1719 $this->analyseBranches(); 1720 $this->analyseContent(); 1721 } 1722 public function allowsChild(TemplateInspector $child) 1723 { 1724 if (!$this->allowsDescendant($child)) 1725 return \false; 1726 foreach ($child->rootBitfields as $rootBitfield) 1727 foreach ($this->allowChildBitfields as $allowChildBitfield) 1728 if (!self::match($rootBitfield, $allowChildBitfield)) 1729 return \false; 1730 return ($this->allowsText || !$child->hasRootText); 1731 } 1732 public function allowsDescendant(TemplateInspector $descendant) 1733 { 1734 if (self::match($descendant->contentBitfield, $this->denyDescendantBitfield)) 1735 return \false; 1736 return ($this->allowsChildElements || !$descendant->hasElements); 1737 } 1738 public function allowsChildElements() 1739 { 1740 return $this->allowsChildElements; 1741 } 1742 public function allowsText() 1743 { 1744 return $this->allowsText; 1745 } 1746 public function closesParent(TemplateInspector $parent) 1747 { 1748 foreach ($this->rootNodes as $rootNode) 1749 foreach ($parent->leafNodes as $leafNode) 1750 if (ElementInspector::closesParent($rootNode, $leafNode)) 1751 return \true; 1752 return \false; 1753 } 1754 public function evaluate($expr, DOMElement $node = \null) 1755 { 1756 return $this->xpath->evaluate($expr, $node); 1757 } 1758 public function isBlock() 1759 { 1760 return $this->isBlock; 1761 } 1762 public function isFormattingElement() 1763 { 1764 return $this->isFormattingElement; 1765 } 1766 public function isEmpty() 1767 { 1768 return $this->isEmpty; 1769 } 1770 public function isPassthrough() 1771 { 1772 return $this->isPassthrough; 1773 } 1774 public function isTransparent() 1775 { 1776 return $this->isTransparent; 1777 } 1778 public function isVoid() 1779 { 1780 return $this->isVoid; 1781 } 1782 public function preservesNewLines() 1783 { 1784 return $this->preservesNewLines; 1785 } 1786 protected function analyseContent() 1787 { 1788 $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]'; 1789 foreach ($this->xpath->query($query) as $node) 1790 { 1791 $this->contentBitfield |= ElementInspector::getCategoryBitfield($node); 1792 $this->hasElements = \true; 1793 } 1794 $this->isPassthrough = (bool) $this->evaluate('count(//xsl:apply-templates)'); 1795 } 1796 protected function analyseRootNodes() 1797 { 1798 $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"][not(ancestor::*[namespace-uri() != "' . self::XMLNS_XSL . '"])]'; 1799 foreach ($this->xpath->query($query) as $node) 1800 { 1801 $this->rootNodes[] = $node; 1802 if ($this->elementIsBlock($node)) 1803 $this->isBlock = \true; 1804 $this->rootBitfields[] = ElementInspector::getCategoryBitfield($node); 1805 } 1806 $predicate = '[not(ancestor::*[namespace-uri() != "' . self::XMLNS_XSL . '"])]'; 1807 $predicate .= '[not(ancestor::xsl:attribute | ancestor::xsl:comment | ancestor::xsl:variable)]'; 1808 $query = '//text()[normalize-space() != ""]' . $predicate 1809 . '|//xsl:text[normalize-space() != ""]' . $predicate 1810 . '|//xsl:value-of' . $predicate; 1811 $this->hasRootText = (bool) $this->evaluate('count(' . $query . ')'); 1812 } 1813 protected function analyseBranches() 1814 { 1815 $this->branches = []; 1816 foreach ($this->xpath->query('//xsl:apply-templates') as $applyTemplates) 1817 { 1818 $query = 'ancestor::*[namespace-uri() != "' . self::XMLNS_XSL . '"]'; 1819 $this->branches[] = \iterator_to_array($this->xpath->query($query, $applyTemplates)); 1820 } 1821 $this->computeAllowsChildElements(); 1822 $this->computeAllowsText(); 1823 $this->computeBitfields(); 1824 $this->computeFormattingElement(); 1825 $this->computeIsEmpty(); 1826 $this->computeIsTransparent(); 1827 $this->computeIsVoid(); 1828 $this->computePreservesNewLines(); 1829 $this->storeLeafNodes(); 1830 } 1831 protected function anyBranchHasProperty($methodName) 1832 { 1833 foreach ($this->branches as $branch) 1834 foreach ($branch as $element) 1835 if (ElementInspector::$methodName($element)) 1836 return \true; 1837 return \false; 1838 } 1839 protected function computeBitfields() 1840 { 1841 if (empty($this->branches)) 1842 { 1843 $this->allowChildBitfields = ["\0"]; 1844 return; 1845 } 1846 foreach ($this->branches as $branch) 1847 { 1848 $branchBitfield = $this->defaultBranchBitfield; 1849 foreach ($branch as $element) 1850 { 1851 if (!ElementInspector::isTransparent($element)) 1852 $branchBitfield = "\0"; 1853 $branchBitfield |= ElementInspector::getAllowChildBitfield($element); 1854 $this->denyDescendantBitfield |= ElementInspector::getDenyDescendantBitfield($element); 1855 } 1856 $this->allowChildBitfields[] = $branchBitfield; 1857 } 1858 } 1859 protected function computeAllowsChildElements() 1860 { 1861 $this->allowsChildElements = ($this->anyBranchHasProperty('isTextOnly')) ? \false : !empty($this->branches); 1862 } 1863 protected function computeAllowsText() 1864 { 1865 foreach (\array_filter($this->branches) as $branch) 1866 if (ElementInspector::disallowsText(\end($branch))) 1867 { 1868 $this->allowsText = \false; 1869 return; 1870 } 1871 $this->allowsText = \true; 1872 } 1873 protected function computeFormattingElement() 1874 { 1875 foreach ($this->branches as $branch) 1876 foreach ($branch as $element) 1877 if (!ElementInspector::isFormattingElement($element) && !$this->isFormattingSpan($element)) 1878 { 1879 $this->isFormattingElement = \false; 1880 return; 1881 } 1882 $this->isFormattingElement = (bool) \count(\array_filter($this->branches)); 1883 } 1884 protected function computeIsEmpty() 1885 { 1886 $this->isEmpty = ($this->anyBranchHasProperty('isEmpty')) || empty($this->branches); 1887 } 1888 protected function computeIsTransparent() 1889 { 1890 foreach ($this->branches as $branch) 1891 foreach ($branch as $element) 1892 if (!ElementInspector::isTransparent($element)) 1893 { 1894 $this->isTransparent = \false; 1895 return; 1896 } 1897 $this->isTransparent = !empty($this->branches); 1898 } 1899 protected function computeIsVoid() 1900 { 1901 $this->isVoid = ($this->anyBranchHasProperty('isVoid')) || empty($this->branches); 1902 } 1903 protected function computePreservesNewLines() 1904 { 1905 foreach ($this->branches as $branch) 1906 { 1907 $style = ''; 1908 foreach ($branch as $element) 1909 $style .= $this->getStyle($element, \true); 1910 if (\preg_match('(.*white-space\\s*:\\s*(no|pre))is', $style, $m) && \strtolower($m[1]) === 'pre') 1911 { 1912 $this->preservesNewLines = \true; 1913 return; 1914 } 1915 } 1916 $this->preservesNewLines = \false; 1917 } 1918 protected function elementIsBlock(DOMElement $element) 1919 { 1920 $style = $this->getStyle($element); 1921 if (\preg_match('(\\bdisplay\\s*:\\s*block)i', $style)) 1922 return \true; 1923 if (\preg_match('(\\bdisplay\\s*:\\s*(?:inli|no)ne)i', $style)) 1924 return \false; 1925 return ElementInspector::isBlock($element); 1926 } 1927 protected function getStyle(DOMElement $node, $deep = \false) 1928 { 1929 $style = ''; 1930 if (ElementInspector::preservesWhitespace($node)) 1931 $style .= 'white-space:pre;'; 1932 $style .= $node->getAttribute('style'); 1933 $query = (($deep) ? './/' : './') . 'xsl:attribute[@name="style"]'; 1934 foreach ($this->xpath->query($query, $node) as $attribute) 1935 $style .= ';' . $attribute->textContent; 1936 return $style; 1937 } 1938 protected function isFormattingSpan(DOMElement $node) 1939 { 1940 if ($node->nodeName !== 'span') 1941 return \false; 1942 if ($node->getAttribute('class') === '' && $node->getAttribute('style') === '') 1943 return \false; 1944 foreach ($node->attributes as $attrName => $attribute) 1945 if ($attrName !== 'class' && $attrName !== 'style') 1946 return \false; 1947 return \true; 1948 } 1949 protected function storeLeafNodes() 1950 { 1951 foreach (\array_filter($this->branches) as $branch) 1952 $this->leafNodes[] = \end($branch); 1953 } 1954 protected static function match($bitfield1, $bitfield2) 1955 { 1956 return (\trim($bitfield1 & $bitfield2, "\0") !== ''); 1957 } 1958 } 1959 1960 /* 1961 * @package s9e\TextFormatter 1962 * @copyright Copyright (c) 2010-2019 The s9e Authors 1963 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 1964 */ 1965 namespace s9e\TextFormatter\Configurator\Helpers; 1966 use DOMDocument; 1967 use DOMElement; 1968 use DOMXPath; 1969 use RuntimeException; 1970 abstract class TemplateLoader 1971 { 1972 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; 1973 public static function innerXML(DOMElement $element) 1974 { 1975 $xml = $element->ownerDocument->saveXML($element); 1976 $pos = 1 + \strpos($xml, '>'); 1977 $len = \strrpos($xml, '<') - $pos; 1978 return ($len < 1) ? '' : \substr($xml, $pos, $len); 1979 } 1980 public static function load($template) 1981 { 1982 $dom = self::loadAsXML($template) ?: self::loadAsXML(self::fixEntities($template)); 1983 if ($dom) 1984 return $dom; 1985 if (\strpos($template, '<xsl:') !== \false) 1986 { 1987 $error = \libxml_get_last_error(); 1988 throw new RuntimeException('Invalid XSL: ' . $error->message); 1989 } 1990 return self::loadAsHTML($template); 1991 } 1992 public static function save(DOMDocument $dom) 1993 { 1994 $xml = self::innerXML($dom->documentElement); 1995 if (\strpos($xml, 'xmlns:xsl') !== \false) 1996 $xml = \preg_replace('((<[^>]+?) xmlns:xsl="' . self::XMLNS_XSL . '")', '$1', $xml); 1997 return $xml; 1998 } 1999 protected static function fixEntities($template) 2000 { 2001 return \preg_replace_callback( 2002 '(&(?!quot;|amp;|apos;|lt;|gt;)\\w+;)', 2003 function ($m) 2004 { 2005 return \html_entity_decode($m[0], \ENT_NOQUOTES, 'UTF-8'); 2006 }, 2007 \preg_replace('(&(?![A-Za-z0-9]+;|#\\d+;|#x[A-Fa-f0-9]+;))', '&', $template) 2008 ); 2009 } 2010 protected static function loadAsHTML($template) 2011 { 2012 $dom = new DOMDocument; 2013 $html = '<?xml version="1.0" encoding="utf-8" ?><html><body><div>' . $template . '</div></body></html>'; 2014 $useErrors = \libxml_use_internal_errors(\true); 2015 $dom->loadHTML($html, \LIBXML_NSCLEAN); 2016 self::removeInvalidAttributes($dom); 2017 \libxml_use_internal_errors($useErrors); 2018 $xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . self::innerXML($dom->documentElement->firstChild->firstChild) . '</xsl:template>'; 2019 $useErrors = \libxml_use_internal_errors(\true); 2020 $dom->loadXML($xml, \LIBXML_NSCLEAN); 2021 \libxml_use_internal_errors($useErrors); 2022 return $dom; 2023 } 2024 protected static function loadAsXML($template) 2025 { 2026 $xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . $template . '</xsl:template>'; 2027 $useErrors = \libxml_use_internal_errors(\true); 2028 $dom = new DOMDocument; 2029 $success = $dom->loadXML($xml, \LIBXML_NSCLEAN); 2030 self::removeInvalidAttributes($dom); 2031 \libxml_use_internal_errors($useErrors); 2032 return ($success) ? $dom : \false; 2033 } 2034 protected static function removeInvalidAttributes(DOMDocument $dom) 2035 { 2036 $xpath = new DOMXPath($dom); 2037 foreach ($xpath->query('//@*') as $attribute) 2038 if (!\preg_match('(^(?:[-\\w]+:)?(?!\\d)[-\\w]+$)D', $attribute->nodeName)) 2039 $attribute->parentNode->removeAttributeNode($attribute); 2040 } 2041 } 2042 2043 /* 2044 * @package s9e\TextFormatter 2045 * @copyright Copyright (c) 2010-2019 The s9e Authors 2046 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2047 */ 2048 namespace s9e\TextFormatter\Configurator\Helpers; 2049 use s9e\TextFormatter\Configurator\Helpers\TemplateParser\Normalizer; 2050 use s9e\TextFormatter\Configurator\Helpers\TemplateParser\Optimizer; 2051 use s9e\TextFormatter\Configurator\Helpers\TemplateParser\Parser; 2052 class TemplateParser 2053 { 2054 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; 2055 public static $voidRegexp = '/^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/Di'; 2056 public static function parse($template) 2057 { 2058 $parser = new Parser(new Normalizer(new Optimizer)); 2059 return $parser->parse($template); 2060 } 2061 public static function parseEqualityExpr($expr) 2062 { 2063 return XPathHelper::parseEqualityExpr($expr); 2064 } 2065 } 2066 2067 /* 2068 * @package s9e\TextFormatter 2069 * @copyright Copyright (c) 2010-2019 The s9e Authors 2070 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2071 */ 2072 namespace s9e\TextFormatter\Configurator\Helpers\TemplateParser; 2073 use DOMDocument; 2074 use DOMElement; 2075 use DOMNode; 2076 use DOMXPath; 2077 abstract class IRProcessor 2078 { 2079 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; 2080 protected $xpath; 2081 protected function appendElement(DOMElement $parentNode, $name, $value = '') 2082 { 2083 return $parentNode->appendChild($parentNode->ownerDocument->createElement($name, $value)); 2084 } 2085 protected function createXPath(DOMDocument $dom) 2086 { 2087 $this->xpath = new DOMXPath($dom); 2088 } 2089 protected function evaluate($expr, DOMNode $node = \null) 2090 { 2091 return (isset($node)) ? $this->xpath->evaluate($expr, $node) : $this->xpath->evaluate($expr); 2092 } 2093 protected function query($query, DOMNode $node = \null) 2094 { 2095 return (isset($node)) ? $this->xpath->query($query, $node) : $this->xpath->query($query); 2096 } 2097 } 2098 2099 /* 2100 * @package s9e\TextFormatter 2101 * @copyright Copyright (c) 2010-2019 The s9e Authors 2102 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2103 */ 2104 namespace s9e\TextFormatter\Configurator\Helpers; 2105 use RuntimeException; 2106 use s9e\TextFormatter\Utils\XPath; 2107 abstract class XPathHelper 2108 { 2109 public static function getVariables($expr) 2110 { 2111 $expr = \preg_replace('/(["\']).*?\\1/s', '$1$1', $expr); 2112 \preg_match_all('/\\$(\\w+)/', $expr, $matches); 2113 $varNames = \array_unique($matches[1]); 2114 \sort($varNames); 2115 return $varNames; 2116 } 2117 public static function isExpressionNumeric($expr) 2118 { 2119 $expr = \strrev(\preg_replace('(\\((?!\\s*(?!vid(?!\\w))\\w))', ' ', \strrev($expr))); 2120 $expr = \str_replace(')', ' ', $expr); 2121 if (\preg_match('(^\\s*([$@][-\\w]++|-?\\.\\d++|-?\\d++(?:\\.\\d++)?)(?>\\s*(?>[-+*]|div)\\s*(?1))++\\s*$)', $expr)) 2122 return \true; 2123 return \false; 2124 } 2125 public static function minify($expr) 2126 { 2127 $old = $expr; 2128 $strings = []; 2129 $expr = \preg_replace_callback( 2130 '/"[^"]*"|\'[^\']*\'/', 2131 function ($m) use (&$strings) 2132 { 2133 $uniqid = '(' . \sha1(\uniqid()) . ')'; 2134 $strings[$uniqid] = $m[0]; 2135 return $uniqid; 2136 }, 2137 \trim($expr) 2138 ); 2139 if (\preg_match('/[\'"]/', $expr)) 2140 throw new RuntimeException("Cannot parse XPath expression '" . $old . "'"); 2141 $expr = \preg_replace('/\\s+/', ' ', $expr); 2142 $expr = \preg_replace('/([-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr); 2143 $expr = \preg_replace('/([^-a-z_0-9]) ([-a-z_0-9])/i', '$1$2', $expr); 2144 $expr = \preg_replace('/(?!- -)([^-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr); 2145 $expr = \preg_replace('/ - ([a-z_0-9])/i', ' -$1', $expr); 2146 $expr = \preg_replace('/((?:^|[ \\(])\\d+) div ?/', '$1div', $expr); 2147 $expr = \preg_replace('/([^-a-z_0-9]div) (?=[$0-9@])/', '$1', $expr); 2148 $expr = \strtr($expr, $strings); 2149 return $expr; 2150 } 2151 public static function parseEqualityExpr($expr) 2152 { 2153 $eq = '(?<equality>(?<key>@[-\\w]+|\\$\\w+|\\.)(?<operator>\\s*=\\s*)(?:(?<literal>(?<string>"[^"]*"|\'[^\']*\')|0|[1-9][0-9]*)|(?<concat>concat\\(\\s*(?&string)\\s*(?:,\\s*(?&string)\\s*)+\\)))|(?:(?<literal>(?&literal))|(?<concat>(?&concat)))(?&operator)(?<key>(?&key)))'; 2154 $regexp = '(^(?J)\\s*' . $eq . '\\s*(?:or\\s*(?&equality)\\s*)*$)'; 2155 if (!\preg_match($regexp, $expr)) 2156 return \false; 2157 \preg_match_all("((?J)$eq)", $expr, $matches, \PREG_SET_ORDER); 2158 $map = []; 2159 foreach ($matches as $m) 2160 { 2161 $key = $m['key']; 2162 $value = (!empty($m['concat'])) 2163 ? self::evaluateConcat($m['concat']) 2164 : self::evaluateLiteral($m['literal']); 2165 $map[$key][] = $value; 2166 } 2167 return $map; 2168 } 2169 protected static function evaluateConcat($expr) 2170 { 2171 \preg_match_all('(\'[^\']*\'|"[^"]*")', $expr, $strings); 2172 $value = ''; 2173 foreach ($strings[0] as $string) 2174 $value .= \substr($string, 1, -1); 2175 return $value; 2176 } 2177 protected static function evaluateLiteral($expr) 2178 { 2179 if ($expr[0] === '"' || $expr[0] === "'") 2180 $expr = \substr($expr, 1, -1); 2181 return $expr; 2182 } 2183 } 2184 2185 /* 2186 * @package s9e\TextFormatter 2187 * @copyright Copyright (c) 2010-2019 The s9e Authors 2188 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2189 */ 2190 namespace s9e\TextFormatter\Configurator\Items; 2191 use DOMDocument; 2192 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 2193 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 2194 use s9e\TextFormatter\Configurator\TemplateNormalizer; 2195 class Template 2196 { 2197 protected $inspector; 2198 protected $isNormalized = \false; 2199 protected $template; 2200 public function __construct($template) 2201 { 2202 $this->template = $template; 2203 } 2204 public function __call($methodName, $args) 2205 { 2206 return \call_user_func_array([$this->getInspector(), $methodName], $args); 2207 } 2208 public function __toString() 2209 { 2210 return $this->template; 2211 } 2212 public function asDOM() 2213 { 2214 $xml = '<xsl:template xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' 2215 . $this->__toString() 2216 . '</xsl:template>'; 2217 $dom = new TemplateDocument($this); 2218 $dom->loadXML($xml); 2219 return $dom; 2220 } 2221 public function getCSSNodes() 2222 { 2223 return TemplateHelper::getCSSNodes($this->asDOM()); 2224 } 2225 public function getInspector() 2226 { 2227 if (!isset($this->inspector)) 2228 $this->inspector = new TemplateInspector($this->__toString()); 2229 return $this->inspector; 2230 } 2231 public function getJSNodes() 2232 { 2233 return TemplateHelper::getJSNodes($this->asDOM()); 2234 } 2235 public function getURLNodes() 2236 { 2237 return TemplateHelper::getURLNodes($this->asDOM()); 2238 } 2239 public function getParameters() 2240 { 2241 return TemplateHelper::getParametersFromXSL($this->__toString()); 2242 } 2243 public function isNormalized($bool = \null) 2244 { 2245 if (isset($bool)) 2246 $this->isNormalized = $bool; 2247 return $this->isNormalized; 2248 } 2249 public function normalize(TemplateNormalizer $templateNormalizer) 2250 { 2251 $this->inspector = \null; 2252 $this->template = $templateNormalizer->normalizeTemplate($this->template); 2253 $this->isNormalized = \true; 2254 } 2255 public function replaceTokens($regexp, $fn) 2256 { 2257 $this->inspector = \null; 2258 $this->template = TemplateHelper::replaceTokens($this->template, $regexp, $fn); 2259 $this->isNormalized = \false; 2260 } 2261 public function setContent($template) 2262 { 2263 $this->inspector = \null; 2264 $this->template = (string) $template; 2265 $this->isNormalized = \false; 2266 } 2267 } 2268 2269 /* 2270 * @package s9e\TextFormatter 2271 * @copyright Copyright (c) 2010-2019 The s9e Authors 2272 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2273 */ 2274 namespace s9e\TextFormatter\Configurator\JavaScript; 2275 use InvalidArgumentException; 2276 class FunctionProvider 2277 { 2278 public static $cache = [ 2279 'addslashes'=>'function(str) 2280 { 2281 return str.replace(/["\'\\\\]/g, \'\\\\$&\').replace(/\\u0000/g, \'\\\\0\'); 2282 }', 2283 'dechex'=>'function(str) 2284 { 2285 return parseInt(str).toString(16); 2286 }', 2287 'intval'=>'function(str) 2288 { 2289 return parseInt(str) || 0; 2290 }', 2291 'ltrim'=>'function(str) 2292 { 2293 return str.replace(/^[ \\n\\r\\t\\0\\x0B]+/g, \'\'); 2294 }', 2295 'mb_strtolower'=>'function(str) 2296 { 2297 return str.toLowerCase(); 2298 }', 2299 'mb_strtoupper'=>'function(str) 2300 { 2301 return str.toUpperCase(); 2302 }', 2303 'mt_rand'=>'function(min, max) 2304 { 2305 return (min + Math.floor(Math.random() * (max + 1 - min))); 2306 }', 2307 'rawurlencode'=>'function(str) 2308 { 2309 return encodeURIComponent(str).replace( 2310 /[!\'()*]/g, 2311 /** 2312 * @param {!string} c 2313 */ 2314 function(c) 2315 { 2316 return \'%\' + c.charCodeAt(0).toString(16).toUpperCase(); 2317 } 2318 ); 2319 }', 2320 'rtrim'=>'function(str) 2321 { 2322 return str.replace(/[ \\n\\r\\t\\0\\x0B]+$/g, \'\'); 2323 }', 2324 'str_rot13'=>'function(str) 2325 { 2326 return str.replace( 2327 /[a-z]/gi, 2328 function(c) 2329 { 2330 return String.fromCharCode(c.charCodeAt(0) + ((c.toLowerCase() < \'n\') ? 13 : -13)); 2331 } 2332 ); 2333 }', 2334 'stripslashes'=>'function(str) 2335 { 2336 // NOTE: this will not correctly transform \\0 into a NULL byte. I consider this a feature 2337 // rather than a bug. There\'s no reason to use NULL bytes in a text. 2338 return str.replace(/\\\\([\\s\\S]?)/g, \'\\\\1\'); 2339 }', 2340 'strrev'=>'function(str) 2341 { 2342 return str.split(\'\').reverse().join(\'\'); 2343 }', 2344 'strtolower'=>'function(str) 2345 { 2346 return str.toLowerCase(); 2347 }', 2348 'strtotime'=>'function(str) 2349 { 2350 return Date.parse(str) / 1000; 2351 }', 2352 'strtoupper'=>'function(str) 2353 { 2354 return str.toUpperCase(); 2355 }', 2356 'trim'=>'function(str) 2357 { 2358 return str.replace(/^[ \\n\\r\\t\\0\\x0B]+/g, \'\').replace(/[ \\n\\r\\t\\0\\x0B]+$/g, \'\'); 2359 }', 2360 'ucfirst'=>'function(str) 2361 { 2362 return str[0].toUpperCase() + str.substr(1); 2363 }', 2364 'ucwords'=>'function(str) 2365 { 2366 return str.replace( 2367 /(?:^|\\s)[a-z]/g, 2368 function(m) 2369 { 2370 return m.toUpperCase() 2371 } 2372 ); 2373 }', 2374 'urldecode'=>'function(str) 2375 { 2376 return decodeURIComponent(str); 2377 }', 2378 'urlencode'=>'function(str) 2379 { 2380 return encodeURIComponent(str); 2381 }' 2382 ]; 2383 public static function get($funcName) 2384 { 2385 if (isset(self::$cache[$funcName])) 2386 return self::$cache[$funcName]; 2387 if (\preg_match('(^[a-z_0-9]+$)D', $funcName)) 2388 { 2389 $filepath = __DIR__ . '/Configurator/JavaScript/functions/' . $funcName . '.js'; 2390 if (\file_exists($filepath)) 2391 return \file_get_contents($filepath); 2392 } 2393 throw new InvalidArgumentException("Unknown function '" . $funcName . "'"); 2394 } 2395 } 2396 2397 /* 2398 * @package s9e\TextFormatter 2399 * @copyright Copyright (c) 2010-2019 The s9e Authors 2400 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2401 */ 2402 namespace s9e\TextFormatter\Configurator; 2403 interface RendererGenerator 2404 { 2405 public function getRenderer(Rendering $rendering); 2406 } 2407 2408 /* 2409 * @package s9e\TextFormatter 2410 * @copyright Copyright (c) 2010-2019 The s9e Authors 2411 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2412 */ 2413 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; 2414 abstract class AbstractOptimizer 2415 { 2416 protected $cnt; 2417 protected $i; 2418 protected $changed; 2419 protected $tokens; 2420 public function optimize($php) 2421 { 2422 $this->reset($php); 2423 $this->optimizeTokens(); 2424 if ($this->changed) 2425 $php = $this->serialize(); 2426 unset($this->tokens); 2427 return $php; 2428 } 2429 abstract protected function optimizeTokens(); 2430 protected function reset($php) 2431 { 2432 $this->tokens = \token_get_all('<?php ' . $php); 2433 $this->i = 0; 2434 $this->cnt = \count($this->tokens); 2435 $this->changed = \false; 2436 } 2437 protected function serialize() 2438 { 2439 unset($this->tokens[0]); 2440 $php = ''; 2441 foreach ($this->tokens as $token) 2442 $php .= (\is_string($token)) ? $token : $token[1]; 2443 return $php; 2444 } 2445 protected function skipToString($str) 2446 { 2447 while (++$this->i < $this->cnt && $this->tokens[$this->i] !== $str); 2448 } 2449 protected function skipWhitespace() 2450 { 2451 while (++$this->i < $this->cnt && $this->tokens[$this->i][0] === \T_WHITESPACE); 2452 } 2453 protected function unindentBlock($start, $end) 2454 { 2455 $this->i = $start; 2456 do 2457 { 2458 if ($this->tokens[$this->i][0] === \T_WHITESPACE || $this->tokens[$this->i][0] === \T_DOC_COMMENT) 2459 $this->tokens[$this->i][1] = \preg_replace("/^\t/m", '', $this->tokens[$this->i][1]); 2460 } 2461 while (++$this->i <= $end); 2462 } 2463 } 2464 2465 /* 2466 * @package s9e\TextFormatter 2467 * @copyright Copyright (c) 2010-2019 The s9e Authors 2468 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2469 */ 2470 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; 2471 class BranchOutputOptimizer 2472 { 2473 protected $cnt; 2474 protected $i; 2475 protected $tokens; 2476 public function optimize(array $tokens) 2477 { 2478 $this->tokens = $tokens; 2479 $this->i = 0; 2480 $this->cnt = \count($this->tokens); 2481 $php = ''; 2482 while (++$this->i < $this->cnt) 2483 if ($this->tokens[$this->i][0] === \T_IF) 2484 $php .= $this->serializeIfBlock($this->parseIfBlock()); 2485 else 2486 $php .= $this->serializeToken($this->tokens[$this->i]); 2487 unset($this->tokens); 2488 return $php; 2489 } 2490 protected function captureOutput() 2491 { 2492 $expressions = []; 2493 while ($this->skipOutputAssignment()) 2494 { 2495 do 2496 { 2497 $expressions[] = $this->captureOutputExpression(); 2498 } 2499 while ($this->tokens[$this->i++] === '.'); 2500 } 2501 return $expressions; 2502 } 2503 protected function captureOutputExpression() 2504 { 2505 $parens = 0; 2506 $php = ''; 2507 do 2508 { 2509 if ($this->tokens[$this->i] === ';') 2510 break; 2511 elseif ($this->tokens[$this->i] === '.' && !$parens) 2512 break; 2513 elseif ($this->tokens[$this->i] === '(') 2514 ++$parens; 2515 elseif ($this->tokens[$this->i] === ')') 2516 --$parens; 2517 $php .= $this->serializeToken($this->tokens[$this->i]); 2518 } 2519 while (++$this->i < $this->cnt); 2520 return $php; 2521 } 2522 protected function captureStructure() 2523 { 2524 $php = ''; 2525 do 2526 { 2527 $php .= $this->serializeToken($this->tokens[$this->i]); 2528 } 2529 while ($this->tokens[++$this->i] !== '{'); 2530 ++$this->i; 2531 return $php; 2532 } 2533 protected function isBranchToken() 2534 { 2535 return \in_array($this->tokens[$this->i][0], [\T_ELSE, \T_ELSEIF, \T_IF], \true); 2536 } 2537 protected function mergeIfBranches(array $branches) 2538 { 2539 $lastBranch = \end($branches); 2540 if ($lastBranch['structure'] === 'else') 2541 { 2542 $before = $this->optimizeBranchesHead($branches); 2543 $after = $this->optimizeBranchesTail($branches); 2544 } 2545 else 2546 $before = $after = []; 2547 $source = ''; 2548 foreach ($branches as $branch) 2549 $source .= $this->serializeBranch($branch); 2550 return [ 2551 'before' => $before, 2552 'source' => $source, 2553 'after' => $after 2554 ]; 2555 } 2556 protected function mergeOutput(array $left, array $right) 2557 { 2558 if (empty($left)) 2559 return $right; 2560 if (empty($right)) 2561 return $left; 2562 $k = \count($left) - 1; 2563 if (\substr($left[$k], -1) === "'" && $right[0][0] === "'") 2564 { 2565 $right[0] = \substr($left[$k], 0, -1) . \substr($right[0], 1); 2566 unset($left[$k]); 2567 } 2568 return \array_merge($left, $right); 2569 } 2570 protected function optimizeBranchesHead(array &$branches) 2571 { 2572 $before = $this->optimizeBranchesOutput($branches, 'head'); 2573 foreach ($branches as &$branch) 2574 { 2575 if ($branch['body'] !== '' || !empty($branch['tail'])) 2576 continue; 2577 $branch['tail'] = \array_reverse($branch['head']); 2578 $branch['head'] = []; 2579 } 2580 unset($branch); 2581 return $before; 2582 } 2583 protected function optimizeBranchesOutput(array &$branches, $which) 2584 { 2585 $expressions = []; 2586 while (isset($branches[0][$which][0])) 2587 { 2588 $expr = $branches[0][$which][0]; 2589 foreach ($branches as $branch) 2590 if (!isset($branch[$which][0]) || $branch[$which][0] !== $expr) 2591 break 2; 2592 $expressions[] = $expr; 2593 foreach ($branches as &$branch) 2594 \array_shift($branch[$which]); 2595 unset($branch); 2596 } 2597 return $expressions; 2598 } 2599 protected function optimizeBranchesTail(array &$branches) 2600 { 2601 return $this->optimizeBranchesOutput($branches, 'tail'); 2602 } 2603 protected function parseBranch() 2604 { 2605 $structure = $this->captureStructure(); 2606 $head = $this->captureOutput(); 2607 $body = ''; 2608 $tail = []; 2609 $braces = 0; 2610 do 2611 { 2612 $tail = $this->mergeOutput($tail, \array_reverse($this->captureOutput())); 2613 if ($this->tokens[$this->i] === '}' && !$braces) 2614 break; 2615 $body .= $this->serializeOutput(\array_reverse($tail)); 2616 $tail = []; 2617 if ($this->tokens[$this->i][0] === \T_IF) 2618 { 2619 $child = $this->parseIfBlock(); 2620 if ($body === '') 2621 $head = $this->mergeOutput($head, $child['before']); 2622 else 2623 $body .= $this->serializeOutput($child['before']); 2624 $body .= $child['source']; 2625 $tail = $child['after']; 2626 } 2627 else 2628 { 2629 $body .= $this->serializeToken($this->tokens[$this->i]); 2630 if ($this->tokens[$this->i] === '{') 2631 ++$braces; 2632 elseif ($this->tokens[$this->i] === '}') 2633 --$braces; 2634 } 2635 } 2636 while (++$this->i < $this->cnt); 2637 return [ 2638 'structure' => $structure, 2639 'head' => $head, 2640 'body' => $body, 2641 'tail' => $tail 2642 ]; 2643 } 2644 protected function parseIfBlock() 2645 { 2646 $branches = []; 2647 do 2648 { 2649 $branches[] = $this->parseBranch(); 2650 } 2651 while (++$this->i < $this->cnt && $this->isBranchToken()); 2652 --$this->i; 2653 return $this->mergeIfBranches($branches); 2654 } 2655 protected function serializeBranch(array $branch) 2656 { 2657 if ($branch['structure'] === 'else' 2658 && $branch['body'] === '' 2659 && empty($branch['head']) 2660 && empty($branch['tail'])) 2661 return ''; 2662 return $branch['structure'] . '{' . $this->serializeOutput($branch['head']) . $branch['body'] . $this->serializeOutput(\array_reverse($branch['tail'])) . '}'; 2663 } 2664 protected function serializeIfBlock(array $block) 2665 { 2666 return $this->serializeOutput($block['before']) . $block['source'] . $this->serializeOutput(\array_reverse($block['after'])); 2667 } 2668 protected function serializeOutput(array $expressions) 2669 { 2670 if (empty($expressions)) 2671 return ''; 2672 return '$this->out.=' . \implode('.', $expressions) . ';'; 2673 } 2674 protected function serializeToken($token) 2675 { 2676 return (\is_array($token)) ? $token[1] : $token; 2677 } 2678 protected function skipOutputAssignment() 2679 { 2680 if ($this->tokens[$this->i ][0] !== \T_VARIABLE 2681 || $this->tokens[$this->i ][1] !== '$this' 2682 || $this->tokens[$this->i + 1][0] !== \T_OBJECT_OPERATOR 2683 || $this->tokens[$this->i + 2][0] !== \T_STRING 2684 || $this->tokens[$this->i + 2][1] !== 'out' 2685 || $this->tokens[$this->i + 3][0] !== \T_CONCAT_EQUAL) 2686 return \false; 2687 $this->i += 4; 2688 return \true; 2689 } 2690 } 2691 2692 /* 2693 * @package s9e\TextFormatter 2694 * @copyright Copyright (c) 2010-2019 The s9e Authors 2695 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2696 */ 2697 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; 2698 class Optimizer 2699 { 2700 public $branchOutputOptimizer; 2701 protected $cnt; 2702 protected $i; 2703 public $maxLoops = 10; 2704 protected $tokens; 2705 public function __construct() 2706 { 2707 $this->branchOutputOptimizer = new BranchOutputOptimizer; 2708 } 2709 public function optimize($php) 2710 { 2711 $this->tokens = \token_get_all('<?php ' . $php); 2712 $this->cnt = \count($this->tokens); 2713 $this->i = 0; 2714 foreach ($this->tokens as &$token) 2715 if (\is_array($token)) 2716 unset($token[2]); 2717 unset($token); 2718 $passes = [ 2719 'optimizeOutConcatEqual', 2720 'optimizeConcatenations', 2721 'optimizeHtmlspecialchars' 2722 ]; 2723 $remainingLoops = $this->maxLoops; 2724 do 2725 { 2726 $continue = \false; 2727 foreach ($passes as $pass) 2728 { 2729 $this->$pass(); 2730 $cnt = \count($this->tokens); 2731 if ($this->cnt !== $cnt) 2732 { 2733 $this->tokens = \array_values($this->tokens); 2734 $this->cnt = $cnt; 2735 $continue = \true; 2736 } 2737 } 2738 } 2739 while ($continue && --$remainingLoops); 2740 $php = $this->branchOutputOptimizer->optimize($this->tokens); 2741 unset($this->tokens); 2742 return $php; 2743 } 2744 protected function isBetweenHtmlspecialcharCalls() 2745 { 2746 return ($this->tokens[$this->i + 1] === [\T_STRING, 'htmlspecialchars'] 2747 && $this->tokens[$this->i + 2] === '(' 2748 && $this->tokens[$this->i - 1] === ')' 2749 && $this->tokens[$this->i - 2][0] === \T_LNUMBER 2750 && $this->tokens[$this->i - 3] === ','); 2751 } 2752 protected function isHtmlspecialcharSafeVar() 2753 { 2754 return ($this->tokens[$this->i ] === [\T_VARIABLE, '$node'] 2755 && $this->tokens[$this->i + 1] === [\T_OBJECT_OPERATOR, '->'] 2756 && ($this->tokens[$this->i + 2] === [\T_STRING, 'localName'] 2757 || $this->tokens[$this->i + 2] === [\T_STRING, 'nodeName']) 2758 && $this->tokens[$this->i + 3] === ',' 2759 && $this->tokens[$this->i + 4][0] === \T_LNUMBER 2760 && $this->tokens[$this->i + 5] === ')'); 2761 } 2762 protected function isOutputAssignment() 2763 { 2764 return ($this->tokens[$this->i ] === [\T_VARIABLE, '$this'] 2765 && $this->tokens[$this->i + 1] === [\T_OBJECT_OPERATOR, '->'] 2766 && $this->tokens[$this->i + 2] === [\T_STRING, 'out'] 2767 && $this->tokens[$this->i + 3] === [\T_CONCAT_EQUAL, '.=']); 2768 } 2769 protected function isPrecededByOutputVar() 2770 { 2771 return ($this->tokens[$this->i - 1] === [\T_STRING, 'out'] 2772 && $this->tokens[$this->i - 2] === [\T_OBJECT_OPERATOR, '->'] 2773 && $this->tokens[$this->i - 3] === [\T_VARIABLE, '$this']); 2774 } 2775 protected function mergeConcatenatedHtmlSpecialChars() 2776 { 2777 if (!$this->isBetweenHtmlspecialcharCalls()) 2778 return \false; 2779 $escapeMode = $this->tokens[$this->i - 2][1]; 2780 $startIndex = $this->i - 3; 2781 $endIndex = $this->i + 2; 2782 $this->i = $endIndex; 2783 $parens = 0; 2784 while (++$this->i < $this->cnt) 2785 { 2786 if ($this->tokens[$this->i] === ',' && !$parens) 2787 break; 2788 if ($this->tokens[$this->i] === '(') 2789 ++$parens; 2790 elseif ($this->tokens[$this->i] === ')') 2791 --$parens; 2792 } 2793 if ($this->tokens[$this->i + 1] !== [\T_LNUMBER, $escapeMode]) 2794 return \false; 2795 $this->tokens[$startIndex] = '.'; 2796 $this->i = $startIndex; 2797 while (++$this->i <= $endIndex) 2798 unset($this->tokens[$this->i]); 2799 return \true; 2800 } 2801 protected function mergeConcatenatedStrings() 2802 { 2803 if ($this->tokens[$this->i - 1][0] !== \T_CONSTANT_ENCAPSED_STRING 2804 || $this->tokens[$this->i + 1][0] !== \T_CONSTANT_ENCAPSED_STRING 2805 || $this->tokens[$this->i - 1][1][0] !== $this->tokens[$this->i + 1][1][0]) 2806 return \false; 2807 $this->tokens[$this->i + 1][1] = \substr($this->tokens[$this->i - 1][1], 0, -1) 2808 . \substr($this->tokens[$this->i + 1][1], 1); 2809 unset($this->tokens[$this->i - 1]); 2810 unset($this->tokens[$this->i]); 2811 ++$this->i; 2812 return \true; 2813 } 2814 protected function optimizeOutConcatEqual() 2815 { 2816 $this->i = 3; 2817 while ($this->skipTo([\T_CONCAT_EQUAL, '.='])) 2818 { 2819 if (!$this->isPrecededByOutputVar()) 2820 continue; 2821 while ($this->skipPast(';')) 2822 { 2823 if (!$this->isOutputAssignment()) 2824 break; 2825 $this->tokens[$this->i - 1] = '.'; 2826 unset($this->tokens[$this->i++]); 2827 unset($this->tokens[$this->i++]); 2828 unset($this->tokens[$this->i++]); 2829 unset($this->tokens[$this->i++]); 2830 } 2831 } 2832 } 2833 protected function optimizeConcatenations() 2834 { 2835 $this->i = 1; 2836 while ($this->skipTo('.')) 2837 $this->mergeConcatenatedStrings() || $this->mergeConcatenatedHtmlSpecialChars(); 2838 } 2839 protected function optimizeHtmlspecialchars() 2840 { 2841 $this->i = 0; 2842 while ($this->skipPast([\T_STRING, 'htmlspecialchars'])) 2843 if ($this->tokens[$this->i] === '(') 2844 { 2845 ++$this->i; 2846 $this->replaceHtmlspecialcharsLiteral() || $this->removeHtmlspecialcharsSafeVar(); 2847 } 2848 } 2849 protected function removeHtmlspecialcharsSafeVar() 2850 { 2851 if (!$this->isHtmlspecialcharSafeVar()) 2852 return \false; 2853 unset($this->tokens[$this->i - 2]); 2854 unset($this->tokens[$this->i - 1]); 2855 unset($this->tokens[$this->i + 3]); 2856 unset($this->tokens[$this->i + 4]); 2857 unset($this->tokens[$this->i + 5]); 2858 $this->i += 6; 2859 return \true; 2860 } 2861 protected function replaceHtmlspecialcharsLiteral() 2862 { 2863 if ($this->tokens[$this->i ][0] !== \T_CONSTANT_ENCAPSED_STRING 2864 || $this->tokens[$this->i + 1] !== ',' 2865 || $this->tokens[$this->i + 2][0] !== \T_LNUMBER 2866 || $this->tokens[$this->i + 3] !== ')') 2867 return \false; 2868 $this->tokens[$this->i][1] = \var_export( 2869 \htmlspecialchars( 2870 \stripslashes(\substr($this->tokens[$this->i][1], 1, -1)), 2871 $this->tokens[$this->i + 2][1] 2872 ), 2873 \true 2874 ); 2875 unset($this->tokens[$this->i - 2]); 2876 unset($this->tokens[$this->i - 1]); 2877 unset($this->tokens[++$this->i]); 2878 unset($this->tokens[++$this->i]); 2879 unset($this->tokens[++$this->i]); 2880 return \true; 2881 } 2882 protected function skipPast($token) 2883 { 2884 return ($this->skipTo($token) && ++$this->i < $this->cnt); 2885 } 2886 protected function skipTo($token) 2887 { 2888 while (++$this->i < $this->cnt) 2889 if ($this->tokens[$this->i] === $token) 2890 return \true; 2891 return \false; 2892 } 2893 } 2894 2895 /* 2896 * @package s9e\TextFormatter 2897 * @copyright Copyright (c) 2010-2019 The s9e Authors 2898 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 2899 */ 2900 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; 2901 use Closure; 2902 use RuntimeException; 2903 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder; 2904 class Quick 2905 { 2906 public static function getSource(array $compiledTemplates) 2907 { 2908 $map = ['dynamic' => [], 'php' => [], 'static' => []]; 2909 $tagNames = []; 2910 $unsupported = []; 2911 unset($compiledTemplates['br']); 2912 unset($compiledTemplates['e']); 2913 unset($compiledTemplates['i']); 2914 unset($compiledTemplates['p']); 2915 unset($compiledTemplates['s']); 2916 foreach ($compiledTemplates as $tagName => $php) 2917 { 2918 $renderings = self::getRenderingStrategy($php); 2919 if (empty($renderings)) 2920 { 2921 $unsupported[] = $tagName; 2922 continue; 2923 } 2924 foreach ($renderings as $i => $_562c18b7) 2925 { 2926 list($strategy, $replacement) = $_562c18b7; 2927 $match = (($i) ? '/' : '') . $tagName; 2928 $map[$strategy][$match] = $replacement; 2929 } 2930 if (!isset($renderings[1])) 2931 $tagNames[] = $tagName; 2932 } 2933 $php = []; 2934 $php[] = ' /** {@inheritdoc} */'; 2935 $php[] = ' public $enableQuickRenderer=true;'; 2936 $php[] = ' /** {@inheritdoc} */'; 2937 $php[] = ' protected $static=' . self::export($map['static']) . ';'; 2938 $php[] = ' /** {@inheritdoc} */'; 2939 $php[] = ' protected $dynamic=' . self::export($map['dynamic']) . ';'; 2940 $quickSource = ''; 2941 if (!empty($map['php'])) 2942 $quickSource = SwitchStatement::generate('$id', $map['php']); 2943 $regexp = '(<(?:(?!/)('; 2944 $regexp .= ($tagNames) ? RegexpBuilder::fromList($tagNames) : '(?!)'; 2945 $regexp .= ')(?: [^>]*)?>.*?</\\1|(/?(?!br/|p>)[^ />]+)[^>]*?(/)?)>)s'; 2946 $php[] = ' /** {@inheritdoc} */'; 2947 $php[] = ' protected $quickRegexp=' . \var_export($regexp, \true) . ';'; 2948 if (!empty($unsupported)) 2949 { 2950 $regexp = '(<(?:[!?]|' . RegexpBuilder::fromList($unsupported) . '[ />]))'; 2951 $php[] = ' /** {@inheritdoc} */'; 2952 $php[] = ' protected $quickRenderingTest=' . \var_export($regexp, \true) . ';'; 2953 } 2954 $php[] = ' /** {@inheritdoc} */'; 2955 $php[] = ' protected function renderQuickTemplate($id, $xml)'; 2956 $php[] = ' {'; 2957 $php[] = ' $attributes=$this->matchAttributes($xml);'; 2958 $php[] = " \$html='';" . $quickSource; 2959 $php[] = ''; 2960 $php[] = ' return $html;'; 2961 $php[] = ' }'; 2962 return \implode("\n", $php); 2963 } 2964 protected static function export(array $arr) 2965 { 2966 $exportKeys = (\array_keys($arr) !== \range(0, \count($arr) - 1)); 2967 \ksort($arr); 2968 $entries = []; 2969 foreach ($arr as $k => $v) 2970 $entries[] = (($exportKeys) ? \var_export($k, \true) . '=>' : '') 2971 . ((\is_array($v)) ? self::export($v) : \var_export($v, \true)); 2972 return '[' . \implode(',', $entries) . ']'; 2973 } 2974 public static function getRenderingStrategy($php) 2975 { 2976 $phpRenderings = self::getQuickRendering($php); 2977 if (empty($phpRenderings)) 2978 return []; 2979 $renderings = self::getStringRenderings($php); 2980 foreach ($phpRenderings as $i => $phpRendering) 2981 if (!isset($renderings[$i]) || \strpos($phpRendering, '$this->attributes[]') !== \false) 2982 $renderings[$i] = ['php', $phpRendering]; 2983 return $renderings; 2984 } 2985 protected static function getQuickRendering($php) 2986 { 2987 if (\preg_match('(\\$this->at\\((?!\\$node\\);))', $php)) 2988 return []; 2989 $tokens = \token_get_all('<?php ' . $php); 2990 $tokens[] = [0, '']; 2991 \array_shift($tokens); 2992 $cnt = \count($tokens); 2993 $branch = [ 2994 'braces' => -1, 2995 'branches' => [], 2996 'head' => '', 2997 'passthrough' => 0, 2998 'statement' => '', 2999 'tail' => '' 3000 ]; 3001 $braces = 0; 3002 $i = 0; 3003 do 3004 { 3005 if ($tokens[$i ][0] === \T_VARIABLE 3006 && $tokens[$i ][1] === '$this' 3007 && $tokens[$i + 1][0] === \T_OBJECT_OPERATOR 3008 && $tokens[$i + 2][0] === \T_STRING 3009 && $tokens[$i + 2][1] === 'at' 3010 && $tokens[$i + 3] === '(' 3011 && $tokens[$i + 4][0] === \T_VARIABLE 3012 && $tokens[$i + 4][1] === '$node' 3013 && $tokens[$i + 5] === ')' 3014 && $tokens[$i + 6] === ';') 3015 { 3016 if (++$branch['passthrough'] > 1) 3017 return []; 3018 $i += 6; 3019 continue; 3020 } 3021 $key = ($branch['passthrough']) ? 'tail' : 'head'; 3022 $branch[$key] .= (\is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i]; 3023 if ($tokens[$i] === '{') 3024 { 3025 ++$braces; 3026 continue; 3027 } 3028 if ($tokens[$i] === '}') 3029 { 3030 --$braces; 3031 if ($branch['braces'] === $braces) 3032 { 3033 $branch[$key] = \substr($branch[$key], 0, -1); 3034 $branch =& $branch['parent']; 3035 $j = $i; 3036 while ($tokens[++$j][0] === \T_WHITESPACE); 3037 if ($tokens[$j][0] !== \T_ELSEIF && $tokens[$j][0] !== \T_ELSE) 3038 { 3039 $passthroughs = self::getBranchesPassthrough($branch['branches']); 3040 if ($passthroughs === [0]) 3041 { 3042 foreach ($branch['branches'] as $child) 3043 $branch['head'] .= $child['statement'] . '{' . $child['head'] . '}'; 3044 $branch['branches'] = []; 3045 continue; 3046 } 3047 if ($passthroughs === [1]) 3048 { 3049 ++$branch['passthrough']; 3050 continue; 3051 } 3052 return []; 3053 } 3054 } 3055 continue; 3056 } 3057 if ($branch['passthrough']) 3058 continue; 3059 if ($tokens[$i][0] === \T_IF 3060 || $tokens[$i][0] === \T_ELSEIF 3061 || $tokens[$i][0] === \T_ELSE) 3062 { 3063 $branch[$key] = \substr($branch[$key], 0, -\strlen($tokens[$i][1])); 3064 $branch['branches'][] = [ 3065 'braces' => $braces, 3066 'branches' => [], 3067 'head' => '', 3068 'parent' => &$branch, 3069 'passthrough' => 0, 3070 'statement' => '', 3071 'tail' => '' 3072 ]; 3073 $branch =& $branch['branches'][\count($branch['branches']) - 1]; 3074 do 3075 { 3076 $branch['statement'] .= (\is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i]; 3077 } 3078 while ($tokens[++$i] !== '{'); 3079 ++$braces; 3080 } 3081 } 3082 while (++$i < $cnt); 3083 list($head, $tail) = self::buildPHP($branch['branches']); 3084 $head = $branch['head'] . $head; 3085 $tail .= $branch['tail']; 3086 self::convertPHP($head, $tail, (bool) $branch['passthrough']); 3087 if (\preg_match('((?<!-|\\$this)->)', $head . $tail)) 3088 return []; 3089 return ($branch['passthrough']) ? [$head, $tail] : [$head]; 3090 } 3091 protected static function convertPHP(&$head, &$tail, $passthrough) 3092 { 3093 $saveAttributes = (bool) \preg_match('(\\$node->(?:get|has)Attribute)', $tail); 3094 \preg_match_all( 3095 "(\\\$node->getAttribute\\('([^']+)'\\))", 3096 \preg_replace_callback( 3097 '(if\\(\\$node->hasAttribute\\(([^\\)]+)[^}]+)', 3098 function ($m) 3099 { 3100 return \str_replace('$node->getAttribute(' . $m[1] . ')', '', $m[0]); 3101 }, 3102 $head . $tail 3103 ), 3104 $matches 3105 ); 3106 $attrNames = \array_unique($matches[1]); 3107 self::replacePHP($head); 3108 self::replacePHP($tail); 3109 if (!$passthrough && \strpos($head, '$node->textContent') !== \false) 3110 $head = '$textContent=$this->getQuickTextContent($xml);' . \str_replace('$node->textContent', '$textContent', $head); 3111 if (!empty($attrNames)) 3112 { 3113 \ksort($attrNames); 3114 $head = "\$attributes+=['" . \implode("'=>null,'", $attrNames) . "'=>null];" . $head; 3115 } 3116 if ($saveAttributes) 3117 { 3118 $head .= '$this->attributes[]=$attributes;'; 3119 $tail = '$attributes=array_pop($this->attributes);' . $tail; 3120 } 3121 } 3122 protected static function replacePHP(&$php) 3123 { 3124 $getAttribute = "\\\$node->getAttribute\\(('[^']+')\\)"; 3125 $string = "'(?:[^\\\\']|\\\\.)*+'"; 3126 $replacements = [ 3127 '$this->out' => '$html', 3128 '(htmlspecialchars\\(' . $getAttribute . ',' . \ENT_NOQUOTES . '\\))' 3129 => "str_replace('"','\"',\$attributes[\$1])", 3130 '(htmlspecialchars\\((' . $getAttribute . '(?:\\.' . $getAttribute . ')*),' . \ENT_COMPAT . '\\))' 3131 => function ($m) use ($getAttribute) 3132 { 3133 return \preg_replace('(' . $getAttribute . ')', '$attributes[$1]', $m[1]); 3134 }, 3135 '(htmlspecialchars\\(strtr\\(' . $getAttribute . ",('[^\"&\\\\';<>aglmopqtu]+'),('[^\"&\\\\'<>]+')\\)," . \ENT_COMPAT . '\\))' 3136 => 'strtr($attributes[$1],$2,$3)', 3137 '(' . $getAttribute . '(!?=+)' . $getAttribute . ')' 3138 => '$attributes[$1]$2$attributes[$3]', 3139 '(' . $getAttribute . '===(' . $string . '))s' 3140 => function ($m) 3141 { 3142 return '$attributes[' . $m[1] . ']===' . \htmlspecialchars($m[2], \ENT_COMPAT); 3143 }, 3144 '((' . $string . ')===' . $getAttribute . ')s' 3145 => function ($m) 3146 { 3147 return \htmlspecialchars($m[1], \ENT_COMPAT) . '===$attributes[' . $m[2] . ']'; 3148 }, 3149 '(strpos\\(' . $getAttribute . ',(' . $string . ')\\)([!=]==(?:0|false)))s' 3150 => function ($m) 3151 { 3152 return 'strpos($attributes[' . $m[1] . "]," . \htmlspecialchars($m[2], \ENT_COMPAT) . ')' . $m[3]; 3153 }, 3154 '(strpos\\((' . $string . '),' . $getAttribute . '\\)([!=]==(?:0|false)))s' 3155 => function ($m) 3156 { 3157 return 'strpos(' . \htmlspecialchars($m[1], \ENT_COMPAT) . ',$attributes[' . $m[2] . '])' . $m[3]; 3158 }, 3159 '(' . $getAttribute . '(?=(?:==|[-+*])\\d+))' => '$attributes[$1]', 3160 '(\\b(\\d+(?:==|[-+*]))' . $getAttribute . ')' => '$1$attributes[$2]', 3161 '(empty\\(' . $getAttribute . '\\))' => 'empty($attributes[$1])', 3162 "(\\\$node->hasAttribute\\(('[^']+')\\))" => 'isset($attributes[$1])', 3163 'if($node->attributes->length)' => 'if($this->hasNonNullValues($attributes))', 3164 '(' . $getAttribute . ')' => 'htmlspecialchars_decode($attributes[$1])' 3165 ]; 3166 foreach ($replacements as $match => $replace) 3167 if ($replace instanceof Closure) 3168 $php = \preg_replace_callback($match, $replace, $php); 3169 elseif ($match[0] === '(') 3170 $php = \preg_replace($match, $replace, $php); 3171 else 3172 $php = \str_replace($match, $replace, $php); 3173 } 3174 protected static function buildPHP(array $branches) 3175 { 3176 $return = ['', '']; 3177 foreach ($branches as $branch) 3178 { 3179 $return[0] .= $branch['statement'] . '{' . $branch['head']; 3180 $return[1] .= $branch['statement'] . '{'; 3181 if ($branch['branches']) 3182 { 3183 list($head, $tail) = self::buildPHP($branch['branches']); 3184 $return[0] .= $head; 3185 $return[1] .= $tail; 3186 } 3187 $return[0] .= '}'; 3188 $return[1] .= $branch['tail'] . '}'; 3189 } 3190 return $return; 3191 } 3192 protected static function getBranchesPassthrough(array $branches) 3193 { 3194 $values = []; 3195 foreach ($branches as $branch) 3196 $values[] = $branch['passthrough']; 3197 if ($branch['statement'] !== 'else') 3198 $values[] = 0; 3199 return \array_unique($values); 3200 } 3201 protected static function getDynamicRendering($php) 3202 { 3203 $rendering = ''; 3204 $literal = "(?<literal>'((?>[^'\\\\]+|\\\\['\\\\])*)')"; 3205 $attribute = "(?<attribute>htmlspecialchars\\(\\\$node->getAttribute\\('([^']+)'\\),2\\))"; 3206 $value = "(?<value>$literal|$attribute)"; 3207 $output = "(?<output>\\\$this->out\\.=$value(?:\\.(?&value))*;)"; 3208 $copyOfAttribute = "(?<copyOfAttribute>if\\(\\\$node->hasAttribute\\('([^']+)'\\)\\)\\{\\\$this->out\\.=' \\g-1=\"'\\.htmlspecialchars\\(\\\$node->getAttribute\\('\\g-1'\\),2\\)\\.'\"';\\})"; 3209 $regexp = '(^(' . $output . '|' . $copyOfAttribute . ')*$)'; 3210 if (!\preg_match($regexp, $php, $m)) 3211 return \false; 3212 $copiedAttributes = []; 3213 $usedAttributes = []; 3214 $regexp = '(' . $output . '|' . $copyOfAttribute . ')A'; 3215 $offset = 0; 3216 while (\preg_match($regexp, $php, $m, 0, $offset)) 3217 if ($m['output']) 3218 { 3219 $offset += 12; 3220 while (\preg_match('(' . $value . ')A', $php, $m, 0, $offset)) 3221 { 3222 if ($m['literal']) 3223 { 3224 $str = \stripslashes(\substr($m[0], 1, -1)); 3225 $rendering .= \preg_replace('([\\\\$](?=\\d))', '\\\\$0', $str); 3226 } 3227 else 3228 { 3229 $attrName = \end($m); 3230 if (!isset($usedAttributes[$attrName])) 3231 $usedAttributes[$attrName] = \uniqid($attrName, \true); 3232 $rendering .= $usedAttributes[$attrName]; 3233 } 3234 $offset += 1 + \strlen($m[0]); 3235 } 3236 } 3237 else 3238 { 3239 $attrName = \end($m); 3240 if (!isset($copiedAttributes[$attrName])) 3241 $copiedAttributes[$attrName] = \uniqid($attrName, \true); 3242 $rendering .= $copiedAttributes[$attrName]; 3243 $offset += \strlen($m[0]); 3244 } 3245 $attrNames = \array_keys($copiedAttributes + $usedAttributes); 3246 \sort($attrNames); 3247 $remainingAttributes = \array_combine($attrNames, $attrNames); 3248 $regexp = '(^[^ ]+'; 3249 $index = 0; 3250 foreach ($attrNames as $attrName) 3251 { 3252 $regexp .= '(?> (?!' . RegexpBuilder::fromList($remainingAttributes) . '=)[^=]+="[^"]*")*'; 3253 unset($remainingAttributes[$attrName]); 3254 $regexp .= '('; 3255 if (isset($copiedAttributes[$attrName])) 3256 self::replacePlaceholder($rendering, $copiedAttributes[$attrName], ++$index); 3257 else 3258 $regexp .= '?>'; 3259 $regexp .= ' ' . $attrName . '="'; 3260 if (isset($usedAttributes[$attrName])) 3261 { 3262 $regexp .= '('; 3263 self::replacePlaceholder($rendering, $usedAttributes[$attrName], ++$index); 3264 } 3265 $regexp .= '[^"]*'; 3266 if (isset($usedAttributes[$attrName])) 3267 $regexp .= ')'; 3268 $regexp .= '")?'; 3269 } 3270 $regexp .= '.*)s'; 3271 return [$regexp, $rendering]; 3272 } 3273 protected static function getStaticRendering($php) 3274 { 3275 if ($php === '') 3276 return ''; 3277 $regexp = "(^\\\$this->out\.='((?>[^'\\\\]|\\\\['\\\\])*+)';\$)"; 3278 if (\preg_match($regexp, $php, $m)) 3279 return \stripslashes($m[1]); 3280 return \false; 3281 } 3282 protected static function getStringRenderings($php) 3283 { 3284 $chunks = \explode('$this->at($node);', $php); 3285 if (\count($chunks) > 2) 3286 return []; 3287 $renderings = []; 3288 foreach ($chunks as $k => $chunk) 3289 { 3290 $rendering = self::getStaticRendering($chunk); 3291 if ($rendering !== \false) 3292 $renderings[$k] = ['static', $rendering]; 3293 elseif ($k === 0) 3294 { 3295 $rendering = self::getDynamicRendering($chunk); 3296 if ($rendering !== \false) 3297 $renderings[$k] = ['dynamic', $rendering]; 3298 } 3299 } 3300 return $renderings; 3301 } 3302 protected static function replacePlaceholder(&$str, $uniqid, $index) 3303 { 3304 $str = \preg_replace_callback( 3305 '(' . \preg_quote($uniqid) . '(.))', 3306 function ($m) use ($index) 3307 { 3308 if (\is_numeric($m[1])) 3309 return '${' . $index . '}' . $m[1]; 3310 else 3311 return '$' . $index . $m[1]; 3312 }, 3313 $str 3314 ); 3315 } 3316 } 3317 3318 /* 3319 * @package s9e\TextFormatter 3320 * @copyright Copyright (c) 2010-2019 The s9e Authors 3321 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 3322 */ 3323 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; 3324 use DOMElement; 3325 use DOMXPath; 3326 use RuntimeException; 3327 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 3328 use s9e\TextFormatter\Configurator\Helpers\TemplateParser; 3329 class Serializer 3330 { 3331 public $convertor; 3332 protected $isVoid; 3333 public $useMultibyteStringFunctions = \false; 3334 protected $xpath; 3335 public function __construct() 3336 { 3337 $this->convertor = new XPathConvertor; 3338 } 3339 public function convertCondition($expr) 3340 { 3341 $this->convertor->useMultibyteStringFunctions = $this->useMultibyteStringFunctions; 3342 return $this->convertor->convertCondition($expr); 3343 } 3344 public function convertXPath($expr) 3345 { 3346 $this->convertor->useMultibyteStringFunctions = $this->useMultibyteStringFunctions; 3347 return $this->convertor->convertXPath($expr); 3348 } 3349 public function serialize(DOMElement $ir) 3350 { 3351 $this->xpath = new DOMXPath($ir->ownerDocument); 3352 $this->isVoid = []; 3353 foreach ($this->xpath->query('//element') as $element) 3354 $this->isVoid[$element->getAttribute('id')] = $element->getAttribute('void'); 3355 return $this->serializeChildren($ir); 3356 } 3357 protected function convertAttributeValueTemplate($attrValue) 3358 { 3359 $phpExpressions = []; 3360 foreach (AVTHelper::parse($attrValue) as $token) 3361 if ($token[0] === 'literal') 3362 $phpExpressions[] = \var_export($token[1], \true); 3363 else 3364 $phpExpressions[] = $this->convertXPath($token[1]); 3365 return \implode('.', $phpExpressions); 3366 } 3367 protected function escapeLiteral($text, $context) 3368 { 3369 if ($context === 'raw') 3370 return $text; 3371 $escapeMode = ($context === 'attribute') ? \ENT_COMPAT : \ENT_NOQUOTES; 3372 return \htmlspecialchars($text, $escapeMode); 3373 } 3374 protected function escapePHPOutput($php, $context) 3375 { 3376 if ($context === 'raw') 3377 return $php; 3378 $escapeMode = ($context === 'attribute') ? \ENT_COMPAT : \ENT_NOQUOTES; 3379 return 'htmlspecialchars(' . $php . ',' . $escapeMode . ')'; 3380 } 3381 protected function hasMultipleCases(DOMElement $switch) 3382 { 3383 return $this->xpath->evaluate('count(case[@test]) > 1', $switch); 3384 } 3385 protected function serializeApplyTemplates(DOMElement $applyTemplates) 3386 { 3387 $php = '$this->at($node'; 3388 if ($applyTemplates->hasAttribute('select')) 3389 $php .= ',' . \var_export($applyTemplates->getAttribute('select'), \true); 3390 $php .= ');'; 3391 return $php; 3392 } 3393 protected function serializeAttribute(DOMElement $attribute) 3394 { 3395 $attrName = $attribute->getAttribute('name'); 3396 $phpAttrName = $this->convertAttributeValueTemplate($attrName); 3397 $phpAttrName = 'htmlspecialchars(' . $phpAttrName . ',' . \ENT_QUOTES . ')'; 3398 return "\$this->out.=' '." . $phpAttrName . ".'=\"';" 3399 . $this->serializeChildren($attribute) 3400 . "\$this->out.='\"';"; 3401 } 3402 protected function serializeChildren(DOMElement $ir) 3403 { 3404 $php = ''; 3405 foreach ($ir->childNodes as $node) 3406 if ($node instanceof DOMElement) 3407 { 3408 $methodName = 'serialize' . \ucfirst($node->localName); 3409 $php .= $this->$methodName($node); 3410 } 3411 return $php; 3412 } 3413 protected function serializeCloseTag(DOMElement $closeTag) 3414 { 3415 $php = "\$this->out.='>';"; 3416 $id = $closeTag->getAttribute('id'); 3417 if ($closeTag->hasAttribute('set')) 3418 $php .= '$t' . $id . '=1;'; 3419 if ($closeTag->hasAttribute('check')) 3420 $php = 'if(!isset($t' . $id . ')){' . $php . '}'; 3421 if ($this->isVoid[$id] === 'maybe') 3422 $php .= 'if(!$v' . $id . '){'; 3423 return $php; 3424 } 3425 protected function serializeComment(DOMElement $comment) 3426 { 3427 return "\$this->out.='<!--';" 3428 . $this->serializeChildren($comment) 3429 . "\$this->out.='-->';"; 3430 } 3431 protected function serializeCopyOfAttributes(DOMElement $copyOfAttributes) 3432 { 3433 return 'foreach($node->attributes as $attribute){' 3434 . "\$this->out.=' ';\$this->out.=\$attribute->name;\$this->out.='=\"';\$this->out.=htmlspecialchars(\$attribute->value," . \ENT_COMPAT . ");\$this->out.='\"';" 3435 . '}'; 3436 } 3437 protected function serializeElement(DOMElement $element) 3438 { 3439 $php = ''; 3440 $elName = $element->getAttribute('name'); 3441 $id = $element->getAttribute('id'); 3442 $isVoid = $element->getAttribute('void'); 3443 $isDynamic = (bool) (\strpos($elName, '{') !== \false); 3444 $phpElName = $this->convertAttributeValueTemplate($elName); 3445 $phpElName = 'htmlspecialchars(' . $phpElName . ',' . \ENT_QUOTES . ')'; 3446 if ($isDynamic) 3447 { 3448 $varName = '$e' . $id; 3449 $php .= $varName . '=' . $phpElName . ';'; 3450 $phpElName = $varName; 3451 } 3452 if ($isVoid === 'maybe') 3453 $php .= '$v' . $id . '=preg_match(' . \var_export(TemplateParser::$voidRegexp, \true) . ',' . $phpElName . ');'; 3454 $php .= "\$this->out.='<'." . $phpElName . ';'; 3455 $php .= $this->serializeChildren($element); 3456 if ($isVoid !== 'yes') 3457 $php .= "\$this->out.='</'." . $phpElName . ".'>';"; 3458 if ($isVoid === 'maybe') 3459 $php .= '}'; 3460 return $php; 3461 } 3462 protected function serializeHash(DOMElement $switch) 3463 { 3464 $statements = []; 3465 foreach ($this->xpath->query('case[@branch-values]', $switch) as $case) 3466 foreach (\unserialize($case->getAttribute('branch-values')) as $value) 3467 $statements[$value] = $this->serializeChildren($case); 3468 if (!isset($case)) 3469 throw new RuntimeException; 3470 $defaultCase = $this->xpath->query('case[not(@branch-values)]', $switch)->item(0); 3471 $defaultCode = ($defaultCase instanceof DOMElement) ? $this->serializeChildren($defaultCase) : ''; 3472 $expr = $this->convertXPath($switch->getAttribute('branch-key')); 3473 return SwitchStatement::generate($expr, $statements, $defaultCode); 3474 } 3475 protected function serializeOutput(DOMElement $output) 3476 { 3477 $context = $output->getAttribute('escape'); 3478 $php = '$this->out.='; 3479 if ($output->getAttribute('type') === 'xpath') 3480 $php .= $this->escapePHPOutput($this->convertXPath($output->textContent), $context); 3481 else 3482 $php .= \var_export($this->escapeLiteral($output->textContent, $context), \true); 3483 $php .= ';'; 3484 return $php; 3485 } 3486 protected function serializeSwitch(DOMElement $switch) 3487 { 3488 if ($switch->hasAttribute('branch-key') && $this->hasMultipleCases($switch)) 3489 return $this->serializeHash($switch); 3490 $php = ''; 3491 $if = 'if'; 3492 foreach ($this->xpath->query('case', $switch) as $case) 3493 { 3494 if ($case->hasAttribute('test')) 3495 $php .= $if . '(' . $this->convertCondition($case->getAttribute('test')) . ')'; 3496 else 3497 $php .= 'else'; 3498 $php .= '{' . $this->serializeChildren($case) . '}'; 3499 $if = 'elseif'; 3500 } 3501 return $php; 3502 } 3503 } 3504 3505 /* 3506 * @package s9e\TextFormatter 3507 * @copyright Copyright (c) 2010-2019 The s9e Authors 3508 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 3509 */ 3510 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; 3511 class SwitchStatement 3512 { 3513 protected $branchesCode; 3514 protected $defaultCode; 3515 public function __construct(array $branchesCode, $defaultCode = '') 3516 { 3517 \ksort($branchesCode); 3518 $this->branchesCode = $branchesCode; 3519 $this->defaultCode = $defaultCode; 3520 } 3521 public static function generate($expr, array $branchesCode, $defaultCode = '') 3522 { 3523 $switch = new static($branchesCode, $defaultCode); 3524 return $switch->getSource($expr); 3525 } 3526 protected function getSource($expr) 3527 { 3528 $php = 'switch(' . $expr . '){'; 3529 foreach ($this->getValuesPerCodeBranch() as $branchCode => $values) 3530 { 3531 foreach ($values as $value) 3532 $php .= 'case' . \var_export((string) $value, \true) . ':'; 3533 $php .= $branchCode . 'break;'; 3534 } 3535 if ($this->defaultCode > '') 3536 $php .= 'default:' . $this->defaultCode; 3537 $php = \preg_replace('(break;$)', '', $php) . '}'; 3538 return $php; 3539 } 3540 protected function getValuesPerCodeBranch() 3541 { 3542 $values = []; 3543 foreach ($this->branchesCode as $value => $branchCode) 3544 $values[$branchCode][] = $value; 3545 return $values; 3546 } 3547 } 3548 3549 /* 3550 * @package s9e\TextFormatter 3551 * @copyright Copyright (c) 2010-2019 The s9e Authors 3552 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 3553 */ 3554 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; 3555 use LogicException; 3556 use RuntimeException; 3557 class XPathConvertor 3558 { 3559 public $pcreVersion; 3560 protected $regexp; 3561 public $useMultibyteStringFunctions = \false; 3562 public function __construct() 3563 { 3564 $this->pcreVersion = \PCRE_VERSION; 3565 } 3566 public function convertCondition($expr) 3567 { 3568 $expr = \trim($expr); 3569 if (\preg_match('#^@([-\\w]+)$#', $expr, $m)) 3570 return '$node->hasAttribute(' . \var_export($m[1], \true) . ')'; 3571 if ($expr === '@*') 3572 return '$node->attributes->length'; 3573 if (\preg_match('#^not\\(@([-\\w]+)\\)$#', $expr, $m)) 3574 return '!$node->hasAttribute(' . \var_export($m[1], \true) . ')'; 3575 if (\preg_match('#^\\$(\\w+)$#', $expr, $m)) 3576 return '$this->params[' . \var_export($m[1], \true) . "]!==''"; 3577 if (\preg_match('#^not\\(\\$(\\w+)\\)$#', $expr, $m)) 3578 return '$this->params[' . \var_export($m[1], \true) . "]===''"; 3579 if (\preg_match('#^([$@][-\\w]+)\\s*([<>])\\s*(\\d+)$#', $expr, $m)) 3580 return $this->convertXPath($m[1]) . $m[2] . $m[3]; 3581 if (!\preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr)) 3582 $expr = 'boolean(' . $expr . ')'; 3583 return $this->convertXPath($expr); 3584 } 3585 public function convertXPath($expr) 3586 { 3587 $expr = \trim($expr); 3588 $this->generateXPathRegexp(); 3589 if (\preg_match($this->regexp, $expr, $m)) 3590 { 3591 $methodName = \null; 3592 foreach ($m as $k => $v) 3593 { 3594 if (\is_numeric($k) || $v === '' || $v === \null || !\method_exists($this, $k)) 3595 continue; 3596 $methodName = $k; 3597 break; 3598 } 3599 if (isset($methodName)) 3600 { 3601 $args = [$m[$methodName]]; 3602 $i = 0; 3603 while (isset($m[$methodName . $i])) 3604 { 3605 $args[$i] = $m[$methodName . $i]; 3606 ++$i; 3607 } 3608 return \call_user_func_array([$this, $methodName], $args); 3609 } 3610 } 3611 if (!\preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr)) 3612 $expr = 'string(' . $expr . ')'; 3613 return '$this->xpath->evaluate(' . $this->exportXPath($expr) . ',$node)'; 3614 } 3615 protected function attr($attrName) 3616 { 3617 return '$node->getAttribute(' . \var_export($attrName, \true) . ')'; 3618 } 3619 protected function dot() 3620 { 3621 return '$node->textContent'; 3622 } 3623 protected function param($paramName) 3624 { 3625 return '$this->params[' . \var_export($paramName, \true) . ']'; 3626 } 3627 protected function string($string) 3628 { 3629 return \var_export(\substr($string, 1, -1), \true); 3630 } 3631 protected function lname() 3632 { 3633 return '$node->localName'; 3634 } 3635 protected function name() 3636 { 3637 return '$node->nodeName'; 3638 } 3639 protected function number($sign, $number) 3640 { 3641 $number = \ltrim($number, '0') ?: 0; 3642 if (!$number) 3643 $sign = ''; 3644 return "'" . $sign . $number . "'"; 3645 } 3646 protected function strlen($expr) 3647 { 3648 if ($expr === '') 3649 $expr = '.'; 3650 $php = $this->convertXPath($expr); 3651 return ($this->useMultibyteStringFunctions) 3652 ? 'mb_strlen(' . $php . ",'utf-8')" 3653 : "strlen(preg_replace('(.)us','.'," . $php . '))'; 3654 } 3655 protected function contains($haystack, $needle) 3656 { 3657 return '(strpos(' . $this->convertXPath($haystack) . ',' . $this->convertXPath($needle) . ')!==false)'; 3658 } 3659 protected function startswith($string, $substring) 3660 { 3661 return '(strpos(' . $this->convertXPath($string) . ',' . $this->convertXPath($substring) . ')===0)'; 3662 } 3663 protected function not($expr) 3664 { 3665 return '!(' . $this->convertCondition($expr) . ')'; 3666 } 3667 protected function notcontains($haystack, $needle) 3668 { 3669 return '(strpos(' . $this->convertXPath($haystack) . ',' . $this->convertXPath($needle) . ')===false)'; 3670 } 3671 protected function substr($exprString, $exprPos, $exprLen = \null) 3672 { 3673 if (!$this->useMultibyteStringFunctions) 3674 { 3675 $expr = 'substring(' . $exprString . ',' . $exprPos; 3676 if (isset($exprLen)) 3677 $expr .= ',' . $exprLen; 3678 $expr .= ')'; 3679 return '$this->xpath->evaluate(' . $this->exportXPath($expr) . ',$node)'; 3680 } 3681 $php = 'mb_substr(' . $this->convertXPath($exprString) . ','; 3682 if (\is_numeric($exprPos)) 3683 $php .= \max(0, $exprPos - 1); 3684 else 3685 $php .= 'max(0,' . $this->convertXPath($exprPos) . '-1)'; 3686 $php .= ','; 3687 if (isset($exprLen)) 3688 if (\is_numeric($exprLen)) 3689 if (\is_numeric($exprPos) && $exprPos < 1) 3690 $php .= \max(0, $exprPos + $exprLen - 1); 3691 else 3692 $php .= \max(0, $exprLen); 3693 else 3694 $php .= 'max(0,' . $this->convertXPath(