Verzeichnisstruktur phpBB-3.2.0
- Veröffentlicht
- 06.01.2017
So funktioniert es
|
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
Configurator.php
0001 <?php
0002
0003 /*
0004 * @package s9e\TextFormatter
0005 * @copyright Copyright (c) 2010-2016 The s9e Authors
0006 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
0007 */
0008 namespace s9e\TextFormatter;
0009 use InvalidArgumentException;
0010 use RuntimeException;
0011 use s9e\TextFormatter\Configurator\BundleGenerator;
0012 use s9e\TextFormatter\Configurator\Collections\AttributeFilterCollection;
0013 use s9e\TextFormatter\Configurator\Collections\PluginCollection;
0014 use s9e\TextFormatter\Configurator\Collections\Ruleset;
0015 use s9e\TextFormatter\Configurator\Collections\TagCollection;
0016 use s9e\TextFormatter\Configurator\ConfigProvider;
0017 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
0018 use s9e\TextFormatter\Configurator\Helpers\RulesHelper;
0019 use s9e\TextFormatter\Configurator\JavaScript;
0020 use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
0021 use s9e\TextFormatter\Configurator\Rendering;
0022 use s9e\TextFormatter\Configurator\RulesGenerator;
0023 use s9e\TextFormatter\Configurator\TemplateChecker;
0024 use s9e\TextFormatter\Configurator\TemplateNormalizer;
0025 use s9e\TextFormatter\Configurator\UrlConfig;
0026 class Configurator implements ConfigProvider
0027 {
0028 public $attributeFilters;
0029 public $bundleGenerator;
0030 public $javascript;
0031 public $plugins;
0032 public $registeredVars;
0033 public $rendering;
0034 public $rootRules;
0035 public $rulesGenerator;
0036 public $tags;
0037 public $templateChecker;
0038 public $templateNormalizer;
0039 public function __construct()
0040 {
0041 $this->attributeFilters = new AttributeFilterCollection;
0042 $this->bundleGenerator = new BundleGenerator($this);
0043 $this->plugins = new PluginCollection($this);
0044 $this->registeredVars = array('urlConfig' => new UrlConfig);
0045 $this->rendering = new Rendering($this);
0046 $this->rootRules = new Ruleset;
0047 $this->rulesGenerator = new RulesGenerator;
0048 $this->tags = new TagCollection;
0049 $this->templateChecker = new TemplateChecker;
0050 $this->templateNormalizer = new TemplateNormalizer;
0051 }
0052 public function __get($k)
0053 {
0054 if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k))
0055 return (isset($this->plugins[$k]))
0056 ? $this->plugins[$k]
0057 : $this->plugins->load($k);
0058 if (isset($this->registeredVars[$k]))
0059 return $this->registeredVars[$k];
0060 throw new RuntimeException("Undefined property '" . __CLASS__ . '::$' . $k . "'");
0061 }
0062 public function __isset($k)
0063 {
0064 if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k))
0065 return isset($this->plugins[$k]);
0066 return isset($this->registeredVars[$k]);
0067 }
0068 public function __set($k, $v)
0069 {
0070 if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k))
0071 $this->plugins[$k] = $v;
0072 else
0073 $this->registeredVars[$k] = $v;
0074 }
0075 public function __unset($k)
0076 {
0077 if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k))
0078 unset($this->plugins[$k]);
0079 else
0080 unset($this->registeredVars[$k]);
0081 }
0082 public function enableJavaScript()
0083 {
0084 if (!isset($this->javascript))
0085 $this->javascript = new JavaScript($this);
0086 }
0087 public function finalize(array $options = array())
0088 {
0089 $return = array();
0090 $options += array(
0091 'addHTML5Rules' => \true,
0092 'optimizeConfig' => \true,
0093 'returnJS' => isset($this->javascript),
0094 'returnParser' => \true,
0095 'returnRenderer' => \true
0096 );
0097 if ($options['addHTML5Rules'])
0098 $this->addHTML5Rules($options);
0099 if ($options['returnRenderer'])
0100 {
0101 $renderer = $this->rendering->getRenderer();
0102 if (isset($options['finalizeRenderer']))
0103 \call_user_func($options['finalizeRenderer'], $renderer);
0104 $return['renderer'] = $renderer;
0105 }
0106 if ($options['returnJS'] || $options['returnParser'])
0107 {
0108 $config = $this->asConfig();
0109 if ($options['returnJS'])
0110 $return['js'] = $this->javascript->getParser(ConfigHelper::filterConfig($config, 'JS'));
0111 if ($options['returnParser'])
0112 {
0113 $config = ConfigHelper::filterConfig($config, 'PHP');
0114 if ($options['optimizeConfig'])
0115 ConfigHelper::optimizeArray($config);
0116 $parser = new Parser($config);
0117 if (isset($options['finalizeParser']))
0118 \call_user_func($options['finalizeParser'], $parser);
0119 $return['parser'] = $parser;
0120 }
0121 }
0122 return $return;
0123 }
0124 public function loadBundle($bundleName)
0125 {
0126 if (!\preg_match('#^[A-Z][A-Za-z0-9]+$#D', $bundleName))
0127 throw new InvalidArgumentException("Invalid bundle name '" . $bundleName . "'");
0128 $className = __CLASS__ . '\\Bundles\\' . $bundleName;
0129 $bundle = new $className;
0130 $bundle->configure($this);
0131 }
0132 public function saveBundle($className, $filepath, array $options = array())
0133 {
0134 $file = "<?php\n\n" . $this->bundleGenerator->generate($className, $options);
0135 return (\file_put_contents($filepath, $file) !== \false);
0136 }
0137 public function addHTML5Rules(array $options = array())
0138 {
0139 $options += array('rootRules' => $this->rootRules);
0140 $this->plugins->finalize();
0141 foreach ($this->tags as $tag)
0142 $this->templateNormalizer->normalizeTag($tag);
0143 $rules = $this->rulesGenerator->getRules($this->tags, $options);
0144 $this->rootRules->merge($rules['root'], \false);
0145 foreach ($rules['tags'] as $tagName => $tagRules)
0146 $this->tags[$tagName]->rules->merge($tagRules, \false);
0147 }
0148 public function asConfig()
0149 {
0150 $this->plugins->finalize();
0151 $properties = \get_object_vars($this);
0152 unset($properties['attributeFilters']);
0153 unset($properties['bundleGenerator']);
0154 unset($properties['javascript']);
0155 unset($properties['rendering']);
0156 unset($properties['rulesGenerator']);
0157 unset($properties['registeredVars']);
0158 unset($properties['templateChecker']);
0159 unset($properties['templateNormalizer']);
0160 unset($properties['stylesheet']);
0161 $config = ConfigHelper::toArray($properties);
0162 $bitfields = RulesHelper::getBitfields($this->tags, $this->rootRules);
0163 $config['rootContext'] = $bitfields['root'];
0164 $config['rootContext']['flags'] = $config['rootRules']['flags'];
0165 $config['registeredVars'] = ConfigHelper::toArray($this->registeredVars, \true);
0166 $config += array(
0167 'plugins' => array(),
0168 'tags' => array()
0169 );
0170 $config['tags'] = \array_intersect_key($config['tags'], $bitfields['tags']);
0171 foreach ($bitfields['tags'] as $tagName => $tagBitfields)
0172 $config['tags'][$tagName] += $tagBitfields;
0173 unset($config['rootRules']);
0174 return $config;
0175 }
0176 }
0177
0178 /*
0179 * @package s9e\TextFormatter
0180 * @copyright Copyright (c) 2010-2016 The s9e Authors
0181 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
0182 */
0183 namespace s9e\TextFormatter\Configurator;
0184 use s9e\TextFormatter\Configurator;
0185 use s9e\TextFormatter\Configurator\RendererGenerators\PHP;
0186 class BundleGenerator
0187 {
0188 protected $configurator;
0189 public $serializer = 'serialize';
0190 public $unserializer = 'unserialize';
0191 public function __construct(Configurator $configurator)
0192 {
0193 $this->configurator = $configurator;
0194 }
0195 public function generate($className, array $options = array())
0196 {
0197 $options += array('autoInclude' => \true);
0198 $objects = $this->configurator->finalize($options);
0199 $parser = $objects['parser'];
0200 $renderer = $objects['renderer'];
0201 $namespace = '';
0202 if (\preg_match('#(.*)\\\\([^\\\\]+)$#', $className, $m))
0203 {
0204 $namespace = $m[1];
0205 $className = $m[2];
0206 }
0207 $php = array();
0208 $php[] = '/**';
0209 $php[] = '* @package s9e\TextFormatter';
0210 $php[] = '* @copyright Copyright (c) 2010-2016 The s9e Authors';
0211 $php[] = '* @license http://www.opensource.org/licenses/mit-license.php The MIT License';
0212 $php[] = '*/';
0213 if ($namespace)
0214 {
0215 $php[] = 'namespace ' . $namespace . ';';
0216 $php[] = '';
0217 }
0218 $php[] = 'abstract class ' . $className . ' extends \\s9e\\TextFormatter\\Bundle';
0219 $php[] = '{';
0220 $php[] = ' /**';
0221 $php[] = ' * @var s9e\\TextFormatter\\Parser Singleton instance used by parse()';
0222 $php[] = ' */';
0223 $php[] = ' protected static $parser;';
0224 $php[] = '';
0225 $php[] = ' /**';
0226 $php[] = ' * @var s9e\\TextFormatter\\Renderer Singleton instance used by render()';
0227 $php[] = ' */';
0228 $php[] = ' protected static $renderer;';
0229 $php[] = '';
0230 $events = array(
0231 'beforeParse'
0232 => 'Callback executed before parse(), receives the original text as argument',
0233 'afterParse'
0234 => 'Callback executed after parse(), receives the parsed text as argument',
0235 'beforeRender'
0236 => 'Callback executed before render(), receives the parsed text as argument',
0237 'afterRender'
0238 => 'Callback executed after render(), receives the output as argument',
0239 'beforeUnparse'
0240 => 'Callback executed before unparse(), receives the parsed text as argument',
0241 'afterUnparse'
0242 => 'Callback executed after unparse(), receives the original text as argument'
0243 );
0244 foreach ($events as $eventName => $eventDesc)
0245 if (isset($options[$eventName]))
0246 {
0247 $php[] = ' /**';
0248 $php[] = ' * @var ' . $eventDesc;
0249 $php[] = ' */';
0250 $php[] = ' public static $' . $eventName . ' = ' . \var_export($options[$eventName], \true) . ';';
0251 $php[] = '';
0252 }
0253 $php[] = ' /**';
0254 $php[] = ' * Return a new instance of s9e\\TextFormatter\\Parser';
0255 $php[] = ' *';
0256 $php[] = ' * @return s9e\\TextFormatter\\Parser';
0257 $php[] = ' */';
0258 $php[] = ' public static function getParser()';
0259 $php[] = ' {';
0260 if (isset($options['parserSetup']))
0261 {
0262 $php[] = ' $parser = ' . $this->exportObject($parser) . ';';
0263 $php[] = ' ' . $this->exportCallback($namespace, $options['parserSetup'], '$parser') . ';';
0264 $php[] = '';
0265 $php[] = ' return $parser;';
0266 }
0267 else
0268 $php[] = ' return ' . $this->exportObject($parser) . ';';
0269 $php[] = ' }';
0270 $php[] = '';
0271 $php[] = ' /**';
0272 $php[] = ' * Return a new instance of s9e\\TextFormatter\\Renderer';
0273 $php[] = ' *';
0274 $php[] = ' * @return s9e\\TextFormatter\\Renderer';
0275 $php[] = ' */';
0276 $php[] = ' public static function getRenderer()';
0277 $php[] = ' {';
0278 if (!empty($options['autoInclude'])
0279 && $this->configurator->rendering->engine instanceof PHP
0280 && isset($this->configurator->rendering->engine->lastFilepath))
0281 {
0282 $className = \get_class($renderer);
0283 $filepath = \realpath($this->configurator->rendering->engine->lastFilepath);
0284 $php[] = ' if (!class_exists(' . \var_export($className, \true) . ', false)';
0285 $php[] = ' && file_exists(' . \var_export($filepath, \true) . '))';
0286 $php[] = ' {';
0287 $php[] = ' include ' . \var_export($filepath, \true) . ';';
0288 $php[] = ' }';
0289 $php[] = '';
0290 }
0291 if (isset($options['rendererSetup']))
0292 {
0293 $php[] = ' $renderer = ' . $this->exportObject($renderer) . ';';
0294 $php[] = ' ' . $this->exportCallback($namespace, $options['rendererSetup'], '$renderer') . ';';
0295 $php[] = '';
0296 $php[] = ' return $renderer;';
0297 }
0298 else
0299 $php[] = ' return ' . $this->exportObject($renderer) . ';';
0300 $php[] = ' }';
0301 $php[] = '}';
0302 return \implode("\n", $php);
0303 }
0304 protected function exportCallback($namespace, $callback, $argument)
0305 {
0306 if (\is_array($callback) && \is_string($callback[0]))
0307 $callback = $callback[0] . '::' . $callback[1];
0308 if (!\is_string($callback))
0309 return 'call_user_func(' . \var_export($callback, \true) . ', ' . $argument . ')';
0310 if ($callback[0] !== '\\')
0311 $callback = '\\' . $callback;
0312 if (\substr($callback, 0, 2 + \strlen($namespace)) === '\\' . $namespace . '\\')
0313 $callback = \substr($callback, 2 + \strlen($namespace));
0314 return $callback . '(' . $argument . ')';
0315 }
0316 protected function exportObject($obj)
0317 {
0318 $str = \call_user_func($this->serializer, $obj);
0319 $str = \var_export($str, \true);
0320 return $this->unserializer . '(' . $str . ')';
0321 }
0322 }
0323
0324 /*
0325 * @package s9e\TextFormatter
0326 * @copyright Copyright (c) 2010-2016 The s9e Authors
0327 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
0328 */
0329 namespace s9e\TextFormatter\Configurator;
0330 interface ConfigProvider
0331 {
0332 public function asConfig();
0333 }
0334
0335 /*
0336 * @package s9e\TextFormatter
0337 * @copyright Copyright (c) 2010-2016 The s9e Authors
0338 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
0339 */
0340 namespace s9e\TextFormatter\Configurator;
0341 interface FilterableConfigValue
0342 {
0343 public function filterConfig($target);
0344 }
0345
0346 /*
0347 * @package s9e\TextFormatter
0348 * @copyright Copyright (c) 2010-2016 The s9e Authors
0349 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
0350 */
0351 namespace s9e\TextFormatter\Configurator\Helpers;
0352 use DOMAttr;
0353 use RuntimeException;
0354 abstract class AVTHelper
0355 {
0356 public static function parse($attrValue)
0357 {
0358 $tokens = array();
0359 $attrLen = \strlen($attrValue);
0360 $pos = 0;
0361 while ($pos < $attrLen)
0362 {
0363 if ($attrValue[$pos] === '{')
0364 {
0365 if (\substr($attrValue, $pos, 2) === '{{')
0366 {
0367 $tokens[] = array('literal', '{');
0368 $pos += 2;
0369 continue;
0370 }
0371 ++$pos;
0372 $expr = '';
0373 while ($pos < $attrLen)
0374 {
0375 $spn = \strcspn($attrValue, '\'"}', $pos);
0376 if ($spn)
0377 {
0378 $expr .= \substr($attrValue, $pos, $spn);
0379 $pos += $spn;
0380 }
0381 if ($pos >= $attrLen)
0382 throw new RuntimeException('Unterminated XPath expression');
0383 $c = $attrValue[$pos];
0384 ++$pos;
0385 if ($c === '}')
0386 break;
0387 $quotePos = \strpos($attrValue, $c, $pos);
0388 if ($quotePos === \false)
0389 throw new RuntimeException('Unterminated XPath expression');
0390 $expr .= $c . \substr($attrValue, $pos, $quotePos + 1 - $pos);
0391 $pos = 1 + $quotePos;
0392 }
0393 $tokens[] = array('expression', $expr);
0394 }
0395 $spn = \strcspn($attrValue, '{', $pos);
0396 if ($spn)
0397 {
0398 $str = \substr($attrValue, $pos, $spn);
0399 $str = \str_replace('}}', '}', $str);
0400 $tokens[] = array('literal', $str);
0401 $pos += $spn;
0402 }
0403 }
0404 return $tokens;
0405 }
0406 public static function replace(DOMAttr $attribute, $callback)
0407 {
0408 $tokens = self::parse($attribute->value);
0409 foreach ($tokens as $k => $token)
0410 $tokens[$k] = $callback($token);
0411 $attribute->value = \htmlspecialchars(self::serialize($tokens), \ENT_NOQUOTES, 'UTF-8');
0412 }
0413 public static function serialize(array $tokens)
0414 {
0415 $attrValue = '';
0416 foreach ($tokens as $token)
0417 if ($token[0] === 'literal')
0418 $attrValue .= \preg_replace('([{}])', '$0$0', $token[1]);
0419 elseif ($token[0] === 'expression')
0420 $attrValue .= '{' . $token[1] . '}';
0421 else
0422 throw new RuntimeException('Unknown token type');
0423 return $attrValue;
0424 }
0425 public static function toXSL($attrValue)
0426 {
0427 $xsl = '';
0428 foreach (self::parse($attrValue) as $_f6b3b659)
0429 {
0430 list($type, $content) = $_f6b3b659;
0431 if ($type === 'literal')
0432 $xsl .= \htmlspecialchars($content, \ENT_NOQUOTES, 'UTF-8');
0433 else
0434 $xsl .= '<xsl:value-of select="' . \htmlspecialchars($content, \ENT_COMPAT, 'UTF-8') . '"/>';
0435 }
0436 return $xsl;
0437 }
0438 }
0439
0440 /*
0441 * @package s9e\TextFormatter
0442 * @copyright Copyright (c) 2010-2016 The s9e Authors
0443 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
0444 */
0445 namespace s9e\TextFormatter\Configurator\Helpers;
0446 class CharacterClassBuilder
0447 {
0448 protected $chars;
0449 public $delimiter = '/';
0450 protected $ranges;
0451 public function fromList(array $chars)
0452 {
0453 $this->chars = $chars;
0454 $this->unescapeLiterals();
0455 \sort($this->chars);
0456 $this->storeRanges();
0457 $this->reorderDash();
0458 $this->fixCaret();
0459 $this->escapeSpecialChars();
0460 return $this->buildCharacterClass();
0461 }
0462 protected function buildCharacterClass()
0463 {
0464 $str = '[';
0465 foreach ($this->ranges as $_b7914274)
0466 {
0467 list($start, $end) = $_b7914274;
0468 if ($end > $start + 2)
0469 $str .= $this->chars[$start] . '-' . $this->chars[$end];
0470 else
0471 $str .= \implode('', \array_slice($this->chars, $start, $end + 1 - $start));
0472 }
0473 $str .= ']';
0474 return $str;
0475 }
0476 protected function escapeSpecialChars()
0477 {
0478 $specialChars = array('\\', ']', $this->delimiter);
0479 foreach (\array_intersect($this->chars, $specialChars) as $k => $v)
0480 $this->chars[$k] = '\\' . $v;
0481 }
0482 protected function fixCaret()
0483 {
0484 $k = \array_search('^', $this->chars, \true);
0485 if ($this->ranges[0][0] !== $k)
0486 return;
0487 if (isset($this->ranges[1]))
0488 {
0489 $range = $this->ranges[0];
0490 $this->ranges[0] = $this->ranges[1];
0491 $this->ranges[1] = $range;
0492 }
0493 else
0494 $this->chars[$k] = '\\^';
0495 }
0496 protected function reorderDash()
0497 {
0498 $dashIndex = \array_search('-', $this->chars, \true);
0499 if ($dashIndex === \false)
0500 return;
0501 $k = \array_search(array($dashIndex, $dashIndex), $this->ranges, \true);
0502 if ($k > 0)
0503 {
0504 unset($this->ranges[$k]);
0505 \array_unshift($this->ranges, array($dashIndex, $dashIndex));
0506 }
0507 $commaIndex = \array_search(',', $this->chars);
0508 $range = array($commaIndex, $dashIndex);
0509 $k = \array_search($range, $this->ranges, \true);
0510 if ($k !== \false)
0511 {
0512 $this->ranges[$k] = array($commaIndex, $commaIndex);
0513 \array_unshift($this->ranges, array($dashIndex, $dashIndex));
0514 }
0515 }
0516 protected function storeRanges()
0517 {
0518 $values = array();
0519 foreach ($this->chars as $char)
0520 if (\strlen($char) === 1)
0521 $values[] = \ord($char);
0522 else
0523 $values[] = \false;
0524 $i = \count($values) - 1;
0525 $ranges = array();
0526 while ($i >= 0)
0527 {
0528 $start = $i;
0529 $end = $i;
0530 while ($start > 0 && $values[$start - 1] === $values[$end] - ($end + 1 - $start))
0531 --$start;
0532 $ranges[] = array($start, $end);
0533 $i = $start - 1;
0534 }
0535 $this->ranges = \array_reverse($ranges);
0536 }
0537 protected function unescapeLiterals()
0538 {
0539 foreach ($this->chars as $k => $char)
0540 if ($char[0] === '\\' && \preg_match('(^\\\\[^a-z]$)Di', $char))
0541 $this->chars[$k] = \substr($char, 1);
0542 }
0543 }
0544
0545 /*
0546 * @package s9e\TextFormatter
0547 * @copyright Copyright (c) 2010-2016 The s9e Authors
0548 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
0549 */
0550 namespace s9e\TextFormatter\Configurator\Helpers;
0551 use RuntimeException;
0552 use Traversable;
0553 use s9e\TextFormatter\Configurator\ConfigProvider;
0554 use s9e\TextFormatter\Configurator\FilterableConfigValue;
0555 abstract class ConfigHelper
0556 {
0557 public static function filterConfig(array $config, $target = 'PHP')
0558 {
0559 $filteredConfig = array();
0560 foreach ($config as $name => $value)
0561 {
0562 if ($value instanceof FilterableConfigValue)
0563 {
0564 $value = $value->filterConfig($target);
0565 if (!isset($value))
0566 continue;
0567 }
0568 if (\is_array($value))
0569 $value = self::filterConfig($value, $target);
0570 $filteredConfig[$name] = $value;
0571 }
0572 return $filteredConfig;
0573 }
0574 public static function generateQuickMatchFromList(array $strings)
0575 {
0576 foreach ($strings as $string)
0577 {
0578 $stringLen = \strlen($string);
0579 $substrings = array();
0580 for ($len = $stringLen; $len; --$len)
0581 {
0582 $pos = $stringLen - $len;
0583 do
0584 {
0585 $substrings[\substr($string, $pos, $len)] = 1;
0586 }
0587 while (--$pos >= 0);
0588 }
0589 if (isset($goodStrings))
0590 {
0591 $goodStrings = \array_intersect_key($goodStrings, $substrings);
0592 if (empty($goodStrings))
0593 break;
0594 }
0595 else
0596 $goodStrings = $substrings;
0597 }
0598 if (empty($goodStrings))
0599 return \false;
0600 return \strval(\key($goodStrings));
0601 }
0602 public static function optimizeArray(array &$config, array &$cache = array())
0603 {
0604 foreach ($config as $k => &$v)
0605 {
0606 if (!\is_array($v))
0607 continue;
0608 self::optimizeArray($v, $cache);
0609 $cacheKey = \serialize($v);
0610 if (!isset($cache[$cacheKey]))
0611 $cache[$cacheKey] = $v;
0612 $config[$k] =& $cache[$cacheKey];
0613 }
0614 unset($v);
0615 }
0616 public static function toArray($value, $keepEmpty = \false, $keepNull = \false)
0617 {
0618 $array = array();
0619 foreach ($value as $k => $v)
0620 {
0621 if ($v instanceof ConfigProvider)
0622 $v = $v->asConfig();
0623 elseif ($v instanceof Traversable || \is_array($v))
0624 $v = self::toArray($v, $keepEmpty, $keepNull);
0625 elseif (\is_scalar($v) || \is_null($v))
0626 ;
0627 else
0628 {
0629 $type = (\is_object($v))
0630 ? 'an instance of ' . \get_class($v)
0631 : 'a ' . \gettype($v);
0632 throw new RuntimeException('Cannot convert ' . $type . ' to array');
0633 }
0634 if (!isset($v) && !$keepNull)
0635 continue;
0636 if (!$keepEmpty && $v === array())
0637 continue;
0638 $array[$k] = $v;
0639 }
0640 return $array;
0641 }
0642 }
0643
0644 /*
0645 * @package s9e\TextFormatter
0646 * @copyright Copyright (c) 2010-2016 The s9e Authors
0647 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
0648 */
0649 namespace s9e\TextFormatter\Configurator\Helpers;
0650 use RuntimeException;
0651 abstract class RegexpBuilder
0652 {
0653 protected static $characterClassBuilder;
0654 public static function fromList(array $words, array $options = array())
0655 {
0656 if (empty($words))
0657 return '';
0658 $options += array(
0659 'delimiter' => '/',
0660 'caseInsensitive' => \false,
0661 'specialChars' => array(),
0662 'unicode' => \true,
0663 'useLookahead' => \false
0664 );
0665 if ($options['caseInsensitive'])
0666 {
0667 foreach ($words as &$word)
0668 $word = \strtr(
0669 $word,
0670 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
0671 'abcdefghijklmnopqrstuvwxyz'
0672 );
0673 unset($word);
0674 }
0675 $words = \array_unique($words);
0676 \sort($words);
0677 $initials = array();
0678 $esc = $options['specialChars'];
0679 $esc += array($options['delimiter'] => '\\' . $options['delimiter']);
0680 $esc += array(
0681 '!' => '!',
0682 '-' => '-',
0683 ':' => ':',
0684 '<' => '<',
0685 '=' => '=',
0686 '>' => '>',
0687 '}' => '}'
0688 );
0689 $splitWords = array();
0690 foreach ($words as $word)
0691 {
0692 $regexp = ($options['unicode']) ? '(.)us' : '(.)s';
0693 if (\preg_match_all($regexp, $word, $matches) === \false || ($options['unicode'] && !\preg_match('/^(?:[[:ascii:]]|[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3})*$/D', $word)))
0694 throw new RuntimeException("Invalid UTF-8 string '" . $word . "'");
0695 $splitWord = array();
0696 foreach ($matches[0] as $pos => $c)
0697 {
0698 if (!isset($esc[$c]))
0699 $esc[$c] = \preg_quote($c);
0700 if ($pos === 0)
0701 $initials[] = $esc[$c];
0702 $splitWord[] = $esc[$c];
0703 }
0704 $splitWords[] = $splitWord;
0705 }
0706 self::$characterClassBuilder = new CharacterClassBuilder;
0707 self::$characterClassBuilder->delimiter = $options['delimiter'];
0708 $regexp = self::assemble(array(self::mergeChains($splitWords)));
0709 if ($options['useLookahead']
0710 && \count($initials) > 1
0711 && $regexp[0] !== '[')
0712 {
0713 $useLookahead = \true;
0714 foreach ($initials as $initial)
0715 if (!self::canBeUsedInCharacterClass($initial))
0716 {
0717 $useLookahead = \false;
0718 break;
0719 }
0720 if ($useLookahead)
0721 $regexp = '(?=' . self::generateCharacterClass($initials) . ')' . $regexp;
0722 }
0723 return $regexp;
0724 }
0725 protected static function mergeChains(array $chains)
0726 {
0727 if (!isset($chains[1]))
0728 return $chains[0];
0729 $mergedChain = self::removeLongestCommonPrefix($chains);
0730 if (!isset($chains[0][0])
0731 && !\array_filter($chains))
0732 return $mergedChain;
0733 $suffix = self::removeLongestCommonSuffix($chains);
0734 if (isset($chains[1]))
0735 {
0736 self::optimizeDotChains($chains);
0737 self::optimizeCatchallChains($chains);
0738 }
0739 $endOfChain = \false;
0740 $remerge = \false;
0741 $groups = array();
0742 foreach ($chains as $chain)
0743 {
0744 if (!isset($chain[0]))
0745 {
0746 $endOfChain = \true;
0747 continue;
0748 }
0749 $head = $chain[0];
0750 if (isset($groups[$head]))
0751 $remerge = \true;
0752 $groups[$head][] = $chain;
0753 }
0754 $characterClass = array();
0755 foreach ($groups as $head => $groupChains)
0756 {
0757 $head = (string) $head;
0758 if ($groupChains === array(array($head))
0759 && self::canBeUsedInCharacterClass($head))
0760 $characterClass[$head] = $head;
0761 }
0762 \sort($characterClass);
0763 if (isset($characterClass[1]))
0764 {
0765 foreach ($characterClass as $char)
0766 unset($groups[$char]);
0767 $head = self::generateCharacterClass($characterClass);
0768 $groups[$head][] = array($head);
0769 $groups = array($head => $groups[$head])
0770 + $groups;
0771 }
0772 if ($remerge)
0773 {
0774 $mergedChains = array();
0775 foreach ($groups as $head => $groupChains)
0776 $mergedChains[] = self::mergeChains($groupChains);
0777 self::mergeTails($mergedChains);
0778 $regexp = \implode('', self::mergeChains($mergedChains));
0779 if ($endOfChain)
0780 $regexp = self::makeRegexpOptional($regexp);
0781 $mergedChain[] = $regexp;
0782 }
0783 else
0784 {
0785 self::mergeTails($chains);
0786 $mergedChain[] = self::assemble($chains);
0787 }
0788 foreach ($suffix as $atom)
0789 $mergedChain[] = $atom;
0790 return $mergedChain;
0791 }
0792 protected static function mergeTails(array &$chains)
0793 {
0794 self::mergeTailsCC($chains);
0795 self::mergeTailsAltern($chains);
0796 $chains = \array_values($chains);
0797 }
0798 protected static function mergeTailsCC(array &$chains)
0799 {
0800 $groups = array();
0801 foreach ($chains as $k => $chain)
0802 if (isset($chain[1])
0803 && !isset($chain[2])
0804 && self::canBeUsedInCharacterClass($chain[0]))
0805 $groups[$chain[1]][$k] = $chain;
0806 foreach ($groups as $groupChains)
0807 {
0808 if (\count($groupChains) < 2)
0809 continue;
0810 $chains = \array_diff_key($chains, $groupChains);
0811 $chains[] = self::mergeChains(\array_values($groupChains));
0812 }
0813 }
0814 protected static function mergeTailsAltern(array &$chains)
0815 {
0816 $groups = array();
0817 foreach ($chains as $k => $chain)
0818 if (!empty($chain))
0819 {
0820 $tail = \array_slice($chain, -1);
0821 $groups[$tail[0]][$k] = $chain;
0822 }
0823 foreach ($groups as $tail => $groupChains)
0824 {
0825 if (\count($groupChains) < 2)
0826 continue;
0827 $mergedChain = self::mergeChains(\array_values($groupChains));
0828 $oldLen = 0;
0829 foreach ($groupChains as $groupChain)
0830 $oldLen += \array_sum(\array_map('strlen', $groupChain));
0831 if ($oldLen <= \array_sum(\array_map('strlen', $mergedChain)))
0832 continue;
0833 $chains = \array_diff_key($chains, $groupChains);
0834 $chains[] = $mergedChain;
0835 }
0836 }
0837 protected static function removeLongestCommonPrefix(array &$chains)
0838 {
0839 $pLen = 0;
0840 while (1)
0841 {
0842 $c = \null;
0843 foreach ($chains as $chain)
0844 {
0845 if (!isset($chain[$pLen]))
0846 break 2;
0847 if (!isset($c))
0848 {
0849 $c = $chain[$pLen];
0850 continue;
0851 }
0852 if ($chain[$pLen] !== $c)
0853 break 2;
0854 }
0855 ++$pLen;
0856 }
0857 if (!$pLen)
0858 return array();
0859 $prefix = \array_slice($chains[0], 0, $pLen);
0860 foreach ($chains as &$chain)
0861 $chain = \array_slice($chain, $pLen);
0862 unset($chain);
0863 return $prefix;
0864 }
0865 protected static function removeLongestCommonSuffix(array &$chains)
0866 {
0867 $chainsLen = \array_map('count', $chains);
0868 $maxLen = \min($chainsLen);
0869 if (\max($chainsLen) === $maxLen)
0870 --$maxLen;
0871 $sLen = 0;
0872 while ($sLen < $maxLen)
0873 {
0874 $c = \null;
0875 foreach ($chains as $k => $chain)
0876 {
0877 $pos = $chainsLen[$k] - ($sLen + 1);
0878 if (!isset($c))
0879 {
0880 $c = $chain[$pos];
0881 continue;
0882 }
0883 if ($chain[$pos] !== $c)
0884 break 2;
0885 }
0886 ++$sLen;
0887 }
0888 if (!$sLen)
0889 return array();
0890 $suffix = \array_slice($chains[0], -$sLen);
0891 foreach ($chains as &$chain)
0892 $chain = \array_slice($chain, 0, -$sLen);
0893 unset($chain);
0894 return $suffix;
0895 }
0896 protected static function assemble(array $chains)
0897 {
0898 $endOfChain = \false;
0899 $regexps = array();
0900 $characterClass = array();
0901 foreach ($chains as $chain)
0902 {
0903 if (empty($chain))
0904 {
0905 $endOfChain = \true;
0906 continue;
0907 }
0908 if (!isset($chain[1])
0909 && self::canBeUsedInCharacterClass($chain[0]))
0910 $characterClass[$chain[0]] = $chain[0];
0911 else
0912 $regexps[] = \implode('', $chain);
0913 }
0914 if (!empty($characterClass))
0915 {
0916 \sort($characterClass);
0917 $regexp = (isset($characterClass[1]))
0918 ? self::generateCharacterClass($characterClass)
0919 : $characterClass[0];
0920 \array_unshift($regexps, $regexp);
0921 }
0922 if (empty($regexps))
0923 return '';
0924 if (isset($regexps[1]))
0925 {
0926 $regexp = \implode('|', $regexps);
0927 $regexp = ((self::canUseAtomicGrouping($regexp)) ? '(?>' : '(?:') . $regexp . ')';
0928 }
0929 else
0930 $regexp = $regexps[0];
0931 if ($endOfChain)
0932 $regexp = self::makeRegexpOptional($regexp);
0933 return $regexp;
0934 }
0935 protected static function makeRegexpOptional($regexp)
0936 {
0937 if (\preg_match('#^\\.\\+\\??$#', $regexp))
0938 return \str_replace('+', '*', $regexp);
0939 if (\preg_match('#^(\\\\?.)((?:\\1\\?)+)$#Du', $regexp, $m))
0940 return $m[1] . '?' . $m[2];
0941 if (\preg_match('#^(?:[$^]|\\\\[bBAZzGQEK])$#', $regexp))
0942 return '';
0943 if (\preg_match('#^\\\\?.$#Dus', $regexp))
0944 $isAtomic = \true;
0945 elseif (\preg_match('#^[^[(].#s', $regexp))
0946 $isAtomic = \false;
0947 else
0948 {
0949 $def = RegexpParser::parse('#' . $regexp . '#');
0950 $tokens = $def['tokens'];
0951 switch (\count($tokens))
0952 {
0953 case 1:
0954 $startPos = $tokens[0]['pos'];
0955 $len = $tokens[0]['len'];
0956 $isAtomic = (bool) ($startPos === 0 && $len === \strlen($regexp));
0957 if ($isAtomic && $tokens[0]['type'] === 'characterClass')
0958 {
0959 $regexp = \rtrim($regexp, '+*?');
0960 if (!empty($tokens[0]['quantifiers']) && $tokens[0]['quantifiers'] !== '?')
0961 $regexp .= '*';
0962 }
0963 break;
0964 case 2:
0965 if ($tokens[0]['type'] === 'nonCapturingSubpatternStart'
0966 && $tokens[1]['type'] === 'nonCapturingSubpatternEnd')
0967 {
0968 $startPos = $tokens[0]['pos'];
0969 $len = $tokens[1]['pos'] + $tokens[1]['len'];
0970 $isAtomic = (bool) ($startPos === 0 && $len === \strlen($regexp));
0971 break;
0972 }
0973 default:
0974 $isAtomic = \false;
0975 }
0976 }
0977 if (!$isAtomic)
0978 $regexp = ((self::canUseAtomicGrouping($regexp)) ? '(?>' : '(?:') . $regexp . ')';
0979 $regexp .= '?';
0980 return $regexp;
0981 }
0982 protected static function generateCharacterClass(array $chars)
0983 {
0984 return self::$characterClassBuilder->fromList($chars);
0985 }
0986 protected static function canBeUsedInCharacterClass($char)
0987 {
0988 if (\preg_match('#^\\\\[aefnrtdDhHsSvVwW]$#D', $char))
0989 return \true;
0990 if (\preg_match('#^\\\\[^A-Za-z0-9]$#Dus', $char))
0991 return \true;
0992 if (\preg_match('#..#Dus', $char))
0993 return \false;
0994 if (\preg_quote($char) !== $char
0995 && !\preg_match('#^[-!:<=>}]$#D', $char))
0996 return \false;
0997 return \true;
0998 }
0999 protected static function optimizeDotChains(array &$chains)
1000 {
1001 $validAtoms = array(
1002 '\\d' => 1, '\\D' => 1, '\\h' => 1, '\\H' => 1,
1003 '\\s' => 1, '\\S' => 1, '\\v' => 1, '\\V' => 1,
1004 '\\w' => 1, '\\W' => 1,
1005 '\\^' => 1, '\\$' => 1, '\\.' => 1, '\\?' => 1,
1006 '\\[' => 1, '\\]' => 1, '\\(' => 1, '\\)' => 1,
1007 '\\+' => 1, '\\*' => 1, '\\\\' => 1
1008 );
1009 do
1010 {
1011 $hasMoreDots = \false;
1012 foreach ($chains as $k1 => $dotChain)
1013 {
1014 $dotKeys = \array_keys($dotChain, '.?', \true);
1015 if (!empty($dotKeys))
1016 {
1017 $dotChain[$dotKeys[0]] = '.';
1018 $chains[$k1] = $dotChain;
1019 \array_splice($dotChain, $dotKeys[0], 1);
1020 $chains[] = $dotChain;
1021 if (isset($dotKeys[1]))
1022 $hasMoreDots = \true;
1023 }
1024 }
1025 }
1026 while ($hasMoreDots);
1027 foreach ($chains as $k1 => $dotChain)
1028 {
1029 $dotKeys = \array_keys($dotChain, '.', \true);
1030 if (empty($dotKeys))
1031 continue;
1032 foreach ($chains as $k2 => $tmpChain)
1033 {
1034 if ($k2 === $k1)
1035 continue;
1036 foreach ($dotKeys as $dotKey)
1037 {
1038 if (!isset($tmpChain[$dotKey]))
1039 continue 2;
1040 if (!\preg_match('#^.$#Du', \preg_quote($tmpChain[$dotKey]))
1041 && !isset($validAtoms[$tmpChain[$dotKey]]))
1042 continue 2;
1043 $tmpChain[$dotKey] = '.';
1044 }
1045 if ($tmpChain === $dotChain)
1046 unset($chains[$k2]);
1047 }
1048 }
1049 }
1050 protected static function optimizeCatchallChains(array &$chains)
1051 {
1052 $precedence = array(
1053 '.*' => 3,
1054 '.*?' => 2,
1055 '.+' => 1,
1056 '.+?' => 0
1057 );
1058 $tails = array();
1059 foreach ($chains as $k => $chain)
1060 {
1061 if (!isset($chain[0]))
1062 continue;
1063 $head = $chain[0];
1064 if (!isset($precedence[$head]))
1065 continue;
1066 $tail = \implode('', \array_slice($chain, 1));
1067 if (!isset($tails[$tail])
1068 || $precedence[$head] > $tails[$tail]['precedence'])
1069 $tails[$tail] = array(
1070 'key' => $k,
1071 'precedence' => $precedence[$head]
1072 );
1073 }
1074 $catchallChains = array();
1075 foreach ($tails as $tail => $info)
1076 $catchallChains[$info['key']] = $chains[$info['key']];
1077 foreach ($catchallChains as $k1 => $catchallChain)
1078 {
1079 $headExpr = $catchallChain[0];
1080 $tailExpr = \false;
1081 $match = \array_slice($catchallChain, 1);
1082 if (isset($catchallChain[1])
1083 && isset($precedence[\end($catchallChain)]))
1084 $tailExpr = \array_pop($match);
1085 $matchCnt = \count($match);
1086 foreach ($chains as $k2 => $chain)
1087 {
1088 if ($k2 === $k1)
1089 continue;
1090 $start = 0;
1091 $end = \count($chain);
1092 if ($headExpr[1] === '+')
1093 {
1094 $found = \false;
1095 foreach ($chain as $start => $atom)
1096 if (self::matchesAtLeastOneCharacter($atom))
1097 {
1098 $found = \true;
1099 break;
1100 }
1101 if (!$found)
1102 continue;
1103 }
1104 if ($tailExpr === \false)
1105 $end = $start;
1106 else
1107 {
1108 if ($tailExpr[1] === '+')
1109 {
1110 $found = \false;
1111 while (--$end > $start)
1112 if (self::matchesAtLeastOneCharacter($chain[$end]))
1113 {
1114 $found = \true;
1115 break;
1116 }
1117 if (!$found)
1118 continue;
1119 }
1120 $end -= $matchCnt;
1121 }
1122 while ($start <= $end)
1123 {
1124 if (\array_slice($chain, $start, $matchCnt) === $match)
1125 {
1126 unset($chains[$k2]);
1127 break;
1128 }
1129 ++$start;
1130 }
1131 }
1132 }
1133 }
1134 protected static function matchesAtLeastOneCharacter($expr)
1135 {
1136 if (\preg_match('#^[$*?^]$#', $expr))
1137 return \false;
1138 if (\preg_match('#^.$#u', $expr))
1139 return \true;
1140 if (\preg_match('#^.\\+#u', $expr))
1141 return \true;
1142 if (\preg_match('#^\\\\[^bBAZzGQEK1-9](?![*?])#', $expr))
1143 return \true;
1144 return \false;
1145 }
1146 protected static function canUseAtomicGrouping($expr)
1147 {
1148 if (\preg_match('#(?<!\\\\)(?>\\\\\\\\)*\\.#', $expr))
1149 return \false;
1150 if (\preg_match('#(?<!\\\\)(?>\\\\\\\\)*[+*]#', $expr))
1151 return \false;
1152 if (\preg_match('#(?<!\\\\)(?>\\\\\\\\)*\\(?(?<!\\()\\?#', $expr))
1153 return \false;
1154 if (\preg_match('#(?<!\\\\)(?>\\\\\\\\)*\\\\[a-z0-9]#', $expr))
1155 return \false;
1156 return \true;
1157 }
1158 }
1159
1160 /*
1161 * @package s9e\TextFormatter
1162 * @copyright Copyright (c) 2010-2016 The s9e Authors
1163 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
1164 */
1165 namespace s9e\TextFormatter\Configurator\Helpers;
1166 use s9e\TextFormatter\Configurator\Collections\Ruleset;
1167 use s9e\TextFormatter\Configurator\Collections\TagCollection;
1168 abstract class RulesHelper
1169 {
1170 public static function getBitfields(TagCollection $tags, Ruleset $rootRules)
1171 {
1172 $rules = array('*root*' => \iterator_to_array($rootRules));
1173 foreach ($tags as $tagName => $tag)
1174 $rules[$tagName] = \iterator_to_array($tag->rules);
1175 $matrix = self::unrollRules($rules);
1176 self::pruneMatrix($matrix);
1177 $groupedTags = array();
1178 foreach (\array_keys($matrix) as $tagName)
1179 {
1180 if ($tagName === '*root*')
1181 continue;
1182 $k = '';
1183 foreach ($matrix as $tagMatrix)
1184 {
1185 $k .= $tagMatrix['allowedChildren'][$tagName];
1186 $k .= $tagMatrix['allowedDescendants'][$tagName];
1187 }
1188 $groupedTags[$k][] = $tagName;
1189 }
1190 $bitTag = array();
1191 $bitNumber = 0;
1192 $tagsConfig = array();
1193 foreach ($groupedTags as $tagNames)
1194 {
1195 foreach ($tagNames as $tagName)
1196 {
1197 $tagsConfig[$tagName]['bitNumber'] = $bitNumber;
1198 $bitTag[$bitNumber] = $tagName;
1199 }
1200 ++$bitNumber;
1201 }
1202 foreach ($matrix as $tagName => $tagMatrix)
1203 {
1204 $allowedChildren = '';
1205 $allowedDescendants = '';
1206 foreach ($bitTag as $targetName)
1207 {
1208 $allowedChildren .= $tagMatrix['allowedChildren'][$targetName];
1209 $allowedDescendants .= $tagMatrix['allowedDescendants'][$targetName];
1210 }
1211 $tagsConfig[$tagName]['allowed'] = self::pack($allowedChildren, $allowedDescendants);
1212 }
1213 $return = array(
1214 'root' => $tagsConfig['*root*'],
1215 'tags' => $tagsConfig
1216 );
1217 unset($return['tags']['*root*']);
1218 return $return;
1219 }
1220 protected static function initMatrix(array $rules)
1221 {
1222 $matrix = array();
1223 $tagNames = \array_keys($rules);
1224 foreach ($rules as $tagName => $tagRules)
1225 {
1226 if ($tagRules['defaultDescendantRule'] === 'allow')
1227 {
1228 $childValue = (int) ($tagRules['defaultChildRule'] === 'allow');
1229 $descendantValue = 1;
1230 }
1231 else
1232 {
1233 $childValue = 0;
1234 $descendantValue = 0;
1235 }
1236 $matrix[$tagName]['allowedChildren'] = \array_fill_keys($tagNames, $childValue);
1237 $matrix[$tagName]['allowedDescendants'] = \array_fill_keys($tagNames, $descendantValue);
1238 }
1239 return $matrix;
1240 }
1241 protected static function applyTargetedRule(array &$matrix, $rules, $ruleName, $key, $value)
1242 {
1243 foreach ($rules as $tagName => $tagRules)
1244 {
1245 if (!isset($tagRules[$ruleName]))
1246 continue;
1247 foreach ($tagRules[$ruleName] as $targetName)
1248 $matrix[$tagName][$key][$targetName] = $value;
1249 }
1250 }
1251 protected static function unrollRules(array $rules)
1252 {
1253 $matrix = self::initMatrix($rules);
1254 $tagNames = \array_keys($rules);
1255 foreach ($rules as $tagName => $tagRules)
1256 {
1257 if (!empty($tagRules['ignoreTags']))
1258 $rules[$tagName]['denyDescendant'] = $tagNames;
1259 if (!empty($tagRules['requireParent']))
1260 {
1261 $denyParents = \array_diff($tagNames, $tagRules['requireParent']);
1262 foreach ($denyParents as $parentName)
1263 $rules[$parentName]['denyChild'][] = $tagName;
1264 }
1265 }
1266 self::applyTargetedRule($matrix, $rules, 'allowChild', 'allowedChildren', 1);
1267 self::applyTargetedRule($matrix, $rules, 'allowDescendant', 'allowedChildren', 1);
1268 self::applyTargetedRule($matrix, $rules, 'allowDescendant', 'allowedDescendants', 1);
1269 self::applyTargetedRule($matrix, $rules, 'denyChild', 'allowedChildren', 0);
1270 self::applyTargetedRule($matrix, $rules, 'denyDescendant', 'allowedChildren', 0);
1271 self::applyTargetedRule($matrix, $rules, 'denyDescendant', 'allowedDescendants', 0);
1272 return $matrix;
1273 }
1274 protected static function pruneMatrix(array &$matrix)
1275 {
1276 $usableTags = array('*root*' => 1);
1277 $parentTags = $usableTags;
1278 do
1279 {
1280 $nextTags = array();
1281 foreach (\array_keys($parentTags) as $tagName)
1282 $nextTags += \array_filter($matrix[$tagName]['allowedChildren']);
1283 $parentTags = \array_diff_key($nextTags, $usableTags);
1284 $parentTags = \array_intersect_key($parentTags, $matrix);
1285 $usableTags += $parentTags;
1286 }
1287 while (!empty($parentTags));
1288 $matrix = \array_intersect_key($matrix, $usableTags);
1289 unset($usableTags['*root*']);
1290 foreach ($matrix as $tagName => &$tagMatrix)
1291 {
1292 $tagMatrix['allowedChildren']
1293 = \array_intersect_key($tagMatrix['allowedChildren'], $usableTags);
1294 $tagMatrix['allowedDescendants']
1295 = \array_intersect_key($tagMatrix['allowedDescendants'], $usableTags);
1296 }
1297 unset($tagMatrix);
1298 }
1299 protected static function pack($allowedChildren, $allowedDescendants)
1300 {
1301 $allowedChildren = \str_split($allowedChildren, 8);
1302 $allowedDescendants = \str_split($allowedDescendants, 8);
1303 $allowed = array();
1304 foreach (\array_keys($allowedChildren) as $k)
1305 $allowed[] = \bindec(\sprintf(
1306 '%1$08s%2$08s',
1307 \strrev($allowedDescendants[$k]),
1308 \strrev($allowedChildren[$k])
1309 ));
1310 return $allowed;
1311 }
1312 }
1313
1314 /*
1315 * @package s9e\TextFormatter
1316 * @copyright Copyright (c) 2010-2016 The s9e Authors
1317 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
1318 */
1319 namespace s9e\TextFormatter\Configurator\Helpers;
1320 use DOMDocument;
1321 use DOMElement;
1322 use DOMXPath;
1323 class TemplateForensics
1324 {
1325 protected $allowChildBitfield = "\0";
1326 protected $allowsChildElements = \true;
1327 protected $allowsText = \true;
1328 protected $contentBitfield = "\0";
1329 protected $denyDescendantBitfield = "\0";
1330 protected $dom;
1331 protected $hasElements = \false;
1332 protected $hasRootText = \false;
1333 protected $isBlock = \false;
1334 protected $isEmpty = \true;
1335 protected $isFormattingElement = \false;
1336 protected $isPassthrough = \false;
1337 protected $isTransparent = \false;
1338 protected $isVoid = \true;
1339 protected $leafNodes = array();
1340 protected $preservesNewLines = \false;
1341 protected $rootBitfields = array();
1342 protected $rootNodes = array();
1343 protected $xpath;
1344 public function __construct($template)
1345 {
1346 $this->dom = TemplateHelper::loadTemplate($template);
1347 $this->xpath = new DOMXPath($this->dom);
1348 $this->analyseRootNodes();
1349 $this->analyseBranches();
1350 $this->analyseContent();
1351 }
1352 public function allowsChild(self $child)
1353 {
1354 if (!$this->allowsDescendant($child))
1355 return \false;
1356 foreach ($child->rootBitfields as $rootBitfield)
1357 if (!self::match($rootBitfield, $this->allowChildBitfield))
1358 return \false;
1359 if (!$this->allowsText && $child->hasRootText)
1360 return \false;
1361 return \true;
1362 }
1363 public function allowsDescendant(self $descendant)
1364 {
1365 if (self::match($descendant->contentBitfield, $this->denyDescendantBitfield))
1366 return \false;
1367 if (!$this->allowsChildElements && $descendant->hasElements)
1368 return \false;
1369 return \true;
1370 }
1371 public function allowsChildElements()
1372 {
1373 return $this->allowsChildElements;
1374 }
1375 public function allowsText()
1376 {
1377 return $this->allowsText;
1378 }
1379 public function closesParent(self $parent)
1380 {
1381 foreach ($this->rootNodes as $rootName)
1382 {
1383 if (empty(self::$htmlElements[$rootName]['cp']))
1384 continue;
1385 foreach ($parent->leafNodes as $leafName)
1386 if (\in_array($leafName, self::$htmlElements[$rootName]['cp'], \true))
1387 return \true;
1388 }
1389 return \false;
1390 }
1391 public function getDOM()
1392 {
1393 return $this->dom;
1394 }
1395 public function isBlock()
1396 {
1397 return $this->isBlock;
1398 }
1399 public function isFormattingElement()
1400 {
1401 return $this->isFormattingElement;
1402 }
1403 public function isEmpty()
1404 {
1405 return $this->isEmpty;
1406 }
1407 public function isPassthrough()
1408 {
1409 return $this->isPassthrough;
1410 }
1411 public function isTransparent()
1412 {
1413 return $this->isTransparent;
1414 }
1415 public function isVoid()
1416 {
1417 return $this->isVoid;
1418 }
1419 public function preservesNewLines()
1420 {
1421 return $this->preservesNewLines;
1422 }
1423 protected function analyseContent()
1424 {
1425 $query = '//*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"]';
1426 foreach ($this->xpath->query($query) as $node)
1427 {
1428 $this->contentBitfield |= $this->getBitfield($node->localName, 'c', $node);
1429 $this->hasElements = \true;
1430 }
1431 $this->isPassthrough = (bool) $this->xpath->evaluate('count(//xsl:apply-templates)');
1432 }
1433 protected function analyseRootNodes()
1434 {
1435 $query = '//*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"][not(ancestor::*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"])]';
1436 foreach ($this->xpath->query($query) as $node)
1437 {
1438 $elName = $node->localName;
1439 $this->rootNodes[] = $elName;
1440 if (!isset(self::$htmlElements[$elName]))
1441 $elName = 'span';
1442 if ($this->elementIsBlock($elName, $node))
1443 $this->isBlock = \true;
1444 $this->rootBitfields[] = $this->getBitfield($elName, 'c', $node);
1445 }
1446 $predicate = '[not(ancestor::*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"])]';
1447 $predicate .= '[not(ancestor::xsl:attribute | ancestor::xsl:comment | ancestor::xsl:variable)]';
1448 $query = '//text()[normalize-space() != ""]' . $predicate
1449 . '|//xsl:text[normalize-space() != ""]' . $predicate
1450 . '|//xsl:value-of' . $predicate;
1451 if ($this->evaluate($query, $this->dom->documentElement))
1452 $this->hasRootText = \true;
1453 }
1454 protected function analyseBranches()
1455 {
1456 $branchBitfields = array();
1457 $isFormattingElement = \true;
1458 $this->isTransparent = \true;
1459 foreach ($this->getXSLElements('apply-templates') as $applyTemplates)
1460 {
1461 $nodes = $this->xpath->query(
1462 'ancestor::*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"]',
1463 $applyTemplates
1464 );
1465 $allowsChildElements = \true;
1466 $allowsText = \true;
1467 $branchBitfield = self::$htmlElements['div']['ac'];
1468 $isEmpty = \false;
1469 $isVoid = \false;
1470 $leafNode = \null;
1471 $preservesNewLines = \false;
1472 foreach ($nodes as $node)
1473 {
1474 $elName = $leafNode = $node->localName;
1475 if (!isset(self::$htmlElements[$elName]))
1476 $elName = 'span';
1477 if ($this->hasProperty($elName, 'v', $node))
1478 $isVoid = \true;
1479 if ($this->hasProperty($elName, 'e', $node))
1480 $isEmpty = \true;
1481 if (!$this->hasProperty($elName, 't', $node))
1482 {
1483 $branchBitfield = "\0";
1484 $this->isTransparent = \false;
1485 }
1486 if (!$this->hasProperty($elName, 'fe', $node)
1487 && !$this->isFormattingSpan($node))
1488 $isFormattingElement = \false;
1489 $allowsChildElements = !$this->hasProperty($elName, 'to', $node);
1490 $allowsText = !$this->hasProperty($elName, 'nt', $node);
1491 $branchBitfield |= $this->getBitfield($elName, 'ac', $node);
1492 $this->denyDescendantBitfield |= $this->getBitfield($elName, 'dd', $node);
1493 $style = '';
1494 if ($this->hasProperty($elName, 'pre', $node))
1495 $style .= 'white-space:pre;';
1496 if ($node->hasAttribute('style'))
1497 $style .= $node->getAttribute('style') . ';';
1498 $attributes = $this->xpath->query('.//xsl:attribute[@name="style"]', $node);
1499 foreach ($attributes as $attribute)
1500 $style .= $attribute->textContent;
1501 \preg_match_all(
1502 '/white-space\\s*:\\s*(no|pre)/i',
1503 \strtolower($style),
1504 $matches
1505 );
1506 foreach ($matches[1] as $match)
1507 $preservesNewLines = ($match === 'pre');
1508 }
1509 $branchBitfields[] = $branchBitfield;
1510 if (isset($leafNode))
1511 $this->leafNodes[] = $leafNode;
1512 if (!$allowsChildElements)
1513 $this->allowsChildElements = \false;
1514 if (!$allowsText)
1515 $this->allowsText = \false;
1516 if (!$isEmpty)
1517 $this->isEmpty = \false;
1518 if (!$isVoid)
1519 $this->isVoid = \false;
1520 if ($preservesNewLines)
1521 $this->preservesNewLines = \true;
1522 }
1523 if (empty($branchBitfields))
1524 {
1525 $this->allowsChildElements = \false;
1526 $this->isTransparent = \false;
1527 }
1528 else
1529 {
1530 $this->allowChildBitfield = $branchBitfields[0];
1531 foreach ($branchBitfields as $branchBitfield)
1532 $this->allowChildBitfield &= $branchBitfield;
1533 if (!empty($this->leafNodes))
1534 $this->isFormattingElement = $isFormattingElement;
1535 }
1536 }
1537 protected function elementIsBlock($elName, DOMElement $node)
1538 {
1539 $style = $this->getStyle($node);
1540 if (\preg_match('(\\bdisplay\\s*:\\s*block)i', $style))
1541 return \true;
1542 if (\preg_match('(\\bdisplay\\s*:\\s*inline)i', $style))
1543 return \false;
1544 return $this->hasProperty($elName, 'b', $node);
1545 }
1546 protected function evaluate($query, DOMElement $node)
1547 {
1548 return $this->xpath->evaluate('boolean(' . $query . ')', $node);
1549 }
1550 protected function getStyle(DOMElement $node)
1551 {
1552 $style = $node->getAttribute('style');
1553 $xpath = new DOMXPath($node->ownerDocument);
1554 $query = 'xsl:attribute[@name="style"]';
1555 foreach ($xpath->query($query, $node) as $attribute)
1556 $style .= ';' . $attribute->textContent;
1557 return $style;
1558 }
1559 protected function getXSLElements($elName)
1560 {
1561 return $this->dom->getElementsByTagNameNS('http://www.w3.org/1999/XSL/Transform', $elName);
1562 }
1563 protected function isFormattingSpan(DOMElement $node)
1564 {
1565 if ($node->nodeName !== 'span')
1566 return \false;
1567 if ($node->getAttribute('class') === ''
1568 && $node->getAttribute('style') === '')
1569 return \false;
1570 foreach ($node->attributes as $attrName => $attribute)
1571 if ($attrName !== 'class' && $attrName !== 'style')
1572 return \false;
1573 return \true;
1574 }
1575 protected static $htmlElements = array(
1576 'a'=>array('c'=>"\17\0\0\0\0\1",'c3'=>'@href','ac'=>"\0",'dd'=>"\10\0\0\0\0\1",'t'=>1,'fe'=>1),
1577 'abbr'=>array('c'=>"\7",'ac'=>"\4"),
1578 'address'=>array('c'=>"\3\40",'ac'=>"\1",'dd'=>"\0\45",'b'=>1,'cp'=>array('p')),
1579 'article'=>array('c'=>"\3\4",'ac'=>"\1",'b'=>1,'cp'=>array('p')),
1580 'aside'=>array('c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>array('p')),
1581 'audio'=>array('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),
1582 'b'=>array('c'=>"\7",'ac'=>"\4",'fe'=>1),
1583 'base'=>array('c'=>"\20",'nt'=>1,'e'=>1,'v'=>1,'b'=>1),
1584 'bdi'=>array('c'=>"\7",'ac'=>"\4"),
1585 'bdo'=>array('c'=>"\7",'ac'=>"\4"),
1586 'blockquote'=>array('c'=>"\203",'ac'=>"\1",'b'=>1,'cp'=>array('p')),
1587 'body'=>array('c'=>"\200\0\4",'ac'=>"\1",'b'=>1),
1588 'br'=>array('c'=>"\5",'nt'=>1,'e'=>1,'v'=>1),
1589 'button'=>array('c'=>"\117",'ac'=>"\4",'dd'=>"\10"),
1590 'canvas'=>array('c'=>"\47",'ac'=>"\0",'t'=>1),
1591 'caption'=>array('c'=>"\0\2",'ac'=>"\1",'dd'=>"\0\0\0\200",'b'=>1),
1592 'cite'=>array('c'=>"\7",'ac'=>"\4"),
1593 'code'=>array('c'=>"\7",'ac'=>"\4",'fe'=>1),
1594 'col'=>array('c'=>"\0\0\20",'nt'=>1,'e'=>1,'v'=>1,'b'=>1),
1595 'colgroup'=>array('c'=>"\0\2",'ac'=>"\0\0\20",'ac20'=>'not(@span)','nt'=>1,'e'=>1,'e0'=>'@span','b'=>1),
1596 'data'=>array('c'=>"\7",'ac'=>"\4"),
1597 'datalist'=>array('c'=>"\5",'ac'=>"\4\200\0\10"),
1598 'dd'=>array('c'=>"\0\0\200",'ac'=>"\1",'b'=>1,'cp'=>array('dd','dt')),
1599 'del'=>array('c'=>"\5",'ac'=>"\0",'t'=>1),
1600 'details'=>array('c'=>"\213",'ac'=>"\1\0\0\2",'b'=>1,'cp'=>array('p')),
1601 'dfn'=>array('c'=>"\7\0\0\0\40",'ac'=>"\4",'dd'=>"\0\0\0\0\40"),
1602 'div'=>array('c'=>"\3",'ac'=>"\1",'b'=>1,'cp'=>array('p')),
1603 'dl'=>array('c'=>"\3",'c1'=>'dt and dd','ac'=>"\0\200\200",'nt'=>1,'b'=>1,'cp'=>array('p')),
1604 'dt'=>array('c'=>"\0\0\200",'ac'=>"\1",'dd'=>"\0\5\0\40",'b'=>1,'cp'=>array('dd','dt')),
1605 'em'=>array('c'=>"\7",'ac'=>"\4",'fe'=>1),
1606 'embed'=>array('c'=>"\57",'nt'=>1,'e'=>1,'v'=>1),
1607 'fieldset'=>array('c'=>"\303",'ac'=>"\1\0\0\20",'b'=>1,'cp'=>array('p')),
1608 'figcaption'=>array('c'=>"\0\0\0\0\0\4",'ac'=>"\1",'b'=>1,'cp'=>array('p')),
1609 'figure'=>array('c'=>"\203",'ac'=>"\1\0\0\0\0\4",'b'=>1,'cp'=>array('p')),
1610 'footer'=>array('c'=>"\3\40",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>array('p')),
1611 'form'=>array('c'=>"\3\0\0\0\20",'ac'=>"\1",'dd'=>"\0\0\0\0\20",'b'=>1,'cp'=>array('p')),
1612 'h1'=>array('c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>array('p')),
1613 'h2'=>array('c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>array('p')),
1614 'h3'=>array('c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>array('p')),
1615 'h4'=>array('c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>array('p')),
1616 'h5'=>array('c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>array('p')),
1617 'h6'=>array('c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>array('p')),
1618 'head'=>array('c'=>"\0\0\4",'ac'=>"\20",'nt'=>1,'b'=>1),
1619 'header'=>array('c'=>"\3\40\0\40",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>array('p')),
1620 'hr'=>array('c'=>"\1\100",'nt'=>1,'e'=>1,'v'=>1,'b'=>1,'cp'=>array('p')),
1621 'html'=>array('c'=>"\0",'ac'=>"\0\0\4",'nt'=>1,'b'=>1),
1622 'i'=>array('c'=>"\7",'ac'=>"\4",'fe'=>1),
1623 'iframe'=>array('c'=>"\57",'ac'=>"\4"),
1624 'img'=>array('c'=>"\57\20\10",'c3'=>'@usemap','nt'=>1,'e'=>1,'v'=>1),
1625 'input'=>array('c'=>"\17\20",'c3'=>'@type!="hidden"','c12'=>'@type!="hidden" or @type="hidden"','c1'=>'@type!="hidden"','nt'=>1,'e'=>1,'v'=>1),
1626 'ins'=>array('c'=>"\7",'ac'=>"\0",'t'=>1),
1627 'kbd'=>array('c'=>"\7",'ac'=>"\4"),
1628 'keygen'=>array('c'=>"\117",'nt'=>1,'e'=>1,'v'=>1),
1629 'label'=>array('c'=>"\17\20\0\0\4",'ac'=>"\4",'dd'=>"\0\0\1\0\4"),
1630 'legend'=>array('c'=>"\0\0\0\20",'ac'=>"\4",'b'=>1),
1631 'li'=>array('c'=>"\0\0\0\0\200",'ac'=>"\1",'b'=>1,'cp'=>array('li')),
1632 'link'=>array('c'=>"\20",'nt'=>1,'e'=>1,'v'=>1,'b'=>1),
1633 'main'=>array('c'=>"\3\0\0\0\10",'ac'=>"\1",'b'=>1,'cp'=>array('p')),
1634 'mark'=>array('c'=>"\7",'ac'=>"\4"),
1635 'media element'=>array('c'=>"\0\0\0\0\0\2",'nt'=>1,'b'=>1),
1636 'menu'=>array('c'=>"\1\100",'ac'=>"\0\300",'nt'=>1,'b'=>1,'cp'=>array('p')),
1637 'menuitem'=>array('c'=>"\0\100",'nt'=>1,'e'=>1,'v'=>1,'b'=>1),
1638 'meta'=>array('c'=>"\20",'nt'=>1,'e'=>1,'v'=>1,'b'=>1),
1639 'meter'=>array('c'=>"\7\0\1\0\2",'ac'=>"\4",'dd'=>"\0\0\0\0\2"),
1640 'nav'=>array('c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>array('p')),
1641 'noscript'=>array('c'=>"\25",'nt'=>1),
1642 'object'=>array('c'=>"\147",'ac'=>"\0\0\0\0\1",'t'=>1),
1643 'ol'=>array('c'=>"\3",'c1'=>'li','ac'=>"\0\200\0\0\200",'nt'=>1,'b'=>1,'cp'=>array('p')),
1644 'optgroup'=>array('c'=>"\0\0\2",'ac'=>"\0\200\0\10",'nt'=>1,'b'=>1,'cp'=>array('optgroup','option')),
1645 'option'=>array('c'=>"\0\0\2\10",'b'=>1,'cp'=>array('option')),
1646 'output'=>array('c'=>"\107",'ac'=>"\4"),
1647 'p'=>array('c'=>"\3",'ac'=>"\4",'b'=>1,'cp'=>array('p')),
1648 'param'=>array('c'=>"\0\0\0\0\1",'nt'=>1,'e'=>1,'v'=>1,'b'=>1),
1649 'picture'=>array('c'=>"\45",'ac'=>"\0\200\10",'nt'=>1),
1650 'pre'=>array('c'=>"\3",'ac'=>"\4",'pre'=>1,'b'=>1,'cp'=>array('p')),
1651 'progress'=>array('c'=>"\7\0\1\1",'ac'=>"\4",'dd'=>"\0\0\0\1"),
1652 'q'=>array('c'=>"\7",'ac'=>"\4"),
1653 'rb'=>array('c'=>"\0\10",'ac'=>"\4",'b'=>1),
1654 'rp'=>array('c'=>"\0\10\100",'ac'=>"\4",'b'=>1,'cp'=>array('rp','rt')),
1655 'rt'=>array('c'=>"\0\10\100",'ac'=>"\4",'b'=>1,'cp'=>array('rp','rt')),
1656 'rtc'=>array('c'=>"\0\10",'ac'=>"\4\0\100",'b'=>1),
1657 'ruby'=>array('c'=>"\7",'ac'=>"\4\10"),
1658 's'=>array('c'=>"\7",'ac'=>"\4",'fe'=>1),
1659 'samp'=>array('c'=>"\7",'ac'=>"\4"),
1660 'script'=>array('c'=>"\25\200",'to'=>1),
1661 'section'=>array('c'=>"\3\4",'ac'=>"\1",'b'=>1,'cp'=>array('p')),
1662 'select'=>array('c'=>"\117",'ac'=>"\0\200\2",'nt'=>1),
1663 'small'=>array('c'=>"\7",'ac'=>"\4",'fe'=>1),
1664 'source'=>array('c'=>"\0\0\10\4",'nt'=>1,'e'=>1,'v'=>1,'b'=>1),
1665 'span'=>array('c'=>"\7",'ac'=>"\4"),
1666 'strong'=>array('c'=>"\7",'ac'=>"\4",'fe'=>1),
1667 'style'=>array('c'=>"\20",'to'=>1,'b'=>1),
1668 'sub'=>array('c'=>"\7",'ac'=>"\4"),
1669 'summary'=>array('c'=>"\0\0\0\2",'ac'=>"\4\1",'b'=>1),
1670 'sup'=>array('c'=>"\7",'ac'=>"\4"),
1671 'table'=>array('c'=>"\3\0\0\200",'ac'=>"\0\202",'nt'=>1,'b'=>1,'cp'=>array('p')),
1672 'tbody'=>array('c'=>"\0\2",'ac'=>"\0\200\0\0\100",'nt'=>1,'b'=>1,'cp'=>array('tbody','td','tfoot','th','thead','tr')),
1673 'td'=>array('c'=>"\200\0\40",'ac'=>"\1",'b'=>1,'cp'=>array('td','th')),
1674 'template'=>array('c'=>"\25\200\20",'nt'=>1),
1675 'textarea'=>array('c'=>"\117",'pre'=>1,'to'=>1),
1676 'tfoot'=>array('c'=>"\0\2",'ac'=>"\0\200\0\0\100",'nt'=>1,'b'=>1,'cp'=>array('tbody','td','th','thead','tr')),
1677 'th'=>array('c'=>"\0\0\40",'ac'=>"\1",'dd'=>"\0\5\0\40",'b'=>1,'cp'=>array('td','th')),
1678 'thead'=>array('c'=>"\0\2",'ac'=>"\0\200\0\0\100",'nt'=>1,'b'=>1),
1679 'time'=>array('c'=>"\7",'ac'=>"\4",'ac2'=>'@datetime'),
1680 'title'=>array('c'=>"\20",'to'=>1,'b'=>1),
1681 'tr'=>array('c'=>"\0\2\0\0\100",'ac'=>"\0\200\40",'nt'=>1,'b'=>1,'cp'=>array('td','th','tr')),
1682 'track'=>array('c'=>"\0\0\0\100",'nt'=>1,'e'=>1,'v'=>1,'b'=>1),
1683 'u'=>array('c'=>"\7",'ac'=>"\4",'fe'=>1),
1684 'ul'=>array('c'=>"\3",'c1'=>'li','ac'=>"\0\200\0\0\200",'nt'=>1,'b'=>1,'cp'=>array('p')),
1685 'var'=>array('c'=>"\7",'ac'=>"\4"),
1686 'video'=>array('c'=>"\57",'c3'=>'@controls','ac'=>"\0\0\0\104",'ac26'=>'not(@src)','dd'=>"\0\0\0\0\0\2",'dd41'=>'@src','t'=>1),
1687 'wbr'=>array('c'=>"\5",'nt'=>1,'e'=>1,'v'=>1)
1688 );
1689 protected function getBitfield($elName, $k, DOMElement $node)
1690 {
1691 if (!isset(self::$htmlElements[$elName][$k]))
1692 return "\0";
1693 $bitfield = self::$htmlElements[$elName][$k];
1694 foreach (\str_split($bitfield, 1) as $byteNumber => $char)
1695 {
1696 $byteValue = \ord($char);
1697 for ($bitNumber = 0; $bitNumber < 8; ++$bitNumber)
1698 {
1699 $bitValue = 1 << $bitNumber;
1700 if (!($byteValue & $bitValue))
1701 continue;
1702 $n = $byteNumber * 8 + $bitNumber;
1703 if (isset(self::$htmlElements[$elName][$k . $n]))
1704 {
1705 $xpath = 'boolean(' . self::$htmlElements[$elName][$k . $n] . ')';
1706 if (!$this->evaluate($xpath, $node))
1707 {
1708 $byteValue ^= $bitValue;
1709 $bitfield[$byteNumber] = \chr($byteValue);
1710 }
1711 }
1712 }
1713 }
1714 return $bitfield;
1715 }
1716 protected function hasProperty($elName, $propName, DOMElement $node)
1717 {
1718 if (!empty(self::$htmlElements[$elName][$propName]))
1719 if (!isset(self::$htmlElements[$elName][$propName . '0'])
1720 || $this->evaluate(self::$htmlElements[$elName][$propName . '0'], $node))
1721 return \true;
1722 return \false;
1723 }
1724 protected static function match($bitfield1, $bitfield2)
1725 {
1726 return (\trim($bitfield1 & $bitfield2, "\0") !== '');
1727 }
1728 }
1729
1730 /*
1731 * @package s9e\TextFormatter
1732 * @copyright Copyright (c) 2010-2016 The s9e Authors
1733 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
1734 */
1735 namespace s9e\TextFormatter\Configurator\Helpers;
1736 use DOMAttr;
1737 use DOMCharacterData;
1738 use DOMDocument;
1739 use DOMElement;
1740 use DOMNode;
1741 use DOMProcessingInstruction;
1742 use DOMXPath;
1743 use RuntimeException;
1744 use s9e\TextFormatter\Configurator\Exceptions\InvalidXslException;
1745 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
1746 abstract class TemplateHelper
1747 {
1748 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
1749 public static function getAttributesByRegexp(DOMDocument $dom, $regexp)
1750 {
1751 $xpath = new DOMXPath($dom);
1752 $nodes = array();
1753 foreach ($xpath->query('//@*') as $attribute)
1754 if (\preg_match($regexp, $attribute->name))
1755 $nodes[] = $attribute;
1756 foreach ($xpath->query('//xsl:attribute') as $attribute)
1757 if (\preg_match($regexp, $attribute->getAttribute('name')))
1758 $nodes[] = $attribute;
1759 foreach ($xpath->query('//xsl:copy-of') as $node)
1760 {
1761 $expr = $node->getAttribute('select');
1762 if (\preg_match('/^@(\\w+)$/', $expr, $m)
1763 && \preg_match($regexp, $m[1]))
1764 $nodes[] = $node;
1765 }
1766 return $nodes;
1767 }
1768 public static function getCSSNodes(DOMDocument $dom)
1769 {
1770 $regexp = '/^style$/i';
1771 $nodes = \array_merge(
1772 self::getAttributesByRegexp($dom, $regexp),
1773 self::getElementsByRegexp($dom, '/^style$/i')
1774 );
1775 return $nodes;
1776 }
1777 public static function getElementsByRegexp(DOMDocument $dom, $regexp)
1778 {
1779 $xpath = new DOMXPath($dom);
1780 $nodes = array();
1781 foreach ($xpath->query('//*') as $element)
1782 if (\preg_match($regexp, $element->localName))
1783 $nodes[] = $element;
1784 foreach ($xpath->query('//xsl:element') as $element)
1785 if (\preg_match($regexp, $element->getAttribute('name')))
1786 $nodes[] = $element;
1787 foreach ($xpath->query('//xsl:copy-of') as $node)
1788 {
1789 $expr = $node->getAttribute('select');
1790 if (\preg_match('/^\\w+$/', $expr)
1791 && \preg_match($regexp, $expr))
1792 $nodes[] = $node;
1793 }
1794 return $nodes;
1795 }
1796 public static function getJSNodes(DOMDocument $dom)
1797 {
1798 $regexp = '/^(?>data-s9e-livepreview-postprocess$|on)/i';
1799 $nodes = \array_merge(
1800 self::getAttributesByRegexp($dom, $regexp),
1801 self::getElementsByRegexp($dom, '/^script$/i')
1802 );
1803 return $nodes;
1804 }
1805 public static function getMetaElementsRegexp(array $templates)
1806 {
1807 $exprs = array();
1808 $xsl = '<xsl:template xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' . \implode('', $templates) . '</xsl:template>';
1809 $dom = new DOMDocument;
1810 $dom->loadXML($xsl);
1811 $xpath = new DOMXPath($dom);
1812 $query = '//xsl:*/@*[contains("matchselectest", name())]';
1813 foreach ($xpath->query($query) as $attribute)
1814 $exprs[] = $attribute->value;
1815 $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*';
1816 foreach ($xpath->query($query) as $attribute)
1817 foreach (AVTHelper::parse($attribute->value) as $token)
1818 if ($token[0] === 'expression')
1819 $exprs[] = $token[1];
1820 $tagNames = array(
1821 'e' => \true,
1822 'i' => \true,
1823 's' => \true
1824 );
1825 foreach (\array_keys($tagNames) as $tagName)
1826 if (isset($templates[$tagName]) && $templates[$tagName] !== '')
1827 unset($tagNames[$tagName]);
1828 $regexp = '(\\b(?<![$@])(' . \implode('|', \array_keys($tagNames)) . ')(?!-)\\b)';
1829 \preg_match_all($regexp, \implode("\n", $exprs), $m);
1830 foreach ($m[0] as $tagName)
1831 unset($tagNames[$tagName]);
1832 if (empty($tagNames))
1833 return '((?!))';
1834 return '(<' . RegexpBuilder::fromList(\array_keys($tagNames)) . '>[^<]*</[^>]+>)';
1835 }
1836 public static function getObjectParamsByRegexp(DOMDocument $dom, $regexp)
1837 {
1838 $xpath = new DOMXPath($dom);
1839 $nodes = array();
1840 foreach (self::getAttributesByRegexp($dom, $regexp) as $attribute)
1841 if ($attribute->nodeType === \XML_ATTRIBUTE_NODE)
1842 {
1843 if (\strtolower($attribute->parentNode->localName) === 'embed')
1844 $nodes[] = $attribute;
1845 }
1846 elseif ($xpath->evaluate('ancestor::embed', $attribute))
1847 $nodes[] = $attribute;
1848 foreach ($dom->getElementsByTagName('object') as $object)
1849 foreach ($object->getElementsByTagName('param') as $param)
1850 if (\preg_match($regexp, $param->getAttribute('name')))
1851 $nodes[] = $param;
1852 return $nodes;
1853 }
1854 public static function getParametersFromXSL($xsl)
1855 {
1856 $paramNames = array();
1857 $xsl = '<xsl:stylesheet xmlns:xsl="' . self::XMLNS_XSL . '"><xsl:template>'
1858 . $xsl
1859 . '</xsl:template></xsl:stylesheet>';
1860 $dom = new DOMDocument;
1861 $dom->loadXML($xsl);
1862 $xpath = new DOMXPath($dom);
1863 $query = '//xsl:*/@match | //xsl:*/@select | //xsl:*/@test';
1864 foreach ($xpath->query($query) as $attribute)
1865 foreach (XPathHelper::getVariables($attribute->value) as $varName)
1866 {
1867 $varQuery = 'ancestor-or-self::*/preceding-sibling::xsl:variable[@name="' . $varName . '"]';
1868 if (!$xpath->query($varQuery, $attribute)->length)
1869 $paramNames[] = $varName;
1870 }
1871 $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{")]';
1872 foreach ($xpath->query($query) as $attribute)
1873 {
1874 $tokens = AVTHelper::parse($attribute->value);
1875 foreach ($tokens as $token)
1876 {
1877 if ($token[0] !== 'expression')
1878 continue;
1879 foreach (XPathHelper::getVariables($token[1]) as $varName)
1880 {
1881 $varQuery = 'ancestor-or-self::*/preceding-sibling::xsl:variable[@name="' . $varName . '"]';
1882 if (!$xpath->query($varQuery, $attribute)->length)
1883 $paramNames[] = $varName;
1884 }
1885 }
1886 }
1887 $paramNames = \array_unique($paramNames);
1888 \sort($paramNames);
1889 return $paramNames;
1890 }
1891 public static function getURLNodes(DOMDocument $dom)
1892 {
1893 $regexp = '/(?>^(?>action|background|c(?>ite|lassid|odebase)|data|formaction|href|icon|longdesc|manifest|p(?>luginspage|oster|rofile)|usemap)|src)$/i';
1894 $nodes = self::getAttributesByRegexp($dom, $regexp);
1895 foreach (self::getObjectParamsByRegexp($dom, '/^(?:dataurl|movie)$/i') as $param)
1896 {
1897 $node = $param->getAttributeNode('value');
1898 if ($node)
1899 $nodes[] = $node;
1900 }
1901 return $nodes;
1902 }
1903 public static function highlightNode(DOMNode $node, $prepend, $append)
1904 {
1905 $uniqid = \uniqid('_');
1906 if ($node instanceof DOMAttr)
1907 $node->value .= $uniqid;
1908 elseif ($node instanceof DOMElement)
1909 $node->setAttribute($uniqid, '');
1910 elseif ($node instanceof DOMCharacterData
1911 || $node instanceof DOMProcessingInstruction)
1912 $node->data .= $uniqid;
1913 $dom = $node->ownerDocument;
1914 $dom->formatOutput = \true;
1915 $docXml = self::innerXML($dom->documentElement);
1916 $docXml = \trim(\str_replace("\n ", "\n", $docXml));
1917 $nodeHtml = \htmlspecialchars(\trim($dom->saveXML($node)));
1918 $docHtml = \htmlspecialchars($docXml);
1919 $html = \str_replace($nodeHtml, $prepend . $nodeHtml . $append, $docHtml);
1920 if ($node instanceof DOMAttr)
1921 {
1922 $node->value = \substr($node->value, 0, -\strlen($uniqid));
1923 $html = \str_replace($uniqid, '', $html);
1924 }
1925 elseif ($node instanceof DOMElement)
1926 {
1927 $node->removeAttribute($uniqid);
1928 $html = \str_replace(' ' . $uniqid . '=""', '', $html);
1929 }
1930 elseif ($node instanceof DOMCharacterData
1931 || $node instanceof DOMProcessingInstruction)
1932 {
1933 $node->data .= $uniqid;
1934 $html = \str_replace($uniqid, '', $html);
1935 }
1936 return $html;
1937 }
1938 public static function loadTemplate($template)
1939 {
1940 $dom = self::loadTemplateAsXML($template);
1941 if ($dom)
1942 return $dom;
1943 $dom = self::loadTemplateAsXML(self::fixEntities($template));
1944 if ($dom)
1945 return $dom;
1946 if (\strpos($template, '<xsl:') !== \false)
1947 {
1948 $error = \libxml_get_last_error();
1949 throw new InvalidXslException($error->message);
1950 }
1951 return self::loadTemplateAsHTML($template);
1952 }
1953 public static function replaceHomogeneousTemplates(array &$templates, $minCount = 3)
1954 {
1955 $tagNames = array();
1956 $expr = 'name()';
1957 foreach ($templates as $tagName => $template)
1958 {
1959 $elName = \strtolower(\preg_replace('/^[^:]+:/', '', $tagName));
1960 if ($template === '<' . $elName . '><xsl:apply-templates/></' . $elName . '>')
1961 {
1962 $tagNames[] = $tagName;
1963 if (\strpos($tagName, ':') !== \false)
1964 $expr = 'local-name()';
1965 }
1966 }
1967 if (\count($tagNames) < $minCount)
1968 return;
1969 $chars = \preg_replace('/[^A-Z]+/', '', \count_chars(\implode('', $tagNames), 3));
1970 if (\is_string($chars) && $chars !== '')
1971 $expr = 'translate(' . $expr . ",'" . $chars . "','" . \strtolower($chars) . "')";
1972 $template = '<xsl:element name="{' . $expr . '}"><xsl:apply-templates/></xsl:element>';
1973 foreach ($tagNames as $tagName)
1974 $templates[$tagName] = $template;
1975 }
1976 public static function replaceTokens($template, $regexp, $fn)
1977 {
1978 if ($template === '')
1979 return $template;
1980 $dom = self::loadTemplate($template);
1981 $xpath = new DOMXPath($dom);
1982 foreach ($xpath->query('//@*') as $attribute)
1983 {
1984 $attrValue = \preg_replace_callback(
1985 $regexp,
1986 function ($m) use ($fn, $attribute)
1987 {
1988 $replacement = $fn($m, $attribute);
1989 if ($replacement[0] === 'expression')
1990 return '{' . $replacement[1] . '}';
1991 elseif ($replacement[0] === 'passthrough')
1992 return '{.}';
1993 else
1994 return $replacement[1];
1995 },
1996 $attribute->value
1997 );
1998 $attribute->value = \htmlspecialchars($attrValue, \ENT_COMPAT, 'UTF-8');
1999 }
2000 foreach ($xpath->query('//text()') as $node)
2001 {
2002 \preg_match_all(
2003 $regexp,
2004 $node->textContent,
2005 $matches,
2006 \PREG_SET_ORDER | \PREG_OFFSET_CAPTURE
2007 );
2008 if (empty($matches))
2009 continue;
2010 $parentNode = $node->parentNode;
2011 $lastPos = 0;
2012 foreach ($matches as $m)
2013 {
2014 $pos = $m[0][1];
2015 if ($pos > $lastPos)
2016 $parentNode->insertBefore(
2017 $dom->createTextNode(
2018 \substr($node->textContent, $lastPos, $pos - $lastPos)
2019 ),
2020 $node
2021 );
2022 $lastPos = $pos + \strlen($m[0][0]);
2023 $_m = array();
2024 foreach ($m as $capture)
2025 $_m[] = $capture[0];
2026 $replacement = $fn($_m, $node);
2027 if ($replacement[0] === 'expression')
2028 $parentNode
2029 ->insertBefore(
2030 $dom->createElementNS(self::XMLNS_XSL, 'xsl:value-of'),
2031 $node
2032 )
2033 ->setAttribute('select', $replacement[1]);
2034 elseif ($replacement[0] === 'passthrough')
2035 $parentNode->insertBefore(
2036 $dom->createElementNS(self::XMLNS_XSL, 'xsl:apply-templates'),
2037 $node
2038 );
2039 else
2040 $parentNode->insertBefore($dom->createTextNode($replacement[1]), $node);
2041 }
2042 $text = \substr($node->textContent, $lastPos);
2043 if ($text > '')
2044 $parentNode->insertBefore($dom->createTextNode($text), $node);
2045 $parentNode->removeChild($node);
2046 }
2047 return self::saveTemplate($dom);
2048 }
2049 public static function saveTemplate(DOMDocument $dom)
2050 {
2051 return self::innerXML($dom->documentElement);
2052 }
2053 protected static function fixEntities($template)
2054 {
2055 return \preg_replace_callback(
2056 '(&(?!quot;|amp;|apos;|lt;|gt;)\\w+;)',
2057 function ($m)
2058 {
2059 return \html_entity_decode($m[0], \ENT_NOQUOTES, 'UTF-8');
2060 },
2061 \preg_replace('(&(?![A-Za-z0-9]+;|#\\d+;|#x[A-Fa-f0-9]+;))', '&', $template)
2062 );
2063 }
2064 protected static function innerXML(DOMElement $element)
2065 {
2066 $xml = $element->ownerDocument->saveXML($element);
2067 $pos = 1 + \strpos($xml, '>');
2068 $len = \strrpos($xml, '<') - $pos;
2069 if ($len < 1)
2070 return '';
2071 $xml = \substr($xml, $pos, $len);
2072 return $xml;
2073 }
2074 protected static function loadTemplateAsHTML($template)
2075 {
2076 $dom = new DOMDocument;
2077 $html = '<?xml version="1.0" encoding="utf-8" ?><html><body><div>' . $template . '</div></body></html>';
2078 $useErrors = \libxml_use_internal_errors(\true);
2079 $dom->loadHTML($html);
2080 self::removeInvalidAttributes($dom);
2081 \libxml_use_internal_errors($useErrors);
2082 $xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . self::innerXML($dom->documentElement->firstChild->firstChild) . '</xsl:template>';
2083 $useErrors = \libxml_use_internal_errors(\true);
2084 $dom->loadXML($xml);
2085 \libxml_use_internal_errors($useErrors);
2086 return $dom;
2087 }
2088 protected static function loadTemplateAsXML($template)
2089 {
2090 $xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . $template . '</xsl:template>';
2091 $useErrors = \libxml_use_internal_errors(\true);
2092 $dom = new DOMDocument;
2093 $success = $dom->loadXML($xml);
2094 self::removeInvalidAttributes($dom);
2095 \libxml_use_internal_errors($useErrors);
2096 return ($success) ? $dom : \false;
2097 }
2098 protected static function removeInvalidAttributes(DOMDocument $dom)
2099 {
2100 $xpath = new DOMXPath($dom);
2101 foreach ($xpath->query('//@*') as $attribute)
2102 if (!\preg_match('(^(?:[-\\w]+:)?(?!\\d)[-\\w]+$)D', $attribute->nodeName))
2103 $attribute->parentNode->removeAttributeNode($attribute);
2104 }
2105 }
2106
2107 /*
2108 * @package s9e\TextFormatter
2109 * @copyright Copyright (c) 2010-2016 The s9e Authors
2110 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
2111 */
2112 namespace s9e\TextFormatter\Configurator\Helpers;
2113 use DOMDocument;
2114 use DOMElement;
2115 use DOMNode;
2116 use DOMXPath;
2117 use RuntimeException;
2118 class TemplateParser
2119 {
2120 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
2121 public static $voidRegexp = '/^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/Di';
2122 public static function parse($template)
2123 {
2124 $xsl = '<xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . $template . '</xsl:template>';
2125 $dom = new DOMDocument;
2126 $dom->loadXML($xsl);
2127 $ir = new DOMDocument;
2128 $ir->loadXML('<template/>');
2129 self::parseChildren($ir->documentElement, $dom->documentElement);
2130 self::normalize($ir);
2131 return $ir;
2132 }
2133 public static function parseEqualityExpr($expr)
2134 {
2135 $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)))';
2136 $regexp = '(^(?J)\\s*' . $eq . '\\s*(?:or\\s*(?&equality)\\s*)*$)';
2137 if (!\preg_match($regexp, $expr))
2138 return \false;
2139 \preg_match_all("((?J)$eq)", $expr, $matches, \PREG_SET_ORDER);
2140 $map = array();
2141 foreach ($matches as $m)
2142 {
2143 $key = $m['key'];
2144 if (!empty($m['concat']))
2145 {
2146 \preg_match_all('(\'[^\']*\'|"[^"]*")', $m['concat'], $strings);
2147 $value = '';
2148 foreach ($strings[0] as $string)
2149 $value .= \substr($string, 1, -1);
2150 }
2151 else
2152 {
2153 $value = $m['literal'];
2154 if ($value[0] === "'" || $value[0] === '"')
2155 $value = \substr($value, 1, -1);
2156 }
2157 $map[$key][] = $value;
2158 }
2159 return $map;
2160 }
2161 protected static function parseChildren(DOMElement $ir, DOMElement $parent)
2162 {
2163 foreach ($parent->childNodes as $child)
2164 {
2165 switch ($child->nodeType)
2166 {
2167 case \XML_COMMENT_NODE:
2168 break;
2169 case \XML_TEXT_NODE:
2170 if (\trim($child->textContent) !== '')
2171 self::appendOutput($ir, 'literal', $child->textContent);
2172 break;
2173 case \XML_ELEMENT_NODE:
2174 self::parseNode($ir, $child);
2175 break;
2176 default:
2177 throw new RuntimeException("Cannot parse node '" . $child->nodeName . "''");
2178 }
2179 }
2180 }
2181 protected static function parseNode(DOMElement $ir, DOMElement $node)
2182 {
2183 if ($node->namespaceURI === self::XMLNS_XSL)
2184 {
2185 $methodName = 'parseXsl' . \str_replace(' ', '', \ucwords(\str_replace('-', ' ', $node->localName)));
2186 if (!\method_exists(__CLASS__, $methodName))
2187 throw new RuntimeException("Element '" . $node->nodeName . "' is not supported");
2188 return self::$methodName($ir, $node);
2189 }
2190 if (!\is_null($node->namespaceURI))
2191 throw new RuntimeException("Namespaced element '" . $node->nodeName . "' is not supported");
2192 $element = self::appendElement($ir, 'element');
2193 $element->setAttribute('name', $node->localName);
2194 foreach ($node->attributes as $attribute)
2195 {
2196 $irAttribute = self::appendElement($element, 'attribute');
2197 $irAttribute->setAttribute('name', $attribute->name);
2198 self::appendOutput($irAttribute, 'avt', $attribute->value);
2199 }
2200 self::parseChildren($element, $node);
2201 }
2202 protected static function parseXslApplyTemplates(DOMElement $ir, DOMElement $node)
2203 {
2204 $applyTemplates = self::appendElement($ir, 'applyTemplates');
2205 if ($node->hasAttribute('select'))
2206 $applyTemplates->setAttribute(
2207 'select',
2208 $node->getAttribute('select')
2209 );
2210 }
2211 protected static function parseXslAttribute(DOMElement $ir, DOMElement $node)
2212 {
2213 $attrName = $node->getAttribute('name');
2214 if ($attrName !== '')
2215 {
2216 $attribute = self::appendElement($ir, 'attribute');
2217 $attribute->setAttribute('name', $attrName);
2218 self::parseChildren($attribute, $node);
2219 }
2220 }
2221 protected static function parseXslChoose(DOMElement $ir, DOMElement $node)
2222 {
2223 $switch = self::appendElement($ir, 'switch');
2224 foreach ($node->getElementsByTagNameNS(self::XMLNS_XSL, 'when') as $when)
2225 {
2226 if ($when->parentNode !== $node)
2227 continue;
2228 $case = self::appendElement($switch, 'case');
2229 $case->setAttribute('test', $when->getAttribute('test'));
2230 self::parseChildren($case, $when);
2231 }
2232 foreach ($node->getElementsByTagNameNS(self::XMLNS_XSL, 'otherwise') as $otherwise)
2233 {
2234 if ($otherwise->parentNode !== $node)
2235 continue;
2236 $case = self::appendElement($switch, 'case');
2237 self::parseChildren($case, $otherwise);
2238 break;
2239 }
2240 }
2241 protected static function parseXslComment(DOMElement $ir, DOMElement $node)
2242 {
2243 $comment = self::appendElement($ir, 'comment');
2244 self::parseChildren($comment, $node);
2245 }
2246 protected static function parseXslCopyOf(DOMElement $ir, DOMElement $node)
2247 {
2248 $expr = $node->getAttribute('select');
2249 if (\preg_match('#^@([-\\w]+)$#', $expr, $m))
2250 {
2251 $switch = self::appendElement($ir, 'switch');
2252 $case = self::appendElement($switch, 'case');
2253 $case->setAttribute('test', $expr);
2254 $attribute = self::appendElement($case, 'attribute');
2255 $attribute->setAttribute('name', $m[1]);
2256 self::appendOutput($attribute, 'xpath', $expr);
2257 return;
2258 }
2259 if ($expr === '@*')
2260 {
2261 self::appendElement($ir, 'copyOfAttributes');
2262 return;
2263 }
2264 throw new RuntimeException("Unsupported <xsl:copy-of/> expression '" . $expr . "'");
2265 }
2266 protected static function parseXslElement(DOMElement $ir, DOMElement $node)
2267 {
2268 $elName = $node->getAttribute('name');
2269 if ($elName !== '')
2270 {
2271 $element = self::appendElement($ir, 'element');
2272 $element->setAttribute('name', $elName);
2273 self::parseChildren($element, $node);
2274 }
2275 }
2276 protected static function parseXslIf(DOMElement $ir, DOMElement $node)
2277 {
2278 $switch = self::appendElement($ir, 'switch');
2279 $case = self::appendElement($switch, 'case');
2280 $case->setAttribute('test', $node->getAttribute('test'));
2281 self::parseChildren($case, $node);
2282 }
2283 protected static function parseXslText(DOMElement $ir, DOMElement $node)
2284 {
2285 self::appendOutput($ir, 'literal', $node->textContent);
2286 }
2287 protected static function parseXslValueOf(DOMElement $ir, DOMElement $node)
2288 {
2289 self::appendOutput($ir, 'xpath', $node->getAttribute('select'));
2290 }
2291 protected static function normalize(DOMDocument $ir)
2292 {
2293 self::addDefaultCase($ir);
2294 self::addElementIds($ir);
2295 self::addCloseTagElements($ir);
2296 self::markEmptyElements($ir);
2297 self::optimize($ir);
2298 self::markConditionalCloseTagElements($ir);
2299 self::setOutputContext($ir);
2300 self::markBranchTables($ir);
2301 }
2302 protected static function addDefaultCase(DOMDocument $ir)
2303 {
2304 $xpath = new DOMXPath($ir);
2305 foreach ($xpath->query('//switch[not(case[not(@test)])]') as $switch)
2306 self::appendElement($switch, 'case');
2307 }
2308 protected static function addElementIds(DOMDocument $ir)
2309 {
2310 $id = 0;
2311 foreach ($ir->getElementsByTagName('element') as $element)
2312 $element->setAttribute('id', ++$id);
2313 }
2314 protected static function addCloseTagElements(DOMDocument $ir)
2315 {
2316 $xpath = new DOMXPath($ir);
2317 $exprs = array(
2318 '//applyTemplates[not(ancestor::attribute)]',
2319 '//comment',
2320 '//element',
2321 '//output[not(ancestor::attribute)]'
2322 );
2323 foreach ($xpath->query(\implode('|', $exprs)) as $node)
2324 {
2325 $parentElementId = self::getParentElementId($node);
2326 if (isset($parentElementId))
2327 $node->parentNode
2328 ->insertBefore($ir->createElement('closeTag'), $node)
2329 ->setAttribute('id', $parentElementId);
2330 if ($node->nodeName === 'element')
2331 {
2332 $id = $node->getAttribute('id');
2333 self::appendElement($node, 'closeTag')->setAttribute('id', $id);
2334 }
2335 }
2336 }
2337 protected static function markConditionalCloseTagElements(DOMDocument $ir)
2338 {
2339 $xpath = new DOMXPath($ir);
2340 foreach ($ir->getElementsByTagName('closeTag') as $closeTag)
2341 {
2342 $id = $closeTag->getAttribute('id');
2343 $query = 'ancestor::switch/following-sibling::*/descendant-or-self::closeTag[@id = "' . $id . '"]';
2344 foreach ($xpath->query($query, $closeTag) as $following)
2345 {
2346 $following->setAttribute('check', '');
2347 $closeTag->setAttribute('set', '');
2348 }
2349 }
2350 }
2351 protected static function markEmptyElements(DOMDocument $ir)
2352 {
2353 foreach ($ir->getElementsByTagName('element') as $element)
2354 {
2355 $elName = $element->getAttribute('name');
2356 if (\strpos($elName, '{') !== \false)
2357 $element->setAttribute('void', 'maybe');
2358 elseif (\preg_match(self::$voidRegexp, $elName))
2359 $element->setAttribute('void', 'yes');
2360 $isEmpty = self::isEmpty($element);
2361 if ($isEmpty === 'yes' || $isEmpty === 'maybe')
2362 $element->setAttribute('empty', $isEmpty);
2363 }
2364 }
2365 protected static function getOutputContext(DOMNode $output)
2366 {
2367 $xpath = new DOMXPath($output->ownerDocument);
2368 if ($xpath->evaluate('boolean(ancestor::attribute)', $output))
2369 return 'attribute';
2370 if ($xpath->evaluate('boolean(ancestor::element[@name="script"])', $output))
2371 return 'raw';
2372 return 'text';
2373 }
2374 protected static function getParentElementId(DOMNode $node)
2375 {
2376 $parentNode = $node->parentNode;
2377 while (isset($parentNode))
2378 {
2379 if ($parentNode->nodeName === 'element')
2380 return $parentNode->getAttribute('id');
2381 $parentNode = $parentNode->parentNode;
2382 }
2383 }
2384 protected static function setOutputContext(DOMDocument $ir)
2385 {
2386 foreach ($ir->getElementsByTagName('output') as $output)
2387 $output->setAttribute('escape', self::getOutputContext($output));
2388 }
2389 protected static function optimize(DOMDocument $ir)
2390 {
2391 $xml = $ir->saveXML();
2392 $remainingLoops = 10;
2393 do
2394 {
2395 $old = $xml;
2396 self::optimizeCloseTagElements($ir);
2397 $xml = $ir->saveXML();
2398 }
2399 while (--$remainingLoops > 0 && $xml !== $old);
2400 self::removeCloseTagSiblings($ir);
2401 self::removeContentFromVoidElements($ir);
2402 self::mergeConsecutiveLiteralOutputElements($ir);
2403 self::removeEmptyDefaultCases($ir);
2404 }
2405 protected static function removeCloseTagSiblings(DOMDocument $ir)
2406 {
2407 $xpath = new DOMXPath($ir);
2408 $query = '//switch[not(case[not(closeTag)])]/following-sibling::closeTag';
2409 foreach ($xpath->query($query) as $closeTag)
2410 $closeTag->parentNode->removeChild($closeTag);
2411 }
2412 protected static function removeEmptyDefaultCases(DOMDocument $ir)
2413 {
2414 $xpath = new DOMXPath($ir);
2415 foreach ($xpath->query('//case[not(@test | node())]') as $case)
2416 $case->parentNode->removeChild($case);
2417 }
2418 protected static function mergeConsecutiveLiteralOutputElements(DOMDocument $ir)
2419 {
2420 $xpath = new DOMXPath($ir);
2421 foreach ($xpath->query('//output[@type="literal"]') as $output)
2422 while ($output->nextSibling
2423 && $output->nextSibling->nodeName === 'output'
2424 && $output->nextSibling->getAttribute('type') === 'literal')
2425 {
2426 $output->nodeValue
2427 = \htmlspecialchars($output->nodeValue . $output->nextSibling->nodeValue);
2428 $output->parentNode->removeChild($output->nextSibling);
2429 }
2430 }
2431 protected static function optimizeCloseTagElements(DOMDocument $ir)
2432 {
2433 self::cloneCloseTagElementsIntoSwitch($ir);
2434 self::cloneCloseTagElementsOutOfSwitch($ir);
2435 self::removeRedundantCloseTagElementsInSwitch($ir);
2436 self::removeRedundantCloseTagElements($ir);
2437 }
2438 protected static function cloneCloseTagElementsIntoSwitch(DOMDocument $ir)
2439 {
2440 $xpath = new DOMXPath($ir);
2441 $query = '//switch[name(following-sibling::*) = "closeTag"]';
2442 foreach ($xpath->query($query) as $switch)
2443 {
2444 $closeTag = $switch->nextSibling;
2445 foreach ($switch->childNodes as $case)
2446 if (!$case->lastChild || $case->lastChild->nodeName !== 'closeTag')
2447 $case->appendChild($closeTag->cloneNode());
2448 }
2449 }
2450 protected static function cloneCloseTagElementsOutOfSwitch(DOMDocument $ir)
2451 {
2452 $xpath = new DOMXPath($ir);
2453 $query = '//switch[not(preceding-sibling::closeTag)]';
2454 foreach ($xpath->query($query) as $switch)
2455 {
2456 foreach ($switch->childNodes as $case)
2457 if (!$case->firstChild || $case->firstChild->nodeName !== 'closeTag')
2458 continue 2;
2459 $switch->parentNode->insertBefore($switch->lastChild->firstChild->cloneNode(), $switch);
2460 }
2461 }
2462 protected static function removeRedundantCloseTagElementsInSwitch(DOMDocument $ir)
2463 {
2464 $xpath = new DOMXPath($ir);
2465 $query = '//switch[name(following-sibling::*) = "closeTag"]';
2466 foreach ($xpath->query($query) as $switch)
2467 foreach ($switch->childNodes as $case)
2468 while ($case->lastChild && $case->lastChild->nodeName === 'closeTag')
2469 $case->removeChild($case->lastChild);
2470 }
2471 protected static function removeRedundantCloseTagElements(DOMDocument $ir)
2472 {
2473 $xpath = new DOMXPath($ir);
2474 foreach ($xpath->query('//closeTag') as $closeTag)
2475 {
2476 $id = $closeTag->getAttribute('id');
2477 $query = 'following-sibling::*/descendant-or-self::closeTag[@id="' . $id . '"]';
2478 foreach ($xpath->query($query, $closeTag) as $dupe)
2479 $dupe->parentNode->removeChild($dupe);
2480 }
2481 }
2482 protected static function removeContentFromVoidElements(DOMDocument $ir)
2483 {
2484 $xpath = new DOMXPath($ir);
2485 foreach ($xpath->query('//element[@void="yes"]') as $element)
2486 {
2487 $id = $element->getAttribute('id');
2488 $query = './/closeTag[@id="' . $id . '"]/following-sibling::*';
2489 foreach ($xpath->query($query, $element) as $node)
2490 $node->parentNode->removeChild($node);
2491 }
2492 }
2493 protected static function markBranchTables(DOMDocument $ir)
2494 {
2495 $xpath = new DOMXPath($ir);
2496 foreach ($xpath->query('//switch[case[2][@test]]') as $switch)
2497 {
2498 $key = \null;
2499 $branchValues = array();
2500 foreach ($switch->childNodes as $i => $case)
2501 {
2502 if (!$case->hasAttribute('test'))
2503 continue;
2504 $map = self::parseEqualityExpr($case->getAttribute('test'));
2505 if ($map === \false)
2506 continue 2;
2507 if (\count($map) !== 1)
2508 continue 2;
2509 if (isset($key) && $key !== \key($map))
2510 continue 2;
2511 $key = \key($map);
2512 $branchValues[$i] = \end($map);
2513 }
2514 $switch->setAttribute('branch-key', $key);
2515 foreach ($branchValues as $i => $values)
2516 {
2517 \sort($values);
2518 $switch->childNodes->item($i)->setAttribute('branch-values', \serialize($values));
2519 }
2520 }
2521 }
2522 protected static function appendElement(DOMElement $parentNode, $name, $value = '')
2523 {
2524 if ($value === '')
2525 $element = $parentNode->ownerDocument->createElement($name);
2526 else
2527 $element = $parentNode->ownerDocument->createElement($name, $value);
2528 $parentNode->appendChild($element);
2529 return $element;
2530 }
2531 protected static function appendOutput(DOMElement $ir, $type, $content)
2532 {
2533 if ($type === 'avt')
2534 {
2535 foreach (AVTHelper::parse($content) as $token)
2536 {
2537 $type = ($token[0] === 'expression') ? 'xpath' : 'literal';
2538 self::appendOutput($ir, $type, $token[1]);
2539 }
2540 return;
2541 }
2542 if ($type === 'xpath')
2543 $content = \trim($content);
2544 if ($type === 'literal' && $content === '')
2545 return;
2546 self::appendElement($ir, 'output', \htmlspecialchars($content))
2547 ->setAttribute('type', $type);
2548 }
2549 protected static function isEmpty(DOMElement $ir)
2550 {
2551 $xpath = new DOMXPath($ir->ownerDocument);
2552 if ($xpath->evaluate('count(comment | element | output[@type="literal"])', $ir))
2553 return 'no';
2554 $cases = array();
2555 foreach ($xpath->query('switch/case', $ir) as $case)
2556 $cases[self::isEmpty($case)] = 1;
2557 if (isset($cases['maybe']))
2558 return 'maybe';
2559 if (isset($cases['no']))
2560 {
2561 if (!isset($cases['yes']))
2562 return 'no';
2563 return 'maybe';
2564 }
2565 if ($xpath->evaluate('count(applyTemplates | output[@type="xpath"])', $ir))
2566 return 'maybe';
2567 return 'yes';
2568 }
2569 }
2570
2571 /*
2572 * @package s9e\TextFormatter
2573 * @copyright Copyright (c) 2010-2016 The s9e Authors
2574 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
2575 */
2576 namespace s9e\TextFormatter\Configurator\Helpers;
2577 use RuntimeException;
2578 abstract class XPathHelper
2579 {
2580 public static function export($str)
2581 {
2582 if (\strpos($str, "'") === \false)
2583 return "'" . $str . "'";
2584 if (\strpos($str, '"') === \false)
2585 return '"' . $str . '"';
2586 $toks = array();
2587 $c = '"';
2588 $pos = 0;
2589 while ($pos < \strlen($str))
2590 {
2591 $spn = \strcspn($str, $c, $pos);
2592 if ($spn)
2593 {
2594 $toks[] = $c . \substr($str, $pos, $spn) . $c;
2595 $pos += $spn;
2596 }
2597 $c = ($c === '"') ? "'" : '"';
2598 }
2599 return 'concat(' . \implode(',', $toks) . ')';
2600 }
2601 public static function getVariables($expr)
2602 {
2603 $expr = \preg_replace('/(["\']).*?\\1/s', '$1$1', $expr);
2604 \preg_match_all('/\\$(\\w+)/', $expr, $matches);
2605 $varNames = \array_unique($matches[1]);
2606 \sort($varNames);
2607 return $varNames;
2608 }
2609 public static function isExpressionNumeric($expr)
2610 {
2611 $expr = \strrev(\preg_replace('(\\((?!\\s*(?!vid(?!\\w))\\w))', ' ', \strrev($expr)));
2612 $expr = \str_replace(')', ' ', $expr);
2613 if (\preg_match('(^\\s*([$@][-\\w]++|-?\\d++)(?>\\s*(?>[-+*]|div)\\s*(?1))++\\s*$)', $expr))
2614 return \true;
2615 return \false;
2616 }
2617 public static function minify($expr)
2618 {
2619 $old = $expr;
2620 $strings = array();
2621 $expr = \preg_replace_callback(
2622 '/"[^"]*"|\'[^\']*\'/',
2623 function ($m) use (&$strings)
2624 {
2625 $uniqid = '(' . \sha1(\uniqid()) . ')';
2626 $strings[$uniqid] = $m[0];
2627 return $uniqid;
2628 },
2629 \trim($expr)
2630 );
2631 if (\preg_match('/[\'"]/', $expr))
2632 throw new RuntimeException("Cannot parse XPath expression '" . $old . "'");
2633 $expr = \preg_replace('/\\s+/', ' ', $expr);
2634 $expr = \preg_replace('/([-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr);
2635 $expr = \preg_replace('/([^-a-z_0-9]) ([-a-z_0-9])/i', '$1$2', $expr);
2636 $expr = \preg_replace('/(?!- -)([^-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr);
2637 $expr = \preg_replace('/ - ([a-z_0-9])/i', ' -$1', $expr);
2638 $expr = \preg_replace('/((?:^|[ \\(])\\d+) div ?/', '$1div', $expr);
2639 $expr = \preg_replace('/([^-a-z_0-9]div) (?=[$0-9@])/', '$1', $expr);
2640 $expr = \strtr($expr, $strings);
2641 return $expr;
2642 }
2643 }
2644
2645 /*
2646 * @package s9e\TextFormatter
2647 * @copyright Copyright (c) 2010-2016 The s9e Authors
2648 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
2649 */
2650 namespace s9e\TextFormatter\Configurator\Items;
2651 use DOMDocument;
2652 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
2653 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
2654 use s9e\TextFormatter\Configurator\TemplateNormalizer;
2655 class Template
2656 {
2657 protected $forensics;
2658 protected $isNormalized = \false;
2659 protected $template;
2660 public function __construct($template)
2661 {
2662 $this->template = $template;
2663 }
2664 public function __call($methodName, $args)
2665 {
2666 return \call_user_func_array(array($this->getForensics(), $methodName), $args);
2667 }
2668 public function __toString()
2669 {
2670 return $this->template;
2671 }
2672 public function asDOM()
2673 {
2674 $xml = '<xsl:template xmlns:xsl="http://www.w3.org/1999/XSL/Transform">'
2675 . $this->__toString()
2676 . '</xsl:template>';
2677 $dom = new TemplateDocument($this);
2678 $dom->loadXML($xml);
2679 return $dom;
2680 }
2681 public function getCSSNodes()
2682 {
2683 return TemplateHelper::getCSSNodes($this->asDOM());
2684 }
2685 public function getForensics()
2686 {
2687 if (!isset($this->forensics))
2688 $this->forensics = new TemplateForensics($this->__toString());
2689 return $this->forensics;
2690 }
2691 public function getJSNodes()
2692 {
2693 return TemplateHelper::getJSNodes($this->asDOM());
2694 }
2695 public function getURLNodes()
2696 {
2697 return TemplateHelper::getURLNodes($this->asDOM());
2698 }
2699 public function getParameters()
2700 {
2701 return TemplateHelper::getParametersFromXSL($this->__toString());
2702 }
2703 public function isNormalized($bool = \null)
2704 {
2705 if (isset($bool))
2706 $this->isNormalized = $bool;
2707 return $this->isNormalized;
2708 }
2709 public function normalize(TemplateNormalizer $templateNormalizer)
2710 {
2711 $this->forensics = \null;
2712 $this->template = $templateNormalizer->normalizeTemplate($this->template);
2713 $this->isNormalized = \true;
2714 }
2715 public function replaceTokens($regexp, $fn)
2716 {
2717 $this->forensics = \null;
2718 $this->template = TemplateHelper::replaceTokens($this->template, $regexp, $fn);
2719 $this->isNormalized = \false;
2720 }
2721 public function setContent($template)
2722 {
2723 $this->forensics = \null;
2724 $this->template = (string) $template;
2725 $this->isNormalized = \false;
2726 }
2727 }
2728
2729 /*
2730 * @package s9e\TextFormatter
2731 * @copyright Copyright (c) 2010-2016 The s9e Authors
2732 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
2733 */
2734 namespace s9e\TextFormatter\Configurator\JavaScript;
2735 use InvalidArgumentException;
2736 class FunctionProvider
2737 {
2738 public static $cache = array(
2739 'addslashes'=>'function(str)
2740 {
2741 return str.replace(/["\'\\\\]/g, \'\\\\$&\').replace(/\\u0000/g, \'\\\\0\');
2742 }',
2743 'dechex'=>'function(str)
2744 {
2745 return parseInt(str).toString(16);
2746 }',
2747 'intval'=>'function(str)
2748 {
2749 return parseInt(str) || 0;
2750 }',
2751 'ltrim'=>'function(str)
2752 {
2753 return str.replace(/^[ \\n\\r\\t\\0\\x0B]+/g, \'\');
2754 }',
2755 'mb_strtolower'=>'function(str)
2756 {
2757 return str.toLowerCase();
2758 }',
2759 'mb_strtoupper'=>'function(str)
2760 {
2761 return str.toUpperCase();
2762 }',
2763 'mt_rand'=>'function(min, max)
2764 {
2765 return (min + Math.floor(Math.random() * (max + 1 - min)));
2766 }',
2767 'rawurlencode'=>'function(str)
2768 {
2769 return encodeURIComponent(str).replace(
2770 /[!\'()*]/g,
2771 /**
2772 * @param {!string} c
2773 */
2774 function(c)
2775 {
2776 return \'%\' + c.charCodeAt(0).toString(16).toUpperCase();
2777 }
2778 );
2779 }',
2780 'rtrim'=>'function(str)
2781 {
2782 return str.replace(/[ \\n\\r\\t\\0\\x0B]+$/g, \'\');
2783 }',
2784 'str_rot13'=>'function(str)
2785 {
2786 return str.replace(
2787 /[a-z]/gi,
2788 function(c)
2789 {
2790 return String.fromCharCode(c.charCodeAt(0) + ((c.toLowerCase() < \'n\') ? 13 : -13));
2791 }
2792 );
2793 }',
2794 'stripslashes'=>'function(str)
2795 {
2796 // NOTE: this will not correctly transform \\0 into a NULL byte. I consider this a feature
2797 // rather than a bug. There\'s no reason to use NULL bytes in a text.
2798 return str.replace(/\\\\([\\s\\S]?)/g, \'\\\\1\');
2799 }',
2800 'strrev'=>'function(str)
2801 {
2802 return str.split(\'\').reverse().join(\'\');
2803 }',
2804 'strtolower'=>'function(str)
2805 {
2806 return str.toLowerCase();
2807 }',
2808 'strtotime'=>'function(str)
2809 {
2810 return Date.parse(str) / 1000;
2811 }',
2812 'strtoupper'=>'function(str)
2813 {
2814 return str.toUpperCase();
2815 }',
2816 'trim'=>'function(str)
2817 {
2818 return str.replace(/^[ \\n\\r\\t\\0\\x0B]+/g, \'\').replace(/[ \\n\\r\\t\\0\\x0B]+$/g, \'\');
2819 }',
2820 'ucfirst'=>'function(str)
2821 {
2822 return str.charAt(0).toUpperCase() + str.substr(1);
2823 }',
2824 'ucwords'=>'function(str)
2825 {
2826 return str.replace(
2827 /(?:^|\\s)[a-z]/g,
2828 function(m)
2829 {
2830 return m.toUpperCase()
2831 }
2832 );
2833 }',
2834 'urldecode'=>'function(str)
2835 {
2836 return decodeURIComponent(str);
2837 }',
2838 'urlencode'=>'function(str)
2839 {
2840 return encodeURIComponent(str);
2841 }'
2842 );
2843 public static function get($funcName)
2844 {
2845 if (isset(self::$cache[$funcName]))
2846 return self::$cache[$funcName];
2847 if (\preg_match('(^[a-z_0-9]+$)D', $funcName))
2848 {
2849 $filepath = __DIR__ . '/Configurator/JavaScript/functions/' . $funcName . '.js';
2850 if (\file_exists($filepath))
2851 return \file_get_contents($filepath);
2852 }
2853 throw new InvalidArgumentException("Unknown function '" . $funcName . "'");
2854 }
2855 }
2856
2857 /*
2858 * @package s9e\TextFormatter
2859 * @copyright Copyright (c) 2010-2016 The s9e Authors
2860 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
2861 */
2862 namespace s9e\TextFormatter\Configurator;
2863 interface RendererGenerator
2864 {
2865 public function getRenderer(Rendering $rendering);
2866 }
2867
2868 /*
2869 * @package s9e\TextFormatter
2870 * @copyright Copyright (c) 2010-2016 The s9e Authors
2871 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
2872 */
2873 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
2874 abstract class AbstractOptimizer
2875 {
2876 protected $cnt;
2877 protected $i;
2878 protected $changed;
2879 protected $tokens;
2880 public function optimize($php)
2881 {
2882 $this->reset($php);
2883 $this->optimizeTokens();
2884 if ($this->changed)
2885 $php = $this->serialize();
2886 unset($this->tokens);
2887 return $php;
2888 }
2889 abstract protected function optimizeTokens();
2890 protected function reset($php)
2891 {
2892 $this->tokens = \token_get_all('<?php ' . $php);
2893 $this->i = 0;
2894 $this->cnt = \count($this->tokens);
2895 $this->changed = \false;
2896 }
2897 protected function serialize()
2898 {
2899 unset($this->tokens[0]);
2900 $php = '';
2901 foreach ($this->tokens as $token)
2902 $php .= (\is_string($token)) ? $token : $token[1];
2903 return $php;
2904 }
2905 protected function skipToString($str)
2906 {
2907 while (++$this->i < $this->cnt && $this->tokens[$this->i] !== $str);
2908 }
2909 protected function skipWhitespace()
2910 {
2911 while (++$this->i < $this->cnt && $this->tokens[$this->i][0] === \T_WHITESPACE);
2912 }
2913 protected function unindentBlock($start, $end)
2914 {
2915 $this->i = $start;
2916 do
2917 {
2918 if ($this->tokens[$this->i][0] === \T_WHITESPACE || $this->tokens[$this->i][0] === \T_DOC_COMMENT)
2919 $this->tokens[$this->i][1] = \preg_replace("/^\t/m", '', $this->tokens[$this->i][1]);
2920 }
2921 while (++$this->i <= $end);
2922 }
2923 }
2924
2925 /*
2926 * @package s9e\TextFormatter
2927 * @copyright Copyright (c) 2010-2016 The s9e Authors
2928 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
2929 */
2930 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
2931 class BranchOutputOptimizer
2932 {
2933 protected $cnt;
2934 protected $i;
2935 protected $tokens;
2936 public function optimize(array $tokens)
2937 {
2938 $this->tokens = $tokens;
2939 $this->i = 0;
2940 $this->cnt = \count($this->tokens);
2941 $php = '';
2942 while (++$this->i < $this->cnt)
2943 if ($this->tokens[$this->i][0] === \T_IF)
2944 $php .= $this->serializeIfBlock($this->parseIfBlock());
2945 else
2946 $php .= $this->serializeToken($this->tokens[$this->i]);
2947 unset($this->tokens);
2948 return $php;
2949 }
2950 protected function captureOutput()
2951 {
2952 $expressions = array();
2953 while ($this->skipOutputAssignment())
2954 {
2955 do
2956 {
2957 $expressions[] = $this->captureOutputExpression();
2958 }
2959 while ($this->tokens[$this->i++] === '.');
2960 }
2961 return $expressions;
2962 }
2963 protected function captureOutputExpression()
2964 {
2965 $parens = 0;
2966 $php = '';
2967 do
2968 {
2969 if ($this->tokens[$this->i] === ';')
2970 break;
2971 elseif ($this->tokens[$this->i] === '.' && !$parens)
2972 break;
2973 elseif ($this->tokens[$this->i] === '(')
2974 ++$parens;
2975 elseif ($this->tokens[$this->i] === ')')
2976 --$parens;
2977 $php .= $this->serializeToken($this->tokens[$this->i]);
2978 }
2979 while (++$this->i < $this->cnt);
2980 return $php;
2981 }
2982 protected function captureStructure()
2983 {
2984 $php = '';
2985 do
2986 {
2987 $php .= $this->serializeToken($this->tokens[$this->i]);
2988 }
2989 while ($this->tokens[++$this->i] !== '{');
2990 ++$this->i;
2991 return $php;
2992 }
2993 protected function isBranchToken()
2994 {
2995 return \in_array($this->tokens[$this->i][0], array(\T_ELSE, \T_ELSEIF, \T_IF), \true);
2996 }
2997 protected function mergeIfBranches(array $branches)
2998 {
2999 $lastBranch = \end($branches);
3000 if ($lastBranch['structure'] === 'else')
3001 {
3002 $before = $this->optimizeBranchesHead($branches);
3003 $after = $this->optimizeBranchesTail($branches);
3004 }
3005 else
3006 $before = $after = array();
3007 $source = '';
3008 foreach ($branches as $branch)
3009 $source .= $this->serializeBranch($branch);
3010 return array(
3011 'before' => $before,
3012 'source' => $source,
3013 'after' => $after
3014 );
3015 }
3016 protected function mergeOutput(array $left, array $right)
3017 {
3018 if (empty($left))
3019 return $right;
3020 if (empty($right))
3021 return $left;
3022 $k = \count($left) - 1;
3023 if (\substr($left[$k], -1) === "'" && $right[0][0] === "'")
3024 {
3025 $right[0] = \substr($left[$k], 0, -1) . \substr($right[0], 1);
3026 unset($left[$k]);
3027 }
3028 return \array_merge($left, $right);
3029 }
3030 protected function optimizeBranchesHead(array &$branches)
3031 {
3032 $before = $this->optimizeBranchesOutput($branches, 'head');
3033 foreach ($branches as &$branch)
3034 {
3035 if ($branch['body'] !== '' || !empty($branch['tail']))
3036 continue;
3037 $branch['tail'] = \array_reverse($branch['head']);
3038 $branch['head'] = array();
3039 }
3040 unset($branch);
3041 return $before;
3042 }
3043 protected function optimizeBranchesOutput(array &$branches, $which)
3044 {
3045 $expressions = array();
3046 while (isset($branches[0][$which][0]))
3047 {
3048 $expr = $branches[0][$which][0];
3049 foreach ($branches as $branch)
3050 if (!isset($branch[$which][0]) || $branch[$which][0] !== $expr)
3051 break 2;
3052 $expressions[] = $expr;
3053 foreach ($branches as &$branch)
3054 \array_shift($branch[$which]);
3055 unset($branch);
3056 }
3057 return $expressions;
3058 }
3059 protected function optimizeBranchesTail(array &$branches)
3060 {
3061 return $this->optimizeBranchesOutput($branches, 'tail');
3062 }
3063 protected function parseBranch()
3064 {
3065 $structure = $this->captureStructure();
3066 $head = $this->captureOutput();
3067 $body = '';
3068 $tail = array();
3069 $braces = 0;
3070 do
3071 {
3072 $tail = $this->mergeOutput($tail, \array_reverse($this->captureOutput()));
3073 if ($this->tokens[$this->i] === '}' && !$braces)
3074 break;
3075 $body .= $this->serializeOutput(\array_reverse($tail));
3076 $tail = array();
3077 if ($this->tokens[$this->i][0] === \T_IF)
3078 {
3079 $child = $this->parseIfBlock();
3080 if ($body === '')
3081 $head = $this->mergeOutput($head, $child['before']);
3082 else
3083 $body .= $this->serializeOutput($child['before']);
3084 $body .= $child['source'];
3085 $tail = $child['after'];
3086 }
3087 else
3088 {
3089 $body .= $this->serializeToken($this->tokens[$this->i]);
3090 if ($this->tokens[$this->i] === '{')
3091 ++$braces;
3092 elseif ($this->tokens[$this->i] === '}')
3093 --$braces;
3094 }
3095 }
3096 while (++$this->i < $this->cnt);
3097 return array(
3098 'structure' => $structure,
3099 'head' => $head,
3100 'body' => $body,
3101 'tail' => $tail
3102 );
3103 }
3104 protected function parseIfBlock()
3105 {
3106 $branches = array();
3107 do
3108 {
3109 $branches[] = $this->parseBranch();
3110 }
3111 while (++$this->i < $this->cnt && $this->isBranchToken());
3112 --$this->i;
3113 return $this->mergeIfBranches($branches);
3114 }
3115 protected function serializeBranch(array $branch)
3116 {
3117 if ($branch['structure'] === 'else'
3118 && $branch['body'] === ''
3119 && empty($branch['head'])
3120 && empty($branch['tail']))
3121 return '';
3122 return $branch['structure'] . '{' . $this->serializeOutput($branch['head']) . $branch['body'] . $this->serializeOutput(\array_reverse($branch['tail'])) . '}';
3123 }
3124 protected function serializeIfBlock(array $block)
3125 {
3126 return $this->serializeOutput($block['before']) . $block['source'] . $this->serializeOutput(\array_reverse($block['after']));
3127 }
3128 protected function serializeOutput(array $expressions)
3129 {
3130 if (empty($expressions))
3131 return '';
3132 return '$this->out.=' . \implode('.', $expressions) . ';';
3133 }
3134 protected function serializeToken($token)
3135 {
3136 return (\is_array($token)) ? $token[1] : $token;
3137 }
3138 protected function skipOutputAssignment()
3139 {
3140 if ($this->tokens[$this->i ][0] !== \T_VARIABLE
3141 || $this->tokens[$this->i ][1] !== '$this'
3142 || $this->tokens[$this->i + 1][0] !== \T_OBJECT_OPERATOR
3143 || $this->tokens[$this->i + 2][0] !== \T_STRING
3144 || $this->tokens[$this->i + 2][1] !== 'out'
3145 || $this->tokens[$this->i + 3][0] !== \T_CONCAT_EQUAL)
3146 return \false;
3147 $this->i += 4;
3148 return \true;
3149 }
3150 }
3151
3152 /*
3153 * @package s9e\TextFormatter
3154 * @copyright Copyright (c) 2010-2016 The s9e Authors
3155 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
3156 */
3157 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
3158 class Optimizer
3159 {
3160 public $branchOutputOptimizer;
3161 protected $cnt;
3162 protected $i;
3163 public $maxLoops = 10;
3164 protected $tokens;
3165 public function __construct()
3166 {
3167 $this->branchOutputOptimizer = new BranchOutputOptimizer;
3168 }
3169 public function optimize($php)
3170 {
3171 $this->tokens = \token_get_all('<?php ' . $php);
3172 $this->cnt = \count($this->tokens);
3173 $this->i = 0;
3174 foreach ($this->tokens as &$token)
3175 if (\is_array($token))
3176 unset($token[2]);
3177 unset($token);
3178 $passes = array(
3179 'optimizeOutConcatEqual',
3180 'optimizeConcatenations',
3181 'optimizeHtmlspecialchars'
3182 );
3183 $remainingLoops = $this->maxLoops;
3184 do
3185 {
3186 $continue = \false;
3187 foreach ($passes as $pass)
3188 {
3189 $this->$pass();
3190 $cnt = \count($this->tokens);
3191 if ($this->cnt !== $cnt)
3192 {
3193 $this->tokens = \array_values($this->tokens);
3194 $this->cnt = $cnt;
3195 $continue = \true;
3196 }
3197 }
3198 }
3199 while ($continue && --$remainingLoops);
3200 $php = $this->branchOutputOptimizer->optimize($this->tokens);
3201 unset($this->tokens);
3202 return $php;
3203 }
3204 protected function isBetweenHtmlspecialcharCalls()
3205 {
3206 return ($this->tokens[$this->i + 1] === array(\T_STRING, 'htmlspecialchars')
3207 && $this->tokens[$this->i + 2] === '('
3208 && $this->tokens[$this->i - 1] === ')'
3209 && $this->tokens[$this->i - 2][0] === \T_LNUMBER
3210 && $this->tokens[$this->i - 3] === ',');
3211 }
3212 protected function isHtmlspecialcharSafeVar()
3213 {
3214 return ($this->tokens[$this->i ] === array(\T_VARIABLE, '$node')
3215 && $this->tokens[$this->i + 1] === array(\T_OBJECT_OPERATOR, '->')
3216 && ($this->tokens[$this->i + 2] === array(\T_STRING, 'localName')
3217 || $this->tokens[$this->i + 2] === array(\T_STRING, 'nodeName'))
3218 && $this->tokens[$this->i + 3] === ','
3219 && $this->tokens[$this->i + 4][0] === \T_LNUMBER
3220 && $this->tokens[$this->i + 5] === ')');
3221 }
3222 protected function isOutputAssignment()
3223 {
3224 return ($this->tokens[$this->i ] === array(\T_VARIABLE, '$this')
3225 && $this->tokens[$this->i + 1] === array(\T_OBJECT_OPERATOR, '->')
3226 && $this->tokens[$this->i + 2] === array(\T_STRING, 'out')
3227 && $this->tokens[$this->i + 3] === array(\T_CONCAT_EQUAL, '.='));
3228 }
3229 protected function isPrecededByOutputVar()
3230 {
3231 return ($this->tokens[$this->i - 1] === array(\T_STRING, 'out')
3232 && $this->tokens[$this->i - 2] === array(\T_OBJECT_OPERATOR, '->')
3233 && $this->tokens[$this->i - 3] === array(\T_VARIABLE, '$this'));
3234 }
3235 protected function mergeConcatenatedHtmlSpecialChars()
3236 {
3237 if (!$this->isBetweenHtmlspecialcharCalls())
3238 return \false;
3239 $escapeMode = $this->tokens[$this->i - 2][1];
3240 $startIndex = $this->i - 3;
3241 $endIndex = $this->i + 2;
3242 $this->i = $endIndex;
3243 $parens = 0;
3244 while (++$this->i < $this->cnt)
3245 {
3246 if ($this->tokens[$this->i] === ',' && !$parens)
3247 break;
3248 if ($this->tokens[$this->i] === '(')
3249 ++$parens;
3250 elseif ($this->tokens[$this->i] === ')')
3251 --$parens;
3252 }
3253 if ($this->tokens[$this->i + 1] !== array(\T_LNUMBER, $escapeMode))
3254 return \false;
3255 $this->tokens[$startIndex] = '.';
3256 $this->i = $startIndex;
3257 while (++$this->i <= $endIndex)
3258 unset($this->tokens[$this->i]);
3259 return \true;
3260 }
3261 protected function mergeConcatenatedStrings()
3262 {
3263 if ($this->tokens[$this->i - 1][0] !== \T_CONSTANT_ENCAPSED_STRING
3264 || $this->tokens[$this->i + 1][0] !== \T_CONSTANT_ENCAPSED_STRING
3265 || $this->tokens[$this->i - 1][1][0] !== $this->tokens[$this->i + 1][1][0])
3266 return \false;
3267 $this->tokens[$this->i + 1][1] = \substr($this->tokens[$this->i - 1][1], 0, -1)
3268 . \substr($this->tokens[$this->i + 1][1], 1);
3269 unset($this->tokens[$this->i - 1]);
3270 unset($this->tokens[$this->i]);
3271 ++$this->i;
3272 return \true;
3273 }
3274 protected function optimizeOutConcatEqual()
3275 {
3276 $this->i = 3;
3277 while ($this->skipTo(array(\T_CONCAT_EQUAL, '.=')))
3278 {
3279 if (!$this->isPrecededByOutputVar())
3280 continue;
3281 while ($this->skipPast(';'))
3282 {
3283 if (!$this->isOutputAssignment())
3284 break;
3285 $this->tokens[$this->i - 1] = '.';
3286 unset($this->tokens[$this->i++]);
3287 unset($this->tokens[$this->i++]);
3288 unset($this->tokens[$this->i++]);
3289 unset($this->tokens[$this->i++]);
3290 }
3291 }
3292 }
3293 protected function optimizeConcatenations()
3294 {
3295 $this->i = 1;
3296 while ($this->skipTo('.'))
3297 $this->mergeConcatenatedStrings() || $this->mergeConcatenatedHtmlSpecialChars();
3298 }
3299 protected function optimizeHtmlspecialchars()
3300 {
3301 $this->i = 0;
3302 while ($this->skipPast(array(\T_STRING, 'htmlspecialchars')))
3303 if ($this->tokens[$this->i] === '(')
3304 {
3305 ++$this->i;
3306 $this->replaceHtmlspecialcharsLiteral() || $this->removeHtmlspecialcharsSafeVar();
3307 }
3308 }
3309 protected function removeHtmlspecialcharsSafeVar()
3310 {
3311 if (!$this->isHtmlspecialcharSafeVar())
3312 return \false;
3313 unset($this->tokens[$this->i - 2]);
3314 unset($this->tokens[$this->i - 1]);
3315 unset($this->tokens[$this->i + 3]);
3316 unset($this->tokens[$this->i + 4]);
3317 unset($this->tokens[$this->i + 5]);
3318 $this->i += 6;
3319 return \true;
3320 }
3321 protected function replaceHtmlspecialcharsLiteral()
3322 {
3323 if ($this->tokens[$this->i ][0] !== \T_CONSTANT_ENCAPSED_STRING
3324 || $this->tokens[$this->i + 1] !== ','
3325 || $this->tokens[$this->i + 2][0] !== \T_LNUMBER
3326 || $this->tokens[$this->i + 3] !== ')')
3327 return \false;
3328 $this->tokens[$this->i][1] = \var_export(
3329 \htmlspecialchars(
3330 \stripslashes(\substr($this->tokens[$this->i][1], 1, -1)),
3331 $this->tokens[$this->i + 2][1]
3332 ),
3333 \true
3334 );
3335 unset($this->tokens[$this->i - 2]);
3336 unset($this->tokens[$this->i - 1]);
3337 unset($this->tokens[++$this->i]);
3338 unset($this->tokens[++$this->i]);
3339 unset($this->tokens[++$this->i]);
3340 return \true;
3341 }
3342 protected function skipPast($token)
3343 {
3344 return ($this->skipTo($token) && ++$this->i < $this->cnt);
3345 }
3346 protected function skipTo($token)
3347 {
3348 while (++$this->i < $this->cnt)
3349 if ($this->tokens[$this->i] === $token)
3350 return \true;
3351 return \false;
3352 }
3353 }
3354
3355 /*
3356 * @package s9e\TextFormatter
3357 * @copyright Copyright (c) 2010-2016 The s9e Authors
3358 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
3359 */
3360 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
3361 use RuntimeException;
3362 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
3363 class Quick
3364 {
3365 public static function getSource(array $compiledTemplates)
3366 {
3367 $map = array();
3368 $tagNames = array();
3369 $unsupported = array();
3370 foreach ($compiledTemplates as $tagName => $php)
3371 {
3372 if (\preg_match('(^(?:br|[ieps])$)', $tagName))
3373 continue;
3374 $rendering = self::getRenderingStrategy($php);
3375 if ($rendering === \false)
3376 {
3377 $unsupported[] = $tagName;
3378 continue;
3379 }
3380 foreach ($rendering as $i => $_562c18b7)
3381 {
3382 list($strategy, $replacement) = $_562c18b7;
3383 $match = (($i) ? '/' : '') . $tagName;
3384 $map[$strategy][$match] = $replacement;
3385 }
3386 if (!isset($rendering[1]))
3387 $tagNames[] = $tagName;
3388 }
3389 $php = array();
3390 if (isset($map['static']))
3391 $php[] = ' private static $static=' . self::export($map['static']) . ';';
3392 if (isset($map['dynamic']))
3393 $php[] = ' private static $dynamic=' . self::export($map['dynamic']) . ';';
3394 if (isset($map['php']))
3395 {
3396 list($quickBranches, $quickSource) = self::generateBranchTable('$qb', $map['php']);
3397 $php[] = ' private static $attributes;';
3398 $php[] = ' private static $quickBranches=' . self::export($quickBranches) . ';';
3399 }
3400 if (!empty($unsupported))
3401 {
3402 $regexp = '(<' . RegexpBuilder::fromList($unsupported, array('useLookahead' => \true)) . '[ />])';
3403 $php[] = ' public $quickRenderingTest=' . \var_export($regexp, \true) . ';';
3404 }
3405 $php[] = '';
3406 $php[] = ' protected function renderQuick($xml)';
3407 $php[] = ' {';
3408 $php[] = ' $xml = $this->decodeSMP($xml);';
3409 if (isset($map['php']))
3410 $php[] = ' self::$attributes = array();';
3411 $regexp = '(<(?:(?!/)(';
3412 $regexp .= ($tagNames) ? RegexpBuilder::fromList($tagNames) : '(?!)';
3413 $regexp .= ')(?: [^>]*)?>.*?</\\1|(/?(?!br/|p>)[^ />]+)[^>]*?(/)?)>)s';
3414 $php[] = ' $html = preg_replace_callback(';
3415 $php[] = ' ' . \var_export($regexp, \true) . ',';
3416 $php[] = " array(\$this, 'quick'),";
3417 $php[] = ' preg_replace(';
3418 $php[] = " '(<[eis]>[^<]*</[eis]>)',";
3419 $php[] = " '',";
3420 $php[] = ' substr($xml, 1 + strpos($xml, \'>\'), -4)';
3421 $php[] = ' )';
3422 $php[] = ' );';
3423 $php[] = '';
3424 $php[] = " return str_replace('<br/>', '<br>', \$html);";
3425 $php[] = ' }';
3426 $php[] = '';
3427 $php[] = ' protected function quick($m)';
3428 $php[] = ' {';
3429 $php[] = ' if (isset($m[2]))';
3430 $php[] = ' {';
3431 $php[] = ' $id = $m[2];';
3432 $php[] = '';
3433 $php[] = ' if (isset($m[3]))';
3434 $php[] = ' {';
3435 $php[] = ' unset($m[3]);';
3436 $php[] = '';
3437 $php[] = ' $m[0] = substr($m[0], 0, -2) . \'>\';';
3438 $php[] = ' $html = $this->quick($m);';
3439 $php[] = '';
3440 $php[] = ' $m[0] = \'</\' . $id . \'>\';';
3441 $php[] = ' $m[2] = \'/\' . $id;';
3442 $php[] = ' $html .= $this->quick($m);';
3443 $php[] = '';
3444 $php[] = ' return $html;';
3445 $php[] = ' }';
3446 $php[] = ' }';
3447 $php[] = ' else';
3448 $php[] = ' {';
3449 $php[] = ' $id = $m[1];';
3450 $php[] = '';
3451 $php[] = ' $lpos = 1 + strpos($m[0], \'>\');';
3452 $php[] = ' $rpos = strrpos($m[0], \'<\');';
3453 $php[] = ' $textContent = substr($m[0], $lpos, $rpos - $lpos);';
3454 $php[] = '';
3455 $php[] = ' if (strpos($textContent, \'<\') !== false)';
3456 $php[] = ' {';
3457 $php[] = ' throw new \\RuntimeException;';
3458 $php[] = ' }';
3459 $php[] = '';
3460 $php[] = ' $textContent = htmlspecialchars_decode($textContent);';
3461 $php[] = ' }';
3462 $php[] = '';
3463 if (isset($map['static']))
3464 {
3465 $php[] = ' if (isset(self::$static[$id]))';
3466 $php[] = ' {';
3467 $php[] = ' return self::$static[$id];';
3468 $php[] = ' }';
3469 $php[] = '';
3470 }
3471 if (isset($map['dynamic']))
3472 {
3473 $php[] = ' if (isset(self::$dynamic[$id]))';
3474 $php[] = ' {';
3475 $php[] = ' list($match, $replace) = self::$dynamic[$id];';
3476 $php[] = ' return preg_replace($match, $replace, $m[0], 1);';
3477 $php[] = ' }';
3478 $php[] = '';
3479 }
3480 if (isset($map['php']))
3481 {
3482 $php[] = ' if (!isset(self::$quickBranches[$id]))';
3483 $php[] = ' {';
3484 }
3485 $condition = "\$id[0] === '!' || \$id[0] === '?'";
3486 if (!empty($unsupported))
3487 {
3488 $regexp = '(^/?' . RegexpBuilder::fromList($unsupported) . '$)';
3489 $condition .= ' || preg_match(' . \var_export($regexp, \true) . ', $id)';
3490 }
3491 $php[] = ' if (' . $condition . ')';
3492 $php[] = ' {';
3493 $php[] = ' throw new \\RuntimeException;';
3494 $php[] = ' }';
3495 $php[] = " return '';";
3496 if (isset($map['php']))
3497 {
3498 $php[] = ' }';
3499 $php[] = '';
3500 $php[] = ' $attributes = array();';
3501 $php[] = ' if (strpos($m[0], \'="\') !== false)';
3502 $php[] = ' {';
3503 $php[] = ' preg_match_all(\'(([^ =]++)="([^"]*))S\', substr($m[0], 0, strpos($m[0], \'>\')), $matches);';
3504 $php[] = ' foreach ($matches[1] as $i => $attrName)';
3505 $php[] = ' {';
3506 $php[] = ' $attributes[$attrName] = $matches[2][$i];';
3507 $php[] = ' }';
3508 $php[] = ' }';
3509 $php[] = '';
3510 $php[] = ' $qb = self::$quickBranches[$id];';
3511 $php[] = ' ' . $quickSource;
3512 $php[] = '';
3513 $php[] = ' return $html;';
3514 }
3515 $php[] = ' }';
3516 return \implode("\n", $php);
3517 }
3518 protected static function export(array $arr)
3519 {
3520 $exportKeys = (\array_keys($arr) !== \range(0, \count($arr) - 1));
3521 \ksort($arr);
3522 $entries = array();
3523 foreach ($arr as $k => $v)
3524 $entries[] = (($exportKeys) ? \var_export($k, \true) . '=>' : '')
3525 . ((\is_array($v)) ? self::export($v) : \var_export($v, \true));
3526 return 'array(' . \implode(',', $entries) . ')';
3527 }
3528 public static function getRenderingStrategy($php)
3529 {
3530 $chunks = \explode('$this->at($node);', $php);
3531 $renderings = array();
3532 if (\count($chunks) <= 2)
3533 {
3534 foreach ($chunks as $k => $chunk)
3535 {
3536 $rendering = self::getStaticRendering($chunk);
3537 if ($rendering !== \false)
3538 {
3539 $renderings[$k] = array('static', $rendering);
3540 continue;
3541 }
3542 if ($k === 0)
3543 {
3544 $rendering = self::getDynamicRendering($chunk);
3545 if ($rendering !== \false)
3546 {
3547 $renderings[$k] = array('dynamic', $rendering);
3548 continue;
3549 }
3550 }
3551 $renderings[$k] = \false;
3552 }
3553 if (!\in_array(\false, $renderings, \true))
3554 return $renderings;
3555 }
3556 $phpRenderings = self::getQuickRendering($php);
3557 if ($phpRenderings === \false)
3558 return \false;
3559 foreach ($phpRenderings as $i => $phpRendering)
3560 if (!isset($renderings[$i]) || $renderings[$i] === \false || \strpos($phpRendering, 'self::$attributes[]') !== \false)
3561 $renderings[$i] = array('php', $phpRendering);
3562 return $renderings;
3563 }
3564 protected static function getQuickRendering($php)
3565 {
3566 if (\preg_match('(\\$this->at\\((?!\\$node\\);))', $php))
3567 return \false;
3568 $tokens = \token_get_all('<?php ' . $php);
3569 $tokens[] = array(0, '');
3570 \array_shift($tokens);
3571 $cnt = \count($tokens);
3572 $branch = array(
3573 'braces' => -1,
3574 'branches' => array(),
3575 'head' => '',
3576 'passthrough' => 0,
3577 'statement' => '',
3578 'tail' => ''
3579 );
3580 $braces = 0;
3581 $i = 0;
3582 do
3583 {
3584 if ($tokens[$i ][0] === \T_VARIABLE
3585 && $tokens[$i ][1] === '$this'
3586 && $tokens[$i + 1][0] === \T_OBJECT_OPERATOR
3587 && $tokens[$i + 2][0] === \T_STRING
3588 && $tokens[$i + 2][1] === 'at'
3589 && $tokens[$i + 3] === '('
3590 && $tokens[$i + 4][0] === \T_VARIABLE
3591 && $tokens[$i + 4][1] === '$node'
3592 && $tokens[$i + 5] === ')'
3593 && $tokens[$i + 6] === ';')
3594 {
3595 if (++$branch['passthrough'] > 1)
3596 return \false;
3597 $i += 6;
3598 continue;
3599 }
3600 $key = ($branch['passthrough']) ? 'tail' : 'head';
3601 $branch[$key] .= (\is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i];
3602 if ($tokens[$i] === '{')
3603 {
3604 ++$braces;
3605 continue;
3606 }
3607 if ($tokens[$i] === '}')
3608 {
3609 --$braces;
3610 if ($branch['braces'] === $braces)
3611 {
3612 $branch[$key] = \substr($branch[$key], 0, -1);
3613 $branch =& $branch['parent'];
3614 $j = $i;
3615 while ($tokens[++$j][0] === \T_WHITESPACE);
3616 if ($tokens[$j][0] !== \T_ELSEIF
3617 && $tokens[$j][0] !== \T_ELSE)
3618 {
3619 $passthroughs = self::getBranchesPassthrough($branch['branches']);
3620 if ($passthroughs === array(0))
3621 {
3622 foreach ($branch['branches'] as $child)
3623 $branch['head'] .= $child['statement'] . '{' . $child['head'] . '}';
3624 $branch['branches'] = array();
3625 continue;
3626 }
3627 if ($passthroughs === array(1))
3628 {
3629 ++$branch['passthrough'];
3630 continue;
3631 }
3632 return \false;
3633 }
3634 }
3635 continue;
3636 }
3637 if ($branch['passthrough'])
3638 continue;
3639 if ($tokens[$i][0] === \T_IF
3640 || $tokens[$i][0] === \T_ELSEIF
3641 || $tokens[$i][0] === \T_ELSE)
3642 {
3643 $branch[$key] = \substr($branch[$key], 0, -\strlen($tokens[$i][1]));
3644 $branch['branches'][] = array(
3645 'braces' => $braces,
3646 'branches' => array(),
3647 'head' => '',
3648 'parent' => &$branch,
3649 'passthrough' => 0,
3650 'statement' => '',
3651 'tail' => ''
3652 );
3653 $branch =& $branch['branches'][\count($branch['branches']) - 1];
3654 do
3655 {
3656 $branch['statement'] .= (\is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i];
3657 }
3658 while ($tokens[++$i] !== '{');
3659 ++$braces;
3660 }
3661 }
3662 while (++$i < $cnt);
3663 list($head, $tail) = self::buildPHP($branch['branches']);
3664 $head = $branch['head'] . $head;
3665 $tail .= $branch['tail'];
3666 self::convertPHP($head, $tail, (bool) $branch['passthrough']);
3667 if (\preg_match('((?<!-)->(?!params\\[))', $head . $tail))
3668 return \false;
3669 return ($branch['passthrough']) ? array($head, $tail) : array($head);
3670 }
3671 protected static function convertPHP(&$head, &$tail, $passthrough)
3672 {
3673 $saveAttributes = (bool) \preg_match('(\\$node->(?:get|has)Attribute)', $tail);
3674 \preg_match_all(
3675 "(\\\$node->getAttribute\\('([^']+)'\\))",
3676 \preg_replace_callback(
3677 '(if\\(\\$node->hasAttribute\\(([^\\)]+)[^}]+)',
3678 function ($m)
3679 {
3680 return \str_replace('$node->getAttribute(' . $m[1] . ')', '', $m[0]);
3681 },
3682 $head . $tail
3683 ),
3684 $matches
3685 );
3686 $attrNames = \array_unique($matches[1]);
3687 self::replacePHP($head);
3688 self::replacePHP($tail);
3689 if (!$passthrough)
3690 $head = \str_replace('$node->textContent', '$textContent', $head);
3691 if (!empty($attrNames))
3692 {
3693 \ksort($attrNames);
3694 $head = "\$attributes+=array('" . \implode("'=>null,'", $attrNames) . "'=>null);" . $head;
3695 }
3696 if ($saveAttributes)
3697 {
3698 if (\strpos($head, '$html') === \false)
3699 $head .= "\$html='';";
3700 $head .= 'self::$attributes[]=$attributes;';
3701 $tail = '$attributes=array_pop(self::$attributes);' . $tail;
3702 }
3703 }
3704 protected static function replacePHP(&$php)
3705 {
3706 if ($php === '')
3707 return;
3708 $php = \str_replace('$this->out', '$html', $php);
3709 $getAttribute = "\\\$node->getAttribute\\(('[^']+')\\)";
3710 $php = \preg_replace(
3711 '(htmlspecialchars\\(' . $getAttribute . ',' . \ENT_NOQUOTES . '\\))',
3712 "str_replace('"','\"',\$attributes[\$1])",
3713 $php
3714 );
3715 $php = \preg_replace(
3716 '(htmlspecialchars\\(' . $getAttribute . ',' . \ENT_COMPAT . '\\))',
3717 '$attributes[$1]',
3718 $php
3719 );
3720 $php = \preg_replace(
3721 '(htmlspecialchars\\(strtr\\(' . $getAttribute . ",('[^\"&\\\\';<>aglmopqtu]+'),('[^\"&\\\\'<>]+')\\)," . \ENT_COMPAT . '\\))',
3722 'strtr($attributes[$1],$2,$3)',
3723 $php
3724 );
3725 $php = \preg_replace(
3726 '(' . $getAttribute . '(!?=+)' . $getAttribute . ')',
3727 '$attributes[$1]$2$attributes[$3]',
3728 $php
3729 );
3730 $php = \preg_replace_callback(
3731 '(' . $getAttribute . "===('.*?(?<!\\\\)(?:\\\\\\\\)*'))s",
3732 function ($m)
3733 {
3734 return '$attributes[' . $m[1] . ']===' . \htmlspecialchars($m[2], \ENT_COMPAT);
3735 },
3736 $php
3737 );
3738 $php = \preg_replace_callback(
3739 "(('.*?(?<!\\\\)(?:\\\\\\\\)*')===" . $getAttribute . ')s',
3740 function ($m)
3741 {
3742 return \htmlspecialchars($m[1], \ENT_COMPAT) . '===$attributes[' . $m[2] . ']';
3743 },
3744 $php
3745 );
3746 $php = \preg_replace_callback(
3747 '(strpos\\(' . $getAttribute . ",('.*?(?<!\\\\)(?:\\\\\\\\)*')\\)([!=]==(?:0|false)))s",
3748 function ($m)
3749 {
3750 return 'strpos($attributes[' . $m[1] . "]," . \htmlspecialchars($m[2], \ENT_COMPAT) . ')' . $m[3];
3751 },
3752 $php
3753 );
3754 $php = \preg_replace_callback(
3755 "(strpos\\(('.*?(?<!\\\\)(?:\\\\\\\\)*')," . $getAttribute . '\\)([!=]==(?:0|false)))s',
3756 function ($m)
3757 {
3758 return 'strpos(' . \htmlspecialchars($m[1], \ENT_COMPAT) . ',$attributes[' . $m[2] . '])' . $m[3];
3759 },
3760 $php
3761 );
3762 $php = \preg_replace(
3763 '(' . $getAttribute . '(?=(?:==|[-+*])\\d+))',
3764 '$attributes[$1]',
3765 $php
3766 );
3767 $php = \preg_replace(
3768 '((?<!\\w)(\\d+(?:==|[-+*]))' . $getAttribute . ')',
3769 '$1$attributes[$2]',
3770 $php
3771 );
3772 $php = \preg_replace(
3773 "(empty\\(\\\$node->getAttribute\\(('[^']+')\\)\\))",
3774 'empty($attributes[$1])',
3775 $php
3776 );
3777 $php = \preg_replace(
3778 "(\\\$node->hasAttribute\\(('[^']+')\\))",
3779 'isset($attributes[$1])',
3780 $php
3781 );
3782 $php = \preg_replace(
3783 "(\\\$node->getAttribute\\(('[^']+')\\))",
3784 'htmlspecialchars_decode($attributes[$1])',
3785 $php
3786 );
3787 if (\substr($php, 0, 7) === '$html.=')
3788 $php = '$html=' . \substr($php, 7);
3789 else
3790 $php = "\$html='';" . $php;
3791 }
3792 protected static function buildPHP(array $branches)
3793 {
3794 $return = array('', '');
3795 foreach ($branches as $branch)
3796 {
3797 $return[0] .= $branch['statement'] . '{' . $branch['head'];
3798 $return[1] .= $branch['statement'] . '{';
3799 if ($branch['branches'])
3800 {
3801 list($head, $tail) = self::buildPHP($branch['branches']);
3802 $return[0] .= $head;
3803 $return[1] .= $tail;
3804 }
3805 $return[0] .= '}';
3806 $return[1] .= $branch['tail'] . '}';
3807 }
3808 return $return;
3809 }
3810 protected static function getBranchesPassthrough(array $branches)
3811 {
3812 $values = array();
3813 foreach ($branches as $branch)
3814 $values[] = $branch['passthrough'];
3815 if ($branch['statement'] !== 'else')
3816 $values[] = 0;
3817 return \array_unique($values);
3818 }
3819 protected static function getDynamicRendering($php)
3820 {
3821 $rendering = '';
3822 $literal = "(?<literal>'((?>[^'\\\\]+|\\\\['\\\\])*)')";
3823 $attribute = "(?<attribute>htmlspecialchars\\(\\\$node->getAttribute\\('([^']+)'\\),2\\))";
3824 $value = "(?<value>$literal|$attribute)";
3825 $output = "(?<output>\\\$this->out\\.=$value(?:\\.(?&value))*;)";
3826 $copyOfAttribute = "(?<copyOfAttribute>if\\(\\\$node->hasAttribute\\('([^']+)'\\)\\)\\{\\\$this->out\\.=' \\g-1=\"'\\.htmlspecialchars\\(\\\$node->getAttribute\\('\\g-1'\\),2\\)\\.'\"';\\})";
3827 $regexp = '(^(' . $output . '|' . $copyOfAttribute . ')*$)';
3828 if (!\preg_match($regexp, $php, $m))
3829 return \false;
3830 $copiedAttributes = array();
3831 $usedAttributes = array();
3832 $regexp = '(' . $output . '|' . $copyOfAttribute . ')A';
3833 $offset = 0;
3834 while (\preg_match($regexp, $php, $m, 0, $offset))
3835 if ($m['output'])
3836 {
3837 $offset += 12;
3838 while (\preg_match('(' . $value . ')A', $php, $m, 0, $offset))
3839 {
3840 if ($m['literal'])
3841 {
3842 $str = \stripslashes(\substr($m[0], 1, -1));
3843 $rendering .= \preg_replace('([\\\\$](?=\\d))', '\\\\$0', $str);
3844 }
3845 else
3846 {
3847 $attrName = \end($m);
3848 if (!isset($usedAttributes[$attrName]))
3849 $usedAttributes[$attrName] = \uniqid($attrName, \true);
3850 $rendering .= $usedAttributes[$attrName];
3851 }
3852 $offset += 1 + \strlen($m[0]);
3853 }
3854 }
3855 else
3856 {
3857 $attrName = \end($m);
3858 if (!isset($copiedAttributes[$attrName]))
3859 $copiedAttributes[$attrName] = \uniqid($attrName, \true);
3860 $rendering .= $copiedAttributes[$attrName];
3861 $offset += \strlen($m[0]);
3862 }
3863 $attrNames = \array_keys($copiedAttributes + $usedAttributes);
3864 \sort($attrNames);
3865 $remainingAttributes = \array_combine($attrNames, $attrNames);
3866 $regexp = '(^[^ ]+';
3867 $index = 0;
3868 foreach ($attrNames as $attrName)
3869 {
3870 $regexp .= '(?> (?!' . RegexpBuilder::fromList($remainingAttributes) . '=)[^=]+="[^"]*")*';
3871 unset($remainingAttributes[$attrName]);
3872 $regexp .= '(';
3873 if (isset($copiedAttributes[$attrName]))
3874 self::replacePlaceholder($rendering, $copiedAttributes[$attrName], ++$index);
3875 else
3876 $regexp .= '?>';
3877 $regexp .= ' ' . $attrName . '="';
3878 if (isset($usedAttributes[$attrName]))
3879 {
3880 $regexp .= '(';
3881 self::replacePlaceholder($rendering, $usedAttributes[$attrName], ++$index);
3882 }
3883 $regexp .= '[^"]*';
3884 if (isset($usedAttributes[$attrName]))
3885 $regexp .= ')';
3886 $regexp .= '")?';
3887 }
3888 $regexp .= '.*)s';
3889 return array($regexp, $rendering);
3890 }
3891 protected static function getStaticRendering($php)
3892 {
3893 if ($php === '')
3894 return '';
3895 $regexp = "(^\\\$this->out\.='((?>[^'\\\\]+|\\\\['\\\\])*)';\$)";
3896 if (!\preg_match($regexp, $php, $m))
3897 return \false;
3898 return \stripslashes($m[1]);
3899 }
3900 protected static function replacePlaceholder(&$str, $uniqid, $index)
3901 {
3902 $str = \preg_replace_callback(
3903 '(' . \preg_quote($uniqid) . '(.))',
3904 function ($m) use ($index)
3905 {
3906 if (\is_numeric($m[1]))
3907 return '${' . $index . '}' . $m[1];
3908 else
3909 return '$' . $index . $m[1];
3910 },
3911 $str
3912 );
3913 }
3914 public static function generateConditionals($expr, array $statements)
3915 {
3916 $keys = \array_keys($statements);
3917 $cnt = \count($statements);
3918 $min = (int) $keys[0];
3919 $max = (int) $keys[$cnt - 1];
3920 if ($cnt <= 4)
3921 {
3922 if ($cnt === 1)
3923 return \end($statements);
3924 $php = '';
3925 $k = $min;
3926 do
3927 {
3928 $php .= 'if(' . $expr . '===' . $k . '){' . $statements[$k] . '}else';
3929 }
3930 while (++$k < $max);
3931 $php .= '{' . $statements[$max] . '}';
3932
3933 return $php;
3934 }
3935 $cutoff = \ceil($cnt / 2);
3936 $chunks = \array_chunk($statements, $cutoff, \true);
3937 return 'if(' . $expr . '<' . \key($chunks[1]) . '){' . self::generateConditionals($expr, \array_slice($statements, 0, $cutoff, \true)) . '}else' . self::generateConditionals($expr, \array_slice($statements, $cutoff, \null, \true));
3938 }
3939 public static function generateBranchTable($expr, array $statements)
3940 {
3941 $branchTable = array();
3942 $branchIds = array();
3943 \ksort($statements);
3944 foreach ($statements as $value => $statement)
3945 {
3946 if (!isset($branchIds[$statement]))
3947 $branchIds[$statement] = \count($branchIds);
3948 $branchTable[$value] = $branchIds[$statement];
3949 }
3950 return array($branchTable, self::generateConditionals($expr, \array_keys($branchIds)));
3951 }
3952 }
3953
3954 /*
3955 * @package s9e\TextFormatter
3956 * @copyright Copyright (c) 2010-2016 The s9e Authors
3957 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
3958 */
3959 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
3960 use DOMElement;
3961 use DOMXPath;
3962 use RuntimeException;
3963 use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
3964 use s9e\TextFormatter\Configurator\Helpers\TemplateParser;
3965 class Serializer
3966 {
3967 public $branchTableThreshold = 8;
3968 public $branchTables = array();
3969 public $convertor;
3970 public $useMultibyteStringFunctions = \false;
3971 public function __construct()
3972 {
3973 $this->convertor = new XPathConvertor;
3974 }
3975 protected function convertAttributeValueTemplate($attrValue)
3976 {
3977 $phpExpressions = array();
3978 foreach (AVTHelper::parse($attrValue) as $token)
3979 if ($token[0] === 'literal')
3980 $phpExpressions[] = \var_export($token[1], \true);
3981 else
3982 $phpExpressions[] = $this->convertXPath($token[1]);
3983 return \implode('.', $phpExpressions);
3984 }
3985 public function convertCondition($expr)
3986 {
3987 $this->convertor->useMultibyteStringFunctions = $this->useMultibyteStringFunctions;
3988 return $this->convertor->convertCondition($expr);
3989 }
3990 public function convertXPath($expr)
3991 {
3992 $this->convertor->useMultibyteStringFunctions = $this->useMultibyteStringFunctions;
3993 return $this->convertor->convertXPath($expr);
3994 }
3995 protected function escapeLiteral($text, $context)
3996 {
3997 if ($context === 'raw')
3998 return $text;
3999 $escapeMode = ($context === 'attribute') ? \ENT_COMPAT : \ENT_NOQUOTES;
4000 return \htmlspecialchars($text, $escapeMode);
4001 }
4002 protected function escapePHPOutput($php, $context)
4003 {
4004 if ($context === 'raw')
4005 return $php;
4006 $escapeMode = ($context === 'attribute') ? \ENT_COMPAT : \ENT_NOQUOTES;
4007 return 'htmlspecialchars(' . $php . ',' . $escapeMode . ')';
4008 }
4009 protected function serializeApplyTemplates(DOMElement $applyTemplates)
4010 {
4011 $php = '$this->at($node';
4012 if ($applyTemplates->hasAttribute('select'))
4013 $php .= ',' . \var_export($applyTemplates->getAttribute('select'), \true);
4014 $php .= ');';
4015 return $php;
4016 }
4017 protected function serializeAttribute(DOMElement $attribute)
4018 {
4019 $attrName = $attribute->getAttribute('name');
4020 $phpAttrName = $this->convertAttributeValueTemplate($attrName);
4021 $phpAttrName = 'htmlspecialchars(' . $phpAttrName . ',' . \ENT_QUOTES . ')';
4022 return "\$this->out.=' '." . $phpAttrName . ".'=\"';"
4023 . $this->serializeChildren($attribute)
4024 . "\$this->out.='\"';";
4025 }
4026 public function serialize(DOMElement $ir)
4027 {
4028 $this->branchTables = array();
4029 return $this->serializeChildren($ir);
4030 }
4031 protected function serializeChildren(DOMElement $ir)
4032 {
4033 $php = '';
4034 foreach ($ir->childNodes as $node)
4035 {
4036 $methodName = 'serialize' . \ucfirst($node->localName);
4037 $php .= $this->$methodName($node);
4038 }
4039 return $php;
4040 }
4041 protected function serializeCloseTag(DOMElement $closeTag)
4042 {
4043 $php = '';
4044 $id = $closeTag->getAttribute('id');
4045 if ($closeTag->hasAttribute('check'))
4046 $php .= 'if(!isset($t' . $id . ')){';
4047 if ($closeTag->hasAttribute('set'))
4048 $php .= '$t' . $id . '=1;';
4049 $xpath = new DOMXPath($closeTag->ownerDocument);
4050 $element = $xpath->query('ancestor::element[@id="' . $id . '"]', $closeTag)->item(0);
4051 if (!($element instanceof DOMElement))
4052 throw new RuntimeException;
4053 $php .= "\$this->out.='>';";
4054 if ($element->getAttribute('void') === 'maybe')
4055 $php .= 'if(!$v' . $id . '){';
4056 if ($closeTag->hasAttribute('check'))
4057 $php .= '}';
4058 return $php;
4059 }
4060 protected function serializeComment(DOMElement $comment)
4061 {
4062 return "\$this->out.='<!--';"
4063 . $this->serializeChildren($comment)
4064 . "\$this->out.='-->';";
4065 }
4066 protected function serializeCopyOfAttributes(DOMElement $copyOfAttributes)
4067 {
4068 return 'foreach($node->attributes as $attribute){'
4069 . "\$this->out.=' ';\$this->out.=\$attribute->name;\$this->out.='=\"';\$this->out.=htmlspecialchars(\$attribute->value," . \ENT_COMPAT . ");\$this->out.='\"';"
4070 . '}';
4071 }
4072 protected function serializeElement(DOMElement $element)
4073 {
4074 $php = '';
4075 $elName = $element->getAttribute('name');
4076 $id = $element->getAttribute('id');
4077 $isVoid = $element->getAttribute('void');
4078 $isDynamic = (bool) (\strpos($elName, '{') !== \false);
4079 $phpElName = $this->convertAttributeValueTemplate($elName);
4080 $phpElName = 'htmlspecialchars(' . $phpElName . ',' . \ENT_QUOTES . ')';
4081 if ($isDynamic)
4082 {
4083 $varName = '$e' . $id;
4084 $php .= $varName . '=' . $phpElName . ';';
4085 $phpElName = $varName;
4086 }
4087 if ($isVoid === 'maybe')
4088 $php .= '$v' . $id . '=preg_match(' . \var_export(TemplateParser::$voidRegexp, \true) . ',' . $phpElName . ');';
4089 $php .= "\$this->out.='<'." . $phpElName . ';';
4090 $php .= $this->serializeChildren($element);
4091 if ($isVoid !== 'yes')
4092 $php .= "\$this->out.='</'." . $phpElName . ".'>';";
4093 if ($isVoid === 'maybe')
4094 $php .= '}';
4095 return $php;
4096 }
4097 protected function serializeHash(DOMElement $switch)
4098 {
4099 $statements = array();
4100 foreach ($switch->getElementsByTagName('case') as $case)
4101 {
4102 if (!$case->parentNode->isSameNode($switch))
4103 continue;
4104 if ($case->hasAttribute('branch-values'))
4105 {
4106 $php = $this->serializeChildren($case);
4107 foreach (\unserialize($case->getAttribute('branch-values')) as $value)
4108 $statements[$value] = $php;
4109 }
4110 }
4111 if (!isset($case))
4112 throw new RuntimeException;
4113 list($branchTable, $php) = Quick::generateBranchTable('$n', $statements);
4114 $varName = 'bt' . \sprintf('%08X', \crc32(\serialize($branchTable)));
4115 $expr = 'self::$' . $varName . '[' . $this->convertXPath($switch->getAttribute('branch-key')) . ']';
4116 $php = 'if(isset(' . $expr . ')){$n=' . $expr . ';' . $php . '}';
4117 if (!$case->hasAttribute('branch-values'))
4118 $php .= 'else{' . $this->serializeChildren($case) . '}';
4119 $this->branchTables[$varName] = $branchTable;
4120 return $php;
4121 }
4122 protected function serializeOutput(DOMElement $output)
4123 {
4124 $context = $output->getAttribute('escape');
4125 $php = '$this->out.=';
4126 if ($output->getAttribute('type') === 'xpath')
4127 $php .= $this->escapePHPOutput($this->convertXPath($output->textContent), $context);
4128 else
4129 $php .= \var_export($this->escapeLiteral($output->textContent, $context), \true);
4130 $php .= ';';
4131 return $php;
4132 }
4133 protected function serializeSwitch(DOMElement $switch)
4134 {
4135 if ($switch->hasAttribute('branch-key')
4136 && $switch->childNodes->length >= $this->branchTableThreshold)
4137 return $this->serializeHash($switch);
4138 $php = '';
4139 $else = '';
4140 foreach ($switch->getElementsByTagName('case') as $case)
4141 {
4142 if (!$case->parentNode->isSameNode($switch))
4143 continue;
4144 if ($case->hasAttribute('test'))
4145 $php .= $else . 'if(' . $this->convertCondition($case->getAttribute('test')) . ')';
4146 else
4147 $php .= 'else';
4148 $else = 'else';
4149 $php .= '{';
4150 $php .= $this->serializeChildren($case);
4151 $php .= '}';
4152 }
4153 return $php;
4154 }
4155 }
4156
4157 /*
4158 * @package s9e\TextFormatter
4159 * @copyright Copyright (c) 2010-2016 The s9e Authors
4160 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
4161 */
4162 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
4163 use LogicException;
4164 use RuntimeException;
4165 class XPathConvertor
4166 {
4167 public $pcreVersion;
4168 protected $regexp;
4169 public $useMultibyteStringFunctions = \false;
4170 public function __construct()
4171 {
4172 $this->pcreVersion = \PCRE_VERSION;
4173 }
4174 public function convertCondition($expr)
4175 {
4176 $expr = \trim($expr);
4177 if (\preg_match('#^@([-\\w]+)$#', $expr, $m))
4178 return '$node->hasAttribute(' . \var_export($m[1], \true) . ')';
4179 if (\preg_match('#^not\\(@([-\\w]+)\\)$#', $expr, $m))
4180 return '!$node->hasAttribute(' . \var_export($m[1], \true) . ')';
4181 if (\preg_match('#^\\$(\\w+)$#', $expr, $m))
4182 return '!empty($this->params[' . \var_export($m[1], \true) . '])';
4183 if (\preg_match('#^not\\(\\$(\\w+)\\)$#', $expr, $m))
4184 return 'empty($this->params[' . \var_export($m[1], \true) . '])';
4185 if (\preg_match('#^([$@][-\\w]+)\\s*([<>])\\s*(\\d+)$#', $expr, $m))
4186 return $this->convertXPath($m[1]) . $m[2] . $m[3];
4187 if (!\preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr))
4188 $expr = 'boolean(' . $expr . ')';
4189 return $this->convertXPath($expr);
4190 }
4191 public function convertXPath($expr)
4192 {
4193 $expr = \trim($expr);
4194 $this->generateXPathRegexp();
4195 if (\preg_match($this->regexp, $expr, $m))
4196 {
4197 $methodName = \null;
4198 foreach ($m as $k => $v)
4199 {
4200 if (\is_numeric($k) || $v === '' || !\method_exists($this, $k))
4201 continue;
4202 $methodName = $k;
4203 break;
4204 }
4205 if (isset($methodName))
4206 {
4207 $args = array($m[$methodName]);
4208 $i = 0;
4209 while (isset($m[$methodName . $i]))
4210 {
4211 $args[$i] = $m[$methodName . $i];
4212 ++$i;
4213 }
4214 return \call_user_func_array(array($this, $methodName), $args);
4215 }
4216 }
4217 if (!\preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr))
4218 $expr = 'string(' . $expr . ')';
4219 return '$this->xpath->evaluate(' . $this->exportXPath($expr) . ',$node)';
4220 }
4221 protected function attr($attrName)
4222 {
4223 return '$node->getAttribute(' . \var_export($attrName, \true) . ')';
4224 }
4225 protected function dot()
4226 {
4227 return '$node->textContent';
4228 }
4229 protected function param($paramName)
4230 {
4231 return '$this->params[' . \var_export($paramName, \true) . ']';
4232 }
4233 protected function string($string)
4234 {
4235 return \var_export(\substr($string, 1, -1), \true);
4236 }
4237 protected function lname()
4238 {
4239 return '$node->localName';
4240 }
4241 protected function name()
4242 {
4243 return '$node->nodeName';
4244 }
4245 protected function number($number)
4246 {
4247 return "'" . $number . "'";
4248 }
4249 protected function strlen($expr)
4250 {
4251 if ($expr === '')
4252 $expr = '.';
4253 $php = $this->convertXPath($expr);
4254 return ($this->useMultibyteStringFunctions)
4255 ? 'mb_strlen(' . $php . ",'utf-8')"
4256 : "strlen(preg_replace('(.)us','.'," . $php . '))';
4257 }
4258 protected function contains($haystack, $needle)
4259 {
4260 return '(strpos(' . $this->convertXPath($haystack) . ',' . $this->convertXPath($needle) . ')!==false)';
4261 }
4262 protected function startswith($string, $substring)
4263 {
4264 return '(strpos(' . $this->convertXPath($string) . ',' . $this->convertXPath($substring) . ')===0)';
4265 }
4266 protected function not($expr)
4267 {
4268 return '!(' . $this->convertCondition($expr) . ')';
4269 }
4270 protected function notcontains($haystack, $needle)
4271 {
4272 return '(strpos(' . $this->convertXPath($haystack) . ',' . $this->convertXPath($needle) . ')===false)';
4273 }
4274 protected function substr($exprString, $exprPos, $exprLen = \null)
4275 {
4276 if (!$this->useMultibyteStringFunctions)
4277 {
4278 $expr = 'substring(' . $exprString . ',' . $exprPos;
4279 if (isset($exprLen))
4280 $expr .= ',' . $exprLen;
4281 $expr .= ')';
4282 return '$this->xpath->evaluate(' . $this->exportXPath($expr) . ',$node)';
4283 }
4284 $php = 'mb_substr(' . $this->convertXPath($exprString) . ',';
4285 if (\is_numeric($exprPos))
4286 $php .= \max(0, $exprPos - 1);
4287 else
4288 $php .= 'max(0,' . $this->convertXPath($exprPos) . '-1)';
4289 $php .= ',';
4290 if (isset($exprLen))
4291 if (\is_numeric($exprLen))
4292 if (\is_numeric($exprPos) && $exprPos < 1)
4293 $php .= \max(0, $exprPos + $exprLen - 1);
4294 else
4295 $php .= \max(0, $exprLen);
4296 else
4297 $php .= 'max(0,' . $this->convertXPath($exprLen) . ')';
4298 else
4299 $php .= 0x7fffffe;
4300 $php .= ",'utf-8')";
4301 return $php;
4302 }
4303 protected function substringafter($expr, $str)
4304 {
4305 return 'substr(strstr(' . $this->convertXPath($expr) . ',' . $this->convertXPath($str) . '),' . (\strlen($str) - 2) . ')';
4306 }
4307 protected function substringbefore($expr1, $expr2)
4308 {
4309 return 'strstr(' . $this->convertXPath($expr1) . ',' . $this->convertXPath($expr2) . ',true)';
4310 }
4311 protected function cmp($expr1, $operator, $expr2)
4312 {
4313 $operands = array();
4314 $operators = array(
4315 '=' => '===',
4316 '!=' => '!==',
4317 '>' => '>',
4318 '>=' => '>=',
4319 '<' => '<',
4320 '<=' => '<='
4321 );
4322 foreach (array($expr1, $expr2) as $expr)
4323 if (\is_numeric($expr))
4324 {
4325 $operators['='] = '==';
4326 $operators['!='] = '!=';
4327 $operands[] = \preg_replace('(^0(.+))', '$1', $expr);
4328 }
4329 else
4330 $operands[] = $this->convertXPath($expr);
4331 return \implode($operators[$operator], $operands);
4332 }
4333 protected function bool($expr1, $operator, $expr2)
4334 {
4335 $operators = array(
4336 'and' => '&&',
4337 'or' => '||'
4338 );
4339 return $this->convertCondition($expr1) . $operators[$operator] . $this->convertCondition($expr2);
4340 }
4341 protected function parens($expr)
4342 {
4343 return '(' . $this->convertXPath($expr) . ')';
4344 }
4345 protected function translate($str, $from, $to)
4346 {
4347 \preg_match_all('(.)su', \substr($from, 1, -1), $matches);
4348 $from = $matches[0];
4349 \preg_match_all('(.)su', \substr($to, 1, -1), $matches);
4350 $to = $matches[0];
4351 if (\count($to) > \count($from))
4352 $to = \array_slice($to, 0, \count($from));
4353 else
4354 while (\count($from) > \count($to))
4355 $to[] = '';
4356 $from = \array_unique($from);
4357 $to = \array_intersect_key($to, $from);
4358 $php = 'strtr(' . $this->convertXPath($str) . ',';
4359 if (array(1) === \array_unique(\array_map('strlen', $from))
4360 && array(1) === \array_unique(\array_map('strlen', $to)))
4361 $php .= \var_export(\implode('', $from), \true) . ',' . \var_export(\implode('', $to), \true);
4362 else
4363 {
4364 $php .= 'array(';
4365 $cnt = \count($from);
4366 for ($i = 0; $i < $cnt; ++$i)
4367 {
4368 if ($i)
4369 $php .= ',';
4370 $php .= \var_export($from[$i], \true) . '=>' . \var_export($to[$i], \true);
4371 }
4372 $php .= ')';
4373 }
4374 $php .= ')';
4375 return $php;
4376 }
4377 protected function math($expr1, $operator, $expr2)
4378 {
4379 if (!\is_numeric($expr1))
4380 $expr1 = $this->convertXPath($expr1);
4381 if (!\is_numeric($expr2))
4382 $expr2 = $this->convertXPath($expr2);
4383 if ($operator === 'div')
4384 $operator = '/';
4385 return $expr1 . $operator . $expr2;
4386 }
4387 protected function exportXPath($expr)
4388 {
4389 $phpTokens = array();
4390 $pos = 0;
4391 $len = \strlen($expr);
4392 while ($pos < $len)
4393 {
4394 if ($expr[$pos] === "'" || $expr[$pos] === '"')
4395 {
4396 $nextPos = \strpos($expr, $expr[$pos], 1 + $pos);
4397 if ($nextPos === \false)
4398 throw new RuntimeException('Unterminated string literal in XPath expression ' . \var_export($expr, \true));
4399 $phpTokens[] = \var_export(\substr($expr, $pos, $nextPos + 1 - $pos), \true);
4400 $pos = $nextPos + 1;
4401 continue;
4402 }
4403 if ($expr[$pos] === '$' && \preg_match('/\\$(\\w+)/', $expr, $m, 0, $pos))
4404 {
4405 $phpTokens[] = '$this->getParamAsXPath(' . \var_export($m[1], \true) . ')';
4406 $pos += \strlen($m[0]);
4407 continue;
4408 }
4409 $spn = \strcspn($expr, '\'"$', $pos);
4410 if ($spn)
4411 {
4412 $phpTokens[] = \var_export(\substr($expr, $pos, $spn), \true);
4413 $pos += $spn;
4414 }
4415 }
4416 return \implode('.', $phpTokens);
4417 }
4418 protected function generateXPathRegexp()
4419 {
4420 if (isset($this->regexp))
4421 return;
4422 $patterns = array(
4423 'attr' => array('@', '(?<attr0>[-\\w]+)'),
4424 'dot' => '\\.',
4425 'name' => 'name\\(\\)',
4426 'lname' => 'local-name\\(\\)',
4427 'param' => array('\\$', '(?<param0>\\w+)'),
4428 'string' => '"[^"]*"|\'[^\']*\'',
4429 'number' => array('-?', '\\d++'),
4430 'strlen' => array('string-length', '\\(', '(?<strlen0>(?&value)?)', '\\)'),
4431 'contains' => array(
4432 'contains',
4433 '\\(',
4434 '(?<contains0>(?&value))',
4435 ',',
4436 '(?<contains1>(?&value))',
4437 '\\)'
4438 ),
4439 'translate' => array(
4440 'translate',
4441 '\\(',
4442 '(?<translate0>(?&value))',
4443 ',',
4444 '(?<translate1>(?&string))',
4445 ',',
4446 '(?<translate2>(?&string))',
4447 '\\)'
4448 ),
4449 'substr' => array(
4450 'substring',
4451 '\\(',
4452 '(?<substr0>(?&value))',
4453 ',',
4454 '(?<substr1>(?&value))',
4455 '(?:, (?<substr2>(?&value)))?',
4456 '\\)'
4457 ),
4458 'substringafter' => array(
4459 'substring-after',
4460 '\\(',
4461 '(?<substringafter0>(?&value))',
4462 ',',
4463 '(?<substringafter1>(?&string))',
4464 '\\)'
4465 ),
4466 'substringbefore' => array(
4467 'substring-before',
4468 '\\(',
4469 '(?<substringbefore0>(?&value))',
4470 ',',
4471 '(?<substringbefore1>(?&value))',
4472 '\\)'
4473 ),
4474 'startswith' => array(
4475 'starts-with',
4476 '\\(',
4477 '(?<startswith0>(?&value))',
4478 ',',
4479 '(?<startswith1>(?&value))',
4480 '\\)'
4481 ),
4482 'math' => array(
4483 '(?<math0>(?&attr)|(?&number)|(?¶m))',
4484 '(?<math1>[-+*]|div)',
4485 '(?<math2>(?&math)|(?&math0))'
4486 ),
4487 'notcontains' => array(
4488 'not',
4489 '\\(',
4490 'contains',
4491 '\\(',
4492 '(?<notcontains0>(?&value))',
4493 ',',
4494 '(?<notcontains1>(?&value))',
4495 '\\)',
4496 '\\)'
4497 )
4498 );
4499 $exprs = array();
4500 if (\version_compare($this->pcreVersion, '8.13', '>='))
4501 {
4502 $exprs[] = '(?<cmp>(?<cmp0>(?&value)) (?<cmp1>!?=) (?<cmp2>(?&value)))';
4503 $exprs[] = '(?<parens>\\( (?<parens0>(?&bool)|(?&cmp)|(?&math)) \\))';
4504 $exprs[] = '(?<bool>(?<bool0>(?&cmp)|(?¬)|(?&value)|(?&parens)) (?<bool1>and|or) (?<bool2>(?&bool)|(?&cmp)|(?¬)|(?&value)|(?&parens)))';
4505 $exprs[] = '(?<not>not \\( (?<not0>(?&bool)|(?&value)) \\))';
4506 $patterns['math'][0] = \str_replace('))', ')|(?&parens))', $patterns['math'][0]);
4507 $patterns['math'][1] = \str_replace('))', ')|(?&parens))', $patterns['math'][1]);
4508 }
4509 $valueExprs = array();
4510 foreach ($patterns as $name => $pattern)
4511 {
4512 if (\is_array($pattern))
4513 $pattern = \implode(' ', $pattern);
4514 if (\strpos($pattern, '?&') === \false || \version_compare($this->pcreVersion, '8.13', '>='))
4515 $valueExprs[] = '(?<' . $name . '>' . $pattern . ')';
4516 }
4517 \array_unshift($exprs, '(?<value>' . \implode('|', $valueExprs) . ')');
4518
4519 $regexp = '#^(?:' . \implode('|', $exprs) . ')$#S';
4520 $regexp = \str_replace(' ', '\\s*', $regexp);
4521 $this->regexp = $regexp;
4522 }
4523 }
4524
4525 /*
4526 * @package s9e\TextFormatter
4527 * @copyright Copyright (c) 2010-2016 The s9e Authors
4528 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
4529 */
4530 namespace s9e\TextFormatter\Configurator;
4531 use InvalidArgumentException;
4532 use ReflectionClass;
4533 use RuntimeException;
4534 use Traversable;
4535 use s9e\TextFormatter\Configurator;
4536 use s9e\TextFormatter\Configurator\Collections\Collection;
4537 use s9e\TextFormatter\Configurator\Collections\NormalizedCollection;
4538 use s9e\TextFormatter\Configurator\Collections\TemplateParameterCollection;
4539 use s9e\TextFormatter\Configurator\RendererGenerator;
4540 use s9e\TextFormatter\Configurator\Traits\Configurable;
4541 class Rendering
4542 {
4543 public function __get($propName)
4544 {
4545 $methodName = 'get' . \ucfirst($propName);
4546 if (\method_exists($this, $methodName))
4547 return $this->$methodName();
4548 if (!\property_exists($this, $propName))
4549 throw new RuntimeException("Property '" . $propName . "' does not exist");
4550 return $this->$propName;
4551 }
4552 public function __set($propName, $propValue)
4553 {
4554 $methodName = 'set' . \ucfirst($propName);
4555 if (\method_exists($this, $methodName))
4556 {
4557 $this->$methodName($propValue);
4558 return;
4559 }
4560 if (!isset($this->$propName))
4561 {
4562 $this->$propName = $propValue;
4563 return;
4564 }
4565 if ($this->$propName instanceof NormalizedCollection)
4566 {
4567 if (!\is_array($propValue)
4568 && !($propValue instanceof Traversable))
4569 throw new InvalidArgumentException("Property '" . $propName . "' expects an array or a traversable object to be passed");
4570 $this->$propName->clear();
4571 foreach ($propValue as $k => $v)
4572 $this->$propName->set($k, $v);
4573 return;
4574 }
4575 if (\is_object($this->$propName))
4576 {
4577 if (!($propValue instanceof $this->$propName))
4578 throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of class '" . \get_class($this->$propName) . "' with instance of '" . \get_class($propValue) . "'");
4579 }
4580 else
4581 {
4582 $oldType = \gettype($this->$propName);
4583 $newType = \gettype($propValue);
4584 if ($oldType === 'boolean')
4585 if ($propValue === 'false')
4586 {
4587 $newType = 'boolean';
4588 $propValue = \false;
4589 }
4590 elseif ($propValue === 'true')
4591 {
4592 $newType = 'boolean';
4593 $propValue = \true;
4594 }
4595 if ($oldType !== $newType)
4596 {
4597 $tmp = $propValue;
4598 \settype($tmp, $oldType);
4599 \settype($tmp, $newType);
4600 if ($tmp !== $propValue)
4601 throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of type " . $oldType . ' with value of type ' . $newType);
4602 \settype($propValue, $oldType);
4603 }
4604 }
4605 $this->$propName = $propValue;
4606 }
4607 public function __isset($propName)
4608 {
4609 $methodName = 'isset' . \ucfirst($propName);
4610 if (\method_exists($this, $methodName))
4611 return $this->$methodName();
4612 return isset($this->$propName);
4613 }
4614 public function __unset($propName)
4615 {
4616 $methodName = 'unset' . \ucfirst($propName);
4617 if (\method_exists($this, $methodName))
4618 {
4619 $this->$methodName();
4620 return;
4621 }
4622 if (!isset($this->$propName))
4623 return;
4624 if ($this->$propName instanceof Collection)
4625 {
4626 $this->$propName->clear();
4627 return;
4628 }
4629 throw new RuntimeException("Property '" . $propName . "' cannot be unset");
4630 }
4631 protected $configurator;
4632 protected $engine;
4633 protected $parameters;
4634 public function __construct(Configurator $configurator)
4635 {
4636 $this->configurator = $configurator;
4637 $this->parameters = new TemplateParameterCollection;
4638 }
4639 public function getAllParameters()
4640 {
4641 $params = array();
4642 foreach ($this->configurator->tags as $tag)
4643 if (isset($tag->template))
4644 foreach ($tag->template->getParameters() as $paramName)
4645 $params[$paramName] = '';
4646 $params = \iterator_to_array($this->parameters) + $params;
4647 \ksort($params);
4648 return $params;
4649 }
4650 public function getEngine()
4651 {
4652 if (!isset($this->engine))
4653 $this->setEngine('XSLT');
4654 return $this->engine;
4655 }
4656 public function getRenderer()
4657 {
4658 return $this->getEngine()->getRenderer($this);
4659 }
4660 public function getTemplates()
4661 {
4662 $templates = array(
4663 'br' => '<br/>',
4664 'e' => '',
4665 'i' => '',
4666 'p' => '<p><xsl:apply-templates/></p>',
4667 's' => ''
4668 );
4669 foreach ($this->configurator->tags as $tagName => $tag)
4670 if (isset($tag->template))
4671 $templates[$tagName] = (string) $tag->template;
4672 \ksort($templates);
4673 return $templates;
4674 }
4675 public function setEngine($engine)
4676 {
4677 if (!($engine instanceof RendererGenerator))
4678 {
4679 $className = 's9e\\TextFormatter\\Configurator\\RendererGenerators\\' . $engine;
4680 $reflection = new ReflectionClass($className);
4681 $engine = (\func_num_args() > 1) ? $reflection->newInstanceArgs(\array_slice(\func_get_args(), 1)) : $reflection->newInstance();
4682 }
4683 $this->engine = $engine;
4684 return $engine;
4685 }
4686 }
4687
4688 /*
4689 * @package s9e\TextFormatter
4690 * @copyright Copyright (c) 2010-2016 The s9e Authors
4691 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
4692 */
4693 namespace s9e\TextFormatter\Configurator;
4694 use ArrayAccess;
4695 use DOMDocument;
4696 use Iterator;
4697 use s9e\TextFormatter\Configurator\Collections\RulesGeneratorList;
4698 use s9e\TextFormatter\Configurator\Collections\TagCollection;
4699 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
4700 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
4701 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator;
4702 use s9e\TextFormatter\Configurator\Traits\CollectionProxy;
4703 class RulesGenerator implements ArrayAccess, Iterator
4704 {
4705 public function __call($methodName, $args)
4706 {
4707 return \call_user_func_array(array($this->collection, $methodName), $args);
4708 }
4709 public function offsetExists($offset)
4710 {
4711 return isset($this->collection[$offset]);
4712 }
4713 public function offsetGet($offset)
4714 {
4715 return $this->collection[$offset];
4716 }
4717 public function offsetSet($offset, $value)
4718 {
4719 $this->collection[$offset] = $value;
4720 }
4721 public function offsetUnset($offset)
4722 {
4723 unset($this->collection[$offset]);
4724 }
4725 public function count()
4726 {
4727 return \count($this->collection);
4728 }
4729 public function current()
4730 {
4731 return $this->collection->current();
4732 }
4733 public function key()
4734 {
4735 return $this->collection->key();
4736 }
4737 public function next()
4738 {
4739 return $this->collection->next();
4740 }
4741 public function rewind()
4742 {
4743 $this->collection->rewind();
4744 }
4745 public function valid()
4746 {
4747 return $this->collection->valid();
4748 }
4749 protected $collection;
4750 public function __construct()
4751 {
4752 $this->collection = new RulesGeneratorList;
4753 $this->collection->append('AutoCloseIfVoid');
4754 $this->collection->append('AutoReopenFormattingElements');
4755 $this->collection->append('BlockElementsFosterFormattingElements');
4756 $this->collection->append('DisableAutoLineBreaksIfNewLinesArePreserved');
4757 $this->collection->append('EnforceContentModels');
4758 $this->collection->append('EnforceOptionalEndTags');
4759 $this->collection->append('IgnoreTagsInCode');
4760 $this->collection->append('IgnoreTextIfDisallowed');
4761 $this->collection->append('IgnoreWhitespaceAroundBlockElements');
4762 $this->collection->append('TrimFirstLineInCodeBlocks');
4763 }
4764 public function getRules(TagCollection $tags, array $options = array())
4765 {
4766 $parentHTML = (isset($options['parentHTML'])) ? $options['parentHTML'] : '<div>';
4767 $rootForensics = $this->generateRootForensics($parentHTML);
4768 $templateForensics = array();
4769 foreach ($tags as $tagName => $tag)
4770 {
4771 $template = (isset($tag->template)) ? $tag->template : '<xsl:apply-templates/>';
4772 $templateForensics[$tagName] = new TemplateForensics($template);
4773 }
4774 $rules = $this->generateRulesets($templateForensics, $rootForensics);
4775 unset($rules['root']['autoClose']);
4776 unset($rules['root']['autoReopen']);
4777 unset($rules['root']['breakParagraph']);
4778 unset($rules['root']['closeAncestor']);
4779 unset($rules['root']['closeParent']);
4780 unset($rules['root']['fosterParent']);
4781 unset($rules['root']['ignoreSurroundingWhitespace']);
4782 unset($rules['root']['isTransparent']);
4783 unset($rules['root']['requireAncestor']);
4784 unset($rules['root']['requireParent']);
4785 return $rules;
4786 }
4787 protected function generateRootForensics($html)
4788 {
4789 $dom = new DOMDocument;
4790 $dom->loadHTML($html);
4791 $body = $dom->getElementsByTagName('body')->item(0);
4792 $node = $body;
4793 while ($node->firstChild)
4794 $node = $node->firstChild;
4795 $node->appendChild($dom->createElementNS(
4796 'http://www.w3.org/1999/XSL/Transform',
4797 'xsl:apply-templates'
4798 ));
4799 return new TemplateForensics($dom->saveXML($body));
4800 }
4801 protected function generateRulesets(array $templateForensics, TemplateForensics $rootForensics)
4802 {
4803 $rules = array(
4804 'root' => $this->generateRuleset($rootForensics, $templateForensics),
4805 'tags' => array()
4806 );
4807 foreach ($templateForensics as $tagName => $src)
4808 $rules['tags'][$tagName] = $this->generateRuleset($src, $templateForensics);
4809 return $rules;
4810 }
4811 protected function generateRuleset(TemplateForensics $src, array $targets)
4812 {
4813 $rules = array();
4814 foreach ($this->collection as $rulesGenerator)
4815 {
4816 if ($rulesGenerator instanceof BooleanRulesGenerator)
4817 foreach ($rulesGenerator->generateBooleanRules($src) as $ruleName => $bool)
4818 $rules[$ruleName] = $bool;
4819 if ($rulesGenerator instanceof TargetedRulesGenerator)
4820 foreach ($targets as $tagName => $trg)
4821 foreach ($rulesGenerator->generateTargetedRules($src, $trg) as $ruleName)
4822 $rules[$ruleName][] = $tagName;
4823 }
4824 return $rules;
4825 }
4826 }
4827
4828 /*
4829 * @package s9e\TextFormatter
4830 * @copyright Copyright (c) 2010-2016 The s9e Authors
4831 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
4832 */
4833 namespace s9e\TextFormatter\Configurator\RulesGenerators\Interfaces;
4834 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
4835 interface BooleanRulesGenerator
4836 {
4837 public function generateBooleanRules(TemplateForensics $src);
4838 }
4839
4840 /*
4841 * @package s9e\TextFormatter
4842 * @copyright Copyright (c) 2010-2016 The s9e Authors
4843 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
4844 */
4845 namespace s9e\TextFormatter\Configurator\RulesGenerators\Interfaces;
4846 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
4847 interface TargetedRulesGenerator
4848 {
4849 public function generateTargetedRules(TemplateForensics $src, TemplateForensics $trg);
4850 }
4851
4852 /*
4853 * @package s9e\TextFormatter
4854 * @copyright Copyright (c) 2010-2016 The s9e Authors
4855 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
4856 */
4857 namespace s9e\TextFormatter\Configurator;
4858 use DOMElement;
4859 use s9e\TextFormatter\Configurator\Items\Tag;
4860 abstract class TemplateCheck
4861 {
4862 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
4863 abstract public function check(DOMElement $template, Tag $tag);
4864 }
4865
4866 /*
4867 * @package s9e\TextFormatter
4868 * @copyright Copyright (c) 2010-2016 The s9e Authors
4869 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
4870 */
4871 namespace s9e\TextFormatter\Configurator;
4872 use ArrayAccess;
4873 use Iterator;
4874 use s9e\TextFormatter\Configurator\Collections\TemplateCheckList;
4875 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
4876 use s9e\TextFormatter\Configurator\Items\Tag;
4877 use s9e\TextFormatter\Configurator\Items\UnsafeTemplate;
4878 use s9e\TextFormatter\Configurator\TemplateChecks\DisallowElementNS;
4879 use s9e\TextFormatter\Configurator\TemplateChecks\DisallowXPathFunction;
4880 use s9e\TextFormatter\Configurator\TemplateChecks\RestrictFlashScriptAccess;
4881 use s9e\TextFormatter\Configurator\Traits\CollectionProxy;
4882 class TemplateChecker implements ArrayAccess, Iterator
4883 {
4884 public function __call($methodName, $args)
4885 {
4886 return \call_user_func_array(array($this->collection, $methodName), $args);
4887 }
4888 public function offsetExists($offset)
4889 {
4890 return isset($this->collection[$offset]);
4891 }
4892 public function offsetGet($offset)
4893 {
4894 return $this->collection[$offset];
4895 }
4896 public function offsetSet($offset, $value)
4897 {
4898 $this->collection[$offset] = $value;
4899 }
4900 public function offsetUnset($offset)
4901 {
4902 unset($this->collection[$offset]);
4903 }
4904 public function count()
4905 {
4906 return \count($this->collection);
4907 }
4908 public function current()
4909 {
4910 return $this->collection->current();
4911 }
4912 public function key()
4913 {
4914 return $this->collection->key();
4915 }
4916 public function next()
4917 {
4918 return $this->collection->next();
4919 }
4920 public function rewind()
4921 {
4922 $this->collection->rewind();
4923 }
4924 public function valid()
4925 {
4926 return $this->collection->valid();
4927 }
4928 protected $collection;
4929 protected $disabled = \false;
4930 public function __construct()
4931 {
4932 $this->collection = new TemplateCheckList;
4933 $this->collection->append('DisallowAttributeSets');
4934 $this->collection->append('DisallowCopy');
4935 $this->collection->append('DisallowDisableOutputEscaping');
4936 $this->collection->append('DisallowDynamicAttributeNames');
4937 $this->collection->append('DisallowDynamicElementNames');
4938 $this->collection->append('DisallowObjectParamsWithGeneratedName');
4939 $this->collection->append('DisallowPHPTags');
4940 $this->collection->append('DisallowUnsafeCopyOf');
4941 $this->collection->append('DisallowUnsafeDynamicCSS');
4942 $this->collection->append('DisallowUnsafeDynamicJS');
4943 $this->collection->append('DisallowUnsafeDynamicURL');
4944 $this->collection->append(new DisallowElementNS('http://icl.com/saxon', 'output'));
4945 $this->collection->append(new DisallowXPathFunction('document'));
4946 $this->collection->append(new RestrictFlashScriptAccess('sameDomain', \true));
4947 }
4948 public function checkTag(Tag $tag)
4949 {
4950 if (isset($tag->template) && !($tag->template instanceof UnsafeTemplate))
4951 {
4952 $template = (string) $tag->template;
4953 $this->checkTemplate($template, $tag);
4954 }
4955 }
4956 public function checkTemplate($template, Tag $tag = \null)
4957 {
4958 if ($this->disabled)
4959 return;
4960 if (!isset($tag))
4961 $tag = new Tag;
4962 $dom = TemplateHelper::loadTemplate($template);
4963 foreach ($this->collection as $check)
4964 $check->check($dom->documentElement, $tag);
4965 }
4966 public function disable()
4967 {
4968 $this->disabled = \true;
4969 }
4970 public function enable()
4971 {
4972 $this->disabled = \false;
4973 }
4974 }
4975
4976 /*
4977 * @package s9e\TextFormatter
4978 * @copyright Copyright (c) 2010-2016 The s9e Authors
4979 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
4980 */
4981 namespace s9e\TextFormatter\Configurator;
4982 use DOMElement;
4983 abstract class TemplateNormalization
4984 {
4985 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
4986 public $onlyOnce = \false;
4987 abstract public function normalize(DOMElement $template);
4988 public static function lowercase($str)
4989 {
4990 return \strtr($str, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
4991 }
4992 }
4993
4994 /*
4995 * @package s9e\TextFormatter
4996 * @copyright Copyright (c) 2010-2016 The s9e Authors
4997 * @license http://www.opensource.org/licenses/mit-license'); The MIT License
4998 */
4999 namespace s9e\TextFormatter\Configurator;
5000 use ArrayAccess;
5001 use Iterator;
5002 use s9e\TextFormatter\Configurator\Collections\TemplateNormalizationList;
5003 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
5004 use s9e\TextFormatter\Configurator\Items\Tag;
5005 use s9e\TextFormatter\Configurator\Traits\CollectionProxy;
5006 class TemplateNormalizer implements ArrayAccess, Iterator
5007 {
5008 public function __call($methodName, $args)
5009 {
5010 return \call_user_func_array(array($this->collection, $methodName), $args);
5011 }
5012 public function offsetExists($offset)
5013 {
5014 return isset($this->collection[$offset]);
5015 }
5016 public function offsetGet($offset)
5017 {
5018 return $this->collection[$offset];
5019 }
5020 public function offsetSet($offset, $value)
5021 {
5022 $this->collection[$offset] = $value;
5023 }
5024 public function offsetUnset($offset)
5025 {
5026 unset($this->collection[$offset]);
5027 }
5028 public function count()
5029 {
5030 return \count($this->collection);
5031 }
5032 public function current()
5033 {
5034 return $this->collection->current();
5035 }
5036 public function key()
5037 {
5038 return $this->collection->key();
5039 }
5040 public function next()
5041 {
5042 return $this->collection->next();
5043 }
5044 public function rewind()
5045 {
5046 $this->collection->rewind();
5047 }
5048 public function valid()
5049 {
5050 return $this->collection->valid();
5051 }
5052 protected $collection;
5053 public function __construct()
5054 {
5055 $this->collection = new TemplateNormalizationList;
5056 $this->collection->append('PreserveSingleSpaces');
5057 $this->collection->append('RemoveComments');
5058 $this->collection->append('RemoveInterElementWhitespace');
5059 $this->collection->append('FixUnescapedCurlyBracesInHtmlAttributes');
5060 $this->collection->append('FoldArithmeticConstants');
5061 $this->collection->append('FoldConstantXPathExpressions');
5062 $this->collection->append('InlineAttributes');
5063 $this->collection->append('InlineCDATA');
5064 $this->collection->append('InlineElements');
5065 $this->collection->append('InlineInferredValues');
5066 $this->collection->append('InlineTextElements');
5067 $this->collection->append('InlineXPathLiterals');
5068 $this->collection->append('MinifyXPathExpressions');
5069 $this->collection->append('NormalizeAttributeNames');
5070 $this->collection->append('NormalizeElementNames');
5071 $this->collection->append('NormalizeUrls');
5072 $this->collection->append('OptimizeConditionalAttributes');
5073 $this->collection->append('OptimizeConditionalValueOf');
5074 $this->collection->append('OptimizeChoose');
5075 $this->collection->append('SetRelNoreferrerOnTargetedLinks');
5076 }
5077 public function normalizeTag(Tag $tag)
5078 {
5079 if (isset($tag->template) && !$tag->template->isNormalized())
5080 $tag->template->normalize($this);
5081 }
5082 public function normalizeTemplate($template)
5083 {
5084 $dom = TemplateHelper::loadTemplate($template);
5085 $applied = array();
5086 $loops = 5;
5087 do
5088 {
5089 $old = $template;
5090 foreach ($this->collection as $k => $normalization)
5091 {
5092 if (isset($applied[$k]) && !empty($normalization->onlyOnce))
5093 continue;
5094 $normalization->normalize($dom->documentElement);
5095 $applied[$k] = 1;
5096 }
5097 $template = TemplateHelper::saveTemplate($dom);
5098 }
5099 while (--$loops && $template !== $old);
5100 return $template;
5101 }
5102 }
5103
5104 /*
5105 * @package s9e\TextFormatter
5106 * @copyright Copyright (c) 2010-2016 The s9e Authors
5107 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
5108 */
5109 namespace s9e\TextFormatter\Configurator\Validators;
5110 use InvalidArgumentException;
5111 abstract class AttributeName
5112 {
5113 public static function isValid($name)
5114 {
5115 return (bool) \preg_match('#^(?!xmlns$)[a-z_][-a-z_0-9]*$#Di', $name);
5116 }
5117 public static function normalize($name)
5118 {
5119 if (!static::isValid($name))
5120 throw new InvalidArgumentException("Invalid attribute name '" . $name . "'");
5121 return \strtolower($name);
5122 }
5123 }
5124
5125 /*
5126 * @package s9e\TextFormatter
5127 * @copyright Copyright (c) 2010-2016 The s9e Authors
5128 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
5129 */
5130 namespace s9e\TextFormatter\Configurator\Validators;
5131 use InvalidArgumentException;
5132 abstract class TagName
5133 {
5134 public static function isValid($name)
5135 {
5136 return (bool) \preg_match('#^(?:(?!xmlns|xsl|s9e)[a-z_][a-z_0-9]*:)?[a-z_][-a-z_0-9]*$#Di', $name);
5137 }
5138 public static function normalize($name)
5139 {
5140 if (!static::isValid($name))
5141 throw new InvalidArgumentException("Invalid tag name '" . $name . "'");
5142 if (\strpos($name, ':') === \false)
5143 $name = \strtoupper($name);
5144 return $name;
5145 }
5146 }
5147
5148 /*
5149 * @package s9e\TextFormatter
5150 * @copyright Copyright (c) 2010-2016 The s9e Authors
5151 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
5152 */
5153 namespace s9e\TextFormatter\Configurator\Collections;
5154 use Countable;
5155 use Iterator;
5156 use s9e\TextFormatter\Configurator\ConfigProvider;
5157 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
5158 class Collection implements ConfigProvider, Countable, Iterator
5159 {
5160 protected $items = array();
5161 public function clear()
5162 {
5163 $this->items = array();
5164 }
5165 public function asConfig()
5166 {
5167 return ConfigHelper::toArray($this->items, \true);
5168 }
5169 public function count()
5170 {
5171 return \count($this->items);
5172 }
5173 public function current()
5174 {
5175 return \current($this->items);
5176 }
5177 public function key()
5178 {
5179 return \key($this->items);
5180 }
5181 public function next()
5182 {
5183 return \next($this->items);
5184 }
5185 public function rewind()
5186 {
5187 \reset($this->items);
5188 }
5189 public function valid()
5190 {
5191 return (\key($this->items) !== \null);
5192 }
5193 }
5194
5195 /*
5196 * @package s9e\TextFormatter
5197 * @copyright Copyright (c) 2010-2016 The s9e Authors
5198 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
5199 */
5200 namespace s9e\TextFormatter\Configurator\Items;
5201 use InvalidArgumentException;
5202 use RuntimeException;
5203 use Traversable;
5204 use s9e\TextFormatter\Configurator\Collections\AttributeFilterChain;
5205 use s9e\TextFormatter\Configurator\Collections\Collection;
5206 use s9e\TextFormatter\Configurator\Collections\NormalizedCollection;
5207 use s9e\TextFormatter\Configurator\ConfigProvider;
5208 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
5209 use s9e\TextFormatter\Configurator\Items\ProgrammableCallback;
5210 use s9e\TextFormatter\Configurator\Traits\Configurable;
5211 use s9e\TextFormatter\Configurator\Traits\TemplateSafeness;
5212 class Attribute implements ConfigProvider
5213 {
5214 public function __get($propName)
5215 {
5216 $methodName = 'get' . \ucfirst($propName);
5217 if (\method_exists($this, $methodName))
5218 return $this->$methodName();
5219 if (!\property_exists($this, $propName))
5220 throw new RuntimeException("Property '" . $propName . "' does not exist");
5221 return $this->$propName;
5222 }
5223 public function __set($propName, $propValue)
5224 {
5225 $methodName = 'set' . \ucfirst($propName);
5226 if (\method_exists($this, $methodName))
5227 {
5228 $this->$methodName($propValue);
5229 return;
5230 }
5231 if (!isset($this->$propName))
5232 {
5233 $this->$propName = $propValue;
5234 return;
5235 }
5236 if ($this->$propName instanceof NormalizedCollection)
5237 {
5238 if (!\is_array($propValue)
5239 && !($propValue instanceof Traversable))
5240 throw new InvalidArgumentException("Property '" . $propName . "' expects an array or a traversable object to be passed");
5241 $this->$propName->clear();
5242 foreach ($propValue as $k => $v)
5243 $this->$propName->set($k, $v);
5244 return;
5245 }
5246 if (\is_object($this->$propName))
5247 {
5248 if (!($propValue instanceof $this->$propName))
5249 throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of class '" . \get_class($this->$propName) . "' with instance of '" . \get_class($propValue) . "'");
5250 }
5251 else
5252 {
5253 $oldType = \gettype($this->$propName);
5254 $newType = \gettype($propValue);
5255 if ($oldType === 'boolean')
5256 if ($propValue === 'false')
5257 {
5258 $newType = 'boolean';
5259 $propValue = \false;
5260 }
5261 elseif ($propValue === 'true')
5262 {
5263 $newType = 'boolean';
5264 $propValue = \true;
5265 }
5266 if ($oldType !== $newType)
5267 {
5268 $tmp = $propValue;
5269 \settype($tmp, $oldType);
5270 \settype($tmp, $newType);
5271 if ($tmp !== $propValue)
5272 throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of type " . $oldType . ' with value of type ' . $newType);
5273 \settype($propValue, $oldType);
5274 }
5275 }
5276 $this->$propName = $propValue;
5277 }
5278 public function __isset($propName)
5279 {
5280 $methodName = 'isset' . \ucfirst($propName);
5281 if (\method_exists($this, $methodName))
5282 return $this->$methodName();
5283 return isset($this->$propName);
5284 }
5285 public function __unset($propName)
5286 {
5287 $methodName = 'unset' . \ucfirst($propName);
5288 if (\method_exists($this, $methodName))
5289 {
5290 $this->$methodName();
5291 return;
5292 }
5293 if (!isset($this->$propName))
5294 return;
5295 if ($this->$propName instanceof Collection)
5296 {
5297 $this->$propName->clear();
5298 return;
5299 }
5300 throw new RuntimeException("Property '" . $propName . "' cannot be unset");
5301 }
5302 protected $markedSafe = array();
5303
5304 public function isSafeAsURL()
5305 {
5306 return $this->isSafe('AsURL');
5307 }
5308 public function isSafeInCSS()
5309 {
5310 return $this->isSafe('InCSS');
5311 }
5312 public function isSafeInJS()
5313 {
5314 return $this->isSafe('InJS');
5315 }
5316 public function markAsSafeAsURL()
5317 {
5318 $this->markedSafe['AsURL'] = \true;
5319 return $this;
5320 }
5321 public function markAsSafeInCSS()
5322 {
5323 $this->markedSafe['InCSS'] = \true;
5324 return $this;
5325 }
5326 public function markAsSafeInJS()
5327 {
5328 $this->markedSafe['InJS'] = \true;
5329 return $this;
5330 }
5331 public function resetSafeness()
5332 {
5333 $this->markedSafe = array();
5334 return $this;
5335 }
5336 protected $defaultValue;
5337 protected $filterChain;
5338 protected $generator;
5339 protected $required = \true;
5340 public function __construct(array $options = \null)
5341 {
5342 $this->filterChain = new AttributeFilterChain;
5343 if (isset($options))
5344 foreach ($options as $optionName => $optionValue)
5345 $this->__set($optionName, $optionValue);
5346 }
5347 protected function isSafe($context)
5348 {
5349 $methodName = 'isSafe' . $context;
5350 foreach ($this->filterChain as $filter)
5351 if ($filter->$methodName())
5352 return \true;
5353 return !empty($this->markedSafe[$context]);
5354 }
5355 public function setGenerator($callback)
5356 {
5357 if (!($callback instanceof ProgrammableCallback))
5358 $callback = new ProgrammableCallback($callback);
5359 $this->generator = $callback;
5360 }
5361 public function asConfig()
5362 {
5363 $vars = \get_object_vars($this);
5364 unset($vars['markedSafe']);
5365 return ConfigHelper::toArray($vars);
5366 }
5367 }
5368
5369 /*
5370 * @package s9e\TextFormatter
5371 * @copyright Copyright (c) 2010-2016 The s9e Authors
5372 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
5373 */
5374 namespace s9e\TextFormatter\Configurator\Items;
5375 use InvalidArgumentException;
5376 use s9e\TextFormatter\Configurator\ConfigProvider;
5377 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
5378 use s9e\TextFormatter\Configurator\JavaScript\Code;
5379 use s9e\TextFormatter\Configurator\JavaScript\FunctionProvider;
5380 class ProgrammableCallback implements ConfigProvider
5381 {
5382 protected $callback;
5383 protected $js = 'returnFalse';
5384 protected $params = array();
5385 protected $vars = array();
5386 public function __construct($callback)
5387 {
5388 if (!\is_callable($callback))
5389 throw new InvalidArgumentException(__METHOD__ . '() expects a callback');
5390 $this->callback = $this->normalizeCallback($callback);
5391 $this->autoloadJS();
5392 }
5393 public function addParameterByValue($paramValue)
5394 {
5395 $this->params[] = $paramValue;
5396 return $this;
5397 }
5398 public function addParameterByName($paramName)
5399 {
5400 if (\array_key_exists($paramName, $this->params))
5401 throw new InvalidArgumentException("Parameter '" . $paramName . "' already exists");
5402 $this->params[$paramName] = \null;
5403 return $this;
5404 }
5405 public function getCallback()
5406 {
5407 return $this->callback;
5408 }
5409 public function getJS()
5410 {
5411 return $this->js;
5412 }
5413 public function getVars()
5414 {
5415 return $this->vars;
5416 }
5417 public function resetParameters()
5418 {
5419 $this->params = array();
5420 return $this;
5421 }
5422 public function setJS($js)
5423 {
5424 $this->js = $js;
5425 return $this;
5426 }
5427 public function setVar($name, $value)
5428 {
5429 $this->vars[$name] = $value;
5430 return $this;
5431 }
5432 public function setVars(array $vars)
5433 {
5434 $this->vars = $vars;
5435 return $this;
5436 }
5437 public function asConfig()
5438 {
5439 $config = array('callback' => $this->callback);
5440 foreach ($this->params as $k => $v)
5441 if (\is_numeric($k))
5442 $config['params'][] = $v;
5443 elseif (isset($this->vars[$k]))
5444 $config['params'][] = $this->vars[$k];
5445 else
5446 $config['params'][$k] = \null;
5447 if (isset($config['params']))
5448 $config['params'] = ConfigHelper::toArray($config['params'], \true, \true);
5449 $config['js'] = new Code($this->js);
5450 return $config;
5451 }
5452 protected function autoloadJS()
5453 {
5454 if (!\is_string($this->callback))
5455 return;
5456 try
5457 {
5458 $this->js = FunctionProvider::get($this->callback);
5459 }
5460 catch (InvalidArgumentException $e)
5461 {
5462 }
5463 }
5464 protected function normalizeCallback($callback)
5465 {
5466 if (\is_array($callback) && \is_string($callback[0]))
5467 $callback = $callback[0] . '::' . $callback[1];
5468 if (\is_string($callback))
5469 $callback = \ltrim($callback, '\\');
5470 return $callback;
5471 }
5472 }
5473
5474 /*
5475 * @package s9e\TextFormatter
5476 * @copyright Copyright (c) 2010-2016 The s9e Authors
5477 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
5478 */
5479 namespace s9e\TextFormatter\Configurator\Items;
5480 use InvalidArgumentException;
5481 use s9e\TextFormatter\Configurator\ConfigProvider;
5482 use s9e\TextFormatter\Configurator\FilterableConfigValue;
5483 use s9e\TextFormatter\Configurator\Helpers\RegexpParser;
5484 use s9e\TextFormatter\Configurator\JavaScript\Code;
5485 use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor;
5486 class Regexp implements ConfigProvider, FilterableConfigValue
5487 {
5488 protected $isGlobal;
5489 protected $jsRegexp;
5490 protected $regexp;
5491 public function __construct($regexp, $isGlobal = \false)
5492 {
5493 if (@\preg_match($regexp, '') === \false)
5494 throw new InvalidArgumentException('Invalid regular expression ' . \var_export($regexp, \true));
5495 $this->regexp = $regexp;
5496 $this->isGlobal = $isGlobal;
5497 }
5498 public function __toString()
5499 {
5500 return $this->regexp;
5501 }
5502 public function asConfig()
5503 {
5504 return $this;
5505 }
5506 public function filterConfig($target)
5507 {
5508 return ($target === 'JS') ? new Code($this->getJS()) : (string) $this;
5509 }
5510 public function getCaptureNames()
5511 {
5512 return RegexpParser::getCaptureNames($this->regexp);
5513 }
5514 public function getJS()
5515 {
5516 if (!isset($this->jsRegexp))
5517 $this->jsRegexp = RegexpConvertor::toJS($this->regexp, $this->isGlobal);
5518 return $this->jsRegexp;
5519 }
5520 public function getNamedCaptures()
5521 {
5522 $captures = array();
5523 $regexpInfo = RegexpParser::parse($this->regexp);
5524 $start = $regexpInfo['delimiter'] . '^';
5525 $end = '$' . $regexpInfo['delimiter'] . $regexpInfo['modifiers'];
5526 if (\strpos($regexpInfo['modifiers'], 'D') === \false)
5527 $end .= 'D';
5528 foreach ($this->getNamedCapturesExpressions($regexpInfo['tokens']) as $name => $expr)
5529 $captures[$name] = $start . $expr . $end;
5530 return $captures;
5531 }
5532 protected function getNamedCapturesExpressions(array $tokens)
5533 {
5534 $exprs = array();
5535 foreach ($tokens as $token)
5536 {
5537 if ($token['type'] !== 'capturingSubpatternStart' || !isset($token['name']))
5538 continue;
5539 $expr = $token['content'];
5540 if (\strpos($expr, '|') !== \false)
5541 $expr = '(?:' . $expr . ')';
5542 $exprs[$token['name']] = $expr;
5543 }
5544 return $exprs;
5545 }
5546 public function setJS($jsRegexp)
5547 {
5548 $this->jsRegexp = $jsRegexp;
5549 }
5550 }
5551
5552 /*
5553 * @package s9e\TextFormatter
5554 * @copyright Copyright (c) 2010-2016 The s9e Authors
5555 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
5556 */
5557 namespace s9e\TextFormatter\Configurator\Items;
5558 use InvalidArgumentException;
5559 use RuntimeException;
5560 use Traversable;
5561 use s9e\TextFormatter\Configurator\Collections\AttributeCollection;
5562 use s9e\TextFormatter\Configurator\Collections\AttributePreprocessorCollection;
5563 use s9e\TextFormatter\Configurator\Collections\Collection;
5564 use s9e\TextFormatter\Configurator\Collections\NormalizedCollection;
5565 use s9e\TextFormatter\Configurator\Collections\Ruleset;
5566 use s9e\TextFormatter\Configurator\Collections\TagFilterChain;
5567 use s9e\TextFormatter\Configurator\ConfigProvider;
5568 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
5569 use s9e\TextFormatter\Configurator\Items\Template;
5570 use s9e\TextFormatter\Configurator\Traits\Configurable;
5571 class Tag implements ConfigProvider
5572 {
5573 public function __get($propName)
5574 {
5575 $methodName = 'get' . \ucfirst($propName);
5576 if (\method_exists($this, $methodName))
5577 return $this->$methodName();
5578 if (!\property_exists($this, $propName))
5579 throw new RuntimeException("Property '" . $propName . "' does not exist");
5580 return $this->$propName;
5581 }
5582 public function __set($propName, $propValue)
5583 {
5584 $methodName = 'set' . \ucfirst($propName);
5585 if (\method_exists($this, $methodName))
5586 {
5587 $this->$methodName($propValue);
5588 return;
5589 }
5590 if (!isset($this->$propName))
5591 {
5592 $this->$propName = $propValue;
5593 return;
5594 }
5595 if ($this->$propName instanceof NormalizedCollection)
5596 {
5597 if (!\is_array($propValue)
5598 && !($propValue instanceof Traversable))
5599 throw new InvalidArgumentException("Property '" . $propName . "' expects an array or a traversable object to be passed");
5600 $this->$propName->clear();
5601 foreach ($propValue as $k => $v)
5602 $this->$propName->set($k, $v);
5603 return;
5604 }
5605 if (\is_object($this->$propName))
5606 {
5607 if (!($propValue instanceof $this->$propName))
5608 throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of class '" . \get_class($this->$propName) . "' with instance of '" . \get_class($propValue) . "'");
5609 }
5610 else
5611 {
5612 $oldType = \gettype($this->$propName);
5613 $newType = \gettype($propValue);
5614 if ($oldType === 'boolean')
5615 if ($propValue === 'false')
5616 {
5617 $newType = 'boolean';
5618 $propValue = \false;
5619 }
5620 elseif ($propValue === 'true')
5621 {
5622 $newType = 'boolean';
5623 $propValue = \true;
5624 }
5625 if ($oldType !== $newType)
5626 {
5627 $tmp = $propValue;
5628 \settype($tmp, $oldType);
5629 \settype($tmp, $newType);
5630 if ($tmp !== $propValue)
5631 throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of type " . $oldType . ' with value of type ' . $newType);
5632 \settype($propValue, $oldType);
5633 }
5634 }
5635 $this->$propName = $propValue;
5636 }
5637 public function __isset($propName)
5638 {
5639 $methodName = 'isset' . \ucfirst($propName);
5640 if (\method_exists($this, $methodName))
5641 return $this->$methodName();
5642 return isset($this->$propName);
5643 }
5644 public function __unset($propName)
5645 {
5646 $methodName = 'unset' . \ucfirst($propName);
5647 if (\method_exists($this, $methodName))
5648 {
5649 $this->$methodName();
5650 return;
5651 }
5652 if (!isset($this->$propName))
5653 return;
5654 if ($this->$propName instanceof Collection)
5655 {
5656 $this->$propName->clear();
5657 return;
5658 }
5659 throw new RuntimeException("Property '" . $propName . "' cannot be unset");
5660 }
5661 protected $attributes;
5662 protected $attributePreprocessors;
5663 protected $filterChain;
5664 protected $nestingLimit = 10;
5665 protected $rules;
5666 protected $tagLimit = 1000;
5667 protected $template;
5668 public function __construct(array $options = \null)
5669 {
5670 $this->attributes = new AttributeCollection;
5671 $this->attributePreprocessors = new AttributePreprocessorCollection;
5672 $this->filterChain = new TagFilterChain;
5673 $this->rules = new Ruleset;
5674 $this->filterChain->append('s9e\\TextFormatter\\Parser::executeAttributePreprocessors')
5675 ->addParameterByName('tagConfig')
5676 ->setJS('executeAttributePreprocessors');
5677 $this->filterChain->append('s9e\\TextFormatter\\Parser::filterAttributes')
5678 ->addParameterByName('tagConfig')
5679 ->addParameterByName('registeredVars')
5680 ->addParameterByName('logger')
5681 ->setJS('filterAttributes');
5682 if (isset($options))
5683 {
5684 \ksort($options);
5685 foreach ($options as $optionName => $optionValue)
5686 $this->__set($optionName, $optionValue);
5687 }
5688 }
5689 public function asConfig()
5690 {
5691 $vars = \get_object_vars($this);
5692 unset($vars['defaultChildRule']);
5693 unset($vars['defaultDescendantRule']);
5694 unset($vars['template']);
5695 if (!\count($this->attributePreprocessors))
5696 {
5697 $callback = 's9e\\TextFormatter\\Parser::executeAttributePreprocessors';
5698 $filterChain = clone $vars['filterChain'];
5699 $i = \count($filterChain);
5700 while (--$i >= 0)
5701 if ($filterChain[$i]->getCallback() === $callback)
5702 unset($filterChain[$i]);
5703 $vars['filterChain'] = $filterChain;
5704 }
5705 return ConfigHelper::toArray($vars);
5706 }
5707 public function getTemplate()
5708 {
5709 return $this->template;
5710 }
5711 public function issetTemplate()
5712 {
5713 return isset($this->template);
5714 }
5715 public function setAttributePreprocessors($attributePreprocessors)
5716 {
5717 $this->attributePreprocessors->clear();
5718 $this->attributePreprocessors->merge($attributePreprocessors);
5719 }
5720 public function setNestingLimit($limit)
5721 {
5722 $limit = (int) $limit;
5723 if ($limit < 1)
5724 throw new InvalidArgumentException('nestingLimit must be a number greater than 0');
5725 $this->nestingLimit = $limit;
5726 }
5727 public function setRules($rules)
5728 {
5729 $this->rules->clear();
5730 $this->rules->merge($rules);
5731 }
5732 public function setTagLimit($limit)
5733 {
5734 $limit = (int) $limit;
5735 if ($limit < 1)
5736 throw new InvalidArgumentException('tagLimit must be a number greater than 0');
5737 $this->tagLimit = $limit;
5738 }
5739 public function setTemplate($template)
5740 {
5741 if (!($template instanceof Template))
5742 $template = new Template($template);
5743 $this->template = $template;
5744 }
5745 public function unsetTemplate()
5746 {
5747 unset($this->template);
5748 }
5749 }
5750
5751 /*
5752 * @package s9e\TextFormatter
5753 * @copyright Copyright (c) 2010-2016 The s9e Authors
5754 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
5755 */
5756 namespace s9e\TextFormatter\Configurator\JavaScript;
5757 use s9e\TextFormatter\Configurator\FilterableConfigValue;
5758 class Code implements FilterableConfigValue
5759 {
5760 public $code;
5761 public function __construct($code)
5762 {
5763 $this->code = $code;
5764 }
5765 public function __toString()
5766 {
5767 return (string) $this->code;
5768 }
5769 public function filterConfig($target)
5770 {
5771 return ($target === 'JS') ? $this : \null;
5772 }
5773 }
5774
5775 /*
5776 * @package s9e\TextFormatter
5777 * @copyright Copyright (c) 2010-2016 The s9e Authors
5778 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
5779 */
5780 namespace s9e\TextFormatter\Configurator\RendererGenerators;
5781 use DOMElement;
5782 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
5783 use s9e\TextFormatter\Configurator\Helpers\TemplateParser;
5784 use s9e\TextFormatter\Configurator\RendererGenerator;
5785 use s9e\TextFormatter\Configurator\RendererGenerators\PHP\ControlStructuresOptimizer;
5786 use s9e\TextFormatter\Configurator\RendererGenerators\PHP\Optimizer;
5787 use s9e\TextFormatter\Configurator\RendererGenerators\PHP\Quick;
5788 use s9e\TextFormatter\Configurator\RendererGenerators\PHP\Serializer;
5789 use s9e\TextFormatter\Configurator\Rendering;
5790 class PHP implements RendererGenerator
5791 {
5792 const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
5793 public $cacheDir;
5794 public $className;
5795 public $controlStructuresOptimizer;
5796 public $defaultClassPrefix = 'Renderer_';
5797 public $enableQuickRenderer = \true;
5798 public $filepath;
5799 public $lastClassName;
5800 public $lastFilepath;
5801 public $optimizer;
5802 public $serializer;
5803 public $useMultibyteStringFunctions;
5804 public function __construct($cacheDir = \null)
5805 {
5806 $this->cacheDir = (isset($cacheDir)) ? $cacheDir : \sys_get_temp_dir();
5807 if (\extension_loaded('tokenizer'))
5808 {
5809 $this->controlStructuresOptimizer = new ControlStructuresOptimizer;
5810 $this->optimizer = new Optimizer;
5811 }
5812 $this->useMultibyteStringFunctions = \extension_loaded('mbstring');
5813 $this->serializer = new Serializer;
5814 }
5815 public function getRenderer(Rendering $rendering)
5816 {
5817 $php = $this->generate($rendering);
5818 if (isset($this->filepath))
5819 $filepath = $this->filepath;
5820 else
5821 $filepath = $this->cacheDir . '/' . \str_replace('\\', '_', $this->lastClassName) . '.php';
5822 \file_put_contents($filepath, "<?php\n" . $php);
5823 $this->lastFilepath = \realpath($filepath);
5824 if (!\class_exists($this->lastClassName, \false))
5825 include $filepath;
5826 $renderer = new $this->lastClassName;
5827 $renderer->source = $php;
5828 return $renderer;
5829 }
5830 public function generate(Rendering $rendering)
5831 {
5832 $this->serializer->useMultibyteStringFunctions = $this->useMultibyteStringFunctions;
5833 $templates = $rendering->getTemplates();
5834 $groupedTemplates = array();
5835 foreach ($templates as $tagName => $template)
5836 $groupedTemplates[$template][] = $tagName;
5837 $hasApplyTemplatesSelect = \false;
5838 $tagBranch = 0;
5839 $tagBranches = array();
5840 $compiledTemplates = array();
5841 $branchTables = array();
5842 foreach ($groupedTemplates as $template => $tagNames)
5843 {
5844 $ir = TemplateParser::parse($template);
5845 if (!$hasApplyTemplatesSelect)
5846 foreach ($ir->getElementsByTagName('applyTemplates') as $applyTemplates)
5847 if ($applyTemplates->hasAttribute('select'))
5848 $hasApplyTemplatesSelect = \true;
5849 $templateSource = $this->serializer->serialize($ir->documentElement);
5850 if (isset($this->optimizer))
5851 $templateSource = $this->optimizer->optimize($templateSource);
5852 $branchTables += $this->serializer->branchTables;
5853 $compiledTemplates[$tagBranch] = $templateSource;
5854 foreach ($tagNames as $tagName)
5855 $tagBranches[$tagName] = $tagBranch;
5856 ++$tagBranch;
5857 }
5858 unset($groupedTemplates, $ir, $quickRender);
5859 $quickSource = \false;
5860 if ($this->enableQuickRenderer)
5861 {
5862 $quickRender = array();
5863 foreach ($tagBranches as $tagName => $tagBranch)
5864 $quickRender[$tagName] = $compiledTemplates[$tagBranch];
5865 $quickSource = Quick::getSource($quickRender);
5866 unset($quickRender);
5867 }
5868 $templatesSource = Quick::generateConditionals('$tb', $compiledTemplates);
5869 unset($compiledTemplates);
5870 if ($hasApplyTemplatesSelect)
5871 $needsXPath = \true;
5872 elseif (\strpos($templatesSource, '$this->getParamAsXPath') !== \false)
5873 $needsXPath = \true;
5874 elseif (\strpos($templatesSource, '$this->xpath') !== \false)
5875 $needsXPath = \true;
5876 else
5877 $needsXPath = \false;
5878 $php = array();
5879 $php[] = ' extends \\s9e\\TextFormatter\\Renderer';
5880 $php[] = '{';
5881 $php[] = ' protected $params=' . self::export($rendering->getAllParameters()) . ';';
5882 $php[] = ' protected static $tagBranches=' . self::export($tagBranches) . ';';
5883 foreach ($branchTables as $varName => $branchTable)
5884 $php[] = ' protected static $' . $varName . '=' . self::export($branchTable) . ';';
5885 if ($needsXPath)
5886 $php[] = ' protected $xpath;';
5887 $php[] = ' public function __sleep()';
5888 $php[] = ' {';
5889 $php[] = ' $props = get_object_vars($this);';
5890 $php[] = " unset(\$props['out'], \$props['proc'], \$props['source']" . (($needsXPath) ? ", \$props['xpath']" : '') . ');';
5891 $php[] = ' return array_keys($props);';
5892 $php[] = ' }';
5893 $php[] = ' public function renderRichText($xml)';
5894 $php[] = ' {';
5895 if ($quickSource !== \false)
5896 {
5897 $php[] = ' if (!isset($this->quickRenderingTest) || !preg_match($this->quickRenderingTest, $xml))';
5898 $php[] = ' {';
5899 $php[] = ' try';
5900 $php[] = ' {';
5901 $php[] = ' return $this->renderQuick($xml);';
5902 $php[] = ' }';
5903 $php[] = ' catch (\\Exception $e)';
5904 $php[] = ' {';
5905 $php[] = ' }';
5906 $php[] = ' }';
5907 }
5908 $php[] = ' $dom = $this->loadXML($xml);';
5909 if ($needsXPath)
5910 $php[] = ' $this->xpath = new \\DOMXPath($dom);';
5911 $php[] = " \$this->out = '';";
5912 $php[] = ' $this->at($dom->documentElement);';
5913 if ($needsXPath)
5914 $php[] = ' $this->xpath = null;';
5915 $php[] = ' return $this->out;';
5916 $php[] = ' }';
5917 if ($hasApplyTemplatesSelect)
5918 $php[] = ' protected function at(\\DOMNode $root, $xpath = null)';
5919 else
5920 $php[] = ' protected function at(\\DOMNode $root)';
5921 $php[] = ' {';
5922 $php[] = ' if ($root->nodeType === 3)';
5923 $php[] = ' {';
5924 $php[] = ' $this->out .= htmlspecialchars($root->textContent,' . \ENT_NOQUOTES . ');';
5925 $php[] = ' }';
5926 $php[] = ' else';
5927 $php[] = ' {';
5928 if ($hasApplyTemplatesSelect)
5929 $php[] = ' foreach (isset($xpath) ? $this->xpath->query($xpath, $root) : $root->childNodes as $node)';
5930 else
5931 $php[] = ' foreach ($root->childNodes as $node)';
5932 $php[] = ' {';
5933 $php[] = ' if (!isset(self::$tagBranches[$node->nodeName]))';
5934 $php[] = ' {';
5935 $php[] = ' $this->at($node);';
5936 $php[] = ' }';
5937 $php[] = ' else';
5938 $php[] = ' {';
5939 $php[] = ' $tb = self::$tagBranches[$node->nodeName];';
5940 $php[] = ' ' . $templatesSource;
5941 $php[] = ' }';
5942 $php[] = ' }';
5943 $php[] = ' }';
5944 $php[] = ' }';
5945 if (\strpos($templatesSource, '$this->getParamAsXPath') !== \false)
5946 {
5947 $php[] = ' protected function getParamAsXPath($k)';
5948 $php[] = ' {';
5949 $php[] = ' if (!isset($this->params[$k]))';
5950 $php[] = ' {';
5951 $php[] = ' return "\'\'";';
5952 $php[] = ' }';
5953 $php[] = ' $str = $this->params[$k];';
5954 $php[] = ' if (strpos($str, "\'") === false)';
5955 $php[] = ' {';
5956 $php[] = ' return "\'$str\'";';
5957 $php[] = ' }';
5958 $php[] = ' if (strpos($str, \'"\') === false)';
5959 $php[] = ' {';
5960 $php[] = ' return "\\"$str\\"";';
5961 $php[] = ' }';
5962 $php[] = ' $toks = array();';
5963 $php[] = ' $c = \'"\';';
5964 $php[] = ' $pos = 0;';
5965 $php[] = ' while ($pos < strlen($str))';
5966 $php[] = ' {';
5967 $php[] = ' $spn = strcspn($str, $c, $pos);';
5968 $php[] = ' if ($spn)';
5969 $php[] = ' {';
5970 $php[] = ' $toks[] = $c . substr($str, $pos, $spn) . $c;';
5971 $php[] = ' $pos += $spn;';
5972 $php[] = ' }';
5973 $php[] = ' $c = ($c === \'"\') ? "\'" : \'"\';';
5974 $php[] = ' }';
5975 $php[] = ' return \'concat(\' . implode(\',\', $toks) . \')\';';
5976 $php[] = ' }';
5977 }
5978 if ($quickSource !== \false)
5979 $php[] = $quickSource;
5980 $php[] = '}';
5981 $php = \implode("\n", $php);
5982 if (isset($this->controlStructuresOptimizer))
5983 $php = $this->controlStructuresOptimizer->optimize($php);
5984 $className = (isset($this->className))
5985 ? $this->className
5986 : $this->defaultClassPrefix . \sha1($php);
5987 $this->lastClassName = $className;
5988 $header = "\n/**\n* @package s9e\TextFormatter\n* @copyright Copyright (c) 2010-2016 The s9e Authors\n* @license http://www.opensource.org/licenses/mit-license.php The MIT License\n*/\n";
5989 $pos = \strrpos($className, '\\');
5990 if ($pos !== \false)
5991 {
5992 $header .= 'namespace ' . \substr($className, 0, $pos) . ";\n\n";
5993 $className = \substr($className, 1 + $pos);
5994 }
5995 $php = $header . 'class ' . $className . $php;
5996 return $php;
5997 }
5998 protected static function export(array $value)
5999 {
6000 $pairs = array();
6001 foreach ($value as $k => $v)
6002 $pairs[] = \var_export($k, \true) . '=>' . \var_export($v, \true);
6003 return 'array(' . \implode(',', $pairs) . ')';
6004 }
6005 }
6006
6007 /*
6008 * @package s9e\TextFormatter
6009 * @copyright Copyright (c) 2010-2016 The s9e Authors
6010 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6011 */
6012 namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
6013 class ControlStructuresOptimizer extends AbstractOptimizer
6014 {
6015 protected $braces;
6016 protected $context;
6017 protected function blockEndsWithIf()
6018 {
6019 return \in_array($this->context['lastBlock'], array(\T_IF, \T_ELSEIF), \true);
6020 }
6021 protected function isControlStructure()
6022 {
6023 return \in_array(
6024 $this->tokens[$this->i][0],
6025 array(\T_ELSE, \T_ELSEIF, \T_FOR, \T_FOREACH, \T_IF, \T_WHILE),
6026 \true
6027 );
6028 }
6029 protected function isFollowedByElse()
6030 {
6031 if ($this->i > $this->cnt - 4)
6032 return \false;
6033 $k = $this->i + 1;
6034 if ($this->tokens[$k][0] === \T_WHITESPACE)
6035 ++$k;
6036 return \in_array($this->tokens[$k][0], array(\T_ELSEIF, \T_ELSE), \true);
6037 }
6038 protected function mustPreserveBraces()
6039 {
6040 return ($this->blockEndsWithIf() && $this->isFollowedByElse());
6041 }
6042 protected function optimizeTokens()
6043 {
6044 while (++$this->i < $this->cnt)
6045 if ($this->tokens[$this->i] === ';')
6046 ++$this->context['statements'];
6047 elseif ($this->tokens[$this->i] === '{')
6048 ++$this->braces;
6049 elseif ($this->tokens[$this->i] === '}')
6050 {
6051 if ($this->context['braces'] === $this->braces)
6052 $this->processEndOfBlock();
6053 --$this->braces;
6054 }
6055 elseif ($this->isControlStructure())
6056 $this->processControlStructure();
6057 }
6058 protected function processControlStructure()
6059 {
6060 $savedIndex = $this->i;
6061 if (!\in_array($this->tokens[$this->i][0], array(\T_ELSE, \T_ELSEIF), \true))
6062 ++$this->context['statements'];
6063 if ($this->tokens[$this->i][0] !== \T_ELSE)
6064 $this->skipCondition();
6065 $this->skipWhitespace();
6066 if ($this->tokens[$this->i] !== '{')
6067 {
6068 $this->i = $savedIndex;
6069 return;
6070 }
6071 ++$this->braces;
6072 $replacement = array(\T_WHITESPACE, '');
6073 if ($this->tokens[$savedIndex][0] === \T_ELSE
6074 && $this->tokens[$this->i + 1][0] !== \T_VARIABLE
6075 && $this->tokens[$this->i + 1][0] !== \T_WHITESPACE)
6076 $replacement = array(\T_WHITESPACE, ' ');
6077 $this->context['lastBlock'] = $this->tokens[$savedIndex][0];
6078 $this->context = array(
6079 'braces' => $this->braces,
6080 'index' => $this->i,
6081 'lastBlock' => \null,
6082 'parent' => $this->context,
6083 'replacement' => $replacement,
6084 'savedIndex' => $savedIndex,
6085 'statements' => 0
6086 );
6087 }
6088 protected function processEndOfBlock()
6089 {
6090 if ($this->context['statements'] < 2 && !$this->mustPreserveBraces())
6091 $this->removeBracesInCurrentContext();
6092 $this->context = $this->context['parent'];
6093 $this->context['parent']['lastBlock'] = $this->context['lastBlock'];
6094 }
6095 protected function removeBracesInCurrentContext()
6096 {
6097 $this->tokens[$this->context['index']] = $this->context['replacement'];
6098 $this->tokens[$this->i] = ($this->context['statements']) ? array(\T_WHITESPACE, '') : ';';
6099 foreach (array($this->context['index'] - 1, $this->i - 1) as $tokenIndex)
6100 if ($this->tokens[$tokenIndex][0] === \T_WHITESPACE)
6101 $this->tokens[$tokenIndex][1] = '';
6102 if ($this->tokens[$this->context['savedIndex']][0] === \T_ELSE)
6103 {
6104 $j = 1 + $this->context['savedIndex'];
6105 while ($this->tokens[$j][0] === \T_WHITESPACE
6106 || $this->tokens[$j][0] === \T_COMMENT
6107 || $this->tokens[$j][0] === \T_DOC_COMMENT)
6108 ++$j;
6109 if ($this->tokens[$j][0] === \T_IF)
6110 {
6111 $this->tokens[$j] = array(\T_ELSEIF, 'elseif');
6112 $j = $this->context['savedIndex'];
6113 $this->tokens[$j] = array(\T_WHITESPACE, '');
6114 if ($this->tokens[$j - 1][0] === \T_WHITESPACE)
6115 $this->tokens[$j - 1][1] = '';
6116 $this->unindentBlock($j, $this->i - 1);
6117 $this->tokens[$this->context['index']] = array(\T_WHITESPACE, '');
6118 }
6119 }
6120 $this->changed = \true;
6121 }
6122 protected function reset($php)
6123 {
6124 parent::reset($php);
6125 $this->braces = 0;
6126 $this->context = array(
6127 'braces' => 0,
6128 'index' => -1,
6129 'parent' => array(),
6130 'preventElse' => \false,
6131 'savedIndex' => 0,
6132 'statements' => 0
6133 );
6134 }
6135 protected function skipCondition()
6136 {
6137 $this->skipToString('(');
6138 $parens = 0;
6139 while (++$this->i < $this->cnt)
6140 if ($this->tokens[$this->i] === ')')
6141 if ($parens)
6142 --$parens;
6143 else
6144 break;
6145 elseif ($this->tokens[$this->i] === '(')
6146 ++$parens;
6147 }
6148 }
6149
6150 /*
6151 * @package s9e\TextFormatter
6152 * @copyright Copyright (c) 2010-2016 The s9e Authors
6153 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6154 */
6155 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6156 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6157 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
6158 class AutoCloseIfVoid implements BooleanRulesGenerator
6159 {
6160 public function generateBooleanRules(TemplateForensics $src)
6161 {
6162 return ($src->isVoid()) ? array('autoClose' => \true) : array();
6163 }
6164 }
6165
6166 /*
6167 * @package s9e\TextFormatter
6168 * @copyright Copyright (c) 2010-2016 The s9e Authors
6169 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6170 */
6171 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6172 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6173 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
6174 class AutoReopenFormattingElements implements BooleanRulesGenerator
6175 {
6176 public function generateBooleanRules(TemplateForensics $src)
6177 {
6178 return ($src->isFormattingElement()) ? array('autoReopen' => \true) : array();
6179 }
6180 }
6181
6182 /*
6183 * @package s9e\TextFormatter
6184 * @copyright Copyright (c) 2010-2016 The s9e Authors
6185 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6186 */
6187 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6188 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6189 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator;
6190 class BlockElementsFosterFormattingElements implements TargetedRulesGenerator
6191 {
6192 public function generateTargetedRules(TemplateForensics $src, TemplateForensics $trg)
6193 {
6194 return ($src->isBlock() && $src->isPassthrough() && $trg->isFormattingElement()) ? array('fosterParent') : array();
6195 }
6196 }
6197
6198 /*
6199 * @package s9e\TextFormatter
6200 * @copyright Copyright (c) 2010-2016 The s9e Authors
6201 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6202 */
6203 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6204 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6205 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
6206 class DisableAutoLineBreaksIfNewLinesArePreserved implements BooleanRulesGenerator
6207 {
6208 public function generateBooleanRules(TemplateForensics $src)
6209 {
6210 return ($src->preservesNewLines()) ? array('disableAutoLineBreaks' => \true) : array();
6211 }
6212 }
6213
6214 /*
6215 * @package s9e\TextFormatter
6216 * @copyright Copyright (c) 2010-2016 The s9e Authors
6217 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6218 */
6219 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6220 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6221 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
6222 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator;
6223 class EnforceContentModels implements BooleanRulesGenerator, TargetedRulesGenerator
6224 {
6225 protected $br;
6226 protected $span;
6227 public function __construct()
6228 {
6229 $this->br = new TemplateForensics('<br/>');
6230 $this->span = new TemplateForensics('<span><xsl:apply-templates/></span>');
6231 }
6232 public function generateBooleanRules(TemplateForensics $src)
6233 {
6234 $rules = array();
6235 if ($src->isTransparent())
6236 $rules['isTransparent'] = \true;
6237 if (!$src->allowsChild($this->br))
6238 {
6239 $rules['preventLineBreaks'] = \true;
6240 $rules['suspendAutoLineBreaks'] = \true;
6241 }
6242 if (!$src->allowsDescendant($this->br))
6243 {
6244 $rules['disableAutoLineBreaks'] = \true;
6245 $rules['preventLineBreaks'] = \true;
6246 }
6247 return $rules;
6248 }
6249 public function generateTargetedRules(TemplateForensics $src, TemplateForensics $trg)
6250 {
6251 if (!$src->allowsChildElements())
6252 $src = $this->span;
6253 $rules = array();
6254 if (!$src->allowsChild($trg))
6255 $rules[] = 'denyChild';
6256 if (!$src->allowsDescendant($trg))
6257 $rules[] = 'denyDescendant';
6258 return $rules;
6259 }
6260 }
6261
6262 /*
6263 * @package s9e\TextFormatter
6264 * @copyright Copyright (c) 2010-2016 The s9e Authors
6265 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6266 */
6267 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6268 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6269 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator;
6270 class EnforceOptionalEndTags implements TargetedRulesGenerator
6271 {
6272 public function generateTargetedRules(TemplateForensics $src, TemplateForensics $trg)
6273 {
6274 return ($src->closesParent($trg)) ? array('closeParent') : array();
6275 }
6276 }
6277
6278 /*
6279 * @package s9e\TextFormatter
6280 * @copyright Copyright (c) 2010-2016 The s9e Authors
6281 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6282 */
6283 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6284 use DOMXPath;
6285 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6286 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
6287 class IgnoreTagsInCode implements BooleanRulesGenerator
6288 {
6289 public function generateBooleanRules(TemplateForensics $src)
6290 {
6291 $xpath = new DOMXPath($src->getDOM());
6292 if ($xpath->evaluate('count(//code//xsl:apply-templates)'))
6293 return array('ignoreTags' => \true);
6294 return array();
6295 }
6296 }
6297
6298 /*
6299 * @package s9e\TextFormatter
6300 * @copyright Copyright (c) 2010-2016 The s9e Authors
6301 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6302 */
6303 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6304 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6305 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
6306 class IgnoreTextIfDisallowed implements BooleanRulesGenerator
6307 {
6308 public function generateBooleanRules(TemplateForensics $src)
6309 {
6310 return ($src->allowsText()) ? array() : array('ignoreText' => \true);
6311 }
6312 }
6313
6314 /*
6315 * @package s9e\TextFormatter
6316 * @copyright Copyright (c) 2010-2016 The s9e Authors
6317 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6318 */
6319 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6320 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6321 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
6322 class IgnoreWhitespaceAroundBlockElements implements BooleanRulesGenerator
6323 {
6324 public function generateBooleanRules(TemplateForensics $src)
6325 {
6326 return ($src->isBlock()) ? array('ignoreSurroundingWhitespace' => \true) : array();
6327 }
6328 }
6329
6330 /*
6331 * @package s9e\TextFormatter
6332 * @copyright Copyright (c) 2010-2016 The s9e Authors
6333 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6334 */
6335 namespace s9e\TextFormatter\Configurator\RulesGenerators;
6336 use DOMXPath;
6337 use s9e\TextFormatter\Configurator\Helpers\TemplateForensics;
6338 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
6339 class TrimFirstLineInCodeBlocks implements BooleanRulesGenerator
6340 {
6341 public function generateBooleanRules(TemplateForensics $src)
6342 {
6343 $rules = array();
6344 $xpath = new DOMXPath($src->getDOM());
6345 if ($xpath->evaluate('count(//pre//code//xsl:apply-templates)') > 0)
6346 $rules['trimFirstLine'] = \true;
6347 return $rules;
6348 }
6349 }
6350
6351 /*
6352 * @package s9e\TextFormatter
6353 * @copyright Copyright (c) 2010-2016 The s9e Authors
6354 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6355 */
6356 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6357 use DOMAttr;
6358 use DOMElement;
6359 use DOMNode;
6360 use DOMXPath;
6361 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6362 use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
6363 use s9e\TextFormatter\Configurator\Items\Attribute;
6364 use s9e\TextFormatter\Configurator\Items\Tag;
6365 use s9e\TextFormatter\Configurator\TemplateCheck;
6366 abstract class AbstractDynamicContentCheck extends TemplateCheck
6367 {
6368 protected $ignoreUnknownAttributes = \false;
6369 abstract protected function getNodes(DOMElement $template);
6370 abstract protected function isSafe(Attribute $attribute);
6371 public function check(DOMElement $template, Tag $tag)
6372 {
6373 foreach ($this->getNodes($template) as $node)
6374 $this->checkNode($node, $tag);
6375 }
6376 public function detectUnknownAttributes()
6377 {
6378 $this->ignoreUnknownAttributes = \false;
6379 }
6380 public function ignoreUnknownAttributes()
6381 {
6382 $this->ignoreUnknownAttributes = \true;
6383 }
6384 protected function checkAttribute(DOMNode $node, Tag $tag, $attrName)
6385 {
6386 if (!isset($tag->attributes[$attrName]))
6387 {
6388 if ($this->ignoreUnknownAttributes)
6389 return;
6390 throw new UnsafeTemplateException("Cannot assess the safety of unknown attribute '" . $attrName . "'", $node);
6391 }
6392 if (!$this->tagFiltersAttributes($tag) || !$this->isSafe($tag->attributes[$attrName]))
6393 throw new UnsafeTemplateException("Attribute '" . $attrName . "' is not properly sanitized to be used in this context", $node);
6394 }
6395 protected function checkAttributeNode(DOMAttr $attribute, Tag $tag)
6396 {
6397 foreach (AVTHelper::parse($attribute->value) as $token)
6398 if ($token[0] === 'expression')
6399 $this->checkExpression($attribute, $token[1], $tag);
6400 }
6401 protected function checkContext(DOMNode $node)
6402 {
6403 $xpath = new DOMXPath($node->ownerDocument);
6404 $ancestors = $xpath->query('ancestor::xsl:for-each', $node);
6405 if ($ancestors->length)
6406 throw new UnsafeTemplateException("Cannot assess context due to '" . $ancestors->item(0)->nodeName . "'", $node);
6407 }
6408 protected function checkCopyOfNode(DOMElement $node, Tag $tag)
6409 {
6410 $this->checkSelectNode($node->getAttributeNode('select'), $tag);
6411 }
6412 protected function checkElementNode(DOMElement $element, Tag $tag)
6413 {
6414 $xpath = new DOMXPath($element->ownerDocument);
6415 $predicate = ($element->localName === 'attribute') ? '' : '[not(ancestor::xsl:attribute)]';
6416 $query = './/xsl:value-of' . $predicate;
6417 foreach ($xpath->query($query, $element) as $valueOf)
6418 $this->checkSelectNode($valueOf->getAttributeNode('select'), $tag);
6419 $query = './/xsl:apply-templates' . $predicate;
6420 foreach ($xpath->query($query, $element) as $applyTemplates)
6421 throw new UnsafeTemplateException('Cannot allow unfiltered data in this context', $applyTemplates);
6422 }
6423 protected function checkExpression(DOMNode $node, $expr, Tag $tag)
6424 {
6425 $this->checkContext($node);
6426 if (\preg_match('/^\\$(\\w+)$/', $expr, $m))
6427 {
6428 $this->checkVariable($node, $tag, $m[1]);
6429 return;
6430 }
6431 if ($this->isExpressionSafe($expr))
6432 return;
6433 if (\preg_match('/^@(\\w+)$/', $expr, $m))
6434 {
6435 $this->checkAttribute($node, $tag, $m[1]);
6436 return;
6437 }
6438 throw new UnsafeTemplateException("Cannot assess the safety of expression '" . $expr . "'", $node);
6439 }
6440 protected function checkNode(DOMNode $node, Tag $tag)
6441 {
6442 if ($node instanceof DOMAttr)
6443 $this->checkAttributeNode($node, $tag);
6444 elseif ($node instanceof DOMElement)
6445 if ($node->namespaceURI === self::XMLNS_XSL
6446 && $node->localName === 'copy-of')
6447 $this->checkCopyOfNode($node, $tag);
6448 else
6449 $this->checkElementNode($node, $tag);
6450 }
6451 protected function checkVariable(DOMNode $node, $tag, $qname)
6452 {
6453 $this->checkVariableDeclaration($node, $tag, 'xsl:param[@name="' . $qname . '"]');
6454 $this->checkVariableDeclaration($node, $tag, 'xsl:variable[@name="' . $qname . '"]');
6455 }
6456 protected function checkVariableDeclaration(DOMNode $node, $tag, $query)
6457 {
6458 $query = 'ancestor-or-self::*/preceding-sibling::' . $query . '[@select]';
6459 $xpath = new DOMXPath($node->ownerDocument);
6460 foreach ($xpath->query($query, $node) as $varNode)
6461 {
6462 try
6463 {
6464 $this->checkExpression($varNode, $varNode->getAttribute('select'), $tag);
6465 }
6466 catch (UnsafeTemplateException $e)
6467 {
6468 $e->setNode($node);
6469 throw $e;
6470 }
6471 }
6472 }
6473 protected function checkSelectNode(DOMAttr $select, Tag $tag)
6474 {
6475 $this->checkExpression($select, $select->value, $tag);
6476 }
6477 protected function isExpressionSafe($expr)
6478 {
6479 return \false;
6480 }
6481 protected function tagFiltersAttributes(Tag $tag)
6482 {
6483 return $tag->filterChain->containsCallback('s9e\\TextFormatter\\Parser::filterAttributes');
6484 }
6485 }
6486
6487 /*
6488 * @package s9e\TextFormatter
6489 * @copyright Copyright (c) 2010-2016 The s9e Authors
6490 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6491 */
6492 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6493 use DOMElement;
6494 use DOMNode;
6495 use DOMXPath;
6496 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6497 use s9e\TextFormatter\Configurator\Items\Tag;
6498 use s9e\TextFormatter\Configurator\TemplateCheck;
6499 abstract class AbstractFlashRestriction extends TemplateCheck
6500 {
6501 public $defaultSetting;
6502 public $maxSetting;
6503 public $onlyIfDynamic;
6504 protected $settingName;
6505 protected $settings;
6506 protected $template;
6507 public function __construct($maxSetting, $onlyIfDynamic = \false)
6508 {
6509 $this->maxSetting = $maxSetting;
6510 $this->onlyIfDynamic = $onlyIfDynamic;
6511 }
6512 public function check(DOMElement $template, Tag $tag)
6513 {
6514 $this->template = $template;
6515 $this->checkEmbeds();
6516 $this->checkObjects();
6517 }
6518 protected function checkAttributes(DOMElement $embed)
6519 {
6520 $settingName = \strtolower($this->settingName);
6521 $useDefault = \true;
6522 foreach ($embed->attributes as $attribute)
6523 {
6524 $attrName = \strtolower($attribute->name);
6525 if ($attrName === $settingName)
6526 {
6527 $this->checkSetting($attribute, $attribute->value);
6528 $useDefault = \false;
6529 }
6530 }
6531 if ($useDefault)
6532 $this->checkSetting($embed, $this->defaultSetting);
6533 }
6534 protected function checkDynamicAttributes(DOMElement $embed)
6535 {
6536 $settingName = \strtolower($this->settingName);
6537 foreach ($embed->getElementsByTagNameNS(self::XMLNS_XSL, 'attribute') as $attribute)
6538 {
6539 $attrName = \strtolower($attribute->getAttribute('name'));
6540 if ($attrName === $settingName)
6541 throw new UnsafeTemplateException('Cannot assess the safety of dynamic attributes', $attribute);
6542 }
6543 }
6544 protected function checkDynamicParams(DOMElement $object)
6545 {
6546 foreach ($this->getObjectParams($object) as $param)
6547 foreach ($param->getElementsByTagNameNS(self::XMLNS_XSL, 'attribute') as $attribute)
6548 if (\strtolower($attribute->getAttribute('name')) === 'value')
6549 throw new UnsafeTemplateException('Cannot assess the safety of dynamic attributes', $attribute);
6550 }
6551 protected function checkEmbeds()
6552 {
6553 foreach ($this->getElements('embed') as $embed)
6554 {
6555 $this->checkDynamicAttributes($embed);
6556 $this->checkAttributes($embed);
6557 }
6558 }
6559 protected function checkObjects()
6560 {
6561 foreach ($this->getElements('object') as $object)
6562 {
6563 $this->checkDynamicParams($object);
6564 $params = $this->getObjectParams($object);
6565 foreach ($params as $param)
6566 $this->checkSetting($param, $param->getAttribute('value'));
6567 if (empty($params))
6568 $this->checkSetting($object, $this->defaultSetting);
6569 }
6570 }
6571 protected function checkSetting(DOMNode $node, $setting)
6572 {
6573 if (!isset($this->settings[\strtolower($setting)]))
6574 {
6575 if (\preg_match('/(?<!\\{)\\{(?:\\{\\{)*(?!\\{)/', $setting))
6576 throw new UnsafeTemplateException('Cannot assess ' . $this->settingName . " setting '" . $setting . "'", $node);
6577 throw new UnsafeTemplateException('Unknown ' . $this->settingName . " value '" . $setting . "'", $node);
6578 }
6579 $value = $this->settings[\strtolower($setting)];
6580 $maxValue = $this->settings[\strtolower($this->maxSetting)];
6581 if ($value > $maxValue)
6582 throw new UnsafeTemplateException($this->settingName . " setting '" . $setting . "' exceeds restricted value '" . $this->maxSetting . "'", $node);
6583 }
6584 protected function isDynamic(DOMElement $node)
6585 {
6586 if ($node->getElementsByTagNameNS(self::XMLNS_XSL, '*')->length)
6587 return \true;
6588 $xpath = new DOMXPath($node->ownerDocument);
6589 $query = './/@*[contains(., "{")]';
6590 foreach ($xpath->query($query, $node) as $attribute)
6591 if (\preg_match('/(?<!\\{)\\{(?:\\{\\{)*(?!\\{)/', $attribute->value))
6592 return \true;
6593 return \false;
6594 }
6595 protected function getElements($tagName)
6596 {
6597 $nodes = array();
6598 foreach ($this->template->ownerDocument->getElementsByTagName($tagName) as $node)
6599 if (!$this->onlyIfDynamic || $this->isDynamic($node))
6600 $nodes[] = $node;
6601 return $nodes;
6602 }
6603 protected function getObjectParams(DOMElement $object)
6604 {
6605 $params = array();
6606 $settingName = \strtolower($this->settingName);
6607 foreach ($object->getElementsByTagName('param') as $param)
6608 {
6609 $paramName = \strtolower($param->getAttribute('name'));
6610 if ($paramName === $settingName && $param->parentNode->isSameNode($object))
6611 $params[] = $param;
6612 }
6613 return $params;
6614 }
6615 }
6616
6617 /*
6618 * @package s9e\TextFormatter
6619 * @copyright Copyright (c) 2010-2016 The s9e Authors
6620 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6621 */
6622 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6623 use DOMElement;
6624 use DOMXPath;
6625 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6626 use s9e\TextFormatter\Configurator\Items\Tag;
6627 use s9e\TextFormatter\Configurator\TemplateCheck;
6628 class DisallowAttributeSets extends TemplateCheck
6629 {
6630 public function check(DOMElement $template, Tag $tag)
6631 {
6632 $xpath = new DOMXPath($template->ownerDocument);
6633 $nodes = $xpath->query('//@use-attribute-sets');
6634 if ($nodes->length)
6635 throw new UnsafeTemplateException('Cannot assess the safety of attribute sets', $nodes->item(0));
6636 }
6637 }
6638
6639 /*
6640 * @package s9e\TextFormatter
6641 * @copyright Copyright (c) 2010-2016 The s9e Authors
6642 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6643 */
6644 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6645 use DOMElement;
6646 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6647 use s9e\TextFormatter\Configurator\Items\Tag;
6648 use s9e\TextFormatter\Configurator\TemplateCheck;
6649 class DisallowCopy extends TemplateCheck
6650 {
6651 public function check(DOMElement $template, Tag $tag)
6652 {
6653 $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'copy');
6654 $node = $nodes->item(0);
6655 if ($node)
6656 throw new UnsafeTemplateException("Cannot assess the safety of an '" . $node->nodeName . "' element", $node);
6657 }
6658 }
6659
6660 /*
6661 * @package s9e\TextFormatter
6662 * @copyright Copyright (c) 2010-2016 The s9e Authors
6663 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6664 */
6665 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6666 use DOMElement;
6667 use DOMXPath;
6668 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6669 use s9e\TextFormatter\Configurator\Items\Tag;
6670 use s9e\TextFormatter\Configurator\TemplateCheck;
6671 class DisallowDisableOutputEscaping extends TemplateCheck
6672 {
6673 public function check(DOMElement $template, Tag $tag)
6674 {
6675 $xpath = new DOMXPath($template->ownerDocument);
6676 $node = $xpath->query('//@disable-output-escaping')->item(0);
6677 if ($node)
6678 throw new UnsafeTemplateException("The template contains a 'disable-output-escaping' attribute", $node);
6679 }
6680 }
6681
6682 /*
6683 * @package s9e\TextFormatter
6684 * @copyright Copyright (c) 2010-2016 The s9e Authors
6685 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6686 */
6687 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6688 use DOMElement;
6689 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6690 use s9e\TextFormatter\Configurator\Items\Tag;
6691 use s9e\TextFormatter\Configurator\TemplateCheck;
6692 class DisallowDynamicAttributeNames extends TemplateCheck
6693 {
6694 public function check(DOMElement $template, Tag $tag)
6695 {
6696 $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'attribute');
6697 foreach ($nodes as $node)
6698 if (\strpos($node->getAttribute('name'), '{') !== \false)
6699 throw new UnsafeTemplateException('Dynamic <xsl:attribute/> names are disallowed', $node);
6700 }
6701 }
6702
6703 /*
6704 * @package s9e\TextFormatter
6705 * @copyright Copyright (c) 2010-2016 The s9e Authors
6706 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6707 */
6708 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6709 use DOMElement;
6710 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6711 use s9e\TextFormatter\Configurator\Items\Tag;
6712 use s9e\TextFormatter\Configurator\TemplateCheck;
6713 class DisallowDynamicElementNames extends TemplateCheck
6714 {
6715 public function check(DOMElement $template, Tag $tag)
6716 {
6717 $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'element');
6718 foreach ($nodes as $node)
6719 if (\strpos($node->getAttribute('name'), '{') !== \false)
6720 throw new UnsafeTemplateException('Dynamic <xsl:element/> names are disallowed', $node);
6721 }
6722 }
6723
6724 /*
6725 * @package s9e\TextFormatter
6726 * @copyright Copyright (c) 2010-2016 The s9e Authors
6727 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6728 */
6729 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6730 use DOMElement;
6731 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6732 use s9e\TextFormatter\Configurator\Items\Tag;
6733 use s9e\TextFormatter\Configurator\TemplateCheck;
6734 class DisallowElementNS extends TemplateCheck
6735 {
6736 public $elName;
6737 public $namespaceURI;
6738 public function __construct($namespaceURI, $elName)
6739 {
6740 $this->namespaceURI = $namespaceURI;
6741 $this->elName = $elName;
6742 }
6743 public function check(DOMElement $template, Tag $tag)
6744 {
6745 $node = $template->getElementsByTagNameNS($this->namespaceURI, $this->elName)->item(0);
6746 if ($node)
6747 throw new UnsafeTemplateException("Element '" . $node->nodeName . "' is disallowed", $node);
6748 }
6749 }
6750
6751 /*
6752 * @package s9e\TextFormatter
6753 * @copyright Copyright (c) 2010-2016 The s9e Authors
6754 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6755 */
6756 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6757 use DOMElement;
6758 use DOMXPath;
6759 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6760 use s9e\TextFormatter\Configurator\Items\Tag;
6761 use s9e\TextFormatter\Configurator\TemplateCheck;
6762 class DisallowObjectParamsWithGeneratedName extends TemplateCheck
6763 {
6764 public function check(DOMElement $template, Tag $tag)
6765 {
6766 $xpath = new DOMXPath($template->ownerDocument);
6767 $query = '//object//param[contains(@name, "{") or .//xsl:attribute[translate(@name, "NAME", "name") = "name"]]';
6768 $nodes = $xpath->query($query);
6769 foreach ($nodes as $node)
6770 throw new UnsafeTemplateException("A 'param' element with a suspect name has been found", $node);
6771 }
6772 }
6773
6774 /*
6775 * @package s9e\TextFormatter
6776 * @copyright Copyright (c) 2010-2016 The s9e Authors
6777 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6778 */
6779 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6780 use DOMElement;
6781 use DOMXPath;
6782 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6783 use s9e\TextFormatter\Configurator\Items\Tag;
6784 use s9e\TextFormatter\Configurator\TemplateCheck;
6785 class DisallowPHPTags extends TemplateCheck
6786 {
6787 public function check(DOMElement $template, Tag $tag)
6788 {
6789 $queries = array(
6790 '//processing-instruction()["php" = translate(name(),"HP","hp")]'
6791 => 'PHP tags are not allowed in the template',
6792 '//script["php" = translate(@language,"HP","hp")]'
6793 => 'PHP tags are not allowed in the template',
6794 '//xsl:processing-instruction["php" = translate(@name,"HP","hp")]'
6795 => 'PHP tags are not allowed in the output',
6796 '//xsl:processing-instruction[contains(@name, "{")]'
6797 => 'Dynamic processing instructions are not allowed',
6798 );
6799 $xpath = new DOMXPath($template->ownerDocument);
6800 foreach ($queries as $query => $error)
6801 {
6802 $nodes = $xpath->query($query);
6803 if ($nodes->length)
6804 throw new UnsafeTemplateException($error, $nodes->item(0));
6805 }
6806 }
6807 }
6808
6809 /*
6810 * @package s9e\TextFormatter
6811 * @copyright Copyright (c) 2010-2016 The s9e Authors
6812 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6813 */
6814 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6815 use DOMElement;
6816 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6817 use s9e\TextFormatter\Configurator\Items\Tag;
6818 use s9e\TextFormatter\Configurator\TemplateCheck;
6819 class DisallowUnsafeCopyOf extends TemplateCheck
6820 {
6821 public function check(DOMElement $template, Tag $tag)
6822 {
6823 $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'copy-of');
6824 foreach ($nodes as $node)
6825 {
6826 $expr = $node->getAttribute('select');
6827 if (!\preg_match('#^@[-\\w]*$#D', $expr))
6828 throw new UnsafeTemplateException("Cannot assess the safety of '" . $node->nodeName . "' select expression '" . $expr . "'", $node);
6829 }
6830 }
6831 }
6832
6833 /*
6834 * @package s9e\TextFormatter
6835 * @copyright Copyright (c) 2010-2016 The s9e Authors
6836 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6837 */
6838 namespace s9e\TextFormatter\Configurator\TemplateChecks;
6839 use DOMElement;
6840 use DOMXPath;
6841 use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
6842 use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
6843 use s9e\TextFormatter\Configurator\Items\Tag;
6844 use s9e\TextFormatter\Configurator\TemplateCheck;
6845 class DisallowXPathFunction extends TemplateCheck
6846 {
6847 public $funcName;
6848 public function __construct($funcName)
6849 {
6850 $this->funcName = $funcName;
6851 }
6852 public function check(DOMElement $template, Tag $tag)
6853 {
6854 $regexp = '#(?!<\\pL)' . \preg_quote($this->funcName, '#') . '\\s*\\(#iu';
6855 $regexp = \str_replace('\\:', '\\s*:\\s*', $regexp);
6856 foreach ($this->getExpressions($template) as $expr => $node)
6857 {
6858 $expr = \preg_replace('#([\'"]).*?\\1#s', '', $expr);
6859 if (\preg_match($regexp, $expr))
6860 throw new UnsafeTemplateException('An XPath expression uses the ' . $this->funcName . '() function', $node);
6861 }
6862 }
6863 protected function getExpressions(DOMElement $template)
6864 {
6865 $xpath = new DOMXPath($template->ownerDocument);
6866 $exprs = array();
6867 foreach ($xpath->query('//@*') as $attribute)
6868 if ($attribute->parentNode->namespaceURI === self::XMLNS_XSL)
6869 {
6870 $expr = $attribute->value;
6871 $exprs[$expr] = $attribute;
6872 }
6873 else
6874 foreach (AVTHelper::parse($attribute->value) as $token)
6875 if ($token[0] === 'expression')
6876 $exprs[$token[1]] = $attribute;
6877 return $exprs;
6878 }
6879 }
6880
6881 /*
6882 * @package s9e\TextFormatter
6883 * @copyright Copyright (c) 2010-2016 The s9e Authors
6884 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6885 */
6886 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
6887 use DOMAttr;
6888 use DOMElement;
6889 use DOMXPath;
6890 use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
6891 use s9e\TextFormatter\Configurator\TemplateNormalization;
6892 abstract class AbstractConstantFolding extends TemplateNormalization
6893 {
6894 abstract protected function getOptimizationPasses();
6895 public function normalize(DOMElement $template)
6896 {
6897 $xpath = new DOMXPath($template->ownerDocument);
6898 $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(.,"{")]';
6899 foreach ($xpath->query($query) as $attribute)
6900 $this->replaceAVT($attribute);
6901 foreach ($template->getElementsByTagNameNS(self::XMLNS_XSL, 'value-of') as $valueOf)
6902 $this->replaceValueOf($valueOf);
6903 }
6904 public function evaluateExpression($expr)
6905 {
6906 $original = $expr;
6907 foreach ($this->getOptimizationPasses() as $regexp => $methodName)
6908 {
6909 $regexp = \str_replace(' ', '\\s*', $regexp);
6910 $expr = \preg_replace_callback($regexp, array($this, $methodName), $expr);
6911 }
6912 return ($expr === $original) ? $expr : $this->evaluateExpression(\trim($expr));
6913 }
6914 protected function replaceAVT(DOMAttr $attribute)
6915 {
6916 $_this = $this;
6917 AVTHelper::replace(
6918 $attribute,
6919 function ($token) use ($_this)
6920 {
6921 if ($token[0] === 'expression')
6922 $token[1] = $_this->evaluateExpression($token[1]);
6923 return $token;
6924 }
6925 );
6926 }
6927 protected function replaceValueOf(DOMElement $valueOf)
6928 {
6929 $valueOf->setAttribute('select', $this->evaluateExpression($valueOf->getAttribute('select')));
6930 }
6931 }
6932
6933 /*
6934 * @package s9e\TextFormatter
6935 * @copyright Copyright (c) 2010-2016 The s9e Authors
6936 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6937 */
6938 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
6939 use DOMAttr;
6940 use DOMElement;
6941 use DOMXPath;
6942 use s9e\TextFormatter\Configurator\TemplateNormalization;
6943 class FixUnescapedCurlyBracesInHtmlAttributes extends TemplateNormalization
6944 {
6945 public function normalize(DOMElement $template)
6946 {
6947 $dom = $template->ownerDocument;
6948 $xpath = new DOMXPath($dom);
6949 $query = '//@*[contains(., "{")]';
6950 foreach ($xpath->query($query) as $attribute)
6951 $this->fixAttribute($attribute);
6952 }
6953 protected function fixAttribute(DOMAttr $attribute)
6954 {
6955 $parentNode = $attribute->parentNode;
6956 if ($parentNode->namespaceURI === self::XMLNS_XSL)
6957 return;
6958 $attribute->value = \htmlspecialchars(
6959 \preg_replace(
6960 '(\\b(?:do|else|(?:if|while)\\s*\\(.*?\\))\\s*\\{(?![{@]))',
6961 '$0{',
6962 $attribute->value
6963 ),
6964 \ENT_NOQUOTES,
6965 'UTF-8'
6966 );
6967 }
6968 }
6969
6970 /*
6971 * @package s9e\TextFormatter
6972 * @copyright Copyright (c) 2010-2016 The s9e Authors
6973 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6974 */
6975 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
6976 use DOMElement;
6977 use DOMException;
6978 use DOMText;
6979 use DOMXPath;
6980 use s9e\TextFormatter\Configurator\TemplateNormalization;
6981 class InlineAttributes extends TemplateNormalization
6982 {
6983 public function normalize(DOMElement $template)
6984 {
6985 $xpath = new DOMXPath($template->ownerDocument);
6986 $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/xsl:attribute';
6987 foreach ($xpath->query($query) as $attribute)
6988 $this->inlineAttribute($attribute);
6989 }
6990 protected function inlineAttribute(DOMElement $attribute)
6991 {
6992 $value = '';
6993 foreach ($attribute->childNodes as $node)
6994 if ($node instanceof DOMText
6995 || array($node->namespaceURI, $node->localName) === array(self::XMLNS_XSL, 'text'))
6996 $value .= \preg_replace('([{}])', '$0$0', $node->textContent);
6997 elseif (array($node->namespaceURI, $node->localName) === array(self::XMLNS_XSL, 'value-of'))
6998 $value .= '{' . $node->getAttribute('select') . '}';
6999 else
7000 return;
7001 $attribute->parentNode->setAttribute($attribute->getAttribute('name'), $value);
7002 $attribute->parentNode->removeChild($attribute);
7003 }
7004 }
7005
7006 /*
7007 * @package s9e\TextFormatter
7008 * @copyright Copyright (c) 2010-2016 The s9e Authors
7009 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7010 */
7011 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7012 use DOMElement;
7013 use DOMXPath;
7014 use s9e\TextFormatter\Configurator\TemplateNormalization;
7015 class InlineCDATA extends TemplateNormalization
7016 {
7017 public function normalize(DOMElement $template)
7018 {
7019 $dom = $template->ownerDocument;
7020 $xpath = new DOMXPath($dom);
7021 foreach ($xpath->query('//text()') as $textNode)
7022 if ($textNode->nodeType === \XML_CDATA_SECTION_NODE)
7023 $textNode->parentNode->replaceChild(
7024 $dom->createTextNode($textNode->textContent),
7025 $textNode
7026 );
7027 }
7028 }
7029
7030 /*
7031 * @package s9e\TextFormatter
7032 * @copyright Copyright (c) 2010-2016 The s9e Authors
7033 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7034 */
7035 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7036 use DOMElement;
7037 use DOMException;
7038 use s9e\TextFormatter\Configurator\TemplateNormalization;
7039 class InlineElements extends TemplateNormalization
7040 {
7041 public function normalize(DOMElement $template)
7042 {
7043 $dom = $template->ownerDocument;
7044 foreach ($template->getElementsByTagNameNS(self::XMLNS_XSL, 'element') as $element)
7045 {
7046 $elName = $element->getAttribute('name');
7047 try
7048 {
7049 $newElement = ($element->hasAttribute('namespace'))
7050 ? $dom->createElementNS($element->getAttribute('namespace'), $elName)
7051 : $dom->createElement($elName);
7052 }
7053 catch (DOMException $e)
7054 {
7055 continue;
7056 }
7057 $element->parentNode->replaceChild($newElement, $element);
7058 while ($element->firstChild)
7059 $newElement->appendChild($element->removeChild($element->firstChild));
7060 }
7061 }
7062 }
7063
7064 /*
7065 * @package s9e\TextFormatter
7066 * @copyright Copyright (c) 2010-2016 The s9e Authors
7067 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7068 */
7069 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7070 use DOMAttr;
7071 use DOMElement;
7072 use DOMNode;
7073 use DOMXPath;
7074 use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
7075 use s9e\TextFormatter\Configurator\Helpers\TemplateParser;
7076 use s9e\TextFormatter\Configurator\TemplateNormalization;
7077 class InlineInferredValues extends TemplateNormalization
7078 {
7079 public function normalize(DOMElement $template)
7080 {
7081 $xpath = new DOMXPath($template->ownerDocument);
7082 $query = '//xsl:if | //xsl:when';
7083 foreach ($xpath->query($query) as $node)
7084 {
7085 $map = TemplateParser::parseEqualityExpr($node->getAttribute('test'));
7086 if ($map === \false || \count($map) !== 1 || \count($map[\key($map)]) !== 1)
7087 continue;
7088 $expr = \key($map);
7089 $value = \end($map[$expr]);
7090 $this->inlineInferredValue($node, $expr, $value);
7091 }
7092 }
7093 protected function inlineInferredValue(DOMNode $node, $expr, $value)
7094 {
7095 $xpath = new DOMXPath($node->ownerDocument);
7096 $query = './/xsl:value-of[@select="' . $expr . '"]';
7097 foreach ($xpath->query($query, $node) as $valueOf)
7098 $this->replaceValueOf($valueOf, $value);
7099 $query = './/*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{' . $expr . '}")]';
7100 foreach ($xpath->query($query, $node) as $attribute)
7101 $this->replaceAttribute($attribute, $expr, $value);
7102 }
7103 protected function replaceAttribute(DOMAttr $attribute, $expr, $value)
7104 {
7105 AVTHelper::replace(
7106 $attribute,
7107 function ($token) use ($expr, $value)
7108 {
7109 if ($token[0] === 'expression' && $token[1] === $expr)
7110 $token = array('literal', $value);
7111 return $token;
7112 }
7113 );
7114 }
7115 protected function replaceValueOf(DOMElement $valueOf, $value)
7116 {
7117 $valueOf->parentNode->replaceChild(
7118 $valueOf->ownerDocument->createTextNode($value),
7119 $valueOf
7120 );
7121 }
7122 }
7123
7124 /*
7125 * @package s9e\TextFormatter
7126 * @copyright Copyright (c) 2010-2016 The s9e Authors
7127 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7128 */
7129 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7130 use DOMElement;
7131 use DOMXPath;
7132 use s9e\TextFormatter\Configurator\TemplateNormalization;
7133 class InlineTextElements extends TemplateNormalization
7134 {
7135 public function normalize(DOMElement $template)
7136 {
7137 $dom = $template->ownerDocument;
7138 $xpath = new DOMXPath($dom);
7139 foreach ($xpath->query('//xsl:text') as $node)
7140 {
7141 if (\trim($node->textContent) === '')
7142 if ($node->previousSibling && $node->previousSibling->nodeType === \XML_TEXT_NODE)
7143 ;
7144 elseif ($node->nextSibling && $node->nextSibling->nodeType === \XML_TEXT_NODE)
7145 ;
7146 else
7147 continue;
7148 $node->parentNode->replaceChild(
7149 $dom->createTextNode($node->textContent),
7150 $node
7151 );
7152 }
7153 }
7154 }
7155
7156 /*
7157 * @package s9e\TextFormatter
7158 * @copyright Copyright (c) 2010-2016 The s9e Authors
7159 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7160 */
7161 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7162 use DOMElement;
7163 use DOMXPath;
7164 use s9e\TextFormatter\Configurator\TemplateNormalization;
7165 use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
7166 class InlineXPathLiterals extends TemplateNormalization
7167 {
7168 public function normalize(DOMElement $template)
7169 {
7170 $_this = $this;
7171 $xpath = new DOMXPath($template->ownerDocument);
7172 foreach ($xpath->query('//xsl:value-of') as $valueOf)
7173 {
7174 $textContent = $this->getTextContent($valueOf->getAttribute('select'));
7175 if ($textContent !== \false)
7176 $this->replaceElement($valueOf, $textContent);
7177 }
7178 $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{")]';
7179 foreach ($xpath->query($query) as $attribute)
7180 {
7181 AVTHelper::replace(
7182 $attribute,
7183 function ($token) use ($_this)
7184 {
7185 if ($token[0] === 'expression')
7186 {
7187 $textContent = $_this->getTextContent($token[1]);
7188 if ($textContent !== \false)
7189 $token = array('literal', $textContent);
7190 }
7191 return $token;
7192 }
7193 );
7194 }
7195 }
7196 public function getTextContent($expr)
7197 {
7198 $expr = \trim($expr);
7199 if (\preg_match('(^(?:\'[^\']*\'|"[^"]*")$)', $expr))
7200 return \substr($expr, 1, -1);
7201 if (\preg_match('(^0*([0-9]+(?:\\.[0-9]+)?)$)', $expr, $m))
7202 return $m[1];
7203 return \false;
7204 }
7205 protected function replaceElement(DOMElement $valueOf, $textContent)
7206 {
7207 $valueOf->parentNode->replaceChild(
7208 $valueOf->ownerDocument->createTextNode($textContent),
7209 $valueOf
7210 );
7211 }
7212 }
7213
7214 /*
7215 * @package s9e\TextFormatter
7216 * @copyright Copyright (c) 2010-2016 The s9e Authors
7217 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7218 */
7219 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7220 use DOMElement;
7221 use DOMXPath;
7222 use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
7223 use s9e\TextFormatter\Configurator\Helpers\XPathHelper;
7224 use s9e\TextFormatter\Configurator\TemplateNormalization;
7225 class MinifyXPathExpressions extends TemplateNormalization
7226 {
7227 public function normalize(DOMElement $template)
7228 {
7229 $xpath = new DOMXPath($template->ownerDocument);
7230 $query = '//xsl:*/@*[contains(., " ")][contains("matchselectest", name())]';
7231 foreach ($xpath->query($query) as $attribute)
7232 $attribute->parentNode->setAttribute(
7233 $attribute->nodeName,
7234 XPathHelper::minify($attribute->nodeValue)
7235 );
7236 $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., " ")]';
7237 foreach ($xpath->query($query) as $attribute)
7238 {
7239 AVTHelper::replace(
7240 $attribute,
7241 function ($token)
7242 {
7243 if ($token[0] === 'expression')
7244 $token[1] = XPathHelper::minify($token[1]);
7245 return $token;
7246 }
7247 );
7248 }
7249 }
7250 }
7251
7252 /*
7253 * @package s9e\TextFormatter
7254 * @copyright Copyright (c) 2010-2016 The s9e Authors
7255 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7256 */
7257 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7258 use DOMElement;
7259 use DOMXPath;
7260 use s9e\TextFormatter\Configurator\TemplateNormalization;
7261 class NormalizeAttributeNames extends TemplateNormalization
7262 {
7263 public function normalize(DOMElement $template)
7264 {
7265 $xpath = new DOMXPath($template->ownerDocument);
7266 foreach ($xpath->query('.//@*', $template) as $attribute)
7267 {
7268 $attrName = self::lowercase($attribute->localName);
7269 if ($attrName !== $attribute->localName)
7270 {
7271 $attribute->parentNode->setAttribute($attrName, $attribute->value);
7272 $attribute->parentNode->removeAttributeNode($attribute);
7273 }
7274 }
7275 foreach ($xpath->query('//xsl:attribute[not(contains(@name, "{"))]') as $attribute)
7276 {
7277 $attrName = self::lowercase($attribute->getAttribute('name'));
7278 $attribute->setAttribute('name', $attrName);
7279 }
7280 }
7281 }
7282
7283 /*
7284 * @package s9e\TextFormatter
7285 * @copyright Copyright (c) 2010-2016 The s9e Authors
7286 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7287 */
7288 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7289 use DOMElement;
7290 use DOMXPath;
7291 use s9e\TextFormatter\Configurator\TemplateNormalization;
7292 class NormalizeElementNames extends TemplateNormalization
7293 {
7294 public function normalize(DOMElement $template)
7295 {
7296 $dom = $template->ownerDocument;
7297 $xpath = new DOMXPath($dom);
7298 foreach ($xpath->query('//*[namespace-uri() != "' . self::XMLNS_XSL . '"]') as $element)
7299 {
7300 $elName = self::lowercase($element->localName);
7301 if ($elName === $element->localName)
7302 continue;
7303 $newElement = (\is_null($element->namespaceURI))
7304 ? $dom->createElement($elName)
7305 : $dom->createElementNS($element->namespaceURI, $elName);
7306 while ($element->firstChild)
7307 $newElement->appendChild($element->removeChild($element->firstChild));
7308 foreach ($element->attributes as $attribute)
7309 $newElement->setAttributeNS(
7310 $attribute->namespaceURI,
7311 $attribute->nodeName,
7312 $attribute->value
7313 );
7314 $element->parentNode->replaceChild($newElement, $element);
7315 }
7316 foreach ($xpath->query('//xsl:element[not(contains(@name, "{"))]') as $element)
7317 {
7318 $elName = self::lowercase($element->getAttribute('name'));
7319 $element->setAttribute('name', $elName);
7320 }
7321 }
7322 }
7323
7324 /*
7325 * @package s9e\TextFormatter
7326 * @copyright Copyright (c) 2010-2016 The s9e Authors
7327 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7328 */
7329 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7330 use DOMAttr;
7331 use DOMElement;
7332 use DOMXPath;
7333 use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
7334 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
7335 use s9e\TextFormatter\Configurator\TemplateNormalization;
7336 use s9e\TextFormatter\Parser\BuiltInFilters;
7337 class NormalizeUrls extends TemplateNormalization
7338 {
7339 public function normalize(DOMElement $template)
7340 {
7341 foreach (TemplateHelper::getURLNodes($template->ownerDocument) as $node)
7342 if ($node instanceof DOMAttr)
7343 $this->normalizeAttribute($node);
7344 elseif ($node instanceof DOMElement)
7345 $this->normalizeElement($node);
7346 }
7347 protected function normalizeAttribute(DOMAttr $attribute)
7348 {
7349 $tokens = AVTHelper::parse(\trim($attribute->value));
7350 $attrValue = '';
7351 foreach ($tokens as $_f6b3b659)
7352 {
7353 list($type, $content) = $_f6b3b659;
7354 if ($type === 'literal')
7355 $attrValue .= BuiltInFilters::sanitizeUrl($content);
7356 else
7357 $attrValue .= '{' . $content . '}';
7358 }
7359 $attrValue = $this->unescapeBrackets($attrValue);
7360 $attribute->value = \htmlspecialchars($attrValue);
7361 }
7362 protected function normalizeElement(DOMElement $element)
7363 {
7364 $xpath = new DOMXPath($element->ownerDocument);
7365 $query = './/text()[normalize-space() != ""]';
7366 foreach ($xpath->query($query, $element) as $i => $node)
7367 {
7368 $value = BuiltInFilters::sanitizeUrl($node->nodeValue);
7369 if (!$i)
7370 $value = $this->unescapeBrackets(\ltrim($value));
7371 $node->nodeValue = $value;
7372 }
7373 if (isset($node))
7374 $node->nodeValue = \rtrim($node->nodeValue);
7375 }
7376 protected function unescapeBrackets($url)
7377 {
7378 return \preg_replace('#^(\\w+://)%5B([-\\w:._%]+)%5D#i', '$1[$2]', $url);
7379 }
7380 }
7381
7382 /*
7383 * @package s9e\TextFormatter
7384 * @copyright Copyright (c) 2010-2016 The s9e Authors
7385 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7386 */
7387 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7388 use DOMElement;
7389 use DOMNode;
7390 use DOMXPath;
7391 use s9e\TextFormatter\Configurator\TemplateNormalization;
7392 class OptimizeChoose extends TemplateNormalization
7393 {
7394 protected $choose;
7395 protected $xpath;
7396 public function normalize(DOMElement $template)
7397 {
7398 $this->xpath = new DOMXPath($template->ownerDocument);
7399 foreach ($template->getElementsByTagNameNS(self::XMLNS_XSL, 'choose') as $choose)
7400 {
7401 $this->choose = $choose;
7402 $this->optimizeChooseElement();
7403 }
7404 }
7405 protected function adoptChildren(DOMElement $branch)
7406 {
7407 while ($branch->firstChild->firstChild)
7408 $branch->appendChild($branch->firstChild->removeChild($branch->firstChild->firstChild));
7409 $branch->removeChild($branch->firstChild);
7410 }
7411 protected function getAttributes(DOMElement $element)
7412 {
7413 $attributes = array();
7414 foreach ($element->attributes as $attribute)
7415 {
7416 $key = $attribute->namespaceURI . '#' . $attribute->nodeName;
7417 $attributes[$key] = $attribute->nodeValue;
7418 }
7419 return $attributes;
7420 }
7421 protected function getBranches()
7422 {
7423 $query = 'xsl:when|xsl:otherwise';
7424 $nodes = array();
7425 foreach ($this->xpath->query($query, $this->choose) as $node)
7426 $nodes[] = $node;
7427 return $nodes;
7428 }
7429 protected function hasNoContent()
7430 {
7431 $query = 'count(xsl:when/node() | xsl:otherwise/node())';
7432 return !$this->xpath->evaluate($query, $this->choose);
7433 }
7434 protected function hasOtherwise()
7435 {
7436 return (bool) $this->xpath->evaluate('count(xsl:otherwise)', $this->choose);
7437 }
7438 protected function isEqualNode(DOMNode $node1, DOMNode $node2)
7439 {
7440 return ($node1->ownerDocument->saveXML($node1) === $node2->ownerDocument->saveXML($node2));
7441 }
7442 protected function isEqualTag(DOMElement $el1, DOMElement $el2)
7443 {
7444 return ($el1->namespaceURI === $el2->namespaceURI && $el1->nodeName === $el2->nodeName && $this->getAttributes($el1) === $this->getAttributes($el2));
7445 }
7446 protected function matchBranches($childType)
7447 {
7448 $branches = $this->getBranches();
7449 if (!isset($branches[0]->$childType))
7450 return \false;
7451 $childNode = $branches[0]->$childType;
7452 foreach ($branches as $branch)
7453 if (!isset($branch->$childType) || !$this->isEqualNode($childNode, $branch->$childType))
7454 return \false;
7455 return \true;
7456 }
7457 protected function matchOnlyChild()
7458 {
7459 $branches = $this->getBranches();
7460 if (!isset($branches[0]->firstChild))
7461 return \false;
7462 $firstChild = $branches[0]->firstChild;
7463 foreach ($branches as $branch)
7464 {
7465 if ($branch->childNodes->length !== 1 || !($branch->firstChild instanceof DOMElement))
7466 return \false;
7467 if (!$this->isEqualTag($firstChild, $branch->firstChild))
7468 return \false;
7469 }
7470 return \true;
7471 }
7472 protected function moveFirstChildBefore()
7473 {
7474 $branches = $this->getBranches();
7475 $this->choose->parentNode->insertBefore(\array_pop($branches)->firstChild, $this->choose);
7476 foreach ($branches as $branch)
7477 $branch->removeChild($branch->firstChild);
7478 }
7479 protected function moveLastChildAfter()
7480 {
7481 $branches = $this->getBranches();
7482 $node = \array_pop($branches)->lastChild;
7483 if (isset($this->choose->nextSibling))
7484 $this->choose->parentNode->insertBefore($node, $this->choose->nextSibling);
7485 else
7486 $this->choose->parentNode->appendChild($node);
7487 foreach ($branches as $branch)
7488 $branch->removeChild($branch->lastChild);
7489 }
7490 protected function optimizeChooseElement()
7491 {
7492 if ($this->hasOtherwise())
7493 {
7494 $this->optimizeCommonFirstChild();
7495 $this->optimizeCommonLastChild();
7496 $this->optimizeCommonOnlyChild();
7497 $this->optimizeEmptyOtherwise();
7498 }
7499 if ($this->hasNoContent())
7500 $this->choose->parentNode->removeChild($this->choose);
7501 else
7502 $this->optimizeSingleBranch();
7503 }
7504 protected function optimizeCommonFirstChild()
7505 {
7506 while ($this->matchBranches('firstChild'))
7507 $this->moveFirstChildBefore();
7508 }
7509 protected function optimizeCommonLastChild()
7510 {
7511 while ($this->matchBranches('lastChild'))
7512 $this->moveLastChildAfter();
7513 }
7514 protected function optimizeCommonOnlyChild()
7515 {
7516 while ($this->matchOnlyChild())
7517 $this->reparentChild();
7518 }
7519 protected function optimizeEmptyOtherwise()
7520 {
7521 $query = 'xsl:otherwise[count(node()) = 0]';
7522 foreach ($this->xpath->query($query, $this->choose) as $otherwise)
7523 $this->choose->removeChild($otherwise);
7524 }
7525 protected function optimizeSingleBranch()
7526 {
7527 $query = 'count(xsl:when) = 1 and not(xsl:otherwise)';
7528 if (!$this->xpath->evaluate($query, $this->choose))
7529 return;
7530 $when = $this->xpath->query('xsl:when', $this->choose)->item(0);
7531 $if = $this->choose->ownerDocument->createElementNS(self::XMLNS_XSL, 'xsl:if');
7532 $if->setAttribute('test', $when->getAttribute('test'));
7533 while ($when->firstChild)
7534 $if->appendChild($when->removeChild($when->firstChild));
7535 $this->choose->parentNode->replaceChild($if, $this->choose);
7536 }
7537 protected function reparentChild()
7538 {
7539 $branches = $this->getBranches();
7540 $childNode = $branches[0]->firstChild->cloneNode();
7541 $childNode->appendChild($this->choose->parentNode->replaceChild($childNode, $this->choose));
7542 foreach ($branches as $branch)
7543 $this->adoptChildren($branch);
7544 }
7545 }
7546
7547 /*
7548 * @package s9e\TextFormatter
7549 * @copyright Copyright (c) 2010-2016 The s9e Authors
7550 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7551 */
7552 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7553 use DOMElement;
7554 use DOMXPath;
7555 use s9e\TextFormatter\Configurator\TemplateNormalization;
7556 class OptimizeConditionalAttributes extends TemplateNormalization
7557 {
7558 public function normalize(DOMElement $template)
7559 {
7560 $dom = $template->ownerDocument;
7561 $xpath = new DOMXPath($dom);
7562 $query = '//xsl:if'
7563 . "[starts-with(@test, '@')]"
7564 . '[count(descendant::node()) = 2][xsl:attribute[@name = substring(../@test, 2)][xsl:value-of[@select = ../../@test]]]';
7565 foreach ($xpath->query($query) as $if)
7566 {
7567 $copyOf = $dom->createElementNS(self::XMLNS_XSL, 'xsl:copy-of');
7568 $copyOf->setAttribute('select', $if->getAttribute('test'));
7569 $if->parentNode->replaceChild($copyOf, $if);
7570 }
7571 }
7572 }
7573
7574 /*
7575 * @package s9e\TextFormatter
7576 * @copyright Copyright (c) 2010-2016 The s9e Authors
7577 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7578 */
7579 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7580 use DOMElement;
7581 use DOMXPath;
7582 use s9e\TextFormatter\Configurator\TemplateNormalization;
7583 class OptimizeConditionalValueOf extends TemplateNormalization
7584 {
7585 public function normalize(DOMElement $template)
7586 {
7587 $xpath = new DOMXPath($template->ownerDocument);
7588 $query = '//xsl:if[count(descendant::node()) = 1]/xsl:value-of';
7589 foreach ($xpath->query($query) as $valueOf)
7590 {
7591 $if = $valueOf->parentNode;
7592 $test = $if->getAttribute('test');
7593 $select = $valueOf->getAttribute('select');
7594 if ($select !== $test
7595 || !\preg_match('#^@[-\\w]+$#D', $select))
7596 continue;
7597 $if->parentNode->replaceChild(
7598 $if->removeChild($valueOf),
7599 $if
7600 );
7601 }
7602 }
7603 }
7604
7605 /*
7606 * @package s9e\TextFormatter
7607 * @copyright Copyright (c) 2010-2016 The s9e Authors
7608 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7609 */
7610 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7611 use DOMElement;
7612 use DOMXPath;
7613 use s9e\TextFormatter\Configurator\TemplateNormalization;
7614 class PreserveSingleSpaces extends TemplateNormalization
7615 {
7616 public function normalize(DOMElement $template)
7617 {
7618 $dom = $template->ownerDocument;
7619 $xpath = new DOMXPath($dom);
7620 $query = '//text()[. = " "][not(parent::xsl:text)]';
7621 foreach ($xpath->query($query) as $textNode)
7622 $textNode->parentNode->replaceChild(
7623 $dom->createElementNS(self::XMLNS_XSL, 'text', ' '),
7624 $textNode
7625 );
7626 }
7627 }
7628
7629 /*
7630 * @package s9e\TextFormatter
7631 * @copyright Copyright (c) 2010-2016 The s9e Authors
7632 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7633 */
7634 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7635 use DOMElement;
7636 use DOMXPath;
7637 use s9e\TextFormatter\Configurator\TemplateNormalization;
7638 class RemoveComments extends TemplateNormalization
7639 {
7640 public function normalize(DOMElement $template)
7641 {
7642 $xpath = new DOMXPath($template->ownerDocument);
7643 foreach ($xpath->query('//comment()') as $comment)
7644 $comment->parentNode->removeChild($comment);
7645 }
7646 }
7647
7648 /*
7649 * @package s9e\TextFormatter
7650 * @copyright Copyright (c) 2010-2016 The s9e Authors
7651 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7652 */
7653 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7654 use DOMElement;
7655 use DOMXPath;
7656 use s9e\TextFormatter\Configurator\TemplateNormalization;
7657 class RemoveInterElementWhitespace extends TemplateNormalization
7658 {
7659 public function normalize(DOMElement $template)
7660 {
7661 $xpath = new DOMXPath($template->ownerDocument);
7662 $query = '//text()[normalize-space() = ""][. != " "][not(parent::xsl:text)]';
7663 foreach ($xpath->query($query) as $textNode)
7664 $textNode->parentNode->removeChild($textNode);
7665 }
7666 }
7667
7668 /*
7669 * @package s9e\TextFormatter
7670 * @copyright Copyright (c) 2010-2016 The s9e Authors
7671 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7672 */
7673 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
7674 use DOMElement;
7675 use DOMNodeList;
7676 use s9e\TextFormatter\Configurator\TemplateNormalization;
7677 class SetRelNoreferrerOnTargetedLinks extends TemplateNormalization
7678 {
7679 public function normalize(DOMElement $template)
7680 {
7681 $this->normalizeElements($template->ownerDocument->getElementsByTagName('a'));
7682 $this->normalizeElements($template->ownerDocument->getElementsByTagName('area'));
7683 }
7684 protected function addRelAttribute(DOMElement $element)
7685 {
7686 $rel = $element->getAttribute('rel');
7687 if (\preg_match('(\\S$)', $rel))
7688 $rel .= ' ';
7689 $rel .= 'noreferrer';
7690 $element->setAttribute('rel', $rel);
7691 }
7692 protected function linkTargetCanAccessOpener(DOMElement $element)
7693 {
7694 if (!$element->hasAttribute('target'))
7695 return \false;
7696 if (\preg_match('(\\bno(?:open|referr)er\\b)', $element->getAttribute('rel')))
7697 return \false;
7698 return \true;
7699 }
7700 protected function normalizeElements(DOMNodeList $elements)
7701 {
7702 foreach ($elements as $element)
7703 if ($this->linkTargetCanAccessOpener($element))
7704 $this->addRelAttribute($element);
7705 }
7706 }
7707
7708 /*
7709 * @package s9e\TextFormatter
7710 * @copyright Copyright (c) 2010-2016 The s9e Authors
7711 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7712 */
7713 namespace s9e\TextFormatter\Configurator;
7714 use RuntimeException;
7715 use s9e\TextFormatter\Configurator\Collections\HostnameList;
7716 use s9e\TextFormatter\Configurator\Collections\SchemeList;
7717 use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
7718 class UrlConfig implements ConfigProvider
7719 {
7720 protected $allowedSchemes;
7721 protected $disallowedHosts;
7722 protected $restrictedHosts;
7723 public function __construct()
7724 {
7725 $this->disallowedHosts = new HostnameList;
7726 $this->restrictedHosts = new HostnameList;
7727 $this->allowedSchemes = new SchemeList;
7728 $this->allowedSchemes[] = 'http';
7729 $this->allowedSchemes[] = 'https';
7730 }
7731 public function asConfig()
7732 {
7733 return ConfigHelper::toArray(\get_object_vars($this));
7734 }
7735 public function allowScheme($scheme)
7736 {
7737 if (\strtolower($scheme) === 'javascript')
7738 throw new RuntimeException('The JavaScript URL scheme cannot be allowed');
7739 $this->allowedSchemes[] = $scheme;
7740 }
7741 public function disallowHost($host, $matchSubdomains = \true)
7742 {
7743 $this->disallowedHosts[] = $host;
7744 if ($matchSubdomains && \substr($host, 0, 1) !== '*')
7745 $this->disallowedHosts[] = '*.' . $host;
7746 }
7747 public function disallowScheme($scheme)
7748 {
7749 $this->allowedSchemes->remove($scheme);
7750 }
7751 public function getAllowedSchemes()
7752 {
7753 return \iterator_to_array($this->allowedSchemes);
7754 }
7755 public function restrictHost($host, $matchSubdomains = \true)
7756 {
7757 $this->restrictedHosts[] = $host;
7758 if ($matchSubdomains && \substr($host, 0, 1) !== '*')
7759 $this->restrictedHosts[] = '*.' . $host;
7760 }
7761 }
7762
7763 /*
7764 * @package s9e\TextFormatter
7765 * @copyright Copyright (c) 2010-2016 The s9e Authors
7766 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7767 */
7768 namespace s9e\TextFormatter\Configurator\Collections;
7769 use InvalidArgumentException;
7770 use s9e\TextFormatter\Configurator\Helpers\RegexpParser;
7771 use s9e\TextFormatter\Configurator\Items\AttributePreprocessor;
7772 use s9e\TextFormatter\Configurator\Items\Regexp;
7773 use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor;
7774 use s9e\TextFormatter\Configurator\Validators\AttributeName;
7775 class AttributePreprocessorCollection extends Collection
7776 {
7777 public function add($attrName, $regexp)
7778 {
7779 $attrName = AttributeName::normalize($attrName);
7780 $k = \serialize(array($attrName, $regexp));
7781 $this->items[$k] = new AttributePreprocessor($regexp);
7782 return $this->items[$k];
7783 }
7784 public function key()
7785 {
7786 list($attrName) = \unserialize(\key($this->items));
7787 return $attrName;
7788 }
7789 public function merge($attributePreprocessors)
7790 {
7791 $error = \false;
7792 if ($attributePreprocessors instanceof AttributePreprocessorCollection)
7793 foreach ($attributePreprocessors as $attrName => $attributePreprocessor)
7794 $this->add($attrName, $attributePreprocessor->getRegexp());
7795 elseif (\is_array($attributePreprocessors))
7796 {
7797 foreach ($attributePreprocessors as $values)
7798 {
7799 if (!\is_array($values))
7800 {
7801 $error = \true;
7802 break;
7803 }
7804 list($attrName, $value) = $values;
7805 if ($value instanceof AttributePreprocessor)
7806 $value = $value->getRegexp();
7807 $this->add($attrName, $value);
7808 }
7809 }
7810 else
7811 $error = \true;
7812 if ($error)
7813 throw new InvalidArgumentException('merge() expects an instance of AttributePreprocessorCollection or a 2D array where each element is a [attribute name, regexp] pair');
7814 }
7815 public function asConfig()
7816 {
7817 $config = array();
7818 foreach ($this->items as $k => $ap)
7819 {
7820 list($attrName) = \unserialize($k);
7821 $config[] = array($attrName, $ap, $ap->getCaptureNames());
7822 }
7823 return $config;
7824 }
7825 }
7826
7827 /*
7828 * @package s9e\TextFormatter
7829 * @copyright Copyright (c) 2010-2016 The s9e Authors
7830 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7831 */
7832 namespace s9e\TextFormatter\Configurator\Collections;
7833 use ArrayAccess;
7834 use InvalidArgumentException;
7835 use RuntimeException;
7836 class NormalizedCollection extends Collection implements ArrayAccess
7837 {
7838 protected $onDuplicateAction = 'error';
7839 public function asConfig()
7840 {
7841 $config = parent::asConfig();
7842 \ksort($config);
7843 return $config;
7844 }
7845 public function onDuplicate($action = \null)
7846 {
7847 $old = $this->onDuplicateAction;
7848 if (\func_num_args() && $action !== 'error' && $action !== 'ignore' && $action !== 'replace')
7849 throw new InvalidArgumentException("Invalid onDuplicate action '" . $action . "'. Expected: 'error', 'ignore' or 'replace'");
7850 $this->onDuplicateAction = $action;
7851 return $old;
7852 }
7853 protected function getAlreadyExistsException($key)
7854 {
7855 return new RuntimeException("Item '" . $key . "' already exists");
7856 }
7857 protected function getNotExistException($key)
7858 {
7859 return new RuntimeException("Item '" . $key . "' does not exist");
7860 }
7861 public function normalizeKey($key)
7862 {
7863 return $key;
7864 }
7865 public function normalizeValue($value)
7866 {
7867 return $value;
7868 }
7869 public function add($key, $value = \null)
7870 {
7871 if ($this->exists($key))
7872 if ($this->onDuplicateAction === 'ignore')
7873 return $this->get($key);
7874 elseif ($this->onDuplicateAction === 'error')
7875 throw $this->getAlreadyExistsException($key);
7876 return $this->set($key, $value);
7877 }
7878 public function contains($value)
7879 {
7880 return \in_array($this->normalizeValue($value), $this->items);
7881 }
7882 public function delete($key)
7883 {
7884 $key = $this->normalizeKey($key);
7885 unset($this->items[$key]);
7886 }
7887 public function exists($key)
7888 {
7889 $key = $this->normalizeKey($key);
7890 return \array_key_exists($key, $this->items);
7891 }
7892 public function get($key)
7893 {
7894 if (!$this->exists($key))
7895 throw $this->getNotExistException($key);
7896 $key = $this->normalizeKey($key);
7897 return $this->items[$key];
7898 }
7899 public function indexOf($value)
7900 {
7901 return \array_search($this->normalizeValue($value), $this->items);
7902 }
7903 public function set($key, $value)
7904 {
7905 $key = $this->normalizeKey($key);
7906 $this->items[$key] = $this->normalizeValue($value);
7907 return $this->items[$key];
7908 }
7909 public function offsetExists($offset)
7910 {
7911 return $this->exists($offset);
7912 }
7913 public function offsetGet($offset)
7914 {
7915 return $this->get($offset);
7916 }
7917 public function offsetSet($offset, $value)
7918 {
7919 $this->set($offset, $value);
7920 }
7921 public function offsetUnset($offset)
7922 {
7923 $this->delete($offset);
7924 }
7925 }
7926
7927 /*
7928 * @package s9e\TextFormatter
7929 * @copyright Copyright (c) 2010-2016 The s9e Authors
7930 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7931 */
7932 namespace s9e\TextFormatter\Configurator\Collections;
7933 use ArrayAccess;
7934 use InvalidArgumentException;
7935 use RuntimeException;
7936 use s9e\TextFormatter\Configurator\ConfigProvider;
7937 use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
7938 use s9e\TextFormatter\Configurator\Validators\TagName;
7939 use s9e\TextFormatter\Parser;
7940 class Ruleset extends Collection implements ArrayAccess, ConfigProvider
7941 {
7942 public function __construct()
7943 {
7944 $this->clear();
7945 }
7946 public function clear()
7947 {
7948 parent::clear();
7949 $this->defaultChildRule('allow');
7950 $this->defaultDescendantRule('allow');
7951 }
7952 public function offsetExists($k)
7953 {
7954 return isset($this->items[$k]);
7955 }
7956 public function offsetGet($k)
7957 {
7958 return $this->items[$k];
7959 }
7960 public function offsetSet($k, $v)
7961 {
7962 throw new RuntimeException('Not supported');
7963 }
7964 public function offsetUnset($k)
7965 {
7966 return $this->remove($k);
7967 }
7968 public function asConfig()
7969 {
7970 $config = $this->items;
7971 unset($config['allowChild']);
7972 unset($config['allowDescendant']);
7973 unset($config['defaultChildRule']);
7974 unset($config['defaultDescendantRule']);
7975 unset($config['denyChild']);
7976 unset($config['denyDescendant']);
7977 unset($config['requireParent']);
7978 $bitValues = array(
7979 'autoClose' => Parser::RULE_AUTO_CLOSE,
7980 'autoReopen' => Parser::RULE_AUTO_REOPEN,
7981 'breakParagraph' => Parser::RULE_BREAK_PARAGRAPH,
7982 'createParagraphs' => Parser::RULE_CREATE_PARAGRAPHS,
7983 'disableAutoLineBreaks' => Parser::RULE_DISABLE_AUTO_BR,
7984 'enableAutoLineBreaks' => Parser::RULE_ENABLE_AUTO_BR,
7985 'ignoreSurroundingWhitespace' => Parser::RULE_IGNORE_WHITESPACE,
7986 'ignoreTags' => Parser::RULE_IGNORE_TAGS,
7987 'ignoreText' => Parser::RULE_IGNORE_TEXT,
7988 'isTransparent' => Parser::RULE_IS_TRANSPARENT,
7989 'preventLineBreaks' => Parser::RULE_PREVENT_BR,
7990 'suspendAutoLineBreaks' => Parser::RULE_SUSPEND_AUTO_BR,
7991 'trimFirstLine' => Parser::RULE_TRIM_FIRST_LINE
7992 );
7993 $bitfield = 0;
7994 foreach ($bitValues as $ruleName => $bitValue)
7995 {
7996 if (!empty($config[$ruleName]))
7997 $bitfield |= $bitValue;
7998 unset($config[$ruleName]);
7999 }
8000 foreach (array('closeAncestor', 'closeParent', 'fosterParent') as $ruleName)
8001 if (isset($config[$ruleName]))
8002 {
8003 $targets = \array_fill_keys($config[$ruleName], 1);
8004 $config[$ruleName] = new Dictionary($targets);
8005 }
8006 $config['flags'] = $bitfield;
8007 return $config;
8008 }
8009 public function merge($rules, $overwrite = \true)
8010 {
8011 if (!\is_array($rules)
8012 && !($rules instanceof self))
8013 throw new InvalidArgumentException('merge() expects an array or an instance of Ruleset');
8014 foreach ($rules as $action => $value)
8015 if (\is_array($value))
8016 foreach ($value as $tagName)
8017 $this->$action($tagName);
8018 elseif ($overwrite || !isset($this->items[$action]))
8019 $this->$action($value);
8020 }
8021 public function remove($type, $tagName = \null)
8022 {
8023 if (\preg_match('(^default(?:Child|Descendant)Rule)', $type))
8024 throw new RuntimeException('Cannot remove ' . $type);
8025 if (isset($tagName))
8026 {
8027 $tagName = TagName::normalize($tagName);
8028 if (isset($this->items[$type]))
8029 {
8030 $this->items[$type] = \array_diff(
8031 $this->items[$type],
8032 array($tagName)
8033 );
8034 if (empty($this->items[$type]))
8035 unset($this->items[$type]);
8036 else
8037 $this->items[$type] = \array_values($this->items[$type]);
8038 }
8039 }
8040 else
8041 unset($this->items[$type]);
8042 }
8043 protected function addBooleanRule($ruleName, $bool)
8044 {
8045 if (!\is_bool($bool))
8046 throw new InvalidArgumentException($ruleName . '() expects a boolean');
8047 $this->items[$ruleName] = $bool;
8048 return $this;
8049 }
8050 protected function addTargetedRule($ruleName, $tagName)
8051 {
8052 $this->items[$ruleName][] = TagName::normalize($tagName);
8053 return $this;
8054 }
8055 public function allowChild($tagName)
8056 {
8057 return $this->addTargetedRule('allowChild', $tagName);
8058 }
8059 public function allowDescendant($tagName)
8060 {
8061 return $this->addTargetedRule('allowDescendant', $tagName);
8062 }
8063 public function autoClose($bool = \true)
8064 {
8065 return $this->addBooleanRule('autoClose', $bool);
8066 }
8067 public function autoReopen($bool = \true)
8068 {
8069 return $this->addBooleanRule('autoReopen', $bool);
8070 }
8071 public function breakParagraph($bool = \true)
8072 {
8073 return $this->addBooleanRule('breakParagraph', $bool);
8074 }
8075 public function closeAncestor($tagName)
8076 {
8077 return $this->addTargetedRule('closeAncestor', $tagName);
8078 }
8079 public function closeParent($tagName)
8080 {
8081 return $this->addTargetedRule('closeParent', $tagName);
8082 }
8083 public function createChild($tagName)
8084 {
8085 return $this->addTargetedRule('createChild', $tagName);
8086 }
8087 public function createParagraphs($bool = \true)
8088 {
8089 return $this->addBooleanRule('createParagraphs', $bool);
8090 }
8091 public function defaultChildRule($rule)
8092 {
8093 if ($rule !== 'allow' && $rule !== 'deny')
8094 throw new InvalidArgumentException("defaultChildRule() only accepts 'allow' or 'deny'");
8095 $this->items['defaultChildRule'] = $rule;
8096 return $this;
8097 }
8098 public function defaultDescendantRule($rule)
8099 {
8100 if ($rule !== 'allow' && $rule !== 'deny')
8101 throw new InvalidArgumentException("defaultDescendantRule() only accepts 'allow' or 'deny'");
8102 $this->items['defaultDescendantRule'] = $rule;
8103 return $this;
8104 }
8105 public function denyChild($tagName)
8106 {
8107 return $this->addTargetedRule('denyChild', $tagName);
8108 }
8109 public function denyDescendant($tagName)
8110 {
8111 return $this->addTargetedRule('denyDescendant', $tagName);
8112 }
8113 public function disableAutoLineBreaks($bool = \true)
8114 {
8115 return $this->addBooleanRule('disableAutoLineBreaks', $bool);
8116 }
8117 public function enableAutoLineBreaks($bool = \true)
8118 {
8119 return $this->addBooleanRule('enableAutoLineBreaks', $bool);
8120 }
8121 public function fosterParent($tagName)
8122 {
8123 return $this->addTargetedRule('fosterParent', $tagName);
8124 }
8125 public function ignoreSurroundingWhitespace($bool = \true)
8126 {
8127 return $this->addBooleanRule('ignoreSurroundingWhitespace', $bool);
8128 }
8129 public function ignoreTags($bool = \true)
8130 {
8131 return $this->addBooleanRule('ignoreTags', $bool);
8132 }
8133 public function ignoreText($bool = \true)
8134 {
8135 return $this->addBooleanRule('ignoreText', $bool);
8136 }
8137 public function isTransparent($bool = \true)
8138 {
8139 return $this->addBooleanRule('isTransparent', $bool);
8140 }
8141 public function preventLineBreaks($bool = \true)
8142 {
8143 return $this->addBooleanRule('preventLineBreaks', $bool);
8144 }
8145 public function requireParent($tagName)
8146 {
8147 return $this->addTargetedRule('requireParent', $tagName);
8148 }
8149 public function requireAncestor($tagName)
8150 {
8151 return $this->addTargetedRule('requireAncestor', $tagName);
8152 }
8153 public function suspendAutoLineBreaks($bool = \true)
8154 {
8155 return $this->addBooleanRule('suspendAutoLineBreaks', $bool);
8156 }
8157 public function trimFirstLine($bool = \true)
8158 {
8159 return $this->addBooleanRule('trimFirstLine', $bool);
8160 }
8161 }
8162
8163 /*
8164 * @package s9e\TextFormatter
8165 * @copyright Copyright (c) 2010-2016 The s9e Authors
8166 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8167 */
8168 namespace s9e\TextFormatter\Configurator\Items;
8169 abstract class Filter extends ProgrammableCallback
8170 {
8171 }
8172
8173 /*
8174 * @package s9e\TextFormatter
8175 * @copyright Copyright (c) 2010-2016 The s9e Authors
8176 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8177 */
8178 namespace s9e\TextFormatter\Configurator\TemplateChecks;
8179 use DOMElement;
8180 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
8181 use s9e\TextFormatter\Configurator\Helpers\XPathHelper;
8182 use s9e\TextFormatter\Configurator\Items\Attribute;
8183 class DisallowUnsafeDynamicCSS extends AbstractDynamicContentCheck
8184 {
8185 protected function getNodes(DOMElement $template)
8186 {
8187 return TemplateHelper::getCSSNodes($template->ownerDocument);
8188 }
8189 protected function isExpressionSafe($expr)
8190 {
8191 return XPathHelper::isExpressionNumeric($expr);
8192 }
8193 protected function isSafe(Attribute $attribute)
8194 {
8195 return $attribute->isSafeInCSS();
8196 }
8197 }
8198
8199 /*
8200 * @package s9e\TextFormatter
8201 * @copyright Copyright (c) 2010-2016 The s9e Authors
8202 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8203 */
8204 namespace s9e\TextFormatter\Configurator\TemplateChecks;
8205 use DOMElement;
8206 use s9e\TextFormatter\Configurator\Helpers\XPathHelper;
8207 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
8208 use s9e\TextFormatter\Configurator\Items\Attribute;
8209 class DisallowUnsafeDynamicJS extends AbstractDynamicContentCheck
8210 {
8211 protected function getNodes(DOMElement $template)
8212 {
8213 return TemplateHelper::getJSNodes($template->ownerDocument);
8214 }
8215 protected function isExpressionSafe($expr)
8216 {
8217 return XPathHelper::isExpressionNumeric($expr);
8218 }
8219 protected function isSafe(Attribute $attribute)
8220 {
8221 return $attribute->isSafeInJS();
8222 }
8223 }
8224
8225 /*
8226 * @package s9e\TextFormatter
8227 * @copyright Copyright (c) 2010-2016 The s9e Authors
8228 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8229 */
8230 namespace s9e\TextFormatter\Configurator\TemplateChecks;
8231 use DOMAttr;
8232 use DOMElement;
8233 use DOMText;
8234 use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
8235 use s9e\TextFormatter\Configurator\Items\Attribute;
8236 use s9e\TextFormatter\Configurator\Items\Tag;
8237 class DisallowUnsafeDynamicURL extends AbstractDynamicContentCheck
8238 {
8239 protected $exceptionRegexp = '(^(?:(?!data|\\w*script)\\w+:|[^:]*/|#))i';
8240 protected function getNodes(DOMElement $template)
8241 {
8242 return TemplateHelper::getURLNodes($template->ownerDocument);
8243 }
8244 protected function isSafe(Attribute $attribute)
8245 {
8246 return $attribute->isSafeAsURL();
8247 }
8248 protected function checkAttributeNode(DOMAttr $attribute, Tag $tag)
8249 {
8250 if (\preg_match($this->exceptionRegexp, $attribute->value))
8251 return;
8252 parent::checkAttributeNode($attribute, $tag);
8253 }
8254 protected function checkElementNode(DOMElement $element, Tag $tag)
8255 {
8256 if ($element->firstChild
8257 && $element->firstChild instanceof DOMText
8258 && \preg_match($this->exceptionRegexp, $element->firstChild->textContent))
8259 return;
8260 parent::checkElementNode($element, $tag);
8261 }
8262 }
8263
8264 /*
8265 * @package s9e\TextFormatter
8266 * @copyright Copyright (c) 2010-2016 The s9e Authors
8267 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8268 */
8269 namespace s9e\TextFormatter\Configurator\TemplateChecks;
8270 class RestrictFlashScriptAccess extends AbstractFlashRestriction
8271 {
8272 public $defaultSetting = 'sameDomain';
8273 protected $settingName = 'allowScriptAccess';
8274 protected $settings = array(
8275 'always' => 3,
8276 'samedomain' => 2,
8277 'never' => 1
8278 );
8279 }
8280
8281 /*
8282 * @package s9e\TextFormatter
8283 * @copyright Copyright (c) 2010-2016 The s9e Authors
8284 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8285 */
8286 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
8287 use DOMDocument;
8288 use DOMXPath;
8289 class FoldArithmeticConstants extends AbstractConstantFolding
8290 {
8291 protected $xpath;
8292 public function __construct()
8293 {
8294 $this->xpath = new DOMXPath(new DOMDocument);
8295 }
8296 protected function getOptimizationPasses()
8297 {
8298 return array(
8299 '(^[-+0-9\\s]+$)' => 'foldOperation',
8300 '( \\+ 0(?! [^+\\)])|(?<![-\\w])0 \\+ )' => 'foldAdditiveIdentity',
8301 '(^((?>\\d+ [-+] )*)(\\d+) div (\\d+))' => 'foldDivision',
8302 '(^((?>\\d+ [-+] )*)(\\d+) \\* (\\d+))' => 'foldMultiplication',
8303 '(\\( \\d+ (?>(?>[-+*]|div) \\d+ )+\\))' => 'foldSubExpression',
8304 '((?<=[-+*\\(]|\\bdiv|^) \\( ([@$][-\\w]+|\\d+(?>\\.\\d+)?) \\) (?=[-+*\\)]|div|$))' => 'removeParentheses'
8305 );
8306 }
8307 public function evaluateExpression($expr)
8308 {
8309 $expr = \preg_replace_callback(
8310 '(([\'"])(.*?)\\1)s',
8311 function ($m)
8312 {
8313 return $m[1] . \bin2hex($m[2]) . $m[1];
8314 },
8315 $expr
8316 );
8317 $expr = parent::evaluateExpression($expr);
8318 $expr = \preg_replace_callback(
8319 '(([\'"])(.*?)\\1)s',
8320 function ($m)
8321 {
8322 return $m[1] . \pack('H*', $m[2]) . $m[1];
8323 },
8324 $expr
8325 );
8326 return $expr;
8327 }
8328 protected function foldAdditiveIdentity(array $m)
8329 {
8330 return '';
8331 }
8332 protected function foldDivision(array $m)
8333 {
8334 return $m[1] . ($m[2] / $m[3]);
8335 }
8336 protected function foldMultiplication(array $m)
8337 {
8338 return $m[1] . ($m[2] * $m[3]);
8339 }
8340 protected function foldOperation(array $m)
8341 {
8342 return (string) $this->xpath->evaluate($m[0]);
8343 }
8344 protected function foldSubExpression(array $m)
8345 {
8346 return '(' . $this->evaluateExpression(\trim(\substr($m[0], 1, -1))) . ')';
8347 }
8348 protected function removeParentheses(array $m)
8349 {
8350 return ' ' . $m[1] . ' ';
8351 }
8352 }
8353
8354 /*
8355 * @package s9e\TextFormatter
8356 * @copyright Copyright (c) 2010-2016 The s9e Authors
8357 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8358 */
8359 namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
8360 use DOMDocument;
8361 use DOMXPath;
8362 use s9e\TextFormatter\Configurator\Helpers\XPathHelper;
8363 class FoldConstantXPathExpressions extends AbstractConstantFolding
8364 {
8365 protected $supportedFunctions = array(
8366 'ceiling',
8367 'concat',
8368 'contains',
8369 'floor',
8370 'normalize-space',
8371 'number',
8372 'round',
8373 'starts-with',
8374 'string',
8375 'string-length',
8376 'substring',
8377 'substring-after',
8378 'substring-before',
8379 'sum',
8380 'translate'
8381 );
8382 protected $xpath;
8383 public function __construct()
8384 {
8385 $this->xpath = new DOMXPath(new DOMDocument);
8386 }
8387 protected function getOptimizationPasses()
8388 {
8389 return array(
8390 '(^(?:"[^"]*"|\'[^\']*\'|\\.[0-9]|[^"$&\'./:<=>@[\\]])++$)' => 'foldConstantXPathExpression'
8391 );
8392 }
8393 protected function canBeSerialized($value)
8394 {
8395 return (\is_string($value) || \is_integer($value) || \is_float($value));
8396 }
8397 protected function evaluate($expr)
8398 {
8399 $useErrors = \libxml_use_internal_errors(\true);
8400 $result = $this->xpath->evaluate($expr);
8401 \libxml_use_internal_errors($useErrors);
8402 return $result;
8403 }
8404 protected function foldConstantXPathExpression(array $m)
8405 {
8406 $expr = $m[0];
8407 if ($this->isConstantExpression($expr))
8408 {
8409 $result = $this->evaluate($expr);
8410 if ($this->canBeSerialized($result))
8411 {
8412 $foldedExpr = XPathHelper::export($result);
8413 if (\strlen($foldedExpr) < \strlen($expr))
8414 $expr = $foldedExpr;
8415 }
8416 }
8417 return $expr;
8418 }
8419 protected function isConstantExpression($expr)
8420 {
8421 $expr = \preg_replace('("[^"]*"|\'[^\']*\')', '', $expr);
8422 \preg_match_all('(\\w[-\\w]+(?=\\())', $expr, $m);
8423 if (\count(\array_diff($m[0], $this->supportedFunctions)) > 0)
8424 return \false;
8425 return !\preg_match('([^\\s\\-0-9a-z\\(-.]|\\.(?![0-9])|\\b[-a-z](?![-\\w]+\\())i', $expr);
8426 }
8427 }
8428
8429 /*
8430 * @package s9e\TextFormatter
8431 * @copyright Copyright (c) 2010-2016 The s9e Authors
8432 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8433 */
8434 namespace s9e\TextFormatter\Configurator\Collections;
8435 use RuntimeException;
8436 use s9e\TextFormatter\Configurator\Items\Attribute;
8437 use s9e\TextFormatter\Configurator\Validators\AttributeName;
8438 class AttributeCollection extends NormalizedCollection
8439 {
8440 protected $onDuplicateAction = 'replace';
8441 protected function getAlreadyExistsException($key)
8442 {
8443 return new RuntimeException("Attribute '" . $key . "' already exists");
8444 }
8445 protected function getNotExistException($key)
8446 {
8447 return new RuntimeException("Attribute '" . $key . "' does not exist");
8448 }
8449 public function normalizeKey($key)
8450 {
8451 return AttributeName::normalize($key);
8452 }
8453 public function normalizeValue($value)
8454 {
8455 return ($value instanceof Attribute)
8456 ? $value
8457 : new Attribute($value);
8458 }
8459 }
8460
8461 /*
8462 * @package s9e\TextFormatter
8463 * @copyright Copyright (c) 2010-2016 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 InvalidArgumentException;
8468 use s9e\TextFormatter\Configurator\Items\AttributeFilter;
8469 class AttributeFilterCollection extends NormalizedCollection
8470 {
8471 public function get($key)
8472 {
8473 $key = $this->normalizeKey($key);
8474 if (!$this->exists($key))
8475 if ($key[0] === '#')
8476 $this->set($key, self::getDefaultFilter(\substr($key, 1)));
8477 else
8478 $this->set($key, new AttributeFilter($key));
8479 $filter = parent::get($key);
8480 $filter = clone $filter;
8481 return $filter;
8482 }
8483 public static function getDefaultFilter($filterName)
8484 {
8485 $filterName = \ucfirst(\strtolower($filterName));
8486 $className = 's9e\\TextFormatter\\Configurator\\Items\\AttributeFilters\\' . $filterName . 'Filter';
8487 if (!\class_exists($className))
8488 throw new InvalidArgumentException("Unknown attribute filter '" . $filterName . "'");
8489 return new $className;
8490 }
8491 public function normalizeKey($key)
8492 {
8493 if (\preg_match('/^#[a-z_0-9]+$/Di', $key))
8494 return \strtolower($key);
8495 if (\is_string($key) && \is_callable($key))
8496 return $key;
8497 throw new InvalidArgumentException("Invalid filter name '" . $key . "'");
8498 }
8499 public function normalizeValue($value)
8500 {
8501 if ($value instanceof AttributeFilter)
8502 return $value;
8503 if (\is_callable($value))
8504 return new AttributeFilter($value);
8505 throw new InvalidArgumentException('Argument 1 passed to ' . __METHOD__ . ' must be a valid callback or an instance of s9e\\TextFormatter\\Configurator\\Items\\AttributeFilter');
8506 }
8507 }
8508
8509 /*
8510 * @package s9e\TextFormatter
8511 * @copyright Copyright (c) 2010-2016 The s9e Authors
8512 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8513 */
8514 namespace s9e\TextFormatter\Configurator\Collections;
8515 use InvalidArgumentException;
8516 class NormalizedList extends NormalizedCollection
8517 {
8518 public function add($value, $void = \null)
8519 {
8520 return $this->append($value);
8521 }
8522 public function append($value)
8523 {
8524 $value = $this->normalizeValue($value);
8525 $this->items[] = $value;
8526 return $value;
8527 }
8528 public function delete($key)
8529 {
8530 parent::delete($key);
8531 $this->items = \array_values($this->items);
8532 }
8533 public function insert($offset, $value)
8534 {
8535 $offset = $this->normalizeKey($offset);
8536 $value = $this->normalizeValue($value);
8537 \array_splice($this->items, $offset, 0, array($value));
8538 return $value;
8539 }
8540 public function normalizeKey($key)
8541 {
8542 $normalizedKey = \filter_var(
8543 (\preg_match('(^-\\d+$)D', $key)) ? \count($this->items) + $key : $key,
8544 \FILTER_VALIDATE_INT,
8545 array(
8546 'options' => array(
8547 'min_range' => 0,
8548 'max_range' => \count($this->items)
8549 )
8550 )
8551 );
8552 if ($normalizedKey === \false)
8553 throw new InvalidArgumentException("Invalid offset '" . $key . "'");
8554 return $normalizedKey;
8555 }
8556 public function offsetSet($offset, $value)
8557 {
8558 if ($offset === \null)
8559 $this->append($value);
8560 else
8561 parent::offsetSet($offset, $value);
8562 }
8563 public function prepend($value)
8564 {
8565 $value = $this->normalizeValue($value);
8566 \array_unshift($this->items, $value);
8567 return $value;
8568 }
8569 public function remove($value)
8570 {
8571 $keys = \array_keys($this->items, $this->normalizeValue($value));
8572 foreach ($keys as $k)
8573 unset($this->items[$k]);
8574 $this->items = \array_values($this->items);
8575 return \count($keys);
8576 }
8577 }
8578
8579 /*
8580 * @package s9e\TextFormatter
8581 * @copyright Copyright (c) 2010-2016 The s9e Authors
8582 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8583 */
8584 namespace s9e\TextFormatter\Configurator\Collections;
8585 use InvalidArgumentException;
8586 use RuntimeException;
8587 use s9e\TextFormatter\Configurator;
8588 use s9e\TextFormatter\Plugins\ConfiguratorBase;
8589 class PluginCollection extends NormalizedCollection
8590 {
8591 protected $configurator;
8592 public function __construct(Configurator $configurator)
8593 {
8594 $this->configurator = $configurator;
8595 }
8596 public function finalize()
8597 {
8598 foreach ($this->items as $plugin)
8599 $plugin->finalize();
8600 }
8601 public function normalizeKey($pluginName)
8602 {
8603 if (!\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $pluginName))
8604 throw new InvalidArgumentException("Invalid plugin name '" . $pluginName . "'");
8605 return $pluginName;
8606 }
8607 public function normalizeValue($value)
8608 {
8609 if (\is_string($value) && \class_exists($value))
8610 $value = new $value($this->configurator);
8611 if ($value instanceof ConfiguratorBase)
8612 return $value;
8613 throw new InvalidArgumentException('PluginCollection::normalizeValue() expects a class name or an object that implements s9e\\TextFormatter\\Plugins\\ConfiguratorBase');
8614 }
8615 public function load($pluginName, array $overrideProps = array())
8616 {
8617 $pluginName = $this->normalizeKey($pluginName);
8618 $className = 's9e\\TextFormatter\\Plugins\\' . $pluginName . '\\Configurator';
8619 if (!\class_exists($className))
8620 throw new RuntimeException("Class '" . $className . "' does not exist");
8621 $plugin = new $className($this->configurator, $overrideProps);
8622 $this->set($pluginName, $plugin);
8623 return $plugin;
8624 }
8625 public function asConfig()
8626 {
8627 $plugins = parent::asConfig();
8628 foreach ($plugins as $pluginName => &$pluginConfig)
8629 {
8630 $plugin = $this->get($pluginName);
8631 $pluginConfig += $plugin->getBaseProperties();
8632 if ($pluginConfig['quickMatch'] === \false)
8633 unset($pluginConfig['quickMatch']);
8634 if (!isset($pluginConfig['regexp']))
8635 unset($pluginConfig['regexpLimit']);
8636 $className = 's9e\\TextFormatter\\Plugins\\' . $pluginName . '\\Parser';
8637 if ($pluginConfig['className'] === $className)
8638 unset($pluginConfig['className']);
8639 }
8640 unset($pluginConfig);
8641 return $plugins;
8642 }
8643 }
8644
8645 /*
8646 * @package s9e\TextFormatter
8647 * @copyright Copyright (c) 2010-2016 The s9e Authors
8648 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8649 */
8650 namespace s9e\TextFormatter\Configurator\Collections;
8651 use RuntimeException;
8652 use s9e\TextFormatter\Configurator\Items\Tag;
8653 use s9e\TextFormatter\Configurator\Validators\TagName;
8654 class TagCollection extends NormalizedCollection
8655 {
8656 protected $onDuplicateAction = 'replace';
8657 protected function getAlreadyExistsException($key)
8658 {
8659 return new RuntimeException("Tag '" . $key . "' already exists");
8660 }
8661 protected function getNotExistException($key)
8662 {
8663 return new RuntimeException("Tag '" . $key . "' does not exist");
8664 }
8665 public function normalizeKey($key)
8666 {
8667 return TagName::normalize($key);
8668 }
8669 public function normalizeValue($value)
8670 {
8671 return ($value instanceof Tag)
8672 ? $value
8673 : new Tag($value);
8674 }
8675 }
8676
8677 /*
8678 * @package s9e\TextFormatter
8679 * @copyright Copyright (c) 2010-2016 The s9e Authors
8680 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8681 */
8682 namespace s9e\TextFormatter\Configurator\Collections;
8683 use s9e\TextFormatter\Configurator\Validators\TemplateParameterName;
8684 class TemplateParameterCollection extends NormalizedCollection
8685 {
8686 public function normalizeKey($key)
8687 {
8688 return TemplateParameterName::normalize($key);
8689 }
8690 public function normalizeValue($value)
8691 {
8692 return (string) $value;
8693 }
8694 }
8695
8696 /*
8697 * @package s9e\TextFormatter
8698 * @copyright Copyright (c) 2010-2016 The s9e Authors
8699 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8700 */
8701 namespace s9e\TextFormatter\Configurator\Items;
8702 use s9e\TextFormatter\Configurator\Traits\TemplateSafeness;
8703 class AttributeFilter extends Filter
8704 {
8705 protected $markedSafe = array();
8706 protected function isSafe($context)
8707 {
8708 return !empty($this->markedSafe[$context]);
8709 }
8710 public function isSafeAsURL()
8711 {
8712 return $this->isSafe('AsURL');
8713 }
8714 public function isSafeInCSS()
8715 {
8716 return $this->isSafe('InCSS');
8717 }
8718
8719 public function markAsSafeAsURL()
8720 {
8721 $this->markedSafe['AsURL'] = \true;
8722 return $this;
8723 }
8724 public function markAsSafeInCSS()
8725 {
8726 $this->markedSafe['InCSS'] = \true;
8727 return $this;
8728 }
8729 public function markAsSafeInJS()
8730 {
8731 $this->markedSafe['InJS'] = \true;
8732 return $this;
8733 }
8734 public function resetSafeness()
8735 {
8736 $this->markedSafe = array();
8737 return $this;
8738 }
8739 public function __construct($callback)
8740 {
8741 parent::__construct($callback);
8742 $this->resetParameters();
8743 $this->addParameterByName('attrValue');
8744 }
8745 public function isSafeInJS()
8746 {
8747 $safeCallbacks = array(
8748 'urlencode',
8749 'strtotime',
8750 'rawurlencode'
8751 );
8752 if (\in_array($this->callback, $safeCallbacks, \true))
8753 return \true;
8754 return $this->isSafe('InJS');
8755 }
8756 }
8757
8758 /*
8759 * @package s9e\TextFormatter
8760 * @copyright Copyright (c) 2010-2016 The s9e Authors
8761 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8762 */
8763 namespace s9e\TextFormatter\Configurator\Items;
8764 class TagFilter extends Filter
8765 {
8766 public function __construct($callback)
8767 {
8768 parent::__construct($callback);
8769 $this->resetParameters();
8770 $this->addParameterByName('tag');
8771 }
8772 }
8773
8774 /*
8775 * @package s9e\TextFormatter
8776 * @copyright Copyright (c) 2010-2016 The s9e Authors
8777 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8778 */
8779 namespace s9e\TextFormatter\Configurator\Collections;
8780 use InvalidArgumentException;
8781 use s9e\TextFormatter\Configurator\Items\ProgrammableCallback;
8782 abstract class FilterChain extends NormalizedList
8783 {
8784 abstract protected function getFilterClassName();
8785 public function containsCallback($callback)
8786 {
8787 $pc = new ProgrammableCallback($callback);
8788 $callback = $pc->getCallback();
8789 foreach ($this->items as $filter)
8790 if ($callback === $filter->getCallback())
8791 return \true;
8792 return \false;
8793 }
8794 public function normalizeValue($value)
8795 {
8796 $className = $this->getFilterClassName();
8797 if ($value instanceof $className)
8798 return $value;
8799 if (!\is_callable($value))
8800 throw new InvalidArgumentException('Filter ' . \var_export($value, \true) . ' is neither callable nor an instance of ' . $className);
8801 return new $className($value);
8802 }
8803 }
8804
8805 /*
8806 * @package s9e\TextFormatter
8807 * @copyright Copyright (c) 2010-2016 The s9e Authors
8808 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8809 */
8810 namespace s9e\TextFormatter\Configurator\Collections;
8811 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
8812 use s9e\TextFormatter\Configurator\Items\Regexp;
8813 class HostnameList extends NormalizedList
8814 {
8815 public function asConfig()
8816 {
8817 if (empty($this->items))
8818 return \null;
8819 return new Regexp($this->getRegexp());
8820 }
8821 public function getRegexp()
8822 {
8823 $hosts = array();
8824 foreach ($this->items as $host)
8825 $hosts[] = $this->normalizeHostmask($host);
8826 $regexp = RegexpBuilder::fromList(
8827 $hosts,
8828 array(
8829 'specialChars' => array(
8830 '*' => '.*',
8831 '^' => '^',
8832 '$' => '$'
8833 )
8834 )
8835 );
8836 return '/' . $regexp . '/DSis';
8837 }
8838 protected function normalizeHostmask($host)
8839 {
8840 if (\preg_match('#[\\x80-\xff]#', $host) && \function_exists('idn_to_ascii'))
8841 $host = \idn_to_ascii($host);
8842 if (\substr($host, 0, 1) === '*')
8843 $host = \ltrim($host, '*');
8844 else
8845 $host = '^' . $host;
8846 if (\substr($host, -1) === '*')
8847 $host = \rtrim($host, '*');
8848 else
8849 $host .= '$';
8850 return $host;
8851 }
8852 }
8853
8854 /*
8855 * @package s9e\TextFormatter
8856 * @copyright Copyright (c) 2010-2016 The s9e Authors
8857 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8858 */
8859 namespace s9e\TextFormatter\Configurator\Collections;
8860 use InvalidArgumentException;
8861 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
8862 use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator;
8863 class RulesGeneratorList extends NormalizedList
8864 {
8865 public function normalizeValue($generator)
8866 {
8867 if (\is_string($generator))
8868 {
8869 $className = 's9e\\TextFormatter\\Configurator\\RulesGenerators\\' . $generator;
8870 if (\class_exists($className))
8871 $generator = new $className;
8872 }
8873 if (!($generator instanceof BooleanRulesGenerator)
8874 && !($generator instanceof TargetedRulesGenerator))
8875 throw new InvalidArgumentException('Invalid rules generator ' . \var_export($generator, \true));
8876 return $generator;
8877 }
8878 }
8879
8880 /*
8881 * @package s9e\TextFormatter
8882 * @copyright Copyright (c) 2010-2016 The s9e Authors
8883 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8884 */
8885 namespace s9e\TextFormatter\Configurator\Collections;
8886 use InvalidArgumentException;
8887 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
8888 use s9e\TextFormatter\Configurator\Items\Regexp;
8889 class SchemeList extends NormalizedList
8890 {
8891 public function asConfig()
8892 {
8893 return new Regexp('/^' . RegexpBuilder::fromList($this->items) . '$/Di');
8894 }
8895 public function normalizeValue($scheme)
8896 {
8897 if (!\preg_match('#^[a-z][a-z0-9+\\-.]*$#Di', $scheme))
8898 throw new InvalidArgumentException("Invalid scheme name '" . $scheme . "'");
8899 return \strtolower($scheme);
8900 }
8901 }
8902
8903 /*
8904 * @package s9e\TextFormatter
8905 * @copyright Copyright (c) 2010-2016 The s9e Authors
8906 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8907 */
8908 namespace s9e\TextFormatter\Configurator\Collections;
8909 use s9e\TextFormatter\Configurator\TemplateCheck;
8910 class TemplateCheckList extends NormalizedList
8911 {
8912 public function normalizeValue($check)
8913 {
8914 if (!($check instanceof TemplateCheck))
8915 {
8916 $className = 's9e\\TextFormatter\\Configurator\\TemplateChecks\\' . $check;
8917 $check = new $className;
8918 }
8919 return $check;
8920 }
8921 }
8922
8923 /*
8924 * @package s9e\TextFormatter
8925 * @copyright Copyright (c) 2010-2016 The s9e Authors
8926 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8927 */
8928 namespace s9e\TextFormatter\Configurator\Collections;
8929 use s9e\TextFormatter\Configurator\TemplateNormalization;
8930 use s9e\TextFormatter\Configurator\TemplateNormalizations\Custom;
8931 class TemplateNormalizationList extends NormalizedList
8932 {
8933 public function normalizeValue($value)
8934 {
8935 if ($value instanceof TemplateNormalization)
8936 return $value;
8937 if (\is_callable($value))
8938 return new Custom($value);
8939 $className = 's9e\\TextFormatter\\Configurator\\TemplateNormalizations\\' . $value;
8940 return new $className;
8941 }
8942 }
8943
8944 /*
8945 * @package s9e\TextFormatter
8946 * @copyright Copyright (c) 2010-2016 The s9e Authors
8947 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8948 */
8949 namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
8950 use s9e\TextFormatter\Configurator\Items\AttributeFilter;
8951 class UrlFilter extends AttributeFilter
8952 {
8953 public function __construct()
8954 {
8955 parent::__construct('s9e\\TextFormatter\\Parser\\BuiltInFilters::filterUrl');
8956 $this->resetParameters();
8957 $this->addParameterByName('attrValue');
8958 $this->addParameterByName('urlConfig');
8959 $this->addParameterByName('logger');
8960 $this->setJS('BuiltInFilters.filterUrl');
8961 }
8962 public function isSafeInCSS()
8963 {
8964 return \true;
8965 }
8966 public function isSafeInJS()
8967 {
8968 return \true;
8969 }
8970 public function isSafeAsURL()
8971 {
8972 return \true;
8973 }
8974 }
8975
8976 /*
8977 * @package s9e\TextFormatter
8978 * @copyright Copyright (c) 2010-2016 The s9e Authors
8979 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
8980 */
8981 namespace s9e\TextFormatter\Configurator\Collections;
8982 class AttributeFilterChain extends FilterChain
8983 {
8984 public function getFilterClassName()
8985 {
8986 return 's9e\\TextFormatter\\Configurator\\Items\\AttributeFilter';
8987 }
8988 public function normalizeValue($value)
8989 {
8990 if (\is_string($value) && \preg_match('(^#\\w+$)', $value))
8991 $value = AttributeFilterCollection::getDefaultFilter(\substr($value, 1));
8992 return parent::normalizeValue($value);
8993 }
8994 }
8995
8996 /*
8997 * @package s9e\TextFormatter
8998 * @copyright Copyright (c) 2010-2016 The s9e Authors
8999 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
9000 */
9001 namespace s9e\TextFormatter\Configurator\Collections;
9002 class TagFilterChain extends FilterChain
9003 {
9004 public function getFilterClassName()
9005 {
9006 return 's9e\\TextFormatter\\Configurator\\Items\\TagFilter';
9007 }
9008 }