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.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

Configurator.php

Zuletzt modifiziert: 09.10.2024, 12:56 - Dateigröße: 271.44 KiB


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 . '=&quot;&quot;', '', $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]+;))', '&amp;', $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('&quot;','\"',\$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)|(?&param))',
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)|(?&not)|(?&value)|(?&parens)) (?<bool1>and|or) (?<bool2>(?&bool)|(?&cmp)|(?&not)|(?&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  }