[ 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($exprLen) . ')'; 3695 else 3696 $php .= 'null'; 3697 $php .= ",'utf-8')"; 3698 return $php; 3699 } 3700 protected function substringafter($expr, $str) 3701 { 3702 return 'substr(strstr(' . $this->convertXPath($expr) . ',' . $this->convertXPath($str) . '),' . (\strlen($str) - 2) . ')'; 3703 } 3704 protected function substringbefore($expr1, $expr2) 3705 { 3706 return 'strstr(' . $this->convertXPath($expr1) . ',' . $this->convertXPath($expr2) . ',true)'; 3707 } 3708 protected function cmp($expr1, $operator, $expr2) 3709 { 3710 $operands = []; 3711 $operators = [ 3712 '=' => '===', 3713 '!=' => '!==', 3714 '>' => '>', 3715 '>=' => '>=', 3716 '<' => '<', 3717 '<=' => '<=' 3718 ]; 3719 foreach ([$expr1, $expr2] as $expr) 3720 if (\is_numeric($expr)) 3721 { 3722 $operators['='] = '=='; 3723 $operators['!='] = '!='; 3724 $operands[] = \preg_replace('(^0(.+))', '$1', $expr); 3725 } 3726 else 3727 $operands[] = $this->convertXPath($expr); 3728 return \implode($operators[$operator], $operands); 3729 } 3730 protected function bool($expr1, $operator, $expr2) 3731 { 3732 $operators = [ 3733 'and' => '&&', 3734 'or' => '||' 3735 ]; 3736 return $this->convertCondition($expr1) . $operators[$operator] . $this->convertCondition($expr2); 3737 } 3738 protected function parens($expr) 3739 { 3740 return '(' . $this->convertXPath($expr) . ')'; 3741 } 3742 protected function translate($str, $from, $to) 3743 { 3744 \preg_match_all('(.)su', \substr($from, 1, -1), $matches); 3745 $from = $matches[0]; 3746 \preg_match_all('(.)su', \substr($to, 1, -1), $matches); 3747 $to = $matches[0]; 3748 $from = \array_unique($from); 3749 $to = \array_intersect_key($to, $from); 3750 $to += \array_fill_keys(\array_keys(\array_diff_key($from, $to)), ''); 3751 $php = 'strtr(' . $this->convertXPath($str) . ','; 3752 if ([1] === \array_unique(\array_map('strlen', $from)) 3753 && [1] === \array_unique(\array_map('strlen', $to))) 3754 $php .= \var_export(\implode('', $from), \true) . ',' . \var_export(\implode('', $to), \true); 3755 else 3756 { 3757 $elements = []; 3758 foreach ($from as $k => $str) 3759 $elements[] = \var_export($str, \true) . '=>' . \var_export($to[$k], \true); 3760 $php .= '[' . \implode(',', $elements) . ']'; 3761 } 3762 $php .= ')'; 3763 return $php; 3764 } 3765 protected function math($expr1, $operator, $expr2) 3766 { 3767 if (!\is_numeric($expr1)) 3768 $expr1 = $this->convertXPath($expr1); 3769 if (!\is_numeric($expr2)) 3770 $expr2 = $this->convertXPath($expr2); 3771 if ($operator === 'div') 3772 $operator = '/'; 3773 return $expr1 . $operator . $expr2; 3774 } 3775 protected function exportXPath($expr) 3776 { 3777 $phpTokens = []; 3778 foreach ($this->tokenizeXPathForExport($expr) as $_f6b3b659) 3779 { 3780 list($type, $content) = $_f6b3b659; 3781 $methodName = 'exportXPath' . \ucfirst($type); 3782 $phpTokens[] = $this->$methodName($content); 3783 } 3784 return \implode('.', $phpTokens); 3785 } 3786 protected function exportXPathCurrent() 3787 { 3788 return '$node->getNodePath()'; 3789 } 3790 protected function exportXPathFragment($fragment) 3791 { 3792 return \var_export($fragment, \true); 3793 } 3794 protected function exportXPathParam($param) 3795 { 3796 $paramName = \ltrim($param, '$'); 3797 return '$this->getParamAsXPath(' . \var_export($paramName, \true) . ')'; 3798 } 3799 protected function generateXPathRegexp() 3800 { 3801 if (isset($this->regexp)) 3802 return; 3803 $patterns = [ 3804 'attr' => ['@', '(?<attr0>[-\\w]+)'], 3805 'dot' => '\\.', 3806 'name' => 'name\\(\\)', 3807 'lname' => 'local-name\\(\\)', 3808 'param' => ['\\$', '(?<param0>\\w+)'], 3809 'string' => '"[^"]*"|\'[^\']*\'', 3810 'number' => ['(?<number0>-?)', '(?<number1>\\d++)'], 3811 'strlen' => ['string-length', '\\(', '(?<strlen0>(?&value)?)', '\\)'], 3812 'contains' => [ 3813 'contains', 3814 '\\(', 3815 '(?<contains0>(?&value))', 3816 ',', 3817 '(?<contains1>(?&value))', 3818 '\\)' 3819 ], 3820 'translate' => [ 3821 'translate', 3822 '\\(', 3823 '(?<translate0>(?&value))', 3824 ',', 3825 '(?<translate1>(?&string))', 3826 ',', 3827 '(?<translate2>(?&string))', 3828 '\\)' 3829 ], 3830 'substr' => [ 3831 'substring', 3832 '\\(', 3833 '(?<substr0>(?&value))', 3834 ',', 3835 '(?<substr1>(?&value))', 3836 '(?:, (?<substr2>(?&value)))?', 3837 '\\)' 3838 ], 3839 'substringafter' => [ 3840 'substring-after', 3841 '\\(', 3842 '(?<substringafter0>(?&value))', 3843 ',', 3844 '(?<substringafter1>(?&string))', 3845 '\\)' 3846 ], 3847 'substringbefore' => [ 3848 'substring-before', 3849 '\\(', 3850 '(?<substringbefore0>(?&value))', 3851 ',', 3852 '(?<substringbefore1>(?&value))', 3853 '\\)' 3854 ], 3855 'startswith' => [ 3856 'starts-with', 3857 '\\(', 3858 '(?<startswith0>(?&value))', 3859 ',', 3860 '(?<startswith1>(?&value))', 3861 '\\)' 3862 ], 3863 'math' => [ 3864 '(?<math0>(?&attr)|(?&number)|(?¶m))', 3865 '(?<math1>[-+*]|div)', 3866 '(?<math2>(?&math)|(?&math0))' 3867 ], 3868 'notcontains' => [ 3869 'not', 3870 '\\(', 3871 'contains', 3872 '\\(', 3873 '(?<notcontains0>(?&value))', 3874 ',', 3875 '(?<notcontains1>(?&value))', 3876 '\\)', 3877 '\\)' 3878 ] 3879 ]; 3880 $exprs = []; 3881 if (\version_compare($this->pcreVersion, '8.13', '>=')) 3882 { 3883 $exprs[] = '(?<cmp>(?<cmp0>(?&value)) (?<cmp1>!?=) (?<cmp2>(?&value)))'; 3884 $exprs[] = '(?<parens>\\( (?<parens0>(?&bool)|(?&cmp)|(?&math)) \\))'; 3885 $exprs[] = '(?<bool>(?<bool0>(?&cmp)|(?¬)|(?&value)|(?&parens)) (?<bool1>and|or) (?<bool2>(?&bool)|(?&cmp)|(?¬)|(?&value)|(?&parens)))'; 3886 $exprs[] = '(?<not>not \\( (?<not0>(?&bool)|(?&value)) \\))'; 3887 $patterns['math'][0] = \str_replace('))', ')|(?&parens))', $patterns['math'][0]); 3888 $patterns['math'][1] = \str_replace('))', ')|(?&parens))', $patterns['math'][1]); 3889 } 3890 $valueExprs = []; 3891 foreach ($patterns as $name => $pattern) 3892 { 3893 if (\is_array($pattern)) 3894 $pattern = \implode(' ', $pattern); 3895 if (\strpos($pattern, '?&') === \false || \version_compare($this->pcreVersion, '8.13', '>=')) 3896 $valueExprs[] = '(?<' . $name . '>' . $pattern . ')'; 3897 } 3898 \array_unshift($exprs, '(?<value>' . \implode('|', $valueExprs) . ')'); 3899 $regexp = '#^(?:' . \implode('|', $exprs) . ')$#S'; 3900 $regexp = \str_replace(' ', '\\s*', $regexp); 3901 $this->regexp = $regexp; 3902 } 3903 protected function matchXPathForExport($expr) 3904 { 3905 $tokenExprs = [ 3906 '(?<current>\\bcurrent\\(\\))', 3907 '(?<param>\\$\\w+)', 3908 '(?<fragment>"[^"]*"|\'[^\']*\'|.)' 3909 ]; 3910 \preg_match_all('(' . \implode('|', $tokenExprs) . ')s', $expr, $matches, \PREG_SET_ORDER); 3911 $i = \count($matches); 3912 while (--$i > 0) 3913 if (isset($matches[$i]['fragment'], $matches[$i - 1]['fragment'])) 3914 { 3915 $matches[$i - 1]['fragment'] .= $matches[$i]['fragment']; 3916 unset($matches[$i]); 3917 } 3918 return \array_values($matches); 3919 } 3920 protected function tokenizeXPathForExport($expr) 3921 { 3922 $tokens = []; 3923 foreach ($this->matchXPathForExport($expr) as $match) 3924 foreach (\array_reverse($match) as $k => $v) 3925 if (!\is_numeric($k)) 3926 { 3927 $tokens[] = [$k, $v]; 3928 break; 3929 } 3930 return $tokens; 3931 } 3932 } 3933 3934 /* 3935 * @package s9e\TextFormatter 3936 * @copyright Copyright (c) 2010-2019 The s9e Authors 3937 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 3938 */ 3939 namespace s9e\TextFormatter\Configurator\RulesGenerators\Interfaces; 3940 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 3941 interface BooleanRulesGenerator 3942 { 3943 public function generateBooleanRules(TemplateInspector $src); 3944 } 3945 3946 /* 3947 * @package s9e\TextFormatter 3948 * @copyright Copyright (c) 2010-2019 The s9e Authors 3949 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 3950 */ 3951 namespace s9e\TextFormatter\Configurator\RulesGenerators\Interfaces; 3952 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 3953 interface TargetedRulesGenerator 3954 { 3955 public function generateTargetedRules(TemplateInspector $src, TemplateInspector $trg); 3956 } 3957 3958 /* 3959 * @package s9e\TextFormatter 3960 * @copyright Copyright (c) 2010-2019 The s9e Authors 3961 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 3962 */ 3963 namespace s9e\TextFormatter\Configurator; 3964 use DOMElement; 3965 use s9e\TextFormatter\Configurator\Items\Tag; 3966 abstract class TemplateCheck 3967 { 3968 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; 3969 abstract public function check(DOMElement $template, Tag $tag); 3970 } 3971 3972 /* 3973 * @package s9e\TextFormatter 3974 * @copyright Copyright (c) 2010-2019 The s9e Authors 3975 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 3976 */ 3977 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 3978 use DOMAttr; 3979 use DOMComment; 3980 use DOMElement; 3981 use DOMNode; 3982 use DOMXPath; 3983 abstract class AbstractNormalization 3984 { 3985 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; 3986 public $onlyOnce = \false; 3987 protected $ownerDocument; 3988 protected $queries = []; 3989 protected $xpath; 3990 public function normalize(DOMElement $template) 3991 { 3992 $this->ownerDocument = $template->ownerDocument; 3993 $this->xpath = new DOMXPath($this->ownerDocument); 3994 foreach ($this->getNodes() as $node) 3995 $this->normalizeNode($node); 3996 $this->reset(); 3997 } 3998 protected function createElement($nodeName, $textContent = '') 3999 { 4000 $methodName = 'createElement'; 4001 $args = [$nodeName]; 4002 if ($textContent !== '') 4003 $args[] = \htmlspecialchars($textContent, \ENT_NOQUOTES, 'UTF-8'); 4004 $prefix = \strstr($nodeName, ':', \true); 4005 if ($prefix > '') 4006 { 4007 $methodName .= 'NS'; 4008 \array_unshift($args, $this->ownerDocument->lookupNamespaceURI($prefix)); 4009 } 4010 return \call_user_func_array([$this->ownerDocument, $methodName], $args); 4011 } 4012 protected function createText($content) 4013 { 4014 return (\trim($content) === '') 4015 ? $this->createElement('xsl:text', $content) 4016 : $this->ownerDocument->createTextNode($content); 4017 } 4018 protected function createTextNode($content) 4019 { 4020 return $this->ownerDocument->createTextNode($content); 4021 } 4022 protected function getNodes() 4023 { 4024 $query = \implode(' | ', $this->queries); 4025 return ($query === '') ? [] : $this->xpath($query); 4026 } 4027 protected function isXsl(DOMNode $node, $localName = \null) 4028 { 4029 return ($node->namespaceURI === self::XMLNS_XSL && (!isset($localName) || $localName === $node->localName)); 4030 } 4031 protected function lowercase($str) 4032 { 4033 return \strtr($str, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); 4034 } 4035 protected function normalizeAttribute(DOMAttr $attribute) 4036 { 4037 } 4038 protected function normalizeElement(DOMElement $element) 4039 { 4040 } 4041 protected function normalizeNode(DOMNode $node) 4042 { 4043 if (!$node->parentNode) 4044 return; 4045 if ($node instanceof DOMElement) 4046 $this->normalizeElement($node); 4047 elseif ($node instanceof DOMAttr) 4048 $this->normalizeAttribute($node); 4049 } 4050 protected function reset() 4051 { 4052 $this->ownerDocument = \null; 4053 $this->xpath = \null; 4054 } 4055 protected function xpath($query, DOMNode $node = \null) 4056 { 4057 $query = \str_replace('$XSL', '"' . self::XMLNS_XSL . '"', $query); 4058 return \iterator_to_array($this->xpath->query($query, $node)); 4059 } 4060 } 4061 4062 /* 4063 * @package s9e\TextFormatter 4064 * @copyright Copyright (c) 2010-2019 The s9e Authors 4065 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4066 */ 4067 namespace s9e\TextFormatter\Configurator\Traits; 4068 trait CollectionProxy 4069 { 4070 public function __call($methodName, $args) 4071 { 4072 return \call_user_func_array([$this->collection, $methodName], $args); 4073 } 4074 public function offsetExists($offset) 4075 { 4076 return isset($this->collection[$offset]); 4077 } 4078 public function offsetGet($offset) 4079 { 4080 return $this->collection[$offset]; 4081 } 4082 public function offsetSet($offset, $value) 4083 { 4084 $this->collection[$offset] = $value; 4085 } 4086 public function offsetUnset($offset) 4087 { 4088 unset($this->collection[$offset]); 4089 } 4090 public function count() 4091 { 4092 return \count($this->collection); 4093 } 4094 public function current() 4095 { 4096 return $this->collection->current(); 4097 } 4098 public function key() 4099 { 4100 return $this->collection->key(); 4101 } 4102 public function next() 4103 { 4104 return $this->collection->next(); 4105 } 4106 public function rewind() 4107 { 4108 $this->collection->rewind(); 4109 } 4110 public function valid() 4111 { 4112 return $this->collection->valid(); 4113 } 4114 } 4115 4116 /* 4117 * @package s9e\TextFormatter 4118 * @copyright Copyright (c) 2010-2019 The s9e Authors 4119 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4120 */ 4121 namespace s9e\TextFormatter\Configurator\Traits; 4122 use InvalidArgumentException; 4123 use RuntimeException; 4124 use Traversable; 4125 use s9e\TextFormatter\Configurator\Collections\Collection; 4126 use s9e\TextFormatter\Configurator\Collections\NormalizedCollection; 4127 trait Configurable 4128 { 4129 public function __get($propName) 4130 { 4131 $methodName = 'get' . \ucfirst($propName); 4132 if (\method_exists($this, $methodName)) 4133 return $this->$methodName(); 4134 if (!\property_exists($this, $propName)) 4135 throw new RuntimeException("Property '" . $propName . "' does not exist"); 4136 return $this->$propName; 4137 } 4138 public function __set($propName, $propValue) 4139 { 4140 $methodName = 'set' . \ucfirst($propName); 4141 if (\method_exists($this, $methodName)) 4142 { 4143 $this->$methodName($propValue); 4144 return; 4145 } 4146 if (!isset($this->$propName)) 4147 { 4148 $this->$propName = $propValue; 4149 return; 4150 } 4151 if ($this->$propName instanceof NormalizedCollection) 4152 { 4153 if (!\is_array($propValue) 4154 && !($propValue instanceof Traversable)) 4155 throw new InvalidArgumentException("Property '" . $propName . "' expects an array or a traversable object to be passed"); 4156 $this->$propName->clear(); 4157 foreach ($propValue as $k => $v) 4158 $this->$propName->set($k, $v); 4159 return; 4160 } 4161 if (\is_object($this->$propName)) 4162 { 4163 if (!($propValue instanceof $this->$propName)) 4164 throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of class '" . \get_class($this->$propName) . "' with instance of '" . \get_class($propValue) . "'"); 4165 } 4166 else 4167 { 4168 $oldType = \gettype($this->$propName); 4169 $newType = \gettype($propValue); 4170 if ($oldType === 'boolean') 4171 if ($propValue === 'false') 4172 { 4173 $newType = 'boolean'; 4174 $propValue = \false; 4175 } 4176 elseif ($propValue === 'true') 4177 { 4178 $newType = 'boolean'; 4179 $propValue = \true; 4180 } 4181 if ($oldType !== $newType) 4182 { 4183 $tmp = $propValue; 4184 \settype($tmp, $oldType); 4185 \settype($tmp, $newType); 4186 if ($tmp !== $propValue) 4187 throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of type " . $oldType . ' with value of type ' . $newType); 4188 \settype($propValue, $oldType); 4189 } 4190 } 4191 $this->$propName = $propValue; 4192 } 4193 public function __isset($propName) 4194 { 4195 $methodName = 'isset' . \ucfirst($propName); 4196 if (\method_exists($this, $methodName)) 4197 return $this->$methodName(); 4198 return isset($this->$propName); 4199 } 4200 public function __unset($propName) 4201 { 4202 $methodName = 'unset' . \ucfirst($propName); 4203 if (\method_exists($this, $methodName)) 4204 { 4205 $this->$methodName(); 4206 return; 4207 } 4208 if (!isset($this->$propName)) 4209 return; 4210 if ($this->$propName instanceof Collection) 4211 { 4212 $this->$propName->clear(); 4213 return; 4214 } 4215 throw new RuntimeException("Property '" . $propName . "' cannot be unset"); 4216 } 4217 } 4218 4219 /* 4220 * @package s9e\TextFormatter 4221 * @copyright Copyright (c) 2010-2019 The s9e Authors 4222 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4223 */ 4224 namespace s9e\TextFormatter\Configurator\Traits; 4225 trait TemplateSafeness 4226 { 4227 protected $markedSafe = []; 4228 protected function isSafe($context) 4229 { 4230 return !empty($this->markedSafe[$context]); 4231 } 4232 public function isSafeAsURL() 4233 { 4234 return $this->isSafe('AsURL'); 4235 } 4236 public function isSafeInCSS() 4237 { 4238 return $this->isSafe('InCSS'); 4239 } 4240 public function isSafeInJS() 4241 { 4242 return $this->isSafe('InJS'); 4243 } 4244 public function markAsSafeAsURL() 4245 { 4246 $this->markedSafe['AsURL'] = \true; 4247 return $this; 4248 } 4249 public function markAsSafeInCSS() 4250 { 4251 $this->markedSafe['InCSS'] = \true; 4252 return $this; 4253 } 4254 public function markAsSafeInJS() 4255 { 4256 $this->markedSafe['InJS'] = \true; 4257 return $this; 4258 } 4259 public function resetSafeness() 4260 { 4261 $this->markedSafe = []; 4262 return $this; 4263 } 4264 } 4265 4266 /* 4267 * @package s9e\TextFormatter 4268 * @copyright Copyright (c) 2010-2019 The s9e Authors 4269 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4270 */ 4271 namespace s9e\TextFormatter\Configurator\Validators; 4272 use InvalidArgumentException; 4273 abstract class AttributeName 4274 { 4275 public static function isValid($name) 4276 { 4277 return (bool) \preg_match('#^(?!xmlns$)[a-z_][-a-z_0-9]*$#Di', $name); 4278 } 4279 public static function normalize($name) 4280 { 4281 if (!static::isValid($name)) 4282 throw new InvalidArgumentException("Invalid attribute name '" . $name . "'"); 4283 return \strtolower($name); 4284 } 4285 } 4286 4287 /* 4288 * @package s9e\TextFormatter 4289 * @copyright Copyright (c) 2010-2019 The s9e Authors 4290 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4291 */ 4292 namespace s9e\TextFormatter\Configurator\Validators; 4293 use InvalidArgumentException; 4294 abstract class TagName 4295 { 4296 public static function isValid($name) 4297 { 4298 return (bool) \preg_match('#^(?:(?!xmlns|xsl|s9e)[a-z_][a-z_0-9]*:)?[a-z_][-a-z_0-9]*$#Di', $name); 4299 } 4300 public static function normalize($name) 4301 { 4302 if (!static::isValid($name)) 4303 throw new InvalidArgumentException("Invalid tag name '" . $name . "'"); 4304 if (\strpos($name, ':') === \false) 4305 $name = \strtoupper($name); 4306 return $name; 4307 } 4308 } 4309 4310 /* 4311 * @package s9e\TextFormatter 4312 * @copyright Copyright (c) 2010-2019 The s9e Authors 4313 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4314 */ 4315 namespace s9e\TextFormatter\Configurator\Collections; 4316 use Countable; 4317 use Iterator; 4318 use s9e\TextFormatter\Configurator\ConfigProvider; 4319 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper; 4320 class Collection implements ConfigProvider, Countable, Iterator 4321 { 4322 protected $items = []; 4323 public function clear() 4324 { 4325 $this->items = []; 4326 } 4327 public function asConfig() 4328 { 4329 return ConfigHelper::toArray($this->items, \true); 4330 } 4331 public function count() 4332 { 4333 return \count($this->items); 4334 } 4335 public function current() 4336 { 4337 return \current($this->items); 4338 } 4339 public function key() 4340 { 4341 return \key($this->items); 4342 } 4343 public function next() 4344 { 4345 return \next($this->items); 4346 } 4347 public function rewind() 4348 { 4349 \reset($this->items); 4350 } 4351 public function valid() 4352 { 4353 return (\key($this->items) !== \null); 4354 } 4355 } 4356 4357 /* 4358 * @package s9e\TextFormatter 4359 * @copyright Copyright (c) 2010-2019 The s9e Authors 4360 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4361 */ 4362 namespace s9e\TextFormatter\Configurator\Helpers\TemplateParser; 4363 use DOMDocument; 4364 use DOMElement; 4365 use DOMNode; 4366 use s9e\TextFormatter\Configurator\Helpers\XPathHelper; 4367 class Normalizer extends IRProcessor 4368 { 4369 protected $optimizer; 4370 public $voidRegexp = '/^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/Di'; 4371 public function __construct(Optimizer $optimizer) 4372 { 4373 $this->optimizer = $optimizer; 4374 } 4375 public function normalize(DOMDocument $ir) 4376 { 4377 $this->createXPath($ir); 4378 $this->addDefaultCase($ir); 4379 $this->addElementIds($ir); 4380 $this->addCloseTagElements($ir); 4381 $this->markVoidElements($ir); 4382 $this->optimizer->optimize($ir); 4383 $this->markConditionalCloseTagElements($ir); 4384 $this->setOutputContext($ir); 4385 $this->markBranchTables($ir); 4386 } 4387 protected function addCloseTagElements(DOMDocument $ir) 4388 { 4389 $exprs = [ 4390 '//applyTemplates[not(ancestor::attribute)]', 4391 '//comment', 4392 '//element', 4393 '//output[not(ancestor::attribute)]' 4394 ]; 4395 foreach ($this->query(\implode('|', $exprs)) as $node) 4396 { 4397 $parentElementId = $this->getParentElementId($node); 4398 if (isset($parentElementId)) 4399 $node->parentNode 4400 ->insertBefore($ir->createElement('closeTag'), $node) 4401 ->setAttribute('id', $parentElementId); 4402 if ($node->nodeName === 'element') 4403 { 4404 $id = $node->getAttribute('id'); 4405 $this->appendElement($node, 'closeTag')->setAttribute('id', $id); 4406 } 4407 } 4408 } 4409 protected function addDefaultCase(DOMDocument $ir) 4410 { 4411 foreach ($this->query('//switch[not(case[not(@test)])]') as $switch) 4412 $this->appendElement($switch, 'case'); 4413 } 4414 protected function addElementIds(DOMDocument $ir) 4415 { 4416 $id = 0; 4417 foreach ($this->query('//element') as $element) 4418 $element->setAttribute('id', ++$id); 4419 } 4420 protected function getOutputContext(DOMNode $output) 4421 { 4422 $contexts = [ 4423 'boolean(ancestor::attribute)' => 'attribute', 4424 '@disable-output-escaping="yes"' => 'raw', 4425 'count(ancestor::element[@name="script"])' => 'raw' 4426 ]; 4427 foreach ($contexts as $expr => $context) 4428 if ($this->evaluate($expr, $output)) 4429 return $context; 4430 return 'text'; 4431 } 4432 protected function getParentElementId(DOMNode $node) 4433 { 4434 $parentNode = $node->parentNode; 4435 while (isset($parentNode)) 4436 { 4437 if ($parentNode->nodeName === 'element') 4438 return $parentNode->getAttribute('id'); 4439 $parentNode = $parentNode->parentNode; 4440 } 4441 } 4442 protected function markBranchTables(DOMDocument $ir) 4443 { 4444 foreach ($this->query('//switch[case[2][@test]]') as $switch) 4445 $this->markSwitchTable($switch); 4446 } 4447 protected function markSwitchTable(DOMElement $switch) 4448 { 4449 $cases = []; 4450 $maps = []; 4451 foreach ($this->query('./case[@test]', $switch) as $i => $case) 4452 { 4453 $map = XPathHelper::parseEqualityExpr($case->getAttribute('test')); 4454 if ($map === \false) 4455 return; 4456 $maps += $map; 4457 $cases[$i] = [$case, \end($map)]; 4458 } 4459 if (\count($maps) !== 1) 4460 return; 4461 $switch->setAttribute('branch-key', \key($maps)); 4462 foreach ($cases as $_6920557c) 4463 { 4464 list($case, $values) = $_6920557c; 4465 \sort($values); 4466 $case->setAttribute('branch-values', \serialize($values)); 4467 } 4468 } 4469 protected function markConditionalCloseTagElements(DOMDocument $ir) 4470 { 4471 foreach ($this->query('//closeTag') as $closeTag) 4472 { 4473 $id = $closeTag->getAttribute('id'); 4474 $query = 'ancestor::switch/following-sibling::*/descendant-or-self::closeTag[@id = "' . $id . '"]'; 4475 foreach ($this->query($query, $closeTag) as $following) 4476 { 4477 $following->setAttribute('check', ''); 4478 $closeTag->setAttribute('set', ''); 4479 } 4480 } 4481 } 4482 protected function markVoidElements(DOMDocument $ir) 4483 { 4484 foreach ($this->query('//element') as $element) 4485 { 4486 $elName = $element->getAttribute('name'); 4487 if (\strpos($elName, '{') !== \false) 4488 $element->setAttribute('void', 'maybe'); 4489 elseif (\preg_match($this->voidRegexp, $elName)) 4490 $element->setAttribute('void', 'yes'); 4491 } 4492 } 4493 protected function setOutputContext(DOMDocument $ir) 4494 { 4495 foreach ($this->query('//output') as $output) 4496 $output->setAttribute('escape', $this->getOutputContext($output)); 4497 } 4498 } 4499 4500 /* 4501 * @package s9e\TextFormatter 4502 * @copyright Copyright (c) 2010-2019 The s9e Authors 4503 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4504 */ 4505 namespace s9e\TextFormatter\Configurator\Helpers\TemplateParser; 4506 use DOMDocument; 4507 use DOMElement; 4508 use DOMNode; 4509 class Optimizer extends IRProcessor 4510 { 4511 public function optimize(DOMDocument $ir) 4512 { 4513 $this->createXPath($ir); 4514 $xml = $ir->saveXML(); 4515 $remainingLoops = 10; 4516 do 4517 { 4518 $old = $xml; 4519 $this->optimizeCloseTagElements($ir); 4520 $xml = $ir->saveXML(); 4521 } 4522 while (--$remainingLoops > 0 && $xml !== $old); 4523 $this->removeCloseTagSiblings($ir); 4524 $this->removeContentFromVoidElements($ir); 4525 $this->mergeConsecutiveLiteralOutputElements($ir); 4526 $this->removeEmptyDefaultCases($ir); 4527 } 4528 protected function cloneCloseTagElementsIntoSwitch(DOMDocument $ir) 4529 { 4530 $query = '//switch[name(following-sibling::*[1]) = "closeTag"]'; 4531 foreach ($this->query($query) as $switch) 4532 { 4533 $closeTag = $switch->nextSibling; 4534 foreach ($this->query('case', $switch) as $case) 4535 if (!$case->lastChild || $case->lastChild->nodeName !== 'closeTag') 4536 $case->appendChild($closeTag->cloneNode()); 4537 } 4538 } 4539 protected function cloneCloseTagElementsOutOfSwitch(DOMDocument $ir) 4540 { 4541 $query = '//switch[case/closeTag][not(case[name(*[1]) != "closeTag"])]'; 4542 foreach ($this->query($query) as $switch) 4543 { 4544 $case = $this->query('case/closeTag', $switch)->item(0); 4545 $switch->parentNode->insertBefore($case->cloneNode(), $switch); 4546 } 4547 } 4548 protected function mergeConsecutiveLiteralOutputElements(DOMDocument $ir) 4549 { 4550 foreach ($this->query('//output[@type="literal"]') as $output) 4551 { 4552 $disableOutputEscaping = $output->getAttribute('disable-output-escaping'); 4553 while ($this->nextSiblingIsLiteralOutput($output, $disableOutputEscaping)) 4554 { 4555 $output->nodeValue = \htmlspecialchars($output->nodeValue . $output->nextSibling->nodeValue); 4556 $output->parentNode->removeChild($output->nextSibling); 4557 } 4558 } 4559 } 4560 protected function nextSiblingIsLiteralOutput(DOMElement $node, $disableOutputEscaping) 4561 { 4562 return isset($node->nextSibling) && $node->nextSibling->nodeName === 'output' && $node->nextSibling->getAttribute('type') === 'literal' && $node->nextSibling->getAttribute('disable-output-escaping') === $disableOutputEscaping; 4563 } 4564 protected function optimizeCloseTagElements(DOMDocument $ir) 4565 { 4566 $this->cloneCloseTagElementsIntoSwitch($ir); 4567 $this->cloneCloseTagElementsOutOfSwitch($ir); 4568 $this->removeRedundantCloseTagElementsInSwitch($ir); 4569 $this->removeRedundantCloseTagElements($ir); 4570 } 4571 protected function removeCloseTagSiblings(DOMDocument $ir) 4572 { 4573 $query = '//switch[not(case[not(closeTag)])]/following-sibling::closeTag'; 4574 $this->removeNodes($ir, $query); 4575 } 4576 protected function removeContentFromVoidElements(DOMDocument $ir) 4577 { 4578 foreach ($this->query('//element[@void="yes"]') as $element) 4579 { 4580 $id = $element->getAttribute('id'); 4581 $query = './/closeTag[@id="' . $id . '"]/following-sibling::*'; 4582 $this->removeNodes($ir, $query, $element); 4583 } 4584 } 4585 protected function removeEmptyDefaultCases(DOMDocument $ir) 4586 { 4587 $query = '//case[not(@test)][not(*)][. = ""]'; 4588 $this->removeNodes($ir, $query); 4589 } 4590 protected function removeNodes(DOMDocument $ir, $query, DOMNode $contextNode = \null) 4591 { 4592 foreach ($this->query($query, $contextNode) as $node) 4593 if ($node->parentNode instanceof DOMElement) 4594 $node->parentNode->removeChild($node); 4595 } 4596 protected function removeRedundantCloseTagElements(DOMDocument $ir) 4597 { 4598 foreach ($this->query('//closeTag') as $closeTag) 4599 { 4600 $id = $closeTag->getAttribute('id'); 4601 $query = 'following-sibling::*/descendant-or-self::closeTag[@id="' . $id . '"]'; 4602 $this->removeNodes($ir, $query, $closeTag); 4603 } 4604 } 4605 protected function removeRedundantCloseTagElementsInSwitch(DOMDocument $ir) 4606 { 4607 $query = '//switch[name(following-sibling::*[1]) = "closeTag"]'; 4608 foreach ($this->query($query) as $switch) 4609 foreach ($this->query('case', $switch) as $case) 4610 while ($case->lastChild && $case->lastChild->nodeName === 'closeTag') 4611 $case->removeChild($case->lastChild); 4612 } 4613 } 4614 4615 /* 4616 * @package s9e\TextFormatter 4617 * @copyright Copyright (c) 2010-2019 The s9e Authors 4618 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4619 */ 4620 namespace s9e\TextFormatter\Configurator\Helpers\TemplateParser; 4621 use DOMDocument; 4622 use DOMElement; 4623 use DOMXPath; 4624 use RuntimeException; 4625 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 4626 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 4627 class Parser extends IRProcessor 4628 { 4629 protected $normalizer; 4630 public function __construct(Normalizer $normalizer) 4631 { 4632 $this->normalizer = $normalizer; 4633 } 4634 public function parse($template) 4635 { 4636 $dom = TemplateHelper::loadTemplate($template); 4637 $ir = new DOMDocument; 4638 $ir->loadXML('<template/>'); 4639 $this->createXPath($dom); 4640 $this->parseChildren($ir->documentElement, $dom->documentElement); 4641 $this->normalizer->normalize($ir); 4642 return $ir; 4643 } 4644 protected function appendAVT(DOMElement $parentNode, $avt) 4645 { 4646 foreach (AVTHelper::parse($avt) as $token) 4647 if ($token[0] === 'expression') 4648 $this->appendXPathOutput($parentNode, $token[1]); 4649 else 4650 $this->appendLiteralOutput($parentNode, $token[1]); 4651 } 4652 protected function appendLiteralOutput(DOMElement $parentNode, $content) 4653 { 4654 if ($content === '') 4655 return; 4656 $this->appendElement($parentNode, 'output', \htmlspecialchars($content)) 4657 ->setAttribute('type', 'literal'); 4658 } 4659 protected function appendConditionalAttributes(DOMElement $parentNode, $expr) 4660 { 4661 \preg_match_all('(@([-\\w]+))', $expr, $matches); 4662 foreach ($matches[1] as $attrName) 4663 { 4664 $switch = $this->appendElement($parentNode, 'switch'); 4665 $case = $this->appendElement($switch, 'case'); 4666 $case->setAttribute('test', '@' . $attrName); 4667 $attribute = $this->appendElement($case, 'attribute'); 4668 $attribute->setAttribute('name', $attrName); 4669 $this->appendXPathOutput($attribute, '@' . $attrName); 4670 } 4671 } 4672 protected function appendXPathOutput(DOMElement $parentNode, $expr) 4673 { 4674 $this->appendElement($parentNode, 'output', \htmlspecialchars(\trim($expr))) 4675 ->setAttribute('type', 'xpath'); 4676 } 4677 protected function parseChildren(DOMElement $ir, DOMElement $parent) 4678 { 4679 foreach ($parent->childNodes as $child) 4680 { 4681 switch ($child->nodeType) 4682 { 4683 case \XML_COMMENT_NODE: 4684 break; 4685 case \XML_TEXT_NODE: 4686 if (\trim($child->textContent) !== '') 4687 $this->appendLiteralOutput($ir, $child->textContent); 4688 break; 4689 case \XML_ELEMENT_NODE: 4690 $this->parseNode($ir, $child); 4691 break; 4692 default: 4693 throw new RuntimeException("Cannot parse node '" . $child->nodeName . "''"); 4694 } 4695 } 4696 } 4697 protected function parseNode(DOMElement $ir, DOMElement $node) 4698 { 4699 if ($node->namespaceURI === self::XMLNS_XSL) 4700 { 4701 $methodName = 'parseXsl' . \str_replace(' ', '', \ucwords(\str_replace('-', ' ', $node->localName))); 4702 if (!\method_exists($this, $methodName)) 4703 throw new RuntimeException("Element '" . $node->nodeName . "' is not supported"); 4704 return $this->$methodName($ir, $node); 4705 } 4706 $element = $this->appendElement($ir, 'element'); 4707 $element->setAttribute('name', $node->nodeName); 4708 $xpath = new DOMXPath($node->ownerDocument); 4709 foreach ($xpath->query('namespace::*', $node) as $ns) 4710 if ($node->hasAttribute($ns->nodeName)) 4711 { 4712 $irAttribute = $this->appendElement($element, 'attribute'); 4713 $irAttribute->setAttribute('name', $ns->nodeName); 4714 $this->appendLiteralOutput($irAttribute, $ns->nodeValue); 4715 } 4716 foreach ($node->attributes as $attribute) 4717 { 4718 $irAttribute = $this->appendElement($element, 'attribute'); 4719 $irAttribute->setAttribute('name', $attribute->nodeName); 4720 $this->appendAVT($irAttribute, $attribute->value); 4721 } 4722 $this->parseChildren($element, $node); 4723 } 4724 protected function parseXslApplyTemplates(DOMElement $ir, DOMElement $node) 4725 { 4726 $applyTemplates = $this->appendElement($ir, 'applyTemplates'); 4727 if ($node->hasAttribute('select')) 4728 $applyTemplates->setAttribute('select', $node->getAttribute('select')); 4729 } 4730 protected function parseXslAttribute(DOMElement $ir, DOMElement $node) 4731 { 4732 $attribute = $this->appendElement($ir, 'attribute'); 4733 $attribute->setAttribute('name', $node->getAttribute('name')); 4734 $this->parseChildren($attribute, $node); 4735 } 4736 protected function parseXslChoose(DOMElement $ir, DOMElement $node) 4737 { 4738 $switch = $this->appendElement($ir, 'switch'); 4739 foreach ($this->query('./xsl:when', $node) as $when) 4740 { 4741 $case = $this->appendElement($switch, 'case'); 4742 $case->setAttribute('test', $when->getAttribute('test')); 4743 $this->parseChildren($case, $when); 4744 } 4745 foreach ($this->query('./xsl:otherwise', $node) as $otherwise) 4746 { 4747 $case = $this->appendElement($switch, 'case'); 4748 $this->parseChildren($case, $otherwise); 4749 break; 4750 } 4751 } 4752 protected function parseXslComment(DOMElement $ir, DOMElement $node) 4753 { 4754 $comment = $this->appendElement($ir, 'comment'); 4755 $this->parseChildren($comment, $node); 4756 } 4757 protected function parseXslCopyOf(DOMElement $ir, DOMElement $node) 4758 { 4759 $expr = $node->getAttribute('select'); 4760 if (\preg_match('#^@[-\\w]+(?:\\s*\\|\\s*@[-\\w]+)*$#', $expr, $m)) 4761 $this->appendConditionalAttributes($ir, $expr); 4762 elseif ($expr === '@*') 4763 $this->appendElement($ir, 'copyOfAttributes'); 4764 else 4765 throw new RuntimeException("Unsupported <xsl:copy-of/> expression '" . $expr . "'"); 4766 } 4767 protected function parseXslElement(DOMElement $ir, DOMElement $node) 4768 { 4769 $element = $this->appendElement($ir, 'element'); 4770 $element->setAttribute('name', $node->getAttribute('name')); 4771 $this->parseChildren($element, $node); 4772 } 4773 protected function parseXslIf(DOMElement $ir, DOMElement $node) 4774 { 4775 $switch = $this->appendElement($ir, 'switch'); 4776 $case = $this->appendElement($switch, 'case'); 4777 $case->setAttribute('test', $node->getAttribute('test')); 4778 $this->parseChildren($case, $node); 4779 } 4780 protected function parseXslText(DOMElement $ir, DOMElement $node) 4781 { 4782 $this->appendLiteralOutput($ir, $node->textContent); 4783 if ($node->getAttribute('disable-output-escaping') === 'yes') 4784 $ir->lastChild->setAttribute('disable-output-escaping', 'yes'); 4785 } 4786 protected function parseXslValueOf(DOMElement $ir, DOMElement $node) 4787 { 4788 $this->appendXPathOutput($ir, $node->getAttribute('select')); 4789 if ($node->getAttribute('disable-output-escaping') === 'yes') 4790 $ir->lastChild->setAttribute('disable-output-escaping', 'yes'); 4791 } 4792 } 4793 4794 /* 4795 * @package s9e\TextFormatter 4796 * @copyright Copyright (c) 2010-2019 The s9e Authors 4797 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4798 */ 4799 namespace s9e\TextFormatter\Configurator\Items; 4800 use s9e\TextFormatter\Configurator\Collections\AttributeFilterChain; 4801 use s9e\TextFormatter\Configurator\ConfigProvider; 4802 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper; 4803 use s9e\TextFormatter\Configurator\Items\ProgrammableCallback; 4804 use s9e\TextFormatter\Configurator\Traits\Configurable; 4805 use s9e\TextFormatter\Configurator\Traits\TemplateSafeness; 4806 class Attribute implements ConfigProvider 4807 { 4808 use Configurable; 4809 use TemplateSafeness; 4810 protected $defaultValue; 4811 protected $filterChain; 4812 protected $required = \true; 4813 public function __construct(array $options = \null) 4814 { 4815 $this->filterChain = new AttributeFilterChain; 4816 if (isset($options)) 4817 foreach ($options as $optionName => $optionValue) 4818 $this->__set($optionName, $optionValue); 4819 } 4820 protected function isSafe($context) 4821 { 4822 $methodName = 'isSafe' . $context; 4823 foreach ($this->filterChain as $filter) 4824 if ($filter->$methodName()) 4825 return \true; 4826 return !empty($this->markedSafe[$context]); 4827 } 4828 public function asConfig() 4829 { 4830 $vars = \get_object_vars($this); 4831 unset($vars['markedSafe']); 4832 return ConfigHelper::toArray($vars) + ['filterChain' => []]; 4833 } 4834 } 4835 4836 /* 4837 * @package s9e\TextFormatter 4838 * @copyright Copyright (c) 2010-2019 The s9e Authors 4839 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4840 */ 4841 namespace s9e\TextFormatter\Configurator\Items; 4842 use InvalidArgumentException; 4843 use s9e\TextFormatter\Configurator\ConfigProvider; 4844 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper; 4845 use s9e\TextFormatter\Configurator\JavaScript\Code; 4846 use s9e\TextFormatter\Configurator\JavaScript\FunctionProvider; 4847 class ProgrammableCallback implements ConfigProvider 4848 { 4849 protected $callback; 4850 protected $js = 'returnFalse'; 4851 protected $params = []; 4852 protected $vars = []; 4853 public function __construct($callback) 4854 { 4855 if (!\is_callable($callback)) 4856 throw new InvalidArgumentException(__METHOD__ . '() expects a callback'); 4857 $this->callback = $this->normalizeCallback($callback); 4858 $this->autoloadJS(); 4859 } 4860 public function addParameterByValue($paramValue) 4861 { 4862 $this->params[] = $paramValue; 4863 return $this; 4864 } 4865 public function addParameterByName($paramName) 4866 { 4867 if (\array_key_exists($paramName, $this->params)) 4868 throw new InvalidArgumentException("Parameter '" . $paramName . "' already exists"); 4869 $this->params[$paramName] = \null; 4870 return $this; 4871 } 4872 public function getCallback() 4873 { 4874 return $this->callback; 4875 } 4876 public function getJS() 4877 { 4878 return $this->js; 4879 } 4880 public function getVars() 4881 { 4882 return $this->vars; 4883 } 4884 public function resetParameters() 4885 { 4886 $this->params = []; 4887 return $this; 4888 } 4889 public function setJS($js) 4890 { 4891 $this->js = $js; 4892 return $this; 4893 } 4894 public function setVar($name, $value) 4895 { 4896 $this->vars[$name] = $value; 4897 return $this; 4898 } 4899 public function setVars(array $vars) 4900 { 4901 $this->vars = $vars; 4902 return $this; 4903 } 4904 public function asConfig() 4905 { 4906 $config = ['callback' => $this->callback]; 4907 foreach ($this->params as $k => $v) 4908 if (\is_numeric($k)) 4909 $config['params'][] = $v; 4910 elseif (isset($this->vars[$k])) 4911 $config['params'][] = $this->vars[$k]; 4912 else 4913 $config['params'][$k] = \null; 4914 if (isset($config['params'])) 4915 $config['params'] = ConfigHelper::toArray($config['params'], \true, \true); 4916 $config['js'] = new Code($this->js); 4917 return $config; 4918 } 4919 protected function autoloadJS() 4920 { 4921 if (!\is_string($this->callback)) 4922 return; 4923 try 4924 { 4925 $this->js = FunctionProvider::get($this->callback); 4926 } 4927 catch (InvalidArgumentException $e) 4928 { 4929 } 4930 } 4931 protected function normalizeCallback($callback) 4932 { 4933 if (\is_array($callback) && \is_string($callback[0])) 4934 $callback = $callback[0] . '::' . $callback[1]; 4935 if (\is_string($callback)) 4936 $callback = \ltrim($callback, '\\'); 4937 return $callback; 4938 } 4939 } 4940 4941 /* 4942 * @package s9e\TextFormatter 4943 * @copyright Copyright (c) 2010-2019 The s9e Authors 4944 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 4945 */ 4946 namespace s9e\TextFormatter\Configurator\Items; 4947 use InvalidArgumentException; 4948 use s9e\TextFormatter\Configurator\ConfigProvider; 4949 use s9e\TextFormatter\Configurator\FilterableConfigValue; 4950 use s9e\TextFormatter\Configurator\Helpers\RegexpParser; 4951 use s9e\TextFormatter\Configurator\JavaScript\Code; 4952 use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor; 4953 class Regexp implements ConfigProvider, FilterableConfigValue 4954 { 4955 protected $isGlobal; 4956 protected $jsRegexp; 4957 protected $regexp; 4958 public function __construct($regexp, $isGlobal = \false) 4959 { 4960 if (@\preg_match($regexp, '') === \false) 4961 throw new InvalidArgumentException('Invalid regular expression ' . \var_export($regexp, \true)); 4962 $this->regexp = $regexp; 4963 $this->isGlobal = $isGlobal; 4964 } 4965 public function __toString() 4966 { 4967 return $this->regexp; 4968 } 4969 public function asConfig() 4970 { 4971 return $this; 4972 } 4973 public function filterConfig($target) 4974 { 4975 return ($target === 'JS') ? new Code($this->getJS()) : (string) $this; 4976 } 4977 public function getCaptureNames() 4978 { 4979 return RegexpParser::getCaptureNames($this->regexp); 4980 } 4981 public function getJS() 4982 { 4983 if (!isset($this->jsRegexp)) 4984 $this->jsRegexp = RegexpConvertor::toJS($this->regexp, $this->isGlobal); 4985 return $this->jsRegexp; 4986 } 4987 public function getNamedCaptures() 4988 { 4989 $captures = []; 4990 $regexpInfo = RegexpParser::parse($this->regexp); 4991 $start = $regexpInfo['delimiter'] . '^'; 4992 $end = '$' . $regexpInfo['delimiter'] . $regexpInfo['modifiers']; 4993 if (\strpos($regexpInfo['modifiers'], 'D') === \false) 4994 $end .= 'D'; 4995 foreach ($this->getNamedCapturesExpressions($regexpInfo['tokens']) as $name => $expr) 4996 $captures[$name] = $start . $expr . $end; 4997 return $captures; 4998 } 4999 protected function getNamedCapturesExpressions(array $tokens) 5000 { 5001 $exprs = []; 5002 foreach ($tokens as $token) 5003 { 5004 if ($token['type'] !== 'capturingSubpatternStart' || !isset($token['name'])) 5005 continue; 5006 $expr = $token['content']; 5007 if (\strpos($expr, '|') !== \false) 5008 $expr = '(?:' . $expr . ')'; 5009 $exprs[$token['name']] = $expr; 5010 } 5011 return $exprs; 5012 } 5013 public function setJS($jsRegexp) 5014 { 5015 $this->jsRegexp = $jsRegexp; 5016 } 5017 } 5018 5019 /* 5020 * @package s9e\TextFormatter 5021 * @copyright Copyright (c) 2010-2019 The s9e Authors 5022 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5023 */ 5024 namespace s9e\TextFormatter\Configurator\Items; 5025 use InvalidArgumentException; 5026 use s9e\TextFormatter\Configurator\Collections\AttributeCollection; 5027 use s9e\TextFormatter\Configurator\Collections\AttributePreprocessorCollection; 5028 use s9e\TextFormatter\Configurator\Collections\Ruleset; 5029 use s9e\TextFormatter\Configurator\Collections\TagFilterChain; 5030 use s9e\TextFormatter\Configurator\ConfigProvider; 5031 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper; 5032 use s9e\TextFormatter\Configurator\Items\Template; 5033 use s9e\TextFormatter\Configurator\Traits\Configurable; 5034 class Tag implements ConfigProvider 5035 { 5036 use Configurable; 5037 protected $attributes; 5038 protected $attributePreprocessors; 5039 protected $filterChain; 5040 protected $nestingLimit = 10; 5041 protected $rules; 5042 protected $tagLimit = 5000; 5043 protected $template; 5044 public function __construct(array $options = \null) 5045 { 5046 $this->attributes = new AttributeCollection; 5047 $this->attributePreprocessors = new AttributePreprocessorCollection; 5048 $this->filterChain = new TagFilterChain; 5049 $this->rules = new Ruleset; 5050 $this->filterChain->append('s9e\\TextFormatter\\Parser\\FilterProcessing::executeAttributePreprocessors') 5051 ->addParameterByName('tagConfig') 5052 ->setJS('executeAttributePreprocessors'); 5053 $this->filterChain->append('s9e\\TextFormatter\\Parser\\FilterProcessing::filterAttributes') 5054 ->addParameterByName('tagConfig') 5055 ->addParameterByName('registeredVars') 5056 ->addParameterByName('logger') 5057 ->setJS('filterAttributes'); 5058 if (isset($options)) 5059 { 5060 \ksort($options); 5061 foreach ($options as $optionName => $optionValue) 5062 $this->__set($optionName, $optionValue); 5063 } 5064 } 5065 public function asConfig() 5066 { 5067 $vars = \get_object_vars($this); 5068 unset($vars['template']); 5069 if (!\count($this->attributePreprocessors)) 5070 { 5071 $callback = 's9e\\TextFormatter\\Parser\\FilterProcessing::executeAttributePreprocessors'; 5072 $filterChain = clone $vars['filterChain']; 5073 $i = \count($filterChain); 5074 while (--$i >= 0) 5075 if ($filterChain[$i]->getCallback() === $callback) 5076 unset($filterChain[$i]); 5077 $vars['filterChain'] = $filterChain; 5078 } 5079 return ConfigHelper::toArray($vars) + ['attributes' => [], 'filterChain' => []]; 5080 } 5081 public function getTemplate() 5082 { 5083 return $this->template; 5084 } 5085 public function issetTemplate() 5086 { 5087 return isset($this->template); 5088 } 5089 public function setAttributePreprocessors($attributePreprocessors) 5090 { 5091 $this->attributePreprocessors->clear(); 5092 $this->attributePreprocessors->merge($attributePreprocessors); 5093 } 5094 public function setNestingLimit($limit) 5095 { 5096 $limit = (int) $limit; 5097 if ($limit < 1) 5098 throw new InvalidArgumentException('nestingLimit must be a number greater than 0'); 5099 $this->nestingLimit = $limit; 5100 } 5101 public function setRules($rules) 5102 { 5103 $this->rules->clear(); 5104 $this->rules->merge($rules); 5105 } 5106 public function setTagLimit($limit) 5107 { 5108 $limit = (int) $limit; 5109 if ($limit < 1) 5110 throw new InvalidArgumentException('tagLimit must be a number greater than 0'); 5111 $this->tagLimit = $limit; 5112 } 5113 public function setTemplate($template) 5114 { 5115 if (!($template instanceof Template)) 5116 $template = new Template($template); 5117 $this->template = $template; 5118 } 5119 public function unsetTemplate() 5120 { 5121 unset($this->template); 5122 } 5123 } 5124 5125 /* 5126 * @package s9e\TextFormatter 5127 * @copyright Copyright (c) 2010-2019 The s9e Authors 5128 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5129 */ 5130 namespace s9e\TextFormatter\Configurator\JavaScript; 5131 use s9e\TextFormatter\Configurator\FilterableConfigValue; 5132 class Code implements FilterableConfigValue 5133 { 5134 public $code; 5135 public function __construct($code) 5136 { 5137 $this->code = $code; 5138 } 5139 public function __toString() 5140 { 5141 return (string) $this->code; 5142 } 5143 public function filterConfig($target) 5144 { 5145 return ($target === 'JS') ? $this : \null; 5146 } 5147 } 5148 5149 /* 5150 * @package s9e\TextFormatter 5151 * @copyright Copyright (c) 2010-2019 The s9e Authors 5152 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5153 */ 5154 namespace s9e\TextFormatter\Configurator\RendererGenerators; 5155 use DOMElement; 5156 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 5157 use s9e\TextFormatter\Configurator\Helpers\TemplateParser; 5158 use s9e\TextFormatter\Configurator\RendererGenerator; 5159 use s9e\TextFormatter\Configurator\RendererGenerators\PHP\ControlStructuresOptimizer; 5160 use s9e\TextFormatter\Configurator\RendererGenerators\PHP\Optimizer; 5161 use s9e\TextFormatter\Configurator\RendererGenerators\PHP\Quick; 5162 use s9e\TextFormatter\Configurator\RendererGenerators\PHP\Serializer; 5163 use s9e\TextFormatter\Configurator\RendererGenerators\PHP\SwitchStatement; 5164 use s9e\TextFormatter\Configurator\Rendering; 5165 use s9e\TextFormatter\Configurator\TemplateNormalizer; 5166 class PHP implements RendererGenerator 5167 { 5168 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; 5169 public $cacheDir; 5170 public $className; 5171 public $controlStructuresOptimizer; 5172 public $defaultClassPrefix = 'Renderer_'; 5173 public $enableQuickRenderer = \true; 5174 public $filepath; 5175 public $lastClassName; 5176 public $lastFilepath; 5177 protected $normalizer; 5178 public $optimizer; 5179 public $serializer; 5180 public $useMultibyteStringFunctions; 5181 public function __construct($cacheDir = \null) 5182 { 5183 $this->cacheDir = (isset($cacheDir)) ? $cacheDir : \sys_get_temp_dir(); 5184 if (\extension_loaded('tokenizer')) 5185 { 5186 $this->controlStructuresOptimizer = new ControlStructuresOptimizer; 5187 $this->optimizer = new Optimizer; 5188 } 5189 $this->useMultibyteStringFunctions = \extension_loaded('mbstring'); 5190 $this->serializer = new Serializer; 5191 $this->normalizer = new TemplateNormalizer; 5192 $this->normalizer->clear(); 5193 $this->normalizer->append('RemoveLivePreviewAttributes'); 5194 } 5195 public function getRenderer(Rendering $rendering) 5196 { 5197 $php = $this->generate($rendering); 5198 if (isset($this->filepath)) 5199 $filepath = $this->filepath; 5200 else 5201 $filepath = $this->cacheDir . '/' . \str_replace('\\', '_', $this->lastClassName) . '.php'; 5202 \file_put_contents($filepath, "<?php\n" . $php); 5203 $this->lastFilepath = \realpath($filepath); 5204 if (!\class_exists($this->lastClassName, \false)) 5205 include $filepath; 5206 return new $this->lastClassName; 5207 } 5208 public function generate(Rendering $rendering) 5209 { 5210 $this->serializer->useMultibyteStringFunctions = $this->useMultibyteStringFunctions; 5211 $compiledTemplates = \array_map([$this, 'compileTemplate'], $rendering->getTemplates()); 5212 $php = []; 5213 $php[] = ' extends \\s9e\\TextFormatter\\Renderers\\PHP'; 5214 $php[] = '{'; 5215 $php[] = ' protected $params=' . self::export($rendering->getAllParameters()) . ';'; 5216 $php[] = ' protected function renderNode(\\DOMNode $node)'; 5217 $php[] = ' {'; 5218 $php[] = ' ' . SwitchStatement::generate('$node->nodeName', $compiledTemplates, '$this->at($node);'); 5219 $php[] = ' }'; 5220 if ($this->enableQuickRenderer) 5221 $php[] = Quick::getSource($compiledTemplates); 5222 $php[] = '}'; 5223 $php = \implode("\n", $php); 5224 if (isset($this->controlStructuresOptimizer)) 5225 $php = $this->controlStructuresOptimizer->optimize($php); 5226 $className = (isset($this->className)) 5227 ? $this->className 5228 : $this->defaultClassPrefix . \sha1($php); 5229 $this->lastClassName = $className; 5230 $header = "\n/**\n* @package s9e\TextFormatter\n* @copyright Copyright (c) 2010-2019 The s9e Authors\n* @license http://www.opensource.org/licenses/mit-license.php The MIT License\n*/\n"; 5231 $pos = \strrpos($className, '\\'); 5232 if ($pos !== \false) 5233 { 5234 $header .= 'namespace ' . \substr($className, 0, $pos) . ";\n\n"; 5235 $className = \substr($className, 1 + $pos); 5236 } 5237 $php = $header . 'class ' . $className . $php; 5238 return $php; 5239 } 5240 protected static function export(array $value) 5241 { 5242 $pairs = []; 5243 foreach ($value as $k => $v) 5244 $pairs[] = \var_export($k, \true) . '=>' . \var_export($v, \true); 5245 return '[' . \implode(',', $pairs) . ']'; 5246 } 5247 protected function compileTemplate($template) 5248 { 5249 $template = $this->normalizer->normalizeTemplate($template); 5250 $ir = TemplateParser::parse($template); 5251 $php = $this->serializer->serialize($ir->documentElement); 5252 if (isset($this->optimizer)) 5253 $php = $this->optimizer->optimize($php); 5254 return $php; 5255 } 5256 } 5257 5258 /* 5259 * @package s9e\TextFormatter 5260 * @copyright Copyright (c) 2010-2019 The s9e Authors 5261 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5262 */ 5263 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; 5264 class ControlStructuresOptimizer extends AbstractOptimizer 5265 { 5266 protected $braces; 5267 protected $context; 5268 protected function blockEndsWithIf() 5269 { 5270 return \in_array($this->context['lastBlock'], [\T_IF, \T_ELSEIF], \true); 5271 } 5272 protected function isControlStructure() 5273 { 5274 return \in_array( 5275 $this->tokens[$this->i][0], 5276 [\T_ELSE, \T_ELSEIF, \T_FOR, \T_FOREACH, \T_IF, \T_WHILE], 5277 \true 5278 ); 5279 } 5280 protected function isFollowedByElse() 5281 { 5282 if ($this->i > $this->cnt - 4) 5283 return \false; 5284 $k = $this->i + 1; 5285 if ($this->tokens[$k][0] === \T_WHITESPACE) 5286 ++$k; 5287 return \in_array($this->tokens[$k][0], [\T_ELSEIF, \T_ELSE], \true); 5288 } 5289 protected function mustPreserveBraces() 5290 { 5291 return ($this->blockEndsWithIf() && $this->isFollowedByElse()); 5292 } 5293 protected function optimizeTokens() 5294 { 5295 while (++$this->i < $this->cnt) 5296 if ($this->tokens[$this->i] === ';') 5297 ++$this->context['statements']; 5298 elseif ($this->tokens[$this->i] === '{') 5299 ++$this->braces; 5300 elseif ($this->tokens[$this->i] === '}') 5301 { 5302 if ($this->context['braces'] === $this->braces) 5303 $this->processEndOfBlock(); 5304 --$this->braces; 5305 } 5306 elseif ($this->isControlStructure()) 5307 $this->processControlStructure(); 5308 } 5309 protected function processControlStructure() 5310 { 5311 $savedIndex = $this->i; 5312 if (!\in_array($this->tokens[$this->i][0], [\T_ELSE, \T_ELSEIF], \true)) 5313 ++$this->context['statements']; 5314 if ($this->tokens[$this->i][0] !== \T_ELSE) 5315 $this->skipCondition(); 5316 $this->skipWhitespace(); 5317 if ($this->tokens[$this->i] !== '{') 5318 { 5319 $this->i = $savedIndex; 5320 return; 5321 } 5322 ++$this->braces; 5323 $replacement = [\T_WHITESPACE, '']; 5324 if ($this->tokens[$savedIndex][0] === \T_ELSE 5325 && $this->tokens[$this->i + 1][0] !== \T_VARIABLE 5326 && $this->tokens[$this->i + 1][0] !== \T_WHITESPACE) 5327 $replacement = [\T_WHITESPACE, ' ']; 5328 $this->context['lastBlock'] = $this->tokens[$savedIndex][0]; 5329 $this->context = [ 5330 'braces' => $this->braces, 5331 'index' => $this->i, 5332 'lastBlock' => \null, 5333 'parent' => $this->context, 5334 'replacement' => $replacement, 5335 'savedIndex' => $savedIndex, 5336 'statements' => 0 5337 ]; 5338 } 5339 protected function processEndOfBlock() 5340 { 5341 if ($this->context['statements'] < 2 && !$this->mustPreserveBraces()) 5342 $this->removeBracesInCurrentContext(); 5343 $this->context = $this->context['parent']; 5344 $this->context['parent']['lastBlock'] = $this->context['lastBlock']; 5345 } 5346 protected function removeBracesInCurrentContext() 5347 { 5348 $this->tokens[$this->context['index']] = $this->context['replacement']; 5349 $this->tokens[$this->i] = ($this->context['statements']) ? [\T_WHITESPACE, ''] : ';'; 5350 foreach ([$this->context['index'] - 1, $this->i - 1] as $tokenIndex) 5351 if ($this->tokens[$tokenIndex][0] === \T_WHITESPACE) 5352 $this->tokens[$tokenIndex][1] = ''; 5353 if ($this->tokens[$this->context['savedIndex']][0] === \T_ELSE) 5354 { 5355 $j = 1 + $this->context['savedIndex']; 5356 while ($this->tokens[$j][0] === \T_WHITESPACE 5357 || $this->tokens[$j][0] === \T_COMMENT 5358 || $this->tokens[$j][0] === \T_DOC_COMMENT) 5359 ++$j; 5360 if ($this->tokens[$j][0] === \T_IF) 5361 { 5362 $this->tokens[$j] = [\T_ELSEIF, 'elseif']; 5363 $j = $this->context['savedIndex']; 5364 $this->tokens[$j] = [\T_WHITESPACE, '']; 5365 if ($this->tokens[$j - 1][0] === \T_WHITESPACE) 5366 $this->tokens[$j - 1][1] = ''; 5367 $this->unindentBlock($j, $this->i - 1); 5368 $this->tokens[$this->context['index']] = [\T_WHITESPACE, '']; 5369 } 5370 } 5371 $this->changed = \true; 5372 } 5373 protected function reset($php) 5374 { 5375 parent::reset($php); 5376 $this->braces = 0; 5377 $this->context = [ 5378 'braces' => 0, 5379 'index' => -1, 5380 'parent' => [], 5381 'preventElse' => \false, 5382 'savedIndex' => 0, 5383 'statements' => 0 5384 ]; 5385 } 5386 protected function skipCondition() 5387 { 5388 $this->skipToString('('); 5389 $parens = 0; 5390 while (++$this->i < $this->cnt) 5391 if ($this->tokens[$this->i] === ')') 5392 if ($parens) 5393 --$parens; 5394 else 5395 break; 5396 elseif ($this->tokens[$this->i] === '(') 5397 ++$parens; 5398 } 5399 } 5400 5401 /* 5402 * @package s9e\TextFormatter 5403 * @copyright Copyright (c) 2010-2019 The s9e Authors 5404 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5405 */ 5406 namespace s9e\TextFormatter\Configurator; 5407 use ReflectionClass; 5408 use RuntimeException; 5409 use s9e\TextFormatter\Configurator; 5410 use s9e\TextFormatter\Configurator\Collections\TemplateParameterCollection; 5411 use s9e\TextFormatter\Configurator\RendererGenerator; 5412 use s9e\TextFormatter\Configurator\Traits\Configurable; 5413 class Rendering 5414 { 5415 use Configurable; 5416 protected $configurator; 5417 protected $engine; 5418 protected $parameters; 5419 public function __construct(Configurator $configurator) 5420 { 5421 $this->configurator = $configurator; 5422 $this->parameters = new TemplateParameterCollection; 5423 } 5424 public function getAllParameters() 5425 { 5426 $params = []; 5427 foreach ($this->configurator->tags as $tag) 5428 if (isset($tag->template)) 5429 foreach ($tag->template->getParameters() as $paramName) 5430 $params[$paramName] = ''; 5431 $params = \iterator_to_array($this->parameters) + $params; 5432 \ksort($params); 5433 return $params; 5434 } 5435 public function getEngine() 5436 { 5437 if (!isset($this->engine)) 5438 $this->setEngine('XSLT'); 5439 return $this->engine; 5440 } 5441 public function getRenderer() 5442 { 5443 return $this->getEngine()->getRenderer($this); 5444 } 5445 public function getTemplates() 5446 { 5447 $templates = [ 5448 'br' => '<br/>', 5449 'e' => '', 5450 'i' => '', 5451 'p' => '<p><xsl:apply-templates/></p>', 5452 's' => '' 5453 ]; 5454 foreach ($this->configurator->tags as $tagName => $tag) 5455 if (isset($tag->template)) 5456 $templates[$tagName] = (string) $tag->template; 5457 \ksort($templates); 5458 return $templates; 5459 } 5460 public function setEngine($engine) 5461 { 5462 if (!($engine instanceof RendererGenerator)) 5463 { 5464 $className = 's9e\\TextFormatter\\Configurator\\RendererGenerators\\' . $engine; 5465 $reflection = new ReflectionClass($className); 5466 $engine = $reflection->newInstanceArgs(\array_slice(\func_get_args(), 1)); 5467 } 5468 $this->engine = $engine; 5469 return $engine; 5470 } 5471 } 5472 5473 /* 5474 * @package s9e\TextFormatter 5475 * @copyright Copyright (c) 2010-2019 The s9e Authors 5476 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5477 */ 5478 namespace s9e\TextFormatter\Configurator; 5479 use ArrayAccess; 5480 use DOMDocument; 5481 use Iterator; 5482 use s9e\TextFormatter\Configurator\Collections\RulesGeneratorList; 5483 use s9e\TextFormatter\Configurator\Collections\TagCollection; 5484 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5485 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 5486 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator; 5487 use s9e\TextFormatter\Configurator\Traits\CollectionProxy; 5488 class RulesGenerator implements ArrayAccess, Iterator 5489 { 5490 use CollectionProxy; 5491 protected $collection; 5492 public function __construct() 5493 { 5494 $this->collection = new RulesGeneratorList; 5495 $this->collection->append('AutoCloseIfVoid'); 5496 $this->collection->append('AutoReopenFormattingElements'); 5497 $this->collection->append('BlockElementsCloseFormattingElements'); 5498 $this->collection->append('BlockElementsFosterFormattingElements'); 5499 $this->collection->append('DisableAutoLineBreaksIfNewLinesArePreserved'); 5500 $this->collection->append('EnforceContentModels'); 5501 $this->collection->append('EnforceOptionalEndTags'); 5502 $this->collection->append('IgnoreTagsInCode'); 5503 $this->collection->append('IgnoreTextIfDisallowed'); 5504 $this->collection->append('IgnoreWhitespaceAroundBlockElements'); 5505 $this->collection->append('TrimFirstLineInCodeBlocks'); 5506 } 5507 public function getRules(TagCollection $tags) 5508 { 5509 $tagInspectors = $this->getTagInspectors($tags); 5510 return [ 5511 'root' => $this->generateRootRules($tagInspectors), 5512 'tags' => $this->generateTagRules($tagInspectors) 5513 ]; 5514 } 5515 protected function generateTagRules(array $tagInspectors) 5516 { 5517 $rules = []; 5518 foreach ($tagInspectors as $tagName => $tagInspector) 5519 $rules[$tagName] = $this->generateRuleset($tagInspector, $tagInspectors); 5520 return $rules; 5521 } 5522 protected function generateRootRules(array $tagInspectors) 5523 { 5524 $rootInspector = new TemplateInspector('<div><xsl:apply-templates/></div>'); 5525 $rules = $this->generateRuleset($rootInspector, $tagInspectors); 5526 unset($rules['autoClose']); 5527 unset($rules['autoReopen']); 5528 unset($rules['breakParagraph']); 5529 unset($rules['closeAncestor']); 5530 unset($rules['closeParent']); 5531 unset($rules['fosterParent']); 5532 unset($rules['ignoreSurroundingWhitespace']); 5533 unset($rules['isTransparent']); 5534 unset($rules['requireAncestor']); 5535 unset($rules['requireParent']); 5536 return $rules; 5537 } 5538 protected function generateRuleset(TemplateInspector $srcInspector, array $trgInspectors) 5539 { 5540 $rules = []; 5541 foreach ($this->collection as $rulesGenerator) 5542 { 5543 if ($rulesGenerator instanceof BooleanRulesGenerator) 5544 foreach ($rulesGenerator->generateBooleanRules($srcInspector) as $ruleName => $bool) 5545 $rules[$ruleName] = $bool; 5546 if ($rulesGenerator instanceof TargetedRulesGenerator) 5547 foreach ($trgInspectors as $tagName => $trgInspector) 5548 { 5549 $targetedRules = $rulesGenerator->generateTargetedRules($srcInspector, $trgInspector); 5550 foreach ($targetedRules as $ruleName) 5551 $rules[$ruleName][] = $tagName; 5552 } 5553 } 5554 return $rules; 5555 } 5556 protected function getTagInspectors(TagCollection $tags) 5557 { 5558 $tagInspectors = []; 5559 foreach ($tags as $tagName => $tag) 5560 { 5561 $template = (isset($tag->template)) ? $tag->template : '<xsl:apply-templates/>'; 5562 $tagInspectors[$tagName] = new TemplateInspector($template); 5563 } 5564 return $tagInspectors; 5565 } 5566 } 5567 5568 /* 5569 * @package s9e\TextFormatter 5570 * @copyright Copyright (c) 2010-2019 The s9e Authors 5571 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5572 */ 5573 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5574 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5575 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 5576 class AutoCloseIfVoid implements BooleanRulesGenerator 5577 { 5578 public function generateBooleanRules(TemplateInspector $src) 5579 { 5580 return ($src->isVoid()) ? ['autoClose' => \true] : []; 5581 } 5582 } 5583 5584 /* 5585 * @package s9e\TextFormatter 5586 * @copyright Copyright (c) 2010-2019 The s9e Authors 5587 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5588 */ 5589 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5590 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5591 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 5592 class AutoReopenFormattingElements implements BooleanRulesGenerator 5593 { 5594 public function generateBooleanRules(TemplateInspector $src) 5595 { 5596 return ($src->isFormattingElement()) ? ['autoReopen' => \true] : []; 5597 } 5598 } 5599 5600 /* 5601 * @package s9e\TextFormatter 5602 * @copyright Copyright (c) 2010-2019 The s9e Authors 5603 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5604 */ 5605 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5606 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5607 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator; 5608 class BlockElementsCloseFormattingElements implements TargetedRulesGenerator 5609 { 5610 public function generateTargetedRules(TemplateInspector $src, TemplateInspector $trg) 5611 { 5612 return ($src->isBlock() && $trg->isFormattingElement()) ? ['closeParent'] : []; 5613 } 5614 } 5615 5616 /* 5617 * @package s9e\TextFormatter 5618 * @copyright Copyright (c) 2010-2019 The s9e Authors 5619 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5620 */ 5621 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5622 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5623 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator; 5624 class BlockElementsFosterFormattingElements implements TargetedRulesGenerator 5625 { 5626 public function generateTargetedRules(TemplateInspector $src, TemplateInspector $trg) 5627 { 5628 return ($src->isBlock() && $src->isPassthrough() && $trg->isFormattingElement()) ? ['fosterParent'] : []; 5629 } 5630 } 5631 5632 /* 5633 * @package s9e\TextFormatter 5634 * @copyright Copyright (c) 2010-2019 The s9e Authors 5635 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5636 */ 5637 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5638 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5639 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 5640 class DisableAutoLineBreaksIfNewLinesArePreserved implements BooleanRulesGenerator 5641 { 5642 public function generateBooleanRules(TemplateInspector $src) 5643 { 5644 return ($src->preservesNewLines()) ? ['disableAutoLineBreaks' => \true] : []; 5645 } 5646 } 5647 5648 /* 5649 * @package s9e\TextFormatter 5650 * @copyright Copyright (c) 2010-2019 The s9e Authors 5651 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5652 */ 5653 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5654 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5655 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 5656 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator; 5657 class EnforceContentModels implements BooleanRulesGenerator, TargetedRulesGenerator 5658 { 5659 protected $br; 5660 protected $span; 5661 public function __construct() 5662 { 5663 $this->br = new TemplateInspector('<br/>'); 5664 $this->span = new TemplateInspector('<span><xsl:apply-templates/></span>'); 5665 } 5666 public function generateBooleanRules(TemplateInspector $src) 5667 { 5668 $rules = []; 5669 if ($src->isTransparent()) 5670 $rules['isTransparent'] = \true; 5671 if (!$src->allowsChild($this->br)) 5672 { 5673 $rules['preventLineBreaks'] = \true; 5674 $rules['suspendAutoLineBreaks'] = \true; 5675 } 5676 if (!$src->allowsDescendant($this->br)) 5677 { 5678 $rules['disableAutoLineBreaks'] = \true; 5679 $rules['preventLineBreaks'] = \true; 5680 } 5681 return $rules; 5682 } 5683 public function generateTargetedRules(TemplateInspector $src, TemplateInspector $trg) 5684 { 5685 $rules = []; 5686 if ($src->allowsChild($trg)) 5687 $rules[] = 'allowChild'; 5688 if ($src->allowsDescendant($trg)) 5689 $rules[] = 'allowDescendant'; 5690 return $rules; 5691 } 5692 } 5693 5694 /* 5695 * @package s9e\TextFormatter 5696 * @copyright Copyright (c) 2010-2019 The s9e Authors 5697 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5698 */ 5699 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5700 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5701 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator; 5702 class EnforceOptionalEndTags implements TargetedRulesGenerator 5703 { 5704 public function generateTargetedRules(TemplateInspector $src, TemplateInspector $trg) 5705 { 5706 return ($src->closesParent($trg)) ? ['closeParent'] : []; 5707 } 5708 } 5709 5710 /* 5711 * @package s9e\TextFormatter 5712 * @copyright Copyright (c) 2010-2019 The s9e Authors 5713 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5714 */ 5715 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5716 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5717 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 5718 class IgnoreTagsInCode implements BooleanRulesGenerator 5719 { 5720 public function generateBooleanRules(TemplateInspector $src) 5721 { 5722 return ($src->evaluate('count(//code//xsl:apply-templates)')) ? ['ignoreTags' => \true] : []; 5723 } 5724 } 5725 5726 /* 5727 * @package s9e\TextFormatter 5728 * @copyright Copyright (c) 2010-2019 The s9e Authors 5729 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5730 */ 5731 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5732 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5733 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 5734 class IgnoreTextIfDisallowed implements BooleanRulesGenerator 5735 { 5736 public function generateBooleanRules(TemplateInspector $src) 5737 { 5738 return ($src->allowsText()) ? [] : ['ignoreText' => \true]; 5739 } 5740 } 5741 5742 /* 5743 * @package s9e\TextFormatter 5744 * @copyright Copyright (c) 2010-2019 The s9e Authors 5745 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5746 */ 5747 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5748 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5749 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 5750 class IgnoreWhitespaceAroundBlockElements implements BooleanRulesGenerator 5751 { 5752 public function generateBooleanRules(TemplateInspector $src) 5753 { 5754 return ($src->isBlock()) ? ['ignoreSurroundingWhitespace' => \true] : []; 5755 } 5756 } 5757 5758 /* 5759 * @package s9e\TextFormatter 5760 * @copyright Copyright (c) 2010-2019 The s9e Authors 5761 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5762 */ 5763 namespace s9e\TextFormatter\Configurator\RulesGenerators; 5764 use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; 5765 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 5766 class TrimFirstLineInCodeBlocks implements BooleanRulesGenerator 5767 { 5768 public function generateBooleanRules(TemplateInspector $src) 5769 { 5770 return ($src->evaluate('count(//pre//code//xsl:apply-templates)')) ? ['trimFirstLine' => \true] : []; 5771 } 5772 } 5773 5774 /* 5775 * @package s9e\TextFormatter 5776 * @copyright Copyright (c) 2010-2019 The s9e Authors 5777 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5778 */ 5779 namespace s9e\TextFormatter\Configurator; 5780 use ArrayAccess; 5781 use Iterator; 5782 use s9e\TextFormatter\Configurator\Collections\TemplateCheckList; 5783 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 5784 use s9e\TextFormatter\Configurator\Items\Tag; 5785 use s9e\TextFormatter\Configurator\Items\UnsafeTemplate; 5786 use s9e\TextFormatter\Configurator\TemplateChecks\DisallowElementNS; 5787 use s9e\TextFormatter\Configurator\TemplateChecks\DisallowXPathFunction; 5788 use s9e\TextFormatter\Configurator\TemplateChecks\RestrictFlashScriptAccess; 5789 use s9e\TextFormatter\Configurator\Traits\CollectionProxy; 5790 class TemplateChecker implements ArrayAccess, Iterator 5791 { 5792 use CollectionProxy; 5793 protected $collection; 5794 protected $disabled = \false; 5795 public function __construct() 5796 { 5797 $this->collection = new TemplateCheckList; 5798 $this->collection->append('DisallowAttributeSets'); 5799 $this->collection->append('DisallowCopy'); 5800 $this->collection->append('DisallowDisableOutputEscaping'); 5801 $this->collection->append('DisallowDynamicAttributeNames'); 5802 $this->collection->append('DisallowDynamicElementNames'); 5803 $this->collection->append('DisallowObjectParamsWithGeneratedName'); 5804 $this->collection->append('DisallowPHPTags'); 5805 $this->collection->append('DisallowUnsafeCopyOf'); 5806 $this->collection->append('DisallowUnsafeDynamicCSS'); 5807 $this->collection->append('DisallowUnsafeDynamicJS'); 5808 $this->collection->append('DisallowUnsafeDynamicURL'); 5809 $this->collection->append(new DisallowElementNS('http://icl.com/saxon', 'output')); 5810 $this->collection->append(new DisallowXPathFunction('document')); 5811 $this->collection->append(new RestrictFlashScriptAccess('sameDomain', \true)); 5812 } 5813 public function checkTag(Tag $tag) 5814 { 5815 if (isset($tag->template) && !($tag->template instanceof UnsafeTemplate)) 5816 { 5817 $template = (string) $tag->template; 5818 $this->checkTemplate($template, $tag); 5819 } 5820 } 5821 public function checkTemplate($template, Tag $tag = \null) 5822 { 5823 if ($this->disabled) 5824 return; 5825 if (!isset($tag)) 5826 $tag = new Tag; 5827 $dom = TemplateHelper::loadTemplate($template); 5828 foreach ($this->collection as $check) 5829 $check->check($dom->documentElement, $tag); 5830 } 5831 public function disable() 5832 { 5833 $this->disabled = \true; 5834 } 5835 public function enable() 5836 { 5837 $this->disabled = \false; 5838 } 5839 } 5840 5841 /* 5842 * @package s9e\TextFormatter 5843 * @copyright Copyright (c) 2010-2019 The s9e Authors 5844 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5845 */ 5846 namespace s9e\TextFormatter\Configurator\TemplateChecks; 5847 use DOMAttr; 5848 use DOMElement; 5849 use DOMNode; 5850 use DOMXPath; 5851 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 5852 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 5853 use s9e\TextFormatter\Configurator\Items\Attribute; 5854 use s9e\TextFormatter\Configurator\Items\Tag; 5855 use s9e\TextFormatter\Configurator\TemplateCheck; 5856 abstract class AbstractDynamicContentCheck extends TemplateCheck 5857 { 5858 protected $ignoreUnknownAttributes = \false; 5859 abstract protected function getNodes(DOMElement $template); 5860 abstract protected function isSafe(Attribute $attribute); 5861 public function check(DOMElement $template, Tag $tag) 5862 { 5863 foreach ($this->getNodes($template) as $node) 5864 $this->checkNode($node, $tag); 5865 } 5866 public function detectUnknownAttributes() 5867 { 5868 $this->ignoreUnknownAttributes = \false; 5869 } 5870 public function ignoreUnknownAttributes() 5871 { 5872 $this->ignoreUnknownAttributes = \true; 5873 } 5874 protected function checkAttribute(DOMNode $node, Tag $tag, $attrName) 5875 { 5876 if (!isset($tag->attributes[$attrName])) 5877 { 5878 if ($this->ignoreUnknownAttributes) 5879 return; 5880 throw new UnsafeTemplateException("Cannot assess the safety of unknown attribute '" . $attrName . "'", $node); 5881 } 5882 if (!$this->tagFiltersAttributes($tag) || !$this->isSafe($tag->attributes[$attrName])) 5883 throw new UnsafeTemplateException("Attribute '" . $attrName . "' is not properly sanitized to be used in this context", $node); 5884 } 5885 protected function checkAttributeExpression(DOMNode $node, Tag $tag, $expr) 5886 { 5887 \preg_match_all('(@([-\\w]+))', $expr, $matches); 5888 foreach ($matches[1] as $attrName) 5889 $this->checkAttribute($node, $tag, $attrName); 5890 } 5891 protected function checkAttributeNode(DOMAttr $attribute, Tag $tag) 5892 { 5893 foreach (AVTHelper::parse($attribute->value) as $token) 5894 if ($token[0] === 'expression') 5895 $this->checkExpression($attribute, $token[1], $tag); 5896 } 5897 protected function checkContext(DOMNode $node) 5898 { 5899 $xpath = new DOMXPath($node->ownerDocument); 5900 $ancestors = $xpath->query('ancestor::xsl:for-each', $node); 5901 if ($ancestors->length) 5902 throw new UnsafeTemplateException("Cannot assess context due to '" . $ancestors->item(0)->nodeName . "'", $node); 5903 } 5904 protected function checkCopyOfNode(DOMElement $node, Tag $tag) 5905 { 5906 $this->checkSelectNode($node->getAttributeNode('select'), $tag); 5907 } 5908 protected function checkElementNode(DOMElement $element, Tag $tag) 5909 { 5910 $xpath = new DOMXPath($element->ownerDocument); 5911 $predicate = ($element->localName === 'attribute') ? '' : '[not(ancestor::xsl:attribute)]'; 5912 $query = './/xsl:value-of' . $predicate; 5913 foreach ($xpath->query($query, $element) as $valueOf) 5914 $this->checkSelectNode($valueOf->getAttributeNode('select'), $tag); 5915 $query = './/xsl:apply-templates' . $predicate; 5916 foreach ($xpath->query($query, $element) as $applyTemplates) 5917 throw new UnsafeTemplateException('Cannot allow unfiltered data in this context', $applyTemplates); 5918 } 5919 protected function checkExpression(DOMNode $node, $expr, Tag $tag) 5920 { 5921 $this->checkContext($node); 5922 if (\preg_match('/^\\$(\\w+)$/', $expr, $m)) 5923 $this->checkVariable($node, $tag, $m[1]); 5924 elseif (\preg_match('/^@[-\\w]+(?:\\s*\\|\\s*@[-\\w]+)*$/', $expr)) 5925 $this->checkAttributeExpression($node, $tag, $expr); 5926 elseif (!$this->isExpressionSafe($expr)) 5927 throw new UnsafeTemplateException("Cannot assess the safety of expression '" . $expr . "'", $node); 5928 } 5929 protected function checkNode(DOMNode $node, Tag $tag) 5930 { 5931 if ($node instanceof DOMAttr) 5932 $this->checkAttributeNode($node, $tag); 5933 elseif ($node instanceof DOMElement) 5934 if ($node->namespaceURI === self::XMLNS_XSL && $node->localName === 'copy-of') 5935 $this->checkCopyOfNode($node, $tag); 5936 else 5937 $this->checkElementNode($node, $tag); 5938 } 5939 protected function checkVariable(DOMNode $node, $tag, $qname) 5940 { 5941 $this->checkVariableDeclaration($node, $tag, 'xsl:param[@name="' . $qname . '"]'); 5942 $this->checkVariableDeclaration($node, $tag, 'xsl:variable[@name="' . $qname . '"]'); 5943 } 5944 protected function checkVariableDeclaration(DOMNode $node, $tag, $query) 5945 { 5946 $query = 'ancestor-or-self::*/preceding-sibling::' . $query . '[@select]'; 5947 $xpath = new DOMXPath($node->ownerDocument); 5948 foreach ($xpath->query($query, $node) as $varNode) 5949 { 5950 try 5951 { 5952 $this->checkExpression($varNode, $varNode->getAttribute('select'), $tag); 5953 } 5954 catch (UnsafeTemplateException $e) 5955 { 5956 $e->setNode($node); 5957 throw $e; 5958 } 5959 } 5960 } 5961 protected function checkSelectNode(DOMAttr $select, Tag $tag) 5962 { 5963 $this->checkExpression($select, $select->value, $tag); 5964 } 5965 protected function isExpressionSafe($expr) 5966 { 5967 return \false; 5968 } 5969 protected function tagFiltersAttributes(Tag $tag) 5970 { 5971 return $tag->filterChain->containsCallback('s9e\\TextFormatter\\Parser\\FilterProcessing::filterAttributes'); 5972 } 5973 } 5974 5975 /* 5976 * @package s9e\TextFormatter 5977 * @copyright Copyright (c) 2010-2019 The s9e Authors 5978 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 5979 */ 5980 namespace s9e\TextFormatter\Configurator\TemplateChecks; 5981 use DOMElement; 5982 use DOMNode; 5983 use DOMXPath; 5984 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 5985 use s9e\TextFormatter\Configurator\Items\Tag; 5986 use s9e\TextFormatter\Configurator\TemplateCheck; 5987 abstract class AbstractFlashRestriction extends TemplateCheck 5988 { 5989 public $defaultSetting; 5990 public $maxSetting; 5991 public $onlyIfDynamic; 5992 protected $settingName; 5993 protected $settings; 5994 protected $template; 5995 public function __construct($maxSetting, $onlyIfDynamic = \false) 5996 { 5997 $this->maxSetting = $maxSetting; 5998 $this->onlyIfDynamic = $onlyIfDynamic; 5999 } 6000 public function check(DOMElement $template, Tag $tag) 6001 { 6002 $this->template = $template; 6003 $this->checkEmbeds(); 6004 $this->checkObjects(); 6005 } 6006 protected function checkAttributes(DOMElement $embed) 6007 { 6008 $settingName = \strtolower($this->settingName); 6009 $useDefault = \true; 6010 foreach ($embed->attributes as $attribute) 6011 { 6012 $attrName = \strtolower($attribute->name); 6013 if ($attrName === $settingName) 6014 { 6015 $this->checkSetting($attribute, $attribute->value); 6016 $useDefault = \false; 6017 } 6018 } 6019 if ($useDefault) 6020 $this->checkSetting($embed, $this->defaultSetting); 6021 } 6022 protected function checkDynamicAttributes(DOMElement $embed) 6023 { 6024 $settingName = \strtolower($this->settingName); 6025 foreach ($embed->getElementsByTagNameNS(self::XMLNS_XSL, 'attribute') as $attribute) 6026 { 6027 $attrName = \strtolower($attribute->getAttribute('name')); 6028 if ($attrName === $settingName) 6029 throw new UnsafeTemplateException('Cannot assess the safety of dynamic attributes', $attribute); 6030 } 6031 } 6032 protected function checkDynamicParams(DOMElement $object) 6033 { 6034 foreach ($this->getObjectParams($object) as $param) 6035 foreach ($param->getElementsByTagNameNS(self::XMLNS_XSL, 'attribute') as $attribute) 6036 if (\strtolower($attribute->getAttribute('name')) === 'value') 6037 throw new UnsafeTemplateException('Cannot assess the safety of dynamic attributes', $attribute); 6038 } 6039 protected function checkEmbeds() 6040 { 6041 foreach ($this->getElements('embed') as $embed) 6042 { 6043 $this->checkDynamicAttributes($embed); 6044 $this->checkAttributes($embed); 6045 } 6046 } 6047 protected function checkObjects() 6048 { 6049 foreach ($this->getElements('object') as $object) 6050 { 6051 $this->checkDynamicParams($object); 6052 $params = $this->getObjectParams($object); 6053 foreach ($params as $param) 6054 $this->checkSetting($param, $param->getAttribute('value')); 6055 if (empty($params)) 6056 $this->checkSetting($object, $this->defaultSetting); 6057 } 6058 } 6059 protected function checkSetting(DOMNode $node, $setting) 6060 { 6061 if (!isset($this->settings[\strtolower($setting)])) 6062 { 6063 if (\preg_match('/(?<!\\{)\\{(?:\\{\\{)*(?!\\{)/', $setting)) 6064 throw new UnsafeTemplateException('Cannot assess ' . $this->settingName . " setting '" . $setting . "'", $node); 6065 throw new UnsafeTemplateException('Unknown ' . $this->settingName . " value '" . $setting . "'", $node); 6066 } 6067 $value = $this->settings[\strtolower($setting)]; 6068 $maxValue = $this->settings[\strtolower($this->maxSetting)]; 6069 if ($value > $maxValue) 6070 throw new UnsafeTemplateException($this->settingName . " setting '" . $setting . "' exceeds restricted value '" . $this->maxSetting . "'", $node); 6071 } 6072 protected function isDynamic(DOMElement $node) 6073 { 6074 if ($node->getElementsByTagNameNS(self::XMLNS_XSL, '*')->length) 6075 return \true; 6076 $xpath = new DOMXPath($node->ownerDocument); 6077 $query = './/@*[contains(., "{")]'; 6078 foreach ($xpath->query($query, $node) as $attribute) 6079 if (\preg_match('/(?<!\\{)\\{(?:\\{\\{)*(?!\\{)/', $attribute->value)) 6080 return \true; 6081 return \false; 6082 } 6083 protected function getElements($tagName) 6084 { 6085 $nodes = []; 6086 foreach ($this->template->ownerDocument->getElementsByTagName($tagName) as $node) 6087 if (!$this->onlyIfDynamic || $this->isDynamic($node)) 6088 $nodes[] = $node; 6089 return $nodes; 6090 } 6091 protected function getObjectParams(DOMElement $object) 6092 { 6093 $params = []; 6094 $settingName = \strtolower($this->settingName); 6095 foreach ($object->getElementsByTagName('param') as $param) 6096 { 6097 $paramName = \strtolower($param->getAttribute('name')); 6098 if ($paramName === $settingName && $param->parentNode->isSameNode($object)) 6099 $params[] = $param; 6100 } 6101 return $params; 6102 } 6103 } 6104 6105 /* 6106 * @package s9e\TextFormatter 6107 * @copyright Copyright (c) 2010-2019 The s9e Authors 6108 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6109 */ 6110 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6111 use DOMElement; 6112 use DOMXPath; 6113 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6114 use s9e\TextFormatter\Configurator\Items\Tag; 6115 use s9e\TextFormatter\Configurator\TemplateCheck; 6116 class DisallowAttributeSets extends TemplateCheck 6117 { 6118 public function check(DOMElement $template, Tag $tag) 6119 { 6120 $xpath = new DOMXPath($template->ownerDocument); 6121 $nodes = $xpath->query('//@use-attribute-sets'); 6122 if ($nodes->length) 6123 throw new UnsafeTemplateException('Cannot assess the safety of attribute sets', $nodes->item(0)); 6124 } 6125 } 6126 6127 /* 6128 * @package s9e\TextFormatter 6129 * @copyright Copyright (c) 2010-2019 The s9e Authors 6130 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6131 */ 6132 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6133 use DOMElement; 6134 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6135 use s9e\TextFormatter\Configurator\Items\Tag; 6136 use s9e\TextFormatter\Configurator\TemplateCheck; 6137 class DisallowCopy extends TemplateCheck 6138 { 6139 public function check(DOMElement $template, Tag $tag) 6140 { 6141 $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'copy'); 6142 $node = $nodes->item(0); 6143 if ($node) 6144 throw new UnsafeTemplateException("Cannot assess the safety of an '" . $node->nodeName . "' element", $node); 6145 } 6146 } 6147 6148 /* 6149 * @package s9e\TextFormatter 6150 * @copyright Copyright (c) 2010-2019 The s9e Authors 6151 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6152 */ 6153 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6154 use DOMElement; 6155 use DOMXPath; 6156 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6157 use s9e\TextFormatter\Configurator\Items\Tag; 6158 use s9e\TextFormatter\Configurator\TemplateCheck; 6159 class DisallowDisableOutputEscaping extends TemplateCheck 6160 { 6161 public function check(DOMElement $template, Tag $tag) 6162 { 6163 $xpath = new DOMXPath($template->ownerDocument); 6164 $node = $xpath->query('//@disable-output-escaping')->item(0); 6165 if ($node) 6166 throw new UnsafeTemplateException("The template contains a 'disable-output-escaping' attribute", $node); 6167 } 6168 } 6169 6170 /* 6171 * @package s9e\TextFormatter 6172 * @copyright Copyright (c) 2010-2019 The s9e Authors 6173 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6174 */ 6175 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6176 use DOMElement; 6177 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6178 use s9e\TextFormatter\Configurator\Items\Tag; 6179 use s9e\TextFormatter\Configurator\TemplateCheck; 6180 class DisallowDynamicAttributeNames extends TemplateCheck 6181 { 6182 public function check(DOMElement $template, Tag $tag) 6183 { 6184 $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'attribute'); 6185 foreach ($nodes as $node) 6186 if (\strpos($node->getAttribute('name'), '{') !== \false) 6187 throw new UnsafeTemplateException('Dynamic <xsl:attribute/> names are disallowed', $node); 6188 } 6189 } 6190 6191 /* 6192 * @package s9e\TextFormatter 6193 * @copyright Copyright (c) 2010-2019 The s9e Authors 6194 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6195 */ 6196 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6197 use DOMElement; 6198 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6199 use s9e\TextFormatter\Configurator\Items\Tag; 6200 use s9e\TextFormatter\Configurator\TemplateCheck; 6201 class DisallowDynamicElementNames extends TemplateCheck 6202 { 6203 public function check(DOMElement $template, Tag $tag) 6204 { 6205 $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'element'); 6206 foreach ($nodes as $node) 6207 if (\strpos($node->getAttribute('name'), '{') !== \false) 6208 throw new UnsafeTemplateException('Dynamic <xsl:element/> names are disallowed', $node); 6209 } 6210 } 6211 6212 /* 6213 * @package s9e\TextFormatter 6214 * @copyright Copyright (c) 2010-2019 The s9e Authors 6215 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6216 */ 6217 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6218 use DOMElement; 6219 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6220 use s9e\TextFormatter\Configurator\Items\Tag; 6221 use s9e\TextFormatter\Configurator\TemplateCheck; 6222 class DisallowElementNS extends TemplateCheck 6223 { 6224 public $elName; 6225 public $namespaceURI; 6226 public function __construct($namespaceURI, $elName) 6227 { 6228 $this->namespaceURI = $namespaceURI; 6229 $this->elName = $elName; 6230 } 6231 public function check(DOMElement $template, Tag $tag) 6232 { 6233 $node = $template->getElementsByTagNameNS($this->namespaceURI, $this->elName)->item(0); 6234 if ($node) 6235 throw new UnsafeTemplateException("Element '" . $node->nodeName . "' is disallowed", $node); 6236 } 6237 } 6238 6239 /* 6240 * @package s9e\TextFormatter 6241 * @copyright Copyright (c) 2010-2019 The s9e Authors 6242 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6243 */ 6244 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6245 use DOMElement; 6246 use DOMXPath; 6247 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6248 use s9e\TextFormatter\Configurator\Items\Tag; 6249 use s9e\TextFormatter\Configurator\TemplateCheck; 6250 class DisallowObjectParamsWithGeneratedName extends TemplateCheck 6251 { 6252 public function check(DOMElement $template, Tag $tag) 6253 { 6254 $xpath = new DOMXPath($template->ownerDocument); 6255 $query = '//object//param[contains(@name, "{") or .//xsl:attribute[translate(@name, "NAME", "name") = "name"]]'; 6256 $nodes = $xpath->query($query); 6257 foreach ($nodes as $node) 6258 throw new UnsafeTemplateException("A 'param' element with a suspect name has been found", $node); 6259 } 6260 } 6261 6262 /* 6263 * @package s9e\TextFormatter 6264 * @copyright Copyright (c) 2010-2019 The s9e Authors 6265 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6266 */ 6267 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6268 use DOMElement; 6269 use DOMXPath; 6270 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6271 use s9e\TextFormatter\Configurator\Items\Tag; 6272 use s9e\TextFormatter\Configurator\TemplateCheck; 6273 class DisallowPHPTags extends TemplateCheck 6274 { 6275 public function check(DOMElement $template, Tag $tag) 6276 { 6277 $queries = [ 6278 '//processing-instruction()["php" = translate(name(),"HP","hp")]' 6279 => 'PHP tags are not allowed in the template', 6280 '//script["php" = translate(@language,"HP","hp")]' 6281 => 'PHP tags are not allowed in the template', 6282 '//xsl:processing-instruction["php" = translate(@name,"HP","hp")]' 6283 => 'PHP tags are not allowed in the output', 6284 '//xsl:processing-instruction[contains(@name, "{")]' 6285 => 'Dynamic processing instructions are not allowed', 6286 ]; 6287 $xpath = new DOMXPath($template->ownerDocument); 6288 foreach ($queries as $query => $error) 6289 { 6290 $nodes = $xpath->query($query); 6291 if ($nodes->length) 6292 throw new UnsafeTemplateException($error, $nodes->item(0)); 6293 } 6294 } 6295 } 6296 6297 /* 6298 * @package s9e\TextFormatter 6299 * @copyright Copyright (c) 2010-2019 The s9e Authors 6300 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6301 */ 6302 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6303 use DOMElement; 6304 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6305 use s9e\TextFormatter\Configurator\Items\Tag; 6306 use s9e\TextFormatter\Configurator\TemplateCheck; 6307 class DisallowUnsafeCopyOf extends TemplateCheck 6308 { 6309 public function check(DOMElement $template, Tag $tag) 6310 { 6311 $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'copy-of'); 6312 foreach ($nodes as $node) 6313 { 6314 $expr = $node->getAttribute('select'); 6315 if (!\preg_match('#^@[-\\w]*(?:\\s*\\|\\s*@[-\\w]*)*$#D', $expr)) 6316 throw new UnsafeTemplateException("Cannot assess the safety of '" . $node->nodeName . "' select expression '" . $expr . "'", $node); 6317 } 6318 } 6319 } 6320 6321 /* 6322 * @package s9e\TextFormatter 6323 * @copyright Copyright (c) 2010-2019 The s9e Authors 6324 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6325 */ 6326 namespace s9e\TextFormatter\Configurator\TemplateChecks; 6327 use DOMElement; 6328 use DOMXPath; 6329 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException; 6330 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 6331 use s9e\TextFormatter\Configurator\Items\Tag; 6332 use s9e\TextFormatter\Configurator\TemplateCheck; 6333 class DisallowXPathFunction extends TemplateCheck 6334 { 6335 public $funcName; 6336 public function __construct($funcName) 6337 { 6338 $this->funcName = $funcName; 6339 } 6340 public function check(DOMElement $template, Tag $tag) 6341 { 6342 $regexp = '#(?!<\\pL)' . \preg_quote($this->funcName, '#') . '\\s*\\(#iu'; 6343 $regexp = \str_replace('\\:', '\\s*:\\s*', $regexp); 6344 foreach ($this->getExpressions($template) as $expr => $node) 6345 { 6346 $expr = \preg_replace('#([\'"]).*?\\1#s', '', $expr); 6347 if (\preg_match($regexp, $expr)) 6348 throw new UnsafeTemplateException('An XPath expression uses the ' . $this->funcName . '() function', $node); 6349 } 6350 } 6351 protected function getExpressions(DOMElement $template) 6352 { 6353 $xpath = new DOMXPath($template->ownerDocument); 6354 $exprs = []; 6355 foreach ($xpath->query('//@*') as $attribute) 6356 if ($attribute->parentNode->namespaceURI === self::XMLNS_XSL) 6357 { 6358 $expr = $attribute->value; 6359 $exprs[$expr] = $attribute; 6360 } 6361 else 6362 foreach (AVTHelper::parse($attribute->value) as $token) 6363 if ($token[0] === 'expression') 6364 $exprs[$token[1]] = $attribute; 6365 return $exprs; 6366 } 6367 } 6368 6369 /* 6370 * @package s9e\TextFormatter 6371 * @copyright Copyright (c) 2010-2019 The s9e Authors 6372 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6373 */ 6374 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6375 use DOMElement; 6376 use DOMNode; 6377 abstract class AbstractChooseOptimization extends AbstractNormalization 6378 { 6379 protected $choose; 6380 protected $queries = ['//xsl:choose']; 6381 protected function getAttributes(DOMElement $element) 6382 { 6383 $attributes = array(); 6384 foreach ($element->attributes as $attribute) 6385 { 6386 $key = $attribute->namespaceURI . '#' . $attribute->nodeName; 6387 $attributes[$key] = $attribute->nodeValue; 6388 } 6389 return $attributes; 6390 } 6391 protected function getBranches() 6392 { 6393 $query = 'xsl:when|xsl:otherwise'; 6394 return $this->xpath($query, $this->choose); 6395 } 6396 protected function hasOtherwise() 6397 { 6398 return (bool) $this->xpath->evaluate('count(xsl:otherwise)', $this->choose); 6399 } 6400 protected function isEmpty() 6401 { 6402 $query = 'count(xsl:when/node() | xsl:otherwise/node())'; 6403 return !$this->xpath->evaluate($query, $this->choose); 6404 } 6405 protected function isEqualNode(DOMNode $node1, DOMNode $node2) 6406 { 6407 return ($node1->ownerDocument->saveXML($node1) === $node2->ownerDocument->saveXML($node2)); 6408 } 6409 protected function isEqualTag(DOMElement $el1, DOMElement $el2) 6410 { 6411 return ($el1->namespaceURI === $el2->namespaceURI && $el1->nodeName === $el2->nodeName && $this->getAttributes($el1) === $this->getAttributes($el2)); 6412 } 6413 protected function normalizeElement(DOMElement $element) 6414 { 6415 $this->choose = $element; 6416 $this->optimizeChoose(); 6417 } 6418 abstract protected function optimizeChoose(); 6419 protected function reset() 6420 { 6421 $this->choose = \null; 6422 parent::reset(); 6423 } 6424 } 6425 6426 /* 6427 * @package s9e\TextFormatter 6428 * @copyright Copyright (c) 2010-2019 The s9e Authors 6429 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6430 */ 6431 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6432 use DOMAttr; 6433 use DOMElement; 6434 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 6435 abstract class AbstractConstantFolding extends AbstractNormalization 6436 { 6437 protected $queries = [ 6438 '//*[namespace-uri() != $XSL]/@*[contains(.,"{")]', 6439 '//xsl:value-of' 6440 ]; 6441 abstract protected function getOptimizationPasses(); 6442 protected function evaluateExpression($expr) 6443 { 6444 $original = $expr; 6445 foreach ($this->getOptimizationPasses() as $regexp => $methodName) 6446 { 6447 $regexp = \str_replace(' ', '\\s*', $regexp); 6448 $expr = \preg_replace_callback($regexp, [$this, $methodName], $expr); 6449 } 6450 return ($expr === $original) ? $expr : $this->evaluateExpression(\trim($expr)); 6451 } 6452 protected function normalizeAttribute(DOMAttr $attribute) 6453 { 6454 AVTHelper::replace( 6455 $attribute, 6456 function ($token) 6457 { 6458 if ($token[0] === 'expression') 6459 $token[1] = $this->evaluateExpression($token[1]); 6460 return $token; 6461 } 6462 ); 6463 } 6464 protected function normalizeElement(DOMElement $valueOf) 6465 { 6466 $valueOf->setAttribute('select', $this->evaluateExpression($valueOf->getAttribute('select'))); 6467 } 6468 } 6469 6470 /* 6471 * @package s9e\TextFormatter 6472 * @copyright Copyright (c) 2010-2019 The s9e Authors 6473 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6474 */ 6475 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6476 use DOMElement; 6477 use s9e\TextFormatter\Configurator\Helpers\ElementInspector; 6478 class EnforceHTMLOmittedEndTags extends AbstractNormalization 6479 { 6480 protected $queries = ['//*[namespace-uri() = ""]/*[namespace-uri() = ""]']; 6481 protected function normalizeElement(DOMElement $element) 6482 { 6483 $parentNode = $element->parentNode; 6484 if (ElementInspector::isVoid($parentNode) || ElementInspector::closesParent($element, $parentNode)) 6485 $this->reparentElement($element); 6486 } 6487 protected function reparentElement(DOMElement $element) 6488 { 6489 $parentNode = $element->parentNode; 6490 do 6491 { 6492 $lastChild = $parentNode->lastChild; 6493 $parentNode->parentNode->insertBefore($lastChild, $parentNode->nextSibling); 6494 } 6495 while (!$lastChild->isSameNode($element)); 6496 } 6497 } 6498 6499 /* 6500 * @package s9e\TextFormatter 6501 * @copyright Copyright (c) 2010-2019 The s9e Authors 6502 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6503 */ 6504 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6505 use DOMAttr; 6506 class FixUnescapedCurlyBracesInHtmlAttributes extends AbstractNormalization 6507 { 6508 protected $queries = ['//*[namespace-uri() != $XSL]/@*[contains(., "{")]']; 6509 protected function normalizeAttribute(DOMAttr $attribute) 6510 { 6511 $match = [ 6512 '(\\b(?:do|else|(?:if|while)\\s*\\(.*?\\))\\s*\\{(?![{@]))', 6513 '(\\bfunction\\s*\\w*\\s*\\([^\\)]*\\)\\s*\\{(?!\\{))', 6514 '((?<!\\{)(?:\\{\\{)*\\{(?!\\{)[^}]*+$)', 6515 '((?<!\\{)\\{\\s*(?:"[^"]*"|\'[^\']*\'|[a-z]\\w*(?:\\s|:\\s|:(?:["\']|\\w+\\s*,))))i' 6516 ]; 6517 $replace = [ 6518 '$0{', 6519 '$0{', 6520 '{$0', 6521 '{$0' 6522 ]; 6523 $attrValue = \preg_replace($match, $replace, $attribute->value); 6524 $attribute->value = \htmlspecialchars($attrValue, \ENT_NOQUOTES, 'UTF-8'); 6525 } 6526 } 6527 6528 /* 6529 * @package s9e\TextFormatter 6530 * @copyright Copyright (c) 2010-2019 The s9e Authors 6531 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6532 */ 6533 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6534 use DOMElement; 6535 use DOMText; 6536 class InlineAttributes extends AbstractNormalization 6537 { 6538 protected $queries = ['//*[namespace-uri() != $XSL]/xsl:attribute']; 6539 protected function normalizeElement(DOMElement $element) 6540 { 6541 $value = ''; 6542 foreach ($element->childNodes as $node) 6543 if ($node instanceof DOMText || $this->isXsl($node, 'text')) 6544 $value .= \preg_replace('([{}])', '$0$0', $node->textContent); 6545 elseif ($this->isXsl($node, 'value-of')) 6546 $value .= '{' . $node->getAttribute('select') . '}'; 6547 else 6548 return; 6549 $element->parentNode->setAttribute($element->getAttribute('name'), $value); 6550 $element->parentNode->removeChild($element); 6551 } 6552 } 6553 6554 /* 6555 * @package s9e\TextFormatter 6556 * @copyright Copyright (c) 2010-2019 The s9e Authors 6557 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6558 */ 6559 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6560 use DOMNode; 6561 class InlineCDATA extends AbstractNormalization 6562 { 6563 protected $queries = ['//text()']; 6564 protected function normalizeNode(DOMNode $node) 6565 { 6566 if ($node->nodeType === \XML_CDATA_SECTION_NODE) 6567 $node->parentNode->replaceChild($this->createText($node->textContent), $node); 6568 } 6569 } 6570 6571 /* 6572 * @package s9e\TextFormatter 6573 * @copyright Copyright (c) 2010-2019 The s9e Authors 6574 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6575 */ 6576 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6577 use DOMElement; 6578 use DOMException; 6579 class InlineElements extends AbstractNormalization 6580 { 6581 protected $queries = ['//xsl:element']; 6582 protected function normalizeElement(DOMElement $element) 6583 { 6584 $elName = $element->getAttribute('name'); 6585 $dom = $this->ownerDocument; 6586 try 6587 { 6588 $newElement = ($element->hasAttribute('namespace')) 6589 ? $dom->createElementNS($element->getAttribute('namespace'), $elName) 6590 : $dom->createElement($elName); 6591 } 6592 catch (DOMException $e) 6593 { 6594 return; 6595 } 6596 $element->parentNode->replaceChild($newElement, $element); 6597 while ($element->firstChild) 6598 $newElement->appendChild($element->removeChild($element->firstChild)); 6599 } 6600 } 6601 6602 /* 6603 * @package s9e\TextFormatter 6604 * @copyright Copyright (c) 2010-2019 The s9e Authors 6605 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6606 */ 6607 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6608 use DOMAttr; 6609 use DOMElement; 6610 use DOMNode; 6611 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 6612 use s9e\TextFormatter\Configurator\Helpers\XPathHelper; 6613 class InlineInferredValues extends AbstractNormalization 6614 { 6615 protected $queries = ['//xsl:if', '//xsl:when']; 6616 protected function normalizeElement(DOMElement $element) 6617 { 6618 $map = XPathHelper::parseEqualityExpr($element->getAttribute('test')); 6619 if ($map === \false || \count($map) !== 1 || \count($map[\key($map)]) !== 1) 6620 return; 6621 $expr = \key($map); 6622 $value = \end($map[$expr]); 6623 $this->inlineInferredValue($element, $expr, $value); 6624 } 6625 protected function inlineInferredValue(DOMNode $node, $expr, $value) 6626 { 6627 $query = './/xsl:value-of[@select="' . $expr . '"]'; 6628 foreach ($this->xpath($query, $node) as $valueOf) 6629 $this->replaceValueOf($valueOf, $value); 6630 $query = './/*[namespace-uri() != $XSL]/@*[contains(., "{' . $expr . '}")]'; 6631 foreach ($this->xpath($query, $node) as $attribute) 6632 $this->replaceAttribute($attribute, $expr, $value); 6633 } 6634 protected function replaceAttribute(DOMAttr $attribute, $expr, $value) 6635 { 6636 AVTHelper::replace( 6637 $attribute, 6638 function ($token) use ($expr, $value) 6639 { 6640 if ($token[0] === 'expression' && $token[1] === $expr) 6641 $token = ['literal', $value]; 6642 return $token; 6643 } 6644 ); 6645 } 6646 protected function replaceValueOf(DOMElement $valueOf, $value) 6647 { 6648 $valueOf->parentNode->replaceChild($this->createText($value), $valueOf); 6649 } 6650 } 6651 6652 /* 6653 * @package s9e\TextFormatter 6654 * @copyright Copyright (c) 2010-2019 The s9e Authors 6655 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6656 */ 6657 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6658 use DOMElement; 6659 class InlineTextElements extends AbstractNormalization 6660 { 6661 protected $queries = ['//xsl:text[not(@disable-output-escaping="yes")]']; 6662 protected function isFollowedByText(DOMElement $element) 6663 { 6664 return ($element->nextSibling && $element->nextSibling->nodeType === \XML_TEXT_NODE); 6665 } 6666 protected function isPrecededByText(DOMElement $element) 6667 { 6668 return ($element->previousSibling && $element->previousSibling->nodeType === \XML_TEXT_NODE); 6669 } 6670 protected function normalizeElement(DOMElement $element) 6671 { 6672 if (\trim($element->textContent) === '') 6673 if (!$this->isFollowedByText($element) && !$this->isPrecededByText($element)) 6674 return; 6675 $element->parentNode->replaceChild($this->createTextNode($element->textContent), $element); 6676 } 6677 } 6678 6679 /* 6680 * @package s9e\TextFormatter 6681 * @copyright Copyright (c) 2010-2019 The s9e Authors 6682 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6683 */ 6684 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6685 use DOMAttr; 6686 use DOMElement; 6687 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 6688 class InlineXPathLiterals extends AbstractNormalization 6689 { 6690 protected $queries = [ 6691 '//xsl:value-of', 6692 '//*[namespace-uri() != $XSL]/@*[contains(., "{")]' 6693 ]; 6694 protected function getTextContent($expr) 6695 { 6696 $expr = \trim($expr); 6697 if (\preg_match('(^(?:\'[^\']*\'|"[^"]*")$)', $expr)) 6698 return \substr($expr, 1, -1); 6699 if (\preg_match('(^0*([0-9]+(?:\\.[0-9]+)?)$)', $expr, $m)) 6700 return $m[1]; 6701 return \false; 6702 } 6703 protected function normalizeAttribute(DOMAttr $attribute) 6704 { 6705 AVTHelper::replace( 6706 $attribute, 6707 function ($token) 6708 { 6709 if ($token[0] === 'expression') 6710 { 6711 $textContent = $this->getTextContent($token[1]); 6712 if ($textContent !== \false) 6713 $token = ['literal', $textContent]; 6714 } 6715 return $token; 6716 } 6717 ); 6718 } 6719 protected function normalizeElement(DOMElement $element) 6720 { 6721 $textContent = $this->getTextContent($element->getAttribute('select')); 6722 if ($textContent !== \false) 6723 $element->parentNode->replaceChild($this->createText($textContent), $element); 6724 } 6725 } 6726 6727 /* 6728 * @package s9e\TextFormatter 6729 * @copyright Copyright (c) 2010-2019 The s9e Authors 6730 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6731 */ 6732 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6733 use DOMAttr; 6734 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 6735 class MinifyInlineCSS extends AbstractNormalization 6736 { 6737 protected $queries = ['//*[namespace-uri() != $XSL]/@style']; 6738 protected function normalizeAttribute(DOMAttr $attribute) 6739 { 6740 $css = $attribute->nodeValue; 6741 if (!\preg_match('(\\{(?!@\\w+\\}))', $css)) 6742 $attribute->nodeValue = $this->minify($css); 6743 } 6744 protected function minify($css) 6745 { 6746 $css = \trim($css, " \n\t;"); 6747 $css = \preg_replace('(\\s*([,:;])\\s*)', '$1', $css); 6748 $css = \preg_replace_callback( 6749 '((?<=[\\s:])#[0-9a-f]{3,6})i', 6750 function ($m) 6751 { 6752 return \strtolower($m[0]); 6753 }, 6754 $css 6755 ); 6756 $css = \preg_replace('((?<=[\\s:])#([0-9a-f])\\1([0-9a-f])\\2([0-9a-f])\\3)', '#$1$2$3', $css); 6757 $css = \preg_replace('((?<=[\\s:])#f00\\b)', 'red', $css); 6758 $css = \preg_replace('((?<=[\\s:])0px\\b)', '0', $css); 6759 return $css; 6760 } 6761 } 6762 6763 /* 6764 * @package s9e\TextFormatter 6765 * @copyright Copyright (c) 2010-2019 The s9e Authors 6766 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6767 */ 6768 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6769 use DOMAttr; 6770 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 6771 use s9e\TextFormatter\Configurator\Helpers\XPathHelper; 6772 class MinifyXPathExpressions extends AbstractNormalization 6773 { 6774 protected $queries = ['//@*[contains(., " ")]']; 6775 protected function normalizeAttribute(DOMAttr $attribute) 6776 { 6777 $element = $attribute->parentNode; 6778 if (!$this->isXsl($element)) 6779 $this->replaceAVT($attribute); 6780 elseif (\in_array($attribute->nodeName, ['match', 'select', 'test'], \true)) 6781 { 6782 $expr = XPathHelper::minify($attribute->nodeValue); 6783 $element->setAttribute($attribute->nodeName, $expr); 6784 } 6785 } 6786 protected function replaceAVT(DOMAttr $attribute) 6787 { 6788 AVTHelper::replace( 6789 $attribute, 6790 function ($token) 6791 { 6792 if ($token[0] === 'expression') 6793 $token[1] = XPathHelper::minify($token[1]); 6794 return $token; 6795 } 6796 ); 6797 } 6798 } 6799 6800 /* 6801 * @package s9e\TextFormatter 6802 * @copyright Copyright (c) 2010-2019 The s9e Authors 6803 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6804 */ 6805 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6806 use DOMAttr; 6807 use DOMElement; 6808 class NormalizeAttributeNames extends AbstractNormalization 6809 { 6810 protected $queries = [ 6811 '//@*[name() != translate(name(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")]', 6812 '//xsl:attribute[not(contains(@name, "{"))][@name != translate(@name, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")]' 6813 ]; 6814 protected function normalizeAttribute(DOMAttr $attribute) 6815 { 6816 $attribute->parentNode->setAttribute($this->lowercase($attribute->localName), $attribute->value); 6817 $attribute->parentNode->removeAttributeNode($attribute); 6818 } 6819 protected function normalizeElement(DOMElement $element) 6820 { 6821 $element->setAttribute('name', $this->lowercase($element->getAttribute('name'))); 6822 } 6823 } 6824 6825 /* 6826 * @package s9e\TextFormatter 6827 * @copyright Copyright (c) 2010-2019 The s9e Authors 6828 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6829 */ 6830 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6831 use DOMElement; 6832 class NormalizeElementNames extends AbstractNormalization 6833 { 6834 protected $queries = [ 6835 '//*[namespace-uri() != $XSL]', 6836 '//xsl:element[not(contains(@name, "{"))]' 6837 ]; 6838 protected function normalizeElement(DOMElement $element) 6839 { 6840 if ($this->isXsl($element, 'element')) 6841 $this->replaceXslElement($element); 6842 else 6843 $this->replaceElement($element); 6844 } 6845 protected function replaceElement(DOMElement $element) 6846 { 6847 $elName = $this->lowercase($element->localName); 6848 if ($elName === $element->localName) 6849 return; 6850 $newElement = (\is_null($element->namespaceURI)) 6851 ? $this->ownerDocument->createElement($elName) 6852 : $this->ownerDocument->createElementNS($element->namespaceURI, $elName); 6853 while ($element->firstChild) 6854 $newElement->appendChild($element->removeChild($element->firstChild)); 6855 foreach ($element->attributes as $attribute) 6856 $newElement->setAttributeNS( 6857 $attribute->namespaceURI, 6858 $attribute->nodeName, 6859 $attribute->value 6860 ); 6861 $element->parentNode->replaceChild($newElement, $element); 6862 } 6863 protected function replaceXslElement(DOMElement $element) 6864 { 6865 $elName = $this->lowercase($element->getAttribute('name')); 6866 $element->setAttribute('name', $elName); 6867 } 6868 } 6869 6870 /* 6871 * @package s9e\TextFormatter 6872 * @copyright Copyright (c) 2010-2019 The s9e Authors 6873 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6874 */ 6875 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6876 use DOMAttr; 6877 use DOMElement; 6878 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 6879 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 6880 use s9e\TextFormatter\Parser\AttributeFilters\UrlFilter; 6881 class NormalizeUrls extends AbstractNormalization 6882 { 6883 protected function getNodes() 6884 { 6885 return TemplateHelper::getURLNodes($this->ownerDocument); 6886 } 6887 protected function normalizeAttribute(DOMAttr $attribute) 6888 { 6889 $tokens = AVTHelper::parse(\trim($attribute->value)); 6890 $attrValue = ''; 6891 foreach ($tokens as $_f6b3b659) 6892 { 6893 list($type, $content) = $_f6b3b659; 6894 if ($type === 'literal') 6895 $attrValue .= UrlFilter::sanitizeUrl($content); 6896 else 6897 $attrValue .= '{' . $content . '}'; 6898 } 6899 $attrValue = $this->unescapeBrackets($attrValue); 6900 $attribute->value = \htmlspecialchars($attrValue); 6901 } 6902 protected function normalizeElement(DOMElement $element) 6903 { 6904 $query = './/text()[normalize-space() != ""]'; 6905 foreach ($this->xpath($query, $element) as $i => $node) 6906 { 6907 $value = UrlFilter::sanitizeUrl($node->nodeValue); 6908 if (!$i) 6909 $value = $this->unescapeBrackets(\ltrim($value)); 6910 $node->nodeValue = $value; 6911 } 6912 if (isset($node)) 6913 $node->nodeValue = \rtrim($node->nodeValue); 6914 } 6915 protected function unescapeBrackets($url) 6916 { 6917 return \preg_replace('#^(\\w+://)%5B([-\\w:._%]+)%5D#i', '$1[$2]', $url); 6918 } 6919 } 6920 6921 /* 6922 * @package s9e\TextFormatter 6923 * @copyright Copyright (c) 2010-2019 The s9e Authors 6924 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6925 */ 6926 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6927 use DOMElement; 6928 class OptimizeConditionalAttributes extends AbstractNormalization 6929 { 6930 protected $queries = ['//xsl:if[starts-with(@test, "@")][count(descendant::node()) = 2][xsl:attribute[@name = substring(../@test, 2)][xsl:value-of[@select = ../../@test]]]']; 6931 protected function normalizeElement(DOMElement $element) 6932 { 6933 $copyOf = $this->createElement('xsl:copy-of'); 6934 $copyOf->setAttribute('select', $element->getAttribute('test')); 6935 $element->parentNode->replaceChild($copyOf, $element); 6936 } 6937 } 6938 6939 /* 6940 * @package s9e\TextFormatter 6941 * @copyright Copyright (c) 2010-2019 The s9e Authors 6942 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6943 */ 6944 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6945 use DOMElement; 6946 class OptimizeConditionalValueOf extends AbstractNormalization 6947 { 6948 protected $queries = ['//xsl:if[count(descendant::node()) = 1]/xsl:value-of']; 6949 protected function normalizeElement(DOMElement $element) 6950 { 6951 $if = $element->parentNode; 6952 $test = $if->getAttribute('test'); 6953 $select = $element->getAttribute('select'); 6954 if ($select !== $test || !\preg_match('#^@[-\\w]+$#D', $select)) 6955 return; 6956 $if->parentNode->replaceChild($if->removeChild($element), $if); 6957 } 6958 } 6959 6960 /* 6961 * @package s9e\TextFormatter 6962 * @copyright Copyright (c) 2010-2019 The s9e Authors 6963 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6964 */ 6965 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6966 use DOMNode; 6967 class PreserveSingleSpaces extends AbstractNormalization 6968 { 6969 protected $queries = ['//text()[. = " "][not(parent::xsl:text)]']; 6970 protected function normalizeNode(DOMNode $node) 6971 { 6972 $node->parentNode->replaceChild($this->createElement('xsl:text', ' '), $node); 6973 } 6974 } 6975 6976 /* 6977 * @package s9e\TextFormatter 6978 * @copyright Copyright (c) 2010-2019 The s9e Authors 6979 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6980 */ 6981 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6982 use DOMNode; 6983 class RemoveComments extends AbstractNormalization 6984 { 6985 protected $queries = ['//comment()']; 6986 protected function normalizeNode(DOMNode $node) 6987 { 6988 $node->parentNode->removeChild($node); 6989 } 6990 } 6991 6992 /* 6993 * @package s9e\TextFormatter 6994 * @copyright Copyright (c) 2010-2019 The s9e Authors 6995 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 6996 */ 6997 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 6998 use DOMNode; 6999 class RemoveInterElementWhitespace extends AbstractNormalization 7000 { 7001 protected $queries = ['//text()[normalize-space() = ""][. != " "][not(parent::xsl:text)]']; 7002 protected function normalizeNode(DOMNode $node) 7003 { 7004 $node->parentNode->removeChild($node); 7005 } 7006 } 7007 7008 /* 7009 * @package s9e\TextFormatter 7010 * @copyright Copyright (c) 2010-2019 The s9e Authors 7011 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7012 */ 7013 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 7014 use DOMAttr; 7015 use DOMElement; 7016 class RemoveLivePreviewAttributes extends AbstractNormalization 7017 { 7018 protected $queries = [ 7019 '//@* [starts-with(name(), "data-s9e-livepreview-")]', 7020 '//xsl:attribute[starts-with(@name, "data-s9e-livepreview-")]' 7021 ]; 7022 protected function normalizeAttribute(DOMAttr $attribute) 7023 { 7024 $attribute->parentNode->removeAttributeNode($attribute); 7025 } 7026 protected function normalizeElement(DOMElement $element) 7027 { 7028 $element->parentNode->removeChild($element); 7029 } 7030 } 7031 7032 /* 7033 * @package s9e\TextFormatter 7034 * @copyright Copyright (c) 2010-2019 The s9e Authors 7035 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7036 */ 7037 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 7038 use DOMElement; 7039 class SetRelNoreferrerOnTargetedLinks extends AbstractNormalization 7040 { 7041 protected $queries = ['//a', '//area']; 7042 protected function addRelAttribute(DOMElement $element) 7043 { 7044 $rel = $element->getAttribute('rel'); 7045 if (\preg_match('(\\S$)', $rel)) 7046 $rel .= ' '; 7047 $rel .= 'noreferrer'; 7048 $element->setAttribute('rel', $rel); 7049 } 7050 protected function linkTargetCanAccessOpener(DOMElement $element) 7051 { 7052 if (!$element->hasAttribute('target')) 7053 return \false; 7054 if (\preg_match('(\\bno(?:open|referr)er\\b)', $element->getAttribute('rel'))) 7055 return \false; 7056 return \true; 7057 } 7058 protected function normalizeElement(DOMElement $element) 7059 { 7060 if ($this->linkTargetCanAccessOpener($element)) 7061 $this->addRelAttribute($element); 7062 } 7063 } 7064 7065 /* 7066 * @package s9e\TextFormatter 7067 * @copyright Copyright (c) 2010-2019 The s9e Authors 7068 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7069 */ 7070 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 7071 use DOMAttr; 7072 use DOMElement; 7073 use s9e\TextFormatter\Configurator\Helpers\AVTHelper; 7074 class UninlineAttributes extends AbstractNormalization 7075 { 7076 protected $queries = ['//*[namespace-uri() != $XSL][@*]']; 7077 protected function normalizeElement(DOMElement $element) 7078 { 7079 $fragment = $element->ownerDocument->createDocumentFragment(); 7080 while ($element->attributes->length > 0) 7081 { 7082 $attribute = $element->attributes->item(0); 7083 $fragment->appendChild($this->uninlineAttribute($attribute)); 7084 } 7085 $element->insertBefore($fragment, $element->firstChild); 7086 } 7087 protected function uninlineAttribute(DOMAttr $attribute) 7088 { 7089 $xslAttribute = (\strpos($attribute->value, '{') === \false) 7090 ? $this->uninlineStaticAttribute($attribute) 7091 : $this->uninlineDynamicAttribute($attribute); 7092 $xslAttribute->setAttribute('name', $attribute->nodeName); 7093 $attribute->parentNode->removeAttributeNode($attribute); 7094 return $xslAttribute; 7095 } 7096 protected function uninlineDynamicAttribute(DOMAttr $attribute) 7097 { 7098 $xslAttribute = $this->createElement('xsl:attribute'); 7099 foreach (AVTHelper::parse($attribute->value) as $_f6b3b659) 7100 { 7101 list($type, $content) = $_f6b3b659; 7102 if ($type === 'expression') 7103 { 7104 $childNode = $this->createElement('xsl:value-of'); 7105 $childNode->setAttribute('select', $content); 7106 } 7107 else 7108 $childNode = $this->createText($content); 7109 $xslAttribute->appendChild($childNode); 7110 } 7111 return $xslAttribute; 7112 } 7113 protected function uninlineStaticAttribute(DOMAttr $attribute) 7114 { 7115 return $this->createElement('xsl:attribute', \str_replace('}}', '}', $attribute->value)); 7116 } 7117 } 7118 7119 /* 7120 * @package s9e\TextFormatter 7121 * @copyright Copyright (c) 2010-2019 The s9e Authors 7122 * @license http://www.opensource.org/licenses/mit-license'); The MIT License 7123 */ 7124 namespace s9e\TextFormatter\Configurator; 7125 use ArrayAccess; 7126 use Iterator; 7127 use s9e\TextFormatter\Configurator\Collections\TemplateNormalizationList; 7128 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 7129 use s9e\TextFormatter\Configurator\Items\Tag; 7130 use s9e\TextFormatter\Configurator\Traits\CollectionProxy; 7131 class TemplateNormalizer implements ArrayAccess, Iterator 7132 { 7133 use CollectionProxy; 7134 protected $collection; 7135 protected $maxIterations = 100; 7136 public function __construct() 7137 { 7138 $this->collection = new TemplateNormalizationList; 7139 $this->collection->append('PreserveSingleSpaces'); 7140 $this->collection->append('RemoveComments'); 7141 $this->collection->append('RemoveInterElementWhitespace'); 7142 $this->collection->append('NormalizeElementNames'); 7143 $this->collection->append('FixUnescapedCurlyBracesInHtmlAttributes'); 7144 $this->collection->append('EnforceHTMLOmittedEndTags'); 7145 $this->collection->append('InlineCDATA'); 7146 $this->collection->append('InlineElements'); 7147 $this->collection->append('InlineTextElements'); 7148 $this->collection->append('UninlineAttributes'); 7149 $this->collection->append('MinifyXPathExpressions'); 7150 $this->collection->append('NormalizeAttributeNames'); 7151 $this->collection->append('OptimizeConditionalAttributes'); 7152 $this->collection->append('FoldArithmeticConstants'); 7153 $this->collection->append('FoldConstantXPathExpressions'); 7154 $this->collection->append('InlineXPathLiterals'); 7155 $this->collection->append('OptimizeChooseText'); 7156 $this->collection->append('OptimizeConditionalValueOf'); 7157 $this->collection->append('OptimizeChoose'); 7158 $this->collection->append('InlineAttributes'); 7159 $this->collection->append('NormalizeUrls'); 7160 $this->collection->append('InlineInferredValues'); 7161 $this->collection->append('SetRelNoreferrerOnTargetedLinks'); 7162 $this->collection->append('MinifyInlineCSS'); 7163 } 7164 public function normalizeTag(Tag $tag) 7165 { 7166 if (isset($tag->template) && !$tag->template->isNormalized()) 7167 $tag->template->normalize($this); 7168 } 7169 public function normalizeTemplate($template) 7170 { 7171 $dom = TemplateHelper::loadTemplate($template); 7172 $i = 0; 7173 do 7174 { 7175 $old = $template; 7176 foreach ($this->collection as $k => $normalization) 7177 { 7178 if ($i > 0 && !empty($normalization->onlyOnce)) 7179 continue; 7180 $normalization->normalize($dom->documentElement); 7181 } 7182 $template = TemplateHelper::saveTemplate($dom); 7183 } 7184 while (++$i < $this->maxIterations && $template !== $old); 7185 return $template; 7186 } 7187 } 7188 7189 /* 7190 * @package s9e\TextFormatter 7191 * @copyright Copyright (c) 2010-2019 The s9e Authors 7192 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7193 */ 7194 namespace s9e\TextFormatter\Configurator; 7195 use RuntimeException; 7196 use s9e\TextFormatter\Configurator\Collections\HostnameList; 7197 use s9e\TextFormatter\Configurator\Collections\SchemeList; 7198 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper; 7199 class UrlConfig implements ConfigProvider 7200 { 7201 protected $allowedSchemes; 7202 protected $disallowedHosts; 7203 protected $restrictedHosts; 7204 public function __construct() 7205 { 7206 $this->disallowedHosts = new HostnameList; 7207 $this->restrictedHosts = new HostnameList; 7208 $this->allowedSchemes = new SchemeList; 7209 $this->allowedSchemes[] = 'http'; 7210 $this->allowedSchemes[] = 'https'; 7211 } 7212 public function asConfig() 7213 { 7214 return ConfigHelper::toArray(\get_object_vars($this)); 7215 } 7216 public function allowScheme($scheme) 7217 { 7218 if (\strtolower($scheme) === 'javascript') 7219 throw new RuntimeException('The JavaScript URL scheme cannot be allowed'); 7220 $this->allowedSchemes[] = $scheme; 7221 } 7222 public function disallowHost($host, $matchSubdomains = \true) 7223 { 7224 $this->disallowedHosts[] = $host; 7225 if ($matchSubdomains && \substr($host, 0, 1) !== '*') 7226 $this->disallowedHosts[] = '*.' . $host; 7227 } 7228 public function disallowScheme($scheme) 7229 { 7230 $this->allowedSchemes->remove($scheme); 7231 } 7232 public function getAllowedSchemes() 7233 { 7234 return \iterator_to_array($this->allowedSchemes); 7235 } 7236 public function restrictHost($host, $matchSubdomains = \true) 7237 { 7238 $this->restrictedHosts[] = $host; 7239 if ($matchSubdomains && \substr($host, 0, 1) !== '*') 7240 $this->restrictedHosts[] = '*.' . $host; 7241 } 7242 } 7243 7244 /* 7245 * @package s9e\TextFormatter 7246 * @copyright Copyright (c) 2010-2019 The s9e Authors 7247 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7248 */ 7249 namespace s9e\TextFormatter\Configurator\Collections; 7250 use InvalidArgumentException; 7251 use s9e\TextFormatter\Configurator\Helpers\RegexpParser; 7252 use s9e\TextFormatter\Configurator\Items\AttributePreprocessor; 7253 use s9e\TextFormatter\Configurator\Items\Regexp; 7254 use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor; 7255 use s9e\TextFormatter\Configurator\Validators\AttributeName; 7256 class AttributePreprocessorCollection extends Collection 7257 { 7258 public function add($attrName, $regexp) 7259 { 7260 $attrName = AttributeName::normalize($attrName); 7261 $k = \serialize([$attrName, $regexp]); 7262 $this->items[$k] = new AttributePreprocessor($regexp); 7263 return $this->items[$k]; 7264 } 7265 public function key() 7266 { 7267 list($attrName) = \unserialize(\key($this->items)); 7268 return $attrName; 7269 } 7270 public function merge($attributePreprocessors) 7271 { 7272 $error = \false; 7273 if ($attributePreprocessors instanceof AttributePreprocessorCollection) 7274 foreach ($attributePreprocessors as $attrName => $attributePreprocessor) 7275 $this->add($attrName, $attributePreprocessor->getRegexp()); 7276 elseif (\is_array($attributePreprocessors)) 7277 { 7278 foreach ($attributePreprocessors as $values) 7279 { 7280 if (!\is_array($values)) 7281 { 7282 $error = \true; 7283 break; 7284 } 7285 list($attrName, $value) = $values; 7286 if ($value instanceof AttributePreprocessor) 7287 $value = $value->getRegexp(); 7288 $this->add($attrName, $value); 7289 } 7290 } 7291 else 7292 $error = \true; 7293 if ($error) 7294 throw new InvalidArgumentException('merge() expects an instance of AttributePreprocessorCollection or a 2D array where each element is a [attribute name, regexp] pair'); 7295 } 7296 public function asConfig() 7297 { 7298 $config = []; 7299 foreach ($this->items as $k => $ap) 7300 { 7301 list($attrName) = \unserialize($k); 7302 $config[] = [$attrName, $ap, $ap->getCaptureNames()]; 7303 } 7304 return $config; 7305 } 7306 } 7307 7308 /* 7309 * @package s9e\TextFormatter 7310 * @copyright Copyright (c) 2010-2019 The s9e Authors 7311 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7312 */ 7313 namespace s9e\TextFormatter\Configurator\Collections; 7314 use ArrayAccess; 7315 use InvalidArgumentException; 7316 use RuntimeException; 7317 class NormalizedCollection extends Collection implements ArrayAccess 7318 { 7319 protected $onDuplicateAction = 'error'; 7320 public function asConfig() 7321 { 7322 $config = parent::asConfig(); 7323 \ksort($config); 7324 return $config; 7325 } 7326 public function onDuplicate($action = \null) 7327 { 7328 $old = $this->onDuplicateAction; 7329 if (\func_num_args() && $action !== 'error' && $action !== 'ignore' && $action !== 'replace') 7330 throw new InvalidArgumentException("Invalid onDuplicate action '" . $action . "'. Expected: 'error', 'ignore' or 'replace'"); 7331 $this->onDuplicateAction = $action; 7332 return $old; 7333 } 7334 protected function getAlreadyExistsException($key) 7335 { 7336 return new RuntimeException("Item '" . $key . "' already exists"); 7337 } 7338 protected function getNotExistException($key) 7339 { 7340 return new RuntimeException("Item '" . $key . "' does not exist"); 7341 } 7342 public function normalizeKey($key) 7343 { 7344 return $key; 7345 } 7346 public function normalizeValue($value) 7347 { 7348 return $value; 7349 } 7350 public function add($key, $value = \null) 7351 { 7352 if ($this->exists($key)) 7353 if ($this->onDuplicateAction === 'ignore') 7354 return $this->get($key); 7355 elseif ($this->onDuplicateAction === 'error') 7356 throw $this->getAlreadyExistsException($key); 7357 return $this->set($key, $value); 7358 } 7359 public function contains($value) 7360 { 7361 return \in_array($this->normalizeValue($value), $this->items); 7362 } 7363 public function delete($key) 7364 { 7365 $key = $this->normalizeKey($key); 7366 unset($this->items[$key]); 7367 } 7368 public function exists($key) 7369 { 7370 $key = $this->normalizeKey($key); 7371 return \array_key_exists($key, $this->items); 7372 } 7373 public function get($key) 7374 { 7375 if (!$this->exists($key)) 7376 throw $this->getNotExistException($key); 7377 $key = $this->normalizeKey($key); 7378 return $this->items[$key]; 7379 } 7380 public function indexOf($value) 7381 { 7382 return \array_search($this->normalizeValue($value), $this->items); 7383 } 7384 public function set($key, $value) 7385 { 7386 $key = $this->normalizeKey($key); 7387 $this->items[$key] = $this->normalizeValue($value); 7388 return $this->items[$key]; 7389 } 7390 public function offsetExists($offset) 7391 { 7392 return $this->exists($offset); 7393 } 7394 public function offsetGet($offset) 7395 { 7396 return $this->get($offset); 7397 } 7398 public function offsetSet($offset, $value) 7399 { 7400 $this->set($offset, $value); 7401 } 7402 public function offsetUnset($offset) 7403 { 7404 $this->delete($offset); 7405 } 7406 } 7407 7408 /* 7409 * @package s9e\TextFormatter 7410 * @copyright Copyright (c) 2010-2019 The s9e Authors 7411 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7412 */ 7413 namespace s9e\TextFormatter\Configurator\Collections; 7414 use ArrayAccess; 7415 use BadMethodCallException; 7416 use InvalidArgumentException; 7417 use RuntimeException; 7418 use s9e\TextFormatter\Configurator\ConfigProvider; 7419 use s9e\TextFormatter\Configurator\JavaScript\Dictionary; 7420 use s9e\TextFormatter\Configurator\Validators\TagName; 7421 use s9e\TextFormatter\Parser; 7422 class Ruleset extends Collection implements ArrayAccess, ConfigProvider 7423 { 7424 protected $rules = [ 7425 'allowChild' => 'addTargetedRule', 7426 'allowDescendant' => 'addTargetedRule', 7427 'autoClose' => 'addBooleanRule', 7428 'autoReopen' => 'addBooleanRule', 7429 'breakParagraph' => 'addBooleanRule', 7430 'closeAncestor' => 'addTargetedRule', 7431 'closeParent' => 'addTargetedRule', 7432 'createChild' => 'addTargetedRule', 7433 'createParagraphs' => 'addBooleanRule', 7434 'denyChild' => 'addTargetedRule', 7435 'denyDescendant' => 'addTargetedRule', 7436 'disableAutoLineBreaks' => 'addBooleanRule', 7437 'enableAutoLineBreaks' => 'addBooleanRule', 7438 'fosterParent' => 'addTargetedRule', 7439 'ignoreSurroundingWhitespace' => 'addBooleanRule', 7440 'ignoreTags' => 'addBooleanRule', 7441 'ignoreText' => 'addBooleanRule', 7442 'isTransparent' => 'addBooleanRule', 7443 'preventLineBreaks' => 'addBooleanRule', 7444 'requireParent' => 'addTargetedRule', 7445 'requireAncestor' => 'addTargetedRule', 7446 'suspendAutoLineBreaks' => 'addBooleanRule', 7447 'trimFirstLine' => 'addBooleanRule' 7448 ]; 7449 public function __construct() 7450 { 7451 $this->clear(); 7452 } 7453 public function __call($methodName, array $args) 7454 { 7455 if (!isset($this->rules[$methodName])) 7456 throw new BadMethodCallException("Undefined method '" . $methodName . "'"); 7457 \array_unshift($args, $methodName); 7458 \call_user_func_array([$this, $this->rules[$methodName]], $args); 7459 return $this; 7460 } 7461 public function offsetExists($k) 7462 { 7463 return isset($this->items[$k]); 7464 } 7465 public function offsetGet($k) 7466 { 7467 return $this->items[$k]; 7468 } 7469 public function offsetSet($k, $v) 7470 { 7471 throw new RuntimeException('Not supported'); 7472 } 7473 public function offsetUnset($k) 7474 { 7475 return $this->remove($k); 7476 } 7477 public function asConfig() 7478 { 7479 $config = $this->items; 7480 unset($config['allowChild']); 7481 unset($config['allowDescendant']); 7482 unset($config['denyChild']); 7483 unset($config['denyDescendant']); 7484 unset($config['requireParent']); 7485 $bitValues = [ 7486 'autoClose' => Parser::RULE_AUTO_CLOSE, 7487 'autoReopen' => Parser::RULE_AUTO_REOPEN, 7488 'breakParagraph' => Parser::RULE_BREAK_PARAGRAPH, 7489 'createParagraphs' => Parser::RULE_CREATE_PARAGRAPHS, 7490 'disableAutoLineBreaks' => Parser::RULE_DISABLE_AUTO_BR, 7491 'enableAutoLineBreaks' => Parser::RULE_ENABLE_AUTO_BR, 7492 'ignoreSurroundingWhitespace' => Parser::RULE_IGNORE_WHITESPACE, 7493 'ignoreTags' => Parser::RULE_IGNORE_TAGS, 7494 'ignoreText' => Parser::RULE_IGNORE_TEXT, 7495 'isTransparent' => Parser::RULE_IS_TRANSPARENT, 7496 'preventLineBreaks' => Parser::RULE_PREVENT_BR, 7497 'suspendAutoLineBreaks' => Parser::RULE_SUSPEND_AUTO_BR, 7498 'trimFirstLine' => Parser::RULE_TRIM_FIRST_LINE 7499 ]; 7500 $bitfield = 0; 7501 foreach ($bitValues as $ruleName => $bitValue) 7502 { 7503 if (!empty($config[$ruleName])) 7504 $bitfield |= $bitValue; 7505 unset($config[$ruleName]); 7506 } 7507 foreach (['closeAncestor', 'closeParent', 'fosterParent'] as $ruleName) 7508 if (isset($config[$ruleName])) 7509 { 7510 $targets = \array_fill_keys($config[$ruleName], 1); 7511 $config[$ruleName] = new Dictionary($targets); 7512 } 7513 $config['flags'] = $bitfield; 7514 return $config; 7515 } 7516 public function merge($rules, $overwrite = \true) 7517 { 7518 if (!\is_array($rules) 7519 && !($rules instanceof self)) 7520 throw new InvalidArgumentException('merge() expects an array or an instance of Ruleset'); 7521 foreach ($rules as $action => $value) 7522 if (\is_array($value)) 7523 foreach ($value as $tagName) 7524 $this->$action($tagName); 7525 elseif ($overwrite || !isset($this->items[$action])) 7526 $this->$action($value); 7527 } 7528 public function remove($type, $tagName = \null) 7529 { 7530 if (\preg_match('(^default(?:Child|Descendant)Rule)', $type)) 7531 throw new RuntimeException('Cannot remove ' . $type); 7532 if (isset($tagName)) 7533 { 7534 $tagName = TagName::normalize($tagName); 7535 if (isset($this->items[$type])) 7536 { 7537 $this->items[$type] = \array_diff( 7538 $this->items[$type], 7539 [$tagName] 7540 ); 7541 if (empty($this->items[$type])) 7542 unset($this->items[$type]); 7543 else 7544 $this->items[$type] = \array_values($this->items[$type]); 7545 } 7546 } 7547 else 7548 unset($this->items[$type]); 7549 } 7550 protected function addBooleanRule($ruleName, $bool = \true) 7551 { 7552 if (!\is_bool($bool)) 7553 throw new InvalidArgumentException($ruleName . '() expects a boolean'); 7554 $this->items[$ruleName] = $bool; 7555 } 7556 protected function addTargetedRule($ruleName, $tagName) 7557 { 7558 $this->items[$ruleName][] = TagName::normalize($tagName); 7559 } 7560 } 7561 7562 /* 7563 * @package s9e\TextFormatter 7564 * @copyright Copyright (c) 2010-2019 The s9e Authors 7565 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7566 */ 7567 namespace s9e\TextFormatter\Configurator\Items; 7568 abstract class Filter extends ProgrammableCallback 7569 { 7570 } 7571 7572 /* 7573 * @package s9e\TextFormatter 7574 * @copyright Copyright (c) 2010-2019 The s9e Authors 7575 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7576 */ 7577 namespace s9e\TextFormatter\Configurator\TemplateChecks; 7578 use DOMElement; 7579 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 7580 use s9e\TextFormatter\Configurator\Helpers\XPathHelper; 7581 use s9e\TextFormatter\Configurator\Items\Attribute; 7582 class DisallowUnsafeDynamicCSS extends AbstractDynamicContentCheck 7583 { 7584 protected function getNodes(DOMElement $template) 7585 { 7586 return TemplateHelper::getCSSNodes($template->ownerDocument); 7587 } 7588 protected function isExpressionSafe($expr) 7589 { 7590 return XPathHelper::isExpressionNumeric($expr); 7591 } 7592 protected function isSafe(Attribute $attribute) 7593 { 7594 return $attribute->isSafeInCSS(); 7595 } 7596 } 7597 7598 /* 7599 * @package s9e\TextFormatter 7600 * @copyright Copyright (c) 2010-2019 The s9e Authors 7601 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7602 */ 7603 namespace s9e\TextFormatter\Configurator\TemplateChecks; 7604 use DOMElement; 7605 use s9e\TextFormatter\Configurator\Helpers\XPathHelper; 7606 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 7607 use s9e\TextFormatter\Configurator\Items\Attribute; 7608 class DisallowUnsafeDynamicJS extends AbstractDynamicContentCheck 7609 { 7610 protected function getNodes(DOMElement $template) 7611 { 7612 return TemplateHelper::getJSNodes($template->ownerDocument); 7613 } 7614 protected function isExpressionSafe($expr) 7615 { 7616 return XPathHelper::isExpressionNumeric($expr); 7617 } 7618 protected function isSafe(Attribute $attribute) 7619 { 7620 return $attribute->isSafeInJS(); 7621 } 7622 } 7623 7624 /* 7625 * @package s9e\TextFormatter 7626 * @copyright Copyright (c) 2010-2019 The s9e Authors 7627 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7628 */ 7629 namespace s9e\TextFormatter\Configurator\TemplateChecks; 7630 use DOMAttr; 7631 use DOMElement; 7632 use DOMText; 7633 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; 7634 use s9e\TextFormatter\Configurator\Items\Attribute; 7635 use s9e\TextFormatter\Configurator\Items\Tag; 7636 class DisallowUnsafeDynamicURL extends AbstractDynamicContentCheck 7637 { 7638 protected $exceptionRegexp = '(^(?:(?!data|\\w*script)\\w+:|[^:]*/|#))i'; 7639 protected function getNodes(DOMElement $template) 7640 { 7641 return TemplateHelper::getURLNodes($template->ownerDocument); 7642 } 7643 protected function isSafe(Attribute $attribute) 7644 { 7645 return $attribute->isSafeAsURL(); 7646 } 7647 protected function checkAttributeNode(DOMAttr $attribute, Tag $tag) 7648 { 7649 if (\preg_match($this->exceptionRegexp, $attribute->value)) 7650 return; 7651 parent::checkAttributeNode($attribute, $tag); 7652 } 7653 protected function checkElementNode(DOMElement $element, Tag $tag) 7654 { 7655 if ($element->firstChild 7656 && $element->firstChild instanceof DOMText 7657 && \preg_match($this->exceptionRegexp, $element->firstChild->textContent)) 7658 return; 7659 parent::checkElementNode($element, $tag); 7660 } 7661 } 7662 7663 /* 7664 * @package s9e\TextFormatter 7665 * @copyright Copyright (c) 2010-2019 The s9e Authors 7666 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7667 */ 7668 namespace s9e\TextFormatter\Configurator\TemplateChecks; 7669 class RestrictFlashScriptAccess extends AbstractFlashRestriction 7670 { 7671 public $defaultSetting = 'sameDomain'; 7672 protected $settingName = 'allowScriptAccess'; 7673 protected $settings = [ 7674 'always' => 3, 7675 'samedomain' => 2, 7676 'never' => 1 7677 ]; 7678 } 7679 7680 /* 7681 * @package s9e\TextFormatter 7682 * @copyright Copyright (c) 2010-2019 The s9e Authors 7683 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7684 */ 7685 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 7686 use s9e\TextFormatter\Utils\XPath; 7687 class FoldArithmeticConstants extends AbstractConstantFolding 7688 { 7689 protected function getOptimizationPasses() 7690 { 7691 $n = '-?\\.[0-9]++|-?[0-9]++(?:\\.[0-9]++)?'; 7692 return [ 7693 '(^[0-9\\s]*[-+][-+0-9\\s]+$)' => 'foldOperation', 7694 '( \\+ 0(?! [^+\\)])|(?<![-\\w])0 \\+ )' => 'foldAdditiveIdentity', 7695 '(^((?>' . $n . ' [-+] )*)(' . $n . ') div (' . $n . '))' => 'foldDivision', 7696 '(^((?>' . $n . ' [-+] )*)(' . $n . ') \\* (' . $n . '))' => 'foldMultiplication', 7697 '(\\( (?:' . $n . ') (?>(?>[-+*]|div) (?:' . $n . ') )+\\))' => 'foldSubExpression', 7698 '((?<=[-+*\\(]|\\bdiv|^) \\( ([@$][-\\w]+|' . $n . ') \\) (?=[-+*\\)]|div|$))' => 'removeParentheses' 7699 ]; 7700 } 7701 protected function evaluateExpression($expr) 7702 { 7703 $expr = \preg_replace_callback( 7704 '(([\'"])(.*?)\\1)s', 7705 function ($m) 7706 { 7707 return $m[1] . \bin2hex($m[2]) . $m[1]; 7708 }, 7709 $expr 7710 ); 7711 $expr = parent::evaluateExpression($expr); 7712 $expr = \preg_replace_callback( 7713 '(([\'"])(.*?)\\1)s', 7714 function ($m) 7715 { 7716 return $m[1] . \hex2bin($m[2]) . $m[1]; 7717 }, 7718 $expr 7719 ); 7720 return $expr; 7721 } 7722 protected function foldAdditiveIdentity(array $m) 7723 { 7724 return ''; 7725 } 7726 protected function foldDivision(array $m) 7727 { 7728 return $m[1] . XPath::export($m[2] / $m[3]); 7729 } 7730 protected function foldMultiplication(array $m) 7731 { 7732 return $m[1] . XPath::export($m[2] * $m[3]); 7733 } 7734 protected function foldOperation(array $m) 7735 { 7736 return XPath::export($this->xpath->evaluate($m[0])); 7737 } 7738 protected function foldSubExpression(array $m) 7739 { 7740 return '(' . $this->evaluateExpression(\trim(\substr($m[0], 1, -1))) . ')'; 7741 } 7742 protected function removeParentheses(array $m) 7743 { 7744 return ' ' . $m[1] . ' '; 7745 } 7746 } 7747 7748 /* 7749 * @package s9e\TextFormatter 7750 * @copyright Copyright (c) 2010-2019 The s9e Authors 7751 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7752 */ 7753 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 7754 use s9e\TextFormatter\Utils\XPath; 7755 class FoldConstantXPathExpressions extends AbstractConstantFolding 7756 { 7757 protected $supportedFunctions = [ 7758 'ceiling', 7759 'concat', 7760 'contains', 7761 'floor', 7762 'normalize-space', 7763 'number', 7764 'round', 7765 'starts-with', 7766 'string', 7767 'string-length', 7768 'substring', 7769 'substring-after', 7770 'substring-before', 7771 'sum', 7772 'translate' 7773 ]; 7774 protected function getOptimizationPasses() 7775 { 7776 return [ 7777 '(^(?:"[^"]*"|\'[^\']*\'|\\.[0-9]|[^"$&\'./:<=>@[\\]])++$)' => 'foldConstantXPathExpression' 7778 ]; 7779 } 7780 protected function canBeSerialized($value) 7781 { 7782 return (\is_string($value) || \is_integer($value) || \is_float($value)); 7783 } 7784 protected function evaluate($expr) 7785 { 7786 $useErrors = \libxml_use_internal_errors(\true); 7787 $result = $this->xpath->evaluate($expr); 7788 \libxml_use_internal_errors($useErrors); 7789 return $result; 7790 } 7791 protected function foldConstantXPathExpression(array $m) 7792 { 7793 $expr = $m[0]; 7794 if ($this->isConstantExpression($expr)) 7795 { 7796 $result = $this->evaluate($expr); 7797 if ($this->canBeSerialized($result)) 7798 { 7799 $foldedExpr = XPath::export($result); 7800 if (\strlen($foldedExpr) < \strlen($expr)) 7801 $expr = $foldedExpr; 7802 } 7803 } 7804 return $expr; 7805 } 7806 protected function isConstantExpression($expr) 7807 { 7808 $expr = \preg_replace('("[^"]*"|\'[^\']*\')', '0', $expr); 7809 \preg_match_all('(\\w[-\\w]+(?=\\())', $expr, $m); 7810 if (\count(\array_diff($m[0], $this->supportedFunctions)) > 0) 7811 return \false; 7812 return !\preg_match('([^\\s\\-0-9a-z\\(-.]|\\.(?![0-9])|\\b[-a-z](?![-\\w]+\\()|\\(\\s*\\))i', $expr); 7813 } 7814 } 7815 7816 /* 7817 * @package s9e\TextFormatter 7818 * @copyright Copyright (c) 2010-2019 The s9e Authors 7819 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7820 */ 7821 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 7822 use DOMElement; 7823 class OptimizeChoose extends AbstractChooseOptimization 7824 { 7825 protected function adoptChildren(DOMElement $branch) 7826 { 7827 while ($branch->firstChild->firstChild) 7828 $branch->appendChild($branch->firstChild->removeChild($branch->firstChild->firstChild)); 7829 $branch->removeChild($branch->firstChild); 7830 } 7831 protected function matchBranches($childType) 7832 { 7833 $branches = $this->getBranches(); 7834 if (!isset($branches[0]->$childType)) 7835 return \false; 7836 $childNode = $branches[0]->$childType; 7837 foreach ($branches as $branch) 7838 if (!isset($branch->$childType) || !$this->isEqualNode($childNode, $branch->$childType)) 7839 return \false; 7840 return \true; 7841 } 7842 protected function matchOnlyChild() 7843 { 7844 $branches = $this->getBranches(); 7845 if (!isset($branches[0]->firstChild)) 7846 return \false; 7847 $firstChild = $branches[0]->firstChild; 7848 if ($this->isXsl($firstChild, 'choose')) 7849 return \false; 7850 foreach ($branches as $branch) 7851 { 7852 if ($branch->childNodes->length !== 1 || !($branch->firstChild instanceof DOMElement)) 7853 return \false; 7854 if (!$this->isEqualTag($firstChild, $branch->firstChild)) 7855 return \false; 7856 } 7857 return \true; 7858 } 7859 protected function moveFirstChildBefore() 7860 { 7861 $branches = $this->getBranches(); 7862 $this->choose->parentNode->insertBefore(\array_pop($branches)->firstChild, $this->choose); 7863 foreach ($branches as $branch) 7864 $branch->removeChild($branch->firstChild); 7865 } 7866 protected function moveLastChildAfter() 7867 { 7868 $branches = $this->getBranches(); 7869 $node = \array_pop($branches)->lastChild; 7870 if (isset($this->choose->nextSibling)) 7871 $this->choose->parentNode->insertBefore($node, $this->choose->nextSibling); 7872 else 7873 $this->choose->parentNode->appendChild($node); 7874 foreach ($branches as $branch) 7875 $branch->removeChild($branch->lastChild); 7876 } 7877 protected function optimizeChoose() 7878 { 7879 if ($this->hasOtherwise()) 7880 { 7881 $this->optimizeCommonFirstChild(); 7882 $this->optimizeCommonLastChild(); 7883 $this->optimizeCommonOnlyChild(); 7884 $this->optimizeEmptyOtherwise(); 7885 } 7886 if ($this->isEmpty()) 7887 $this->choose->parentNode->removeChild($this->choose); 7888 else 7889 $this->optimizeSingleBranch(); 7890 } 7891 protected function optimizeCommonFirstChild() 7892 { 7893 while ($this->matchBranches('firstChild')) 7894 $this->moveFirstChildBefore(); 7895 } 7896 protected function optimizeCommonLastChild() 7897 { 7898 while ($this->matchBranches('lastChild')) 7899 $this->moveLastChildAfter(); 7900 } 7901 protected function optimizeCommonOnlyChild() 7902 { 7903 while ($this->matchOnlyChild()) 7904 $this->reparentChild(); 7905 } 7906 protected function optimizeEmptyOtherwise() 7907 { 7908 $query = 'xsl:otherwise[count(node()) = 0]'; 7909 foreach ($this->xpath($query, $this->choose) as $otherwise) 7910 $this->choose->removeChild($otherwise); 7911 } 7912 protected function optimizeSingleBranch() 7913 { 7914 $query = 'count(xsl:when) = 1 and not(xsl:otherwise)'; 7915 if (!$this->xpath->evaluate($query, $this->choose)) 7916 return; 7917 $when = $this->xpath('xsl:when', $this->choose)[0]; 7918 $if = $this->createElement('xsl:if'); 7919 $if->setAttribute('test', $when->getAttribute('test')); 7920 while ($when->firstChild) 7921 $if->appendChild($when->removeChild($when->firstChild)); 7922 $this->choose->parentNode->replaceChild($if, $this->choose); 7923 } 7924 protected function reparentChild() 7925 { 7926 $branches = $this->getBranches(); 7927 $childNode = $branches[0]->firstChild->cloneNode(); 7928 $childNode->appendChild($this->choose->parentNode->replaceChild($childNode, $this->choose)); 7929 foreach ($branches as $branch) 7930 $this->adoptChildren($branch); 7931 } 7932 } 7933 7934 /* 7935 * @package s9e\TextFormatter 7936 * @copyright Copyright (c) 2010-2019 The s9e Authors 7937 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7938 */ 7939 namespace s9e\TextFormatter\Configurator\TemplateNormalizations; 7940 use DOMElement; 7941 use DOMText; 7942 class OptimizeChooseText extends AbstractChooseOptimization 7943 { 7944 protected function adjustTextNodes($childType, $pos, $len = \PHP_INT_MAX) 7945 { 7946 foreach ($this->getBranches() as $branch) 7947 { 7948 $node = $branch->$childType; 7949 $node->nodeValue = \substr($node->textContent, $pos, $len); 7950 } 7951 } 7952 protected function getPrefixLength(array $strings) 7953 { 7954 $i = 0; 7955 $len = 0; 7956 $maxLen = \min(\array_map('strlen', $strings)); 7957 while ($i < $maxLen) 7958 { 7959 $c = $strings[0][$i]; 7960 foreach ($strings as $string) 7961 if ($string[$i] !== $c) 7962 break 2; 7963 $len = ++$i; 7964 } 7965 return $len; 7966 } 7967 protected function getTextContent($childType) 7968 { 7969 $strings = []; 7970 foreach ($this->getBranches() as $branch) 7971 { 7972 if (!($branch->$childType instanceof DOMText)) 7973 return []; 7974 $strings[] = $branch->$childType->textContent; 7975 } 7976 return $strings; 7977 } 7978 protected function optimizeChoose() 7979 { 7980 if (!$this->hasOtherwise()) 7981 return; 7982 $this->optimizeLeadingText(); 7983 $this->optimizeTrailingText(); 7984 } 7985 protected function optimizeLeadingText() 7986 { 7987 $strings = $this->getTextContent('firstChild'); 7988 if (empty($strings)) 7989 return; 7990 $len = $this->getPrefixLength($strings); 7991 if ($len) 7992 { 7993 $this->adjustTextNodes('firstChild', $len); 7994 $this->choose->parentNode->insertBefore( 7995 $this->createText(\substr($strings[0], 0, $len)), 7996 $this->choose 7997 ); 7998 } 7999 } 8000 protected function optimizeTrailingText() 8001 { 8002 $strings = $this->getTextContent('lastChild'); 8003 if (empty($strings)) 8004 return; 8005 $len = $this->getPrefixLength(\array_map('strrev', $strings)); 8006 if ($len) 8007 { 8008 $this->adjustTextNodes('lastChild', 0, -$len); 8009 $this->choose->parentNode->insertBefore( 8010 $this->createText(\substr($strings[0], -$len)), 8011 $this->choose->nextSibling 8012 ); 8013 } 8014 } 8015 } 8016 8017 /* 8018 * @package s9e\TextFormatter 8019 * @copyright Copyright (c) 2010-2019 The s9e Authors 8020 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8021 */ 8022 namespace s9e\TextFormatter\Configurator\Collections; 8023 use RuntimeException; 8024 use s9e\TextFormatter\Configurator\Items\Attribute; 8025 use s9e\TextFormatter\Configurator\Validators\AttributeName; 8026 class AttributeCollection extends NormalizedCollection 8027 { 8028 protected $onDuplicateAction = 'replace'; 8029 protected function getAlreadyExistsException($key) 8030 { 8031 return new RuntimeException("Attribute '" . $key . "' already exists"); 8032 } 8033 protected function getNotExistException($key) 8034 { 8035 return new RuntimeException("Attribute '" . $key . "' does not exist"); 8036 } 8037 public function normalizeKey($key) 8038 { 8039 return AttributeName::normalize($key); 8040 } 8041 public function normalizeValue($value) 8042 { 8043 return ($value instanceof Attribute) 8044 ? $value 8045 : new Attribute($value); 8046 } 8047 } 8048 8049 /* 8050 * @package s9e\TextFormatter 8051 * @copyright Copyright (c) 2010-2019 The s9e Authors 8052 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8053 */ 8054 namespace s9e\TextFormatter\Configurator\Collections; 8055 use InvalidArgumentException; 8056 use s9e\TextFormatter\Configurator\Items\AttributeFilter; 8057 class AttributeFilterCollection extends NormalizedCollection 8058 { 8059 public function get($key) 8060 { 8061 $key = $this->normalizeKey($key); 8062 if (!$this->exists($key)) 8063 if ($key[0] === '#') 8064 $this->set($key, self::getDefaultFilter(\substr($key, 1))); 8065 else 8066 $this->set($key, new AttributeFilter($key)); 8067 $filter = parent::get($key); 8068 $filter = clone $filter; 8069 return $filter; 8070 } 8071 public static function getDefaultFilter($filterName) 8072 { 8073 $filterName = \ucfirst(\strtolower($filterName)); 8074 $className = 's9e\\TextFormatter\\Configurator\\Items\\AttributeFilters\\' . $filterName . 'Filter'; 8075 if (!\class_exists($className)) 8076 throw new InvalidArgumentException("Unknown attribute filter '" . $filterName . "'"); 8077 return new $className; 8078 } 8079 public function normalizeKey($key) 8080 { 8081 if (\preg_match('/^#[a-z_0-9]+$/Di', $key)) 8082 return \strtolower($key); 8083 if (\is_string($key) && \is_callable($key)) 8084 return $key; 8085 throw new InvalidArgumentException("Invalid filter name '" . $key . "'"); 8086 } 8087 public function normalizeValue($value) 8088 { 8089 if ($value instanceof AttributeFilter) 8090 return $value; 8091 if (\is_callable($value)) 8092 return new AttributeFilter($value); 8093 throw new InvalidArgumentException('Argument 1 passed to ' . __METHOD__ . ' must be a valid callback or an instance of s9e\\TextFormatter\\Configurator\\Items\\AttributeFilter'); 8094 } 8095 } 8096 8097 /* 8098 * @package s9e\TextFormatter 8099 * @copyright Copyright (c) 2010-2019 The s9e Authors 8100 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8101 */ 8102 namespace s9e\TextFormatter\Configurator\Collections; 8103 use InvalidArgumentException; 8104 class NormalizedList extends NormalizedCollection 8105 { 8106 public function add($value, $void = \null) 8107 { 8108 return $this->append($value); 8109 } 8110 public function append($value) 8111 { 8112 $value = $this->normalizeValue($value); 8113 $this->items[] = $value; 8114 return $value; 8115 } 8116 public function delete($key) 8117 { 8118 parent::delete($key); 8119 $this->items = \array_values($this->items); 8120 } 8121 public function insert($offset, $value) 8122 { 8123 $offset = $this->normalizeKey($offset); 8124 $value = $this->normalizeValue($value); 8125 \array_splice($this->items, $offset, 0, [$value]); 8126 return $value; 8127 } 8128 public function normalizeKey($key) 8129 { 8130 $normalizedKey = \filter_var( 8131 (\preg_match('(^-\\d+$)D', $key)) ? \count($this->items) + $key : $key, 8132 \FILTER_VALIDATE_INT, 8133 [ 8134 'options' => [ 8135 'min_range' => 0, 8136 'max_range' => \count($this->items) 8137 ] 8138 ] 8139 ); 8140 if ($normalizedKey === \false) 8141 throw new InvalidArgumentException("Invalid offset '" . $key . "'"); 8142 return $normalizedKey; 8143 } 8144 public function offsetSet($offset, $value) 8145 { 8146 if ($offset === \null) 8147 $this->append($value); 8148 else 8149 parent::offsetSet($offset, $value); 8150 } 8151 public function prepend($value) 8152 { 8153 $value = $this->normalizeValue($value); 8154 \array_unshift($this->items, $value); 8155 return $value; 8156 } 8157 public function remove($value) 8158 { 8159 $keys = \array_keys($this->items, $this->normalizeValue($value)); 8160 foreach ($keys as $k) 8161 unset($this->items[$k]); 8162 $this->items = \array_values($this->items); 8163 return \count($keys); 8164 } 8165 } 8166 8167 /* 8168 * @package s9e\TextFormatter 8169 * @copyright Copyright (c) 2010-2019 The s9e Authors 8170 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8171 */ 8172 namespace s9e\TextFormatter\Configurator\Collections; 8173 use InvalidArgumentException; 8174 use RuntimeException; 8175 use s9e\TextFormatter\Configurator; 8176 use s9e\TextFormatter\Plugins\ConfiguratorBase; 8177 class PluginCollection extends NormalizedCollection 8178 { 8179 protected $configurator; 8180 public function __construct(Configurator $configurator) 8181 { 8182 $this->configurator = $configurator; 8183 } 8184 public function finalize() 8185 { 8186 foreach ($this->items as $plugin) 8187 $plugin->finalize(); 8188 } 8189 public function normalizeKey($pluginName) 8190 { 8191 if (!\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $pluginName)) 8192 throw new InvalidArgumentException("Invalid plugin name '" . $pluginName . "'"); 8193 return $pluginName; 8194 } 8195 public function normalizeValue($value) 8196 { 8197 if (\is_string($value) && \class_exists($value)) 8198 $value = new $value($this->configurator); 8199 if ($value instanceof ConfiguratorBase) 8200 return $value; 8201 throw new InvalidArgumentException('PluginCollection::normalizeValue() expects a class name or an object that implements s9e\\TextFormatter\\Plugins\\ConfiguratorBase'); 8202 } 8203 public function load($pluginName, array $overrideProps = []) 8204 { 8205 $pluginName = $this->normalizeKey($pluginName); 8206 $className = 's9e\\TextFormatter\\Plugins\\' . $pluginName . '\\Configurator'; 8207 if (!\class_exists($className)) 8208 throw new RuntimeException("Class '" . $className . "' does not exist"); 8209 $plugin = new $className($this->configurator, $overrideProps); 8210 $this->set($pluginName, $plugin); 8211 return $plugin; 8212 } 8213 public function asConfig() 8214 { 8215 $plugins = parent::asConfig(); 8216 foreach ($plugins as $pluginName => &$pluginConfig) 8217 { 8218 $plugin = $this->get($pluginName); 8219 $pluginConfig += $plugin->getBaseProperties(); 8220 if ($pluginConfig['quickMatch'] === \false) 8221 unset($pluginConfig['quickMatch']); 8222 if (!isset($pluginConfig['regexp'])) 8223 unset($pluginConfig['regexpLimit']); 8224 $className = 's9e\\TextFormatter\\Plugins\\' . $pluginName . '\\Parser'; 8225 if ($pluginConfig['className'] === $className) 8226 unset($pluginConfig['className']); 8227 } 8228 unset($pluginConfig); 8229 return $plugins; 8230 } 8231 } 8232 8233 /* 8234 * @package s9e\TextFormatter 8235 * @copyright Copyright (c) 2010-2019 The s9e Authors 8236 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8237 */ 8238 namespace s9e\TextFormatter\Configurator\Collections; 8239 use RuntimeException; 8240 use s9e\TextFormatter\Configurator\Items\Tag; 8241 use s9e\TextFormatter\Configurator\Validators\TagName; 8242 class TagCollection extends NormalizedCollection 8243 { 8244 protected $onDuplicateAction = 'replace'; 8245 protected function getAlreadyExistsException($key) 8246 { 8247 return new RuntimeException("Tag '" . $key . "' already exists"); 8248 } 8249 protected function getNotExistException($key) 8250 { 8251 return new RuntimeException("Tag '" . $key . "' does not exist"); 8252 } 8253 public function normalizeKey($key) 8254 { 8255 return TagName::normalize($key); 8256 } 8257 public function normalizeValue($value) 8258 { 8259 return ($value instanceof Tag) 8260 ? $value 8261 : new Tag($value); 8262 } 8263 } 8264 8265 /* 8266 * @package s9e\TextFormatter 8267 * @copyright Copyright (c) 2010-2019 The s9e Authors 8268 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8269 */ 8270 namespace s9e\TextFormatter\Configurator\Collections; 8271 use s9e\TextFormatter\Configurator\Validators\TemplateParameterName; 8272 class TemplateParameterCollection extends NormalizedCollection 8273 { 8274 public function normalizeKey($key) 8275 { 8276 return TemplateParameterName::normalize($key); 8277 } 8278 public function normalizeValue($value) 8279 { 8280 return (string) $value; 8281 } 8282 } 8283 8284 /* 8285 * @package s9e\TextFormatter 8286 * @copyright Copyright (c) 2010-2019 The s9e Authors 8287 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8288 */ 8289 namespace s9e\TextFormatter\Configurator\Items; 8290 use s9e\TextFormatter\Configurator\Traits\TemplateSafeness; 8291 class AttributeFilter extends Filter 8292 { 8293 use TemplateSafeness; 8294 public function __construct($callback) 8295 { 8296 parent::__construct($callback); 8297 $this->resetParameters(); 8298 $this->addParameterByName('attrValue'); 8299 } 8300 public function isSafeInJS() 8301 { 8302 $safeCallbacks = [ 8303 'urlencode', 8304 'strtotime', 8305 'rawurlencode' 8306 ]; 8307 if (\in_array($this->callback, $safeCallbacks, \true)) 8308 return \true; 8309 return $this->isSafe('InJS'); 8310 } 8311 } 8312 8313 /* 8314 * @package s9e\TextFormatter 8315 * @copyright Copyright (c) 2010-2019 The s9e Authors 8316 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8317 */ 8318 namespace s9e\TextFormatter\Configurator\Items; 8319 class TagFilter extends Filter 8320 { 8321 public function __construct($callback) 8322 { 8323 parent::__construct($callback); 8324 $this->resetParameters(); 8325 $this->addParameterByName('tag'); 8326 } 8327 } 8328 8329 /* 8330 * @package s9e\TextFormatter 8331 * @copyright Copyright (c) 2010-2019 The s9e Authors 8332 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8333 */ 8334 namespace s9e\TextFormatter\Configurator\Collections; 8335 use InvalidArgumentException; 8336 use s9e\TextFormatter\Configurator\Items\ProgrammableCallback; 8337 abstract class FilterChain extends NormalizedList 8338 { 8339 abstract protected function getFilterClassName(); 8340 public function containsCallback(callable $callback) 8341 { 8342 $pc = new ProgrammableCallback($callback); 8343 $callback = $pc->getCallback(); 8344 foreach ($this->items as $filter) 8345 if ($callback === $filter->getCallback()) 8346 return \true; 8347 return \false; 8348 } 8349 public function normalizeValue($value) 8350 { 8351 $className = $this->getFilterClassName(); 8352 if ($value instanceof $className) 8353 return $value; 8354 if (!\is_callable($value)) 8355 throw new InvalidArgumentException('Filter ' . \var_export($value, \true) . ' is neither callable nor an instance of ' . $className); 8356 return new $className($value); 8357 } 8358 } 8359 8360 /* 8361 * @package s9e\TextFormatter 8362 * @copyright Copyright (c) 2010-2019 The s9e Authors 8363 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8364 */ 8365 namespace s9e\TextFormatter\Configurator\Collections; 8366 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder; 8367 use s9e\TextFormatter\Configurator\Items\Regexp; 8368 class HostnameList extends NormalizedList 8369 { 8370 public function asConfig() 8371 { 8372 if (empty($this->items)) 8373 return \null; 8374 return new Regexp($this->getRegexp()); 8375 } 8376 public function getRegexp() 8377 { 8378 $hosts = []; 8379 foreach ($this->items as $host) 8380 $hosts[] = $this->normalizeHostmask($host); 8381 $regexp = RegexpBuilder::fromList( 8382 $hosts, 8383 [ 8384 'specialChars' => [ 8385 '*' => '.*', 8386 '^' => '^', 8387 '$' => '$' 8388 ] 8389 ] 8390 ); 8391 return '/' . $regexp . '/DSis'; 8392 } 8393 protected function normalizeHostmask($host) 8394 { 8395 if (\preg_match('#[\\x80-\xff]#', $host) && \function_exists('idn_to_ascii')) 8396 { 8397 $variant = (\defined('INTL_IDNA_VARIANT_UTS46')) ? \INTL_IDNA_VARIANT_UTS46 : 0; 8398 $host = \idn_to_ascii($host, 0, $variant); 8399 } 8400 if (\substr($host, 0, 1) === '*') 8401 $host = \ltrim($host, '*'); 8402 else 8403 $host = '^' . $host; 8404 if (\substr($host, -1) === '*') 8405 $host = \rtrim($host, '*'); 8406 else 8407 $host .= '$'; 8408 return $host; 8409 } 8410 } 8411 8412 /* 8413 * @package s9e\TextFormatter 8414 * @copyright Copyright (c) 2010-2019 The s9e Authors 8415 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8416 */ 8417 namespace s9e\TextFormatter\Configurator\Collections; 8418 use InvalidArgumentException; 8419 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator; 8420 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator; 8421 class RulesGeneratorList extends NormalizedList 8422 { 8423 public function normalizeValue($generator) 8424 { 8425 if (\is_string($generator)) 8426 { 8427 $className = 's9e\\TextFormatter\\Configurator\\RulesGenerators\\' . $generator; 8428 if (\class_exists($className)) 8429 $generator = new $className; 8430 } 8431 if (!($generator instanceof BooleanRulesGenerator) 8432 && !($generator instanceof TargetedRulesGenerator)) 8433 throw new InvalidArgumentException('Invalid rules generator ' . \var_export($generator, \true)); 8434 return $generator; 8435 } 8436 } 8437 8438 /* 8439 * @package s9e\TextFormatter 8440 * @copyright Copyright (c) 2010-2019 The s9e Authors 8441 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8442 */ 8443 namespace s9e\TextFormatter\Configurator\Collections; 8444 use InvalidArgumentException; 8445 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder; 8446 use s9e\TextFormatter\Configurator\Items\Regexp; 8447 class SchemeList extends NormalizedList 8448 { 8449 public function asConfig() 8450 { 8451 return new Regexp('/^' . RegexpBuilder::fromList($this->items) . '$/Di'); 8452 } 8453 public function normalizeValue($scheme) 8454 { 8455 if (!\preg_match('#^[a-z][a-z0-9+\\-.]*$#Di', $scheme)) 8456 throw new InvalidArgumentException("Invalid scheme name '" . $scheme . "'"); 8457 return \strtolower($scheme); 8458 } 8459 } 8460 8461 /* 8462 * @package s9e\TextFormatter 8463 * @copyright Copyright (c) 2010-2019 The s9e Authors 8464 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8465 */ 8466 namespace s9e\TextFormatter\Configurator\Collections; 8467 use s9e\TextFormatter\Configurator\TemplateCheck; 8468 class TemplateCheckList extends NormalizedList 8469 { 8470 public function normalizeValue($check) 8471 { 8472 if (!($check instanceof TemplateCheck)) 8473 { 8474 $className = 's9e\\TextFormatter\\Configurator\\TemplateChecks\\' . $check; 8475 $check = new $className; 8476 } 8477 return $check; 8478 } 8479 } 8480 8481 /* 8482 * @package s9e\TextFormatter 8483 * @copyright Copyright (c) 2010-2019 The s9e Authors 8484 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8485 */ 8486 namespace s9e\TextFormatter\Configurator\Collections; 8487 use s9e\TextFormatter\Configurator\TemplateNormalizations\AbstractNormalization; 8488 use s9e\TextFormatter\Configurator\TemplateNormalizations\Custom; 8489 class TemplateNormalizationList extends NormalizedList 8490 { 8491 public function normalizeValue($value) 8492 { 8493 if ($value instanceof AbstractNormalization) 8494 return $value; 8495 if (\is_callable($value)) 8496 return new Custom($value); 8497 $className = 's9e\\TextFormatter\\Configurator\\TemplateNormalizations\\' . $value; 8498 return new $className; 8499 } 8500 } 8501 8502 /* 8503 * @package s9e\TextFormatter 8504 * @copyright Copyright (c) 2010-2019 The s9e Authors 8505 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8506 */ 8507 namespace s9e\TextFormatter\Configurator\Items\AttributeFilters; 8508 use s9e\TextFormatter\Configurator\Items\AttributeFilter; 8509 class UrlFilter extends AttributeFilter 8510 { 8511 public function __construct() 8512 { 8513 parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\UrlFilter::filter'); 8514 $this->resetParameters(); 8515 $this->addParameterByName('attrValue'); 8516 $this->addParameterByName('urlConfig'); 8517 $this->addParameterByName('logger'); 8518 $this->setJS('UrlFilter.filter'); 8519 } 8520 public function isSafeInCSS() 8521 { 8522 return \true; 8523 } 8524 public function isSafeInJS() 8525 { 8526 return \true; 8527 } 8528 public function isSafeAsURL() 8529 { 8530 return \true; 8531 } 8532 } 8533 8534 /* 8535 * @package s9e\TextFormatter 8536 * @copyright Copyright (c) 2010-2019 The s9e Authors 8537 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8538 */ 8539 namespace s9e\TextFormatter\Configurator\Collections; 8540 class AttributeFilterChain extends FilterChain 8541 { 8542 public function getFilterClassName() 8543 { 8544 return 's9e\\TextFormatter\\Configurator\\Items\\AttributeFilter'; 8545 } 8546 public function normalizeValue($value) 8547 { 8548 if (\is_string($value) && \preg_match('(^#\\w+$)', $value)) 8549 $value = AttributeFilterCollection::getDefaultFilter(\substr($value, 1)); 8550 return parent::normalizeValue($value); 8551 } 8552 } 8553 8554 /* 8555 * @package s9e\TextFormatter 8556 * @copyright Copyright (c) 2010-2019 The s9e Authors 8557 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 8558 */ 8559 namespace s9e\TextFormatter\Configurator\Collections; 8560 class TagFilterChain extends FilterChain 8561 { 8562 public function getFilterClassName() 8563 { 8564 return 's9e\\TextFormatter\\Configurator\\Items\\TagFilter'; 8565 } 8566 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Nov 11 20:33:01 2020 | Cross-referenced by PHPXref 0.7.1 |